@narumitw/pi-cbmem 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/index.ts +523 -0
- package/dist/index.ts.map +7 -0
- package/package.json +65 -0
- package/skills/codebase-memory/SKILL.md +114 -0
- package/src/cbmem.ts +346 -0
- package/src/index.ts +1 -0
- package/src/render-result.ts +28 -0
- package/src/tool-definitions.ts +233 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/cbmem.ts", "../src/render-result.ts", "../src/tool-definitions.ts"],
|
|
4
|
+
"sourcesContent": ["import { spawn } from \"node:child_process\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type {\n\tAgentToolResult,\n\tExtensionAPI,\n\tExtensionContext,\n} from \"@earendil-works/pi-coding-agent\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from \"@earendil-works/pi-coding-agent\";\nimport { sanitizeTerminalText } from \"@narumitw/pi-tui-kit/terminal-text\";\nimport { renderCodebaseMemoryResult } from \"./render-result.js\";\nimport { type BridgeToolDefinition, TOOL_DEFINITIONS, type ToolName } from \"./tool-definitions.js\";\n\nexport { TOOL_NAMES } from \"./tool-definitions.js\";\n\nconst BIN = join(homedir(), \".local\", \"bin\", \"codebase-memory-mcp\");\nconst STDERR_MAX_BYTES = 8 * 1024;\nconst FORCE_KILL_DELAY_MS = 250;\n\nexport interface CbmemToolDetails {\n\ttruncated: boolean;\n\ttotalBytes: number;\n\ttotalLines: number;\n}\n\nclass BoundedPrefixCollector {\n\treadonly maxBytes: number;\n\ttext = \"\";\n\ttotalBytes = 0;\n\tnewlines = 0;\n\tendsWithNewline = false;\n\ttruncated = false;\n\n\tconstructor(maxBytes: number) {\n\t\tthis.maxBytes = maxBytes;\n\t}\n\n\tappend(chunk: string): void {\n\t\tconst chunkBytes = Buffer.byteLength(chunk, \"utf8\");\n\t\tthis.totalBytes += chunkBytes;\n\t\tthis.newlines += countOccurrences(chunk, \"\\n\");\n\t\tif (chunk.length > 0) this.endsWithNewline = chunk.endsWith(\"\\n\");\n\n\t\tconst remaining = this.maxBytes - Buffer.byteLength(this.text, \"utf8\");\n\t\tif (remaining <= 0) {\n\t\t\tif (chunkBytes > 0) this.truncated = true;\n\t\t\treturn;\n\t\t}\n\t\tconst kept = takeUtf8Prefix(chunk, remaining, Number.POSITIVE_INFINITY);\n\t\tthis.text += kept;\n\t\tif (kept.length !== chunk.length) this.truncated = true;\n\t}\n\n\tget totalLines(): number {\n\t\tif (this.totalBytes === 0) return 0;\n\t\treturn this.newlines + (this.endsWithNewline ? 0 : 1);\n\t}\n}\n\nclass BoundedTailCollector {\n\treadonly maxBytes: number;\n\ttext = \"\";\n\ttruncated = false;\n\n\tconstructor(maxBytes: number) {\n\t\tthis.maxBytes = maxBytes;\n\t}\n\n\tappend(chunk: string): void {\n\t\tthis.text += chunk;\n\t\tif (Buffer.byteLength(this.text, \"utf8\") <= this.maxBytes) return;\n\t\tthis.text = takeUtf8Suffix(this.text, this.maxBytes);\n\t\tthis.truncated = true;\n\t}\n}\n\nexport async function callCodebaseMemory(\n\ttool: ToolName,\n\targs: Record<string, unknown>,\n\tsignal: AbortSignal | undefined,\n\tcwd: string,\n\tbinary = BIN,\n): Promise<AgentToolResult<CbmemToolDetails>> {\n\tsignal?.throwIfAborted();\n\tconst input = JSON.stringify(args);\n\n\treturn await new Promise((resolve, reject) => {\n\t\tconst stdout = new BoundedPrefixCollector(DEFAULT_MAX_BYTES);\n\t\tconst stderr = new BoundedTailCollector(STDERR_MAX_BYTES);\n\t\tconst child = spawn(binary, [\"cli\", tool], {\n\t\t\tcwd,\n\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\tenv: { ...process.env, CBM_LOG_LEVEL: \"error\" },\n\t\t});\n\t\tlet settled = false;\n\t\tlet forceKillTimer: NodeJS.Timeout | undefined;\n\n\t\tconst cleanup = () => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tif (forceKillTimer) clearTimeout(forceKillTimer);\n\t\t};\n\t\tconst fail = (error: unknown) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tif (child.exitCode === null && child.signalCode === null) child.kill(\"SIGKILL\");\n\t\t\treject(error);\n\t\t};\n\t\tconst succeed = (result: AgentToolResult<CbmemToolDetails>) => {\n\t\t\tif (settled) return;\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tresolve(result);\n\t\t};\n\t\tconst onAbort = () => {\n\t\t\tif (child.exitCode !== null || child.signalCode !== null) return;\n\t\t\tchild.kill(\"SIGTERM\");\n\t\t\tforceKillTimer = setTimeout(() => {\n\t\t\t\tif (child.exitCode === null && child.signalCode === null) child.kill(\"SIGKILL\");\n\t\t\t}, FORCE_KILL_DELAY_MS);\n\t\t\tforceKillTimer.unref();\n\t\t};\n\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\tif (signal?.aborted) onAbort();\n\n\t\tchild.stdout.setEncoding(\"utf8\");\n\t\tchild.stderr.setEncoding(\"utf8\");\n\t\tchild.stdout.on(\"data\", (chunk: string) => stdout.append(chunk));\n\t\tchild.stderr.on(\"data\", (chunk: string) => stderr.append(chunk));\n\t\tchild.stdout.on(\"error\", fail);\n\t\tchild.stderr.on(\"error\", fail);\n\t\tchild.stdin.on(\"error\", () => {\n\t\t\t// Spawn and exit errors are reported by the child process events below.\n\t\t});\n\t\tchild.on(\"error\", fail);\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (settled) return;\n\t\t\tif (signal?.aborted) {\n\t\t\t\tfail(abortReason(signal));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (code !== 0) {\n\t\t\t\tfail(cliFailure(tool, code, stderr));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (stdout.truncated) {\n\t\t\t\tfail(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`codebase-memory-mcp ${tool} exceeded ${formatSize(DEFAULT_MAX_BYTES)} before a complete JSON response could be validated`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst response = extractLastJson(stdout.text);\n\t\t\t\tconst bounded = boundOutput(response, stdout);\n\t\t\t\tsucceed({\n\t\t\t\t\tcontent: [{ type: \"text\", text: bounded.text }],\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\ttruncated: bounded.truncated,\n\t\t\t\t\t\ttotalBytes: stdout.totalBytes,\n\t\t\t\t\t\ttotalLines: stdout.totalLines,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconst diagnostic = stderr.text.trim();\n\t\t\t\tconst suffix = diagnostic ? `: ${diagnostic}` : \"\";\n\t\t\t\tfail(\n\t\t\t\t\tnew Error(`codebase-memory-mcp ${tool} returned no JSON response${suffix}`, {\n\t\t\t\t\t\tcause: error,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t\tchild.stdin.end(input);\n\t});\n}\n\nexport default function cbmem(pi: ExtensionAPI, binary = BIN): void {\n\tfor (const definition of TOOL_DEFINITIONS) registerBridgeTool(pi, definition, binary);\n}\n\nfunction registerBridgeTool(\n\tpi: ExtensionAPI,\n\tdefinition: BridgeToolDefinition,\n\tbinary: string,\n): void {\n\tpi.registerTool({\n\t\t...definition,\n\t\trenderResult: renderCodebaseMemoryResult,\n\t\tasync execute(_toolCallId, params, signal, _onUpdate, ctx) {\n\t\t\tconst tool = definition.name as ToolName;\n\t\t\tconst args = params as Record<string, unknown>;\n\t\t\tawait confirmDestructiveCall(tool, args, signal, ctx);\n\t\t\treturn await callCodebaseMemory(tool, args, signal, ctx.cwd, binary);\n\t\t},\n\t});\n}\n\nasync function confirmDestructiveCall(\n\ttool: ToolName,\n\targs: Record<string, unknown>,\n\tsignal: AbortSignal | undefined,\n\tctx: ExtensionContext,\n): Promise<void> {\n\tconst prompt = destructivePrompt(tool, args);\n\tif (!prompt) return;\n\n\tsignal?.throwIfAborted();\n\tif (!ctx.hasUI || (ctx.mode !== \"tui\" && ctx.mode !== \"rpc\")) {\n\t\tthrow new Error(\n\t\t\t`Codebase Memory ${tool} requires user confirmation in TUI or RPC mode before it can run.`,\n\t\t);\n\t}\n\tconst confirmed = await ctx.ui.confirm(prompt.title, prompt.message, { signal });\n\tsignal?.throwIfAborted();\n\tif (!confirmed) {\n\t\tthrow new DOMException(`Codebase Memory ${tool} was cancelled by the user.`, \"AbortError\");\n\t}\n}\n\nfunction destructivePrompt(\n\ttool: ToolName,\n\targs: Record<string, unknown>,\n): { title: string; message: string } | undefined {\n\tconst project = safeArgument(args.project, \"unknown project\");\n\tif (tool === \"delete_project\") {\n\t\treturn {\n\t\t\ttitle: \"Delete Codebase Memory project?\",\n\t\t\tmessage: `Project: ${project}\\nThis permanently removes the project's Codebase Memory index.`,\n\t\t};\n\t}\n\tif (tool === \"manage_adr\" && args.mode === \"update\") {\n\t\tconst contentBytes =\n\t\t\ttypeof args.content === \"string\" ? Buffer.byteLength(args.content, \"utf8\") : 0;\n\t\treturn {\n\t\t\ttitle: \"Replace Codebase Memory ADRs?\",\n\t\t\tmessage: `Project: ${project}\\nReplace the complete ADR document with ${contentBytes} bytes of content.`,\n\t\t};\n\t}\n\treturn undefined;\n}\n\nfunction safeArgument(value: unknown, fallback: string): string {\n\tif (typeof value !== \"string\") return fallback;\n\treturn sanitizeTerminalText(value) || fallback;\n}\n\nfunction extractLastJson(output: string): string {\n\tconst trimmed = output.trim();\n\tif (!trimmed) throw new Error(\"empty stdout\");\n\ttry {\n\t\tJSON.parse(trimmed);\n\t\treturn trimmed;\n\t} catch {\n\t\tconst lines = trimmed.split(\"\\n\");\n\t\tfor (let index = lines.length - 1; index >= 0; index--) {\n\t\t\tconst line = lines[index].trim();\n\t\t\tif (!line) continue;\n\t\t\ttry {\n\t\t\t\tJSON.parse(line);\n\t\t\t\treturn line;\n\t\t\t} catch {\n\t\t\t\t// Keep scanning for the last complete JSON response.\n\t\t\t}\n\t\t}\n\t\tthrow new Error(\"stdout did not contain JSON\");\n\t}\n}\n\nfunction boundOutput(\n\ttext: string,\n\tcollector: BoundedPrefixCollector,\n): { text: string; truncated: boolean } {\n\tconst textBytes = Buffer.byteLength(text, \"utf8\");\n\tconst textLines = countLines(text);\n\tconst truncated =\n\t\tcollector.truncated || textBytes > DEFAULT_MAX_BYTES || textLines > DEFAULT_MAX_LINES;\n\tif (!truncated) return { text, truncated: false };\n\n\tconst notice = `[Output truncated: Codebase Memory produced ${collector.totalLines} lines (${formatSize(collector.totalBytes)}); additional output was omitted.]`;\n\tconst separator = \"\\n\";\n\tconst body = takeUtf8Prefix(\n\t\ttext,\n\t\tMath.max(0, DEFAULT_MAX_BYTES - Buffer.byteLength(notice + separator, \"utf8\")),\n\t\tMath.max(0, DEFAULT_MAX_LINES - 1),\n\t);\n\treturn {\n\t\ttext: body ? `${body}${body.endsWith(\"\\n\") ? \"\" : separator}${notice}` : notice,\n\t\ttruncated: true,\n\t};\n}\n\nfunction cliFailure(tool: ToolName, code: number | null, stderr: BoundedTailCollector): Error {\n\tconst diagnostic = stderr.text.trim();\n\tconst truncation = stderr.truncated ? \"[earlier stderr omitted] \" : \"\";\n\tconst suffix = diagnostic ? `: ${truncation}${diagnostic}` : \"\";\n\treturn new Error(`codebase-memory-mcp ${tool} exited with code ${code ?? \"unknown\"}${suffix}`);\n}\n\nfunction abortReason(signal: AbortSignal): unknown {\n\treturn signal.reason instanceof Error\n\t\t? signal.reason\n\t\t: new DOMException(\"Codebase Memory tool call was aborted\", \"AbortError\");\n}\n\nfunction takeUtf8Prefix(text: string, maxBytes: number, maxLines: number): string {\n\tlet bytes = 0;\n\tlet lines = text ? 1 : 0;\n\tlet result = \"\";\n\tfor (const character of text) {\n\t\tconst nextLines = character === \"\\n\" ? lines + 1 : lines;\n\t\tconst characterBytes = Buffer.byteLength(character, \"utf8\");\n\t\tif (bytes + characterBytes > maxBytes || nextLines > maxLines) break;\n\t\tresult += character;\n\t\tbytes += characterBytes;\n\t\tlines = nextLines;\n\t}\n\treturn result;\n}\n\nfunction takeUtf8Suffix(text: string, maxBytes: number): string {\n\tlet bytes = 0;\n\tconst characters = Array.from(text);\n\tlet start = characters.length;\n\twhile (start > 0) {\n\t\tconst characterBytes = Buffer.byteLength(characters[start - 1], \"utf8\");\n\t\tif (bytes + characterBytes > maxBytes) break;\n\t\tbytes += characterBytes;\n\t\tstart--;\n\t}\n\treturn characters.slice(start).join(\"\");\n}\n\nfunction countOccurrences(text: string, character: string): number {\n\tlet count = 0;\n\tfor (const value of text) if (value === character) count++;\n\treturn count;\n}\n\nfunction countLines(text: string): number {\n\tif (!text) return 0;\n\treturn countOccurrences(text, \"\\n\") + (text.endsWith(\"\\n\") ? 0 : 1);\n}\n", "import type { AgentToolResult, ToolRenderResultOptions } from \"@earendil-works/pi-coding-agent\";\nimport { Text } from \"@earendil-works/pi-tui\";\nimport { sanitizeTerminalText } from \"@narumitw/pi-tui-kit/terminal-text\";\n\ninterface RenderTheme {\n\tfg(color: \"toolOutput\" | \"warning\", text: string): string;\n}\n\nexport function renderCodebaseMemoryResult(\n\tresult: AgentToolResult<unknown>,\n\toptions: ToolRenderResultOptions,\n\ttheme: RenderTheme,\n): Text {\n\tconst rawText = result.content\n\t\t.flatMap((content) => (content.type === \"text\" ? [content.text] : []))\n\t\t.join(\"\\n\");\n\tconst displayText = sanitizeMultilineTerminalText(rawText);\n\tconst color = options.isPartial ? \"warning\" : \"toolOutput\";\n\treturn new Text(theme.fg(color, displayText), 0, 0);\n}\n\nfunction sanitizeMultilineTerminalText(value: string): string {\n\treturn value\n\t\t.replace(/\\r\\n?/gu, \"\\n\")\n\t\t.split(\"\\n\")\n\t\t.map((line) => sanitizeTerminalText(line))\n\t\t.join(\"\\n\");\n}\n", "import { StringEnum } from \"@earendil-works/pi-ai\";\nimport type { ToolDefinition } from \"@earendil-works/pi-coding-agent\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from \"@earendil-works/pi-coding-agent\";\nimport type { TSchema } from \"typebox\";\nimport { Type } from \"typebox\";\n\nexport type BridgeToolDefinition = Pick<\n\tToolDefinition<TSchema>,\n\t\"name\" | \"label\" | \"description\" | \"parameters\"\n>;\n\nconst Project = Type.String({ description: \"Indexed project name from list_projects.\" });\nconst outputLimit = `Output is limited to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)}; byte-oversized responses fail validation instead of returning partial JSON.`;\n\nexport const TOOL_DEFINITIONS = [\n\t{\n\t\tname: \"index_repository\",\n\t\tlabel: \"Index Repository\",\n\t\tdescription: `Index a repository in the Codebase Memory graph. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\trepo_path: Type.String({ description: \"Path to the repository.\" }),\n\t\t\tmode: Type.Optional(\n\t\t\t\tStringEnum([\"full\", \"moderate\", \"fast\", \"cross-repo-intelligence\"] as const),\n\t\t\t),\n\t\t\ttarget_projects: Type.Optional(Type.Array(Type.String())),\n\t\t\tname: Type.Optional(Type.String({ description: \"Override the derived project name.\" })),\n\t\t\tpersistence: Type.Optional(Type.Boolean()),\n\t\t}),\n\t},\n\t{\n\t\tname: \"search_graph\",\n\t\tlabel: \"Search Graph\",\n\t\tdescription: `Search indexed symbols by text, name, file, relationship, or degree. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\tproject: Project,\n\t\t\tquery: Type.Optional(Type.String({ description: \"Natural-language or keyword search.\" })),\n\t\t\tlabel: Type.Optional(Type.String()),\n\t\t\tname_pattern: Type.Optional(Type.String()),\n\t\t\tqn_pattern: Type.Optional(Type.String()),\n\t\t\tfile_pattern: Type.Optional(Type.String()),\n\t\t\trelationship: Type.Optional(Type.String()),\n\t\t\tmin_degree: Type.Optional(Type.Integer()),\n\t\t\tmax_degree: Type.Optional(Type.Integer()),\n\t\t\texclude_entry_points: Type.Optional(Type.Boolean()),\n\t\t\tinclude_connected: Type.Optional(Type.Boolean()),\n\t\t\tsemantic_query: Type.Optional(Type.Array(Type.String())),\n\t\t\tlimit: Type.Optional(Type.Integer()),\n\t\t\toffset: Type.Optional(Type.Integer()),\n\t\t\tformat: Type.Optional(StringEnum([\"tree\", \"json\"] as const)),\n\t\t\tfields: Type.Optional(Type.Array(Type.String())),\n\t\t\tdetail: Type.Optional(StringEnum([\"ids\", \"default\"] as const)),\n\t\t}),\n\t},\n\t{\n\t\tname: \"query_graph\",\n\t\tlabel: \"Query Graph\",\n\t\tdescription: `Execute a Cypher query against the code or missed-coverage graph. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\tquery: Type.String({ description: \"Cypher query.\" }),\n\t\t\tproject: Project,\n\t\t\tgraph: Type.Optional(StringEnum([\"code\", \"missed\"] as const)),\n\t\t\tmax_rows: Type.Optional(Type.Integer()),\n\t\t}),\n\t},\n\t{\n\t\tname: \"trace_path\",\n\t\tlabel: \"Trace Path\",\n\t\tdescription: `Trace callers, callees, data flow, or cross-service paths. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\tfunction_name: Type.String(),\n\t\t\tproject: Project,\n\t\t\tdirection: Type.Optional(StringEnum([\"inbound\", \"outbound\", \"both\"] as const)),\n\t\t\tdepth: Type.Optional(Type.Integer()),\n\t\t\tlimit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5000 })),\n\t\t\tcursor: Type.Optional(Type.String()),\n\t\t\tmode: Type.Optional(StringEnum([\"calls\", \"data_flow\", \"cross_service\"] as const)),\n\t\t\tparameter_name: Type.Optional(Type.String()),\n\t\t\tedge_types: Type.Optional(Type.Array(Type.String())),\n\t\t\trisk_labels: Type.Optional(Type.Boolean()),\n\t\t\tinclude_tests: Type.Optional(Type.Boolean()),\n\t\t\tformat: Type.Optional(StringEnum([\"tree\", \"json\"] as const)),\n\t\t\tinclude_evidence: Type.Optional(Type.Boolean()),\n\t\t}),\n\t},\n\t{\n\t\tname: \"get_code_snippet\",\n\t\tlabel: \"Get Code Snippet\",\n\t\tdescription: `Read source for an indexed symbol by qualified name. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\tqualified_name: Type.String(),\n\t\t\tproject: Project,\n\t\t\tinclude_neighbors: Type.Optional(Type.Boolean()),\n\t\t}),\n\t},\n\t{\n\t\tname: \"get_graph_schema\",\n\t\tlabel: \"Get Graph Schema\",\n\t\tdescription: `Get graph node labels and edge types. ${outputLimit}`,\n\t\tparameters: Type.Object({ project: Project }),\n\t},\n\t{\n\t\tname: \"get_architecture\",\n\t\tlabel: \"Get Architecture\",\n\t\tdescription: `Summarize architecture, dependencies, boundaries, clusters, or hotspots. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\tproject: Project,\n\t\t\tpath: Type.Optional(Type.String()),\n\t\t\taspects: Type.Optional(\n\t\t\t\tType.Array(\n\t\t\t\t\tStringEnum([\n\t\t\t\t\t\t\"all\",\n\t\t\t\t\t\t\"overview\",\n\t\t\t\t\t\t\"structure\",\n\t\t\t\t\t\t\"dependencies\",\n\t\t\t\t\t\t\"routes\",\n\t\t\t\t\t\t\"languages\",\n\t\t\t\t\t\t\"packages\",\n\t\t\t\t\t\t\"entry_points\",\n\t\t\t\t\t\t\"hotspots\",\n\t\t\t\t\t\t\"boundaries\",\n\t\t\t\t\t\t\"layers\",\n\t\t\t\t\t\t\"file_tree\",\n\t\t\t\t\t\t\"clusters\",\n\t\t\t\t\t\t\"cycles\",\n\t\t\t\t\t] as const),\n\t\t\t\t),\n\t\t\t),\n\t\t}),\n\t},\n\t{\n\t\tname: \"search_code\",\n\t\tlabel: \"Search Code\",\n\t\tdescription: `Search source text and enrich matches with graph context. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\tpattern: Type.String(),\n\t\t\tproject: Project,\n\t\t\tfile_pattern: Type.Optional(Type.String()),\n\t\t\tpath_filter: Type.Optional(Type.String()),\n\t\t\tmode: Type.Optional(StringEnum([\"compact\", \"full\", \"files\"] as const)),\n\t\t\tcontext: Type.Optional(Type.Integer()),\n\t\t\tregex: Type.Optional(Type.Boolean()),\n\t\t\tdebug: Type.Optional(Type.Boolean()),\n\t\t\tlimit: Type.Optional(Type.Integer({ minimum: 1 })),\n\t\t}),\n\t},\n\t{\n\t\tname: \"list_projects\",\n\t\tlabel: \"List Projects\",\n\t\tdescription: `List indexed Codebase Memory projects. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\toffset: Type.Optional(Type.Integer({ minimum: 0 })),\n\t\t\tlimit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),\n\t\t\tinclude_details: Type.Optional(Type.Boolean()),\n\t\t\tmetadata_only: Type.Optional(Type.Boolean()),\n\t\t}),\n\t},\n\t{\n\t\tname: \"delete_project\",\n\t\tlabel: \"Delete Project\",\n\t\tdescription: `Delete a project from the Codebase Memory index. ${outputLimit}`,\n\t\tparameters: Type.Object({ project: Project }),\n\t},\n\t{\n\t\tname: \"index_status\",\n\t\tlabel: \"Index Status\",\n\t\tdescription: `Get project index health, Git context, and coverage gaps. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\tproject: Project,\n\t\t\tverbose: Type.Optional(Type.Boolean()),\n\t\t}),\n\t},\n\t{\n\t\tname: \"check_index_coverage\",\n\t\tlabel: \"Check Index Coverage\",\n\t\tdescription: `Check best-effort index coverage for exact paths or bounded scopes. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\tproject: Project,\n\t\t\tpaths: Type.Optional(Type.Array(Type.String(), { maxItems: 128 })),\n\t\t\tscopes: Type.Optional(Type.Array(Type.String(), { maxItems: 32 })),\n\t\t\tscope_limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),\n\t\t\tscope_offset: Type.Optional(Type.Integer({ minimum: 0 })),\n\t\t}),\n\t},\n\t{\n\t\tname: \"detect_changes\",\n\t\tlabel: \"Detect Changes\",\n\t\tdescription: `Map a Git diff to changed files and impacted graph symbols. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\tproject: Project,\n\t\t\tscope: Type.Optional(StringEnum([\"files\", \"impact\"] as const)),\n\t\t\tdirection: Type.Optional(StringEnum([\"inbound\", \"outbound\", \"both\"] as const)),\n\t\t\tdepth: Type.Optional(Type.Integer()),\n\t\t\tlimit: Type.Optional(Type.Integer({ maximum: 5000 })),\n\t\t\tbase_branch: Type.Optional(Type.String()),\n\t\t\tsince: Type.Optional(Type.String()),\n\t\t\tformat: Type.Optional(StringEnum([\"tree\", \"json\"] as const)),\n\t\t}),\n\t},\n\t{\n\t\tname: \"manage_adr\",\n\t\tlabel: \"Manage ADR\",\n\t\tdescription: `Read or replace Architecture Decision Records. ${outputLimit}`,\n\t\tparameters: Type.Object(\n\t\t\t{\n\t\t\t\tproject: Project,\n\t\t\t\tmode: Type.Optional(StringEnum([\"get\", \"update\", \"sections\"] as const)),\n\t\t\t\tcontent: Type.Optional(Type.String()),\n\t\t\t},\n\t\t\t{ additionalProperties: false },\n\t\t),\n\t},\n\t{\n\t\tname: \"ingest_traces\",\n\t\tlabel: \"Ingest Traces\",\n\t\tdescription: `Ingest runtime caller-to-callee traces into the graph. ${outputLimit}`,\n\t\tparameters: Type.Object({\n\t\t\ttraces: Type.Array(\n\t\t\t\tType.Object(\n\t\t\t\t\t{\n\t\t\t\t\t\tcaller: Type.Optional(Type.String()),\n\t\t\t\t\t\tcallee: Type.Optional(Type.String()),\n\t\t\t\t\t\tcount: Type.Optional(Type.Integer()),\n\t\t\t\t\t},\n\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t),\n\t\t\t),\n\t\t\tproject: Project,\n\t\t}),\n\t},\n] as const satisfies readonly BridgeToolDefinition[];\n\nexport type ToolName = (typeof TOOL_DEFINITIONS)[number][\"name\"];\nexport const TOOL_NAMES: readonly ToolName[] = TOOL_DEFINITIONS.map(({ name }) => name);\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["DEFAULT_MAX_BYTES", "DEFAULT_MAX_LINES", "formatSize", "sanitizeTerminalText", "DEFAULT_MAX_BYTES", "formatSize", "sanitizeTerminalText", "DEFAULT_MAX_LINES"]
|
|
7
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@narumitw/pi-cbmem",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Private Pi package for Codebase Memory knowledge graph tools and guidance.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"pi",
|
|
12
|
+
"codebase-memory",
|
|
13
|
+
"knowledge-graph",
|
|
14
|
+
"mcp"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"dist",
|
|
19
|
+
"skills",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./dist/index.ts"
|
|
26
|
+
],
|
|
27
|
+
"skills": [
|
|
28
|
+
"./skills"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"piExtension": {
|
|
32
|
+
"lifecycle": "stable"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "node scripts/build-runtime.mjs",
|
|
36
|
+
"check": "npm run build && biome check . && npm run typecheck",
|
|
37
|
+
"format": "biome check --write .",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"prepack": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-ai": "*",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
44
|
+
"@earendil-works/pi-tui": "*",
|
|
45
|
+
"typebox": "*"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@biomejs/biome": "2.5.10",
|
|
49
|
+
"@earendil-works/pi-ai": "0.84.3",
|
|
50
|
+
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
51
|
+
"@earendil-works/pi-tui": "0.84.3",
|
|
52
|
+
"@types/node": "26.2.0",
|
|
53
|
+
"esbuild": "0.28.2",
|
|
54
|
+
"typebox": "1.3.18",
|
|
55
|
+
"typescript": "7.0.2"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@narumitw/pi-tui-kit": "^0.59.0"
|
|
59
|
+
},
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "https://github.com/narumiruna/pi-extensions",
|
|
63
|
+
"directory": "packages/pi-cbmem"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codebase-memory
|
|
3
|
+
description: "Use the codebase knowledge graph for structural code queries. Triggers on: explore the codebase, understand the architecture, what functions exist, show me the structure, who calls this function, what does X call, trace the call chain, find callers of, show dependencies, impact analysis, dead code, unused functions, high fan-out, refactor candidates, code quality audit, graph query syntax, Cypher query examples, edge types, how to use search_graph."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Codebase Memory — Knowledge Graph Tools
|
|
7
|
+
|
|
8
|
+
Graph tools return precise structural results in ~500 tokens vs ~80K for grep.
|
|
9
|
+
|
|
10
|
+
Always prefer MCP graph tools over grep, glob, or file search for code discovery.
|
|
11
|
+
|
|
12
|
+
## Priority Order
|
|
13
|
+
|
|
14
|
+
1. `search_graph` — find functions, classes, routes, and variables by pattern.
|
|
15
|
+
2. `trace_path` — trace who calls a function or what it calls.
|
|
16
|
+
3. `get_code_snippet` — read specific function or class source code.
|
|
17
|
+
4. `check_index_coverage` — validate candidate paths and missed ranges before claims.
|
|
18
|
+
5. `query_graph` — run Cypher queries for complex patterns.
|
|
19
|
+
6. `get_architecture` — get a high-level project summary.
|
|
20
|
+
|
|
21
|
+
## Quick Decision Matrix
|
|
22
|
+
|
|
23
|
+
Use the exact `project="<name>"` returned by `list_projects` in every project-scoped call.
|
|
24
|
+
|
|
25
|
+
| Question | Tool call |
|
|
26
|
+
|----------|----------|
|
|
27
|
+
| Who calls X? | `trace_path(project="<name>", function_name="X", direction="inbound")` |
|
|
28
|
+
| What does X call? | `trace_path(project="<name>", function_name="X", direction="outbound")` |
|
|
29
|
+
| Full call context | `trace_path(project="<name>", function_name="X", direction="both")` |
|
|
30
|
+
| Find by name pattern | `search_graph(project="<name>", name_pattern="...")` |
|
|
31
|
+
| Dead code | `search_graph(project="<name>", max_degree=0, exclude_entry_points=true)` |
|
|
32
|
+
| Cross-service edges | `query_graph(project="<name>", query="<cypher>")` |
|
|
33
|
+
| Impact of local changes | `detect_changes(project="<name>")` |
|
|
34
|
+
| Risk-classified trace | `trace_path(project="<name>", function_name="X", risk_labels=true)` |
|
|
35
|
+
| Text search | `search_code(project="<name>", pattern="...")` or Grep |
|
|
36
|
+
|
|
37
|
+
## Exploration Workflow
|
|
38
|
+
|
|
39
|
+
1. `list_projects` — check whether the project is indexed and copy its exact name.
|
|
40
|
+
2. `get_graph_schema(project="<name>")` — understand node and edge types.
|
|
41
|
+
3. `search_graph(project="<name>", label="Function", name_pattern=".*Pattern.*")` — find code.
|
|
42
|
+
4. `get_code_snippet(project="<name>", qualified_name="project.path.FuncName")` — read source.
|
|
43
|
+
5. `check_index_coverage(project="<name>", paths=["path/to/file"])` — validate every evidence path.
|
|
44
|
+
|
|
45
|
+
## Tracing Workflow
|
|
46
|
+
|
|
47
|
+
1. `search_graph(project="<name>", name_pattern=".*FuncName.*")` — discover the exact name.
|
|
48
|
+
2. `trace_path(project="<name>", function_name="FuncName", direction="both", depth=3)` — trace relationships.
|
|
49
|
+
3. `get_code_snippet(project="<name>", qualified_name="project.path.FuncName")` — verify material source claims.
|
|
50
|
+
4. `check_index_coverage(project="<name>", paths=["path/to/file"])` — validate every evidence path.
|
|
51
|
+
5. `detect_changes(project="<name>")` — map the Git diff to affected symbols.
|
|
52
|
+
|
|
53
|
+
## When to Fall Back to Grep/Glob
|
|
54
|
+
|
|
55
|
+
- Search for string literals, error messages, or configuration values.
|
|
56
|
+
- Search non-code files such as Dockerfiles, shell scripts, or configuration files.
|
|
57
|
+
- Fall back when MCP tools return insufficient results.
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
- Find a handler: `search_graph(project="<name>", name_pattern=".*OrderHandler.*")`.
|
|
62
|
+
- Find who calls it: `trace_path(project="<name>", function_name="OrderHandler", direction="inbound")`.
|
|
63
|
+
- Read its source: `get_code_snippet(project="<name>", qualified_name="pkg/orders.OrderHandler")`.
|
|
64
|
+
|
|
65
|
+
## Evidence Tiers
|
|
66
|
+
|
|
67
|
+
- **Scout (Tier 1):** quick positive lookup with few calls and targeted source checks.
|
|
68
|
+
Mark it provisional, and do not make negative or exhaustive claims.
|
|
69
|
+
- **Verify (Tier 2, default):** task-directed graph evidence, relevant trace directions, exact snippets for material claims, and relevant pagination.
|
|
70
|
+
- **Auditor (Tier 3):** bounded-scope full verification with a current generation, complete relevant pagination, both call directions and broader relationships when material, and every limitation disclosed.
|
|
71
|
+
- After candidate paths are known in any tier, call `check_index_coverage` once with every evidence path.
|
|
72
|
+
Add relevant scopes for negative or exhaustive claims.
|
|
73
|
+
A clean result means no recorded gap, not proof of completeness.
|
|
74
|
+
For partial, skipped, excluded, stale, pending, or unknown coverage, read or grep the reported ranges or scope before relying on graph results.
|
|
75
|
+
|
|
76
|
+
## Session Resets and Subagents
|
|
77
|
+
|
|
78
|
+
- At session start or after compaction, confirm the nearest graph project and generation with `list_projects` or `index_status`, then choose Scout, Verify, or Auditor.
|
|
79
|
+
- Before spawning a subagent, query the graph and coverage in the parent.
|
|
80
|
+
Pass the tier, project, generation or freshness, bounded scope, queries and pagination state, qualified symbols, paths, call-chain findings, coverage evidence with ranges and reasons, source fallback already performed, and unresolved questions in the delegated task context.
|
|
81
|
+
- Do not assume subagents inherit MCP access or the parent conversation.
|
|
82
|
+
If a child lacks MCP tools, it must not call or claim MCP access.
|
|
83
|
+
It should use the supplied evidence and read or grep exact source, especially every reported missed-coverage range.
|
|
84
|
+
|
|
85
|
+
## Quality Analysis
|
|
86
|
+
- Dead code: `search_graph(project="<name>", max_degree=0, exclude_entry_points=true)`
|
|
87
|
+
- High fan-out: `query_graph(project="<name>", query="MATCH (f)-[:CALLS]->() WITH f, count(*) AS n WHERE n >= 10 RETURN f.name, n ORDER BY n DESC LIMIT 20")`
|
|
88
|
+
- High fan-in: `query_graph(project="<name>", query="MATCH ()-[:CALLS]->(f) WITH f, count(*) AS n WHERE n >= 10 RETURN f.name, n ORDER BY n DESC LIMIT 20")`
|
|
89
|
+
|
|
90
|
+
## 15 MCP Tools
|
|
91
|
+
`index_repository`, `index_status`, `list_projects`, `delete_project`,
|
|
92
|
+
`search_graph`, `search_code`, `trace_path`, `detect_changes`,
|
|
93
|
+
`query_graph`, `get_graph_schema`, `get_code_snippet`, `get_architecture`,
|
|
94
|
+
`check_index_coverage`, `manage_adr`, `ingest_traces`
|
|
95
|
+
|
|
96
|
+
## Edge Types
|
|
97
|
+
CALLS, HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, IMPORTS, DEFINES, DEFINES_METHOD,
|
|
98
|
+
HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CALL_REFERENCE, CONFIGURES, FILE_CHANGES_WITH,
|
|
99
|
+
SIMILAR_TO, SEMANTICALLY_RELATED, CONTAINS_FILE, CONTAINS_FOLDER,
|
|
100
|
+
CONTAINS_PACKAGE
|
|
101
|
+
|
|
102
|
+
## Cypher Examples (for query_graph)
|
|
103
|
+
```
|
|
104
|
+
MATCH (a)-[r:HTTP_CALLS]->(b) RETURN a.name, b.name, r.url_path, r.confidence LIMIT 20
|
|
105
|
+
MATCH (f:Function) WHERE f.name =~ '.*Handler.*' RETURN f.name, f.file_path
|
|
106
|
+
MATCH (a)-[r:CALLS]->(b) WHERE a.name = 'main' RETURN b.name
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Gotchas
|
|
110
|
+
1. `search_graph(project="<name>", relationship="HTTP_CALLS")` filters nodes by degree — use `query_graph` with Cypher to see actual edges.
|
|
111
|
+
2. `query_graph` has a 100k row ceiling — add a Cypher `LIMIT` for broad queries or use `search_graph` pagination.
|
|
112
|
+
3. `trace_path` needs exact names — use `search_graph(project="<name>", name_pattern="...")` first.
|
|
113
|
+
4. `direction="outbound"` misses cross-service callers — use `direction="both"`.
|
|
114
|
+
5. `search_graph` results default to 50 per page — check `has_more` and use `offset`.
|
package/src/cbmem.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type {
|
|
5
|
+
AgentToolResult,
|
|
6
|
+
ExtensionAPI,
|
|
7
|
+
ExtensionContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { sanitizeTerminalText } from "@narumitw/pi-tui-kit/terminal-text";
|
|
11
|
+
import { renderCodebaseMemoryResult } from "./render-result.js";
|
|
12
|
+
import { type BridgeToolDefinition, TOOL_DEFINITIONS, type ToolName } from "./tool-definitions.js";
|
|
13
|
+
|
|
14
|
+
export { TOOL_NAMES } from "./tool-definitions.js";
|
|
15
|
+
|
|
16
|
+
const BIN = join(homedir(), ".local", "bin", "codebase-memory-mcp");
|
|
17
|
+
const STDERR_MAX_BYTES = 8 * 1024;
|
|
18
|
+
const FORCE_KILL_DELAY_MS = 250;
|
|
19
|
+
|
|
20
|
+
export interface CbmemToolDetails {
|
|
21
|
+
truncated: boolean;
|
|
22
|
+
totalBytes: number;
|
|
23
|
+
totalLines: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class BoundedPrefixCollector {
|
|
27
|
+
readonly maxBytes: number;
|
|
28
|
+
text = "";
|
|
29
|
+
totalBytes = 0;
|
|
30
|
+
newlines = 0;
|
|
31
|
+
endsWithNewline = false;
|
|
32
|
+
truncated = false;
|
|
33
|
+
|
|
34
|
+
constructor(maxBytes: number) {
|
|
35
|
+
this.maxBytes = maxBytes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
append(chunk: string): void {
|
|
39
|
+
const chunkBytes = Buffer.byteLength(chunk, "utf8");
|
|
40
|
+
this.totalBytes += chunkBytes;
|
|
41
|
+
this.newlines += countOccurrences(chunk, "\n");
|
|
42
|
+
if (chunk.length > 0) this.endsWithNewline = chunk.endsWith("\n");
|
|
43
|
+
|
|
44
|
+
const remaining = this.maxBytes - Buffer.byteLength(this.text, "utf8");
|
|
45
|
+
if (remaining <= 0) {
|
|
46
|
+
if (chunkBytes > 0) this.truncated = true;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const kept = takeUtf8Prefix(chunk, remaining, Number.POSITIVE_INFINITY);
|
|
50
|
+
this.text += kept;
|
|
51
|
+
if (kept.length !== chunk.length) this.truncated = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get totalLines(): number {
|
|
55
|
+
if (this.totalBytes === 0) return 0;
|
|
56
|
+
return this.newlines + (this.endsWithNewline ? 0 : 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class BoundedTailCollector {
|
|
61
|
+
readonly maxBytes: number;
|
|
62
|
+
text = "";
|
|
63
|
+
truncated = false;
|
|
64
|
+
|
|
65
|
+
constructor(maxBytes: number) {
|
|
66
|
+
this.maxBytes = maxBytes;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
append(chunk: string): void {
|
|
70
|
+
this.text += chunk;
|
|
71
|
+
if (Buffer.byteLength(this.text, "utf8") <= this.maxBytes) return;
|
|
72
|
+
this.text = takeUtf8Suffix(this.text, this.maxBytes);
|
|
73
|
+
this.truncated = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function callCodebaseMemory(
|
|
78
|
+
tool: ToolName,
|
|
79
|
+
args: Record<string, unknown>,
|
|
80
|
+
signal: AbortSignal | undefined,
|
|
81
|
+
cwd: string,
|
|
82
|
+
binary = BIN,
|
|
83
|
+
): Promise<AgentToolResult<CbmemToolDetails>> {
|
|
84
|
+
signal?.throwIfAborted();
|
|
85
|
+
const input = JSON.stringify(args);
|
|
86
|
+
|
|
87
|
+
return await new Promise((resolve, reject) => {
|
|
88
|
+
const stdout = new BoundedPrefixCollector(DEFAULT_MAX_BYTES);
|
|
89
|
+
const stderr = new BoundedTailCollector(STDERR_MAX_BYTES);
|
|
90
|
+
const child = spawn(binary, ["cli", tool], {
|
|
91
|
+
cwd,
|
|
92
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
93
|
+
env: { ...process.env, CBM_LOG_LEVEL: "error" },
|
|
94
|
+
});
|
|
95
|
+
let settled = false;
|
|
96
|
+
let forceKillTimer: NodeJS.Timeout | undefined;
|
|
97
|
+
|
|
98
|
+
const cleanup = () => {
|
|
99
|
+
signal?.removeEventListener("abort", onAbort);
|
|
100
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
101
|
+
};
|
|
102
|
+
const fail = (error: unknown) => {
|
|
103
|
+
if (settled) return;
|
|
104
|
+
settled = true;
|
|
105
|
+
cleanup();
|
|
106
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
107
|
+
reject(error);
|
|
108
|
+
};
|
|
109
|
+
const succeed = (result: AgentToolResult<CbmemToolDetails>) => {
|
|
110
|
+
if (settled) return;
|
|
111
|
+
settled = true;
|
|
112
|
+
cleanup();
|
|
113
|
+
resolve(result);
|
|
114
|
+
};
|
|
115
|
+
const onAbort = () => {
|
|
116
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
117
|
+
child.kill("SIGTERM");
|
|
118
|
+
forceKillTimer = setTimeout(() => {
|
|
119
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
120
|
+
}, FORCE_KILL_DELAY_MS);
|
|
121
|
+
forceKillTimer.unref();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
125
|
+
if (signal?.aborted) onAbort();
|
|
126
|
+
|
|
127
|
+
child.stdout.setEncoding("utf8");
|
|
128
|
+
child.stderr.setEncoding("utf8");
|
|
129
|
+
child.stdout.on("data", (chunk: string) => stdout.append(chunk));
|
|
130
|
+
child.stderr.on("data", (chunk: string) => stderr.append(chunk));
|
|
131
|
+
child.stdout.on("error", fail);
|
|
132
|
+
child.stderr.on("error", fail);
|
|
133
|
+
child.stdin.on("error", () => {
|
|
134
|
+
// Spawn and exit errors are reported by the child process events below.
|
|
135
|
+
});
|
|
136
|
+
child.on("error", fail);
|
|
137
|
+
child.on("close", (code) => {
|
|
138
|
+
if (settled) return;
|
|
139
|
+
if (signal?.aborted) {
|
|
140
|
+
fail(abortReason(signal));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (code !== 0) {
|
|
144
|
+
fail(cliFailure(tool, code, stderr));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (stdout.truncated) {
|
|
148
|
+
fail(
|
|
149
|
+
new Error(
|
|
150
|
+
`codebase-memory-mcp ${tool} exceeded ${formatSize(DEFAULT_MAX_BYTES)} before a complete JSON response could be validated`,
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const response = extractLastJson(stdout.text);
|
|
158
|
+
const bounded = boundOutput(response, stdout);
|
|
159
|
+
succeed({
|
|
160
|
+
content: [{ type: "text", text: bounded.text }],
|
|
161
|
+
details: {
|
|
162
|
+
truncated: bounded.truncated,
|
|
163
|
+
totalBytes: stdout.totalBytes,
|
|
164
|
+
totalLines: stdout.totalLines,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const diagnostic = stderr.text.trim();
|
|
169
|
+
const suffix = diagnostic ? `: ${diagnostic}` : "";
|
|
170
|
+
fail(
|
|
171
|
+
new Error(`codebase-memory-mcp ${tool} returned no JSON response${suffix}`, {
|
|
172
|
+
cause: error,
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
child.stdin.end(input);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export default function cbmem(pi: ExtensionAPI, binary = BIN): void {
|
|
182
|
+
for (const definition of TOOL_DEFINITIONS) registerBridgeTool(pi, definition, binary);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function registerBridgeTool(
|
|
186
|
+
pi: ExtensionAPI,
|
|
187
|
+
definition: BridgeToolDefinition,
|
|
188
|
+
binary: string,
|
|
189
|
+
): void {
|
|
190
|
+
pi.registerTool({
|
|
191
|
+
...definition,
|
|
192
|
+
renderResult: renderCodebaseMemoryResult,
|
|
193
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
194
|
+
const tool = definition.name as ToolName;
|
|
195
|
+
const args = params as Record<string, unknown>;
|
|
196
|
+
await confirmDestructiveCall(tool, args, signal, ctx);
|
|
197
|
+
return await callCodebaseMemory(tool, args, signal, ctx.cwd, binary);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function confirmDestructiveCall(
|
|
203
|
+
tool: ToolName,
|
|
204
|
+
args: Record<string, unknown>,
|
|
205
|
+
signal: AbortSignal | undefined,
|
|
206
|
+
ctx: ExtensionContext,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const prompt = destructivePrompt(tool, args);
|
|
209
|
+
if (!prompt) return;
|
|
210
|
+
|
|
211
|
+
signal?.throwIfAborted();
|
|
212
|
+
if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Codebase Memory ${tool} requires user confirmation in TUI or RPC mode before it can run.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const confirmed = await ctx.ui.confirm(prompt.title, prompt.message, { signal });
|
|
218
|
+
signal?.throwIfAborted();
|
|
219
|
+
if (!confirmed) {
|
|
220
|
+
throw new DOMException(`Codebase Memory ${tool} was cancelled by the user.`, "AbortError");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function destructivePrompt(
|
|
225
|
+
tool: ToolName,
|
|
226
|
+
args: Record<string, unknown>,
|
|
227
|
+
): { title: string; message: string } | undefined {
|
|
228
|
+
const project = safeArgument(args.project, "unknown project");
|
|
229
|
+
if (tool === "delete_project") {
|
|
230
|
+
return {
|
|
231
|
+
title: "Delete Codebase Memory project?",
|
|
232
|
+
message: `Project: ${project}\nThis permanently removes the project's Codebase Memory index.`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (tool === "manage_adr" && args.mode === "update") {
|
|
236
|
+
const contentBytes =
|
|
237
|
+
typeof args.content === "string" ? Buffer.byteLength(args.content, "utf8") : 0;
|
|
238
|
+
return {
|
|
239
|
+
title: "Replace Codebase Memory ADRs?",
|
|
240
|
+
message: `Project: ${project}\nReplace the complete ADR document with ${contentBytes} bytes of content.`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function safeArgument(value: unknown, fallback: string): string {
|
|
247
|
+
if (typeof value !== "string") return fallback;
|
|
248
|
+
return sanitizeTerminalText(value) || fallback;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function extractLastJson(output: string): string {
|
|
252
|
+
const trimmed = output.trim();
|
|
253
|
+
if (!trimmed) throw new Error("empty stdout");
|
|
254
|
+
try {
|
|
255
|
+
JSON.parse(trimmed);
|
|
256
|
+
return trimmed;
|
|
257
|
+
} catch {
|
|
258
|
+
const lines = trimmed.split("\n");
|
|
259
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
260
|
+
const line = lines[index].trim();
|
|
261
|
+
if (!line) continue;
|
|
262
|
+
try {
|
|
263
|
+
JSON.parse(line);
|
|
264
|
+
return line;
|
|
265
|
+
} catch {
|
|
266
|
+
// Keep scanning for the last complete JSON response.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
throw new Error("stdout did not contain JSON");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function boundOutput(
|
|
274
|
+
text: string,
|
|
275
|
+
collector: BoundedPrefixCollector,
|
|
276
|
+
): { text: string; truncated: boolean } {
|
|
277
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
278
|
+
const textLines = countLines(text);
|
|
279
|
+
const truncated =
|
|
280
|
+
collector.truncated || textBytes > DEFAULT_MAX_BYTES || textLines > DEFAULT_MAX_LINES;
|
|
281
|
+
if (!truncated) return { text, truncated: false };
|
|
282
|
+
|
|
283
|
+
const notice = `[Output truncated: Codebase Memory produced ${collector.totalLines} lines (${formatSize(collector.totalBytes)}); additional output was omitted.]`;
|
|
284
|
+
const separator = "\n";
|
|
285
|
+
const body = takeUtf8Prefix(
|
|
286
|
+
text,
|
|
287
|
+
Math.max(0, DEFAULT_MAX_BYTES - Buffer.byteLength(notice + separator, "utf8")),
|
|
288
|
+
Math.max(0, DEFAULT_MAX_LINES - 1),
|
|
289
|
+
);
|
|
290
|
+
return {
|
|
291
|
+
text: body ? `${body}${body.endsWith("\n") ? "" : separator}${notice}` : notice,
|
|
292
|
+
truncated: true,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function cliFailure(tool: ToolName, code: number | null, stderr: BoundedTailCollector): Error {
|
|
297
|
+
const diagnostic = stderr.text.trim();
|
|
298
|
+
const truncation = stderr.truncated ? "[earlier stderr omitted] " : "";
|
|
299
|
+
const suffix = diagnostic ? `: ${truncation}${diagnostic}` : "";
|
|
300
|
+
return new Error(`codebase-memory-mcp ${tool} exited with code ${code ?? "unknown"}${suffix}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function abortReason(signal: AbortSignal): unknown {
|
|
304
|
+
return signal.reason instanceof Error
|
|
305
|
+
? signal.reason
|
|
306
|
+
: new DOMException("Codebase Memory tool call was aborted", "AbortError");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function takeUtf8Prefix(text: string, maxBytes: number, maxLines: number): string {
|
|
310
|
+
let bytes = 0;
|
|
311
|
+
let lines = text ? 1 : 0;
|
|
312
|
+
let result = "";
|
|
313
|
+
for (const character of text) {
|
|
314
|
+
const nextLines = character === "\n" ? lines + 1 : lines;
|
|
315
|
+
const characterBytes = Buffer.byteLength(character, "utf8");
|
|
316
|
+
if (bytes + characterBytes > maxBytes || nextLines > maxLines) break;
|
|
317
|
+
result += character;
|
|
318
|
+
bytes += characterBytes;
|
|
319
|
+
lines = nextLines;
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function takeUtf8Suffix(text: string, maxBytes: number): string {
|
|
325
|
+
let bytes = 0;
|
|
326
|
+
const characters = Array.from(text);
|
|
327
|
+
let start = characters.length;
|
|
328
|
+
while (start > 0) {
|
|
329
|
+
const characterBytes = Buffer.byteLength(characters[start - 1], "utf8");
|
|
330
|
+
if (bytes + characterBytes > maxBytes) break;
|
|
331
|
+
bytes += characterBytes;
|
|
332
|
+
start--;
|
|
333
|
+
}
|
|
334
|
+
return characters.slice(start).join("");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function countOccurrences(text: string, character: string): number {
|
|
338
|
+
let count = 0;
|
|
339
|
+
for (const value of text) if (value === character) count++;
|
|
340
|
+
return count;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function countLines(text: string): number {
|
|
344
|
+
if (!text) return 0;
|
|
345
|
+
return countOccurrences(text, "\n") + (text.endsWith("\n") ? 0 : 1);
|
|
346
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./cbmem.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AgentToolResult, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { sanitizeTerminalText } from "@narumitw/pi-tui-kit/terminal-text";
|
|
4
|
+
|
|
5
|
+
interface RenderTheme {
|
|
6
|
+
fg(color: "toolOutput" | "warning", text: string): string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function renderCodebaseMemoryResult(
|
|
10
|
+
result: AgentToolResult<unknown>,
|
|
11
|
+
options: ToolRenderResultOptions,
|
|
12
|
+
theme: RenderTheme,
|
|
13
|
+
): Text {
|
|
14
|
+
const rawText = result.content
|
|
15
|
+
.flatMap((content) => (content.type === "text" ? [content.text] : []))
|
|
16
|
+
.join("\n");
|
|
17
|
+
const displayText = sanitizeMultilineTerminalText(rawText);
|
|
18
|
+
const color = options.isPartial ? "warning" : "toolOutput";
|
|
19
|
+
return new Text(theme.fg(color, displayText), 0, 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sanitizeMultilineTerminalText(value: string): string {
|
|
23
|
+
return value
|
|
24
|
+
.replace(/\r\n?/gu, "\n")
|
|
25
|
+
.split("\n")
|
|
26
|
+
.map((line) => sanitizeTerminalText(line))
|
|
27
|
+
.join("\n");
|
|
28
|
+
}
|