memtrace 0.8.9 → 0.8.10

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.
@@ -0,0 +1,259 @@
1
+ // mcp-client.ts — minimal MCP JSON-RPC 2.0 stdio client for `memtrace mcp`.
2
+ //
3
+ // No runtime npm dependencies. Speaks newline-delimited JSON-RPC over the
4
+ // child process stdin/stdout, implements the initialize handshake, discovers
5
+ // tools via tools/list, and invokes them via tools/call. Designed to be
6
+ // driven by the extension in index.ts.
7
+
8
+ import { ChildProcess, spawn } from "node:child_process";
9
+ import { createInterface } from "node:readline";
10
+
11
+ export interface McpToolDef {
12
+ name: string;
13
+ description?: string;
14
+ inputSchema?: Record<string, unknown>;
15
+ /** Raw field preserved for forward compatibility (e.g. annotations, _meta). */
16
+ [k: string]: unknown;
17
+ }
18
+
19
+ export interface McpContentBlock {
20
+ type: string;
21
+ text?: string;
22
+ data?: string;
23
+ mimeType?: string;
24
+ [k: string]: unknown;
25
+ }
26
+
27
+ export interface McpToolResult {
28
+ content: McpContentBlock[];
29
+ isError?: boolean;
30
+ [k: string]: unknown;
31
+ }
32
+
33
+ export interface McpServerInfo {
34
+ name?: string;
35
+ version?: string;
36
+ protocolVersion?: string;
37
+ }
38
+
39
+ interface PendingReq {
40
+ resolve: (v: unknown) => void;
41
+ reject: (e: Error) => void;
42
+ timer: ReturnType<typeof setTimeout>;
43
+ }
44
+
45
+ export interface StartOptions {
46
+ command: string;
47
+ args: string[];
48
+ env?: Record<string, string>;
49
+ cwd?: string;
50
+ requestTimeoutMs?: number;
51
+ debug?: boolean;
52
+ }
53
+
54
+ const PROTOCOL_VERSION = "2025-06-18";
55
+
56
+ export class MemtraceClient {
57
+ private proc: ChildProcess | null = null;
58
+ private nextId = 1;
59
+ private pending = new Map<number, PendingReq>();
60
+ private readline: ReturnType<typeof createInterface> | null = null;
61
+ private stderrBuf: string[] = [];
62
+ private requestTimeoutMs: number;
63
+ private debug: boolean;
64
+ private disposed = false;
65
+ readonly server: McpServerInfo = {};
66
+
67
+ constructor(opts: StartOptions) {
68
+ this.requestTimeoutMs = opts.requestTimeoutMs ?? 60_000;
69
+ this.debug = opts.debug ?? false;
70
+ this.command = opts.command;
71
+ this.args = opts.args;
72
+ this.env = opts.env;
73
+ this.cwd = opts.cwd;
74
+ }
75
+
76
+ readonly command: string;
77
+ readonly args: string[];
78
+ readonly env?: Record<string, string>;
79
+ readonly cwd?: string;
80
+
81
+ get connected(): boolean {
82
+ return !!this.proc && !this.proc.killed && this.proc.exitCode === null;
83
+ }
84
+
85
+ get lastStderr(): string {
86
+ return this.stderrBuf.slice(-2000).join("");
87
+ }
88
+
89
+ /** Spawn the server and run the initialize handshake. Resolves with server info. */
90
+ async start(): Promise<McpServerInfo> {
91
+ if (this.disposed) throw new Error("client disposed");
92
+ if (this.connected) return this.server;
93
+
94
+ const childEnv = { ...process.env, ...(this.env ?? {}) };
95
+ this.proc = spawn(this.command, this.args, {
96
+ stdio: ["pipe", "pipe", "pipe"],
97
+ env: childEnv,
98
+ cwd: this.cwd,
99
+ });
100
+
101
+ this.proc.on("exit", (code, signal) => this.handleExit(code, signal));
102
+ this.proc.on("error", (err) => this.handleExit(null, null, err));
103
+
104
+ if (this.proc.stderr) {
105
+ const rl = createInterface({ input: this.proc.stderr });
106
+ rl.on("line", (line) => {
107
+ this.stderrBuf.push(line);
108
+ if (this.stderrBuf.length > 400) this.stderrBuf.shift();
109
+ if (this.debug) console.error(`[memtrace stderr] ${line}`);
110
+ });
111
+ }
112
+
113
+ if (this.proc.stdout) {
114
+ this.readline = createInterface({ input: this.proc.stdout });
115
+ this.readline.on("line", (line) => this.handleLine(line));
116
+ }
117
+
118
+ if (!this.proc.stdin) throw new Error("memtrace server has no stdin");
119
+
120
+ const initResult = (await this.request("initialize", {
121
+ protocolVersion: PROTOCOL_VERSION,
122
+ capabilities: {},
123
+ clientInfo: { name: "memtrace-pi", version: "0.1.0" },
124
+ })) as { protocolVersion?: string; serverInfo?: { name?: string; version?: string } };
125
+
126
+ this.server.protocolVersion = initResult?.protocolVersion;
127
+ this.server.name = initResult?.serverInfo?.name;
128
+ this.server.version = initResult?.serverInfo?.version;
129
+
130
+ // Tell the server we're ready. Notifications carry no id and expect no reply.
131
+ this.notify("notifications/initialized", {});
132
+ return this.server;
133
+ }
134
+
135
+ /** List tools the server exposes. */
136
+ async listTools(): Promise<McpToolDef[]> {
137
+ const res = (await this.request("tools/list", {})) as { tools?: McpToolDef[] };
138
+ return res.tools ?? [];
139
+ }
140
+
141
+ /** Invoke a tool by name with JSON arguments. */
142
+ async callTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
143
+ const res = (await this.request("tools/call", { name, arguments: args ?? {} })) as McpToolResult;
144
+ return {
145
+ ...res,
146
+ content: Array.isArray(res?.content) ? res.content : [],
147
+ isError: !!res?.isError,
148
+ };
149
+ }
150
+
151
+ /** Stop the server and release resources. Safe to call multiple times. */
152
+ dispose(): void {
153
+ this.disposed = true;
154
+ for (const [, p] of this.pending) {
155
+ clearTimeout(p.timer);
156
+ p.reject(new Error("memtrace client disposed"));
157
+ }
158
+ this.pending.clear();
159
+ try {
160
+ this.readline?.close();
161
+ } catch { /* noop */ }
162
+ if (this.proc) {
163
+ try {
164
+ this.proc.stdin?.end();
165
+ } catch { /* noop */ }
166
+ try {
167
+ this.proc.kill("SIGTERM");
168
+ } catch { /* noop */ }
169
+ const force = setTimeout(() => {
170
+ try { this.proc?.kill("SIGKILL"); } catch { /* noop */ }
171
+ }, 2000);
172
+ this.proc.once("exit", () => clearTimeout(force));
173
+ }
174
+ this.proc = null;
175
+ }
176
+
177
+ // ---- internals -----------------------------------------------------------
178
+
179
+ private handleLine(line: string): void {
180
+ if (!line) return;
181
+ let msg: unknown;
182
+ try {
183
+ msg = JSON.parse(line);
184
+ } catch {
185
+ return; // ignore non-JSON noise on stdout
186
+ }
187
+ const obj = msg as { id?: number; method?: string; result?: unknown; error?: unknown };
188
+ if (typeof obj.id === "number") {
189
+ const p = this.pending.get(obj.id);
190
+ if (!p) return;
191
+ this.pending.delete(obj.id);
192
+ clearTimeout(p.timer);
193
+ if (obj.error) {
194
+ p.reject(toError(obj.error));
195
+ } else {
196
+ p.resolve(obj.result);
197
+ }
198
+ }
199
+ // Notifications from server (no id) are ignored for now.
200
+ }
201
+
202
+ private request(method: string, params: unknown): Promise<unknown> {
203
+ return new Promise((resolveP, rejectP) => {
204
+ if (!this.proc || !this.proc.stdin || !this.connected) {
205
+ rejectP(new Error("memtrace server is not connected"));
206
+ return;
207
+ }
208
+ const id = this.nextId++;
209
+ const timer = setTimeout(() => {
210
+ if (this.pending.delete(id)) {
211
+ rejectP(new Error(`memtrace request "${method}" timed out after ${this.requestTimeoutMs}ms`));
212
+ }
213
+ }, this.requestTimeoutMs);
214
+ this.pending.set(id, { resolve: resolveP, reject: rejectP, timer });
215
+ const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
216
+ this.proc.stdin.write(payload, (err) => {
217
+ if (err) {
218
+ if (this.pending.delete(id)) {
219
+ clearTimeout(timer);
220
+ rejectP(new Error(`failed to write memtrace request: ${err.message}`));
221
+ }
222
+ }
223
+ });
224
+ });
225
+ }
226
+
227
+ private notify(method: string, params: unknown): void {
228
+ if (!this.proc || !this.proc.stdin) return;
229
+ const payload = JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n";
230
+ try {
231
+ this.proc.stdin.write(payload);
232
+ } catch { /* noop */ }
233
+ }
234
+
235
+ private handleExit(code: number | null, signal: NodeJS.Signals | null, err?: Error): void {
236
+ for (const [, p] of this.pending) {
237
+ clearTimeout(p.timer);
238
+ p.reject(
239
+ err
240
+ ? new Error(`memtrace server failed to start: ${err.message}`)
241
+ : new Error(`memtrace server exited (code=${code}, signal=${signal})`),
242
+ );
243
+ }
244
+ this.pending.clear();
245
+ this.proc = null;
246
+ }
247
+ }
248
+
249
+ function toError(rpcError: unknown): Error {
250
+ if (rpcError && typeof rpcError === "object") {
251
+ const e = rpcError as { message?: string; code?: number; data?: unknown };
252
+ const msg = e.message ?? `MCP error ${e.code ?? ""}`.trim();
253
+ const err = new Error(`memtrace: ${msg}`);
254
+ (err as Error & { code?: number; data?: unknown }).code = e.code;
255
+ (err as Error & { code?: number; data?: unknown }).data = e.data;
256
+ return err;
257
+ }
258
+ return new Error(`memtrace: ${String(rpcError)}`);
259
+ }
@@ -0,0 +1,293 @@
1
+ // schema.ts — convert MCP tool inputSchema (JSON Schema) into Typebox schemas
2
+ // for pi.registerTool, and apply curated labels / prompt guidance for the
3
+ // well-known Memtrace tools. The live server schema is always authoritative;
4
+ // overrides only refine presentation and never change the wire contract.
5
+
6
+ import { Type, TSchema } from "typebox";
7
+ import { StringEnum } from "@earendil-works/pi-ai";
8
+
9
+ type JsonSchema = Record<string, unknown> | undefined;
10
+
11
+ /** Convert a JSON Schema fragment into the equivalent Typebox schema. */
12
+ export function jsonSchemaToTypebox(schema: JsonSchema): TSchema {
13
+ if (!schema || typeof schema !== "object") return Type.Any() as unknown as TSchema;
14
+ const s = schema as {
15
+ type?: string | string[];
16
+ enum?: unknown[];
17
+ description?: string;
18
+ default?: unknown;
19
+ items?: JsonSchema;
20
+ properties?: Record<string, JsonSchema>;
21
+ required?: string[];
22
+ anyOf?: JsonSchema[];
23
+ oneOf?: JsonSchema[];
24
+ allOf?: JsonSchema[];
25
+ additionalProperties?: unknown;
26
+ };
27
+
28
+ const opts: Record<string, unknown> = {};
29
+ if (typeof s.description === "string") opts.description = s.description;
30
+ if ("default" in s) opts.default = s.default;
31
+
32
+ if (Array.isArray(s.enum) && s.enum.length > 0) {
33
+ if (s.type === "string" || s.enum.every((v) => typeof v === "string")) {
34
+ const union = StringEnum(s.enum as string[]);
35
+ return withOpts(union, opts) as unknown as TSchema;
36
+ }
37
+ const lit = Type.Union(s.enum.map((v) => Type.Literal(v as string | number | boolean)));
38
+ return withOpts(lit, opts) as unknown as TSchema;
39
+ }
40
+
41
+ if (Array.isArray(s.anyOf)) return Type.Union(s.anyOf.map((x) => jsonSchemaToTypebox(x))) as unknown as TSchema;
42
+ if (Array.isArray(s.oneOf)) return Type.Union(s.oneOf.map((x) => jsonSchemaToTypebox(x))) as unknown as TSchema;
43
+
44
+ const t = Array.isArray(s.type) ? s.type[0] : s.type;
45
+
46
+ switch (t) {
47
+ case "string":
48
+ return withOpts(Type.String(), opts) as unknown as TSchema;
49
+ case "integer":
50
+ return withOpts(Type.Integer(), opts) as unknown as TSchema;
51
+ case "number":
52
+ return withOpts(Type.Number(), opts) as unknown as TSchema;
53
+ case "boolean":
54
+ return withOpts(Type.Boolean(), opts) as unknown as TSchema;
55
+ case "array":
56
+ return withOpts(Type.Array(jsonSchemaToTypebox(s.items)), opts) as unknown as TSchema;
57
+ case "object":
58
+ case undefined:
59
+ if (s.properties || t === "object") return objectSchema(s, opts) as unknown as TSchema;
60
+ return Type.Any() as unknown as TSchema;
61
+ case "null":
62
+ return Type.Null() as unknown as TSchema;
63
+ default:
64
+ return Type.Any() as unknown as TSchema;
65
+ }
66
+ }
67
+
68
+ function objectSchema(
69
+ s: { properties?: Record<string, JsonSchema>; required?: string[] },
70
+ _opts: Record<string, unknown>,
71
+ ) {
72
+ const props: Record<string, TSchema> = {};
73
+ const required = new Set(s.required ?? []);
74
+ for (const [key, value] of Object.entries(s.properties ?? {})) {
75
+ const child = jsonSchemaToTypebox(value);
76
+ props[key] = required.has(key) ? child : (Type.Optional(child) as TSchema);
77
+ }
78
+ return Type.Object(props);
79
+ }
80
+
81
+ function withOpts<T extends TSchema>(schema: T, opts: Record<string, unknown>): T {
82
+ // Typebox schema objects are plain JSON-Schema; attach description/default.
83
+ if (opts.description !== undefined) (schema as unknown as Record<string, unknown>).description = opts.description;
84
+ if ("default" in opts) (schema as unknown as Record<string, unknown>).default = opts.default;
85
+ return schema;
86
+ }
87
+
88
+ // ---- curated presentation overrides ----------------------------------------
89
+ //
90
+ // Keyed by the raw Memtrace tool name. Only label / promptSnippet /
91
+ // promptGuidelines / a friendlier description are set. The parameter schema
92
+ // always comes from the live server so new tools and new params work without
93
+ // a package release.
94
+
95
+ export interface ToolOverride {
96
+ label?: string;
97
+ description?: string;
98
+ promptSnippet?: string;
99
+ promptGuidelines?: string[];
100
+ }
101
+
102
+ export const TOOL_OVERRIDES: Record<string, ToolOverride> = {
103
+ list_indexed_repositories: {
104
+ label: "Memtrace: Repos",
105
+ description:
106
+ "List repositories indexed in the Memtrace code graph. Call once at session start to discover repo_id values. No parameters.",
107
+ promptSnippet: "List repos indexed in Memtrace and get their repo_id values",
108
+ promptGuidelines: [
109
+ "Call memtrace_list_indexed_repositories once at session start to learn which repos are indexed and capture their repo_id values for later calls.",
110
+ ],
111
+ },
112
+ index_directory: {
113
+ label: "Memtrace: Index",
114
+ description:
115
+ "Index a local source directory into the Memtrace knowledge graph. Pass an absolute path; use incremental:true to refresh, clear_existing:true only for a full rebuild.",
116
+ promptSnippet: "Index a repo into Memtrace (returns a job_id)",
117
+ promptGuidelines: [
118
+ "Use memtrace_index_directory when the current repo is not yet indexed or searches miss a source subdirectory. Poll the returned job_id with memtrace_check_job_status (cap ~100 polls / 5 min).",
119
+ ],
120
+ },
121
+ check_job_status: {
122
+ label: "Memtrace: Job Status",
123
+ description: "Poll an indexing job by job_id until status is completed or failed.",
124
+ },
125
+ list_jobs: { label: "Memtrace: Jobs", description: "List Memtrace indexing jobs." },
126
+
127
+ find_symbol: {
128
+ label: "Memtrace: Find Symbol",
129
+ description:
130
+ "Find a symbol by exact or fuzzy identifier name. Returns name, kind, file_path, start_line, end_line, and symbol_id. Prefer this over grep for 'where is X defined'.",
131
+ promptSnippet: "Locate a symbol by name (exact or fuzzy) with file:line",
132
+ promptGuidelines: [
133
+ "Use memtrace_find_symbol (fuzzy:true) instead of grep/glob to locate a function/class/struct by name. Then use memtrace_get_symbol_context for the WHY (callers/callees), not a whole-file read.",
134
+ ],
135
+ },
136
+ find_code: {
137
+ label: "Memtrace: Find Code",
138
+ description:
139
+ "Hybrid BM25 + semantic search over the indexed codebase. Finds symbols, and text inside their bodies (string literals, error messages, log strings). Prefer over grep for behavior and natural-language queries.",
140
+ promptSnippet: "Semantic + keyword search across indexed code (catches strings inside function bodies)",
141
+ promptGuidelines: [
142
+ "Use memtrace_find_code instead of grep for behavior questions and string literals/error messages inside indexed code — the embedding catches text grep would need wildcards for.",
143
+ ],
144
+ },
145
+ get_source_window: {
146
+ label: "Memtrace: Source Window",
147
+ description:
148
+ "Return a bounded source span around a file/line range returned by other Memtrace tools. Use instead of whole-file read when you only need the span Memtrace pointed at.",
149
+ promptSnippet: "Read a bounded source span Memtrace returned (not the whole file)",
150
+ promptGuidelines: [
151
+ "Use memtrace_get_source_window (or read with offset/limit) to view only the start_line..end_line span Memtrace returned. Never whole-file read a source span Memtrace already located.",
152
+ ],
153
+ },
154
+ get_symbol_context: {
155
+ label: "Memtrace: Symbol Context",
156
+ description:
157
+ "Graph neighborhood of a symbol: callers, callees, type_references, community, processes, cross-repo api_callers. The default next step after find_symbol/find_code to answer 'how does X work'.",
158
+ promptSnippet: "Callers, callees, community, and processes for a symbol",
159
+ promptGuidelines: [
160
+ "After a memtrace_find_symbol / memtrace_find_code hit, call memtrace_get_symbol_context to answer 'how does this work / who calls it'. This is the default next step, not a file read.",
161
+ ],
162
+ },
163
+ analyze_relationships: {
164
+ label: "Memtrace: Relationships",
165
+ description:
166
+ "Directed relationship query: find_callers, find_callees, class_hierarchy, overrides, imports, exporters, type_usages.",
167
+ },
168
+
169
+ get_impact: {
170
+ label: "Memtrace: Blast Radius",
171
+ description:
172
+ "Transitive blast radius of modifying a symbol (upstream callers / downstream deps). Use before any non-trivial edit to know what breaks. Pass parameters per the tool's live schema.",
173
+ promptSnippet: "What breaks if I change this symbol (transitive impact + risk)",
174
+ promptGuidelines: [
175
+ "Before modifying an existing symbol, call memtrace_get_impact to see the blast radius. Do not grep for references to estimate impact — Memtrace computes transitive graph impact.",
176
+ ],
177
+ },
178
+ detect_changes: {
179
+ label: "Memtrace: Diff Impact",
180
+ description: "Scope the symbols and processes affected by a git diff or list of changed files.",
181
+ },
182
+
183
+ get_evolution: {
184
+ label: "Memtrace: Change History",
185
+ description:
186
+ "Symbol-level change history over a time window (requires repo_id and from). Modes: recent (per-episode), compound (top files/symbols), summary/overview (totals). Do NOT pass days — use from.",
187
+ promptSnippet: "What changed in a repo over a time window (symbol-level, not git log)",
188
+ promptGuidelines: [
189
+ "Use memtrace_get_evolution (not git log) for 'what changed since <date>'. It requires repo_id and from (e.g. '30d ago'); there is no days parameter.",
190
+ ],
191
+ },
192
+ get_changes_since: {
193
+ label: "Memtrace: Catch-Up",
194
+ description: "Changes since a stored anchor timestamp or episode id. Use to resume work after time away.",
195
+ },
196
+ get_timeline: {
197
+ label: "Memtrace: Symbol Timeline",
198
+ description: "Full history of one symbol across commits and working-tree saves.",
199
+ },
200
+ get_episode_replay: {
201
+ label: "Memtrace: Episode Replay",
202
+ description: "Replay the graph diff of one commit or working-tree save episode.",
203
+ },
204
+ get_cochange_context: {
205
+ label: "Memtrace: Co-Change",
206
+ description: "Files that historically co-change with a target symbol, ranked by co-occurrence.",
207
+ },
208
+
209
+ find_central_symbols: {
210
+ label: "Memtrace: Central Symbols",
211
+ description: "PageRank / degree centrality over call+reference edges — the symbols the codebase depends on most.",
212
+ },
213
+ find_bridge_symbols: {
214
+ label: "Memtrace: Bridge Symbols",
215
+ description: "Chokepoint symbols connecting otherwise-separate communities (single points of failure).",
216
+ },
217
+ list_communities: {
218
+ label: "Memtrace: Communities",
219
+ description: "Louvain communities — how the codebase naturally clusters into logical modules.",
220
+ },
221
+ list_processes: {
222
+ label: "Memtrace: Processes",
223
+ description: "Detected execution flows / entry points (HTTP handlers, CLI commands, jobs, event handlers).",
224
+ },
225
+ get_process_flow: {
226
+ label: "Memtrace: Process Flow",
227
+ description: "Trace a single execution path through the call graph.",
228
+ },
229
+ find_dependency_path: {
230
+ label: "Memtrace: Dependency Path",
231
+ description: "Shortest BFS path between two symbols through the dependency graph.",
232
+ },
233
+
234
+ get_repository_stats: {
235
+ label: "Memtrace: Repo Stats",
236
+ description: "Scale of an indexed repo: symbol counts by kind, relationship counts, communities, processes.",
237
+ },
238
+ find_dead_code: {
239
+ label: "Memtrace: Dead Code",
240
+ description: "Symbols with zero graph reachability (no callers), with kind filters.",
241
+ },
242
+ find_most_complex_functions: {
243
+ label: "Memtrace: Complexity Hotspots",
244
+ description: "Most complex functions by cyclomatic complexity — refactoring candidates.",
245
+ },
246
+ calculate_cyclomatic_complexity: {
247
+ label: "Memtrace: Complexity",
248
+ description: "Cyclomatic complexity of a single symbol.",
249
+ },
250
+
251
+ find_api_endpoints: {
252
+ label: "Memtrace: API Endpoints",
253
+ description: "HTTP route handlers in a repo, with method and path filters.",
254
+ },
255
+ find_api_calls: {
256
+ label: "Memtrace: API Calls",
257
+ description: "Outbound HTTP calls a repo makes, with method and path filters.",
258
+ },
259
+ get_api_topology: {
260
+ label: "Memtrace: API Topology",
261
+ description: "Cross-repo service-to-service HTTP dependency graph.",
262
+ },
263
+ get_service_diagram: {
264
+ label: "Memtrace: Service Diagram",
265
+ description: "Logical service diagram across indexed repos.",
266
+ },
267
+ link_repositories: {
268
+ label: "Memtrace: Link Repos",
269
+ description: "Connect already-indexed repos so cross-repo API topology can stitch their HTTP graphs.",
270
+ },
271
+ delete_repository: {
272
+ label: "Memtrace: Delete Repo",
273
+ description: "Remove a repo and its graph from the index.",
274
+ },
275
+ get_style_fingerprint: {
276
+ label: "Memtrace: Style Fingerprint",
277
+ description:
278
+ "Empirical codebase style norm (ternary vs if-else, arrow vs fn-decl, etc.). Use when choosing between competing idioms.",
279
+ promptGuidelines: [
280
+ "When choosing between competing idioms (ternary vs if-else, arrow vs fn-decl, const vs let, await vs .then), call memtrace_get_style_fingerprint and match the codebase's empirical norm.",
281
+ ],
282
+ },
283
+ };
284
+
285
+ /** Pi tool name to expose for a raw Memtrace tool name. */
286
+ export function piToolName(memtraceName: string): string {
287
+ return `memtrace_${memtraceName}`;
288
+ }
289
+
290
+ /** Raw Memtrace name from a Pi tool name (best-effort). */
291
+ export function memtraceName(piName: string): string {
292
+ return piName.startsWith("memtrace_") ? piName.slice("memtrace_".length) : piName;
293
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "memtrace-pi",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "description": "Bundled Memtrace Pi extension — installed via memtrace install, not published to npm.",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "pi": {
9
+ "extensions": [
10
+ "./extension/index.ts"
11
+ ],
12
+ "skills": [
13
+ "./skills"
14
+ ]
15
+ },
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "peerDependencies": {
20
+ "@earendil-works/pi-ai": "*",
21
+ "@earendil-works/pi-coding-agent": "*",
22
+ "@earendil-works/pi-tui": "*",
23
+ "typebox": "*"
24
+ }
25
+ }