decorated-pi 0.7.0 → 0.7.2
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 +38 -12
- package/hooks/inject-agents-md.ts +20 -12
- package/hooks/mcp.ts +61 -64
- package/hooks/pi-tool-filter.ts +2 -1
- package/hooks/rtk.ts +3 -2
- package/hooks/skeleton.ts +46 -18
- package/hooks/smart-at.ts +122 -264
- package/hooks/wakatime.ts +3 -2
- package/index.ts +117 -3
- package/package.json +9 -4
- package/settings.ts +5 -3
- package/skills/pi-docs/SKILL.md +13 -0
- package/tools/lsp/tools.ts +0 -1
- package/tools/mcp/builtin/codegraph.ts +1 -1
- package/tools/mcp/client.ts +1 -1
- package/tools/patch/core.ts +17 -1
- package/tools/patch/index.ts +338 -225
package/README.md
CHANGED
|
@@ -14,25 +14,30 @@ pi install /path/to/decorated-pi
|
|
|
14
14
|
|
|
15
15
|
### 1. Token Efficiency
|
|
16
16
|
|
|
17
|
-
Multiple layers of token savings that compound across every session.
|
|
17
|
+
Multiple layers of token savings that compound across every session.
|
|
18
18
|
|
|
19
|
-
**RTK** — integrates [RTK](https://github.com/rtk-ai/rtk) to compress bash output into structured summaries, so the LLM never sees raw noise.
|
|
19
|
+
**RTK** — integrates [RTK](https://github.com/rtk-ai/rtk) to compress bash output into structured summaries, so the LLM never sees raw noise. **Just install the CLI, zero config**.
|
|
20
20
|
|
|
21
|
-
**
|
|
21
|
+
**Codegraph** — integrates [codegraph](https://github.com/colbymchenry/codegraph) to offer a code map of your project, so the LLM can navigate symbols and call graphs without chaining `ls` → `grep` → `read`. **You should manage the code source index yourself, see[codegraph doc](https://github.com/colbymchenry/codegraph/blob/main/README.md)**.
|
|
22
22
|
|
|
23
23
|
**Auxiliary Models** — offloads heavy-but-dumb tasks to cheaper models so your primary model only pays for the hard work:
|
|
24
24
|
|
|
25
|
-
- **Image
|
|
26
|
-
- **Compact
|
|
25
|
+
- **Image Read Fallback** — detects image type via magic bytes, calls a configured vision-capable model, and injects the analysis text, so your main model never touches image tokens
|
|
26
|
+
- **Compact Model** — handles context compaction with a smaller model instead of burning main-model capacity
|
|
27
27
|
|
|
28
|
-
Configured via `/dp-model`.
|
|
28
|
+
> Configured via `/dp-model`.
|
|
29
29
|
|
|
30
|
-
**Cache‑friendly
|
|
30
|
+
**Cache‑friendly Design** — stable system prompt prefix:
|
|
31
31
|
|
|
32
32
|
- tool definitions, guidelines, and skills are sorted alphabetically so the system prompt is identical across sessions
|
|
33
33
|
- volatile elements like `Current date: …` are stripped before prompt assembly
|
|
34
34
|
- MCP tool schemas are persisted to a local cache, so the tool list stays stable regardless of network conditions or server availability
|
|
35
35
|
|
|
36
|
+
**Pi Native Prompt Slimming**
|
|
37
|
+
|
|
38
|
+
- move the default Pi documentation block out of the system prompt and into a builtin `pi-docs` skill, so the docs reference loads on demand instead of sitting in every turn's prompt
|
|
39
|
+
- unregister the native `write` tool, since `bash` can handle it
|
|
40
|
+
|
|
36
41
|
### 2. Smarter Tools
|
|
37
42
|
|
|
38
43
|
Drop‑in replacements for Pi's built‑in tools, with better UX and fewer wasted turns.
|
|
@@ -48,12 +53,33 @@ Drop‑in replacements for Pi's built‑in tools, with better UX and fewer waste
|
|
|
48
53
|
|
|
49
54
|
#### Smarter `@` File Search
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
`decorated-pi` replaces pi's built-in `@` autocomplete with a high-speed file finder backed by **[`@ff-labs/fff-node`](https://github.com/dmtrKovalenko/fff)**— a Rust SIMD fuzzy file search engine with in-memory index, frecency ranking, and git status awareness. Pi's native provider shells out to `fd` on every keystroke.
|
|
57
|
+
|
|
58
|
+
| Aspect | Pi native `@` | `decorated-pi` (FFF) |
|
|
52
59
|
| ------ | :---: | :---: |
|
|
53
|
-
| **Speed** | ❌
|
|
54
|
-
| **
|
|
55
|
-
| **
|
|
56
|
-
|
|
60
|
+
| **Speed** | ❌ walks filesystem via `fd` subprocess on every keystroke | ✅ in‑memory index built once per session, ~0.1 ms / query |
|
|
61
|
+
| **Ranking** | ❌ 4‑bucket case‑sensitive score (exact/starts/contains/path) | ✅ fuzzy match + frecency + git status (boots from git log) |
|
|
62
|
+
| **Noise** | ❌ shows every file in the project, including `.git`, `node_modules`, `dist` | ✅ filters `git‑ignored` files; substring filter on path keeps short queries relevant |
|
|
63
|
+
|
|
64
|
+
###### Benchmark of `@`
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
┌─ smart-at benchmark
|
|
68
|
+
├─ generated 500,000 files in 3.9 s
|
|
69
|
+
├─ FFF scan complete in 664 ms
|
|
70
|
+
│ RSS after FFF index: 408 MB (+330 MB over baseline)
|
|
71
|
+
├─ accuracy (14 queries)
|
|
72
|
+
│ top‑1 top‑3 top‑5 false‑pos
|
|
73
|
+
│ smart-at 93% 93% 93% 0
|
|
74
|
+
│ native (fd) 86% 93% 93% 0
|
|
75
|
+
└─ corpus cleaned
|
|
76
|
+
|
|
77
|
+
name hz mean
|
|
78
|
+
· smart-at 8.10 123 ms
|
|
79
|
+
· native @ (fd subprocess) 3.12 320 ms
|
|
80
|
+
|
|
81
|
+
Summary: smart-at is 2.59x faster than native @ (fd subprocess)
|
|
82
|
+
```
|
|
57
83
|
|
|
58
84
|
#### LSP support
|
|
59
85
|
|
|
@@ -9,6 +9,17 @@ import { dirname, resolve, relative, join, normalize } from "node:path";
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import type { Module } from "./skeleton.js";
|
|
11
11
|
|
|
12
|
+
function isInsideSkillDir(filePath: string): boolean {
|
|
13
|
+
let dir = dirname(resolve(filePath));
|
|
14
|
+
while (true) {
|
|
15
|
+
if (existsSync(join(dir, "SKILL.md"))) return true;
|
|
16
|
+
const parent = dirname(dir);
|
|
17
|
+
if (parent === dir) break;
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
const CUSTOM_TYPE = "decorated-pi.subdir-agents";
|
|
13
24
|
const AGENTS_NAMES = ["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"];
|
|
14
25
|
|
|
@@ -86,7 +97,12 @@ function findNewAgents(
|
|
|
86
97
|
return results.reverse();
|
|
87
98
|
}
|
|
88
99
|
|
|
89
|
-
export const __subdirAgentsTest = { restoreFromBranch, findNewAgents };
|
|
100
|
+
export const __subdirAgentsTest = { restoreFromBranch, findNewAgents, isInsideSkillDir };
|
|
101
|
+
|
|
102
|
+
export const INJECT_AGENTS_MD_GUIDANCE = [
|
|
103
|
+
"### Context Loading, AGENTS.md / CLAUDE.md are auto-injected",
|
|
104
|
+
"- DO NOT read any **AGENTS.md** or **CLAUDE.md** files unless you're explicitly asked to, these files will loaded automatically if necessary.",
|
|
105
|
+
].join("\n");
|
|
90
106
|
|
|
91
107
|
export const injectAgentsMdModule: Module = {
|
|
92
108
|
name: "inject-agents-md",
|
|
@@ -111,7 +127,9 @@ export const injectAgentsMdModule: Module = {
|
|
|
111
127
|
if (event.toolName !== "read" && event.toolName !== "edit")
|
|
112
128
|
return;
|
|
113
129
|
const path = (event.input as { path?: string })?.path;
|
|
114
|
-
if (path)
|
|
130
|
+
if (!path) return;
|
|
131
|
+
if (isInsideSkillDir(resolve(path))) return;
|
|
132
|
+
pendingPaths.set(event.toolCallId, path);
|
|
115
133
|
},
|
|
116
134
|
],
|
|
117
135
|
tool_result: [
|
|
@@ -152,13 +170,3 @@ export const injectAgentsMdModule: Module = {
|
|
|
152
170
|
],
|
|
153
171
|
},
|
|
154
172
|
};
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* System-prompt guidance for the inject-agents-md hook — tells the
|
|
158
|
-
* LLM not to waste tool calls re-reading AGENTS.md / CLAUDE.md, since
|
|
159
|
-
* this hook already auto-injects them.
|
|
160
|
-
*/
|
|
161
|
-
export const INJECT_AGENTS_MD_GUIDANCE = [
|
|
162
|
-
"### Context Loading, AGENTS.md / CLAUDE.md are auto-injected",
|
|
163
|
-
"- DO NOT read any **AGENTS.md** or **CLAUDE.md** files unless you're explicitly asked to, these files will loaded automatically if necessary.",
|
|
164
|
-
].join("\n");
|
package/hooks/mcp.ts
CHANGED
|
@@ -21,7 +21,7 @@ interface ServerStatus {
|
|
|
21
21
|
name: string;
|
|
22
22
|
url: string;
|
|
23
23
|
source: string;
|
|
24
|
-
state: "connecting" | "connected" | "failed" | "disabled" | "
|
|
24
|
+
state: "connecting" | "connected" | "failed" | "disabled" | "waiting reload";
|
|
25
25
|
toolCount: number;
|
|
26
26
|
tools: Array<{ name: string; description?: string; inputSchema?: Record<string, unknown> }>;
|
|
27
27
|
error?: string;
|
|
@@ -36,6 +36,45 @@ function canUseServer(config: McpServerConfig, cwd?: string): boolean {
|
|
|
36
36
|
return config.canUseInProject(cwd ?? process.cwd());
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function markServerFailed(config: McpServerConfig, error: string): void {
|
|
40
|
+
allServers.set(config.name, {
|
|
41
|
+
name: config.name,
|
|
42
|
+
url: config.url ?? config.command ?? "(unknown)",
|
|
43
|
+
source: config.source,
|
|
44
|
+
state: "failed",
|
|
45
|
+
toolCount: 0,
|
|
46
|
+
tools: [],
|
|
47
|
+
error,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function markServerConnected(config: McpServerConfig, tools: McpToolCache[]): void {
|
|
52
|
+
allServers.set(config.name, {
|
|
53
|
+
name: config.name,
|
|
54
|
+
url: config.url ?? config.command ?? "(unknown)",
|
|
55
|
+
source: config.source,
|
|
56
|
+
state: "connected",
|
|
57
|
+
toolCount: tools.length,
|
|
58
|
+
tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function markServerState(
|
|
63
|
+
name: string,
|
|
64
|
+
state: "disabled" | "waiting reload" | "connecting",
|
|
65
|
+
config: { url?: string; command?: string; source: string } | undefined,
|
|
66
|
+
): void {
|
|
67
|
+
allServers.set(name, {
|
|
68
|
+
name,
|
|
69
|
+
url: config?.url ?? config?.command ?? "(unknown)",
|
|
70
|
+
source: config?.source ?? "unknown",
|
|
71
|
+
state,
|
|
72
|
+
toolCount: 0,
|
|
73
|
+
tools: [],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
39
78
|
async function connectAll(
|
|
40
79
|
configs: McpServerConfig[],
|
|
41
80
|
registry: any,
|
|
@@ -51,7 +90,7 @@ async function connectAll(
|
|
|
51
90
|
// every caller's updates are visible.
|
|
52
91
|
allServers.clear();
|
|
53
92
|
for (const s of configs) {
|
|
54
|
-
|
|
93
|
+
markServerState(s.name, "connecting", s);
|
|
55
94
|
}
|
|
56
95
|
|
|
57
96
|
// Watchdog: the inner Promise.race in McpConnection.connect() has a
|
|
@@ -87,11 +126,7 @@ async function connectAll(
|
|
|
87
126
|
if (watchdogFired) break; // remaining servers are already failed
|
|
88
127
|
|
|
89
128
|
if (!canUseServer(server, cachedCwd || undefined)) {
|
|
90
|
-
|
|
91
|
-
name: server.name, url: server.url ?? server.command ?? "(unknown)", source: server.source,
|
|
92
|
-
state: "failed", toolCount: 0, tools: [],
|
|
93
|
-
error: "Project is missing required artefacts for this server",
|
|
94
|
-
});
|
|
129
|
+
markServerFailed(server, "Project is missing required artefacts for this server");
|
|
95
130
|
continue;
|
|
96
131
|
}
|
|
97
132
|
|
|
@@ -108,11 +143,7 @@ async function connectAll(
|
|
|
108
143
|
// failed state and do NOT write an empty cache, otherwise a
|
|
109
144
|
// subsequent /reload will keep using the empty cache forever.
|
|
110
145
|
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
111
|
-
|
|
112
|
-
name: server.name, url: server.url ?? server.command ?? "(unknown)", source: server.source,
|
|
113
|
-
state: "failed", toolCount: 0, tools: [],
|
|
114
|
-
error: "Server connected but returned no tools (e.g. codegraph without a .codegraph index)",
|
|
115
|
-
});
|
|
146
|
+
markServerFailed(server, "Server connected but returned no tools (e.g. codegraph without a .codegraph index)");
|
|
116
147
|
continue;
|
|
117
148
|
}
|
|
118
149
|
activeConnections.push(conn);
|
|
@@ -134,19 +165,13 @@ async function connectAll(
|
|
|
134
165
|
schemaChanges.push(`${server.name} (${parts.join(', ')})`);
|
|
135
166
|
}
|
|
136
167
|
}
|
|
137
|
-
allServers.set(server.name, {
|
|
138
|
-
name: server.name, url: server.url ?? server.command ?? "(unknown)", source: server.source,
|
|
139
|
-
state: "connected", toolCount: conn.tools.length, tools: actualTools,
|
|
140
|
-
});
|
|
141
168
|
const tools: McpToolCache[] = conn.tools.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }));
|
|
169
|
+
markServerConnected(server, tools);
|
|
142
170
|
updateServerCache(server.name, { tools, cachedAt: Date.now() }, cacheScopeForSource(server.source), cachedCwd || undefined);
|
|
143
171
|
} catch (err) {
|
|
144
172
|
if (watchdogFired) return { schemaChanges, hasNewServer };
|
|
145
173
|
const msg = err instanceof Error ? err.message : String(err);
|
|
146
|
-
|
|
147
|
-
name: server.name, url: server.url ?? server.command ?? "(unknown)", source: server.source,
|
|
148
|
-
state: "failed", toolCount: 0, tools: [], error: msg,
|
|
149
|
-
});
|
|
174
|
+
markServerFailed(server, msg);
|
|
150
175
|
}
|
|
151
176
|
}
|
|
152
177
|
|
|
@@ -174,7 +199,7 @@ export function getMcpStatus(): ServerStatus[] {
|
|
|
174
199
|
// or was toggled enabled after startup and needs /reload.
|
|
175
200
|
result.push({
|
|
176
201
|
name: config.name, url: config.url ?? config.command ?? "(unknown)", source: config.source,
|
|
177
|
-
state: config.enabled ? "
|
|
202
|
+
state: config.enabled ? "waiting reload" : "disabled",
|
|
178
203
|
toolCount: cachedEntry?.tools.length ?? 0, tools: cachedEntry?.tools ?? [],
|
|
179
204
|
});
|
|
180
205
|
}
|
|
@@ -194,35 +219,23 @@ export async function updateConfigEnabled(serverName: string, enabled: boolean):
|
|
|
194
219
|
activeConnections = activeConnections.filter(c => c.serverName !== serverName);
|
|
195
220
|
}
|
|
196
221
|
// Mark the server as disabled in allServers (or add it if absent).
|
|
197
|
-
|
|
198
|
-
name: serverName,
|
|
199
|
-
url: config?.url ?? config?.command ?? "(unknown)",
|
|
200
|
-
source: config?.source ?? "unknown",
|
|
201
|
-
state: "disabled",
|
|
202
|
-
toolCount: 0,
|
|
203
|
-
tools: [],
|
|
204
|
-
});
|
|
222
|
+
markServerState(serverName, "disabled", config ?? undefined);
|
|
205
223
|
return;
|
|
206
224
|
}
|
|
207
225
|
|
|
208
226
|
// Re-enabling at runtime: the config is now enabled but tools aren't
|
|
209
227
|
// registered until the next session_start. Mark it as waiting_reload
|
|
210
228
|
// so the user sees exactly why the server isn't usable yet.
|
|
211
|
-
|
|
212
|
-
name: serverName,
|
|
213
|
-
url: config?.url ?? config?.command ?? "(unknown)",
|
|
214
|
-
source: config?.source ?? "unknown",
|
|
215
|
-
state: "waiting reload",
|
|
216
|
-
toolCount: 0,
|
|
217
|
-
tools: [],
|
|
218
|
-
});
|
|
229
|
+
markServerState(serverName, "waiting reload", config ?? undefined);
|
|
219
230
|
}
|
|
220
231
|
|
|
221
232
|
export async function refreshServerCache(serverName: string, registry: any): Promise<{ ok: boolean; error?: string }> {
|
|
222
233
|
const config = resolveMcpConfigs(cachedCwd).find(s => s.name === serverName);
|
|
223
234
|
if (!config) return { ok: false, error: `Server "${serverName}" not found in config.` };
|
|
224
235
|
if (!canUseServer(config, cachedCwd || undefined)) {
|
|
225
|
-
|
|
236
|
+
const error = "Project is missing required artefacts for this server";
|
|
237
|
+
markServerFailed(config, error);
|
|
238
|
+
return { ok: false, error };
|
|
226
239
|
}
|
|
227
240
|
const existing = activeConnections.find(c => c.serverName === serverName);
|
|
228
241
|
if (existing) {
|
|
@@ -236,21 +249,17 @@ export async function refreshServerCache(serverName: string, registry: any): Pro
|
|
|
236
249
|
if (conn.tools.length === 0) {
|
|
237
250
|
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
238
251
|
const error = "Server connected but returned no tools (e.g. codegraph without a .codegraph index)";
|
|
239
|
-
|
|
252
|
+
markServerFailed(config, error);
|
|
240
253
|
return { ok: false, error };
|
|
241
254
|
}
|
|
242
255
|
activeConnections.push(conn);
|
|
243
|
-
allServers.set(config.name, {
|
|
244
|
-
name: config.name, url: config.url ?? config.command ?? "(unknown)", source: config.source,
|
|
245
|
-
state: "connected", toolCount: conn.tools.length,
|
|
246
|
-
tools: conn.tools.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
|
|
247
|
-
});
|
|
248
256
|
const tools: McpToolCache[] = conn.tools.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }));
|
|
257
|
+
markServerConnected(config, tools);
|
|
249
258
|
updateServerCache(config.name, { tools, cachedAt: Date.now() }, cacheScopeForSource(config.source), cachedCwd || undefined);
|
|
250
259
|
return { ok: true };
|
|
251
260
|
} catch (err) {
|
|
252
261
|
const msg = err instanceof Error ? err.message : String(err);
|
|
253
|
-
|
|
262
|
+
markServerFailed(config, msg);
|
|
254
263
|
return { ok: false, error: msg };
|
|
255
264
|
}
|
|
256
265
|
}
|
|
@@ -277,7 +286,10 @@ export function getCachedMcpConfigs(): McpServerConfig[] {
|
|
|
277
286
|
* can find it.
|
|
278
287
|
*/
|
|
279
288
|
export async function ensureMcpServerReady(pi: ExtensionAPI, config: McpServerConfig, cwd?: string): Promise<void> {
|
|
280
|
-
if (!canUseServer(config, cwd))
|
|
289
|
+
if (!canUseServer(config, cwd)) {
|
|
290
|
+
markServerFailed(config, "Project is missing required artefacts for this server");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
281
293
|
|
|
282
294
|
const scope = cacheScopeForSource(config.source);
|
|
283
295
|
const cache = loadScopedMcpCache(scope, cwd);
|
|
@@ -303,15 +315,7 @@ export async function ensureMcpServerReady(pi: ExtensionAPI, config: McpServerCo
|
|
|
303
315
|
// Empty tool list means the server is not useful. Disconnect and
|
|
304
316
|
// leave cache untouched so the next /reload retries the connection.
|
|
305
317
|
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
306
|
-
|
|
307
|
-
name: config.name,
|
|
308
|
-
url: config.url ?? config.command ?? "(unknown)",
|
|
309
|
-
source: config.source,
|
|
310
|
-
state: "failed",
|
|
311
|
-
toolCount: 0,
|
|
312
|
-
tools: [],
|
|
313
|
-
error: "Server connected but returned no tools (e.g. codegraph without a .codegraph index)",
|
|
314
|
-
});
|
|
318
|
+
markServerFailed(config, "Server connected but returned no tools (e.g. codegraph without a .codegraph index)");
|
|
315
319
|
return;
|
|
316
320
|
}
|
|
317
321
|
activeConnections.push(conn);
|
|
@@ -320,6 +324,7 @@ export async function ensureMcpServerReady(pi: ExtensionAPI, config: McpServerCo
|
|
|
320
324
|
description: t.description,
|
|
321
325
|
inputSchema: t.inputSchema,
|
|
322
326
|
}));
|
|
327
|
+
markServerConnected(config, tools);
|
|
323
328
|
updateServerCache(config.name, { tools, cachedAt: Date.now() }, scope, cwd);
|
|
324
329
|
for (const t of tools) {
|
|
325
330
|
try {
|
|
@@ -328,15 +333,7 @@ export async function ensureMcpServerReady(pi: ExtensionAPI, config: McpServerCo
|
|
|
328
333
|
}
|
|
329
334
|
} catch (err) {
|
|
330
335
|
const msg = err instanceof Error ? err.message : String(err);
|
|
331
|
-
|
|
332
|
-
name: config.name,
|
|
333
|
-
url: config.url ?? config.command ?? "(unknown)",
|
|
334
|
-
source: config.source,
|
|
335
|
-
state: "failed",
|
|
336
|
-
toolCount: 0,
|
|
337
|
-
tools: [],
|
|
338
|
-
error: `No cache and initial connect failed: ${msg}`,
|
|
339
|
-
});
|
|
336
|
+
markServerFailed(config, `No cache and initial connect failed: ${msg}`);
|
|
340
337
|
}
|
|
341
338
|
}
|
|
342
339
|
|
package/hooks/pi-tool-filter.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* pi-tool-filter — unregister pi native tools that are replaced by our extensions.
|
|
3
3
|
*
|
|
4
4
|
* edit → replaced by patch
|
|
5
|
+
* write → replaced by patch
|
|
5
6
|
* grep → replaced by bash
|
|
6
7
|
* find → replaced by bash
|
|
7
8
|
* ls → replaced by bash
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
|
|
10
11
|
import type { Module, Skeleton } from "./skeleton.js";
|
|
11
12
|
|
|
12
|
-
const TOOLS_TO_DROP = new Set(["edit", "grep", "find", "ls"]);
|
|
13
|
+
const TOOLS_TO_DROP = new Set(["edit", "write", "grep", "find", "ls"]);
|
|
13
14
|
|
|
14
15
|
export const piToolFilterModule: Module = {
|
|
15
16
|
name: "pi-tool-filter",
|
package/hooks/rtk.ts
CHANGED
|
@@ -160,12 +160,13 @@ export const rtkModule: Module = {
|
|
|
160
160
|
|
|
161
161
|
export function setupRtk(sk: Skeleton, pi: ExtensionAPI): void {
|
|
162
162
|
rtkBinary = findSystemRtk();
|
|
163
|
-
sk.declareDependency({
|
|
163
|
+
const ready = sk.declareDependency({
|
|
164
164
|
label: "rtk",
|
|
165
|
+
module: "rtk",
|
|
165
166
|
check: () => findSystemRtk() !== null,
|
|
166
167
|
hint: "Install RTK so bash rewrite/tagging can activate.",
|
|
167
168
|
});
|
|
168
|
-
if (!
|
|
169
|
+
if (!ready) return;
|
|
169
170
|
|
|
170
171
|
// Register a wrapped bash tool that shows [RTK] tag in TUI.
|
|
171
172
|
const shellSettings = loadPiShellSettings(process.cwd());
|
package/hooks/skeleton.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Skeleton — the only place that calls pi.on(...) for hooks.
|
|
3
3
|
*
|
|
4
4
|
* sk.register(module) → installs module's hook handlers
|
|
5
|
-
* sk.declareDependency({...}) →
|
|
5
|
+
* sk.declareDependency({...}) → check now, report on session_start
|
|
6
6
|
* sk.declareGuideline("...") → skeleton injects on before_agent_start
|
|
7
|
-
* sk.install(pi)
|
|
7
|
+
* sk.install(pi) → call once, after all setup<X> calls
|
|
8
8
|
*
|
|
9
9
|
* Handlers receive (event, ctx, pi) so they can call pi.* APIs
|
|
10
10
|
* (setSessionName, registerTool, appendEntry, etc.).
|
|
@@ -64,6 +64,8 @@ export interface Dependency {
|
|
|
64
64
|
label: string;
|
|
65
65
|
check: () => boolean;
|
|
66
66
|
hint?: string;
|
|
67
|
+
/** Display/source tag for inspection and notifications. */
|
|
68
|
+
module?: string;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/** Collected result shape for a module's declared dependency. Modules
|
|
@@ -88,7 +90,8 @@ const COMPOSE_EVENTS = new Set<HookEvent>([
|
|
|
88
90
|
|
|
89
91
|
export interface Skeleton {
|
|
90
92
|
register(module: Module): void;
|
|
91
|
-
|
|
93
|
+
/** Returns whether the dependency check passed right now. */
|
|
94
|
+
declareDependency(dep: Dependency): boolean;
|
|
92
95
|
install(pi: ExtensionAPI): void;
|
|
93
96
|
inspect(): Inspection;
|
|
94
97
|
}
|
|
@@ -96,13 +99,14 @@ export interface Skeleton {
|
|
|
96
99
|
export interface Inspection {
|
|
97
100
|
modules: string[];
|
|
98
101
|
events: Record<string, Array<{ module: string; order: number }>>;
|
|
99
|
-
dependencies: Array<{ label: string; hint?: string }>;
|
|
102
|
+
dependencies: Array<{ label: string; module?: string; hint?: string }>;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
export function createSkeleton(): Skeleton {
|
|
103
106
|
const modules: Module[] = [];
|
|
104
107
|
const registry = new Map<HookEvent, Array<{ module: string; handler: (event: any, ctx: ExtensionContext, pi: ExtensionAPI) => any }>>();
|
|
105
108
|
const dependencies: Dependency[] = [];
|
|
109
|
+
let dependencyNotifyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
106
110
|
|
|
107
111
|
function collect(mod: Module) {
|
|
108
112
|
for (const [event, handlers] of Object.entries(mod.hooks)) {
|
|
@@ -123,6 +127,11 @@ export function createSkeleton(): Skeleton {
|
|
|
123
127
|
|
|
124
128
|
declareDependency(dep) {
|
|
125
129
|
dependencies.push(dep);
|
|
130
|
+
try {
|
|
131
|
+
return dep.check();
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
126
135
|
},
|
|
127
136
|
|
|
128
137
|
install(pi) {
|
|
@@ -146,21 +155,40 @@ export function createSkeleton(): Skeleton {
|
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
// Skeleton-owned: dependency check on session_start.
|
|
149
|
-
|
|
158
|
+
// Defer with setTimeout(0) so the notification fires AFTER pi's
|
|
159
|
+
// startup/reload UI rebuild (rebuildChatFromMessages); otherwise it
|
|
160
|
+
// is appended to the chat and immediately wiped.
|
|
161
|
+
const runDependencyCheck = (ctx: ExtensionContext) => {
|
|
150
162
|
if (!ctx.hasUI) return;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (
|
|
163
|
+
if (dependencyNotifyTimer) clearTimeout(dependencyNotifyTimer);
|
|
164
|
+
dependencyNotifyTimer = setTimeout(() => {
|
|
165
|
+
dependencyNotifyTimer = undefined;
|
|
166
|
+
const missing: string[] = [];
|
|
167
|
+
for (const dep of dependencies) {
|
|
168
|
+
let ok = false;
|
|
169
|
+
try { ok = dep.check(); } catch { ok = false; }
|
|
170
|
+
if (!ok) missing.push(dep.label);
|
|
159
171
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
172
|
+
if (missing.length) {
|
|
173
|
+
try {
|
|
174
|
+
ctx.ui.notify(`[decorated-pi] missing dependencies: ${missing.join(", ")}`, "info");
|
|
175
|
+
} catch {
|
|
176
|
+
// Extension context may be stale if another reload/session switch happened.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}, 0);
|
|
180
|
+
};
|
|
181
|
+
pi.on("session_start", async (event: any, ctx: ExtensionContext) => {
|
|
182
|
+
// Only check on cold startup and explicit reload — other reasons
|
|
183
|
+
// (new/resume/fork) inherit the existing session's tool/runtime
|
|
184
|
+
// and don't need a fresh check.
|
|
185
|
+
if (event.reason !== "startup" && event.reason !== "reload") return;
|
|
186
|
+
runDependencyCheck(ctx);
|
|
187
|
+
});
|
|
188
|
+
pi.on("session_shutdown", async () => {
|
|
189
|
+
if (!dependencyNotifyTimer) return;
|
|
190
|
+
clearTimeout(dependencyNotifyTimer);
|
|
191
|
+
dependencyNotifyTimer = undefined;
|
|
164
192
|
});
|
|
165
193
|
|
|
166
194
|
// Skeleton-owned: system-prompt options sort for cache stability.
|
|
@@ -179,7 +207,7 @@ export function createSkeleton(): Skeleton {
|
|
|
179
207
|
return {
|
|
180
208
|
modules: modules.map((m) => m.name),
|
|
181
209
|
events,
|
|
182
|
-
dependencies: dependencies.map((d) => ({ label: d.label, hint: d.hint })),
|
|
210
|
+
dependencies: dependencies.map((d) => ({ label: d.label, module: d.module, hint: d.hint })),
|
|
183
211
|
};
|
|
184
212
|
},
|
|
185
213
|
};
|