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
@@ -1,12 +1,22 @@
1
1
  /**
2
- * MCP server configuration — builtin + global + project-level.
2
+ * MCP server configuration — type, discovery, resolution, toggle.
3
+ *
4
+ * Sources (in priority order): builtin → global → project. The first
5
+ * hit sets defaults; later sources can override specific fields.
6
+ *
7
+ * Persistence locations:
8
+ * - global: `~/.pi/agent/decorated-pi.json` (under `mcpServers`)
9
+ * - project: `<cwd>/.pi/agent/mcp.json` (under `mcpServers`)
3
10
  */
4
11
  import * as fs from "node:fs";
5
- import * as path from "node:path";
6
12
  import * as os from "node:os";
13
+ import * as path from "node:path";
7
14
  import { spawnSync } from "node:child_process";
8
- import { loadConfig } from "../settings.js";
9
- import type { DependencyStatus } from "../rtk";
15
+ import { loadConfig } from "../../settings.js";
16
+ import type { DependencyStatus } from "../../hooks/skeleton.js";
17
+ import { BUILTIN_MCP_SERVERS, codegraphEnabled } from "./builtin/index.js";
18
+
19
+ export { BUILTIN_MCP_SERVERS } from "./builtin/index.js";
10
20
 
11
21
  export interface McpServerConfig {
12
22
  name: string;
@@ -19,22 +29,6 @@ export interface McpServerConfig {
19
29
  source: "builtin" | "global" | "project";
20
30
  }
21
31
 
22
- /** Builtin servers — zero-config, always available unless overridden. */
23
- export const BUILTIN_MCP_SERVERS: Omit<McpServerConfig, "source">[] = [
24
- {
25
- name: "context7",
26
- url: "https://mcp.context7.com/mcp",
27
- enabled: true,
28
- },
29
- {
30
- name: "exa",
31
- url: "https://mcp.exa.ai/mcp",
32
- enabled: true,
33
- },
34
- ];
35
-
36
- // ── Project-level config discovery ─────────────────────────────────────────
37
-
38
32
  function readMcpJson(filePath: string): Record<string, { url?: string; command?: string; args?: string[]; env?: Record<string, string>; enabled?: boolean; description?: string }> | null {
39
33
  try {
40
34
  const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
@@ -104,9 +98,14 @@ export function isSseUrl(url: string): boolean {
104
98
  export function resolveMcpConfigs(cwd: string): McpServerConfig[] {
105
99
  const byName = new Map<string, McpServerConfig>();
106
100
 
107
- // Builtin (lowest priority)
101
+ // Builtin (lowest priority). The codegraph entry's `enabled` flag is
102
+ // computed at resolve time from the dp-settings module toggle.
108
103
  for (const s of BUILTIN_MCP_SERVERS) {
109
- byName.set(s.name, { ...s, source: "builtin" });
104
+ if (s.name === "codegraph") {
105
+ byName.set(s.name, { ...s, enabled: codegraphEnabled(), source: "builtin" });
106
+ } else {
107
+ byName.set(s.name, { ...s, source: "builtin" });
108
+ }
110
109
  }
111
110
 
112
111
  // Global — preserve url/command/description from builtin if not overridden
@@ -176,100 +175,6 @@ function commandExists(command: string): boolean {
176
175
  return result.status === 0;
177
176
  }
178
177
 
179
- /** Write auto-generated description back to project mcp.json. */
180
- export function saveProjectMcpDescription(cwd: string, name: string, description: string): void {
181
- const filePath = path.join(cwd, ".pi/agent/mcp.json");
182
- const servers = readMcpJson(filePath);
183
- if (!servers || !servers[name]) return;
184
- servers[name] = { ...servers[name], description };
185
- const dir = path.dirname(filePath);
186
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
187
- fs.writeFileSync(filePath, JSON.stringify({ mcpServers: servers }, null, 2) + "\n", "utf-8");
188
- }
189
-
190
- // ── Metadata cache (tool descriptions + schemas) ──────────────────────────
191
-
192
- export interface McpToolCache {
193
- name: string;
194
- description?: string;
195
- inputSchema: Record<string, unknown>;
196
- }
197
-
198
- export interface McpServerCache {
199
- description?: string;
200
- tools: McpToolCache[];
201
- cachedAt: number;
202
- }
203
-
204
- export interface McpCache {
205
- servers: Record<string, McpServerCache>;
206
- }
207
-
208
- function globalCachePath(): string {
209
- return path.join(os.homedir(), ".pi/agent/mcp-cache.json");
210
- }
211
-
212
- function projectCachePath(cwd: string): string {
213
- return path.join(cwd, ".pi/agent/mcp-cache.json");
214
- }
215
-
216
- function readCacheFile(p: string): McpCache | null {
217
- try {
218
- if (!fs.existsSync(p)) return null;
219
- return JSON.parse(fs.readFileSync(p, "utf-8")) as McpCache;
220
- } catch {
221
- return null;
222
- }
223
- }
224
-
225
- function writeCacheFile(p: string, cache: McpCache): void {
226
- const dir = path.dirname(p);
227
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
228
- const tmp = `${p}.tmp`;
229
- fs.writeFileSync(tmp, JSON.stringify(cache, null, 2), "utf-8");
230
- fs.renameSync(tmp, p);
231
- }
232
-
233
- /** Load merged cache: global + project. */
234
- export function loadMcpCache(cwd?: string): McpCache | null {
235
- const merged: McpCache = { servers: {} };
236
-
237
- const globalCache = readCacheFile(globalCachePath());
238
- if (globalCache) {
239
- merged.servers = { ...merged.servers, ...globalCache.servers };
240
- }
241
-
242
- if (cwd) {
243
- const projectCache = readCacheFile(projectCachePath(cwd));
244
- if (projectCache) {
245
- merged.servers = { ...merged.servers, ...projectCache.servers };
246
- }
247
- }
248
-
249
- return merged;
250
- }
251
-
252
- /** Save cache to global or project scope. */
253
- export function saveMcpCache(cache: McpCache, scope: "global" | "project", cwd?: string): void {
254
- const p = scope === "project" && cwd ? projectCachePath(cwd) : globalCachePath();
255
- writeCacheFile(p, cache);
256
- }
257
-
258
- /** Update a single server's entry in the appropriate cache. */
259
- export function updateServerCache(
260
- serverName: string,
261
- entry: McpServerCache,
262
- scope: "global" | "project",
263
- cwd?: string,
264
- ): void {
265
- const p = scope === "project" && cwd ? projectCachePath(cwd) : globalCachePath();
266
- const existing = readCacheFile(p) || { servers: {} };
267
- existing.servers[serverName] = entry;
268
- writeCacheFile(p, existing);
269
- }
270
-
271
- // ── Enable / Disable helpers ────────────────────────────────────────────
272
-
273
178
  function readMcpJsonSafe(filePath: string): Record<string, any> | null {
274
179
  try {
275
180
  return JSON.parse(fs.readFileSync(filePath, "utf-8"));
@@ -296,7 +201,6 @@ export function toggleMcpServerEnabled(
296
201
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
297
202
  fs.writeFileSync(filePath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
298
203
  } else {
299
- const { loadConfig } = require("../settings.js");
300
204
  const config = loadConfig();
301
205
  config.mcpServers = config.mcpServers || {};
302
206
  config.mcpServers[serverName] = { ...(config.mcpServers[serverName] || {}), enabled };
@@ -310,67 +214,3 @@ export function toggleMcpServerEnabled(
310
214
  return false;
311
215
  }
312
216
  }
313
-
314
- function cleanupOneCache(p: string, names: Set<string>): void {
315
- const cache = readCacheFile(p);
316
- if (!cache) return;
317
- let changed = false;
318
- for (const name of Object.keys(cache.servers)) {
319
- if (!names.has(name)) {
320
- delete cache.servers[name];
321
- changed = true;
322
- }
323
- }
324
- if (changed) writeCacheFile(p, cache);
325
- }
326
-
327
- export function cleanupStaleCache(configs: McpServerConfig[], cwd?: string): void {
328
- const names = new Set(configs.map(c => c.name));
329
- cleanupOneCache(globalCachePath(), names);
330
- if (cwd) {
331
- const projectCache = projectCachePath(cwd);
332
- const projectMcpJson = path.join(cwd, ".pi/agent/mcp.json");
333
- // If project mcp.json doesn't exist, remove project cache entirely
334
- if (!fs.existsSync(projectMcpJson)) {
335
- if (fs.existsSync(projectCache)) {
336
- fs.unlinkSync(projectCache);
337
- }
338
- } else {
339
- cleanupOneCache(projectCache, names);
340
- }
341
- }
342
- }
343
-
344
- // ── Large result externalization ────────────────────────────────────────
345
-
346
- export const EXTERNALIZE_THRESHOLD = 50_000; // 50KB
347
- export const EXTERNALIZE_PREVIEW_SIZE = 2_000; // 2KB preview
348
- import { writeOutputToTemp } from "../io-tool-output.js";
349
-
350
- /**
351
- * If content exceeds threshold, save to temp file and return preview + path.
352
- * Otherwise return original content unchanged.
353
- * Falls back to returning full text if file write fails.
354
- */
355
- export function maybeExternalizeMcpResult(
356
- text: string,
357
- toolName: string,
358
- toolCallId: string,
359
- ): { content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> } {
360
- if (text.length <= EXTERNALIZE_THRESHOLD) {
361
- return { content: [{ type: "text", text }], details: {} };
362
- }
363
-
364
- const filePath = writeOutputToTemp(toolName, toolCallId, text);
365
- if (!filePath) {
366
- return { content: [{ type: "text", text }], details: {} };
367
- }
368
- const preview = text.slice(0, EXTERNALIZE_PREVIEW_SIZE);
369
- return {
370
- content: [{ type: "text", text: `${preview}\n\n[Truncated: ${text.length.toLocaleString()} chars total. Full output saved to: ${filePath}]` }],
371
- details: {
372
- fullOutputPath: filePath,
373
- truncation: { truncated: true, outputChars: EXTERNALIZE_PREVIEW_SIZE, totalChars: text.length, maxBytes: EXTERNALIZE_THRESHOLD },
374
- },
375
- };
376
- }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * mcp tool registration — reads cache, registers tools dynamically.
3
+ * Connection lifecycle is in hooks/mcp.ts.
4
+ */
5
+
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import { loadMcpCache, type McpCache } from "./cache.js";
8
+ import { resolveMcpConfigs, type McpServerConfig } from "./config.js";
9
+ import { buildMcpTool, __mcpToolDefinitionTest } from "./tool-definition.js";
10
+ import { getActiveMcpConnections } from "../../hooks/mcp.js";
11
+
12
+ export function registerMcpToolsFromCache(pi: ExtensionAPI, cache: McpCache, configs: McpServerConfig[]): void {
13
+ for (const config of configs) {
14
+ if (!config.enabled) continue;
15
+ const entry = cache.servers[config.name];
16
+ if (!entry || entry.tools.length === 0) continue;
17
+ for (const t of entry.tools) {
18
+ try {
19
+ pi.registerTool(buildMcpTool(config, t, (name) => getActiveMcpConnections().find(c => c.serverName === name)) as any);
20
+ } catch {
21
+ // pi-core may throw on duplicate name (e.g. on /reload when the
22
+ // same tool is re-registered). The previous registration is
23
+ // still in effect, so silently ignore.
24
+ }
25
+ }
26
+ }
27
+ }
28
+
29
+ export function registerMcpTools(pi: ExtensionAPI, cwd: string): void {
30
+ const cache = loadMcpCache(cwd);
31
+ if (!cache) return;
32
+ const configs = resolveMcpConfigs(cwd).filter(s => s.enabled);
33
+ registerMcpToolsFromCache(pi, cache, configs);
34
+ }
35
+
36
+ // Re-exports
37
+ export { McpConnection } from "./client.js";
38
+ export { buildMcpTool } from "./tool-definition.js";
39
+ export { getActiveMcpConnections, getCachedMcpConfigs, getMcpStatus, updateConfigEnabled, refreshServerCache } from "../../hooks/mcp.js";
40
+ export type { McpServerConfig } from "./config.js";
41
+ export type { McpCache } from "./cache.js";
42
+
43
+ // Test exports
44
+ export const __mcpIndexTest = __mcpToolDefinitionTest;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Pure MCP tool-definition helpers.
3
+ *
4
+ * No hook/module state lives here. Callers provide `findConnection`
5
+ * so both hooks/mcp.ts and tools/mcp/index.ts can share the same tool
6
+ * factory without importing each other.
7
+ */
8
+
9
+ import { keyHint } from "@earendil-works/pi-coding-agent";
10
+ import { Text } from "@earendil-works/pi-tui";
11
+ import type { McpConnection } from "./client.js";
12
+ import type { McpServerConfig } from "./config.js";
13
+
14
+ const MCP_RESULT_FOLD_LINES = 45;
15
+
16
+ function trimTrailingEmptyLines(lines: string[]): string[] {
17
+ let end = lines.length;
18
+ while (end > 0 && lines[end - 1] === "") end -= 1;
19
+ return lines.slice(0, end);
20
+ }
21
+
22
+ function collapseMcpText(text: string, maxLines = MCP_RESULT_FOLD_LINES) {
23
+ const lines = trimTrailingEmptyLines(text.split("\n"));
24
+ return {
25
+ totalLines: lines.length,
26
+ displayLines: lines.slice(0, maxLines),
27
+ remainingLines: Math.max(0, lines.length - maxLines),
28
+ };
29
+ }
30
+
31
+ function getTextContent(result: { content?: Array<{ type: string; text?: string }> }): string {
32
+ return (result.content ?? [])
33
+ .filter((c): c is { type: "text"; text?: string } => c.type === "text")
34
+ .map((c) => c.text ?? "")
35
+ .join("\n");
36
+ }
37
+
38
+ function formatMcpResultText(text: string, expanded: boolean, theme: any): string {
39
+ const { totalLines, displayLines, remainingLines } = collapseMcpText(text, expanded ? Number.MAX_SAFE_INTEGER : MCP_RESULT_FOLD_LINES);
40
+ const lastLine = displayLines[displayLines.length - 1] || "";
41
+ let outputLines = [...displayLines];
42
+ let truncationMsg = "";
43
+ if (lastLine.startsWith("[Truncated: ") && lastLine.endsWith("]")) {
44
+ truncationMsg = lastLine;
45
+ outputLines = outputLines.slice(0, -1);
46
+ }
47
+ const outputText = outputLines.join("\n");
48
+ let rendered = outputText ? theme.fg("toolOutput", outputText) : "";
49
+ if (truncationMsg) rendered += (rendered ? "\n" : "") + theme.fg("warning", truncationMsg);
50
+ if (!expanded && remainingLines > 0) {
51
+ rendered += `${theme.fg("muted", `\n... (${remainingLines} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")})`;
52
+ }
53
+ return rendered;
54
+ }
55
+
56
+ function renderMcpResult(result: any, options: { expanded: boolean }, theme: any, context: any) {
57
+ const component = context.lastComponent ?? new Text("", 0, 0);
58
+ component.setText(formatMcpResultText(getTextContent(result), options.expanded, theme));
59
+ return component;
60
+ }
61
+
62
+ function makeToolName(serverName: string, toolName: string): string {
63
+ return `${serverName}_${toolName}`;
64
+ }
65
+
66
+ function makeToolLabel(serverName: string, toolName: string, desc?: string): string {
67
+ return `MCP ${serverName}: ${toolName}${desc ? ` (${desc.slice(0, 20)})` : ""}`;
68
+ }
69
+
70
+ export function buildMcpTool(
71
+ config: McpServerConfig,
72
+ toolEntry: { name: string; description?: string; inputSchema?: Record<string, unknown> },
73
+ findConnection: (serverName: string) => McpConnection | undefined,
74
+ ): {
75
+ name: string;
76
+ label: string;
77
+ description: string;
78
+ promptSnippet: string;
79
+ renderResult: (result: any, options: { expanded: boolean }, theme: any, context: any) => any;
80
+ parameters: Record<string, unknown> | undefined;
81
+ execute: (id: string, params: any, signal: any, update: any, ctx: any) => Promise<any>;
82
+ } {
83
+ const toolName = makeToolName(config.name, toolEntry.name);
84
+ const desc = toolEntry.description || `${toolEntry.name} (MCP tool)`;
85
+ return {
86
+ name: toolName,
87
+ label: makeToolLabel(config.name, toolEntry.name, toolEntry.description),
88
+ description: desc,
89
+ promptSnippet: desc || `MCP tool ${config.name}/${toolEntry.name}`,
90
+ renderResult: renderMcpResult,
91
+ parameters: toolEntry.inputSchema,
92
+ execute: async (_id: string, params: any, _signal: any, _update: any, _ctx: any) => {
93
+ const conn = findConnection(config.name);
94
+ if (!conn) {
95
+ return {
96
+ content: [{ type: "text", text: `MCP server "${config.name}" is not connected. Use /reload to retry.` }],
97
+ isError: false,
98
+ details: {},
99
+ };
100
+ }
101
+ try {
102
+ const text = await conn.callTool(toolEntry.name, params ?? {});
103
+ return { content: [{ type: "text", text }], isError: false, details: {} };
104
+ } catch (err) {
105
+ const msg = err instanceof Error ? err.message : String(err);
106
+ return { content: [{ type: "text", text: `MCP tool "${toolName}" error: ${msg}` }], isError: true, details: {} };
107
+ }
108
+ },
109
+ };
110
+ }
111
+
112
+ export const __mcpToolDefinitionTest = { collapseMcpText, buildMcpTool };