@mrclrchtr/supi-tree-sitter 1.6.0 → 1.8.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.
@@ -1,31 +1,35 @@
1
- // Prompt guidance and tool description for the tree_sitter tool.
1
+ // Guidance surfaces for the focused tree_sitter_* tool set.
2
2
  //
3
- // Note: We intentionally do NOT include cross-tool routing (e.g., "use lsp for
4
- // type info") because this package can be installed standalone without supi-lsp.
3
+ // Derives from tool-specs.ts so prompt surfaces stay in sync with
4
+ // the public tool metadata.
5
5
 
6
- import {
7
- formatTreeSitterActionList,
8
- getTreeSitterActionNamesByGuidanceGroup,
9
- } from "./action-specs.ts";
6
+ import { TREE_SITTER_TOOL_SPECS, type TreeSitterToolName } from "./tool-specs.ts";
10
7
 
11
- const jsTsStructureActions = getTreeSitterActionNamesByGuidanceGroup("js-ts-structure")
12
- .map((action) => `tree_sitter.${action}(file)`)
13
- .join(", ");
8
+ export interface TreeSitterToolPromptSurface {
9
+ description: string;
10
+ promptSnippet: string;
11
+ promptGuidelines: string[];
12
+ }
14
13
 
15
- export const toolDescription = `Tree-sitter tool — parser-level structure and syntax queries for supported files.
14
+ export type TreeSitterToolPromptSurfaceMap = Record<
15
+ TreeSitterToolName,
16
+ TreeSitterToolPromptSurface
17
+ >;
16
18
 
17
- Actions: ${formatTreeSitterActionList()}.
19
+ const _DEFAULT_SURFACES = buildTreeSitterToolPromptSurfaces();
18
20
 
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.`;
20
-
21
- export const promptGuidelines = [
22
- `Use ${jsTsStructureActions} for shallow JavaScript or TypeScript structure without reading the whole file.`,
23
- "Use tree_sitter.node_at(file, line, character) for the exact syntax node and ancestry at a known position.",
24
- "Use tree_sitter.callees(file, line, character) for outgoing calls from the enclosing function or method at a known position.",
25
- "Use tree_sitter.query(file, query) for custom Tree-sitter patterns when the built-in actions are not specific enough.",
26
- "Use tree_sitter for syntax, node types, source ranges, and other parser-backed structure within one supported file.",
27
- "Do not use tree_sitter for type information, cross-file references, semantic renames, or codebase-wide orientation.",
28
- ];
29
-
30
- export const promptSnippet =
31
- "tree_sitter — parser-backed single-file structure, node lookup, callees, and AST queries";
21
+ /**
22
+ * Build the full prompt-surface map for all 6 tree_sitter_* tools.
23
+ */
24
+ export function buildTreeSitterToolPromptSurfaces(): TreeSitterToolPromptSurfaceMap {
25
+ return Object.fromEntries(
26
+ TREE_SITTER_TOOL_SPECS.map((spec) => [
27
+ spec.name,
28
+ {
29
+ description: spec.description,
30
+ promptSnippet: spec.promptSnippet,
31
+ promptGuidelines: [...spec.promptGuidelines],
32
+ } satisfies TreeSitterToolPromptSurface,
33
+ ]),
34
+ ) as TreeSitterToolPromptSurfaceMap;
35
+ }
@@ -0,0 +1,196 @@
1
+ // Per-action handler functions for the focused tree_sitter tool surface.
2
+ //
3
+ // Each handler receives only the parameters it needs (no action dispatch).
4
+ // Handlers are shared between the extension factory (tree-sitter.ts) and the
5
+ // tool registration layer (register-tools.ts).
6
+
7
+ import { detectGrammar, isJsTsGrammar } from "../language.ts";
8
+ import type { TreeSitterRuntime } from "../session/runtime.ts";
9
+ import {
10
+ formatNonSuccess,
11
+ formatOutlineItemsCapped,
12
+ MAX_ITEMS,
13
+ truncate,
14
+ truncatedNotice,
15
+ truncateText,
16
+ } from "./formatting.ts";
17
+ import { collectOutline } from "./outline.ts";
18
+ import { extractExports, extractImports, lookupCalleesAt, lookupNodeAt } from "./structure.ts";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Outline — JS/TS only
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export async function handleOutline(runtime: TreeSitterRuntime, file: string): Promise<string> {
25
+ const parseResult = await runtime.parseFile(file);
26
+ if (parseResult.kind !== "success") return formatNonSuccess(parseResult);
27
+ if (!isJsTsGrammar(parseResult.data.grammarId)) {
28
+ return "Unsupported language: outline is only supported for JavaScript and TypeScript files";
29
+ }
30
+
31
+ const { tree, source } = parseResult.data;
32
+ let items: ReturnType<typeof collectOutline>;
33
+ try {
34
+ items = collectOutline(tree.rootNode, source);
35
+ } finally {
36
+ tree.delete();
37
+ }
38
+
39
+ if (items.length === 0) return `No structural declarations found in ${file}`;
40
+
41
+ const lines = [`## Outline: ${file}`, ""];
42
+ const { omitted } = formatOutlineItemsCapped(items, lines, MAX_ITEMS);
43
+ if (omitted) lines.push("", truncatedNotice(omitted, "outline items"));
44
+ return lines.join("\n");
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Imports — JS/TS only
49
+ // ---------------------------------------------------------------------------
50
+
51
+ export async function handleImports(runtime: TreeSitterRuntime, file: string): Promise<string> {
52
+ const grammarId = detectGrammar(file);
53
+ if (grammarId && !isJsTsGrammar(grammarId)) {
54
+ return "Unsupported language: imports is only supported for JavaScript and TypeScript files";
55
+ }
56
+ const result = await extractImports(runtime, file);
57
+ if (result.kind !== "success") return formatNonSuccess(result);
58
+
59
+ const { data: imports } = result;
60
+ if (imports.length === 0) return `No imports found in ${file}`;
61
+
62
+ const { included, truncated } = truncate(imports, MAX_ITEMS);
63
+ const lines = [`## Imports: ${file}`, ""];
64
+ for (const imp of included) {
65
+ const r = imp.range;
66
+ lines.push(`- "${imp.moduleSpecifier}" (L${r.startLine}:${r.startCharacter})`);
67
+ }
68
+ if (truncated) lines.push("", truncatedNotice(truncated, "imports"));
69
+ return lines.join("\n");
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Exports — JS/TS only
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export async function handleExports(runtime: TreeSitterRuntime, file: string): Promise<string> {
77
+ const grammarId = detectGrammar(file);
78
+ if (grammarId && !isJsTsGrammar(grammarId)) {
79
+ return "Unsupported language: exports is only supported for JavaScript and TypeScript files";
80
+ }
81
+ const result = await extractExports(runtime, file);
82
+ if (result.kind !== "success") return formatNonSuccess(result);
83
+
84
+ const { data: exports } = result;
85
+ if (exports.length === 0) return `No exports found in ${file}`;
86
+
87
+ const { included, truncated } = truncate(exports, MAX_ITEMS);
88
+ const lines = [`## Exports: ${file}`, ""];
89
+ for (const exp of included) {
90
+ const r = exp.range;
91
+ const from = exp.moduleSpecifier ? ` from "${exp.moduleSpecifier}"` : "";
92
+ lines.push(`- ${exp.kind}: ${exp.name}${from} (L${r.startLine}:${r.startCharacter})`);
93
+ }
94
+ if (truncated) lines.push("", truncatedNotice(truncated, "exports"));
95
+ return lines.join("\n");
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Node at position — all supported grammars
100
+ // ---------------------------------------------------------------------------
101
+
102
+ export async function handleNodeAt(
103
+ runtime: TreeSitterRuntime,
104
+ file: string,
105
+ line: number,
106
+ character: number,
107
+ ): Promise<string> {
108
+ const result = await lookupNodeAt(runtime, file, line, character);
109
+ if (result.kind !== "success") return formatNonSuccess(result);
110
+
111
+ const { data } = result;
112
+ const lines = [
113
+ `## Node at ${file}:${line}:${character}`,
114
+ "",
115
+ `**Type:** ${data.type}`,
116
+ `**Range:** L${data.range.startLine}:${data.range.startCharacter} — L${data.range.endLine}:${data.range.endCharacter}`,
117
+ `**Text:** ${truncateText(data.text, 200)}`,
118
+ ];
119
+
120
+ if (data.ancestry.length > 0) {
121
+ lines.push("", "**Ancestry:**");
122
+ const { included, truncated } = truncate(data.ancestry, MAX_ITEMS);
123
+ for (const ancestor of included) {
124
+ lines.push(
125
+ `- ${ancestor.type} (L${ancestor.range.startLine}:${ancestor.range.startCharacter}-L${ancestor.range.endLine}:${ancestor.range.endCharacter})`,
126
+ );
127
+ }
128
+ if (truncated) lines.push("", truncatedNotice(truncated, "ancestry entries"));
129
+ }
130
+
131
+ return lines.join("\n");
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Query — all supported grammars
136
+ // ---------------------------------------------------------------------------
137
+
138
+ export async function handleQuery(
139
+ runtime: TreeSitterRuntime,
140
+ file: string,
141
+ query: string,
142
+ ): Promise<string> {
143
+ const result = await runtime.queryFile(file, query);
144
+ if (result.kind !== "success") return formatNonSuccess(result);
145
+
146
+ const { data: captures } = result;
147
+ if (captures.length === 0) return `No matches for query in ${file}`;
148
+
149
+ const { included, truncated } = truncate(captures, MAX_ITEMS);
150
+ const lines = [`## Query results: ${file}`, ""];
151
+ for (const capture of included) {
152
+ const r = capture.range;
153
+ lines.push(
154
+ `- ${capture.name}: ${capture.nodeType} (L${r.startLine}:${r.startCharacter}-L${r.endLine}:${r.endCharacter})`,
155
+ );
156
+ lines.push(` \`${truncateText(capture.text, 120)}\``);
157
+ }
158
+ if (truncated) lines.push("", truncatedNotice(truncated, "captures"));
159
+ return lines.join("\n");
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Callees — many supported grammars
164
+ // ---------------------------------------------------------------------------
165
+
166
+ export async function handleCallees(
167
+ runtime: TreeSitterRuntime,
168
+ file: string,
169
+ line: number,
170
+ character: number,
171
+ ): Promise<string> {
172
+ const result = await lookupCalleesAt(runtime, file, line, character);
173
+ if (result.kind !== "success") return formatNonSuccess(result);
174
+
175
+ const { enclosingScope, callees } = result.data;
176
+ if (callees.length === 0) {
177
+ return `No outgoing calls found in \`${enclosingScope.name}\` at ${file}:${line}:${character}`;
178
+ }
179
+
180
+ const lines: string[] = [];
181
+ lines.push(`## Callees: ${file}:${line}:${character}`);
182
+ lines.push("");
183
+ lines.push(
184
+ `**${callees.length} outgoing call${callees.length > 1 ? "s" : ""}** from \`${enclosingScope.name}\` at L${enclosingScope.range.startLine}-L${enclosingScope.range.endLine}`,
185
+ );
186
+ lines.push("");
187
+
188
+ for (const c of callees.slice(0, MAX_ITEMS)) {
189
+ lines.push(`- \`${c.name}\` (L${c.range.startLine})`);
190
+ }
191
+ if (callees.length > MAX_ITEMS) {
192
+ lines.push("", truncatedNotice(callees.length - MAX_ITEMS, "callees"));
193
+ }
194
+
195
+ return lines.join("\n");
196
+ }
@@ -0,0 +1,107 @@
1
+ // Focused tool registration for the tree_sitter extension.
2
+ //
3
+ // Derives tool metadata, schemas, and prompt surfaces from tool-specs.ts.
4
+ // Handler functions from ./handlers.ts do the actual work.
5
+
6
+ import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+ import type { TreeSitterRuntime } from "../session/runtime.ts";
8
+ import {
9
+ handleCallees,
10
+ handleExports,
11
+ handleImports,
12
+ handleNodeAt,
13
+ handleOutline,
14
+ handleQuery,
15
+ } from "./handlers.ts";
16
+ import {
17
+ getTreeSitterToolSpec,
18
+ PARAM_SCHEMAS,
19
+ TREE_SITTER_TOOL_SPECS,
20
+ type TreeSitterToolName,
21
+ } from "./tool-specs.ts";
22
+
23
+ function notInitializedResult(): AgentToolResult<Record<string, unknown>> {
24
+ return {
25
+ content: [{ type: "text", text: "Tree-sitter not initialized. Start a new session first." }],
26
+ details: {},
27
+ };
28
+ }
29
+
30
+ function textResult(text: string): AgentToolResult<Record<string, unknown>> {
31
+ return { content: [{ type: "text", text }], details: {} };
32
+ }
33
+
34
+ /** Wraps a handler call into a pi-compatible execute function. */
35
+ function createExecute(
36
+ fn: (runtime: TreeSitterRuntime, params: Record<string, unknown>) => Promise<string> | string,
37
+ getRuntime: () => TreeSitterRuntime | undefined,
38
+ ) {
39
+ // biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
40
+ return async (
41
+ _toolCallId: string,
42
+ params: Record<string, unknown>,
43
+ _signal: AbortSignal | undefined,
44
+ _onUpdate: unknown,
45
+ _ctx: { cwd: string },
46
+ ): Promise<AgentToolResult<Record<string, unknown>>> => {
47
+ const runtime = getRuntime();
48
+ if (!runtime) return notInitializedResult();
49
+
50
+ // Guard against direct execute calls (tests, tool reuse) that bypass pi's schema validation
51
+ if (!params.file || typeof params.file !== "string") {
52
+ return textResult("Validation error: `file` is required.");
53
+ }
54
+
55
+ try {
56
+ const text = await fn(runtime, params);
57
+ return textResult(text);
58
+ } catch (error) {
59
+ return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+ };
62
+ }
63
+
64
+ // ── Handler dispatch map ───────────────────────────────────────────────
65
+
66
+ type HandlerFn = (
67
+ runtime: TreeSitterRuntime,
68
+ params: Record<string, unknown>,
69
+ ) => Promise<string> | string;
70
+
71
+ const HANDLER_MAP: Record<TreeSitterToolName, HandlerFn> = {
72
+ tree_sitter_outline: (runtime, params) => handleOutline(runtime, String(params.file)),
73
+ tree_sitter_imports: (runtime, params) => handleImports(runtime, String(params.file)),
74
+ tree_sitter_exports: (runtime, params) => handleExports(runtime, String(params.file)),
75
+ tree_sitter_node_at: (runtime, params) =>
76
+ handleNodeAt(runtime, String(params.file), Number(params.line), Number(params.character)),
77
+ tree_sitter_query: (runtime, params) =>
78
+ handleQuery(runtime, String(params.file), String(params.query)),
79
+ tree_sitter_callees: (runtime, params) =>
80
+ handleCallees(runtime, String(params.file), Number(params.line), Number(params.character)),
81
+ };
82
+
83
+ // ── Registration ───────────────────────────────────────────────────────
84
+
85
+ /**
86
+ * Register 6 focused tree-sitter tools.
87
+ *
88
+ * @param pi — extension API
89
+ * @param getRuntime — thunk returning the current runtime (or undefined before session_start)
90
+ */
91
+ export function registerFocusedTreeSitterTools(
92
+ pi: ExtensionAPI,
93
+ getRuntime: () => TreeSitterRuntime | undefined,
94
+ ): void {
95
+ for (const spec of TREE_SITTER_TOOL_SPECS) {
96
+ const spec2 = getTreeSitterToolSpec(spec.name);
97
+ pi.registerTool({
98
+ name: spec2.name,
99
+ label: spec2.label,
100
+ description: spec2.description,
101
+ promptSnippet: spec2.promptSnippet,
102
+ promptGuidelines: spec2.promptGuidelines,
103
+ parameters: PARAM_SCHEMAS[spec2.paramSchemaKey],
104
+ execute: createExecute(HANDLER_MAP[spec2.name], getRuntime),
105
+ });
106
+ }
107
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Single source of truth for the split `tree_sitter_*` tool surface.
3
+ *
4
+ * Tool registration, guidance, and parameter validation should derive
5
+ * from these specs so the public surface does not drift from the
6
+ * metadata that drives it.
7
+ */
8
+
9
+ import { Type } from "typebox";
10
+
11
+ // ── Shared parameter fragments ─────────────────────────────────────────
12
+
13
+ const FileParam = Type.String({ description: "File path" });
14
+ const LineParam = Type.Number({ description: "1-based line", minimum: 1 });
15
+ const CharacterParam = Type.Number({
16
+ description: "1-based UTF-16 column",
17
+ minimum: 1,
18
+ });
19
+ const QueryParam = Type.String({ description: "Tree-sitter query" });
20
+
21
+ export const PARAM_SCHEMAS = {
22
+ fileOnly: Type.Object({ file: FileParam }, { additionalProperties: false }),
23
+ fileLineChar: Type.Object(
24
+ { file: FileParam, line: LineParam, character: CharacterParam },
25
+ { additionalProperties: false },
26
+ ),
27
+ fileQuery: Type.Object({ file: FileParam, query: QueryParam }, { additionalProperties: false }),
28
+ } as const;
29
+
30
+ export type ParamSchemaKey = keyof typeof PARAM_SCHEMAS;
31
+
32
+ // ── Tool names ─────────────────────────────────────────────────────────
33
+
34
+ export const TREE_SITTER_TOOL_NAMES = [
35
+ "tree_sitter_outline",
36
+ "tree_sitter_imports",
37
+ "tree_sitter_exports",
38
+ "tree_sitter_node_at",
39
+ "tree_sitter_query",
40
+ "tree_sitter_callees",
41
+ ] as const;
42
+
43
+ export type TreeSitterToolName = (typeof TREE_SITTER_TOOL_NAMES)[number];
44
+
45
+ // ── Per-tool metadata ──────────────────────────────────────────────────
46
+
47
+ export interface TreeSitterToolSpec {
48
+ name: TreeSitterToolName;
49
+ label: string;
50
+ description: string;
51
+ promptSnippet: string;
52
+ promptGuidelines: string[];
53
+ paramSchemaKey: ParamSchemaKey;
54
+ }
55
+
56
+ export const TREE_SITTER_TOOL_SPECS: readonly TreeSitterToolSpec[] = [
57
+ {
58
+ name: "tree_sitter_outline",
59
+ label: "Tree-sitter Outline",
60
+ description: "Shallow JS/TS outline of top-level declarations and supported members.",
61
+ promptSnippet: "tree_sitter_outline — quick JS/TS outline",
62
+ promptGuidelines: ["Use tree_sitter_outline(file) for a quick JS/TS outline."],
63
+ paramSchemaKey: "fileOnly",
64
+ },
65
+ {
66
+ name: "tree_sitter_imports",
67
+ label: "Tree-sitter Imports",
68
+ description: "List imports in a JS/TS file.",
69
+ promptSnippet: "tree_sitter_imports — JS/TS imports",
70
+ promptGuidelines: ["Use tree_sitter_imports(file) to inspect JS/TS dependencies."],
71
+ paramSchemaKey: "fileOnly",
72
+ },
73
+ {
74
+ name: "tree_sitter_exports",
75
+ label: "Tree-sitter Exports",
76
+ description: "List exports in a JS/TS file.",
77
+ promptSnippet: "tree_sitter_exports — JS/TS exports",
78
+ promptGuidelines: ["Use tree_sitter_exports(file) to inspect JS/TS exports."],
79
+ paramSchemaKey: "fileOnly",
80
+ },
81
+ {
82
+ name: "tree_sitter_node_at",
83
+ label: "Tree-sitter Node At",
84
+ description: "Show the syntax node and ancestry at a file position.",
85
+ promptSnippet: "tree_sitter_node_at — syntax node at a position",
86
+ promptGuidelines: ["Use tree_sitter_node_at(file,line,character) for the exact syntax node."],
87
+ paramSchemaKey: "fileLineChar",
88
+ },
89
+ {
90
+ name: "tree_sitter_query",
91
+ label: "Tree-sitter Query",
92
+ description: "Run a custom Tree-sitter query against a file.",
93
+ promptSnippet: "tree_sitter_query — custom AST query",
94
+ promptGuidelines: ["Use tree_sitter_query(file,query) for custom AST matching."],
95
+ paramSchemaKey: "fileQuery",
96
+ },
97
+ {
98
+ name: "tree_sitter_callees",
99
+ label: "Tree-sitter Callees",
100
+ description: "List outgoing callees from the enclosing scope at a file position.",
101
+ promptSnippet: "tree_sitter_callees — outgoing callees",
102
+ promptGuidelines: ["Use tree_sitter_callees(file,line,character) for outgoing calls."],
103
+ paramSchemaKey: "fileLineChar",
104
+ },
105
+ ];
106
+
107
+ // ── Lookup helpers ─────────────────────────────────────────────────────
108
+
109
+ const TREE_SITTER_TOOL_SPEC_MAP = new Map<TreeSitterToolName, TreeSitterToolSpec>(
110
+ TREE_SITTER_TOOL_SPECS.map((spec) => [spec.name, spec]),
111
+ );
112
+
113
+ export function getTreeSitterToolSpec(toolName: TreeSitterToolName): TreeSitterToolSpec {
114
+ const spec = TREE_SITTER_TOOL_SPEC_MAP.get(toolName);
115
+ if (!spec) throw new Error(`Unknown tree_sitter tool: ${toolName}`);
116
+ return spec;
117
+ }