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.
- package/.codegraph/daemon.pid +6 -0
- package/AGENTS.md +92 -0
- package/README.md +53 -64
- package/commands/dp-model.ts +23 -0
- package/commands/dp-settings.ts +23 -0
- package/commands/mcp-status.ts +62 -0
- package/commands/retry.ts +19 -0
- package/commands/usage.ts +544 -0
- package/hooks/compaction.ts +204 -0
- package/hooks/externalize.ts +70 -0
- package/hooks/image-vision.ts +132 -0
- package/hooks/inject-agents-md.ts +164 -0
- package/hooks/mcp.ts +340 -0
- package/hooks/normalize-codeblocks.ts +88 -0
- package/hooks/pi-tool-filter.ts +28 -0
- package/{extensions/safety → hooks/redact}/types.ts +1 -8
- package/hooks/redact.ts +104 -0
- package/{extensions → hooks}/rtk.ts +92 -115
- package/hooks/session-title.ts +54 -0
- package/hooks/skeleton.ts +212 -0
- package/hooks/smart-at.ts +318 -0
- package/{extensions/file-times.ts → hooks/track-mtime.ts} +40 -38
- package/{extensions → hooks}/wakatime.ts +120 -122
- package/index.ts +155 -1
- package/package.json +5 -4
- package/{extensions/settings.ts → settings.ts} +32 -0
- package/{extensions → tools}/lsp/client.ts +0 -25
- package/{extensions → tools}/lsp/format.ts +2 -99
- package/{extensions → tools}/lsp/index.ts +1 -1
- package/{extensions → tools}/lsp/servers.ts +1 -1
- package/{extensions → tools}/lsp/tools.ts +1 -66
- package/{extensions → tools}/lsp/types.ts +0 -11
- package/tools/mcp/builtin/codegraph.ts +45 -0
- package/tools/mcp/builtin/context7.ts +10 -0
- package/tools/mcp/builtin/exa.ts +10 -0
- package/tools/mcp/builtin/index.ts +19 -0
- package/tools/mcp/cache.ts +124 -0
- package/{extensions → tools}/mcp/client.ts +2 -2
- package/{extensions/mcp/builtin.ts → tools/mcp/config.ts} +21 -181
- package/tools/mcp/index.ts +44 -0
- package/tools/mcp/tool-definition.ts +112 -0
- package/{extensions/patch.ts → tools/patch/core.ts} +336 -65
- package/{extensions/io.ts → tools/patch/index.ts} +29 -205
- package/tsconfig.json +10 -1
- package/ui/mcp-status.ts +202 -0
- package/ui/model-picker.ts +162 -0
- package/ui/module-settings.ts +83 -0
- package/ui/usage.ts +396 -0
- package/extensions/index.ts +0 -154
- package/extensions/io-tool-output.ts +0 -33
- package/extensions/mcp/index.ts +0 -435
- package/extensions/model-integration.ts +0 -531
- package/extensions/providers/ark-coding.ts +0 -75
- package/extensions/providers/index.ts +0 -9
- package/extensions/providers/ollama-cloud.ts +0 -101
- package/extensions/providers/qianfan-coding.ts +0 -71
- package/extensions/safety/index.ts +0 -102
- package/extensions/session-title.ts +0 -40
- package/extensions/slash.ts +0 -458
- package/extensions/smart-at.ts +0 -481
- package/extensions/subdir-agents.ts +0 -151
- /package/{extensions/safety → hooks/redact}/detect.ts +0 -0
- /package/{extensions/safety → hooks/redact}/entropy.ts +0 -0
- /package/{extensions/safety → hooks/redact}/patterns.ts +0 -0
- /package/{extensions → tools}/lsp/env.ts +0 -0
- /package/{extensions → tools}/lsp/manager.ts +0 -0
- /package/{extensions → tools}/lsp/prompt.ts +0 -0
- /package/{extensions → tools}/lsp/protocol.ts +0 -0
package/extensions/index.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* decorated-pi — Essential utilities for pi
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
|
|
7
|
-
import { setupSafety } from "./safety/index.js";
|
|
8
|
-
import { setupModelIntegration } from "./model-integration";
|
|
9
|
-
import { setupSlash } from "./slash";
|
|
10
|
-
import { setupSubdirAgents } from "./subdir-agents";
|
|
11
|
-
import { setupSessionTitle } from "./session-title";
|
|
12
|
-
import { setupIO } from "./io";
|
|
13
|
-
import { setupLsp } from "./lsp/index";
|
|
14
|
-
import { collectLspDependencyStatuses } from "./lsp/servers";
|
|
15
|
-
import { setupProviders } from "./providers/index";
|
|
16
|
-
import { getSmartAtDependencyStatuses, setupSmartAt } from "./smart-at";
|
|
17
|
-
import { setupMcp } from "./mcp/index.js";
|
|
18
|
-
import { collectMcpDependencyStatuses } from "./mcp/builtin";
|
|
19
|
-
import { setupWakatime } from "./wakatime";
|
|
20
|
-
import { findSystemRtk, getRtkDependencyStatuses, setupRtkIntegration, type DependencyStatus } from "./rtk";
|
|
21
|
-
import { isModuleEnabled } from "./settings";
|
|
22
|
-
|
|
23
|
-
function collectDependencyStatuses(cwd: string): DependencyStatus[] {
|
|
24
|
-
const statuses: DependencyStatus[] = [];
|
|
25
|
-
if (isModuleEnabled("rtk")) statuses.push(...getRtkDependencyStatuses());
|
|
26
|
-
if (isModuleEnabled("smart-at")) statuses.push(...getSmartAtDependencyStatuses(cwd));
|
|
27
|
-
if (isModuleEnabled("lsp")) statuses.push(...collectLspDependencyStatuses(cwd));
|
|
28
|
-
if (isModuleEnabled("mcp")) statuses.push(...collectMcpDependencyStatuses(cwd));
|
|
29
|
-
return statuses;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function formatDependencyLines(statuses: DependencyStatus[]): string[] {
|
|
33
|
-
const missing = statuses.filter((item) => item.state === "missing");
|
|
34
|
-
const grouped = new Map<string, string[]>();
|
|
35
|
-
|
|
36
|
-
for (const item of missing) {
|
|
37
|
-
const labels = grouped.get(item.module) ?? [];
|
|
38
|
-
labels.push(item.label);
|
|
39
|
-
grouped.set(item.module, labels);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const lines = ["[decorated-pi] missing dependencies:"];
|
|
43
|
-
for (const [module, labels] of grouped) {
|
|
44
|
-
lines.push(` [${module}] ${labels.join(", ")}`);
|
|
45
|
-
}
|
|
46
|
-
return lines;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function setupDependencyReminders(pi: ExtensionAPI) {
|
|
50
|
-
let notifyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
51
|
-
|
|
52
|
-
pi.on("session_start", async (event, ctx) => {
|
|
53
|
-
if (!ctx.hasUI) return;
|
|
54
|
-
if (event.reason !== "startup" && event.reason !== "reload") return;
|
|
55
|
-
|
|
56
|
-
const statuses = collectDependencyStatuses(ctx.cwd);
|
|
57
|
-
const missing = statuses.filter((item) => item.state === "missing");
|
|
58
|
-
if (missing.length === 0) return;
|
|
59
|
-
|
|
60
|
-
if (notifyTimer) clearTimeout(notifyTimer);
|
|
61
|
-
const message = formatDependencyLines(statuses).join("\n");
|
|
62
|
-
|
|
63
|
-
// Defer until after pi finishes startup/reload UI rebuild, otherwise
|
|
64
|
-
// notify() is appended to the chat and then wiped by rebuildChatFromMessages().
|
|
65
|
-
notifyTimer = setTimeout(() => {
|
|
66
|
-
notifyTimer = undefined;
|
|
67
|
-
try {
|
|
68
|
-
ctx.ui.notify(message, "info");
|
|
69
|
-
} catch {
|
|
70
|
-
// Extension context may be stale if another reload/session switch happened.
|
|
71
|
-
}
|
|
72
|
-
}, 0);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
pi.on("session_shutdown", async () => {
|
|
76
|
-
if (!notifyTimer) return;
|
|
77
|
-
clearTimeout(notifyTimer);
|
|
78
|
-
notifyTimer = undefined;
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const DECORATED_PI_GUIDANCE_MARKER = "## Decorated Pi Guidance";
|
|
83
|
-
|
|
84
|
-
function setupGuidance(pi: ExtensionAPI) {
|
|
85
|
-
pi.on("before_agent_start", async (event) => {
|
|
86
|
-
// Remove "Current date: YYYY-MM-DD" from system prompt to improve cache stability
|
|
87
|
-
let prompt = event.systemPrompt.replace(/\nCurrent date: \d{4}-\d{2}-\d{2}/, "");
|
|
88
|
-
|
|
89
|
-
if (!prompt.includes(DECORATED_PI_GUIDANCE_MARKER)) {
|
|
90
|
-
const guidance = [
|
|
91
|
-
DECORATED_PI_GUIDANCE_MARKER,
|
|
92
|
-
"",
|
|
93
|
-
"- Before acting on a user's prompt, ensure you fully understand their needs. If the intent is ambiguous, ask clarifying questions. Proceed only when the intent is clear.",
|
|
94
|
-
"- Look before you leap! Ensure you have conducted thorough research before taking any action.",
|
|
95
|
-
"- Exercise caution when performing any **write** operations, especially when you are in a research or exploration phase.",
|
|
96
|
-
"- You don't need to read **AGENTS.md** or **CLAUDE.md** files unless you're explicitly asked to, these files will loaded automatically if neccessary.",
|
|
97
|
-
"- CAUTION: Do not perform write operations in the following directories unless explicitly instructed: `node_modules`, `venv`, `env`, `__pycache__`, `.git` or any other hidden directories.",
|
|
98
|
-
"",
|
|
99
|
-
"### Secret Redaction",
|
|
100
|
-
"",
|
|
101
|
-
"- When you see masked secret values (e.g. `sk-***...***` where `*`, `#`, or `?` are mask characters), the real value has been redacted by the system. Do not attempt to read or guess it. If you need the secret, use tools like `jq` or `grep` to extract it from the original source file.",
|
|
102
|
-
].join("\n");
|
|
103
|
-
|
|
104
|
-
prompt = `${prompt}\n\n${guidance}`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
sortSystemPromptOptions(event.systemPromptOptions);
|
|
108
|
-
return { systemPrompt: prompt };
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Sort all fields in systemPromptOptions alphabetically for stable system prompt. */
|
|
113
|
-
export function sortSystemPromptOptions(opts: {
|
|
114
|
-
toolSnippets?: Record<string, string>;
|
|
115
|
-
selectedTools?: string[];
|
|
116
|
-
promptGuidelines?: string[];
|
|
117
|
-
skills?: Array<{ name: string; description: string; filePath: string }>;
|
|
118
|
-
}) {
|
|
119
|
-
const sortedToolNames = Object.keys(opts.toolSnippets ?? {}).sort((a, b) => a.localeCompare(b));
|
|
120
|
-
const sortedToolSnippets: Record<string, string> = {};
|
|
121
|
-
for (const name of sortedToolNames) {
|
|
122
|
-
sortedToolSnippets[name] = opts.toolSnippets![name];
|
|
123
|
-
}
|
|
124
|
-
opts.toolSnippets = sortedToolSnippets;
|
|
125
|
-
if (opts.selectedTools) {
|
|
126
|
-
opts.selectedTools = sortedToolNames;
|
|
127
|
-
}
|
|
128
|
-
if (opts.promptGuidelines) {
|
|
129
|
-
opts.promptGuidelines = [...opts.promptGuidelines].sort((a, b) => a.localeCompare(b));
|
|
130
|
-
}
|
|
131
|
-
if (opts.skills) {
|
|
132
|
-
opts.skills = [...opts.skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export default function (pi: ExtensionAPI) {
|
|
137
|
-
// Always loaded — core commands and providers
|
|
138
|
-
setupSlash(pi);
|
|
139
|
-
setupProviders(pi);
|
|
140
|
-
setupModelIntegration(pi);
|
|
141
|
-
setupSubdirAgents(pi);
|
|
142
|
-
setupSessionTitle(pi);
|
|
143
|
-
setupGuidance(pi);
|
|
144
|
-
setupDependencyReminders(pi);
|
|
145
|
-
|
|
146
|
-
// Configurable modules
|
|
147
|
-
if (isModuleEnabled("patch")) setupIO(pi);
|
|
148
|
-
if (isModuleEnabled("safety")) setupSafety(pi);
|
|
149
|
-
if (isModuleEnabled("lsp")) setupLsp(pi);
|
|
150
|
-
if (isModuleEnabled("smart-at")) setupSmartAt(pi);
|
|
151
|
-
if (isModuleEnabled("mcp")) setupMcp(pi);
|
|
152
|
-
if (isModuleEnabled("wakatime")) setupWakatime(pi);
|
|
153
|
-
if (isModuleEnabled("rtk") && findSystemRtk()) setupRtkIntegration(pi);
|
|
154
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared tool-output externalization helpers.
|
|
3
|
-
*
|
|
4
|
-
* Used by `setupIO` (for read / bash) and `maybeExternalizeMcpResult` (for MCP tools).
|
|
5
|
-
* Each writes to the same temp dir; MCP also adds a 2KB preview, while read / bash
|
|
6
|
-
* emit a single-line placeholder.
|
|
7
|
-
*/
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
|
-
import * as os from "node:os";
|
|
10
|
-
import * as path from "node:path";
|
|
11
|
-
import { randomBytes } from "node:crypto";
|
|
12
|
-
|
|
13
|
-
export const TOOL_OUTPUT_TEMP_DIR = path.join(os.tmpdir(), "decorated-pi-results");
|
|
14
|
-
|
|
15
|
-
/** Write content to a temp file under TOOL_OUTPUT_TEMP_DIR.
|
|
16
|
-
* Returns the file path, or undefined on failure (e.g., /tmp full). */
|
|
17
|
-
export function writeOutputToTemp(
|
|
18
|
-
toolName: string,
|
|
19
|
-
toolCallId: string,
|
|
20
|
-
content: string,
|
|
21
|
-
): string | undefined {
|
|
22
|
-
try {
|
|
23
|
-
if (!fs.existsSync(TOOL_OUTPUT_TEMP_DIR)) {
|
|
24
|
-
fs.mkdirSync(TOOL_OUTPUT_TEMP_DIR, { recursive: true });
|
|
25
|
-
}
|
|
26
|
-
const id = toolCallId ? toolCallId.slice(0, 12) : randomBytes(8).toString("hex");
|
|
27
|
-
const filePath = path.join(TOOL_OUTPUT_TEMP_DIR, `${toolName}-${id}.txt`);
|
|
28
|
-
fs.writeFileSync(filePath, content, "utf-8");
|
|
29
|
-
return filePath;
|
|
30
|
-
} catch {
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
}
|
package/extensions/mcp/index.ts
DELETED
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
import { keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import { McpConnection } from "./client.js";
|
|
4
|
-
import {
|
|
5
|
-
resolveMcpConfigs,
|
|
6
|
-
loadMcpCache, updateServerCache, cleanupStaleCache,
|
|
7
|
-
maybeExternalizeMcpResult,
|
|
8
|
-
BUILTIN_MCP_SERVERS,
|
|
9
|
-
type McpServerConfig, type McpToolCache,
|
|
10
|
-
} from "./builtin.js";
|
|
11
|
-
|
|
12
|
-
export interface McpServerStatus {
|
|
13
|
-
name: string;
|
|
14
|
-
url: string;
|
|
15
|
-
source: string;
|
|
16
|
-
state: "connecting" | "connected" | "failed" | "disabled";
|
|
17
|
-
toolCount: number;
|
|
18
|
-
tools: Array<{ name: string; description?: string; inputSchema?: Record<string, unknown> }>;
|
|
19
|
-
error?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let activeConnections: McpConnection[] = [];
|
|
23
|
-
let allServers = new Map<string, McpServerStatus>();
|
|
24
|
-
let cachedConfigs: McpServerConfig[] = [];
|
|
25
|
-
let connectPromise: Promise<void> | null = null;
|
|
26
|
-
let cachedCwd = "";
|
|
27
|
-
|
|
28
|
-
const MCP_RESULT_FOLD_LINES = 45;
|
|
29
|
-
|
|
30
|
-
function trimTrailingEmptyLines(lines: string[]): string[] {
|
|
31
|
-
let end = lines.length;
|
|
32
|
-
while (end > 0 && lines[end - 1] === "") end -= 1;
|
|
33
|
-
return lines.slice(0, end);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function collapseMcpText(text: string, maxLines = MCP_RESULT_FOLD_LINES) {
|
|
37
|
-
const lines = trimTrailingEmptyLines(text.split("\n"));
|
|
38
|
-
const totalLines = lines.length;
|
|
39
|
-
const displayLines = lines.slice(0, maxLines);
|
|
40
|
-
const remainingLines = Math.max(0, totalLines - maxLines);
|
|
41
|
-
return { totalLines, displayLines, remainingLines };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function getTextContent(result: { content?: Array<{ type: string; text?: string }> }): string {
|
|
45
|
-
return (result.content ?? [])
|
|
46
|
-
.filter((c): c is { type: "text"; text?: string } => c.type === "text")
|
|
47
|
-
.map((c) => c.text ?? "")
|
|
48
|
-
.join("\n");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function formatMcpResultText(text: string, expanded: boolean, theme: any): string {
|
|
52
|
-
const { totalLines, displayLines, remainingLines } = collapseMcpText(
|
|
53
|
-
text,
|
|
54
|
-
expanded ? Number.MAX_SAFE_INTEGER : MCP_RESULT_FOLD_LINES,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
// Check for externalization message in the last line
|
|
58
|
-
const lastLine = displayLines[displayLines.length - 1] || "";
|
|
59
|
-
let outputLines = [...displayLines];
|
|
60
|
-
let truncationMsg = "";
|
|
61
|
-
|
|
62
|
-
if (lastLine.startsWith('[Truncated: ') && lastLine.endsWith(']')) {
|
|
63
|
-
truncationMsg = lastLine;
|
|
64
|
-
outputLines = outputLines.slice(0, -1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const outputText = outputLines.join("\n");
|
|
68
|
-
let rendered = outputText ? theme.fg("toolOutput", outputText) : "";
|
|
69
|
-
|
|
70
|
-
if (truncationMsg) {
|
|
71
|
-
rendered += (rendered ? "\n" : "") + theme.fg("warning", truncationMsg);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!expanded && remainingLines > 0) {
|
|
75
|
-
rendered += `${theme.fg("muted", `\n... (${remainingLines} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
76
|
-
}
|
|
77
|
-
return rendered;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function renderMcpResult(result: any, options: { expanded: boolean }, theme: any, context: any) {
|
|
81
|
-
const component = context.lastComponent ?? new Text("", 0, 0);
|
|
82
|
-
component.setText(formatMcpResultText(getTextContent(result), options.expanded, theme));
|
|
83
|
-
return component;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ── config helpers ────────────────────────────────────────────────────────
|
|
87
|
-
|
|
88
|
-
export function updateConfigEnabled(serverName: string, enabled: boolean): void {
|
|
89
|
-
const config = cachedConfigs.find(c => c.name === serverName);
|
|
90
|
-
if (config) config.enabled = enabled;
|
|
91
|
-
const server = allServers.get(serverName);
|
|
92
|
-
if (server) {
|
|
93
|
-
if (!enabled) {
|
|
94
|
-
// Stash the real connection state, set to disabled
|
|
95
|
-
server.state = "disabled";
|
|
96
|
-
} else {
|
|
97
|
-
// Re-enable: if there's still an active connection, restore it
|
|
98
|
-
const conn = activeConnections.find(c => c.serverName === serverName);
|
|
99
|
-
server.state = conn ? "connected" : "connecting";
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ── helpers ───────────────────────────────────────────────────────────────
|
|
105
|
-
|
|
106
|
-
function makeToolName(serverName: string, toolName: string): string {
|
|
107
|
-
return `${serverName}_${toolName}`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function makeToolLabel(serverName: string, toolName: string, desc?: string): string {
|
|
111
|
-
return `MCP ${serverName}: ${toolName}${desc ? ` (${desc.slice(0, 20)})` : ""}`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ── cache helpers ─────────────────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
function cacheScopeForSource(source: string): "global" | "project" {
|
|
117
|
-
return source === "project" ? "project" : "global";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// ── register cached tools ─────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
function registerCachedToolsFromCache(
|
|
123
|
-
pi: ExtensionAPI,
|
|
124
|
-
cache: McpCache,
|
|
125
|
-
configs: McpServerConfig[]
|
|
126
|
-
): void {
|
|
127
|
-
for (const config of configs) {
|
|
128
|
-
if (!config.enabled) continue;
|
|
129
|
-
const entry = cache.servers[config.name];
|
|
130
|
-
if (!entry || entry.tools.length === 0) continue;
|
|
131
|
-
|
|
132
|
-
for (const t of entry.tools) {
|
|
133
|
-
const toolName = makeToolName(config.name, t.name);
|
|
134
|
-
const desc = t.description || `${t.name} (MCP tool)`;
|
|
135
|
-
pi.registerTool({
|
|
136
|
-
name: toolName,
|
|
137
|
-
label: makeToolLabel(config.name, t.name, t.description),
|
|
138
|
-
description: desc,
|
|
139
|
-
promptSnippet: desc || `MCP tool ${config.name}/${t.name}`,
|
|
140
|
-
renderResult: renderMcpResult,
|
|
141
|
-
parameters: t.inputSchema,
|
|
142
|
-
execute: async (_id, params, _signal, _update, _ctx) => {
|
|
143
|
-
const conn = activeConnections.find(c => c.serverName === config.name);
|
|
144
|
-
if (!conn) {
|
|
145
|
-
// Tool is registered from cache before connectAll finishes. Tell the
|
|
146
|
-
// model to retry — connection is establishing in the background.
|
|
147
|
-
const status = allServers.get(config.name);
|
|
148
|
-
const state = status?.state ?? "connecting";
|
|
149
|
-
return {
|
|
150
|
-
content: [{ type: "text", text: `MCP server "${config.name}" is ${state}. Please retry shortly.` }],
|
|
151
|
-
isError: false,
|
|
152
|
-
details: {},
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
const text = await conn.callTool(t.name, params as Record<string, unknown>);
|
|
157
|
-
return maybeExternalizeMcpResult(text, toolName, _id);
|
|
158
|
-
} catch (err) {
|
|
159
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
160
|
-
return {
|
|
161
|
-
content: [{ type: "text", text: `MCP call failed on "${config.name}/${t.name}": ${msg}` }],
|
|
162
|
-
isError: true,
|
|
163
|
-
details: {},
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function registerCachedTools(pi: ExtensionAPI, configs: McpServerConfig[]): void {
|
|
173
|
-
const cache = loadMcpCache(cachedCwd);
|
|
174
|
-
if (!cache) return;
|
|
175
|
-
registerCachedToolsFromCache(pi, cache, configs);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ── connect ───────────────────────────────────────────────────────────────
|
|
179
|
-
|
|
180
|
-
async function connectAll(
|
|
181
|
-
configs: McpServerConfig[],
|
|
182
|
-
ui?: { notify: (msg: string, type: string) => void }
|
|
183
|
-
): Promise<{ schemaChanges: string[]; hasNewServer: boolean }> {
|
|
184
|
-
// Load current cache for comparison
|
|
185
|
-
const cache = loadMcpCache(cachedCwd);
|
|
186
|
-
const schemaChanges: string[] = [];
|
|
187
|
-
|
|
188
|
-
allServers = new Map(
|
|
189
|
-
configs.map((s) => [
|
|
190
|
-
s.name,
|
|
191
|
-
{
|
|
192
|
-
name: s.name,
|
|
193
|
-
url: s.url ?? s.command ?? "(unknown)",
|
|
194
|
-
source: s.source,
|
|
195
|
-
state: "connecting" as const,
|
|
196
|
-
toolCount: 0,
|
|
197
|
-
tools: [],
|
|
198
|
-
},
|
|
199
|
-
]),
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
connectPromise = Promise.all(
|
|
203
|
-
configs.map(async (server) => {
|
|
204
|
-
const conn = new McpConnection(server.name, server);
|
|
205
|
-
conn.source = server.source;
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
await conn.connect(30_000);
|
|
209
|
-
activeConnections.push(conn);
|
|
210
|
-
|
|
211
|
-
const actualTools = conn.tools.map((t) => ({
|
|
212
|
-
name: t.name,
|
|
213
|
-
description: t.description,
|
|
214
|
-
inputSchema: t.inputSchema,
|
|
215
|
-
}));
|
|
216
|
-
|
|
217
|
-
// Check if schema changed
|
|
218
|
-
const cachedEntry = cache?.servers[server.name];
|
|
219
|
-
if (cachedEntry && cachedEntry.tools.length > 0) {
|
|
220
|
-
const cachedToolNames = new Set(cachedEntry.tools.map(t => t.name));
|
|
221
|
-
const actualToolNames = new Set(actualTools.map(t => t.name));
|
|
222
|
-
const added = actualTools.filter(t => !cachedToolNames.has(t.name));
|
|
223
|
-
const removed = cachedEntry.tools.filter(t => !actualToolNames.has(t.name));
|
|
224
|
-
const changed = actualTools.filter(t => {
|
|
225
|
-
const cached = cachedEntry.tools.find(ct => ct.name === t.name);
|
|
226
|
-
return cached && JSON.stringify(cached.inputSchema) !== JSON.stringify(t.inputSchema);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
if (added.length > 0 || removed.length > 0 || changed.length > 0) {
|
|
230
|
-
const parts: string[] = [];
|
|
231
|
-
if (added.length) parts.push(`${added.length} added`);
|
|
232
|
-
if (removed.length) parts.push(`${removed.length} removed`);
|
|
233
|
-
if (changed.length) parts.push(`${changed.length} changed`);
|
|
234
|
-
schemaChanges.push(`${server.name} (${parts.join(', ')})`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
allServers.set(server.name, {
|
|
239
|
-
name: server.name,
|
|
240
|
-
url: server.url ?? server.command ?? "(unknown)",
|
|
241
|
-
source: server.source,
|
|
242
|
-
state: "connected",
|
|
243
|
-
toolCount: conn.tools.length,
|
|
244
|
-
tools: actualTools,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Update cache with this server's tools
|
|
248
|
-
const tools: McpToolCache[] = conn.tools.map(t => ({
|
|
249
|
-
name: t.name,
|
|
250
|
-
description: t.description,
|
|
251
|
-
inputSchema: t.inputSchema,
|
|
252
|
-
}));
|
|
253
|
-
updateServerCache(
|
|
254
|
-
server.name,
|
|
255
|
-
{ tools, cachedAt: Date.now() },
|
|
256
|
-
cacheScopeForSource(server.source),
|
|
257
|
-
cachedCwd || undefined,
|
|
258
|
-
);
|
|
259
|
-
} catch (err) {
|
|
260
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
261
|
-
allServers.set(server.name, {
|
|
262
|
-
name: server.name,
|
|
263
|
-
url: server.url ?? server.command ?? "(unknown)",
|
|
264
|
-
source: server.source,
|
|
265
|
-
state: "failed",
|
|
266
|
-
toolCount: 0,
|
|
267
|
-
tools: [],
|
|
268
|
-
error: msg,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}),
|
|
272
|
-
).then(() => undefined);
|
|
273
|
-
|
|
274
|
-
await connectPromise;
|
|
275
|
-
connectPromise = null;
|
|
276
|
-
|
|
277
|
-
// Determine whether any server had no cached tools (first-time connection).
|
|
278
|
-
let hasNewServer = false;
|
|
279
|
-
for (const server of configs) {
|
|
280
|
-
const cachedEntry = cache?.servers[server.name];
|
|
281
|
-
if (!cachedEntry || cachedEntry.tools.length === 0) {
|
|
282
|
-
hasNewServer = true;
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return { schemaChanges, hasNewServer };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ── setup ─────────────────────────────────────────────────────────────────
|
|
291
|
-
|
|
292
|
-
export function setupMcp(pi: ExtensionAPI) {
|
|
293
|
-
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
294
|
-
await teardownMcp();
|
|
295
|
-
cachedCwd = ctx.cwd;
|
|
296
|
-
|
|
297
|
-
const configs = resolveMcpConfigs(ctx.cwd).sort((a, b) => a.name.localeCompare(b.name));
|
|
298
|
-
cachedConfigs = configs;
|
|
299
|
-
if (configs.length === 0) return;
|
|
300
|
-
|
|
301
|
-
// Clean stale cache entries for removed servers
|
|
302
|
-
cleanupStaleCache(configs, cachedCwd);
|
|
303
|
-
|
|
304
|
-
const enabledConfigs = configs.filter(s => s.enabled);
|
|
305
|
-
|
|
306
|
-
// Load cache and register tools immediately (before connectAll)
|
|
307
|
-
const cache = loadMcpCache(cachedCwd);
|
|
308
|
-
if (cache) {
|
|
309
|
-
registerCachedToolsFromCache(pi, cache, enabledConfigs);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Connect in background (fire-and-forget, don't block session_start)
|
|
313
|
-
void connectAll(enabledConfigs, ctx.hasUI ? ctx.ui : undefined).then(({ schemaChanges, hasNewServer }) => {
|
|
314
|
-
// Re-register tools when:
|
|
315
|
-
// - A server was not in cache before (first connection)
|
|
316
|
-
// - Schema changed (tools added/removed/changed)
|
|
317
|
-
if (hasNewServer || schemaChanges.length > 0) {
|
|
318
|
-
const cache = loadMcpCache(cachedCwd);
|
|
319
|
-
if (cache) {
|
|
320
|
-
registerCachedToolsFromCache(pi, cache, enabledConfigs);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
if (schemaChanges.length > 0 && ctx.hasUI) {
|
|
324
|
-
ctx.ui.notify(`MCP schema updated: ${schemaChanges.join('; ')}.`, "info");
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
pi.on("session_shutdown", () => {
|
|
330
|
-
void teardownMcp();
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export function getMcpStatus(): McpServerStatus[] {
|
|
335
|
-
const cache = loadMcpCache(cachedCwd);
|
|
336
|
-
const result: McpServerStatus[] = [];
|
|
337
|
-
for (const config of cachedConfigs) {
|
|
338
|
-
const connected = allServers.get(config.name);
|
|
339
|
-
if (connected) {
|
|
340
|
-
result.push(connected);
|
|
341
|
-
} else {
|
|
342
|
-
const cachedEntry = cache?.servers[config.name];
|
|
343
|
-
result.push({
|
|
344
|
-
name: config.name,
|
|
345
|
-
url: config.url ?? config.command ?? "(unknown)",
|
|
346
|
-
source: config.source,
|
|
347
|
-
state: config.enabled ? "connecting" : "disabled",
|
|
348
|
-
toolCount: cachedEntry?.tools.length ?? 0,
|
|
349
|
-
tools: cachedEntry?.tools ?? [],
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return result;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// ── refresh single server cache ───────────────────────────────────────────
|
|
357
|
-
|
|
358
|
-
export const __mcpIndexTest = { collapseMcpText };
|
|
359
|
-
|
|
360
|
-
export async function refreshServerCache(
|
|
361
|
-
serverName: string,
|
|
362
|
-
registry: any,
|
|
363
|
-
): Promise<{ ok: boolean; error?: string }> {
|
|
364
|
-
const config = resolveMcpConfigs(cachedCwd).find(s => s.name === serverName);
|
|
365
|
-
if (!config) return { ok: false, error: `Server "${serverName}" not found in config.` };
|
|
366
|
-
|
|
367
|
-
// Disconnect existing connection for this server
|
|
368
|
-
const existing = activeConnections.find(c => c.serverName === serverName);
|
|
369
|
-
if (existing) {
|
|
370
|
-
try { await existing.disconnect(); } catch { /* ignore */ }
|
|
371
|
-
activeConnections = activeConnections.filter(c => c.serverName !== serverName);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const conn = new McpConnection(config.name, config);
|
|
375
|
-
conn.source = config.source;
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
await conn.connect(30_000);
|
|
379
|
-
activeConnections.push(conn);
|
|
380
|
-
|
|
381
|
-
allServers.set(config.name, {
|
|
382
|
-
name: config.name,
|
|
383
|
-
url: config.url ?? config.command ?? "(unknown)",
|
|
384
|
-
source: config.source,
|
|
385
|
-
state: "connected",
|
|
386
|
-
toolCount: conn.tools.length,
|
|
387
|
-
tools: conn.tools.map(t => ({
|
|
388
|
-
name: t.name,
|
|
389
|
-
description: t.description,
|
|
390
|
-
inputSchema: t.inputSchema,
|
|
391
|
-
})),
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const tools: McpToolCache[] = conn.tools.map(t => ({
|
|
395
|
-
name: t.name,
|
|
396
|
-
description: t.description,
|
|
397
|
-
inputSchema: t.inputSchema,
|
|
398
|
-
}));
|
|
399
|
-
updateServerCache(
|
|
400
|
-
config.name,
|
|
401
|
-
{ tools, cachedAt: Date.now() },
|
|
402
|
-
cacheScopeForSource(config.source),
|
|
403
|
-
cachedCwd || undefined,
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
return { ok: true };
|
|
407
|
-
} catch (err) {
|
|
408
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
409
|
-
allServers.set(config.name, {
|
|
410
|
-
name: config.name,
|
|
411
|
-
url: config.url ?? config.command ?? "(unknown)",
|
|
412
|
-
source: config.source,
|
|
413
|
-
state: "failed",
|
|
414
|
-
toolCount: 0,
|
|
415
|
-
tools: [],
|
|
416
|
-
error: msg,
|
|
417
|
-
});
|
|
418
|
-
return { ok: false, error: msg };
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async function teardownMcp(): Promise<void> {
|
|
423
|
-
await Promise.all(
|
|
424
|
-
activeConnections.map(async (conn) => {
|
|
425
|
-
try {
|
|
426
|
-
await conn.disconnect();
|
|
427
|
-
} catch {
|
|
428
|
-
// Silently ignore disconnect errors.
|
|
429
|
-
}
|
|
430
|
-
}),
|
|
431
|
-
);
|
|
432
|
-
activeConnections = [];
|
|
433
|
-
allServers = new Map();
|
|
434
|
-
cachedConfigs = [];
|
|
435
|
-
}
|