decorated-pi 0.5.0 → 0.5.1
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/README.md +2 -2
- package/extensions/index.ts +58 -2
- package/extensions/io.ts +1 -0
- package/extensions/mcp/builtin.ts +14 -104
- package/extensions/mcp/index.ts +40 -111
- package/extensions/model-integration.ts +6 -14
- package/extensions/rtk.ts +0 -25
- package/extensions/settings.ts +0 -32
- package/extensions/slash.ts +1 -1
- package/package.json +1 -1
- package/extensions/guidance.ts +0 -30
package/README.md
CHANGED
|
@@ -55,12 +55,12 @@ Example redaction on a `read` / `bash` output:
|
|
|
55
55
|
|
|
56
56
|
> `*` = known pattern, `#` = config key regex, `?` = entropy heuristic.
|
|
57
57
|
|
|
58
|
-
### 4. Auxiliary Models
|
|
58
|
+
### 4. Auxiliary Models
|
|
59
59
|
|
|
60
60
|
Offloads auxiliary ops to cheaper models, reducing cost on every session. Configured via `/dp-model`:
|
|
61
61
|
|
|
62
62
|
- **Image read fallback** — when the model reads an image file, detects type via magic bytes, calls a configured vision-capable model, and replaces the read result with image analysis text (jpeg, png, gif, webp)
|
|
63
|
-
- **Compact model** — uses a configured model for context compaction (instead of the main model)
|
|
63
|
+
- **Compact model** — uses a configured model for context compaction (instead of the main model).
|
|
64
64
|
|
|
65
65
|
### 5. Progressive Context from `AGENTS.md` / `CLAUDE.md`
|
|
66
66
|
|
package/extensions/index.ts
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
// Extend prompt cache TTL for all providers: Anthropic 1h, OpenAI 24h
|
|
8
|
+
process.env.PI_CACHE_RETENTION ??= "long";
|
|
6
9
|
import { setupSafety } from "./safety/index.js";
|
|
7
10
|
import { setupModelIntegration } from "./model-integration";
|
|
8
11
|
import { setupSlash } from "./slash";
|
|
9
12
|
import { setupSubdirAgents } from "./subdir-agents";
|
|
10
13
|
import { setupSessionTitle } from "./session-title";
|
|
11
14
|
import { setupIO } from "./io";
|
|
12
|
-
import { setupGuidance } from "./guidance";
|
|
13
15
|
import { setupLsp } from "./lsp/index";
|
|
14
16
|
import { collectLspDependencyStatuses } from "./lsp/servers";
|
|
15
17
|
import { setupProviders } from "./providers/index";
|
|
@@ -79,6 +81,60 @@ function setupDependencyReminders(pi: ExtensionAPI) {
|
|
|
79
81
|
});
|
|
80
82
|
}
|
|
81
83
|
|
|
84
|
+
const DECORATED_PI_GUIDANCE_MARKER = "## Decorated Pi Guidance";
|
|
85
|
+
|
|
86
|
+
function setupGuidance(pi: ExtensionAPI) {
|
|
87
|
+
pi.on("before_agent_start", async (event) => {
|
|
88
|
+
// Remove "Current date: YYYY-MM-DD" from system prompt to improve cache stability
|
|
89
|
+
let prompt = event.systemPrompt.replace(/\nCurrent date: \d{4}-\d{2}-\d{2}/, "");
|
|
90
|
+
|
|
91
|
+
if (!prompt.includes(DECORATED_PI_GUIDANCE_MARKER)) {
|
|
92
|
+
const guidance = [
|
|
93
|
+
DECORATED_PI_GUIDANCE_MARKER,
|
|
94
|
+
"",
|
|
95
|
+
"- 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.",
|
|
96
|
+
"- Look before you leap! Ensure you have conducted thorough research before taking any action.",
|
|
97
|
+
"- Exercise caution when performing any **write** operations, especially when you are in a research or exploration phase.",
|
|
98
|
+
"- 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.",
|
|
99
|
+
"- CAUTION: Do not perform write operations in the following directories unless explicitly instructed: `node_modules`, `venv`, `env`, `__pycache__`, `.git` or any other hidden directories.",
|
|
100
|
+
"",
|
|
101
|
+
"### Secret Redaction",
|
|
102
|
+
"",
|
|
103
|
+
"- 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.",
|
|
104
|
+
].join("\n");
|
|
105
|
+
|
|
106
|
+
prompt = `${prompt}\n\n${guidance}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
sortSystemPromptOptions(event.systemPromptOptions);
|
|
110
|
+
return { systemPrompt: prompt };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Sort all fields in systemPromptOptions alphabetically for stable system prompt. */
|
|
115
|
+
export function sortSystemPromptOptions(opts: {
|
|
116
|
+
toolSnippets?: Record<string, string>;
|
|
117
|
+
selectedTools?: string[];
|
|
118
|
+
promptGuidelines?: string[];
|
|
119
|
+
skills?: Array<{ name: string; description: string; filePath: string }>;
|
|
120
|
+
}) {
|
|
121
|
+
const sortedToolNames = Object.keys(opts.toolSnippets ?? {}).sort((a, b) => a.localeCompare(b));
|
|
122
|
+
const sortedToolSnippets: Record<string, string> = {};
|
|
123
|
+
for (const name of sortedToolNames) {
|
|
124
|
+
sortedToolSnippets[name] = opts.toolSnippets![name];
|
|
125
|
+
}
|
|
126
|
+
opts.toolSnippets = sortedToolSnippets;
|
|
127
|
+
if (opts.selectedTools) {
|
|
128
|
+
opts.selectedTools = sortedToolNames;
|
|
129
|
+
}
|
|
130
|
+
if (opts.promptGuidelines) {
|
|
131
|
+
opts.promptGuidelines = [...opts.promptGuidelines].sort((a, b) => a.localeCompare(b));
|
|
132
|
+
}
|
|
133
|
+
if (opts.skills) {
|
|
134
|
+
opts.skills = [...opts.skills].sort((a, b) => a.name.localeCompare(b.name));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
82
138
|
export default function (pi: ExtensionAPI) {
|
|
83
139
|
// Always loaded — core commands and providers
|
|
84
140
|
setupSlash(pi);
|
|
@@ -97,4 +153,4 @@ export default function (pi: ExtensionAPI) {
|
|
|
97
153
|
if (isModuleEnabled("mcp")) setupMcp(pi);
|
|
98
154
|
if (isModuleEnabled("wakatime")) setupWakatime(pi);
|
|
99
155
|
if (isModuleEnabled("rtk") && findSystemRtk()) setupRtkIntegration(pi);
|
|
100
|
-
}
|
|
156
|
+
}
|
package/extensions/io.ts
CHANGED
|
@@ -448,6 +448,7 @@ export function setupIO(pi: ExtensionAPI) {
|
|
|
448
448
|
"Always prefer modifying files with PATCH tool over bash commands or python scripts.",
|
|
449
449
|
"For full-file replacement, always use patch tool to prevent unintended edits or data loss.",
|
|
450
450
|
"To prevent hallucinations: 1. Keep each edit batch ≤ 5 changes; 2. Process remaining revisions in sequential steps",
|
|
451
|
+
"On repeated failures: read the file first to confirm information accuracy.",
|
|
451
452
|
],
|
|
452
453
|
parameters: PatchSchema,
|
|
453
454
|
renderShell: "self",
|
|
@@ -24,116 +24,15 @@ export const BUILTIN_MCP_SERVERS: Omit<McpServerConfig, "source">[] = [
|
|
|
24
24
|
{
|
|
25
25
|
name: "context7",
|
|
26
26
|
url: "https://mcp.context7.com/mcp",
|
|
27
|
-
description: "Context7 documentation and code examples",
|
|
28
27
|
enabled: true,
|
|
29
28
|
},
|
|
30
29
|
{
|
|
31
30
|
name: "exa",
|
|
32
31
|
url: "https://mcp.exa.ai/mcp",
|
|
33
|
-
description: "Exa web search",
|
|
34
32
|
enabled: true,
|
|
35
33
|
},
|
|
36
34
|
];
|
|
37
35
|
|
|
38
|
-
/** Builtin tool schemas — hardcoded so builtin servers work without a prior connection. */
|
|
39
|
-
export const BUILTIN_MCP_CACHE: McpCache = {
|
|
40
|
-
servers: {
|
|
41
|
-
context7: {
|
|
42
|
-
description: "Context7 documentation and code examples",
|
|
43
|
-
tools: [
|
|
44
|
-
{
|
|
45
|
-
name: "resolve-library-id",
|
|
46
|
-
description: "Resolve a library name to its Context7 library ID",
|
|
47
|
-
inputSchema: {
|
|
48
|
-
type: "object",
|
|
49
|
-
properties: {
|
|
50
|
-
libraryName: {
|
|
51
|
-
type: "string",
|
|
52
|
-
description: "Library or framework name to resolve (e.g. 'react', 'vue')",
|
|
53
|
-
},
|
|
54
|
-
filters: {
|
|
55
|
-
type: "array",
|
|
56
|
-
items: {
|
|
57
|
-
type: "object",
|
|
58
|
-
properties: {
|
|
59
|
-
field: { type: "string" },
|
|
60
|
-
operator: { type: "string" },
|
|
61
|
-
value: { type: "string" },
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
description: "Optional filters to narrow down results",
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
required: ["libraryName"],
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: "query-docs",
|
|
72
|
-
description: "Retrieve and query documentation using a Context7 library ID",
|
|
73
|
-
inputSchema: {
|
|
74
|
-
type: "object",
|
|
75
|
-
properties: {
|
|
76
|
-
libraryId: {
|
|
77
|
-
type: "string",
|
|
78
|
-
description: "Library ID returned by resolve-library-id",
|
|
79
|
-
},
|
|
80
|
-
query: {
|
|
81
|
-
type: "string",
|
|
82
|
-
description: "Question or topic to search in the documentation",
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
required: ["libraryId", "query"],
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
cachedAt: 0,
|
|
90
|
-
},
|
|
91
|
-
exa: {
|
|
92
|
-
description: "Exa web search",
|
|
93
|
-
tools: [
|
|
94
|
-
{
|
|
95
|
-
name: "web_search_exa",
|
|
96
|
-
description: "Search the web for any topic and get results",
|
|
97
|
-
inputSchema: {
|
|
98
|
-
type: "object",
|
|
99
|
-
properties: {
|
|
100
|
-
query: {
|
|
101
|
-
type: "string",
|
|
102
|
-
description: "Search query",
|
|
103
|
-
},
|
|
104
|
-
numResults: {
|
|
105
|
-
type: "number",
|
|
106
|
-
description: "Number of results to return (default: 10)",
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
required: ["query"],
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
name: "web_fetch_exa",
|
|
114
|
-
description: "Read webpage content from specific URLs",
|
|
115
|
-
inputSchema: {
|
|
116
|
-
type: "object",
|
|
117
|
-
properties: {
|
|
118
|
-
urls: {
|
|
119
|
-
type: "array",
|
|
120
|
-
items: { type: "string" },
|
|
121
|
-
description: "URLs to fetch content from",
|
|
122
|
-
},
|
|
123
|
-
maxCharacters: {
|
|
124
|
-
type: "number",
|
|
125
|
-
description: "Maximum characters per page to return",
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
required: ["urls"],
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
],
|
|
132
|
-
cachedAt: 0,
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
|
|
137
36
|
// ── Project-level config discovery ─────────────────────────────────────────
|
|
138
37
|
|
|
139
38
|
function readMcpJson(filePath: string): Record<string, { url?: string; command?: string; args?: string[]; env?: Record<string, string>; enabled?: boolean; description?: string }> | null {
|
|
@@ -331,9 +230,9 @@ function writeCacheFile(p: string, cache: McpCache): void {
|
|
|
331
230
|
fs.renameSync(tmp, p);
|
|
332
231
|
}
|
|
333
232
|
|
|
334
|
-
/** Load merged cache:
|
|
233
|
+
/** Load merged cache: global + project. */
|
|
335
234
|
export function loadMcpCache(cwd?: string): McpCache | null {
|
|
336
|
-
const merged: McpCache = { servers: {
|
|
235
|
+
const merged: McpCache = { servers: {} };
|
|
337
236
|
|
|
338
237
|
const globalCache = readCacheFile(globalCachePath());
|
|
339
238
|
if (globalCache) {
|
|
@@ -428,5 +327,16 @@ function cleanupOneCache(p: string, names: Set<string>): void {
|
|
|
428
327
|
export function cleanupStaleCache(configs: McpServerConfig[], cwd?: string): void {
|
|
429
328
|
const names = new Set(configs.map(c => c.name));
|
|
430
329
|
cleanupOneCache(globalCachePath(), names);
|
|
431
|
-
if (cwd)
|
|
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
|
+
}
|
|
432
342
|
}
|
package/extensions/mcp/index.ts
CHANGED
|
@@ -2,21 +2,15 @@ import { keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-wor
|
|
|
2
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { McpConnection } from "./client.js";
|
|
4
4
|
import {
|
|
5
|
-
resolveMcpConfigs,
|
|
5
|
+
resolveMcpConfigs,
|
|
6
6
|
loadMcpCache, updateServerCache, cleanupStaleCache,
|
|
7
7
|
type McpServerConfig, type McpToolCache,
|
|
8
8
|
} from "./builtin.js";
|
|
9
|
-
import {
|
|
10
|
-
getMcpBrokerModelKey, getCompactModelKey,
|
|
11
|
-
getMcpDescription, setMcpDescription,
|
|
12
|
-
parseModelKey,
|
|
13
|
-
} from "../settings.js";
|
|
14
9
|
|
|
15
10
|
export interface McpServerStatus {
|
|
16
11
|
name: string;
|
|
17
12
|
url: string;
|
|
18
13
|
source: string;
|
|
19
|
-
description?: string;
|
|
20
14
|
state: "connecting" | "connected" | "failed" | "disabled";
|
|
21
15
|
toolCount: number;
|
|
22
16
|
tools: Array<{ name: string; description?: string; inputSchema?: Record<string, unknown> }>;
|
|
@@ -90,10 +84,6 @@ export function updateConfigEnabled(serverName: string, enabled: boolean): void
|
|
|
90
84
|
|
|
91
85
|
// ── helpers ───────────────────────────────────────────────────────────────
|
|
92
86
|
|
|
93
|
-
function serverDescription(s: McpServerConfig): string | undefined {
|
|
94
|
-
return s.description || getMcpDescription(s.name, cachedCwd);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
87
|
function makeToolName(serverName: string, toolName: string): string {
|
|
98
88
|
return `${serverName}_${toolName}`;
|
|
99
89
|
}
|
|
@@ -108,67 +98,6 @@ function cacheScopeForSource(source: string): "global" | "project" {
|
|
|
108
98
|
return source === "project" ? "project" : "global";
|
|
109
99
|
}
|
|
110
100
|
|
|
111
|
-
// ── auto-summary ──────────────────────────────────────────────────────────
|
|
112
|
-
|
|
113
|
-
async function autoDescribeServer(
|
|
114
|
-
conn: McpConnection,
|
|
115
|
-
serverName: string,
|
|
116
|
-
registry: any,
|
|
117
|
-
): Promise<string> {
|
|
118
|
-
const descs = conn.tools.map(t => `- ${t.name}: ${t.description || "(no description)"}`).join("\n");
|
|
119
|
-
|
|
120
|
-
const prompt = `Describe what this MCP server is and what it does, based on the tools it exposes. Start with action verbs directly, like a capability summary.
|
|
121
|
-
|
|
122
|
-
Server: "${serverName}"
|
|
123
|
-
Tools:
|
|
124
|
-
${descs}
|
|
125
|
-
|
|
126
|
-
Respond with ONLY one short sentence. No quotes.`;
|
|
127
|
-
|
|
128
|
-
return await summarizeWithBroker(registry, prompt) || `${serverName} MCP server (${conn.tools.length} tools)`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function summarizeWithBroker(registry: any, prompt: string): Promise<string | undefined> {
|
|
132
|
-
if (!registry) return undefined;
|
|
133
|
-
|
|
134
|
-
const brokerKey = getMcpBrokerModelKey() || getCompactModelKey();
|
|
135
|
-
const model = brokerKey
|
|
136
|
-
? (() => {
|
|
137
|
-
const parsed = parseModelKey(brokerKey);
|
|
138
|
-
return parsed ? registry.find(parsed.provider, parsed.modelId) : undefined;
|
|
139
|
-
})()
|
|
140
|
-
: undefined;
|
|
141
|
-
|
|
142
|
-
if (!model) return undefined;
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const auth = await registry.getApiKeyAndHeaders(model);
|
|
146
|
-
if (!auth.ok) return undefined;
|
|
147
|
-
|
|
148
|
-
const { complete } = await import("@earendil-works/pi-ai");
|
|
149
|
-
const resp = await complete(model, {
|
|
150
|
-
systemPrompt: "You are a concise MCP server description generator.",
|
|
151
|
-
messages: [{
|
|
152
|
-
role: "user" as const,
|
|
153
|
-
content: [{ type: "text" as const, text: prompt }],
|
|
154
|
-
timestamp: Date.now(),
|
|
155
|
-
}],
|
|
156
|
-
}, {
|
|
157
|
-
maxTokens: 128,
|
|
158
|
-
apiKey: auth.apiKey ?? "",
|
|
159
|
-
headers: auth.headers,
|
|
160
|
-
signal: AbortSignal.timeout(15_000),
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
if (resp.stopReason === "error") return undefined;
|
|
164
|
-
return resp.content
|
|
165
|
-
.filter((c: any): c is { type: "text"; text: string } => c.type === "text")
|
|
166
|
-
.map((c: any) => c.text).join(" ").trim() || undefined;
|
|
167
|
-
} catch {
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
101
|
// ── register cached tools ─────────────────────────────────────────────────
|
|
173
102
|
|
|
174
103
|
function registerCachedTools(pi: ExtensionAPI, configs: McpServerConfig[]): void {
|
|
@@ -218,7 +147,11 @@ function registerCachedTools(pi: ExtensionAPI, configs: McpServerConfig[]): void
|
|
|
218
147
|
|
|
219
148
|
// ── connect ───────────────────────────────────────────────────────────────
|
|
220
149
|
|
|
221
|
-
async function connectAll(configs: McpServerConfig[],
|
|
150
|
+
async function connectAll(configs: McpServerConfig[], ui?: { notify: (msg: string, type: string) => void }): Promise<void> {
|
|
151
|
+
// Load current cache for comparison
|
|
152
|
+
const cache = loadMcpCache(cachedCwd);
|
|
153
|
+
const schemaChanges: string[] = [];
|
|
154
|
+
|
|
222
155
|
allServers = new Map(
|
|
223
156
|
configs.map((s) => [
|
|
224
157
|
s.name,
|
|
@@ -226,7 +159,6 @@ async function connectAll(configs: McpServerConfig[], registry: any): Promise<vo
|
|
|
226
159
|
name: s.name,
|
|
227
160
|
url: s.url ?? s.command ?? "(unknown)",
|
|
228
161
|
source: s.source,
|
|
229
|
-
description: serverDescription(s),
|
|
230
162
|
state: "connecting" as const,
|
|
231
163
|
toolCount: 0,
|
|
232
164
|
tools: [],
|
|
@@ -243,12 +175,30 @@ async function connectAll(configs: McpServerConfig[], registry: any): Promise<vo
|
|
|
243
175
|
await conn.connect(30_000);
|
|
244
176
|
activeConnections.push(conn);
|
|
245
177
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
178
|
+
const actualTools = conn.tools.map((t) => ({
|
|
179
|
+
name: t.name,
|
|
180
|
+
description: t.description,
|
|
181
|
+
inputSchema: t.inputSchema,
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
// Check if schema changed
|
|
185
|
+
const cachedEntry = cache?.servers[server.name];
|
|
186
|
+
if (cachedEntry && cachedEntry.tools.length > 0) {
|
|
187
|
+
const cachedToolNames = new Set(cachedEntry.tools.map(t => t.name));
|
|
188
|
+
const actualToolNames = new Set(actualTools.map(t => t.name));
|
|
189
|
+
const added = actualTools.filter(t => !cachedToolNames.has(t.name));
|
|
190
|
+
const removed = cachedEntry.tools.filter(t => !actualToolNames.has(t.name));
|
|
191
|
+
const changed = actualTools.filter(t => {
|
|
192
|
+
const cached = cachedEntry.tools.find(ct => ct.name === t.name);
|
|
193
|
+
return cached && JSON.stringify(cached.inputSchema) !== JSON.stringify(t.inputSchema);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (added.length > 0 || removed.length > 0 || changed.length > 0) {
|
|
197
|
+
const parts: string[] = [];
|
|
198
|
+
if (added.length) parts.push(`${added.length} added`);
|
|
199
|
+
if (removed.length) parts.push(`${removed.length} removed`);
|
|
200
|
+
if (changed.length) parts.push(`${changed.length} changed`);
|
|
201
|
+
schemaChanges.push(`${server.name} (${parts.join(', ')})`);
|
|
252
202
|
}
|
|
253
203
|
}
|
|
254
204
|
|
|
@@ -256,14 +206,9 @@ async function connectAll(configs: McpServerConfig[], registry: any): Promise<vo
|
|
|
256
206
|
name: server.name,
|
|
257
207
|
url: server.url ?? server.command ?? "(unknown)",
|
|
258
208
|
source: server.source,
|
|
259
|
-
description: desc,
|
|
260
209
|
state: "connected",
|
|
261
210
|
toolCount: conn.tools.length,
|
|
262
|
-
tools:
|
|
263
|
-
name: t.name,
|
|
264
|
-
description: t.description,
|
|
265
|
-
inputSchema: t.inputSchema,
|
|
266
|
-
})),
|
|
211
|
+
tools: actualTools,
|
|
267
212
|
});
|
|
268
213
|
|
|
269
214
|
// Update cache with this server's tools
|
|
@@ -274,7 +219,7 @@ async function connectAll(configs: McpServerConfig[], registry: any): Promise<vo
|
|
|
274
219
|
}));
|
|
275
220
|
updateServerCache(
|
|
276
221
|
server.name,
|
|
277
|
-
{
|
|
222
|
+
{ tools, cachedAt: Date.now() },
|
|
278
223
|
cacheScopeForSource(server.source),
|
|
279
224
|
cachedCwd || undefined,
|
|
280
225
|
);
|
|
@@ -284,7 +229,6 @@ async function connectAll(configs: McpServerConfig[], registry: any): Promise<vo
|
|
|
284
229
|
name: server.name,
|
|
285
230
|
url: server.url ?? server.command ?? "(unknown)",
|
|
286
231
|
source: server.source,
|
|
287
|
-
description: serverDescription(server),
|
|
288
232
|
state: "failed",
|
|
289
233
|
toolCount: 0,
|
|
290
234
|
tools: [],
|
|
@@ -296,6 +240,11 @@ async function connectAll(configs: McpServerConfig[], registry: any): Promise<vo
|
|
|
296
240
|
|
|
297
241
|
await connectPromise;
|
|
298
242
|
connectPromise = null;
|
|
243
|
+
|
|
244
|
+
// Notify about schema changes
|
|
245
|
+
if (schemaChanges.length > 0 && ui) {
|
|
246
|
+
ui.notify(`MCP schema updated: ${schemaChanges.join('; ')}. Run /reload to apply.`, "warning");
|
|
247
|
+
}
|
|
299
248
|
}
|
|
300
249
|
|
|
301
250
|
// ── setup ─────────────────────────────────────────────────────────────────
|
|
@@ -317,16 +266,8 @@ export function setupMcp(pi: ExtensionAPI) {
|
|
|
317
266
|
// Register tools from cache — prompt-stable, works even if MCP is down
|
|
318
267
|
registerCachedTools(pi, enabledConfigs);
|
|
319
268
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (needSummary.length === 0) {
|
|
323
|
-
// All servers have descriptions — connect in background, update cache
|
|
324
|
-
void connectAll(enabledConfigs, ctx.modelRegistry);
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Some servers lack description — connect and auto-summarize synchronously
|
|
329
|
-
await connectAll(enabledConfigs, ctx.modelRegistry);
|
|
269
|
+
// Connect in background, pass UI for schema change notifications
|
|
270
|
+
void connectAll(enabledConfigs, ctx.hasUI ? ctx.ui : undefined);
|
|
330
271
|
});
|
|
331
272
|
|
|
332
273
|
pi.on("session_shutdown", () => {
|
|
@@ -347,7 +288,6 @@ export function getMcpStatus(): McpServerStatus[] {
|
|
|
347
288
|
name: config.name,
|
|
348
289
|
url: config.url ?? config.command ?? "(unknown)",
|
|
349
290
|
source: config.source,
|
|
350
|
-
description: serverDescription(config),
|
|
351
291
|
state: config.enabled ? "connecting" : "disabled",
|
|
352
292
|
toolCount: cachedEntry?.tools.length ?? 0,
|
|
353
293
|
tools: cachedEntry?.tools ?? [],
|
|
@@ -382,20 +322,10 @@ export async function refreshServerCache(
|
|
|
382
322
|
await conn.connect(30_000);
|
|
383
323
|
activeConnections.push(conn);
|
|
384
324
|
|
|
385
|
-
let desc = serverDescription(config);
|
|
386
|
-
if (!desc) {
|
|
387
|
-
desc = await autoDescribeServer(conn, config.name, registry);
|
|
388
|
-
if (desc) {
|
|
389
|
-
if (config.source === "project") saveProjectMcpDescription(cachedCwd, config.name, desc);
|
|
390
|
-
else setMcpDescription(config.name, desc);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
325
|
allServers.set(config.name, {
|
|
395
326
|
name: config.name,
|
|
396
327
|
url: config.url ?? config.command ?? "(unknown)",
|
|
397
328
|
source: config.source,
|
|
398
|
-
description: desc,
|
|
399
329
|
state: "connected",
|
|
400
330
|
toolCount: conn.tools.length,
|
|
401
331
|
tools: conn.tools.map(t => ({
|
|
@@ -412,7 +342,7 @@ export async function refreshServerCache(
|
|
|
412
342
|
}));
|
|
413
343
|
updateServerCache(
|
|
414
344
|
config.name,
|
|
415
|
-
{
|
|
345
|
+
{ tools, cachedAt: Date.now() },
|
|
416
346
|
cacheScopeForSource(config.source),
|
|
417
347
|
cachedCwd || undefined,
|
|
418
348
|
);
|
|
@@ -424,7 +354,6 @@ export async function refreshServerCache(
|
|
|
424
354
|
name: config.name,
|
|
425
355
|
url: config.url ?? config.command ?? "(unknown)",
|
|
426
356
|
source: config.source,
|
|
427
|
-
description: serverDescription(config),
|
|
428
357
|
state: "failed",
|
|
429
358
|
toolCount: 0,
|
|
430
359
|
tools: [],
|
|
@@ -25,8 +25,8 @@ import { fileTypeFromFile } from "file-type";
|
|
|
25
25
|
import { isContextOverflow, type Model } from "@earendil-works/pi-ai";
|
|
26
26
|
import {
|
|
27
27
|
loadConfig, saveConfig, parseModelKey, formatModelKey,
|
|
28
|
-
getImageModelKey, getCompactModelKey,
|
|
29
|
-
setImageModelKey, setCompactModelKey,
|
|
28
|
+
getImageModelKey, getCompactModelKey,
|
|
29
|
+
setImageModelKey, setCompactModelKey,
|
|
30
30
|
} from "./settings.js";
|
|
31
31
|
import * as fs from "node:fs";
|
|
32
32
|
import * as os from "node:os";
|
|
@@ -172,7 +172,6 @@ function setupImageReadFallback(pi: ExtensionAPI) {
|
|
|
172
172
|
|
|
173
173
|
const TAB_IMAGE = 0;
|
|
174
174
|
const TAB_COMPACT = 1;
|
|
175
|
-
const TAB_BROKER = 2;
|
|
176
175
|
|
|
177
176
|
export class ModelPickerComponent extends Container {
|
|
178
177
|
private searchInput: Input;
|
|
@@ -183,7 +182,6 @@ export class ModelPickerComponent extends Container {
|
|
|
183
182
|
private activeTab = TAB_IMAGE;
|
|
184
183
|
private imageKey: string | null;
|
|
185
184
|
private compactKey: string | null;
|
|
186
|
-
private brokerKey: string | null;
|
|
187
185
|
private allItems: { label: string; desc: string; model: Model<any> | null; modelName?: string }[] = [];
|
|
188
186
|
private filtered: typeof this.allItems = [];
|
|
189
187
|
private selectedIndex = 0;
|
|
@@ -199,7 +197,6 @@ export class ModelPickerComponent extends Container {
|
|
|
199
197
|
this.onDone = onDone;
|
|
200
198
|
this.imageKey = getImageModelKey();
|
|
201
199
|
this.compactKey = getCompactModelKey();
|
|
202
|
-
this.brokerKey = getMcpBrokerModelKey();
|
|
203
200
|
|
|
204
201
|
this.addChild(new DynamicBorder());
|
|
205
202
|
this.addChild(new Spacer(1));
|
|
@@ -235,8 +232,8 @@ export class ModelPickerComponent extends Container {
|
|
|
235
232
|
}
|
|
236
233
|
}
|
|
237
234
|
|
|
238
|
-
private currentKey() { return this.activeTab === TAB_IMAGE ? this.imageKey : this.
|
|
239
|
-
private currentKind() { return this.activeTab === TAB_IMAGE ? "image" :
|
|
235
|
+
private currentKey() { return this.activeTab === TAB_IMAGE ? this.imageKey : this.compactKey; }
|
|
236
|
+
private currentKind() { return this.activeTab === TAB_IMAGE ? "image" : "compact"; }
|
|
240
237
|
|
|
241
238
|
private switchTab(tab: number) {
|
|
242
239
|
this.activeTab = tab;
|
|
@@ -263,11 +260,9 @@ export class ModelPickerComponent extends Container {
|
|
|
263
260
|
const t = this.theme;
|
|
264
261
|
const im = this.activeTab === TAB_IMAGE ? t.fg("accent", "●") : "○";
|
|
265
262
|
const cm = this.activeTab === TAB_COMPACT ? t.fg("accent", "●") : "○";
|
|
266
|
-
const bm = this.activeTab === TAB_BROKER ? t.fg("accent", "●") : "○";
|
|
267
263
|
const il = this.activeTab === TAB_IMAGE ? t.bold("Image") : t.fg("dim", "Image");
|
|
268
264
|
const cl = this.activeTab === TAB_COMPACT ? t.bold("Compact") : t.fg("dim", "Compact");
|
|
269
|
-
|
|
270
|
-
this.tabTitle.setText(`${im} ${il} | ${cm} ${cl} | ${bm} ${bl}`);
|
|
265
|
+
this.tabTitle.setText(`${im} ${il} | ${cm} ${cl}`);
|
|
271
266
|
const key = this.currentKey();
|
|
272
267
|
this.subtitleText.setText(key ? t.fg("warning", `Current ${this.currentKind()} model: ${key}`) : t.fg("warning", `No ${this.currentKind()} model set`));
|
|
273
268
|
}
|
|
@@ -275,7 +270,7 @@ export class ModelPickerComponent extends Container {
|
|
|
275
270
|
handleInput(keyData: string) {
|
|
276
271
|
const kb = getKeybindings();
|
|
277
272
|
if (kb.matches(keyData, "tui.input.tab")) {
|
|
278
|
-
const next = this.activeTab === TAB_IMAGE ? TAB_COMPACT :
|
|
273
|
+
const next = this.activeTab === TAB_IMAGE ? TAB_COMPACT : TAB_IMAGE;
|
|
279
274
|
this.switchTab(next); this.tui.requestRender(); return;
|
|
280
275
|
}
|
|
281
276
|
if (kb.matches(keyData, "tui.select.up")) { this.selectedIndex = this.selectedIndex === 0 ? this.filtered.length - 1 : this.selectedIndex - 1; this.updateList(); return; }
|
|
@@ -297,15 +292,12 @@ export class ModelPickerComponent extends Container {
|
|
|
297
292
|
const kind = this.currentKind();
|
|
298
293
|
if (model) {
|
|
299
294
|
if (kind === "image") setImageModelKey(formatModelKey(model));
|
|
300
|
-
else if (kind === "broker") setMcpBrokerModelKey(formatModelKey(model));
|
|
301
295
|
else setCompactModelKey(formatModelKey(model));
|
|
302
296
|
} else {
|
|
303
297
|
if (kind === "image") setImageModelKey(null);
|
|
304
|
-
else if (kind === "broker") setMcpBrokerModelKey(null);
|
|
305
298
|
else setCompactModelKey(null);
|
|
306
299
|
}
|
|
307
300
|
if (kind === "image") this.imageKey = model ? formatModelKey(model) : null;
|
|
308
|
-
else if (kind === "broker") this.brokerKey = model ? formatModelKey(model) : null;
|
|
309
301
|
else this.compactKey = model ? formatModelKey(model) : null;
|
|
310
302
|
this.switchTab(this.activeTab); this.tui.requestRender();
|
|
311
303
|
}
|
package/extensions/rtk.ts
CHANGED
|
@@ -73,32 +73,7 @@ export function buildRtkCommand(raw: string, rtkBinaryPath: string): string {
|
|
|
73
73
|
return `export PATH=${shellQuote(binDir)}:$PATH && ${raw}`;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function extractMainCommand(command: string): string {
|
|
77
|
-
let cmd = command.trim().toLowerCase();
|
|
78
|
-
cmd = cmd.replace(/^cd\s+\S+\s*(&&|;|\n)\s*/, "");
|
|
79
|
-
cmd = cmd.replace(/^(?:[a-z_][a-z0-9_]*=\S*\s+)+/, "");
|
|
80
|
-
const prefixes = ["sudo ", "time ", "nohup ", "nice ", "env "];
|
|
81
|
-
let changed = true;
|
|
82
|
-
while (changed) {
|
|
83
|
-
changed = false;
|
|
84
|
-
for (const prefix of prefixes) {
|
|
85
|
-
if (cmd.startsWith(prefix)) {
|
|
86
|
-
cmd = cmd.slice(prefix.length);
|
|
87
|
-
changed = true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return cmd;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function shouldBypassRtkRewrite(command: string): boolean {
|
|
95
|
-
const main = extractMainCommand(command);
|
|
96
|
-
if (!main.startsWith("find ") && main !== "find") return false;
|
|
97
|
-
return /(^|\s)(-o|-or|-a|-and|-not|!|\(|\)|-exec|-ok|-delete|-prune|-printf|-print0)(\s|$)/.test(main);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
76
|
export function rewriteWithRtk(command: string, rtkPath: string): string | null {
|
|
101
|
-
if (shouldBypassRtkRewrite(command)) return null;
|
|
102
77
|
|
|
103
78
|
// NOTE:
|
|
104
79
|
// Some RTK versions return a non-zero exit code even when `rtk rewrite`
|
package/extensions/settings.ts
CHANGED
|
@@ -47,8 +47,6 @@ export interface ModuleSettings {
|
|
|
47
47
|
export interface DecoratedPiConfig {
|
|
48
48
|
imageModelKey?: string | null;
|
|
49
49
|
compactModelKey?: string | null;
|
|
50
|
-
mcpBrokerModelKey?: string | null;
|
|
51
|
-
mcpDescriptions?: Record<string, string>;
|
|
52
50
|
providers?: Record<string, ProviderCache>;
|
|
53
51
|
modules?: ModuleSettings;
|
|
54
52
|
mcpServers?: Record<string, McpServerEntry>;
|
|
@@ -143,36 +141,6 @@ export function setCompactModelKey(key: string | null) {
|
|
|
143
141
|
saveConfig({ compactModelKey: key });
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
export function getMcpBrokerModelKey(): string | null {
|
|
147
|
-
return loadConfig().mcpBrokerModelKey ?? null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function setMcpBrokerModelKey(key: string | null) {
|
|
151
|
-
saveConfig({ mcpBrokerModelKey: key });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function getMcpDescription(name: string, cwd?: string): string | undefined {
|
|
155
|
-
if (cwd) {
|
|
156
|
-
const projectCfg = loadProjectConfig(cwd);
|
|
157
|
-
if (projectCfg.mcpDescriptions?.[name]) return projectCfg.mcpDescriptions[name];
|
|
158
|
-
}
|
|
159
|
-
return loadConfig().mcpDescriptions?.[name];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function setMcpDescription(name: string, description: string, cwd?: string) {
|
|
163
|
-
if (cwd) {
|
|
164
|
-
const cfg = loadProjectConfig(cwd);
|
|
165
|
-
const descriptions = { ...cfg.mcpDescriptions };
|
|
166
|
-
descriptions[name] = description;
|
|
167
|
-
saveProjectConfig(cwd, { mcpDescriptions: descriptions });
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const cfg = loadConfig();
|
|
171
|
-
const descriptions = { ...cfg.mcpDescriptions };
|
|
172
|
-
descriptions[name] = description;
|
|
173
|
-
saveConfig({ mcpDescriptions: descriptions });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
144
|
// ─── Module Switches ──────────────────────────────────────────────────────────
|
|
177
145
|
|
|
178
146
|
const DEFAULT_MODULES: Required<ModuleSettings> = {
|
package/extensions/slash.ts
CHANGED
|
@@ -45,7 +45,7 @@ function getSettingsListTheme(theme: PiTheme): SettingsListTheme {
|
|
|
45
45
|
|
|
46
46
|
function setupDpModelCommand(pi: ExtensionAPI) {
|
|
47
47
|
pi.registerCommand("dp-model", {
|
|
48
|
-
description: "Configure image
|
|
48
|
+
description: "Configure image and compact models",
|
|
49
49
|
handler: async (_args, ctx) => {
|
|
50
50
|
if (ctx.hasUI) {
|
|
51
51
|
await ctx.ui.custom<void>(
|
package/package.json
CHANGED
package/extensions/guidance.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
const DECORATED_PI_GUIDANCE_MARKER = "## Decorated Pi Guidance";
|
|
4
|
-
|
|
5
|
-
export function setupGuidance(pi: ExtensionAPI) {
|
|
6
|
-
pi.on("before_agent_start", async (event) => {
|
|
7
|
-
// Remove "Current date: YYYY-MM-DD" from system prompt to improve cache stability
|
|
8
|
-
let prompt = event.systemPrompt.replace(/\nCurrent date: \d{4}-\d{2}-\d{2}/, "");
|
|
9
|
-
|
|
10
|
-
if (!prompt.includes(DECORATED_PI_GUIDANCE_MARKER)) {
|
|
11
|
-
const guidance = [
|
|
12
|
-
DECORATED_PI_GUIDANCE_MARKER,
|
|
13
|
-
"",
|
|
14
|
-
"- 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.",
|
|
15
|
-
"- Look before you leap! Ensure you have conducted thorough research before taking any action.",
|
|
16
|
-
"- Exercise caution when performing any **write** operations, especially when you are in a research or exploration phase.",
|
|
17
|
-
"- 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.",
|
|
18
|
-
"- CAUTION: Do not perform write operations in the following directories unless explicitly instructed: `node_modules`, `venv`, `env`, `__pycache__`, `.git` or any other hidden directories.",
|
|
19
|
-
"",
|
|
20
|
-
"### Secret Redaction",
|
|
21
|
-
"",
|
|
22
|
-
"- 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.",
|
|
23
|
-
].join("\n");
|
|
24
|
-
|
|
25
|
-
prompt = `${prompt}\n\n${guidance}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return { systemPrompt: prompt };
|
|
29
|
-
});
|
|
30
|
-
}
|