Something went wrong. Try again.
I'm going to build my own OpenClaw, with blackjack... and bun!
Something went wrong. Try again.
21 kB · 489 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490import fs from "node:fs";import path from "node:path";import { Type } from "@sinclair/typebox";import type { AgentToolResult, ExtensionAPI, ExtensionFactory } from "@mariozechner/pi-coding-agent";import { WORKSPACE_DIR } from "../core/config.js";import { normalizeScriptJDoc, type ScriptDiscoveryRole, type ScriptJDoc } from "./discovery-jdoc.js";
export type ScriptCollection = "packaged-skill" | "workspace-skill" | "workspace-note";export type ScriptScope = "packaged" | "workspace" | "all";export type ScriptRoleFilter = ScriptDiscoveryRole | "all";
export const SCRIPT_JDOC_MARKER = "SCRIPT_JDOC:";
const ListRuntimeScriptsSchema = Type.Object({ query: Type.Optional(Type.String({ description: "Filter by script path/summary substring." })), intent: Type.Optional(Type.String({ description: "Short natural-language goal for compact script recommendations." })), scope: Type.Optional(Type.Union([ Type.Literal("packaged"), Type.Literal("workspace"), Type.Literal("all"), ], { description: "Which script roots to inspect: packaged skill-shipped scripts, workspace-owned scripts, or all (default packaged)." })), role: Type.Optional(Type.Union([ Type.Literal("entrypoint"), Type.Literal("module"), Type.Literal("all"), ], { description: "Limit results to runnable entrypoints, helper modules, or all (default entrypoint)." })), limit: Type.Optional(Type.Integer({ description: "Max scripts to return (1-200).", minimum: 1, maximum: 200 })), include_metadata: Type.Optional(Type.Boolean({ description: "Include the parsed ScriptJDoc metadata in details." })),});
type RuntimeScriptParams = { query?: string; intent?: string; scope?: ScriptScope; role?: ScriptRoleFilter; limit?: number; include_metadata?: boolean;};
type ScriptRootSpec = { collection: ScriptCollection; scope: Exclude<ScriptScope, "all">; root: string; include: (absolutePath: string) => boolean;};
export type ScriptCatalogEntry = { name: string; absolutePath: string; displayPath: string; workspacePath: string | null; collection: ScriptCollection; scope: Exclude<ScriptScope, "all">; role: ScriptDiscoveryRole; kind: "read-only" | "mutating" | "mixed"; weight: "lightweight" | "standard" | "heavy"; summary: string; metadata?: ScriptJDoc; invocation: { runner: "bun"; script: string; via: "bun_run" | "bun"; cwd: string; };};
type RecommendationMatch = { script: ScriptCatalogEntry; score: number; matchedTerms: string[]; matchedSources: string[];};
function clampLimit(value: number | undefined, fallback = 100): number { if (!Number.isFinite(value)) return fallback; const num = Number(value); if (Number.isNaN(num)) return fallback; return Math.min(Math.max(num, 1), 200);}
function uniqueStrings(values: Iterable<string>): string[] { return [...new Set(Array.from(values).map((value) => String(value || "").trim()).filter(Boolean))];}
function normalizeText(value: string | undefined): string { return String(value || "") .toLowerCase() .replace(/[_-]+/g, " ") .replace(/[^a-z0-9+.#/\s]+/g, " ") .replace(/\s+/g, " ") .trim();}
const SHORT_TOKEN_ALLOWLIST = new Set(["ai", "db", "fs", "id", "ip", "mcp", "sql", "ssh", "ui", "vm", "vnc"]);const STOP_TOKENS = new Set([ "a", "an", "and", "are", "as", "at", "be", "by", "for", "from", "get", "help", "how", "i", "if", "in", "into", "is", "it", "me", "my", "of", "on", "or", "our", "show", "something", "task", "that", "the", "this", "to", "tool", "tools", "use", "using", "want", "what", "with", "you",]);
function tokenizeText(value: string | undefined): string[] { const text = normalizeText(value); if (!text) return []; return [...new Set(text.split(/\s+/).filter((token) => { if (!token) return false; if (SHORT_TOKEN_ALLOWLIST.has(token)) return true; if (token.length < 3) return false; if (STOP_TOKENS.has(token)) return false; return true; }))];}
function summarize(value: string | undefined, fallback: string): string { const text = String(value || "").replace(/\s+/g, " ").trim(); if (!text) return fallback; return text.length <= 160 ? text : `${text.slice(0, 159)}…`;}
function collectExampleStrings(metadata: ScriptJDoc | undefined): string[] { if (!metadata?.examples?.length) return []; const values: string[] = []; for (const example of metadata.examples) { if (typeof example === "string") { values.push(example); continue; } for (const key of ["text", "description", "summary", "title", "name", "input", "prompt", "intent", "query"] as const) { if (typeof example[key] === "string" && example[key]?.trim()) { values.push(example[key]!.trim()); break; } } } return uniqueStrings(values);}
function walkTsFiles(root: string): string[] { if (!fs.existsSync(root)) return []; const results: string[] = []; const stack = [root]; while (stack.length > 0) { const current = stack.pop()!; for (const entry of fs.readdirSync(current, { withFileTypes: true })) { const absolutePath = path.join(current, entry.name); if (entry.isDirectory()) { stack.push(absolutePath); continue; } if (!entry.isFile()) continue; if (!absolutePath.endsWith(".ts") || absolutePath.endsWith(".d.ts")) continue; results.push(absolutePath); } } return results.sort((a, b) => a.localeCompare(b));}
function cleanCommentLines(value: string): string { return value .split(/\r?\n/) .map((line) => line.replace(/^\s*\* ?/, "")) .join("\n") .trim();}
export function extractScriptJDocJson(sourceText: string): string | null { const markerIndex = sourceText.indexOf(SCRIPT_JDOC_MARKER); if (markerIndex < 0) return null; const commentStart = sourceText.lastIndexOf("/**", markerIndex); const commentEnd = sourceText.indexOf("*/", markerIndex); if (commentStart < 0 || commentEnd < 0) return null; return cleanCommentLines(sourceText.slice(markerIndex + SCRIPT_JDOC_MARKER.length, commentEnd));}
export function parseScriptJDocFromSource(sourceText: string): ScriptJDoc | undefined { const jsonText = extractScriptJDocJson(sourceText); if (!jsonText) return undefined; try { return normalizeScriptJDoc(JSON.parse(jsonText)); } catch { return undefined; }}
function detectRole(absolutePath: string, sourceText: string, metadata: ScriptJDoc | undefined): ScriptDiscoveryRole { if (metadata?.role) return metadata.role; const tokens = absolutePath.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean); if (tokens.includes("helper") || tokens.includes("helpers") || tokens.includes("sidecar")) return "module"; if (tokens.includes("scope") && tokens.includes("session")) return "module"; if (/^#!\/.+/m.test(sourceText) || /process\.argv|Bun\.argv|parseArgs\s*\(/.test(sourceText)) return "entrypoint"; if (absolutePath.includes(`${path.sep}runtime${path.sep}skills${path.sep}`)) return "entrypoint"; if (absolutePath.includes(`${path.sep}skills${path.sep}`) && absolutePath.includes(`${path.sep}runtime${path.sep}extensions${path.sep}`)) return "entrypoint"; if (absolutePath.startsWith(path.join(WORKSPACE_DIR, ".pi", "skills") + path.sep)) return "entrypoint"; if (absolutePath.startsWith(path.join(WORKSPACE_DIR, "notes") + path.sep)) return "entrypoint"; return "module";}
function fallbackSummary(absolutePath: string, collection: ScriptCollection, role: ScriptDiscoveryRole): string { const base = path.basename(absolutePath, ".ts").replace(/[-_]+/g, " "); const noun = collection === "workspace-note" ? "workspace note script" : "skill script"; return `${base.charAt(0).toUpperCase()}${base.slice(1)} ${role === "module" ? "helper module" : noun}.`;}
function getPackageRoot(): string { return path.resolve(import.meta.dir, "../../..");}
function getRuntimeRoot(): string { return path.resolve(import.meta.dir, "../..");}
export function getScriptRoots(workspaceDir = WORKSPACE_DIR): ScriptRootSpec[] { const runtimeRoot = getRuntimeRoot(); return [ { collection: "packaged-skill", scope: "packaged", root: path.join(runtimeRoot, "skills"), include: (absolutePath) => absolutePath.includes(`${path.sep}skills${path.sep}`), }, { collection: "packaged-skill", scope: "packaged", root: path.join(runtimeRoot, "extensions"), include: (absolutePath) => absolutePath.includes(`${path.sep}skills${path.sep}`), }, { collection: "packaged-skill", scope: "packaged", root: path.join(runtimeRoot, "vendor"), include: (absolutePath) => absolutePath.includes(`${path.sep}skills${path.sep}`), }, { collection: "workspace-skill", scope: "workspace", root: path.join(workspaceDir, ".pi", "skills"), include: (absolutePath) => absolutePath.endsWith(".ts") && !absolutePath.endsWith(".d.ts"), }, { collection: "workspace-note", scope: "workspace", root: path.join(workspaceDir, "notes"), include: (absolutePath) => absolutePath.endsWith(".ts") && !absolutePath.endsWith(".d.ts"), }, ];}
function buildInvocation(absolutePath: string, workspacePath: string | null): ScriptCatalogEntry["invocation"] { if (workspacePath) { return { runner: "bun", script: workspacePath, via: "bun_run", cwd: ".", }; } return { runner: "bun", script: absolutePath, via: "bun", cwd: path.dirname(absolutePath), };}
export function loadScriptCatalogEntries(options?: { workspaceDir?: string; scope?: ScriptScope; role?: ScriptRoleFilter }): ScriptCatalogEntry[] { const workspaceDir = options?.workspaceDir ?? WORKSPACE_DIR; const scope = options?.scope ?? "packaged"; const role = options?.role ?? "entrypoint"; const packageRoot = getPackageRoot();
const entries: ScriptCatalogEntry[] = []; for (const root of getScriptRoots(workspaceDir)) { if (scope !== "all" && root.scope !== scope) continue; for (const absolutePath of walkTsFiles(root.root)) { if (!root.include(absolutePath)) continue; const sourceText = fs.readFileSync(absolutePath, "utf8"); const metadata = parseScriptJDocFromSource(sourceText); const detectedRole = detectRole(absolutePath, sourceText, metadata); if (role !== "all" && detectedRole !== role) continue; const workspacePath = absolutePath.startsWith(workspaceDir + path.sep) ? path.relative(workspaceDir, absolutePath).replace(/\\/g, "/") : null; const displayPath = workspacePath || path.relative(packageRoot, absolutePath).replace(/\\/g, "/"); entries.push({ name: path.basename(absolutePath, ".ts"), absolutePath, displayPath, workspacePath, collection: root.collection, scope: root.scope, role: detectedRole, kind: metadata?.kind ?? "mixed", weight: metadata?.weight ?? "standard", summary: summarize(metadata?.summary, fallbackSummary(absolutePath, root.collection, detectedRole)), ...(metadata ? { metadata } : {}), invocation: buildInvocation(absolutePath, workspacePath), }); } } return entries.sort((a, b) => a.displayPath.localeCompare(b.displayPath));}
function filterCatalog(entries: ScriptCatalogEntry[], query: string): ScriptCatalogEntry[] { if (!query) return entries; const normalized = query.toLowerCase(); return entries.filter((entry) => entry.name.toLowerCase().includes(normalized) || entry.displayPath.toLowerCase().includes(normalized) || entry.summary.toLowerCase().includes(normalized) || entry.metadata?.summary?.toLowerCase().includes(normalized) || entry.metadata?.aliases?.some((value) => value.toLowerCase().includes(normalized)) || entry.metadata?.domains?.some((value) => value.toLowerCase().includes(normalized)) || entry.metadata?.verbs?.some((value) => value.toLowerCase().includes(normalized)) || entry.metadata?.nouns?.some((value) => value.toLowerCase().includes(normalized)) || entry.metadata?.keywords?.some((value) => value.toLowerCase().includes(normalized)) || entry.metadata?.guidance?.some((value) => value.toLowerCase().includes(normalized)) || collectExampleStrings(entry.metadata).some((value) => value.toLowerCase().includes(normalized)), );}
function addTokenMatches(intentTokens: Set<string>, terms: string[] | undefined, source: string, points: number, match: RecommendationMatch): void { if (!terms?.length) return; for (const term of uniqueStrings(terms.map((value) => normalizeText(value)))) { if (!term) continue; for (const token of tokenizeText(term)) { if (!intentTokens.has(token)) continue; match.score += points; match.matchedTerms.push(token); match.matchedSources.push(source); } }}
function addExactPhraseMatches(haystack: string, phrases: string[] | undefined, source: string, points: number, match: RecommendationMatch): void { if (!phrases?.length) return; for (const phrase of uniqueStrings(phrases.map((value) => normalizeText(value)))) { if (!phrase || !phrase.includes(" ")) continue; if (!haystack.includes(phrase)) continue; match.score += points; match.matchedTerms.push(phrase); match.matchedSources.push(source); }}
function scoreIntent(entry: ScriptCatalogEntry, intent: string): RecommendationMatch | null { const normalizedIntent = normalizeText(intent); const intentTokens = new Set(tokenizeText(normalizedIntent)); if (intentTokens.size === 0) return null;
const match: RecommendationMatch = { script: entry, score: 0, matchedTerms: [], matchedSources: [], };
addTokenMatches(intentTokens, [entry.name], "name", 4, match); addTokenMatches(intentTokens, [entry.displayPath], "path", 3, match); addTokenMatches(intentTokens, tokenizeText(entry.summary), "summary", 3, match); addTokenMatches(intentTokens, entry.metadata?.aliases, "jdoc.aliases", 3, match); addExactPhraseMatches(normalizedIntent, entry.metadata?.domains, "jdoc.domains", 3, match); addExactPhraseMatches(normalizedIntent, entry.metadata?.verbs, "jdoc.verbs", 2, match); addExactPhraseMatches(normalizedIntent, entry.metadata?.nouns, "jdoc.nouns", 2, match); addTokenMatches(intentTokens, entry.metadata?.domains, "jdoc.domains", 2, match); addTokenMatches(intentTokens, entry.metadata?.verbs, "jdoc.verbs", 2, match); addTokenMatches(intentTokens, entry.metadata?.nouns, "jdoc.nouns", 2, match); addTokenMatches(intentTokens, entry.metadata?.keywords, "jdoc.keywords", 1, match); addTokenMatches(intentTokens, collectExampleStrings(entry.metadata), "jdoc.examples", 1, match); addTokenMatches(intentTokens, entry.metadata?.guidance, "jdoc.guidance", 1, match);
match.matchedTerms = uniqueStrings(match.matchedTerms); match.matchedSources = uniqueStrings(match.matchedSources); if (match.score <= 0 || match.matchedTerms.length === 0) return null; if (entry.role === "entrypoint") match.score += 1; if (entry.role === "module") match.score -= 2; if (entry.weight === "heavy") match.score -= 1; return match.score > 0 ? match : null;}
const RUNTIME_SCRIPTS_HINT = [ "## Script discovery", "Use list_scripts to discover packaged skill scripts and workspace skill/note scripts before running them.", "Prefer entrypoint scripts by default; helper modules are available with role=all or role=module.", "When a script has a workspace-relative path, prefer bun_run to execute it without a shell.",].join("\n");
const VISIBLE_SCRIPT_HINT = "Hint: use scope=packaged|workspace|all, role=entrypoint|module|all, and prefer bun_run for workspace-relative entrypoints.";
/** Extension factory that registers list_scripts. */export const runtimeScripts: ExtensionFactory = (pi: ExtensionAPI) => { pi.on("before_agent_start", async (event) => ({ systemPrompt: `${event.systemPrompt}\n\n${RUNTIME_SCRIPTS_HINT}`, }));
pi.registerTool({ name: "list_scripts", label: "list_scripts", description: "Discover packaged skill scripts plus workspace skill/note scripts with compact summaries and bun invocation hints.", promptSnippet: "list_scripts: discover packaged skill or workspace scripts, then use bun_run for workspace-relative entrypoints when appropriate.", parameters: ListRuntimeScriptsSchema, async execute(_toolCallId, params: RuntimeScriptParams): Promise<AgentToolResult<Record<string, unknown>>> { const scope = params.scope ?? "packaged"; const role = params.role ?? "entrypoint"; const query = params.query?.trim().toLowerCase() || ""; const intent = params.intent?.trim() || ""; const limit = clampLimit(params.limit, 100); const includeMetadata = Boolean(params.include_metadata); const catalog = loadScriptCatalogEntries({ scope, role }); const filtered = filterCatalog(catalog, query);
if (intent) { const recommendations = filtered .map((entry) => scoreIntent(entry, intent)) .filter((entry): entry is RecommendationMatch => Boolean(entry)) .sort((a, b) => b.score - a.score || a.script.displayPath.localeCompare(b.script.displayPath)) .slice(0, Math.min(limit, 8));
if (recommendations.length === 0) { return { content: [{ type: "text", text: `No strong script recommendation for "${intent}".\n${VISIBLE_SCRIPT_HINT}` }], details: { total: filtered.length, count: 0, intent, query: params.query?.trim() || undefined, scope, role, scripts: [] }, }; }
const lines = recommendations.map(({ script }) => `• ${script.displayPath} — ${script.summary} {${script.collection}} [${script.kind}, ${script.weight}, ${script.role}]`); return { content: [{ type: "text", text: `Recommended scripts for "${intent}": ${recommendations.length}.\n${VISIBLE_SCRIPT_HINT}\n${lines.join("\n")}` }], details: { total: filtered.length, count: recommendations.length, intent, query: params.query?.trim() || undefined, scope, role, scripts: recommendations.map(({ script, score, matchedTerms, matchedSources }) => ({ name: script.name, path: script.displayPath, workspace_path: script.workspacePath, collection: script.collection, scope: script.scope, role: script.role, kind: script.kind, weight: script.weight, summary: script.summary, score, matched_terms: matchedTerms, matched_sources: matchedSources, invocation: script.invocation, ...(includeMetadata && script.metadata ? { metadata: script.metadata } : {}), })), }, }; }
const scripts = filtered.slice(0, limit); if (scripts.length === 0) { return { content: [{ type: "text", text: query ? `No scripts found matching "${params.query}".\n${VISIBLE_SCRIPT_HINT}` : `No scripts found.\n${VISIBLE_SCRIPT_HINT}` }], details: { total: filtered.length, count: 0, query: params.query?.trim() || undefined, scope, role, scripts: [] }, }; }
const header = query ? `Available scripts (filtered): ${scripts.length} of ${filtered.length}.` : `Available scripts: ${scripts.length} of ${filtered.length}.`; const lines = scripts.map((script) => `• ${script.displayPath} — ${script.summary} {${script.collection}} [${script.kind}, ${script.weight}, ${script.role}]`); return { content: [{ type: "text", text: `${header}\n${VISIBLE_SCRIPT_HINT}\n${lines.join("\n")}` }], details: { total: filtered.length, count: scripts.length, query: params.query?.trim() || undefined, scope, role, scripts: scripts.map((script) => ({ name: script.name, path: script.displayPath, workspace_path: script.workspacePath, collection: script.collection, scope: script.scope, role: script.role, kind: script.kind, weight: script.weight, summary: script.summary, invocation: script.invocation, ...(includeMetadata && script.metadata ? { metadata: script.metadata } : {}), })), }, }; }, });};