@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 narumiruna
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # 🧠 pi-cbmem — Codebase Memory Tools for Pi
2
+
3
+ [![npm: private](https://img.shields.io/badge/npm-private-lightgrey)](#-install) [![Pi extension](https://img.shields.io/badge/Pi-extension-blue)](https://pi.dev) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
4
+
5
+ pi-cbmem is a private package that bundles the local Codebase Memory extension and its knowledge-graph operating skill.
6
+
7
+ ## ✨ Features
8
+
9
+ - Registers all 15 Codebase Memory graph, search, indexing, coverage, trace, and architecture tools.
10
+ - Invokes the local `codebase-memory-mcp` CLI with cancellation, failure reporting, and bounded output.
11
+ - Bundles the `codebase-memory` skill for evidence-tier and graph-first workflows.
12
+ - Keeps the extension and skill available through one package declaration.
13
+
14
+ ## 📦 Install
15
+
16
+ This package is private and is not published to npm.
17
+
18
+ Install it from this repository checkout:
19
+
20
+ ```bash
21
+ pi install ./packages/pi-cbmem
22
+ ```
23
+
24
+ Build and try it without adding a persistent package setting:
25
+
26
+ ```bash
27
+ npm --workspace @narumitw/pi-cbmem run build
28
+ pi --no-extensions -e ./packages/pi-cbmem
29
+ ```
30
+
31
+ The package declares `dist/index.ts`, so an unbuilt local checkout must run the build before Pi loads the package directory.
32
+
33
+ Pi extensions run with your user permissions.
34
+
35
+ Review the source before loading the package.
36
+
37
+ ## 🚀 Quick start
38
+
39
+ Install `codebase-memory-mcp` at `~/.local/bin/codebase-memory-mcp`, load the package, and ask Pi a structural codebase question.
40
+
41
+ The bundled skill tells Pi to check the active graph project before exploration and to verify material claims with graph snippets and index-coverage evidence.
42
+
43
+ ## 🛠️ Tools
44
+
45
+ The extension registers these tools:
46
+
47
+ - `index_repository`
48
+ - `search_graph`
49
+ - `query_graph`
50
+ - `trace_path`
51
+ - `get_code_snippet`
52
+ - `get_graph_schema`
53
+ - `get_architecture`
54
+ - `search_code`
55
+ - `list_projects`
56
+ - `delete_project`
57
+ - `index_status`
58
+ - `check_index_coverage`
59
+ - `detect_changes`
60
+ - `manage_adr`
61
+ - `ingest_traces`
62
+
63
+ Each tool sends JSON arguments to `codebase-memory-mcp cli <tool>` over standard input and returns the last JSON response written to standard output.
64
+
65
+ Validated JSON output over 2,000 lines is truncated with an explicit omission notice.
66
+
67
+ Standard output over 50 KB fails safely because the complete JSON response cannot be validated within the bounded capture.
68
+
69
+ Spawn failures, nonzero exits, oversized output, and missing JSON responses are reported as failed tool calls.
70
+
71
+ ## 🔒 Security and privacy
72
+
73
+ The Codebase Memory binary runs with the Pi process environment and user permissions.
74
+
75
+ Tool calls can read and index repositories, inspect source, persist graph data, manage architecture decisions and traces, or delete indexed projects.
76
+
77
+ Deleting a project or replacing its Architecture Decision Records requires explicit confirmation in TUI or RPC mode and is rejected in non-interactive modes.
78
+
79
+ Repository content returned by graph tools can be sent to the selected model provider as tool output.
80
+
81
+ Terminal controls are removed only from the TUI rendering boundary; the validated raw result remains available to the model.
82
+
83
+ The extension starts one local CLI child process per tool call in the active session directory and does not start background work during extension factory load.
84
+
85
+ Cancelling a tool call terminates its child process.
86
+
87
+ ## 🚧 Limitations
88
+
89
+ The package expects the binary at `~/.local/bin/codebase-memory-mcp` for the current user.
90
+
91
+ It does not install or update the Codebase Memory binary.
92
+
93
+ The extension exposes static tool schemas that match the bundled Codebase Memory skill and delegates final argument validation to the installed CLI.
94
+
95
+ ## 🗂️ Package layout
96
+
97
+ ```text
98
+ packages/pi-cbmem/
99
+ ├── dist/ # Generated source-mapped Jiti runtime
100
+ ├── scripts/
101
+ │ └── build-runtime.mjs # Deterministic runtime builder and validator
102
+ ├── src/
103
+ │ ├── index.ts # Thin Pi entrypoint
104
+ │ ├── cbmem.ts # Bounded Codebase Memory CLI runner
105
+ │ ├── render-result.ts # Terminal-safe result rendering
106
+ │ └── tool-definitions.ts # Pi tool metadata and TypeBox schemas
107
+ ├── skills/codebase-memory/
108
+ │ └── SKILL.md # Graph-first operating guidance
109
+ ├── test/
110
+ │ ├── build-runtime.test.ts # Generated-runtime and Jiti loader coverage
111
+ │ └── cbmem.test.ts # Tool registration coverage
112
+ ├── package.json # Private Pi extension and skill declarations
113
+ ├── tsconfig.json
114
+ ├── README.md
115
+ └── LICENSE
116
+ ```
117
+
118
+ The generated runtime is built from the authoritative `src/index.ts` graph and does not import back into `src`.
119
+
120
+ ## 🔎 Keywords
121
+
122
+ Pi, Codebase Memory, code knowledge graph, MCP, call graph, architecture, impact analysis, source indexing.
123
+
124
+ ## 📄 License
125
+
126
+ MIT.
127
+ See [`LICENSE`](./LICENSE).
package/dist/index.ts ADDED
@@ -0,0 +1,523 @@
1
+ // @generated by scripts/build-runtime.mjs; do not edit.
2
+ // @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
3
+
4
+ // src/cbmem.ts
5
+ import { spawn } from "node:child_process";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { DEFAULT_MAX_BYTES as DEFAULT_MAX_BYTES2, DEFAULT_MAX_LINES as DEFAULT_MAX_LINES2, formatSize as formatSize2 } from "@earendil-works/pi-coding-agent";
9
+ import { sanitizeTerminalText as sanitizeTerminalText2 } from "@narumitw/pi-tui-kit/terminal-text";
10
+
11
+ // src/render-result.ts
12
+ import { Text } from "@earendil-works/pi-tui";
13
+ import { sanitizeTerminalText } from "@narumitw/pi-tui-kit/terminal-text";
14
+ function renderCodebaseMemoryResult(result, options, theme) {
15
+ const rawText = result.content.flatMap((content) => content.type === "text" ? [content.text] : []).join("\n");
16
+ const displayText = sanitizeMultilineTerminalText(rawText);
17
+ const color = options.isPartial ? "warning" : "toolOutput";
18
+ return new Text(theme.fg(color, displayText), 0, 0);
19
+ }
20
+ function sanitizeMultilineTerminalText(value) {
21
+ return value.replace(/\r\n?/gu, "\n").split("\n").map((line) => sanitizeTerminalText(line)).join("\n");
22
+ }
23
+
24
+ // src/tool-definitions.ts
25
+ import { StringEnum } from "@earendil-works/pi-ai";
26
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "@earendil-works/pi-coding-agent";
27
+ import { Type } from "typebox";
28
+ var Project = Type.String({ description: "Indexed project name from list_projects." });
29
+ var outputLimit = `Output is limited to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)}; byte-oversized responses fail validation instead of returning partial JSON.`;
30
+ var TOOL_DEFINITIONS = [
31
+ {
32
+ name: "index_repository",
33
+ label: "Index Repository",
34
+ description: `Index a repository in the Codebase Memory graph. ${outputLimit}`,
35
+ parameters: Type.Object({
36
+ repo_path: Type.String({ description: "Path to the repository." }),
37
+ mode: Type.Optional(
38
+ StringEnum(["full", "moderate", "fast", "cross-repo-intelligence"])
39
+ ),
40
+ target_projects: Type.Optional(Type.Array(Type.String())),
41
+ name: Type.Optional(Type.String({ description: "Override the derived project name." })),
42
+ persistence: Type.Optional(Type.Boolean())
43
+ })
44
+ },
45
+ {
46
+ name: "search_graph",
47
+ label: "Search Graph",
48
+ description: `Search indexed symbols by text, name, file, relationship, or degree. ${outputLimit}`,
49
+ parameters: Type.Object({
50
+ project: Project,
51
+ query: Type.Optional(Type.String({ description: "Natural-language or keyword search." })),
52
+ label: Type.Optional(Type.String()),
53
+ name_pattern: Type.Optional(Type.String()),
54
+ qn_pattern: Type.Optional(Type.String()),
55
+ file_pattern: Type.Optional(Type.String()),
56
+ relationship: Type.Optional(Type.String()),
57
+ min_degree: Type.Optional(Type.Integer()),
58
+ max_degree: Type.Optional(Type.Integer()),
59
+ exclude_entry_points: Type.Optional(Type.Boolean()),
60
+ include_connected: Type.Optional(Type.Boolean()),
61
+ semantic_query: Type.Optional(Type.Array(Type.String())),
62
+ limit: Type.Optional(Type.Integer()),
63
+ offset: Type.Optional(Type.Integer()),
64
+ format: Type.Optional(StringEnum(["tree", "json"])),
65
+ fields: Type.Optional(Type.Array(Type.String())),
66
+ detail: Type.Optional(StringEnum(["ids", "default"]))
67
+ })
68
+ },
69
+ {
70
+ name: "query_graph",
71
+ label: "Query Graph",
72
+ description: `Execute a Cypher query against the code or missed-coverage graph. ${outputLimit}`,
73
+ parameters: Type.Object({
74
+ query: Type.String({ description: "Cypher query." }),
75
+ project: Project,
76
+ graph: Type.Optional(StringEnum(["code", "missed"])),
77
+ max_rows: Type.Optional(Type.Integer())
78
+ })
79
+ },
80
+ {
81
+ name: "trace_path",
82
+ label: "Trace Path",
83
+ description: `Trace callers, callees, data flow, or cross-service paths. ${outputLimit}`,
84
+ parameters: Type.Object({
85
+ function_name: Type.String(),
86
+ project: Project,
87
+ direction: Type.Optional(StringEnum(["inbound", "outbound", "both"])),
88
+ depth: Type.Optional(Type.Integer()),
89
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5e3 })),
90
+ cursor: Type.Optional(Type.String()),
91
+ mode: Type.Optional(StringEnum(["calls", "data_flow", "cross_service"])),
92
+ parameter_name: Type.Optional(Type.String()),
93
+ edge_types: Type.Optional(Type.Array(Type.String())),
94
+ risk_labels: Type.Optional(Type.Boolean()),
95
+ include_tests: Type.Optional(Type.Boolean()),
96
+ format: Type.Optional(StringEnum(["tree", "json"])),
97
+ include_evidence: Type.Optional(Type.Boolean())
98
+ })
99
+ },
100
+ {
101
+ name: "get_code_snippet",
102
+ label: "Get Code Snippet",
103
+ description: `Read source for an indexed symbol by qualified name. ${outputLimit}`,
104
+ parameters: Type.Object({
105
+ qualified_name: Type.String(),
106
+ project: Project,
107
+ include_neighbors: Type.Optional(Type.Boolean())
108
+ })
109
+ },
110
+ {
111
+ name: "get_graph_schema",
112
+ label: "Get Graph Schema",
113
+ description: `Get graph node labels and edge types. ${outputLimit}`,
114
+ parameters: Type.Object({ project: Project })
115
+ },
116
+ {
117
+ name: "get_architecture",
118
+ label: "Get Architecture",
119
+ description: `Summarize architecture, dependencies, boundaries, clusters, or hotspots. ${outputLimit}`,
120
+ parameters: Type.Object({
121
+ project: Project,
122
+ path: Type.Optional(Type.String()),
123
+ aspects: Type.Optional(
124
+ Type.Array(
125
+ StringEnum([
126
+ "all",
127
+ "overview",
128
+ "structure",
129
+ "dependencies",
130
+ "routes",
131
+ "languages",
132
+ "packages",
133
+ "entry_points",
134
+ "hotspots",
135
+ "boundaries",
136
+ "layers",
137
+ "file_tree",
138
+ "clusters",
139
+ "cycles"
140
+ ])
141
+ )
142
+ )
143
+ })
144
+ },
145
+ {
146
+ name: "search_code",
147
+ label: "Search Code",
148
+ description: `Search source text and enrich matches with graph context. ${outputLimit}`,
149
+ parameters: Type.Object({
150
+ pattern: Type.String(),
151
+ project: Project,
152
+ file_pattern: Type.Optional(Type.String()),
153
+ path_filter: Type.Optional(Type.String()),
154
+ mode: Type.Optional(StringEnum(["compact", "full", "files"])),
155
+ context: Type.Optional(Type.Integer()),
156
+ regex: Type.Optional(Type.Boolean()),
157
+ debug: Type.Optional(Type.Boolean()),
158
+ limit: Type.Optional(Type.Integer({ minimum: 1 }))
159
+ })
160
+ },
161
+ {
162
+ name: "list_projects",
163
+ label: "List Projects",
164
+ description: `List indexed Codebase Memory projects. ${outputLimit}`,
165
+ parameters: Type.Object({
166
+ offset: Type.Optional(Type.Integer({ minimum: 0 })),
167
+ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
168
+ include_details: Type.Optional(Type.Boolean()),
169
+ metadata_only: Type.Optional(Type.Boolean())
170
+ })
171
+ },
172
+ {
173
+ name: "delete_project",
174
+ label: "Delete Project",
175
+ description: `Delete a project from the Codebase Memory index. ${outputLimit}`,
176
+ parameters: Type.Object({ project: Project })
177
+ },
178
+ {
179
+ name: "index_status",
180
+ label: "Index Status",
181
+ description: `Get project index health, Git context, and coverage gaps. ${outputLimit}`,
182
+ parameters: Type.Object({
183
+ project: Project,
184
+ verbose: Type.Optional(Type.Boolean())
185
+ })
186
+ },
187
+ {
188
+ name: "check_index_coverage",
189
+ label: "Check Index Coverage",
190
+ description: `Check best-effort index coverage for exact paths or bounded scopes. ${outputLimit}`,
191
+ parameters: Type.Object({
192
+ project: Project,
193
+ paths: Type.Optional(Type.Array(Type.String(), { maxItems: 128 })),
194
+ scopes: Type.Optional(Type.Array(Type.String(), { maxItems: 32 })),
195
+ scope_limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1e3 })),
196
+ scope_offset: Type.Optional(Type.Integer({ minimum: 0 }))
197
+ })
198
+ },
199
+ {
200
+ name: "detect_changes",
201
+ label: "Detect Changes",
202
+ description: `Map a Git diff to changed files and impacted graph symbols. ${outputLimit}`,
203
+ parameters: Type.Object({
204
+ project: Project,
205
+ scope: Type.Optional(StringEnum(["files", "impact"])),
206
+ direction: Type.Optional(StringEnum(["inbound", "outbound", "both"])),
207
+ depth: Type.Optional(Type.Integer()),
208
+ limit: Type.Optional(Type.Integer({ maximum: 5e3 })),
209
+ base_branch: Type.Optional(Type.String()),
210
+ since: Type.Optional(Type.String()),
211
+ format: Type.Optional(StringEnum(["tree", "json"]))
212
+ })
213
+ },
214
+ {
215
+ name: "manage_adr",
216
+ label: "Manage ADR",
217
+ description: `Read or replace Architecture Decision Records. ${outputLimit}`,
218
+ parameters: Type.Object(
219
+ {
220
+ project: Project,
221
+ mode: Type.Optional(StringEnum(["get", "update", "sections"])),
222
+ content: Type.Optional(Type.String())
223
+ },
224
+ { additionalProperties: false }
225
+ )
226
+ },
227
+ {
228
+ name: "ingest_traces",
229
+ label: "Ingest Traces",
230
+ description: `Ingest runtime caller-to-callee traces into the graph. ${outputLimit}`,
231
+ parameters: Type.Object({
232
+ traces: Type.Array(
233
+ Type.Object(
234
+ {
235
+ caller: Type.Optional(Type.String()),
236
+ callee: Type.Optional(Type.String()),
237
+ count: Type.Optional(Type.Integer())
238
+ },
239
+ { additionalProperties: false }
240
+ )
241
+ ),
242
+ project: Project
243
+ })
244
+ }
245
+ ];
246
+ var TOOL_NAMES = TOOL_DEFINITIONS.map(({ name }) => name);
247
+
248
+ // src/cbmem.ts
249
+ var BIN = join(homedir(), ".local", "bin", "codebase-memory-mcp");
250
+ var STDERR_MAX_BYTES = 8 * 1024;
251
+ var FORCE_KILL_DELAY_MS = 250;
252
+ var BoundedPrefixCollector = class {
253
+ maxBytes;
254
+ text = "";
255
+ totalBytes = 0;
256
+ newlines = 0;
257
+ endsWithNewline = false;
258
+ truncated = false;
259
+ constructor(maxBytes) {
260
+ this.maxBytes = maxBytes;
261
+ }
262
+ append(chunk) {
263
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
264
+ this.totalBytes += chunkBytes;
265
+ this.newlines += countOccurrences(chunk, "\n");
266
+ if (chunk.length > 0) this.endsWithNewline = chunk.endsWith("\n");
267
+ const remaining = this.maxBytes - Buffer.byteLength(this.text, "utf8");
268
+ if (remaining <= 0) {
269
+ if (chunkBytes > 0) this.truncated = true;
270
+ return;
271
+ }
272
+ const kept = takeUtf8Prefix(chunk, remaining, Number.POSITIVE_INFINITY);
273
+ this.text += kept;
274
+ if (kept.length !== chunk.length) this.truncated = true;
275
+ }
276
+ get totalLines() {
277
+ if (this.totalBytes === 0) return 0;
278
+ return this.newlines + (this.endsWithNewline ? 0 : 1);
279
+ }
280
+ };
281
+ var BoundedTailCollector = class {
282
+ maxBytes;
283
+ text = "";
284
+ truncated = false;
285
+ constructor(maxBytes) {
286
+ this.maxBytes = maxBytes;
287
+ }
288
+ append(chunk) {
289
+ this.text += chunk;
290
+ if (Buffer.byteLength(this.text, "utf8") <= this.maxBytes) return;
291
+ this.text = takeUtf8Suffix(this.text, this.maxBytes);
292
+ this.truncated = true;
293
+ }
294
+ };
295
+ async function callCodebaseMemory(tool, args, signal, cwd, binary = BIN) {
296
+ signal?.throwIfAborted();
297
+ const input = JSON.stringify(args);
298
+ return await new Promise((resolve, reject) => {
299
+ const stdout = new BoundedPrefixCollector(DEFAULT_MAX_BYTES2);
300
+ const stderr = new BoundedTailCollector(STDERR_MAX_BYTES);
301
+ const child = spawn(binary, ["cli", tool], {
302
+ cwd,
303
+ stdio: ["pipe", "pipe", "pipe"],
304
+ env: { ...process.env, CBM_LOG_LEVEL: "error" }
305
+ });
306
+ let settled = false;
307
+ let forceKillTimer;
308
+ const cleanup = () => {
309
+ signal?.removeEventListener("abort", onAbort);
310
+ if (forceKillTimer) clearTimeout(forceKillTimer);
311
+ };
312
+ const fail = (error) => {
313
+ if (settled) return;
314
+ settled = true;
315
+ cleanup();
316
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
317
+ reject(error);
318
+ };
319
+ const succeed = (result) => {
320
+ if (settled) return;
321
+ settled = true;
322
+ cleanup();
323
+ resolve(result);
324
+ };
325
+ const onAbort = () => {
326
+ if (child.exitCode !== null || child.signalCode !== null) return;
327
+ child.kill("SIGTERM");
328
+ forceKillTimer = setTimeout(() => {
329
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
330
+ }, FORCE_KILL_DELAY_MS);
331
+ forceKillTimer.unref();
332
+ };
333
+ signal?.addEventListener("abort", onAbort, { once: true });
334
+ if (signal?.aborted) onAbort();
335
+ child.stdout.setEncoding("utf8");
336
+ child.stderr.setEncoding("utf8");
337
+ child.stdout.on("data", (chunk) => stdout.append(chunk));
338
+ child.stderr.on("data", (chunk) => stderr.append(chunk));
339
+ child.stdout.on("error", fail);
340
+ child.stderr.on("error", fail);
341
+ child.stdin.on("error", () => {
342
+ });
343
+ child.on("error", fail);
344
+ child.on("close", (code) => {
345
+ if (settled) return;
346
+ if (signal?.aborted) {
347
+ fail(abortReason(signal));
348
+ return;
349
+ }
350
+ if (code !== 0) {
351
+ fail(cliFailure(tool, code, stderr));
352
+ return;
353
+ }
354
+ if (stdout.truncated) {
355
+ fail(
356
+ new Error(
357
+ `codebase-memory-mcp ${tool} exceeded ${formatSize2(DEFAULT_MAX_BYTES2)} before a complete JSON response could be validated`
358
+ )
359
+ );
360
+ return;
361
+ }
362
+ try {
363
+ const response = extractLastJson(stdout.text);
364
+ const bounded = boundOutput(response, stdout);
365
+ succeed({
366
+ content: [{ type: "text", text: bounded.text }],
367
+ details: {
368
+ truncated: bounded.truncated,
369
+ totalBytes: stdout.totalBytes,
370
+ totalLines: stdout.totalLines
371
+ }
372
+ });
373
+ } catch (error) {
374
+ const diagnostic = stderr.text.trim();
375
+ const suffix = diagnostic ? `: ${diagnostic}` : "";
376
+ fail(
377
+ new Error(`codebase-memory-mcp ${tool} returned no JSON response${suffix}`, {
378
+ cause: error
379
+ })
380
+ );
381
+ }
382
+ });
383
+ child.stdin.end(input);
384
+ });
385
+ }
386
+ function cbmem(pi, binary = BIN) {
387
+ for (const definition of TOOL_DEFINITIONS) registerBridgeTool(pi, definition, binary);
388
+ }
389
+ function registerBridgeTool(pi, definition, binary) {
390
+ pi.registerTool({
391
+ ...definition,
392
+ renderResult: renderCodebaseMemoryResult,
393
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
394
+ const tool = definition.name;
395
+ const args = params;
396
+ await confirmDestructiveCall(tool, args, signal, ctx);
397
+ return await callCodebaseMemory(tool, args, signal, ctx.cwd, binary);
398
+ }
399
+ });
400
+ }
401
+ async function confirmDestructiveCall(tool, args, signal, ctx) {
402
+ const prompt = destructivePrompt(tool, args);
403
+ if (!prompt) return;
404
+ signal?.throwIfAborted();
405
+ if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") {
406
+ throw new Error(
407
+ `Codebase Memory ${tool} requires user confirmation in TUI or RPC mode before it can run.`
408
+ );
409
+ }
410
+ const confirmed = await ctx.ui.confirm(prompt.title, prompt.message, { signal });
411
+ signal?.throwIfAborted();
412
+ if (!confirmed) {
413
+ throw new DOMException(`Codebase Memory ${tool} was cancelled by the user.`, "AbortError");
414
+ }
415
+ }
416
+ function destructivePrompt(tool, args) {
417
+ const project = safeArgument(args.project, "unknown project");
418
+ if (tool === "delete_project") {
419
+ return {
420
+ title: "Delete Codebase Memory project?",
421
+ message: `Project: ${project}
422
+ This permanently removes the project's Codebase Memory index.`
423
+ };
424
+ }
425
+ if (tool === "manage_adr" && args.mode === "update") {
426
+ const contentBytes = typeof args.content === "string" ? Buffer.byteLength(args.content, "utf8") : 0;
427
+ return {
428
+ title: "Replace Codebase Memory ADRs?",
429
+ message: `Project: ${project}
430
+ Replace the complete ADR document with ${contentBytes} bytes of content.`
431
+ };
432
+ }
433
+ return void 0;
434
+ }
435
+ function safeArgument(value, fallback) {
436
+ if (typeof value !== "string") return fallback;
437
+ return sanitizeTerminalText2(value) || fallback;
438
+ }
439
+ function extractLastJson(output) {
440
+ const trimmed = output.trim();
441
+ if (!trimmed) throw new Error("empty stdout");
442
+ try {
443
+ JSON.parse(trimmed);
444
+ return trimmed;
445
+ } catch {
446
+ const lines = trimmed.split("\n");
447
+ for (let index = lines.length - 1; index >= 0; index--) {
448
+ const line = lines[index].trim();
449
+ if (!line) continue;
450
+ try {
451
+ JSON.parse(line);
452
+ return line;
453
+ } catch {
454
+ }
455
+ }
456
+ throw new Error("stdout did not contain JSON");
457
+ }
458
+ }
459
+ function boundOutput(text, collector) {
460
+ const textBytes = Buffer.byteLength(text, "utf8");
461
+ const textLines = countLines(text);
462
+ const truncated = collector.truncated || textBytes > DEFAULT_MAX_BYTES2 || textLines > DEFAULT_MAX_LINES2;
463
+ if (!truncated) return { text, truncated: false };
464
+ const notice = `[Output truncated: Codebase Memory produced ${collector.totalLines} lines (${formatSize2(collector.totalBytes)}); additional output was omitted.]`;
465
+ const separator = "\n";
466
+ const body = takeUtf8Prefix(
467
+ text,
468
+ Math.max(0, DEFAULT_MAX_BYTES2 - Buffer.byteLength(notice + separator, "utf8")),
469
+ Math.max(0, DEFAULT_MAX_LINES2 - 1)
470
+ );
471
+ return {
472
+ text: body ? `${body}${body.endsWith("\n") ? "" : separator}${notice}` : notice,
473
+ truncated: true
474
+ };
475
+ }
476
+ function cliFailure(tool, code, stderr) {
477
+ const diagnostic = stderr.text.trim();
478
+ const truncation = stderr.truncated ? "[earlier stderr omitted] " : "";
479
+ const suffix = diagnostic ? `: ${truncation}${diagnostic}` : "";
480
+ return new Error(`codebase-memory-mcp ${tool} exited with code ${code ?? "unknown"}${suffix}`);
481
+ }
482
+ function abortReason(signal) {
483
+ return signal.reason instanceof Error ? signal.reason : new DOMException("Codebase Memory tool call was aborted", "AbortError");
484
+ }
485
+ function takeUtf8Prefix(text, maxBytes, maxLines) {
486
+ let bytes = 0;
487
+ let lines = text ? 1 : 0;
488
+ let result = "";
489
+ for (const character of text) {
490
+ const nextLines = character === "\n" ? lines + 1 : lines;
491
+ const characterBytes = Buffer.byteLength(character, "utf8");
492
+ if (bytes + characterBytes > maxBytes || nextLines > maxLines) break;
493
+ result += character;
494
+ bytes += characterBytes;
495
+ lines = nextLines;
496
+ }
497
+ return result;
498
+ }
499
+ function takeUtf8Suffix(text, maxBytes) {
500
+ let bytes = 0;
501
+ const characters = Array.from(text);
502
+ let start = characters.length;
503
+ while (start > 0) {
504
+ const characterBytes = Buffer.byteLength(characters[start - 1], "utf8");
505
+ if (bytes + characterBytes > maxBytes) break;
506
+ bytes += characterBytes;
507
+ start--;
508
+ }
509
+ return characters.slice(start).join("");
510
+ }
511
+ function countOccurrences(text, character) {
512
+ let count = 0;
513
+ for (const value of text) if (value === character) count++;
514
+ return count;
515
+ }
516
+ function countLines(text) {
517
+ if (!text) return 0;
518
+ return countOccurrences(text, "\n") + (text.endsWith("\n") ? 0 : 1);
519
+ }
520
+ export {
521
+ cbmem as default
522
+ };
523
+ //# sourceMappingURL=index.ts.map