decorated-pi 0.7.1 → 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 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. **All integrated CLI tools only require installing their respective CLIs — zero config**.
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
- **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`.
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 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
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 design** — stable system prompt prefix:
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.
@@ -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) pendingPaths.set(event.toolCallId, 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" | "waiting_reload";
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
- allServers.set(s.name, { name: s.name, url: s.url ?? s.command ?? "(unknown)", source: s.source, state: "connecting", toolCount: 0, tools: [] });
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
- 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
- });
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
- 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
- });
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
- allServers.set(server.name, {
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 ? "waiting_reload" : "disabled",
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
- allServers.set(serverName, {
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
- allServers.set(serverName, {
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
- return { ok: false, error: "Project is missing required artefacts for this server" };
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
- allServers.set(config.name, { name: config.name, url: config.url ?? config.command ?? "(unknown)", source: config.source, state: "failed", toolCount: 0, tools: [], error });
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
- allServers.set(config.name, { name: config.name, url: config.url ?? config.command ?? "(unknown)", source: config.source, state: "failed", toolCount: 0, tools: [], error: msg });
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)) return;
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
- 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
- });
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
- allServers.set(config.name, {
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
 
@@ -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 (!rtkBinary) return;
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({...}) → skeleton checks on session_start
5
+ * sk.declareDependency({...}) → check now, report on session_start
6
6
  * sk.declareGuideline("...") → skeleton injects on before_agent_start
7
- * sk.install(pi) → call once, after all setup<X> calls
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
- declareDependency(dep: Dependency): void;
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
- pi.on("session_start", async (_event: any, ctx: ExtensionContext) => {
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
- const hints: string[] = [];
152
- let anyMissing = false;
153
- for (const dep of dependencies) {
154
- let ok = false;
155
- try { ok = dep.check(); } catch { ok = false; }
156
- if (!ok) {
157
- anyMissing = true;
158
- if (dep.hint) hints.push(` [${dep.label}] ${dep.hint}`);
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
- if (anyMissing) {
162
- ctx.ui.notify(`[decorated-pi] missing dependencies:\n${hints.join("\n")}`, "info");
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
  };
package/hooks/smart-at.ts CHANGED
@@ -5,6 +5,14 @@
5
5
  * FFF maintains an in-memory index, scores files by fuzzy match +
6
6
  * frecency + git status, and returns ranked results. We create one
7
7
  * FileFinder per session and query it directly for every @ prefix.
8
+ *
9
+ * KNOWN FFF LIMITATION (v0.9.4):
10
+ * FFF only returns directories that directly contain files. Intermediate
11
+ * folders that only hold subdirectories (e.g. product/module/apmanage/ where
12
+ * all files live in product/module/apmanage/deep/) will not appear in
13
+ * mixedSearch/directorySearch results, even though the files underneath do.
14
+ * We intentionally do NOT synthesize these directories on the TypeScript side
15
+ * to avoid extra complexity/cost; the fix belongs upstream.
8
16
  */
9
17
 
10
18
  import { FileFinder, type MixedItem } from "@ff-labs/fff-node";
package/hooks/wakatime.ts CHANGED
@@ -342,12 +342,13 @@ export function setupWakatimeWithApiKey(
342
342
  export function setupWakatime(sk: Skeleton, pi: ExtensionAPI): void {
343
343
  const apiKey = readWakatimeCfgApiKey();
344
344
  const cliPath = findWakatimeCli();
345
- sk.declareDependency({
345
+ const ready = sk.declareDependency({
346
346
  label: "wakatime-cli",
347
+ module: "wakatime",
347
348
  check: () => findWakatimeCli() !== null,
348
349
  hint: "Install wakatime-cli to track coding activity.",
349
350
  });
350
- if (!apiKey || !cliPath) return;
351
+ if (!apiKey || !cliPath || !ready) return;
351
352
 
352
353
  const sendHeartbeat: HeartbeatSender = (hb, cwd) => sendHeartbeatViaCli(hb, apiKey, cliPath, cwd);
353
354