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 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
- | Aspect | Pi native `@` | `decorated-pi` |
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** | ❌ re‑scans filesystem on every trigger | ✅ caches once per `@` trigger |
54
- | **Noise filtering** | ❌ no penalty system, shows hidden files | ✅ tiered penalty auto‑filters clutter |
55
- | **Default suggestions** | ❌ all files visible on empty query | ✅ only visible project files |
56
- | **Match precision** | ❌ case‑insensitive simple scoring | ✅ multi‑level case‑sensitive scoring |
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/decorated-pi.json` (global). Project overrides global. Tool prompts and schemas are cached locally so MCP tools are available immediately on startup — no manual `/reload` required even on first install.
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
- "patch": true,
133
- "safety": true,
134
- "rtk": true,
135
- "lsp": true,
136
- "smart-at": true,
137
- "mcp": true,
138
- "wakatime": true
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
  ```
@@ -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
- ctx.ui.notify("Module settings updated. /reload to apply.", "warning");
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,
@@ -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
- const title = extractFirstMessage(ctx.sessionManager.getBranch());
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
  ],