@mrclrchtr/supi-tree-sitter 1.3.1 → 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 (37) hide show
  1. package/README.md +58 -39
  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 +6 -2
  23. package/src/index.ts +6 -2
  24. package/src/{runtime.ts → session/runtime.ts} +6 -5
  25. package/src/session/service-registry.ts +30 -0
  26. package/src/{session.ts → session/session.ts} +20 -12
  27. package/src/tool/action-specs.ts +92 -0
  28. package/src/{callees.ts → tool/callees.ts} +3 -3
  29. package/src/{exports.ts → tool/exports.ts} +4 -4
  30. package/src/{formatting.ts → tool/formatting.ts} +1 -1
  31. package/src/tool/guidance.ts +31 -0
  32. package/src/{imports.ts → tool/imports.ts} +4 -4
  33. package/src/{node-at.ts → tool/node-at.ts} +3 -3
  34. package/src/{outline.ts → tool/outline.ts} +3 -3
  35. package/src/tree-sitter.ts +118 -91
  36. package/src/types.ts +13 -2
  37. /package/src/{structure.ts → tool/structure.ts} +0 -0
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
- // Public session factory and re-exports for @mrclrchtr/supi-tree-sitter.
1
+ // Public session factory, shared session service access, and re-exports for @mrclrchtr/supi-tree-sitter.
2
2
 
3
- export { createTreeSitterSession } from "./session.ts";
3
+ export { getSessionTreeSitterService } from "./session/service-registry.ts";
4
+ export { createTreeSitterSession } from "./session/session.ts";
4
5
  export type {
5
6
  CalleesAtResult,
6
7
  ExportRecord,
@@ -9,8 +10,11 @@ export type {
9
10
  NodeAtResult,
10
11
  OutlineItem,
11
12
  QueryCapture,
13
+ SessionTreeSitterService,
14
+ SessionTreeSitterServiceState,
12
15
  SourceRange,
13
16
  SupportedExtension,
14
17
  TreeSitterResult,
18
+ TreeSitterService,
15
19
  TreeSitterSession,
16
20
  } from "./types.ts";
@@ -2,10 +2,11 @@
2
2
 
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
+ import { resolveToolPath } from "@mrclrchtr/supi-core/api";
5
6
  import type { Language, Parser, Tree } from "web-tree-sitter";
6
- import { nodeToRange } from "./coordinates.ts";
7
- import { detectGrammar, resolveGrammarWasmPath } from "./language.ts";
8
- import type { GrammarId, QueryCapture, TreeSitterResult } from "./types.ts";
7
+ import { nodeToRange } from "../coordinates.ts";
8
+ import { detectGrammar, resolveGrammarWasmPath } from "../language.ts";
9
+ import type { GrammarId, QueryCapture, TreeSitterResult } from "../types.ts";
9
10
 
10
11
  interface ParserEntry {
11
12
  parser: Parser;
@@ -113,7 +114,7 @@ export class TreeSitterRuntime {
113
114
  grammarId: GrammarId;
114
115
  }>
115
116
  > {
116
- const resolvedPath = path.resolve(this.cwd, filePath);
117
+ const resolvedPath = resolveToolPath(this.cwd, filePath);
117
118
 
118
119
  // Check language support first
119
120
  const grammarId = detectGrammar(filePath);
@@ -212,7 +213,7 @@ export class TreeSitterRuntime {
212
213
 
213
214
  /** Resolve a file path from cwd. */
214
215
  resolvePath(filePath: string): string {
215
- return path.resolve(this.cwd, filePath);
216
+ return resolveToolPath(this.cwd, filePath);
216
217
  }
217
218
 
218
219
  /** Dispose all held parser resources. */
@@ -0,0 +1,30 @@
1
+ // Shared session-scoped Tree-sitter service registry.
2
+ // Peer extensions can import `getSessionTreeSitterService` from the package API
3
+ // to reuse the active structural runtime without creating duplicate sessions.
4
+
5
+ import { createSessionStateRegistry } from "@mrclrchtr/supi-core/api";
6
+ import type { SessionTreeSitterService, SessionTreeSitterServiceState } from "../types.ts";
7
+
8
+ const registry = createSessionStateRegistry<SessionTreeSitterServiceState>(
9
+ "supi-tree-sitter/session-registry",
10
+ );
11
+
12
+ /** Publish the shared Tree-sitter service for one session cwd. */
13
+ export function setSessionTreeSitterService(cwd: string, service: SessionTreeSitterService): void {
14
+ registry.set(cwd, { kind: "ready", service });
15
+ }
16
+
17
+ /** Acquire the shared Tree-sitter service state for one session cwd. */
18
+ export function getSessionTreeSitterService(cwd: string): SessionTreeSitterServiceState {
19
+ return (
20
+ registry.get(cwd) ?? {
21
+ kind: "unavailable",
22
+ reason: "No Tree-sitter session initialized for this workspace",
23
+ }
24
+ );
25
+ }
26
+
27
+ /** Remove the shared Tree-sitter service for one session cwd. */
28
+ export function clearSessionTreeSitterService(cwd: string): void {
29
+ registry.clear(cwd);
30
+ }
@@ -1,14 +1,13 @@
1
- // Session factory — creates a TreeSitterSession bound to a working directory.
1
+ // Session factory — creates runtime-backed Tree-sitter services and owned sessions.
2
2
 
3
- import { detectGrammar, isJsTsGrammar } from "./language.ts";
4
- import { TreeSitterRuntime } from "./runtime.ts";
3
+ import { detectGrammar, isJsTsGrammar } from "../language.ts";
5
4
  import {
6
5
  extractExports,
7
6
  extractImports,
8
7
  extractOutline,
9
8
  lookupCalleesAt,
10
9
  lookupNodeAt,
11
- } from "./structure.ts";
10
+ } from "../tool/structure.ts";
12
11
  import type {
13
12
  CalleesAtResult,
14
13
  ExportRecord,
@@ -17,16 +16,13 @@ import type {
17
16
  OutlineItem,
18
17
  QueryCapture,
19
18
  TreeSitterResult,
19
+ TreeSitterService,
20
20
  TreeSitterSession,
21
- } from "./types.ts";
22
-
23
- /**
24
- * Create a new Tree-sitter session bound to the given working directory.
25
- * The session owns parser/grammar reuse and must be disposed when done.
26
- */
27
- export function createTreeSitterSession(cwd: string): TreeSitterSession {
28
- const runtime = new TreeSitterRuntime(cwd);
21
+ } from "../types.ts";
22
+ import { TreeSitterRuntime } from "./runtime.ts";
29
23
 
24
+ /** Create a runtime-backed structural service without taking ownership of disposal. */
25
+ export function createTreeSitterService(runtime: TreeSitterRuntime): TreeSitterService {
30
26
  return {
31
27
  async canParse(file: string) {
32
28
  const result = await runtime.parseFile(file);
@@ -104,7 +100,19 @@ export function createTreeSitterSession(cwd: string): TreeSitterSession {
104
100
  ): Promise<TreeSitterResult<CalleesAtResult>> {
105
101
  return lookupCalleesAt(runtime, file, line, character);
106
102
  },
103
+ };
104
+ }
107
105
 
106
+ /**
107
+ * Create a new Tree-sitter session bound to the given working directory.
108
+ * The session owns parser/grammar reuse and must be disposed when done.
109
+ */
110
+ export function createTreeSitterSession(cwd: string): TreeSitterSession {
111
+ const runtime = new TreeSitterRuntime(cwd);
112
+ const service = createTreeSitterService(runtime);
113
+
114
+ return {
115
+ ...service,
108
116
  dispose() {
109
117
  runtime.dispose();
110
118
  },
@@ -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
+ }
@@ -1,8 +1,8 @@
1
1
  // Structural callee extraction — enclosing-scope lookup with per-language queries.
2
2
 
3
- import { detectGrammar } from "./language.ts";
4
- import type { TreeSitterRuntime } from "./runtime.ts";
5
- import type { GrammarId, SourceRange, TreeSitterResult } from "./types.ts";
3
+ import { detectGrammar } from "../language.ts";
4
+ import type { TreeSitterRuntime } from "../session/runtime.ts";
5
+ import type { GrammarId, SourceRange, TreeSitterResult } from "../types.ts";
6
6
 
7
7
  /** Result shape returned by lookupCalleesAt. */
8
8
  export interface CalleesAtResult {
@@ -1,9 +1,9 @@
1
1
  // Export extraction for supported files.
2
2
 
3
- import { nodeToRange } from "./coordinates.ts";
4
- import type { TreeSitterRuntime } from "./runtime.ts";
5
- import type { SyntaxNodeLike } from "./syntax-node.ts";
6
- import type { ExportRecord, TreeSitterResult } from "./types.ts";
3
+ import { nodeToRange } from "../coordinates.ts";
4
+ import type { TreeSitterRuntime } from "../session/runtime.ts";
5
+ import type { SyntaxNodeLike } from "../syntax-node.ts";
6
+ import type { ExportRecord, TreeSitterResult } from "../types.ts";
7
7
 
8
8
  /** Extract export records from a supported file. */
9
9
  export async function extractExports(
@@ -1,6 +1,6 @@
1
1
  // Formatting helpers for tree_sitter tool output.
2
2
 
3
- import type { OutlineItem, TreeSitterResult } from "./types.ts";
3
+ import type { OutlineItem, TreeSitterResult } from "../types.ts";
4
4
 
5
5
  export const MAX_ITEMS = 100;
6
6
 
@@ -0,0 +1,31 @@
1
+ // Prompt guidance and tool description for the tree_sitter tool.
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.
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
+
15
+ export const toolDescription = `Tree-sitter tool — parser-level structure and syntax queries for supported files.
16
+
17
+ Actions: ${formatTreeSitterActionList()}.
18
+
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";
@@ -1,9 +1,9 @@
1
1
  // Import extraction for supported files.
2
2
 
3
- import { nodeToRange } from "./coordinates.ts";
4
- import type { TreeSitterRuntime } from "./runtime.ts";
5
- import type { SyntaxNodeLike } from "./syntax-node.ts";
6
- import type { ImportRecord, TreeSitterResult } from "./types.ts";
3
+ import { nodeToRange } from "../coordinates.ts";
4
+ import type { TreeSitterRuntime } from "../session/runtime.ts";
5
+ import type { SyntaxNodeLike } from "../syntax-node.ts";
6
+ import type { ImportRecord, TreeSitterResult } from "../types.ts";
7
7
 
8
8
  /** Extract import records from a supported file. */
9
9
  export async function extractImports(
@@ -1,8 +1,8 @@
1
1
  // Node-at-position lookup.
2
2
 
3
- import { nodeToRange, publicToTreeSitter, splitSourceLines } from "./coordinates.ts";
4
- import type { TreeSitterRuntime } from "./runtime.ts";
5
- import type { NodeAtResult, SourceRange, TreeSitterResult } from "./types.ts";
3
+ import { nodeToRange, publicToTreeSitter, splitSourceLines } from "../coordinates.ts";
4
+ import type { TreeSitterRuntime } from "../session/runtime.ts";
5
+ import type { NodeAtResult, SourceRange, TreeSitterResult } from "../types.ts";
6
6
 
7
7
  const MAX_ANCESTRY = 10;
8
8
 
@@ -1,8 +1,8 @@
1
1
  // Outline extraction for supported files.
2
2
 
3
- import { nodeToRange } from "./coordinates.ts";
4
- import type { SyntaxNodeLike } from "./syntax-node.ts";
5
- import type { OutlineItem } from "./types.ts";
3
+ import { nodeToRange } from "../coordinates.ts";
4
+ import type { SyntaxNodeLike } from "../syntax-node.ts";
5
+ import type { OutlineItem } from "../types.ts";
6
6
 
7
7
  /** Node types that can be extracted directly as outline items. */
8
8
  const OUTLINE_DECLARATION_NODE_TYPES = new Set([
@@ -3,6 +3,20 @@
3
3
  import { StringEnum } from "@earendil-works/pi-ai";
4
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
5
  import { Type } from "typebox";
6
+ import { detectGrammar, isJsTsGrammar } from "./language.ts";
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";
6
20
  import {
7
21
  formatNonSuccess,
8
22
  formatOutlineItemsCapped,
@@ -11,56 +25,35 @@ import {
11
25
  truncatedNotice,
12
26
  truncateText,
13
27
  validationError,
14
- } from "./formatting.ts";
15
- import { detectGrammar, isJsTsGrammar } from "./language.ts";
16
- import { collectOutline } from "./outline.ts";
17
- import { TreeSitterRuntime } from "./runtime.ts";
18
- import { extractExports, extractImports, lookupCalleesAt, lookupNodeAt } from "./structure.ts";
19
-
20
- const TreeSitterActionEnum = StringEnum([
21
- "outline",
22
- "imports",
23
- "exports",
24
- "node_at",
25
- "query",
26
- "callees",
27
- ] as const);
28
-
29
- const toolDescription = `Tree-sitter tool — provides structural AST analysis for supported files.
30
-
31
- Actions:
32
- - outline: Extract structural declarations (functions, classes, interfaces, etc.). JavaScript/TypeScript only.
33
- - imports: List import statements with module specifiers. JavaScript/TypeScript only.
34
- - exports: List export declarations, re-exports, and export assignments with names and kinds. JavaScript/TypeScript only.
35
- - node_at: Find the syntax node at a position. Params: file, line, character
36
- - query: Run a Tree-sitter query. Params: file, query
37
- - callees: Find outgoing function/method calls from a position. Params: file, line, character. Supported for most grammars.
38
-
39
- Coordinates are 1-based (line, character), compatible with the lsp tool convention.
40
- Character is a UTF-16 code-unit column.
41
- Relative file paths resolve from the session working directory.
42
-
43
- Supported extensions: .ts, .tsx, .js, .jsx, .mts, .cts, .mjs, .cjs, .py, .pyi, .rs, .go, .mod, .c, .h, .cpp, .hpp, .cc, .cxx, .hxx, .c++, .h++, .java, .kt, .kts, .rb, .sh, .bash, .zsh, .html, .htm, .xhtml, .r, .sql`;
44
-
45
- const promptGuidelines = [
46
- "Use tree_sitter for structural syntax-tree analysis: extracting declarations, imports, exports, node-at-position lookup, and custom queries.",
47
- "Prefer tree_sitter when you need AST node types, exact source ranges, or parser-level structure that semantic language-server tooling does not expose.",
48
- "tree_sitter is a standalone structural analysis tool; use semantic language-server features separately when they are available for hover, definitions, references, or diagnostics.",
49
- ];
50
-
51
- const promptSnippet = `Use the tree_sitter tool for structural code analysis — outline, imports, exports, node-at-position lookup, and custom queries.`;
28
+ } from "./tool/formatting.ts";
29
+ import { promptGuidelines, promptSnippet, toolDescription } from "./tool/guidance.ts";
30
+ import { collectOutline } from "./tool/outline.ts";
31
+ import { extractExports, extractImports, lookupCalleesAt, lookupNodeAt } from "./tool/structure.ts";
32
+
33
+ const TreeSitterActionEnum = StringEnum(TREE_SITTER_ACTION_NAMES);
52
34
 
53
35
  export default function treeSitterExtension(pi: ExtensionAPI) {
54
36
  let runtime: TreeSitterRuntime | undefined;
37
+ let activeCwd: string | null = null;
55
38
 
56
39
  pi.on("session_start", (_event, ctx) => {
57
- runtime?.dispose();
40
+ if (runtime && activeCwd) {
41
+ clearSessionTreeSitterService(activeCwd);
42
+ runtime.dispose();
43
+ }
44
+
45
+ activeCwd = ctx.cwd;
58
46
  runtime = new TreeSitterRuntime(ctx.cwd);
47
+ setSessionTreeSitterService(ctx.cwd, createTreeSitterService(runtime));
59
48
  });
60
49
 
61
50
  pi.on("session_shutdown", () => {
51
+ if (activeCwd) {
52
+ clearSessionTreeSitterService(activeCwd);
53
+ }
62
54
  runtime?.dispose();
63
55
  runtime = undefined;
56
+ activeCwd = null;
64
57
  });
65
58
 
66
59
  pi.registerTool({
@@ -96,8 +89,6 @@ export default function treeSitterExtension(pi: ExtensionAPI) {
96
89
  });
97
90
  }
98
91
 
99
- type TreeSitterAction = "outline" | "imports" | "exports" | "node_at" | "query" | "callees";
100
-
101
92
  type ToolParams = {
102
93
  action?: string;
103
94
  file?: string;
@@ -106,17 +97,47 @@ type ToolParams = {
106
97
  query?: string;
107
98
  };
108
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
+
109
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 {
110
134
  if (!params.action) {
111
- return validationError(
112
- "`action` is required. Supported: outline, imports, exports, node_at, query, callees.",
113
- );
135
+ return validationError(`\`action\` is required. Supported: ${SUPPORTED_ACTIONS_TEXT}.`);
114
136
  }
115
137
 
116
- const action = toSupportedAction(params.action);
117
- if (!action) {
138
+ if (!isTreeSitterAction(params.action)) {
118
139
  return validationError(
119
- `Unknown action: ${params.action}. Supported: outline, imports, exports, node_at, query, callees`,
140
+ `Unknown action: ${params.action}. Supported: ${SUPPORTED_ACTIONS_TEXT}`,
120
141
  );
121
142
  }
122
143
 
@@ -124,12 +145,37 @@ async function executeToolAction(runtime: TreeSitterRuntime, params: ToolParams)
124
145
  return validationError("`file` is required for all actions.");
125
146
  }
126
147
 
127
- if (action === "outline") return handleOutline(runtime, params.file);
128
- if (action === "imports") return handleImports(runtime, params.file);
129
- if (action === "exports") return handleExports(runtime, params.file);
130
- if (action === "node_at") return handleNodeAt(runtime, params);
131
- if (action === "callees") return handleCallees(runtime, params);
132
- 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;
133
179
  }
134
180
 
135
181
  async function handleOutline(runtime: TreeSitterRuntime, file: string): Promise<string> {
@@ -198,18 +244,13 @@ async function handleExports(runtime: TreeSitterRuntime, file: string): Promise<
198
244
  return lines.join("\n");
199
245
  }
200
246
 
201
- async function handleNodeAt(runtime: TreeSitterRuntime, params: ToolParams): Promise<string> {
202
- if (!Number.isInteger(params.line) || (params.line as number) < 1) {
203
- return validationError("`line` must be a positive 1-based integer for node_at action.");
204
- }
205
- if (!Number.isInteger(params.character) || (params.character as number) < 1) {
206
- return validationError("`character` must be a positive 1-based integer for node_at action.");
207
- }
208
-
209
- const file = params.file;
210
- const line = params.line as number;
211
- const character = params.character as number;
212
- 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);
213
254
  if (result.kind !== "success") return formatNonSuccess(result);
214
255
 
215
256
  const { data } = result;
@@ -235,13 +276,12 @@ async function handleNodeAt(runtime: TreeSitterRuntime, params: ToolParams): Pro
235
276
  return lines.join("\n");
236
277
  }
237
278
 
238
- async function handleQuery(runtime: TreeSitterRuntime, params: ToolParams): Promise<string> {
239
- if (!params.query || params.query.trim().length === 0) {
240
- return validationError("`query` is required and must be non-empty.");
241
- }
242
-
243
- const file = params.file as string;
244
- 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);
245
285
  if (result.kind !== "success") return formatNonSuccess(result);
246
286
 
247
287
  const { data: captures } = result;
@@ -260,25 +300,12 @@ async function handleQuery(runtime: TreeSitterRuntime, params: ToolParams): Prom
260
300
  return lines.join("\n");
261
301
  }
262
302
 
263
- function toSupportedAction(action: string): TreeSitterAction | undefined {
264
- if (["outline", "imports", "exports", "node_at", "query", "callees"].includes(action)) {
265
- return action as TreeSitterAction;
266
- }
267
- return undefined;
268
- }
269
-
270
- async function handleCallees(runtime: TreeSitterRuntime, params: ToolParams): Promise<string> {
271
- if (!Number.isInteger(params.line) || (params.line as number) < 1) {
272
- return validationError("`line` must be a positive 1-based integer for callees action.");
273
- }
274
- if (!Number.isInteger(params.character) || (params.character as number) < 1) {
275
- return validationError("`character` must be a positive 1-based integer for callees action.");
276
- }
277
-
278
- const file = params.file as string;
279
- const line = params.line as number;
280
- const character = params.character as number;
281
-
303
+ async function handleCallees(
304
+ runtime: TreeSitterRuntime,
305
+ file: string,
306
+ line: number,
307
+ character: number,
308
+ ): Promise<string> {
282
309
  const result = await lookupCalleesAt(runtime, file, line, character);
283
310
  if (result.kind !== "success") return formatNonSuccess(result);
284
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"
File without changes