@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
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
|
+
[](#-install) [](https://pi.dev) [](./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
|