@mrclrchtr/supi-tree-sitter 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 (30) hide show
  1. package/README.md +18 -6
  2. package/node_modules/@mrclrchtr/supi-core/README.md +107 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +44 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +85 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +76 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +186 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +119 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/index.ts +85 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +86 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +15 -0
  18. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +41 -0
  19. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +226 -0
  20. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  21. package/package.json +8 -3
  22. package/src/api.ts +5 -1
  23. package/src/index.ts +5 -1
  24. package/src/session/runtime.ts +3 -2
  25. package/src/session/service-registry.ts +30 -0
  26. package/src/session/session.ts +16 -8
  27. package/src/tool/action-specs.ts +92 -0
  28. package/src/tool/guidance.ts +12 -3
  29. package/src/tree-sitter.ts +111 -61
  30. package/src/types.ts +13 -2
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Single source of truth for the public `tree_sitter` action surface.
3
+ *
4
+ * Tool registration, validation, and prompt guidance should derive from these
5
+ * specs so the action list and per-action requirements do not drift apart.
6
+ */
7
+ export const TREE_SITTER_ACTION_SPECS = [
8
+ {
9
+ name: "outline",
10
+ guidanceGroup: "js-ts-structure",
11
+ languageScope: "js-ts-only",
12
+ requiresPosition: false,
13
+ requiresQuery: false,
14
+ },
15
+ {
16
+ name: "imports",
17
+ guidanceGroup: "js-ts-structure",
18
+ languageScope: "js-ts-only",
19
+ requiresPosition: false,
20
+ requiresQuery: false,
21
+ },
22
+ {
23
+ name: "exports",
24
+ guidanceGroup: "js-ts-structure",
25
+ languageScope: "js-ts-only",
26
+ requiresPosition: false,
27
+ requiresQuery: false,
28
+ },
29
+ {
30
+ name: "node_at",
31
+ guidanceGroup: "node-at",
32
+ languageScope: "all-supported",
33
+ requiresPosition: true,
34
+ requiresQuery: false,
35
+ },
36
+ {
37
+ name: "query",
38
+ guidanceGroup: "query",
39
+ languageScope: "all-supported",
40
+ requiresPosition: false,
41
+ requiresQuery: true,
42
+ },
43
+ {
44
+ name: "callees",
45
+ guidanceGroup: "callees",
46
+ languageScope: "many-supported",
47
+ requiresPosition: true,
48
+ requiresQuery: false,
49
+ },
50
+ ] as const;
51
+
52
+ export type TreeSitterAction = (typeof TREE_SITTER_ACTION_SPECS)[number]["name"];
53
+ export type TreeSitterActionSpec = (typeof TREE_SITTER_ACTION_SPECS)[number];
54
+ export type TreeSitterGuidanceGroup = TreeSitterActionSpec["guidanceGroup"];
55
+
56
+ /** Ordered action names for schemas, validation messages, and docs. */
57
+ export const TREE_SITTER_ACTION_NAMES = TREE_SITTER_ACTION_SPECS.map(
58
+ (spec) => spec.name,
59
+ ) as readonly TreeSitterAction[];
60
+
61
+ const TREE_SITTER_ACTION_NAME_SET = new Set<string>(TREE_SITTER_ACTION_NAMES);
62
+ const TREE_SITTER_ACTION_SPEC_MAP = new Map<TreeSitterAction, TreeSitterActionSpec>(
63
+ TREE_SITTER_ACTION_SPECS.map((spec) => [spec.name, spec]),
64
+ );
65
+
66
+ /** Check whether a runtime string is a supported `tree_sitter` action. */
67
+ export function isTreeSitterAction(action: string): action is TreeSitterAction {
68
+ return TREE_SITTER_ACTION_NAME_SET.has(action);
69
+ }
70
+
71
+ /** Look up the spec for one supported `tree_sitter` action. */
72
+ export function getTreeSitterActionSpec(action: TreeSitterAction): TreeSitterActionSpec {
73
+ const spec = TREE_SITTER_ACTION_SPEC_MAP.get(action);
74
+ if (!spec) {
75
+ throw new Error(`Unknown tree_sitter action: ${action}`);
76
+ }
77
+ return spec;
78
+ }
79
+
80
+ /** Get the ordered action names that belong to one prompt-guidance group. */
81
+ export function getTreeSitterActionNamesByGuidanceGroup(
82
+ group: TreeSitterGuidanceGroup,
83
+ ): TreeSitterAction[] {
84
+ return TREE_SITTER_ACTION_SPECS.filter((spec) => spec.guidanceGroup === group).map(
85
+ (spec) => spec.name,
86
+ );
87
+ }
88
+
89
+ /** Format the public action list for validation messages and docs. */
90
+ export function formatTreeSitterActionList(): string {
91
+ return TREE_SITTER_ACTION_NAMES.join(", ");
92
+ }
@@ -3,14 +3,23 @@
3
3
  // Note: We intentionally do NOT include cross-tool routing (e.g., "use lsp for
4
4
  // type info") because this package can be installed standalone without supi-lsp.
5
5
 
6
+ import {
7
+ formatTreeSitterActionList,
8
+ getTreeSitterActionNamesByGuidanceGroup,
9
+ } from "./action-specs.ts";
10
+
11
+ const jsTsStructureActions = getTreeSitterActionNamesByGuidanceGroup("js-ts-structure")
12
+ .map((action) => `tree_sitter.${action}(file)`)
13
+ .join(", ");
14
+
6
15
  export const toolDescription = `Tree-sitter tool — parser-level structure and syntax queries for supported files.
7
16
 
8
- Actions: outline, imports, exports, node_at, query, callees.
17
+ Actions: ${formatTreeSitterActionList()}.
9
18
 
10
- Use tree_sitter for exact syntax nodes, shallow structure, parsed imports/exports, outgoing calls, or custom AST queries within one file. file is required for all actions. line and character are 1-based UTF-16 coordinates for node_at and callees. query is required for query. outline, imports, and exports are JavaScript/TypeScript-only; node_at and query work across supported grammars; callees works for many grammars. Relative paths resolve from the session working directory.`;
19
+ Use tree_sitter for exact syntax nodes, shallow structure, parsed imports/exports, outgoing calls, or custom AST queries within one file. file is required for all actions. line and character are 1-based UTF-16 coordinates for node_at and callees. query is required for query. outline, imports, and exports are JavaScript/TypeScript-only; node_at and query work across supported grammars; callees works for many grammars. Relative paths resolve from the session working directory, and a leading @ on file paths is stripped.`;
11
20
 
12
21
  export const promptGuidelines = [
13
- "Use tree_sitter.outline(file), tree_sitter.imports(file), or tree_sitter.exports(file) for shallow JavaScript or TypeScript structure without reading the whole file.",
22
+ `Use ${jsTsStructureActions} for shallow JavaScript or TypeScript structure without reading the whole file.`,
14
23
  "Use tree_sitter.node_at(file, line, character) for the exact syntax node and ancestry at a known position.",
15
24
  "Use tree_sitter.callees(file, line, character) for outgoing calls from the enclosing function or method at a known position.",
16
25
  "Use tree_sitter.query(file, query) for custom Tree-sitter patterns when the built-in actions are not specific enough.",
@@ -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
 
package/src/types.ts CHANGED
@@ -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"