decorated-pi 0.6.0 → 0.7.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 +45 -15
- package/commands/dp-settings.ts +6 -1
- package/hooks/mcp.ts +52 -0
- package/hooks/session-title.ts +25 -1
- package/hooks/smart-at.ts +114 -264
- package/index.ts +33 -22
- package/package.json +13 -5
- package/settings.ts +170 -31
- package/tools/ask/index.ts +93 -0
- package/tools/mcp/builtin/codegraph.ts +10 -34
- package/tools/mcp/builtin/index.ts +2 -2
- package/tools/mcp/config.ts +93 -21
- package/tools/patch/core.ts +7 -1
- package/tools/patch/index.ts +3 -2
- package/ui/ask.ts +443 -0
- package/ui/module-settings.ts +131 -26
- package/ui/usage.ts +4 -4
- package/.codegraph/daemon.pid +0 -6
- package/AGENTS.md +0 -92
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ 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. **All integrated CLI tools only require installing their respective CLIs — zero config**.
|
|
18
18
|
|
|
19
19
|
**RTK** — integrates [RTK](https://github.com/rtk-ai/rtk) to compress bash output into structured summaries, so the LLM never sees raw noise.
|
|
20
20
|
|
|
@@ -48,12 +48,33 @@ Drop‑in replacements for Pi's built‑in tools, with better UX and fewer waste
|
|
|
48
48
|
|
|
49
49
|
#### Smarter `@` File Search
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
`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.
|
|
52
|
+
|
|
53
|
+
| Aspect | Pi native `@` | `decorated-pi` (FFF) |
|
|
52
54
|
| ------ | :---: | :---: |
|
|
53
|
-
| **Speed** | ❌
|
|
54
|
-
| **
|
|
55
|
-
| **
|
|
56
|
-
|
|
55
|
+
| **Speed** | ❌ walks filesystem via `fd` subprocess on every keystroke | ✅ in‑memory index built once per session, ~0.1 ms / query |
|
|
56
|
+
| **Ranking** | ❌ 4‑bucket case‑sensitive score (exact/starts/contains/path) | ✅ fuzzy match + frecency + git status (boots from git log) |
|
|
57
|
+
| **Noise** | ❌ shows every file in the project, including `.git`, `node_modules`, `dist` | ✅ filters `git‑ignored` files; substring filter on path keeps short queries relevant |
|
|
58
|
+
|
|
59
|
+
###### Benchmark of `@`
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
┌─ smart-at benchmark
|
|
63
|
+
├─ generated 500,000 files in 3.9 s
|
|
64
|
+
├─ FFF scan complete in 664 ms
|
|
65
|
+
│ RSS after FFF index: 408 MB (+330 MB over baseline)
|
|
66
|
+
├─ accuracy (14 queries)
|
|
67
|
+
│ top‑1 top‑3 top‑5 false‑pos
|
|
68
|
+
│ smart-at 93% 93% 93% 0
|
|
69
|
+
│ native (fd) 86% 93% 93% 0
|
|
70
|
+
└─ corpus cleaned
|
|
71
|
+
|
|
72
|
+
name hz mean
|
|
73
|
+
· smart-at 8.10 123 ms
|
|
74
|
+
· native @ (fd subprocess) 3.12 320 ms
|
|
75
|
+
|
|
76
|
+
Summary: smart-at is 2.59x faster than native @ (fd subprocess)
|
|
77
|
+
```
|
|
57
78
|
|
|
58
79
|
#### LSP support
|
|
59
80
|
|
|
@@ -73,7 +94,7 @@ Zero-config MCP client with built-in servers:
|
|
|
73
94
|
| Exa | `exa_*` | `https://mcp.exa.ai/mcp` |
|
|
74
95
|
| codegraph | `codegraph_*` | bundled binary |
|
|
75
96
|
|
|
76
|
-
**Custom servers** in `.pi/agent/mcp.json` (project) or `~/.pi/agent/
|
|
97
|
+
**Custom servers** in `.pi/agent/mcp.json` (project) or `~/.pi/agent/mcp.json` (global). Project overrides global. Tool prompts and schemas are cached locally so MCP tools are available immediately on startup.
|
|
77
98
|
|
|
78
99
|
```json
|
|
79
100
|
{
|
|
@@ -118,8 +139,8 @@ Example redaction on a `read` / `bash` output:
|
|
|
118
139
|
### 5. Other
|
|
119
140
|
|
|
120
141
|
- `/usage` — token stats with cache‑hit rate, per‑model breakdown (Session / Today / This Week / This Month / All Time)
|
|
121
|
-
- Progressive context — supports subdirectory `AGENTS.md` / `CLAUDE.md` discovery and injection
|
|
122
142
|
- `/retry` — continue after interruption
|
|
143
|
+
- Progressive context — supports subdirectory `AGENTS.md` / `CLAUDE.md` discovery and injection
|
|
123
144
|
- **WakaTime** — coding activity tracking via [WakaTime](https://wakatime.com)
|
|
124
145
|
|
|
125
146
|
## Configuration
|
|
@@ -129,13 +150,22 @@ Runtime settings in `~/.pi/agent/decorated-pi.json`. Modules can be toggled via
|
|
|
129
150
|
```json
|
|
130
151
|
{
|
|
131
152
|
"modules": {
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"
|
|
153
|
+
"tools": {
|
|
154
|
+
"patchOverrideEdit": true,
|
|
155
|
+
"ask": true,
|
|
156
|
+
"lsp": true,
|
|
157
|
+
"mcp": true
|
|
158
|
+
},
|
|
159
|
+
"hooks": {
|
|
160
|
+
"secretRedaction": true,
|
|
161
|
+
"rtk": true,
|
|
162
|
+
"wakatime": true
|
|
163
|
+
},
|
|
164
|
+
"commands": {
|
|
165
|
+
"atOverride": true,
|
|
166
|
+
"retry": true,
|
|
167
|
+
"usage": true
|
|
168
|
+
}
|
|
139
169
|
}
|
|
140
170
|
}
|
|
141
171
|
```
|
package/commands/dp-settings.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { moduleSnapshotChanged } from "../settings.js";
|
|
6
7
|
import { ModuleSettingsComponent } from "../ui/module-settings.js";
|
|
7
8
|
|
|
8
9
|
export function registerDpSettingsCommand(pi: ExtensionAPI): void {
|
|
@@ -14,7 +15,11 @@ export function registerDpSettingsCommand(pi: ExtensionAPI): void {
|
|
|
14
15
|
(tui, theme, _kb, done) =>
|
|
15
16
|
new ModuleSettingsComponent(tui, theme, () => done(undefined))
|
|
16
17
|
);
|
|
17
|
-
|
|
18
|
+
// Only prompt for reload when the effective settings differ from
|
|
19
|
+
// the snapshot taken when pi loaded the extension.
|
|
20
|
+
if (moduleSnapshotChanged()) {
|
|
21
|
+
ctx.ui.notify("Module settings updated. /reload to apply.", "warning");
|
|
22
|
+
}
|
|
18
23
|
return;
|
|
19
24
|
}
|
|
20
25
|
ctx.ui.notify("dp-settings requires interactive mode.", "warning");
|
package/hooks/mcp.ts
CHANGED
|
@@ -31,6 +31,11 @@ function cacheScopeForSource(source: string): "global" | "project" {
|
|
|
31
31
|
return source === "project" ? "project" : "global";
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function canUseServer(config: McpServerConfig, cwd?: string): boolean {
|
|
35
|
+
if (!config.canUseInProject) return true;
|
|
36
|
+
return config.canUseInProject(cwd ?? process.cwd());
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
async function connectAll(
|
|
35
40
|
configs: McpServerConfig[],
|
|
36
41
|
registry: any,
|
|
@@ -81,6 +86,15 @@ async function connectAll(
|
|
|
81
86
|
for (const server of configs) {
|
|
82
87
|
if (watchdogFired) break; // remaining servers are already failed
|
|
83
88
|
|
|
89
|
+
if (!canUseServer(server, cachedCwd || undefined)) {
|
|
90
|
+
allServers.set(server.name, {
|
|
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
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
const conn = new McpConnection(server.name, server);
|
|
85
99
|
conn.source = server.source;
|
|
86
100
|
try {
|
|
@@ -89,6 +103,18 @@ async function connectAll(
|
|
|
89
103
|
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
90
104
|
return { schemaChanges, hasNewServer };
|
|
91
105
|
}
|
|
106
|
+
if (conn.tools.length === 0) {
|
|
107
|
+
// Server connected but advertises no tools. Treat this as a
|
|
108
|
+
// failed state and do NOT write an empty cache, otherwise a
|
|
109
|
+
// subsequent /reload will keep using the empty cache forever.
|
|
110
|
+
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
111
|
+
allServers.set(server.name, {
|
|
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
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
92
118
|
activeConnections.push(conn);
|
|
93
119
|
const actualTools = conn.tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }));
|
|
94
120
|
const cachedEntry = cache?.servers[server.name];
|
|
@@ -195,6 +221,9 @@ export async function updateConfigEnabled(serverName: string, enabled: boolean):
|
|
|
195
221
|
export async function refreshServerCache(serverName: string, registry: any): Promise<{ ok: boolean; error?: string }> {
|
|
196
222
|
const config = resolveMcpConfigs(cachedCwd).find(s => s.name === serverName);
|
|
197
223
|
if (!config) return { ok: false, error: `Server "${serverName}" not found in config.` };
|
|
224
|
+
if (!canUseServer(config, cachedCwd || undefined)) {
|
|
225
|
+
return { ok: false, error: "Project is missing required artefacts for this server" };
|
|
226
|
+
}
|
|
198
227
|
const existing = activeConnections.find(c => c.serverName === serverName);
|
|
199
228
|
if (existing) {
|
|
200
229
|
try { await existing.disconnect(); } catch { /* ignore */ }
|
|
@@ -204,6 +233,12 @@ export async function refreshServerCache(serverName: string, registry: any): Pro
|
|
|
204
233
|
conn.source = config.source;
|
|
205
234
|
try {
|
|
206
235
|
await conn.connect(30_000);
|
|
236
|
+
if (conn.tools.length === 0) {
|
|
237
|
+
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
238
|
+
const error = "Server connected but returned no tools (e.g. codegraph without a .codegraph index)";
|
|
239
|
+
allServers.set(config.name, { name: config.name, url: config.url ?? config.command ?? "(unknown)", source: config.source, state: "failed", toolCount: 0, tools: [], error });
|
|
240
|
+
return { ok: false, error };
|
|
241
|
+
}
|
|
207
242
|
activeConnections.push(conn);
|
|
208
243
|
allServers.set(config.name, {
|
|
209
244
|
name: config.name, url: config.url ?? config.command ?? "(unknown)", source: config.source,
|
|
@@ -242,6 +277,8 @@ export function getCachedMcpConfigs(): McpServerConfig[] {
|
|
|
242
277
|
* can find it.
|
|
243
278
|
*/
|
|
244
279
|
export async function ensureMcpServerReady(pi: ExtensionAPI, config: McpServerConfig, cwd?: string): Promise<void> {
|
|
280
|
+
if (!canUseServer(config, cwd)) return;
|
|
281
|
+
|
|
245
282
|
const scope = cacheScopeForSource(config.source);
|
|
246
283
|
const cache = loadScopedMcpCache(scope, cwd);
|
|
247
284
|
const entry = cache?.servers[config.name];
|
|
@@ -262,6 +299,21 @@ export async function ensureMcpServerReady(pi: ExtensionAPI, config: McpServerCo
|
|
|
262
299
|
conn.source = config.source;
|
|
263
300
|
try {
|
|
264
301
|
await conn.connect(30_000);
|
|
302
|
+
if (conn.tools.length === 0) {
|
|
303
|
+
// Empty tool list means the server is not useful. Disconnect and
|
|
304
|
+
// leave cache untouched so the next /reload retries the connection.
|
|
305
|
+
try { await conn.disconnect(); } catch { /* ignore */ }
|
|
306
|
+
allServers.set(config.name, {
|
|
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
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
265
317
|
activeConnections.push(conn);
|
|
266
318
|
const tools: McpToolCache[] = conn.tools.map(t => ({
|
|
267
319
|
name: t.name,
|
package/hooks/session-title.ts
CHANGED
|
@@ -40,13 +40,37 @@ export function extractFirstMessage(entries: SessionEntryLike[]): string | undef
|
|
|
40
40
|
return undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function normalizeTitle(text: string): string | undefined {
|
|
44
|
+
const nl = text.indexOf("\n");
|
|
45
|
+
const oneLine = (nl === -1 ? text : text.slice(0, nl)).trim();
|
|
46
|
+
if (!oneLine) return undefined;
|
|
47
|
+
if (oneLine.length <= MAX_SESSION_TITLE_LENGTH) return oneLine;
|
|
48
|
+
return oneLine.slice(0, MAX_SESSION_TITLE_LENGTH - 1) + "…";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function trySetSessionName(ctx: any, pi: ExtensionAPI): void {
|
|
52
|
+
if (ctx.sessionManager.getSessionName()) return;
|
|
53
|
+
const title = extractFirstMessage(ctx.sessionManager.getBranch());
|
|
54
|
+
if (title) (pi as any).setSessionName(title);
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
export const sessionTitleModule: Module = {
|
|
44
58
|
name: "session-title",
|
|
45
59
|
hooks: {
|
|
46
60
|
session_start: [
|
|
47
61
|
(_event, ctx, pi) => {
|
|
62
|
+
trySetSessionName(ctx, pi);
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
input: [
|
|
66
|
+
(event, ctx, pi) => {
|
|
67
|
+
// Skip if already named, not a fresh interactive message,
|
|
68
|
+
// or stream steering/follow-up messages.
|
|
48
69
|
if (ctx.sessionManager.getSessionName()) return;
|
|
49
|
-
|
|
70
|
+
if (event.source === "extension") return;
|
|
71
|
+
if (event.streamingBehavior) return;
|
|
72
|
+
const text = typeof event.text === "string" ? event.text.trim() : "";
|
|
73
|
+
const title = normalizeTitle(text);
|
|
50
74
|
if (title) (pi as any).setSessionName(title);
|
|
51
75
|
},
|
|
52
76
|
],
|