decorated-pi 0.5.4 → 0.6.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.
Files changed (68) hide show
  1. package/.codegraph/daemon.pid +6 -0
  2. package/AGENTS.md +92 -0
  3. package/README.md +53 -64
  4. package/commands/dp-model.ts +23 -0
  5. package/commands/dp-settings.ts +23 -0
  6. package/commands/mcp-status.ts +62 -0
  7. package/commands/retry.ts +19 -0
  8. package/commands/usage.ts +544 -0
  9. package/hooks/compaction.ts +204 -0
  10. package/hooks/externalize.ts +70 -0
  11. package/hooks/image-vision.ts +132 -0
  12. package/hooks/inject-agents-md.ts +164 -0
  13. package/hooks/mcp.ts +340 -0
  14. package/hooks/normalize-codeblocks.ts +88 -0
  15. package/hooks/pi-tool-filter.ts +28 -0
  16. package/{extensions/safety → hooks/redact}/types.ts +1 -8
  17. package/hooks/redact.ts +104 -0
  18. package/{extensions → hooks}/rtk.ts +92 -115
  19. package/hooks/session-title.ts +54 -0
  20. package/hooks/skeleton.ts +212 -0
  21. package/hooks/smart-at.ts +318 -0
  22. package/{extensions/file-times.ts → hooks/track-mtime.ts} +40 -38
  23. package/{extensions → hooks}/wakatime.ts +120 -122
  24. package/index.ts +155 -1
  25. package/package.json +5 -4
  26. package/{extensions/settings.ts → settings.ts} +32 -0
  27. package/{extensions → tools}/lsp/client.ts +0 -25
  28. package/{extensions → tools}/lsp/format.ts +2 -99
  29. package/{extensions → tools}/lsp/index.ts +1 -1
  30. package/{extensions → tools}/lsp/servers.ts +1 -1
  31. package/{extensions → tools}/lsp/tools.ts +1 -66
  32. package/{extensions → tools}/lsp/types.ts +0 -11
  33. package/tools/mcp/builtin/codegraph.ts +45 -0
  34. package/tools/mcp/builtin/context7.ts +10 -0
  35. package/tools/mcp/builtin/exa.ts +10 -0
  36. package/tools/mcp/builtin/index.ts +19 -0
  37. package/tools/mcp/cache.ts +124 -0
  38. package/{extensions → tools}/mcp/client.ts +2 -2
  39. package/{extensions/mcp/builtin.ts → tools/mcp/config.ts} +21 -181
  40. package/tools/mcp/index.ts +44 -0
  41. package/tools/mcp/tool-definition.ts +112 -0
  42. package/{extensions/patch.ts → tools/patch/core.ts} +336 -65
  43. package/{extensions/io.ts → tools/patch/index.ts} +29 -205
  44. package/tsconfig.json +10 -1
  45. package/ui/mcp-status.ts +202 -0
  46. package/ui/model-picker.ts +162 -0
  47. package/ui/module-settings.ts +83 -0
  48. package/ui/usage.ts +396 -0
  49. package/extensions/index.ts +0 -154
  50. package/extensions/io-tool-output.ts +0 -33
  51. package/extensions/mcp/index.ts +0 -435
  52. package/extensions/model-integration.ts +0 -531
  53. package/extensions/providers/ark-coding.ts +0 -75
  54. package/extensions/providers/index.ts +0 -9
  55. package/extensions/providers/ollama-cloud.ts +0 -101
  56. package/extensions/providers/qianfan-coding.ts +0 -71
  57. package/extensions/safety/index.ts +0 -102
  58. package/extensions/session-title.ts +0 -40
  59. package/extensions/slash.ts +0 -458
  60. package/extensions/smart-at.ts +0 -481
  61. package/extensions/subdir-agents.ts +0 -151
  62. /package/{extensions/safety → hooks/redact}/detect.ts +0 -0
  63. /package/{extensions/safety → hooks/redact}/entropy.ts +0 -0
  64. /package/{extensions/safety → hooks/redact}/patterns.ts +0 -0
  65. /package/{extensions → tools}/lsp/env.ts +0 -0
  66. /package/{extensions → tools}/lsp/manager.ts +0 -0
  67. /package/{extensions → tools}/lsp/prompt.ts +0 -0
  68. /package/{extensions → tools}/lsp/protocol.ts +0 -0
@@ -8,7 +8,6 @@ import {
8
8
  } from "./protocol.js";
9
9
  import type {
10
10
  LspDiagnostic,
11
- LspDocumentSymbol,
12
11
  LspHover,
13
12
  LspLocation,
14
13
  LspPosition,
@@ -199,14 +198,6 @@ export class LspClient {
199
198
  );
200
199
  }
201
200
 
202
- async documentSymbols(uri: string, timeoutMs?: number): Promise<LspDocumentSymbol[]> {
203
- return normalizeDocumentSymbols(
204
- await this.#request("textDocument/documentSymbol", {
205
- textDocument: { uri },
206
- }, timeoutMs),
207
- );
208
- }
209
-
210
201
  async rename(
211
202
  uri: string,
212
203
  position: LspPosition,
@@ -260,22 +251,6 @@ function normalizeLocations(result: unknown): LspLocation[] {
260
251
  });
261
252
  }
262
253
 
263
- function normalizeDocumentSymbols(result: unknown): LspDocumentSymbol[] {
264
- if (!result || !Array.isArray(result) || result.length === 0) return [];
265
- const first = result[0];
266
- if ("range" in first && "selectionRange" in first) {
267
- return result as LspDocumentSymbol[];
268
- }
269
- return result.map((entry: any) => ({
270
- name: entry.name,
271
- kind: entry.kind,
272
- range: entry.location.range,
273
- selectionRange: entry.location.range,
274
- containerName: entry.containerName,
275
- uri: entry.location.uri,
276
- }));
277
- }
278
-
279
254
  function uriToPath(uri: string): string {
280
255
  try {
281
256
  return uri.startsWith("file:") ? new URL(uri).pathname : uri;
@@ -5,10 +5,10 @@
5
5
  * Runtime tools.ts has its own inline copies.
6
6
  */
7
7
  import { fileURLToPath } from "node:url";
8
- import type { LspDiagnostic, LspHover, LspLocation, LspDocumentSymbol } from "./types.js";
8
+ import type { LspDiagnostic, LspHover, LspLocation } from "./types.js";
9
9
  import { LspClientStartError } from "./client.js";
10
10
 
11
- export type { LspDiagnostic, LspHover, LspLocation, LspDocumentSymbol } from "./types.js";
11
+ export type { LspDiagnostic, LspHover, LspLocation } from "./types.js";
12
12
  export { LspClientStartError } from "./client.js";
13
13
 
14
14
  // ─── Error formatting ────────────────────────────────────────────────────
@@ -112,103 +112,6 @@ function fileUrlToPath(uri: string): string {
112
112
  try { return uri.startsWith("file:") ? fileURLToPath(uri) : uri; } catch { return uri; }
113
113
  }
114
114
 
115
- // ─── Symbols ──────────────────────────────────────────────────────────────
116
-
117
- const SYMBOL_KIND_LABELS: Record<number, string> = {
118
- 2: "module", 3: "namespace", 5: "class", 6: "method", 7: "property",
119
- 8: "field", 9: "constructor", 11: "interface", 12: "function",
120
- 13: "variable", 14: "constant", 23: "struct", 24: "event",
121
- };
122
-
123
- export function symbol_kind_label(kind: number): string {
124
- return SYMBOL_KIND_LABELS[kind] ?? "symbol";
125
- }
126
-
127
- export function format_document_symbols(file: string, symbols: LspDocumentSymbol[]): string {
128
- if (symbols.length === 0) return `${file}: no symbols`;
129
- const lines = [`${file}: ${symbols.length} top-level symbol(s)`];
130
- appendSymbols(lines, symbols, 1);
131
- return lines.join("\n");
132
- }
133
-
134
- function appendSymbols(lines: string[], symbols: LspDocumentSymbol[], depth: number) {
135
- for (const s of symbols) {
136
- const indent = " ".repeat(depth);
137
- const detail = s.detail ? ` — ${s.detail}` : "";
138
- const range = `${s.range.start.line + 1}:${s.range.start.character + 1}`;
139
- lines.push(`${indent}${symbol_kind_label(s.kind)} ${s.name}${detail} @ ${range}`);
140
- if (s.children?.length) appendSymbols(lines, s.children, depth + 1);
141
- }
142
- }
143
-
144
- interface SymbolMatchOptions {
145
- max_results: number;
146
- top_level_only: boolean;
147
- exact_match: boolean;
148
- kinds: Set<string>;
149
- language: string;
150
- }
151
-
152
- export interface SymbolMatch { symbol: LspDocumentSymbol; depth: number }
153
-
154
- export function find_symbol_matches(
155
- symbols: LspDocumentSymbol[],
156
- query: string,
157
- options: SymbolMatchOptions,
158
- ): SymbolMatch[] {
159
- const normalized = query.trim().toLowerCase();
160
- if (!normalized) return [];
161
- const matches: SymbolMatch[] = [];
162
- const expandName = (name: string): string[] => {
163
- const trimmed = name.trim().toLowerCase();
164
- if (!trimmed) return [];
165
- const expanded = new Set([trimmed]);
166
- if (options.language === "cpp" && trimmed.includes("::")) {
167
- const parts = trimmed.split("::").map(p => p.trim()).filter(Boolean);
168
- if (parts.length > 0) expanded.add(parts[parts.length - 1]!);
169
- }
170
- return [...expanded];
171
- };
172
-
173
- const matchesQuery = (s: LspDocumentSymbol): boolean => {
174
- const name = s.name.trim().toLowerCase();
175
- const detail = (s.detail ?? "").trim().toLowerCase();
176
- if (options.exact_match) {
177
- const exactValues = [...expandName(s.name), ...(detail ? [detail] : [])];
178
- return exactValues.some(v => v === normalized);
179
- }
180
- return name.includes(normalized) || detail.includes(normalized);
181
- };
182
- const matchesKind = (s: LspDocumentSymbol): boolean =>
183
- options.kinds.size === 0 || options.kinds.has(symbol_kind_label(s.kind));
184
- const visit = (entries: LspDocumentSymbol[], depth: number) => {
185
- for (const symbol of entries) {
186
- if (matchesKind(symbol) && matchesQuery(symbol)) {
187
- matches.push({ symbol, depth });
188
- if (matches.length >= options.max_results) return;
189
- }
190
- if (!options.top_level_only && symbol.children?.length) {
191
- visit(symbol.children, depth + 1);
192
- if (matches.length >= options.max_results) return;
193
- }
194
- }
195
- };
196
- visit(symbols, 1);
197
- return matches;
198
- }
199
-
200
- export function format_symbol_matches(file: string, query: string, matches: SymbolMatch[]): string {
201
- if (matches.length === 0) return `${file}: no symbols matching "${query}"`;
202
- const lines = [`${file}: ${matches.length} symbol match(es) for "${query}"`];
203
- for (const { symbol, depth } of matches) {
204
- const indent = " ".repeat(depth);
205
- const detail = symbol.detail ? ` — ${symbol.detail}` : "";
206
- const range = `${symbol.range.start.line + 1}:${symbol.range.start.character + 1}`;
207
- lines.push(`${indent}${symbol_kind_label(symbol.kind)} ${symbol.name}${detail} @ ${range}`);
208
- }
209
- return lines.join("\n");
210
- }
211
-
212
115
  // ─── Collapse (for tools test) ────────────────────────────────────────────
213
116
 
214
117
  export function collapse_lsp_text(text: string, maxLines = 20) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * LSP Extension — language server integration for Pi.
3
3
  *
4
- * Provides: lsp_diagnostics, lsp_document_symbols.
4
+ * Provides: lsp_diagnostics.
5
5
  */
6
6
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
7
  import { LspServerManager } from "./manager.js";
@@ -4,7 +4,7 @@
4
4
  import { existsSync, readdirSync, type Dirent } from "node:fs";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { dirname, extname, isAbsolute, join, resolve } from "node:path";
7
- import type { DependencyStatus } from "../rtk";
7
+ import type { DependencyStatus } from "../../hooks/skeleton.js";
8
8
 
9
9
  // ─── File extension → language mapping ────────────────────────────────────
10
10
 
@@ -4,7 +4,7 @@
4
4
  import { keyHint, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  import { Type } from "typebox";
7
- import { LspServerManager, LspToolError, formatToolError } from "./manager.js";
7
+ import { LspServerManager, formatToolError } from "./manager.js";
8
8
 
9
9
  // ─── TUI rendering ─────────────────────────────────────────────────────────
10
10
 
@@ -53,24 +53,6 @@ function err(details: any): ToolResult {
53
53
  return ok(formatToolError(details), { ok: false, error: details });
54
54
  }
55
55
 
56
- async function withFile(
57
- manager: LspServerManager,
58
- file: string,
59
- fn: (s: { abs: string; uri: string; state: any }) => Promise<string>,
60
- options: { timeoutMs?: number } = {},
61
- ): Promise<ToolResult> {
62
- const resolved = await manager.resolveFileState(file, { timeoutMs: options.timeoutMs });
63
- if (!resolved.ok) return err(resolved.error);
64
- const { result } = resolved;
65
- try {
66
- const text = await fn(result);
67
- return ok(text, { ok: true, language: result.state.language, workspace_root: result.state.workspaceRoot });
68
- } catch (error) {
69
- if (error instanceof LspToolError) return err(error.details);
70
- return err({ kind: "tool_execution_failed", file: result.abs, language: result.state.language, workspace_root: result.state.workspaceRoot, command: result.state.command, install_hint: result.state.installHint, message: error instanceof Error ? error.message : String(error) });
71
- }
72
- }
73
-
74
56
  function withTimeout(promise: Promise<ToolResult>, ms: number, label: string): Promise<ToolResult> {
75
57
  return Promise.race([
76
58
  promise,
@@ -84,25 +66,6 @@ function severityLabel(s: number): string {
84
66
  return s === 1 ? "error" : s === 2 ? "warning" : s === 3 ? "info" : "hint";
85
67
  }
86
68
 
87
- function symbolKindLabel(kind: number): string {
88
- const labels: Record<number, string> = { 2:"module",3:"namespace",5:"class",6:"method",7:"property",8:"field",9:"constructor",11:"interface",12:"function",13:"variable",14:"constant",23:"struct",24:"event" };
89
- return labels[kind] ?? "symbol";
90
- }
91
-
92
- function formatDocumentSymbols(file: string, symbols: any[]): string {
93
- if (symbols.length === 0) return `${file}: no symbols`;
94
- const lines = [`${file}: ${symbols.length} top-level symbol(s)`];
95
- const append = (syms: any[], depth: number) => {
96
- for (const s of syms) {
97
- const detail = s.detail ? ` — ${s.detail}` : "";
98
- lines.push(`${" ".repeat(depth)}${symbolKindLabel(s.kind)} ${s.name}${detail} @ ${s.range.start.line + 1}:${s.range.start.character + 1}`);
99
- if (s.children?.length) append(s.children, depth + 1);
100
- }
101
- };
102
- append(symbols, 1);
103
- return lines.join("\n");
104
- }
105
-
106
69
  // ─── Register tools ───────────────────────────────────────────────────────
107
70
 
108
71
  export function registerLspTools(pi: ExtensionAPI, manager: LspServerManager) {
@@ -156,34 +119,6 @@ export function registerLspTools(pi: ExtensionAPI, manager: LspServerManager) {
156
119
  })(), totalTimeout, "LSP diagnostics");
157
120
  },
158
121
  });
159
-
160
- // ── lsp_document_symbols ───────────────────────────────────────────────
161
- pi.registerTool({
162
- name: "lsp_document_symbols",
163
- label: "LSP: document symbols",
164
- description: "List symbols in a file (functions, classes, variables) using the language server.",
165
- promptSnippet: "List functions, classes, and variables in a file",
166
- promptGuidelines: [
167
- "Prefer lsp_document_symbols to find functions, classes, or variables in code instead of grep.",
168
- "Supported languages: typescript, c, cpp, python, rust, go, ruby, java, lua, svelte.",
169
- ],
170
- renderResult: renderLspResult,
171
- parameters: Type.Object({
172
- path: Type.String({ description: "Path to the file (relative or absolute)." }),
173
- timeout_ms: Type.Optional(Type.Number({ description: "Overall max ms including server startup. Default 10000." })),
174
- }),
175
- execute: async (_id, params, _signal, _update, _ctx): Promise<ToolResult> => {
176
- const timeoutMs = params.timeout_ms ?? 10_000;
177
- return withTimeout(
178
- withFile(manager, params.path, async (result) => {
179
- const symbols = await result.state.client.documentSymbols(result.uri, timeoutMs);
180
- return formatDocumentSymbols(result.abs, symbols);
181
- }, { timeoutMs }),
182
- timeoutMs,
183
- "LSP document symbols",
184
- );
185
- },
186
- });
187
122
  }
188
123
 
189
124
  // ─── Formatting ────────────────────────────────────────────────────────────
@@ -29,14 +29,3 @@ export interface LspHover {
29
29
  contents: unknown;
30
30
  range?: LspRange;
31
31
  }
32
-
33
- export interface LspDocumentSymbol {
34
- name: string;
35
- kind: number;
36
- range: LspRange;
37
- selectionRange?: LspRange;
38
- containerName?: string;
39
- detail?: string;
40
- children?: LspDocumentSymbol[];
41
- uri?: string;
42
- }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * codegraph builtin MCP server — config + system-prompt guidance.
3
+ *
4
+ * Injected by `index.ts` → `buildGuidelines()` when the codegraph
5
+ * module is enabled (see `isModuleEnabled("codegraph")` in settings).
6
+ */
7
+ import type { McpServerConfig } from "../config.js";
8
+ import { isCodegraphModuleEnabled } from "../../../settings.js";
9
+
10
+ export const CODEGRAPH_BUILTIN: Omit<McpServerConfig, "source"> = {
11
+ name: "codegraph",
12
+ command: "codegraph",
13
+ args: ["serve", "--mcp"],
14
+ enabled: false, // overridden by isCodegraphModuleEnabled() at resolve time
15
+ description:
16
+ "Local code knowledge graph (colbymchenry/codegraph). Enable via /dp-settings.",
17
+ };
18
+
19
+ /** Predicate for `resolveMcpConfigs` to gate the codegraph server. */
20
+ export function codegraphEnabled(): boolean {
21
+ return isCodegraphModuleEnabled();
22
+ }
23
+
24
+ export const CODEGRAPH_GUIDANCE = [
25
+ "### CodeGraph, code source map",
26
+ "- This project's `codegraph_*` MCP tools are enabled. The graph is a pre-built index; grep/glob/Read of source code is repeating work the index already did.",
27
+ "",
28
+ "#### When to reach for it",
29
+ '- Starting any task that touches code → `codegraph_explore("how does X work")` or `codegraph_files`',
30
+ "- Looking for where a symbol is defined → `codegraph_search <name>`",
31
+ "- Reading a function's body → `codegraph_node <name>` (or `codegraph_explore`)",
32
+ "- Tracing call flow → `codegraph_callers` / `codegraph_callees`",
33
+ "- Assessing refactor risk → `codegraph_impact <name>`",
34
+ "",
35
+ "#### Do NOT do this",
36
+ "- `ls`, `find`, `grep -rn`, `rg` to discover symbols → use `codegraph_search`",
37
+ "- `read` of an entire file to find a function → use `codegraph_explore` first",
38
+ '- Reading 3+ files to understand a module → use `codegraph_explore("how does X work")`',
39
+ "- `bash` with `cat`, `head`, `sed` to view source → use `codegraph_node` or `read` (single file only)",
40
+ "",
41
+ "#### If it errors",
42
+ '- "Project not initialized" → ask the user to run `codegraph init -i` in their terminal',
43
+ "- Empty results → fall back to grep/Read (the index is best-effort, not authoritative)",
44
+ "- Tool timeout → `codegraph_status` to check; if indexer is dead, fall back",
45
+ ].join("\n");
@@ -0,0 +1,10 @@
1
+ /**
2
+ * context7 builtin MCP server definition.
3
+ */
4
+ import type { McpServerConfig } from "../config.js";
5
+
6
+ export const CONTEXT7_BUILTIN: Omit<McpServerConfig, "source"> = {
7
+ name: "context7",
8
+ url: "https://mcp.context7.com/mcp",
9
+ enabled: true,
10
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * exa builtin MCP server definition.
3
+ */
4
+ import type { McpServerConfig } from "../config.js";
5
+
6
+ export const EXA_BUILTIN: Omit<McpServerConfig, "source"> = {
7
+ name: "exa",
8
+ url: "https://mcp.exa.ai/mcp",
9
+ enabled: true,
10
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Builtin MCP servers — aggregate. Each server has its own file; this
3
+ * module re-exports them and the all-server list.
4
+ */
5
+ import type { McpServerConfig } from "../config.js";
6
+ import { CONTEXT7_BUILTIN } from "./context7.js";
7
+ import { EXA_BUILTIN } from "./exa.js";
8
+ import { CODEGRAPH_BUILTIN, CODEGRAPH_GUIDANCE } from "./codegraph.js";
9
+
10
+ export { CONTEXT7_BUILTIN } from "./context7.js";
11
+ export { EXA_BUILTIN } from "./exa.js";
12
+ export { CODEGRAPH_BUILTIN, CODEGRAPH_GUIDANCE, codegraphEnabled } from "./codegraph.js";
13
+
14
+ /** All builtin servers — flat list for `resolveMcpConfigs` to merge. */
15
+ export const BUILTIN_MCP_SERVERS: Omit<McpServerConfig, "source">[] = [
16
+ CONTEXT7_BUILTIN,
17
+ EXA_BUILTIN,
18
+ CODEGRAPH_BUILTIN,
19
+ ];
@@ -0,0 +1,124 @@
1
+ /**
2
+ * MCP server metadata cache — persisted tool descriptions and schemas.
3
+ * Stored at `~/.pi/agent/mcp-cache.json` (global) and `<cwd>/.pi/agent/mcp-cache.json` (project).
4
+ */
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ import type { McpServerConfig } from "./config.js";
9
+
10
+ export interface McpToolCache {
11
+ name: string;
12
+ description?: string;
13
+ inputSchema: Record<string, unknown>;
14
+ }
15
+
16
+ export interface McpServerCache {
17
+ description?: string;
18
+ tools: McpToolCache[];
19
+ cachedAt: number;
20
+ }
21
+
22
+ export interface McpCache {
23
+ servers: Record<string, McpServerCache>;
24
+ }
25
+
26
+ function globalCachePath(): string {
27
+ return path.join(os.homedir(), ".pi/agent/mcp-cache.json");
28
+ }
29
+
30
+ function projectCachePath(cwd: string): string {
31
+ return path.join(cwd, ".pi/agent/mcp-cache.json");
32
+ }
33
+
34
+ function scopedCachePath(scope: "global" | "project", cwd?: string): string {
35
+ return scope === "project" && cwd ? projectCachePath(cwd) : globalCachePath();
36
+ }
37
+
38
+ function readCacheFile(p: string): McpCache | null {
39
+ try {
40
+ if (!fs.existsSync(p)) return null;
41
+ return JSON.parse(fs.readFileSync(p, "utf-8")) as McpCache;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function writeCacheFile(p: string, cache: McpCache): void {
48
+ const dir = path.dirname(p);
49
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
50
+ const tmp = `${p}.tmp`;
51
+ fs.writeFileSync(tmp, JSON.stringify(cache, null, 2), "utf-8");
52
+ fs.renameSync(tmp, p);
53
+ }
54
+
55
+ /** Load merged cache: global + project. */
56
+ export function loadMcpCache(cwd?: string): McpCache | null {
57
+ const merged: McpCache = { servers: {} };
58
+
59
+ const globalCache = readCacheFile(globalCachePath());
60
+ if (globalCache) {
61
+ merged.servers = { ...merged.servers, ...globalCache.servers };
62
+ }
63
+
64
+ if (cwd) {
65
+ const projectCache = readCacheFile(projectCachePath(cwd));
66
+ if (projectCache) {
67
+ merged.servers = { ...merged.servers, ...projectCache.servers };
68
+ }
69
+ }
70
+
71
+ return merged;
72
+ }
73
+
74
+ export function loadScopedMcpCache(scope: "global" | "project", cwd?: string): McpCache | null {
75
+ return readCacheFile(scopedCachePath(scope, cwd));
76
+ }
77
+
78
+ /** Save cache to global or project scope. */
79
+ export function saveMcpCache(cache: McpCache, scope: "global" | "project", cwd?: string): void {
80
+ writeCacheFile(scopedCachePath(scope, cwd), cache);
81
+ }
82
+
83
+ /** Update a single server's entry in the appropriate cache. */
84
+ export function updateServerCache(
85
+ serverName: string,
86
+ entry: McpServerCache,
87
+ scope: "global" | "project",
88
+ cwd?: string,
89
+ ): void {
90
+ const p = scopedCachePath(scope, cwd);
91
+ const existing = readCacheFile(p) || { servers: {} };
92
+ existing.servers[serverName] = entry;
93
+ writeCacheFile(p, existing);
94
+ }
95
+
96
+ function cleanupOneCache(p: string, names: Set<string>): void {
97
+ const cache = readCacheFile(p);
98
+ if (!cache) return;
99
+ let changed = false;
100
+ for (const name of Object.keys(cache.servers)) {
101
+ if (!names.has(name)) {
102
+ delete cache.servers[name];
103
+ changed = true;
104
+ }
105
+ }
106
+ if (changed) writeCacheFile(p, cache);
107
+ }
108
+
109
+ export function cleanupStaleCache(configs: McpServerConfig[], cwd?: string): void {
110
+ const names = new Set(configs.map(c => c.name));
111
+ cleanupOneCache(globalCachePath(), names);
112
+ if (cwd) {
113
+ const projectCache = projectCachePath(cwd);
114
+ const projectMcpJson = path.join(cwd, ".pi/agent/mcp.json");
115
+ // If project mcp.json doesn't exist, remove project cache entirely
116
+ if (!fs.existsSync(projectMcpJson)) {
117
+ if (fs.existsSync(projectCache)) {
118
+ fs.unlinkSync(projectCache);
119
+ }
120
+ } else {
121
+ cleanupOneCache(projectCache, names);
122
+ }
123
+ }
124
+ }
@@ -2,8 +2,8 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
3
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4
4
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
- import type { McpServerConfig } from "./builtin.js";
6
- import { isSseUrl } from "./builtin.js";
5
+ import type { McpServerConfig } from "./config.js";
6
+ import { isSseUrl } from "./config.js";
7
7
 
8
8
  export interface McpToolSpec {
9
9
  name: string;