@mrclrchtr/supi-code-intelligence 1.4.0 → 1.5.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 (56) hide show
  1. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  2. package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
  3. package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +42 -10
  6. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  7. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
  8. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +42 -10
  11. package/node_modules/@mrclrchtr/supi-lsp/package.json +2 -2
  12. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +2 -16
  13. package/node_modules/@mrclrchtr/supi-lsp/src/session/service-registry.ts +5 -21
  14. package/node_modules/@mrclrchtr/supi-lsp/src/tool/guidance.ts +15 -75
  15. package/node_modules/@mrclrchtr/supi-lsp/src/tool/register-tools.ts +13 -166
  16. package/node_modules/@mrclrchtr/supi-lsp/src/tool/tool-specs.ts +248 -0
  17. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +5 -34
  18. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +18 -6
  19. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +107 -0
  20. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +44 -0
  21. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +85 -0
  22. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +76 -0
  23. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +186 -0
  24. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +119 -0
  25. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
  26. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
  27. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  28. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -0
  29. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +85 -0
  30. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  31. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  32. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +86 -0
  33. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  34. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +15 -0
  35. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +41 -0
  36. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +226 -0
  37. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  38. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +8 -3
  39. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +5 -1
  40. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +5 -1
  41. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +3 -2
  42. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/service-registry.ts +30 -0
  43. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +16 -8
  44. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/action-specs.ts +92 -0
  45. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/guidance.ts +12 -3
  46. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tree-sitter.ts +111 -61
  47. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +13 -2
  48. package/package.json +4 -4
  49. package/src/actions/brief-action.ts +5 -5
  50. package/src/code-intelligence.ts +3 -10
  51. package/src/pattern-structured.ts +1 -1
  52. package/src/providers/structural-provider.ts +15 -3
  53. package/src/search-helpers.ts +4 -15
  54. package/src/tool/action-specs.ts +66 -0
  55. package/src/tool/guidance.ts +4 -7
  56. package/src/tool-actions.ts +23 -40
@@ -5,6 +5,18 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
5
  import { Type } from "typebox";
6
6
  import { detectGrammar, isJsTsGrammar } from "./language.ts";
7
7
  import { TreeSitterRuntime } from "./session/runtime.ts";
8
+ import {
9
+ clearSessionTreeSitterService,
10
+ setSessionTreeSitterService,
11
+ } from "./session/service-registry.ts";
12
+ import { createTreeSitterService } from "./session/session.ts";
13
+ import {
14
+ formatTreeSitterActionList,
15
+ getTreeSitterActionSpec,
16
+ isTreeSitterAction,
17
+ TREE_SITTER_ACTION_NAMES,
18
+ type TreeSitterAction,
19
+ } from "./tool/action-specs.ts";
8
20
  import {
9
21
  formatNonSuccess,
10
22
  formatOutlineItemsCapped,
@@ -18,26 +30,30 @@ import { promptGuidelines, promptSnippet, toolDescription } from "./tool/guidanc
18
30
  import { collectOutline } from "./tool/outline.ts";
19
31
  import { extractExports, extractImports, lookupCalleesAt, lookupNodeAt } from "./tool/structure.ts";
20
32
 
21
- const TreeSitterActionEnum = StringEnum([
22
- "outline",
23
- "imports",
24
- "exports",
25
- "node_at",
26
- "query",
27
- "callees",
28
- ] as const);
33
+ const TreeSitterActionEnum = StringEnum(TREE_SITTER_ACTION_NAMES);
29
34
 
30
35
  export default function treeSitterExtension(pi: ExtensionAPI) {
31
36
  let runtime: TreeSitterRuntime | undefined;
37
+ let activeCwd: string | null = null;
32
38
 
33
39
  pi.on("session_start", (_event, ctx) => {
34
- runtime?.dispose();
40
+ if (runtime && activeCwd) {
41
+ clearSessionTreeSitterService(activeCwd);
42
+ runtime.dispose();
43
+ }
44
+
45
+ activeCwd = ctx.cwd;
35
46
  runtime = new TreeSitterRuntime(ctx.cwd);
47
+ setSessionTreeSitterService(ctx.cwd, createTreeSitterService(runtime));
36
48
  });
37
49
 
38
50
  pi.on("session_shutdown", () => {
51
+ if (activeCwd) {
52
+ clearSessionTreeSitterService(activeCwd);
53
+ }
39
54
  runtime?.dispose();
40
55
  runtime = undefined;
56
+ activeCwd = null;
41
57
  });
42
58
 
43
59
  pi.registerTool({
@@ -73,8 +89,6 @@ export default function treeSitterExtension(pi: ExtensionAPI) {
73
89
  });
74
90
  }
75
91
 
76
- type TreeSitterAction = "outline" | "imports" | "exports" | "node_at" | "query" | "callees";
77
-
78
92
  type ToolParams = {
79
93
  action?: string;
80
94
  file?: string;
@@ -83,17 +97,47 @@ type ToolParams = {
83
97
  query?: string;
84
98
  };
85
99
 
100
+ interface ValidatedToolParams {
101
+ action: TreeSitterAction;
102
+ file: string;
103
+ line?: number;
104
+ character?: number;
105
+ query?: string;
106
+ }
107
+
108
+ const SUPPORTED_ACTIONS_TEXT = formatTreeSitterActionList();
109
+
110
+ const ACTION_HANDLERS: Record<
111
+ TreeSitterAction,
112
+ (runtime: TreeSitterRuntime, params: ValidatedToolParams) => Promise<string>
113
+ > = {
114
+ outline: (runtime, params) => handleOutline(runtime, params.file),
115
+ imports: (runtime, params) => handleImports(runtime, params.file),
116
+ exports: (runtime, params) => handleExports(runtime, params.file),
117
+ node_at: (runtime, params) =>
118
+ handleNodeAt(runtime, params.file, params.line as number, params.character as number),
119
+ query: (runtime, params) => handleQuery(runtime, params.file, params.query as string),
120
+ callees: (runtime, params) =>
121
+ handleCallees(runtime, params.file, params.line as number, params.character as number),
122
+ };
123
+
86
124
  async function executeToolAction(runtime: TreeSitterRuntime, params: ToolParams): Promise<string> {
125
+ const validated = validateToolParams(params);
126
+ if (typeof validated === "string") {
127
+ return validated;
128
+ }
129
+
130
+ return ACTION_HANDLERS[validated.action](runtime, validated);
131
+ }
132
+
133
+ function validateToolParams(params: ToolParams): ValidatedToolParams | string {
87
134
  if (!params.action) {
88
- return validationError(
89
- "`action` is required. Supported: outline, imports, exports, node_at, query, callees.",
90
- );
135
+ return validationError(`\`action\` is required. Supported: ${SUPPORTED_ACTIONS_TEXT}.`);
91
136
  }
92
137
 
93
- const action = toSupportedAction(params.action);
94
- if (!action) {
138
+ if (!isTreeSitterAction(params.action)) {
95
139
  return validationError(
96
- `Unknown action: ${params.action}. Supported: outline, imports, exports, node_at, query, callees`,
140
+ `Unknown action: ${params.action}. Supported: ${SUPPORTED_ACTIONS_TEXT}`,
97
141
  );
98
142
  }
99
143
 
@@ -101,12 +145,37 @@ async function executeToolAction(runtime: TreeSitterRuntime, params: ToolParams)
101
145
  return validationError("`file` is required for all actions.");
102
146
  }
103
147
 
104
- if (action === "outline") return handleOutline(runtime, params.file);
105
- if (action === "imports") return handleImports(runtime, params.file);
106
- if (action === "exports") return handleExports(runtime, params.file);
107
- if (action === "node_at") return handleNodeAt(runtime, params);
108
- if (action === "callees") return handleCallees(runtime, params);
109
- return handleQuery(runtime, params);
148
+ const spec = getTreeSitterActionSpec(params.action);
149
+ if (spec.requiresPosition) {
150
+ const lineError = validatePositiveInteger("line", params.line, params.action);
151
+ if (lineError) return lineError;
152
+
153
+ const characterError = validatePositiveInteger("character", params.character, params.action);
154
+ if (characterError) return characterError;
155
+ }
156
+
157
+ if (spec.requiresQuery && (!params.query || params.query.trim().length === 0)) {
158
+ return validationError("`query` is required and must be non-empty.");
159
+ }
160
+
161
+ return {
162
+ action: params.action,
163
+ file: params.file,
164
+ line: params.line,
165
+ character: params.character,
166
+ query: params.query,
167
+ };
168
+ }
169
+
170
+ function validatePositiveInteger(
171
+ field: "line" | "character",
172
+ value: number | undefined,
173
+ action: TreeSitterAction,
174
+ ): string | null {
175
+ if (value === undefined || !Number.isInteger(value) || value < 1) {
176
+ return validationError(`\`${field}\` must be a positive 1-based integer for ${action} action.`);
177
+ }
178
+ return null;
110
179
  }
111
180
 
112
181
  async function handleOutline(runtime: TreeSitterRuntime, file: string): Promise<string> {
@@ -175,18 +244,13 @@ async function handleExports(runtime: TreeSitterRuntime, file: string): Promise<
175
244
  return lines.join("\n");
176
245
  }
177
246
 
178
- async function handleNodeAt(runtime: TreeSitterRuntime, params: ToolParams): Promise<string> {
179
- if (!Number.isInteger(params.line) || (params.line as number) < 1) {
180
- return validationError("`line` must be a positive 1-based integer for node_at action.");
181
- }
182
- if (!Number.isInteger(params.character) || (params.character as number) < 1) {
183
- return validationError("`character` must be a positive 1-based integer for node_at action.");
184
- }
185
-
186
- const file = params.file;
187
- const line = params.line as number;
188
- const character = params.character as number;
189
- const result = await lookupNodeAt(runtime, file as string, line, character);
247
+ async function handleNodeAt(
248
+ runtime: TreeSitterRuntime,
249
+ file: string,
250
+ line: number,
251
+ character: number,
252
+ ): Promise<string> {
253
+ const result = await lookupNodeAt(runtime, file, line, character);
190
254
  if (result.kind !== "success") return formatNonSuccess(result);
191
255
 
192
256
  const { data } = result;
@@ -212,13 +276,12 @@ async function handleNodeAt(runtime: TreeSitterRuntime, params: ToolParams): Pro
212
276
  return lines.join("\n");
213
277
  }
214
278
 
215
- async function handleQuery(runtime: TreeSitterRuntime, params: ToolParams): Promise<string> {
216
- if (!params.query || params.query.trim().length === 0) {
217
- return validationError("`query` is required and must be non-empty.");
218
- }
219
-
220
- const file = params.file as string;
221
- const result = await runtime.queryFile(file, params.query);
279
+ async function handleQuery(
280
+ runtime: TreeSitterRuntime,
281
+ file: string,
282
+ query: string,
283
+ ): Promise<string> {
284
+ const result = await runtime.queryFile(file, query);
222
285
  if (result.kind !== "success") return formatNonSuccess(result);
223
286
 
224
287
  const { data: captures } = result;
@@ -237,25 +300,12 @@ async function handleQuery(runtime: TreeSitterRuntime, params: ToolParams): Prom
237
300
  return lines.join("\n");
238
301
  }
239
302
 
240
- function toSupportedAction(action: string): TreeSitterAction | undefined {
241
- if (["outline", "imports", "exports", "node_at", "query", "callees"].includes(action)) {
242
- return action as TreeSitterAction;
243
- }
244
- return undefined;
245
- }
246
-
247
- async function handleCallees(runtime: TreeSitterRuntime, params: ToolParams): Promise<string> {
248
- if (!Number.isInteger(params.line) || (params.line as number) < 1) {
249
- return validationError("`line` must be a positive 1-based integer for callees action.");
250
- }
251
- if (!Number.isInteger(params.character) || (params.character as number) < 1) {
252
- return validationError("`character` must be a positive 1-based integer for callees action.");
253
- }
254
-
255
- const file = params.file as string;
256
- const line = params.line as number;
257
- const character = params.character as number;
258
-
303
+ async function handleCallees(
304
+ runtime: TreeSitterRuntime,
305
+ file: string,
306
+ line: number,
307
+ character: number,
308
+ ): Promise<string> {
259
309
  const result = await lookupCalleesAt(runtime, file, line, character);
260
310
  if (result.kind !== "success") return formatNonSuccess(result);
261
311
 
@@ -66,8 +66,8 @@ export interface QueryCapture {
66
66
  text: string;
67
67
  }
68
68
 
69
- /** Session-level Tree-sitter service. */
70
- export interface TreeSitterSession {
69
+ /** Shared Tree-sitter service surface, independent of lifecycle ownership. */
70
+ export interface TreeSitterService {
71
71
  /** Validate that a supported file can be read and parsed; does not expose the raw tree. */
72
72
  canParse(file: string): Promise<TreeSitterResult<{ file: string; language: string }>>;
73
73
  /** Run a Tree-sitter query and return all captures. */
@@ -86,10 +86,21 @@ export interface TreeSitterSession {
86
86
  line: number,
87
87
  character: number,
88
88
  ): Promise<TreeSitterResult<CalleesAtResult>>;
89
+ }
90
+
91
+ /** Owned Tree-sitter session that must release its runtime resources. */
92
+ export interface TreeSitterSession extends TreeSitterService {
89
93
  /** Release parser and grammar resources owned by this session. */
90
94
  dispose(): void;
91
95
  }
92
96
 
97
+ /** Session-scoped shared structural service published by the extension runtime. */
98
+ export type SessionTreeSitterService = TreeSitterService;
99
+
100
+ export type SessionTreeSitterServiceState =
101
+ | { kind: "ready"; service: SessionTreeSitterService }
102
+ | { kind: "unavailable"; reason: string };
103
+
93
104
  /** Supported grammar identifiers. */
94
105
  export type GrammarId =
95
106
  | "javascript"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-intelligence",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "SuPi Code Intelligence extension — architecture briefs, caller/callee analysis, impact assessment, and pattern search for pi",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,9 +19,9 @@
19
19
  "src/**/*.ts"
20
20
  ],
21
21
  "dependencies": {
22
- "@mrclrchtr/supi-core": "1.4.0",
23
- "@mrclrchtr/supi-lsp": "1.4.0",
24
- "@mrclrchtr/supi-tree-sitter": "1.4.0"
22
+ "@mrclrchtr/supi-core": "1.5.0",
23
+ "@mrclrchtr/supi-tree-sitter": "1.5.0",
24
+ "@mrclrchtr/supi-lsp": "1.5.0"
25
25
  },
26
26
  "bundledDependencies": [
27
27
  "@mrclrchtr/supi-core",
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
- import type { TreeSitterSession } from "@mrclrchtr/supi-tree-sitter/api";
5
+ import type { TreeSitterService } from "@mrclrchtr/supi-tree-sitter/api";
6
6
  import { buildArchitectureModel, findModuleForPath } from "../architecture.ts";
7
7
  import { generateFocusedBrief, generateProjectBrief } from "../brief.ts";
8
8
  import { withStructuralSession } from "../providers/structural-provider.ts";
@@ -123,7 +123,7 @@ async function addTreeSitterContext(input: TreeSitterContextInput): Promise<void
123
123
 
124
124
  async function addNodeContext(
125
125
  lines: string[],
126
- ts: TreeSitterSession,
126
+ ts: TreeSitterService,
127
127
  relPath: string,
128
128
  pos: { line: number; char: number },
129
129
  ): Promise<void> {
@@ -143,7 +143,7 @@ async function addNodeContext(
143
143
 
144
144
  async function addOutlineContext(
145
145
  lines: string[],
146
- ts: TreeSitterSession,
146
+ ts: TreeSitterService,
147
147
  relPath: string,
148
148
  line1: number,
149
149
  ): Promise<void> {
@@ -182,7 +182,7 @@ function getOutlinePrefix(kind: string): string {
182
182
 
183
183
  async function addImportsContext(
184
184
  lines: string[],
185
- ts: TreeSitterSession,
185
+ ts: TreeSitterService,
186
186
  relPath: string,
187
187
  ): Promise<void> {
188
188
  const result = await ts.imports(relPath);
@@ -200,7 +200,7 @@ async function addImportsContext(
200
200
 
201
201
  async function addExportsContext(
202
202
  lines: string[],
203
- ts: TreeSitterSession,
203
+ ts: TreeSitterService,
204
204
  relPath: string,
205
205
  ): Promise<void> {
206
206
  const result = await ts.exports(relPath);
@@ -6,20 +6,13 @@ import type { BeforeAgentStartEventResult, ExtensionAPI } from "@earendil-works/
6
6
  import { Type } from "typebox";
7
7
  import { buildArchitectureModel } from "./architecture.ts";
8
8
  import { generateOverview } from "./brief.ts";
9
+ import { CODE_INTEL_ACTION_NAMES, type CodeIntelAction } from "./tool/action-specs.ts";
9
10
  import { promptGuidelines, promptSnippet, toolDescription } from "./tool/guidance.ts";
10
- import { type CodeIntelAction, executeAction } from "./tool-actions.ts";
11
+ import { executeAction } from "./tool-actions.ts";
11
12
 
12
13
  const OVERVIEW_CUSTOM_TYPE = "code-intelligence-overview";
13
14
 
14
- const CodeIntelActionEnum = StringEnum([
15
- "brief",
16
- "callers",
17
- "callees",
18
- "implementations",
19
- "affected",
20
- "pattern",
21
- "index",
22
- ] as const);
15
+ const CodeIntelActionEnum = StringEnum(CODE_INTEL_ACTION_NAMES);
23
16
 
24
17
  /**
25
18
  * Register the `code_intel` tool and inject a lightweight architecture overview
@@ -72,7 +72,7 @@ export async function getStructuredPatternMatches(
72
72
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: kind-specific tree-sitter matching is clearest as one helper
73
73
  async function collectMatchesForFile(
74
74
  matches: StructuredMatch[],
75
- tsSession: import("@mrclrchtr/supi-tree-sitter/api").TreeSitterSession,
75
+ tsSession: import("@mrclrchtr/supi-tree-sitter/api").TreeSitterService,
76
76
  relFile: string,
77
77
  kind: StructuredPatternKind,
78
78
  matcher: (value: string) => boolean,
@@ -1,10 +1,22 @@
1
- import { createTreeSitterSession, type TreeSitterSession } from "@mrclrchtr/supi-tree-sitter/api";
1
+ import {
2
+ createTreeSitterSession,
3
+ getSessionTreeSitterService,
4
+ type TreeSitterService,
5
+ } from "@mrclrchtr/supi-tree-sitter/api";
2
6
 
3
- /** Run work against a short-lived Tree-sitter session and dispose it afterward. */
7
+ /**
8
+ * Run work against the shared session-scoped Tree-sitter service when available,
9
+ * falling back to a short-lived owned session otherwise.
10
+ */
4
11
  export async function withStructuralSession<T>(
5
12
  cwd: string,
6
- fn: (session: TreeSitterSession) => Promise<T>,
13
+ fn: (session: TreeSitterService) => Promise<T>,
7
14
  ): Promise<T> {
15
+ const current = getSessionTreeSitterService(cwd);
16
+ if (current.kind === "ready") {
17
+ return fn(current.service);
18
+ }
19
+
8
20
  const session = createTreeSitterSession(cwd);
9
21
  try {
10
22
  return await fn(session);
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { execFileSync } from "node:child_process";
4
4
  import * as path from "node:path";
5
+ import { resolveToolPath, uriToFile as uriToFileShared } from "@mrclrchtr/supi-core/api";
5
6
 
6
7
  const LOW_SIGNAL_DIRS = new Set([
7
8
  "node_modules",
@@ -25,19 +26,8 @@ export function isLowSignalPath(filePath: string): boolean {
25
26
  return segments.some((s) => LOW_SIGNAL_DIRS.has(s));
26
27
  }
27
28
 
28
- /** Convert a file:// URI to a file path, matching the normalization in supi-lsp. */
29
- export function uriToFile(uri: string): string {
30
- if (!uri.startsWith("file://")) return uri;
31
- let filePath = decodeURIComponent(uri.slice(7));
32
- if (
33
- process.platform === "win32" &&
34
- filePath.startsWith("/") &&
35
- /^[A-Za-z]:/.test(filePath.slice(1))
36
- ) {
37
- filePath = filePath.slice(1);
38
- }
39
- return filePath;
40
- }
29
+ /** Convert a file:// URI to a file path, matching the shared SuPi normalization. */
30
+ export const uriToFile = uriToFileShared;
41
31
 
42
32
  /** Check whether a resolved file path is inside the current project (within cwd, not under node_modules or .pnpm). */
43
33
  export function isInProjectPath(filePath: string, cwd: string): boolean {
@@ -59,8 +49,7 @@ export function escapeRegex(s: string): string {
59
49
 
60
50
  /** Normalize a file/path value: strip leading @, resolve relative to cwd. */
61
51
  export function normalizePath(input: string, cwd: string): string {
62
- const stripped = input.startsWith("@") ? input.slice(1) : input;
63
- return path.resolve(cwd, stripped);
52
+ return resolveToolPath(cwd, input);
64
53
  }
65
54
 
66
55
  export interface RgMatch {
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Single source of truth for the public `code_intel` action surface.
3
+ *
4
+ * The action list, prompt guidance, and router validation should derive from
5
+ * these specs so the high-level orchestration tool stays internally coherent.
6
+ */
7
+ export const CODE_INTEL_ACTION_SPECS = [
8
+ {
9
+ name: "brief",
10
+ promptGuideline:
11
+ 'Use code_intel with `action: "brief"` for a project, package, directory, file, or anchored-position brief before opening more files.',
12
+ },
13
+ {
14
+ name: "callers",
15
+ promptGuideline:
16
+ 'Use code_intel with `action: "callers"` to find who invokes a symbol or file-level surface before falling back to text search.',
17
+ },
18
+ {
19
+ name: "callees",
20
+ promptGuideline:
21
+ 'Use code_intel with `action: "callees"` for outgoing calls from a function or method at a known `file`, `line`, and `character`.',
22
+ },
23
+ {
24
+ name: "implementations",
25
+ promptGuideline:
26
+ 'Use code_intel with `action: "implementations"` to find which concrete types implement a declaration.',
27
+ },
28
+ {
29
+ name: "affected",
30
+ promptGuideline:
31
+ 'Use code_intel with `action: "affected"` before edits for blast radius, downstream modules, risk, and likely follow-up checks or tests.',
32
+ },
33
+ {
34
+ name: "pattern",
35
+ promptGuideline:
36
+ 'Use code_intel with `action: "pattern"` for bounded search within a path; `pattern` is literal by default, set `regex: true` for regex, and use `kind: "definition" | "export" | "import"` for structured search.',
37
+ },
38
+ {
39
+ name: "index",
40
+ promptGuideline:
41
+ 'Use code_intel with `action: "index"` for a project map, top-level directories, language mix, or landmark files.',
42
+ },
43
+ ] as const;
44
+
45
+ export type CodeIntelAction = (typeof CODE_INTEL_ACTION_SPECS)[number]["name"];
46
+ export type CodeIntelActionSpec = (typeof CODE_INTEL_ACTION_SPECS)[number];
47
+
48
+ /** Ordered action names for schemas, validation messages, and docs. */
49
+ export const CODE_INTEL_ACTION_NAMES = CODE_INTEL_ACTION_SPECS.map(
50
+ (spec) => spec.name,
51
+ ) as readonly CodeIntelAction[];
52
+
53
+ const CODE_INTEL_ACTION_NAME_SET = new Set<string>(CODE_INTEL_ACTION_NAMES);
54
+
55
+ /** Check whether a runtime string is a supported `code_intel` action. */
56
+ export function isCodeIntelAction(action: string): action is CodeIntelAction {
57
+ return CODE_INTEL_ACTION_NAME_SET.has(action);
58
+ }
59
+
60
+ /** Format the public action list for validation messages and docs. */
61
+ export function formatCodeIntelActionList(options?: { fenced?: boolean }): string {
62
+ if (options?.fenced) {
63
+ return CODE_INTEL_ACTION_NAMES.map((name) => `\`${name}\``).join(", ");
64
+ }
65
+ return CODE_INTEL_ACTION_NAMES.join(", ");
66
+ }
@@ -1,18 +1,15 @@
1
1
  // Prompt guidance and tool description for the code_intel tool.
2
2
 
3
+ import { CODE_INTEL_ACTION_SPECS, formatCodeIntelActionList } from "./action-specs.ts";
4
+
3
5
  export const toolDescription = `Code intelligence tool — codebase orientation, semantic relationships, impact analysis, and scoped search.
4
6
 
5
- Actions: brief, callers, callees, implementations, affected, pattern, index.
7
+ Actions: ${formatCodeIntelActionList()}.
6
8
 
7
9
  Use code_intel to localize relevant files before precise drill-down: summarize a project/package/file, find callers/callees/implementations, estimate blast radius, or search within a scope. Prefer lsp_lookup, lsp_document_symbols, lsp_workspace_symbols, lsp_diagnostics, lsp_refactor, and lsp_recover for semantic drill-down once the target is known; use tree_sitter for exact syntax and read/rg once you know the file. line and character are 1-based and require file. pattern is literal unless regex is true; kind supports definition, export, or import. Relative paths resolve from cwd, and leading @ on path/file is stripped.`;
8
10
 
9
11
  export const promptGuidelines = [
10
- 'Use code_intel with `action: "brief"` for a project, package, directory, file, or anchored-position brief before opening more files.',
11
- 'Use code_intel with `action: "index"` for a project map, top-level directories, language mix, or landmark files.',
12
- 'Use code_intel with `action: "callers"` or `action: "implementations"` to find who invokes a symbol or which concrete types implement a declaration.',
13
- 'Use code_intel with `action: "callees"` for outgoing calls from a function or method at a known `file`, `line`, and `character`.',
14
- 'Use code_intel with `action: "affected"` before edits for blast radius, downstream modules, risk, and likely follow-up checks or tests.',
15
- 'Use code_intel with `action: "pattern"` for bounded search within a path; `pattern` is literal by default, set `regex: true` for regex, and use `kind: "definition" | "export" | "import"` for structured search.',
12
+ ...CODE_INTEL_ACTION_SPECS.map((spec) => spec.promptGuideline),
16
13
  "Use code_intel with `file`, `line`, and `character` for anchored positions; do not pair `line` or `character` with `path`.",
17
14
  "Use code_intel first when the area is not yet localized; switch to lsp_lookup, lsp_document_symbols, lsp_workspace_symbols, lsp_diagnostics, lsp_refactor, or lsp_recover for semantic drill-down once code_intel narrows the target.",
18
15
  ];
@@ -9,16 +9,14 @@ import { executeImplementationsAction } from "./actions/implementations-action.t
9
9
  import { executeIndexAction } from "./actions/index-action.ts";
10
10
  import { executePatternAction } from "./actions/pattern-action.ts";
11
11
  import { normalizePath } from "./search-helpers.ts";
12
+ import {
13
+ type CodeIntelAction,
14
+ formatCodeIntelActionList,
15
+ isCodeIntelAction,
16
+ } from "./tool/action-specs.ts";
12
17
  import type { CodeIntelResult } from "./types.ts";
13
18
 
14
- export type CodeIntelAction =
15
- | "brief"
16
- | "callers"
17
- | "callees"
18
- | "implementations"
19
- | "affected"
20
- | "pattern"
21
- | "index";
19
+ export type { CodeIntelAction } from "./tool/action-specs.ts";
22
20
 
23
21
  /** Flat parameter bag shared by `code_intel` action handlers. */
24
22
  export interface ActionParams {
@@ -40,15 +38,20 @@ export interface ActionParams {
40
38
  summary?: boolean;
41
39
  }
42
40
 
43
- const SUPPORTED_ACTIONS = new Set<string>([
44
- "brief",
45
- "callers",
46
- "callees",
47
- "implementations",
48
- "affected",
49
- "pattern",
50
- "index",
51
- ]);
41
+ type ActionHandler = (
42
+ params: ActionParams,
43
+ cwd: string,
44
+ ) => CodeIntelResult | Promise<CodeIntelResult>;
45
+
46
+ const ACTION_HANDLERS: Record<CodeIntelAction, ActionHandler> = {
47
+ brief: executeBriefAction,
48
+ callers: executeCallersAction,
49
+ callees: executeCalleesAction,
50
+ implementations: executeImplementationsAction,
51
+ affected: executeAffectedAction,
52
+ pattern: executePatternAction,
53
+ index: (_params, cwd) => executeIndexAction(cwd),
54
+ };
52
55
 
53
56
  /**
54
57
  * Main action dispatcher — validates params and routes to specific action handlers.
@@ -62,32 +65,12 @@ export async function executeAction(
62
65
  const error = validateParams(params, cwd);
63
66
  if (error) return { content: error, details: undefined };
64
67
 
65
- switch (params.action) {
66
- case "brief":
67
- return executeBriefAction(params, cwd);
68
- case "callers":
69
- return executeCallersAction(params, cwd);
70
- case "callees":
71
- return executeCalleesAction(params, cwd);
72
- case "implementations":
73
- return executeImplementationsAction(params, cwd);
74
- case "affected":
75
- return executeAffectedAction(params, cwd);
76
- case "pattern":
77
- return executePatternAction(params, cwd);
78
- case "index":
79
- return executeIndexAction(cwd);
80
- default:
81
- return {
82
- content: `**Error:** Unknown action \`${params.action}\`.`,
83
- details: undefined,
84
- };
85
- }
68
+ return ACTION_HANDLERS[params.action](params, cwd);
86
69
  }
87
70
 
88
71
  function validateParams(params: ActionParams, cwd: string): string | null {
89
- if (!params.action || !SUPPORTED_ACTIONS.has(params.action)) {
90
- return `**Error:** Unknown action \`${params.action ?? "(none)"}\`. Supported: \`brief\`, \`callers\`, \`callees\`, \`implementations\`, \`affected\`, \`pattern\`, \`index\`.`;
72
+ if (!params.action || !isCodeIntelAction(params.action)) {
73
+ return `**Error:** Unknown action \`${params.action ?? "(none)"}\`. Supported: ${formatCodeIntelActionList({ fenced: true })}.`;
91
74
  }
92
75
 
93
76
  if (params.path && (params.line != null || params.character != null)) {