decorated-pi 0.5.5 → 0.7.0

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.
Files changed (70) hide show
  1. package/README.md +69 -71
  2. package/commands/dp-model.ts +23 -0
  3. package/commands/dp-settings.ts +28 -0
  4. package/commands/mcp-status.ts +62 -0
  5. package/commands/retry.ts +19 -0
  6. package/commands/usage.ts +544 -0
  7. package/hooks/compaction.ts +204 -0
  8. package/hooks/externalize.ts +70 -0
  9. package/hooks/image-vision.ts +132 -0
  10. package/hooks/inject-agents-md.ts +164 -0
  11. package/hooks/mcp.ts +392 -0
  12. package/hooks/normalize-codeblocks.ts +88 -0
  13. package/hooks/pi-tool-filter.ts +28 -0
  14. package/{extensions/safety → hooks/redact}/types.ts +1 -8
  15. package/hooks/redact.ts +104 -0
  16. package/{extensions → hooks}/rtk.ts +92 -115
  17. package/hooks/session-title.ts +78 -0
  18. package/hooks/skeleton.ts +212 -0
  19. package/hooks/smart-at.ts +318 -0
  20. package/{extensions/file-times.ts → hooks/track-mtime.ts} +40 -38
  21. package/{extensions → hooks}/wakatime.ts +120 -122
  22. package/index.ts +166 -1
  23. package/package.json +8 -6
  24. package/settings.ts +341 -0
  25. package/tools/ask/index.ts +93 -0
  26. package/{extensions → tools}/lsp/client.ts +0 -25
  27. package/{extensions → tools}/lsp/format.ts +2 -99
  28. package/{extensions → tools}/lsp/index.ts +1 -1
  29. package/{extensions → tools}/lsp/servers.ts +1 -1
  30. package/{extensions → tools}/lsp/tools.ts +1 -66
  31. package/{extensions → tools}/lsp/types.ts +0 -11
  32. package/tools/mcp/builtin/codegraph.ts +21 -0
  33. package/tools/mcp/builtin/context7.ts +10 -0
  34. package/tools/mcp/builtin/exa.ts +10 -0
  35. package/tools/mcp/builtin/index.ts +19 -0
  36. package/tools/mcp/cache.ts +124 -0
  37. package/{extensions → tools}/mcp/client.ts +2 -2
  38. package/tools/mcp/config.ts +288 -0
  39. package/tools/mcp/index.ts +44 -0
  40. package/tools/mcp/tool-definition.ts +112 -0
  41. package/{extensions/patch.ts → tools/patch/core.ts} +135 -43
  42. package/{extensions/io.ts → tools/patch/index.ts} +24 -206
  43. package/tsconfig.json +10 -1
  44. package/ui/ask.ts +443 -0
  45. package/ui/mcp-status.ts +202 -0
  46. package/ui/model-picker.ts +162 -0
  47. package/ui/module-settings.ts +188 -0
  48. package/ui/usage.ts +396 -0
  49. package/extensions/index.ts +0 -154
  50. package/extensions/io-tool-output.ts +0 -33
  51. package/extensions/mcp/builtin.ts +0 -376
  52. package/extensions/mcp/index.ts +0 -435
  53. package/extensions/model-integration.ts +0 -531
  54. package/extensions/providers/ark-coding.ts +0 -75
  55. package/extensions/providers/index.ts +0 -9
  56. package/extensions/providers/ollama-cloud.ts +0 -101
  57. package/extensions/providers/qianfan-coding.ts +0 -71
  58. package/extensions/safety/index.ts +0 -102
  59. package/extensions/session-title.ts +0 -65
  60. package/extensions/settings.ts +0 -170
  61. package/extensions/slash.ts +0 -458
  62. package/extensions/smart-at.ts +0 -481
  63. package/extensions/subdir-agents.ts +0 -151
  64. /package/{extensions/safety → hooks/redact}/detect.ts +0 -0
  65. /package/{extensions/safety → hooks/redact}/entropy.ts +0 -0
  66. /package/{extensions/safety → hooks/redact}/patterns.ts +0 -0
  67. /package/{extensions → tools}/lsp/env.ts +0 -0
  68. /package/{extensions → tools}/lsp/manager.ts +0 -0
  69. /package/{extensions → tools}/lsp/prompt.ts +0 -0
  70. /package/{extensions → tools}/lsp/protocol.ts +0 -0
package/settings.ts ADDED
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Settings — 配置读写(唯一写文件)
3
+ *
4
+ * 所有其他模块只通过此文件访问 decorated-pi.json
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import * as os from "node:os";
9
+ import type { Model } from "@earendil-works/pi-ai";
10
+
11
+ const CONFIG_DIR = path.join(os.homedir(), ".pi", "agent");
12
+ const CONFIG_FILE = path.join(CONFIG_DIR, "decorated-pi.json");
13
+
14
+ export interface ProviderModelEntry {
15
+ id: string;
16
+ name: string;
17
+ reasoning: boolean;
18
+ contextWindow: number;
19
+ maxTokens: number;
20
+ input: ("text" | "image")[];
21
+ }
22
+
23
+ export interface ProviderCache {
24
+ lastSynced?: string;
25
+ models: ProviderModelEntry[];
26
+ }
27
+
28
+ export interface McpServerEntry {
29
+ url?: string;
30
+ command?: string;
31
+ args?: string[];
32
+ env?: Record<string, string>;
33
+ enabled?: boolean;
34
+ description?: string;
35
+ }
36
+
37
+ export interface ModuleSettings {
38
+ tools?: {
39
+ /** Replace Pi native edit/write with the patch tool. */
40
+ patchOverrideEdit?: boolean;
41
+ /** Interactive ask tool for user clarification (blocks loop until answered). */
42
+ ask?: boolean;
43
+ /** Language server diagnostics, hover, definition, references, symbols, rename. */
44
+ lsp?: boolean;
45
+ /** MCP client with builtin servers (context7, exa, codegraph). */
46
+ mcp?: boolean;
47
+ };
48
+ hooks?: {
49
+ /** Redact secrets from read/bash output before model context. */
50
+ secretRedaction?: boolean;
51
+ /** Rewrite bash through system RTK when available. */
52
+ "rtk"?: boolean;
53
+ /** Send coding activity heartbeats to WakaTime. */
54
+ wakatime?: boolean;
55
+ };
56
+ commands?: {
57
+ /** Project-aware file search replacing default autocomplete. */
58
+ atOverride?: boolean;
59
+ /** /retry command to continue after interruption. */
60
+ retry?: boolean;
61
+ /** /usage command for token stats. */
62
+ usage?: boolean;
63
+ };
64
+ }
65
+
66
+ export interface UsageIndexEntry {
67
+ inode: number;
68
+ size: number;
69
+ mtime: number;
70
+ }
71
+
72
+ export interface DecoratedPiConfig {
73
+ imageModelKey?: string | null;
74
+ compactModelKey?: string | null;
75
+ providers?: Record<string, ProviderCache>;
76
+ modules?: ModuleSettings;
77
+ mcpServers?: Record<string, McpServerEntry>;
78
+ usageIndex?: Record<string, UsageIndexEntry>;
79
+ }
80
+
81
+ export function loadConfig(): DecoratedPiConfig {
82
+ try {
83
+ if (fs.existsSync(CONFIG_FILE)) {
84
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")) as DecoratedPiConfig;
85
+ if (migrateModuleSettings(config)) {
86
+ saveConfig(config);
87
+ }
88
+ return config;
89
+ }
90
+ } catch {}
91
+ return {};
92
+ }
93
+
94
+ export function saveConfig(config: Partial<DecoratedPiConfig>) {
95
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
96
+ const current = loadConfig();
97
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...config }, null, 2), "utf-8");
98
+ }
99
+
100
+ // ─── 辅助 ──────────────────────────────────────────────────────────────────
101
+
102
+ export function formatModelKey(m: Model<any>): string {
103
+ return `${m.provider}/${m.id}`;
104
+ }
105
+
106
+ export function parseModelKey(key: string): { provider: string; modelId: string } | null {
107
+ const i = key.indexOf("/");
108
+ if (i === -1) return null;
109
+ return { provider: key.slice(0, i), modelId: key.slice(i + 1) };
110
+ }
111
+
112
+ // ─── Provider ────────────────────────────────────────────────────────────────
113
+
114
+ export function loadProvider(name: string): ProviderCache | null {
115
+ return loadConfig().providers?.[name] ?? null;
116
+ }
117
+
118
+ export function saveProvider(name: string, data: ProviderCache) {
119
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
120
+ const current = loadConfig();
121
+ if (!current.providers) current.providers = {};
122
+ current.providers[name] = data;
123
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(current, null, 2) + "\n", "utf-8");
124
+ }
125
+
126
+ export function removeProvider(name: string) {
127
+ const current = loadConfig();
128
+ if (current.providers?.[name]) {
129
+ delete current.providers[name];
130
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(current, null, 2) + "\n", "utf-8");
131
+ }
132
+ }
133
+
134
+ // ─── Project-level config ────────────────────────────────────────────────
135
+
136
+ function projectConfigPath(cwd: string): string {
137
+ return path.join(cwd, ".pi", "agent", "decorated-pi.json");
138
+ }
139
+
140
+ function loadProjectConfig(cwd: string): DecoratedPiConfig {
141
+ try {
142
+ const p = projectConfigPath(cwd);
143
+ if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, "utf-8"));
144
+ } catch {}
145
+ return {};
146
+ }
147
+
148
+ function saveProjectConfig(cwd: string, partial: Partial<DecoratedPiConfig>) {
149
+ const p = projectConfigPath(cwd);
150
+ const dir = path.dirname(p);
151
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
152
+ const current = loadProjectConfig(cwd);
153
+ fs.writeFileSync(p, JSON.stringify({ ...current, ...partial }, null, 2), "utf-8");
154
+ }
155
+
156
+ // ─── Getter ─────────────────────────────────────────────────────────────────
157
+
158
+ export function getImageModelKey(): string | null {
159
+ return loadConfig().imageModelKey ?? null;
160
+ }
161
+
162
+ export function getCompactModelKey(): string | null {
163
+ return loadConfig().compactModelKey ?? null;
164
+ }
165
+
166
+ // ─── Setter ─────────────────────────────────────────────────────────────────
167
+
168
+ export function setImageModelKey(key: string | null) {
169
+ saveConfig({ imageModelKey: key });
170
+ }
171
+
172
+ export function setCompactModelKey(key: string | null) {
173
+ saveConfig({ compactModelKey: key });
174
+ }
175
+
176
+ // ─── Module Switches ──────────────────────────────────────────────────────────
177
+
178
+ const DEFAULT_MODULES: Required<ModuleSettings> = {
179
+ tools: {
180
+ patchOverrideEdit: true,
181
+ ask: true,
182
+ lsp: true,
183
+ mcp: true,
184
+ },
185
+ hooks: {
186
+ secretRedaction: true,
187
+ "rtk": true,
188
+ wakatime: true,
189
+ },
190
+ commands: {
191
+ atOverride: true,
192
+ retry: true,
193
+ usage: true,
194
+ },
195
+ };
196
+
197
+ /** Maps every module name to its category. Used by isModuleEnabled/setModuleEnabled. */
198
+ const MODULE_TO_CATEGORY: Record<string, keyof ModuleSettings> = {
199
+ patchOverrideEdit: "tools",
200
+ ask: "tools",
201
+ lsp: "tools",
202
+ mcp: "tools",
203
+ secretRedaction: "hooks",
204
+ "rtk": "hooks",
205
+ wakatime: "hooks",
206
+ atOverride: "commands",
207
+ retry: "commands",
208
+ usage: "commands",
209
+ };
210
+
211
+ /** Legacy flat module keys that were renamed. Applied before flat→nested migration. */
212
+ const LEGACY_MODULE_KEYS: Record<string, string> = {
213
+ patch: "patchOverrideEdit",
214
+ safety: "secretRedaction",
215
+ };
216
+
217
+ /**
218
+ * Migrate module settings to the nested tools/hooks/commands layout.
219
+ * Handles three legacy shapes:
220
+ * 1. Flat config with current names (patchOverrideEdit, secretRedaction, ...)
221
+ * 2. Flat config with old names (patch, safety)
222
+ * 3. Nested config with old inner names (tools.patch, hooks.safety)
223
+ * Already-correct nested configs are left untouched.
224
+ */
225
+ function migrateModuleSettings(config: DecoratedPiConfig): boolean {
226
+ if (!config.modules) return false;
227
+
228
+ let migrated = false;
229
+ const result: ModuleSettings = {};
230
+
231
+ // Start from any already-nested values.
232
+ if (config.modules.tools) result.tools = { ...config.modules.tools };
233
+ if (config.modules.hooks) result.hooks = { ...config.modules.hooks };
234
+ if (config.modules.commands) result.commands = { ...config.modules.commands };
235
+
236
+ // Rename legacy keys inside already-nested categories.
237
+ function renameInCategory(
238
+ category: keyof ModuleSettings,
239
+ oldKey: string,
240
+ newKey: string,
241
+ ) {
242
+ const cat = result[category] as Record<string, boolean> | undefined;
243
+ if (cat && oldKey in cat && !(newKey in cat)) {
244
+ cat[newKey] = cat[oldKey];
245
+ delete cat[oldKey];
246
+ migrated = true;
247
+ }
248
+ }
249
+ renameInCategory("tools", "patch", "patchOverrideEdit");
250
+ renameInCategory("hooks", "safety", "secretRedaction");
251
+ renameInCategory("commands", "smart-at", "atOverride");
252
+
253
+ // Migrate flat keys into nested categories.
254
+ const flatMapping: Record<string, [keyof ModuleSettings, string]> = {
255
+ patchOverrideEdit: ["tools", "patchOverrideEdit"],
256
+ ask: ["tools", "ask"],
257
+ lsp: ["tools", "lsp"],
258
+ mcp: ["tools", "mcp"],
259
+ secretRedaction: ["hooks", "secretRedaction"],
260
+ "rtk": ["hooks", "rtk"],
261
+ wakatime: ["hooks", "wakatime"],
262
+ atOverride: ["commands", "atOverride"],
263
+ retry: ["commands", "retry"],
264
+ usage: ["commands", "usage"],
265
+ patch: ["tools", "patchOverrideEdit"],
266
+ safety: ["hooks", "secretRedaction"],
267
+ "smart-at": ["commands", "atOverride"],
268
+ };
269
+
270
+ for (const [key, value] of Object.entries(config.modules)) {
271
+ if (key === "tools" || key === "hooks" || key === "commands") continue;
272
+ const mapping = flatMapping[key];
273
+ if (!mapping) continue;
274
+ const [category, newKey] = mapping;
275
+ if (!result[category]) result[category] = {} as any;
276
+ const cat = result[category] as Record<string, boolean>;
277
+ if (!(newKey in cat)) {
278
+ cat[newKey] = value as boolean;
279
+ }
280
+ migrated = true;
281
+ }
282
+
283
+ if (!migrated) return false;
284
+ config.modules = result;
285
+ return true;
286
+ }
287
+
288
+ export function isModuleEnabled(name: string): boolean {
289
+ const category = MODULE_TO_CATEGORY[name];
290
+ if (!category) return true;
291
+ const modules = loadConfig().modules ?? {};
292
+ const cat = modules[category] as Record<string, boolean> | undefined;
293
+ const defaults = DEFAULT_MODULES[category] as Record<string, boolean>;
294
+ if (cat && name in cat) return cat[name];
295
+ return defaults[name] ?? true;
296
+ }
297
+
298
+ export function setModuleEnabled(name: string, enabled: boolean) {
299
+ const category = MODULE_TO_CATEGORY[name];
300
+ if (!category) return;
301
+ const modules: ModuleSettings = { ...loadConfig().modules };
302
+ modules[category] = { ...(modules[category] ?? {}), [name]: enabled } as any;
303
+ saveConfig({ modules });
304
+ }
305
+
306
+ export function getAllModuleSettings(): Required<ModuleSettings> {
307
+ const modules = loadConfig().modules ?? {};
308
+ return {
309
+ tools: { ...DEFAULT_MODULES.tools, ...modules.tools },
310
+ hooks: { ...DEFAULT_MODULES.hooks, ...modules.hooks },
311
+ commands: { ...DEFAULT_MODULES.commands, ...modules.commands },
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Snapshot of the module settings that pi is currently running with.
317
+ * Captured once when the extension loads; /dp-settings compares the
318
+ * current effective settings against this snapshot to decide whether a
319
+ * reload is actually necessary.
320
+ */
321
+ let loadedModuleSnapshot: Required<ModuleSettings> | null = null;
322
+
323
+ export function captureModuleSnapshot(): void {
324
+ loadedModuleSnapshot = getAllModuleSettings();
325
+ }
326
+
327
+ export function moduleSnapshotChanged(): boolean {
328
+ if (!loadedModuleSnapshot) return true;
329
+ return JSON.stringify(loadedModuleSnapshot) !== JSON.stringify(getAllModuleSettings());
330
+ }
331
+
332
+ // ─── Usage index (增量同步元数据) ─────────────────────────────────────────────
333
+
334
+ /** 缓存的上次同步状态: { 文件路径 → { inode, size, mtime } } */
335
+ export function loadUsageIndex(): Record<string, UsageIndexEntry> {
336
+ return loadConfig().usageIndex ?? {};
337
+ }
338
+
339
+ export function saveUsageIndex(index: Record<string, UsageIndexEntry>) {
340
+ saveConfig({ usageIndex: index });
341
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * ask — ask the user one or more questions and return their answers.
3
+ *
4
+ * Supports free text, single-choice, and multiple-choice questions.
5
+ * Uses a custom TUI component so multiple questions can be presented
6
+ * together and navigated with Tab / Shift+Tab.
7
+ */
8
+
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import { Type } from "typebox";
11
+ import { AskComponent, type AskAnswer, type AskQuestion } from "../../ui/ask.js";
12
+
13
+ const askQuestionSchema = Type.Object({
14
+ id: Type.String({ description: "Unique identifier for this question in the result." }),
15
+ type: Type.Union(
16
+ [Type.Literal("text"), Type.Literal("single"), Type.Literal("multi")],
17
+ { description: "text = free input, single = one option, multi = many options" },
18
+ ),
19
+ question: Type.String({ description: "Question text shown to the user." }),
20
+ options: Type.Optional(Type.Array(Type.String(), { description: "Options for single or multi choice." })),
21
+ default: Type.Optional(Type.String({ description: "Default answer. For multi, comma-separated values." })),
22
+ allowCustom: Type.Optional(Type.Boolean({
23
+ description: "For single/multi: append an \"Other\" row that toggles into a free-text input. Lets the user enter a custom answer not in the preset list.",
24
+ })),
25
+ });
26
+
27
+ function formatAnswer(q: AskQuestion, value: string | string[]): string {
28
+ if (q.type === "text") return value as string;
29
+ if (q.type === "single") return value as string;
30
+ return (value as string[]).join(", ");
31
+ }
32
+
33
+ export function formatAnswers(questions: AskQuestion[], answers: { id: string; value: string | string[] }[]): string {
34
+ const lines: string[] = [];
35
+ for (const q of questions) {
36
+ const a = answers.find((x) => x.id === q.id);
37
+ if (!a) continue;
38
+ lines.push(`${q.question}: ${formatAnswer(q, a.value)}`);
39
+ }
40
+ return lines.join("\n");
41
+ }
42
+
43
+ export function registerAskTool(pi: ExtensionAPI): void {
44
+ pi.registerTool({
45
+ name: "ask",
46
+ label: "Ask user",
47
+ description: "Ask the user one or more questions and return their answers. Supports free text, single choice, and multiple choice. Use when you need clarification or a decision before continuing.",
48
+ promptSnippet: "Ask the user one or more clarifying questions",
49
+ promptGuidelines: [
50
+ "Before acting on a prompt, ensure you fully understand the user's intent — if ambiguous, ask clarifying questions using the ask tool.",
51
+ ],
52
+ parameters: Type.Object({
53
+ questions: Type.Array(askQuestionSchema, { minItems: 1, description: "Questions to ask. User navigates between them with Tab / Shift+Tab." }),
54
+ }),
55
+ execute: async (_id, params, _signal, _update, ctx) => {
56
+ if (!ctx.hasUI) {
57
+ return {
58
+ content: [{ type: "text", text: "ask tool requires an interactive UI session." }],
59
+ isError: true,
60
+ details: {},
61
+ };
62
+ }
63
+
64
+ // Validate options for single/multi.
65
+ for (const q of params.questions) {
66
+ if ((q.type === "single" || q.type === "multi") && (!q.options || q.options.length === 0)) {
67
+ return {
68
+ content: [{ type: "text", text: `Question "${q.id}" (${q.type}) requires at least one option.` }],
69
+ isError: true,
70
+ details: {},
71
+ };
72
+ }
73
+ }
74
+
75
+ const answers = await ctx.ui.custom<AskAnswer[] | undefined>(
76
+ (tui, theme, _kb, done) => new AskComponent(tui, theme, params.questions, done),
77
+ );
78
+
79
+ if (!answers) {
80
+ return {
81
+ content: [{ type: "text", text: "User cancelled the questions." }],
82
+ isError: false,
83
+ details: { cancelled: true },
84
+ };
85
+ }
86
+
87
+ return {
88
+ content: [{ type: "text", text: formatAnswers(params.questions, answers) }],
89
+ details: { answers },
90
+ };
91
+ },
92
+ });
93
+ }
@@ -8,7 +8,6 @@ import {
8
8
  } from "./protocol.js";
9
9
  import type {
10
10
  LspDiagnostic,
11
- LspDocumentSymbol,
12
11
  LspHover,
13
12
  LspLocation,
14
13
  LspPosition,
@@ -199,14 +198,6 @@ export class LspClient {
199
198
  );
200
199
  }
201
200
 
202
- async documentSymbols(uri: string, timeoutMs?: number): Promise<LspDocumentSymbol[]> {
203
- return normalizeDocumentSymbols(
204
- await this.#request("textDocument/documentSymbol", {
205
- textDocument: { uri },
206
- }, timeoutMs),
207
- );
208
- }
209
-
210
201
  async rename(
211
202
  uri: string,
212
203
  position: LspPosition,
@@ -260,22 +251,6 @@ function normalizeLocations(result: unknown): LspLocation[] {
260
251
  });
261
252
  }
262
253
 
263
- function normalizeDocumentSymbols(result: unknown): LspDocumentSymbol[] {
264
- if (!result || !Array.isArray(result) || result.length === 0) return [];
265
- const first = result[0];
266
- if ("range" in first && "selectionRange" in first) {
267
- return result as LspDocumentSymbol[];
268
- }
269
- return result.map((entry: any) => ({
270
- name: entry.name,
271
- kind: entry.kind,
272
- range: entry.location.range,
273
- selectionRange: entry.location.range,
274
- containerName: entry.containerName,
275
- uri: entry.location.uri,
276
- }));
277
- }
278
-
279
254
  function uriToPath(uri: string): string {
280
255
  try {
281
256
  return uri.startsWith("file:") ? new URL(uri).pathname : uri;
@@ -5,10 +5,10 @@
5
5
  * Runtime tools.ts has its own inline copies.
6
6
  */
7
7
  import { fileURLToPath } from "node:url";
8
- import type { LspDiagnostic, LspHover, LspLocation, LspDocumentSymbol } from "./types.js";
8
+ import type { LspDiagnostic, LspHover, LspLocation } from "./types.js";
9
9
  import { LspClientStartError } from "./client.js";
10
10
 
11
- export type { LspDiagnostic, LspHover, LspLocation, LspDocumentSymbol } from "./types.js";
11
+ export type { LspDiagnostic, LspHover, LspLocation } from "./types.js";
12
12
  export { LspClientStartError } from "./client.js";
13
13
 
14
14
  // ─── Error formatting ────────────────────────────────────────────────────
@@ -112,103 +112,6 @@ function fileUrlToPath(uri: string): string {
112
112
  try { return uri.startsWith("file:") ? fileURLToPath(uri) : uri; } catch { return uri; }
113
113
  }
114
114
 
115
- // ─── Symbols ──────────────────────────────────────────────────────────────
116
-
117
- const SYMBOL_KIND_LABELS: Record<number, string> = {
118
- 2: "module", 3: "namespace", 5: "class", 6: "method", 7: "property",
119
- 8: "field", 9: "constructor", 11: "interface", 12: "function",
120
- 13: "variable", 14: "constant", 23: "struct", 24: "event",
121
- };
122
-
123
- export function symbol_kind_label(kind: number): string {
124
- return SYMBOL_KIND_LABELS[kind] ?? "symbol";
125
- }
126
-
127
- export function format_document_symbols(file: string, symbols: LspDocumentSymbol[]): string {
128
- if (symbols.length === 0) return `${file}: no symbols`;
129
- const lines = [`${file}: ${symbols.length} top-level symbol(s)`];
130
- appendSymbols(lines, symbols, 1);
131
- return lines.join("\n");
132
- }
133
-
134
- function appendSymbols(lines: string[], symbols: LspDocumentSymbol[], depth: number) {
135
- for (const s of symbols) {
136
- const indent = " ".repeat(depth);
137
- const detail = s.detail ? ` — ${s.detail}` : "";
138
- const range = `${s.range.start.line + 1}:${s.range.start.character + 1}`;
139
- lines.push(`${indent}${symbol_kind_label(s.kind)} ${s.name}${detail} @ ${range}`);
140
- if (s.children?.length) appendSymbols(lines, s.children, depth + 1);
141
- }
142
- }
143
-
144
- interface SymbolMatchOptions {
145
- max_results: number;
146
- top_level_only: boolean;
147
- exact_match: boolean;
148
- kinds: Set<string>;
149
- language: string;
150
- }
151
-
152
- export interface SymbolMatch { symbol: LspDocumentSymbol; depth: number }
153
-
154
- export function find_symbol_matches(
155
- symbols: LspDocumentSymbol[],
156
- query: string,
157
- options: SymbolMatchOptions,
158
- ): SymbolMatch[] {
159
- const normalized = query.trim().toLowerCase();
160
- if (!normalized) return [];
161
- const matches: SymbolMatch[] = [];
162
- const expandName = (name: string): string[] => {
163
- const trimmed = name.trim().toLowerCase();
164
- if (!trimmed) return [];
165
- const expanded = new Set([trimmed]);
166
- if (options.language === "cpp" && trimmed.includes("::")) {
167
- const parts = trimmed.split("::").map(p => p.trim()).filter(Boolean);
168
- if (parts.length > 0) expanded.add(parts[parts.length - 1]!);
169
- }
170
- return [...expanded];
171
- };
172
-
173
- const matchesQuery = (s: LspDocumentSymbol): boolean => {
174
- const name = s.name.trim().toLowerCase();
175
- const detail = (s.detail ?? "").trim().toLowerCase();
176
- if (options.exact_match) {
177
- const exactValues = [...expandName(s.name), ...(detail ? [detail] : [])];
178
- return exactValues.some(v => v === normalized);
179
- }
180
- return name.includes(normalized) || detail.includes(normalized);
181
- };
182
- const matchesKind = (s: LspDocumentSymbol): boolean =>
183
- options.kinds.size === 0 || options.kinds.has(symbol_kind_label(s.kind));
184
- const visit = (entries: LspDocumentSymbol[], depth: number) => {
185
- for (const symbol of entries) {
186
- if (matchesKind(symbol) && matchesQuery(symbol)) {
187
- matches.push({ symbol, depth });
188
- if (matches.length >= options.max_results) return;
189
- }
190
- if (!options.top_level_only && symbol.children?.length) {
191
- visit(symbol.children, depth + 1);
192
- if (matches.length >= options.max_results) return;
193
- }
194
- }
195
- };
196
- visit(symbols, 1);
197
- return matches;
198
- }
199
-
200
- export function format_symbol_matches(file: string, query: string, matches: SymbolMatch[]): string {
201
- if (matches.length === 0) return `${file}: no symbols matching "${query}"`;
202
- const lines = [`${file}: ${matches.length} symbol match(es) for "${query}"`];
203
- for (const { symbol, depth } of matches) {
204
- const indent = " ".repeat(depth);
205
- const detail = symbol.detail ? ` — ${symbol.detail}` : "";
206
- const range = `${symbol.range.start.line + 1}:${symbol.range.start.character + 1}`;
207
- lines.push(`${indent}${symbol_kind_label(symbol.kind)} ${symbol.name}${detail} @ ${range}`);
208
- }
209
- return lines.join("\n");
210
- }
211
-
212
115
  // ─── Collapse (for tools test) ────────────────────────────────────────────
213
116
 
214
117
  export function collapse_lsp_text(text: string, maxLines = 20) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * LSP Extension — language server integration for Pi.
3
3
  *
4
- * Provides: lsp_diagnostics, lsp_document_symbols.
4
+ * Provides: lsp_diagnostics.
5
5
  */
6
6
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
7
  import { LspServerManager } from "./manager.js";
@@ -4,7 +4,7 @@
4
4
  import { existsSync, readdirSync, type Dirent } from "node:fs";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { dirname, extname, isAbsolute, join, resolve } from "node:path";
7
- import type { DependencyStatus } from "../rtk";
7
+ import type { DependencyStatus } from "../../hooks/skeleton.js";
8
8
 
9
9
  // ─── File extension → language mapping ────────────────────────────────────
10
10