mcp-coordinator 0.4.0 → 0.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.
@@ -0,0 +1,36 @@
1
+ import type { Metrics } from "./metrics.js";
2
+ /**
3
+ * Tree-sitter symbol extractor.
4
+ *
5
+ * Loads grammars asynchronously at boot. extract() runs synchronously per call
6
+ * so it slots into the existing synchronous file_activity ingest path.
7
+ *
8
+ * Naming table per language documented in the v0.6 spec:
9
+ * - top-level fn / arrow assigned to const → `name`
10
+ * - class member → `Class.method`
11
+ * - anonymous default export → `<file_basename>:default`
12
+ * - re-exports, anonymous IIFE → not emitted
13
+ */
14
+ export declare class TreeSitterExtractor {
15
+ private grammars;
16
+ private ready;
17
+ private grammarsLoaded;
18
+ private totalGrammars;
19
+ private metrics?;
20
+ constructor(metrics?: Metrics);
21
+ load(): Promise<void>;
22
+ status(): {
23
+ ok: boolean;
24
+ grammars_loaded: number;
25
+ total_grammars: number;
26
+ optional: true;
27
+ };
28
+ /**
29
+ * Extract qualified symbol names from `content`. Returns null on parse
30
+ * failure, unsupported extension, or grammar not loaded.
31
+ * Caps output at 200 entries (per spec).
32
+ */
33
+ extract(filePath: string, content: string, _changedRanges: Array<[number, number]> | null): string[] | null;
34
+ private extToKey;
35
+ private walk;
36
+ }
@@ -0,0 +1,354 @@
1
+ import path from "path";
2
+ // ---------------------------------------------------------------------------
3
+ // Go receiver helpers (module-level so HANDLERS closure can reference them)
4
+ // ---------------------------------------------------------------------------
5
+ function goReceiverType(recv) {
6
+ if (!recv)
7
+ return null;
8
+ for (let i = 0; i < recv.namedChildCount; i++) {
9
+ const found = findGoTypeIdent(recv.namedChild(i));
10
+ if (found)
11
+ return found;
12
+ }
13
+ return null;
14
+ }
15
+ function findGoTypeIdent(node) {
16
+ if (!node)
17
+ return null;
18
+ if (node.type === "type_identifier")
19
+ return node.text;
20
+ if (node.type === "pointer_type") {
21
+ for (let i = 0; i < node.namedChildCount; i++) {
22
+ const found = findGoTypeIdent(node.namedChild(i));
23
+ if (found)
24
+ return found;
25
+ }
26
+ }
27
+ if (node.type === "parameter_declaration") {
28
+ for (let i = 0; i < node.namedChildCount; i++) {
29
+ const child = node.namedChild(i);
30
+ if (child.type !== "identifier") {
31
+ const found = findGoTypeIdent(child);
32
+ if (found)
33
+ return found;
34
+ }
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ // ---------------------------------------------------------------------------
40
+ // HANDLERS registry — one entry per language key
41
+ // ---------------------------------------------------------------------------
42
+ const HANDLERS = {
43
+ ts: {
44
+ classNodeTypes: new Set(["class_declaration"]),
45
+ fnNodeTypes: new Set(["function_declaration", "method_definition"]),
46
+ varDeclTypes: new Set(["variable_declarator"]),
47
+ exportStmtTypes: new Set(["export_statement"]),
48
+ },
49
+ tsx: {
50
+ classNodeTypes: new Set(["class_declaration"]),
51
+ fnNodeTypes: new Set(["function_declaration", "method_definition"]),
52
+ varDeclTypes: new Set(["variable_declarator"]),
53
+ exportStmtTypes: new Set(["export_statement"]),
54
+ },
55
+ js: {
56
+ classNodeTypes: new Set(["class_declaration"]),
57
+ fnNodeTypes: new Set(["function_declaration", "method_definition"]),
58
+ varDeclTypes: new Set(["variable_declarator"]),
59
+ exportStmtTypes: new Set(["export_statement"]),
60
+ },
61
+ py: {
62
+ classNodeTypes: new Set(["class_definition"]),
63
+ fnNodeTypes: new Set(["function_definition"]),
64
+ },
65
+ go: {
66
+ classNodeTypes: new Set(),
67
+ fnNodeTypes: new Set(["function_declaration", "method_declaration"]),
68
+ extractFnName: (node, rawName, classCtx, _ctx) => {
69
+ if (node.type === "method_declaration") {
70
+ const recv = node.childForFieldName?.("receiver");
71
+ const recvType = goReceiverType(recv);
72
+ if (recvType && rawName)
73
+ return `${recvType}.${rawName}`;
74
+ return rawName;
75
+ }
76
+ if (classCtx && rawName)
77
+ return `${classCtx}.${rawName}`;
78
+ return rawName;
79
+ },
80
+ },
81
+ rust: {
82
+ classNodeTypes: new Set(),
83
+ fnNodeTypes: new Set(["function_item"]),
84
+ containerNodeTypes: new Set(["impl_item"]),
85
+ extractContainerName: (node) => node.childForFieldName?.("type")?.text ?? null,
86
+ },
87
+ java: {
88
+ classNodeTypes: new Set(["class_declaration"]),
89
+ fnNodeTypes: new Set(["method_declaration"]),
90
+ },
91
+ cs: {
92
+ classNodeTypes: new Set(["class_declaration", "interface_declaration", "struct_declaration", "record_declaration"]),
93
+ fnNodeTypes: new Set(["method_declaration", "constructor_declaration", "property_declaration"]),
94
+ containerNodeTypes: new Set(["namespace_declaration"]),
95
+ extractContainerName: (_node) => null,
96
+ },
97
+ c: {
98
+ // C: function name lives in declarator field, not "name" field
99
+ classNodeTypes: new Set(),
100
+ fnNodeTypes: new Set(["function_definition"]),
101
+ extractFnName: (node, _rawName, classCtx, _ctx) => {
102
+ // function_definition -> declarator (function_declarator) -> declarator (identifier)
103
+ const decl = node.childForFieldName?.("declarator");
104
+ const name = decl?.childForFieldName?.("declarator")?.text ?? decl?.namedChild?.(0)?.text ?? null;
105
+ if (name && classCtx)
106
+ return `${classCtx}.${name}`;
107
+ return name;
108
+ },
109
+ },
110
+ cpp: {
111
+ // C++: class_specifier has "name" field; function name via declarator chain
112
+ classNodeTypes: new Set(["class_specifier", "struct_specifier"]),
113
+ fnNodeTypes: new Set(["function_definition"]),
114
+ extractFnName: (node, _rawName, classCtx, _ctx) => {
115
+ const decl = node.childForFieldName?.("declarator");
116
+ const name = decl?.childForFieldName?.("declarator")?.text ?? decl?.namedChild?.(0)?.text ?? null;
117
+ if (name && classCtx)
118
+ return `${classCtx}.${name}`;
119
+ return name;
120
+ },
121
+ },
122
+ ruby: {
123
+ classNodeTypes: new Set(["class"]),
124
+ fnNodeTypes: new Set(["method", "singleton_method"]),
125
+ containerNodeTypes: new Set(["module"]),
126
+ extractContainerName: (node) => node.childForFieldName?.("name")?.text ?? null,
127
+ },
128
+ php: {
129
+ classNodeTypes: new Set(["class_declaration", "interface_declaration", "trait_declaration"]),
130
+ fnNodeTypes: new Set(["method_declaration", "function_definition"]),
131
+ containerNodeTypes: new Set(["namespace_definition"]),
132
+ extractContainerName: (_node) => null,
133
+ },
134
+ kotlin: {
135
+ // Kotlin: class/object have no "name" field — use namedChild(0) (type_identifier)
136
+ // function_declaration has no "name" field — use namedChild(0) (simple_identifier)
137
+ classNodeTypes: new Set(),
138
+ fnNodeTypes: new Set(["function_declaration"]),
139
+ containerNodeTypes: new Set(["class_declaration", "object_declaration"]),
140
+ extractContainerName: (node) => node.namedChild(0)?.text ?? null,
141
+ extractFnName: (node, _rawName, classCtx, _ctx) => {
142
+ const name = node.namedChild(0)?.text ?? null;
143
+ if (name && classCtx)
144
+ return `${classCtx}.${name}`;
145
+ return name;
146
+ },
147
+ },
148
+ swift: {
149
+ classNodeTypes: new Set(["class_declaration", "struct_declaration", "protocol_declaration"]),
150
+ fnNodeTypes: new Set(["function_declaration"]),
151
+ containerNodeTypes: new Set(["extension_declaration"]),
152
+ extractContainerName: (node) => node.childForFieldName?.("type")?.text
153
+ ?? node.namedChild(0)?.text
154
+ ?? null,
155
+ },
156
+ bash: {
157
+ classNodeTypes: new Set(),
158
+ fnNodeTypes: new Set(["function_definition"]),
159
+ },
160
+ };
161
+ /**
162
+ * Tree-sitter symbol extractor.
163
+ *
164
+ * Loads grammars asynchronously at boot. extract() runs synchronously per call
165
+ * so it slots into the existing synchronous file_activity ingest path.
166
+ *
167
+ * Naming table per language documented in the v0.6 spec:
168
+ * - top-level fn / arrow assigned to const → `name`
169
+ * - class member → `Class.method`
170
+ * - anonymous default export → `<file_basename>:default`
171
+ * - re-exports, anonymous IIFE → not emitted
172
+ */
173
+ export class TreeSitterExtractor {
174
+ grammars = new Map();
175
+ ready = false;
176
+ grammarsLoaded = 0;
177
+ totalGrammars = 15;
178
+ metrics;
179
+ constructor(metrics) {
180
+ this.metrics = metrics;
181
+ }
182
+ async load() {
183
+ const tryLoad = async (key, modName, sub) => {
184
+ try {
185
+ const tsMod = await import("tree-sitter").catch(() => null);
186
+ const langMod = await import(modName).catch(() => null);
187
+ if (!tsMod || !langMod)
188
+ return;
189
+ const Parser = tsMod.default || tsMod;
190
+ const parser = new Parser();
191
+ const langObj = langMod.default || langMod;
192
+ const language = sub ? langObj[sub] : langObj;
193
+ if (!language)
194
+ return;
195
+ parser.setLanguage(language);
196
+ this.grammars.set(key, { parser, language });
197
+ this.grammarsLoaded++;
198
+ }
199
+ catch {
200
+ // optionalDependency — silently skip when unavailable
201
+ }
202
+ };
203
+ await tryLoad("ts", "tree-sitter-typescript", "typescript");
204
+ await tryLoad("tsx", "tree-sitter-typescript", "tsx");
205
+ await tryLoad("js", "tree-sitter-javascript");
206
+ await tryLoad("py", "tree-sitter-python");
207
+ await tryLoad("go", "tree-sitter-go");
208
+ await tryLoad("rust", "tree-sitter-rust");
209
+ await tryLoad("java", "tree-sitter-java");
210
+ await tryLoad("cs", "tree-sitter-c-sharp");
211
+ await tryLoad("c", "tree-sitter-c");
212
+ await tryLoad("cpp", "tree-sitter-cpp");
213
+ await tryLoad("ruby", "tree-sitter-ruby");
214
+ await tryLoad("php", "tree-sitter-php", "php");
215
+ await tryLoad("kotlin", "tree-sitter-kotlin");
216
+ await tryLoad("swift", "tree-sitter-swift");
217
+ await tryLoad("bash", "tree-sitter-bash");
218
+ this.ready = true;
219
+ }
220
+ status() {
221
+ return {
222
+ ok: this.ready && this.grammarsLoaded > 0,
223
+ grammars_loaded: this.grammarsLoaded,
224
+ total_grammars: this.totalGrammars,
225
+ optional: true,
226
+ };
227
+ }
228
+ /**
229
+ * Extract qualified symbol names from `content`. Returns null on parse
230
+ * failure, unsupported extension, or grammar not loaded.
231
+ * Caps output at 200 entries (per spec).
232
+ */
233
+ extract(filePath, content, _changedRanges) {
234
+ const ext = path.extname(filePath).toLowerCase();
235
+ const key = this.extToKey(ext);
236
+ if (!key)
237
+ return null;
238
+ const grammar = this.grammars.get(key);
239
+ if (!grammar)
240
+ return null;
241
+ let tree;
242
+ try {
243
+ tree = grammar.parser.parse(content);
244
+ }
245
+ catch {
246
+ this.metrics?.treeSitterParseFailures.inc();
247
+ return null;
248
+ }
249
+ if (!tree || !tree.rootNode || tree.rootNode.hasError) {
250
+ this.metrics?.treeSitterParseFailures.inc();
251
+ return null;
252
+ }
253
+ const symbols = [];
254
+ this.walk(tree.rootNode, symbols, key, path.basename(filePath, ext));
255
+ return symbols.slice(0, 200);
256
+ }
257
+ extToKey(ext) {
258
+ switch (ext) {
259
+ case ".ts": return "ts";
260
+ case ".tsx": return "tsx";
261
+ case ".js":
262
+ case ".jsx":
263
+ case ".mjs":
264
+ case ".cjs": return "js";
265
+ case ".py": return "py";
266
+ case ".go": return "go";
267
+ case ".rs": return "rust";
268
+ case ".java": return "java";
269
+ case ".cs": return "cs";
270
+ case ".c":
271
+ case ".h": return "c";
272
+ case ".cpp":
273
+ case ".cc":
274
+ case ".cxx":
275
+ case ".hpp":
276
+ case ".hh": return "cpp";
277
+ case ".rb": return "ruby";
278
+ case ".php": return "php";
279
+ case ".kt":
280
+ case ".kts": return "kotlin";
281
+ case ".swift": return "swift";
282
+ case ".sh":
283
+ case ".bash": return "bash";
284
+ default: return null;
285
+ }
286
+ }
287
+ walk(node, out, lang, basename, classCtx = null) {
288
+ if (out.length >= 200)
289
+ return;
290
+ const handler = HANDLERS[lang];
291
+ if (!handler)
292
+ return;
293
+ const type = node.type;
294
+ const nameField = handler.classNameField ?? "name";
295
+ const nameNode = node.childForFieldName?.(nameField);
296
+ const ctx = { out, lang, basename };
297
+ // 1. Class-like containers
298
+ if (handler.classNodeTypes.has(type)) {
299
+ const className = nameNode?.text ?? null;
300
+ for (let i = 0; i < node.namedChildCount; i++) {
301
+ this.walk(node.namedChild(i), out, lang, basename, className);
302
+ }
303
+ return;
304
+ }
305
+ // 2. Non-class containers (Rust impl, Ruby module, C# namespace, etc.)
306
+ if (handler.containerNodeTypes?.has(type)) {
307
+ const containerName = handler.extractContainerName?.(node) ?? null;
308
+ for (let i = 0; i < node.namedChildCount; i++) {
309
+ this.walk(node.namedChild(i), out, lang, basename, containerName);
310
+ }
311
+ return;
312
+ }
313
+ // 3. Function-like leaves
314
+ if (handler.fnNodeTypes.has(type)) {
315
+ const rawName = node.childForFieldName?.("name")?.text ?? null;
316
+ let emitted;
317
+ if (handler.extractFnName) {
318
+ emitted = handler.extractFnName(node, rawName, classCtx, ctx);
319
+ }
320
+ else {
321
+ emitted = rawName;
322
+ if (emitted && classCtx)
323
+ emitted = `${classCtx}.${emitted}`;
324
+ }
325
+ if (emitted)
326
+ out.push(emitted);
327
+ return;
328
+ }
329
+ // 4. Variable declarators (const X = () => …)
330
+ if (handler.varDeclTypes?.has(type)) {
331
+ const valNode = node.childForFieldName?.("value");
332
+ if (valNode && (valNode.type === "arrow_function" || valNode.type === "function_expression")) {
333
+ const name = node.childForFieldName?.("name")?.text;
334
+ if (name)
335
+ out.push(name);
336
+ }
337
+ return;
338
+ }
339
+ // 5. Anonymous default exports
340
+ if (handler.exportStmtTypes?.has(type)) {
341
+ const decl = node.namedChild(0);
342
+ if (decl && (decl.type === "arrow_function" ||
343
+ (decl.type === "function_declaration" && !decl.childForFieldName?.("name")))) {
344
+ out.push(`${basename}:default`);
345
+ return;
346
+ }
347
+ // Named export — fall through to recurse so class/fn inside are captured
348
+ }
349
+ // Recurse
350
+ for (let i = 0; i < node.namedChildCount; i++) {
351
+ this.walk(node.namedChild(i), out, lang, basename, classCtx);
352
+ }
353
+ }
354
+ }
@@ -0,0 +1,42 @@
1
+ import { type Logger } from "./logger.js";
2
+ import type { Metrics } from "./metrics.js";
3
+ /**
4
+ * Tracks files an agent is currently editing (between PreToolUse and
5
+ * PostToolUse hooks). Distinct from file_activity — that's an append-only
6
+ * historical log; this is current state with TTL.
7
+ *
8
+ * Lifecycle:
9
+ * PreToolUse → start(agent, file, ttlMin) → UPSERT row
10
+ * PostToolUse → stop(agent, file) → DELETE row
11
+ * Sweeper → sweepExpired() → DELETE rows past claim_until
12
+ * Agent LWT → clearForAgent(agent) → DELETE all rows for agent
13
+ */
14
+ export declare class WorkingFilesTracker {
15
+ private sweeperHandle;
16
+ private log;
17
+ private metrics?;
18
+ constructor(logger?: Logger, metrics?: Metrics);
19
+ /**
20
+ * Start (or refresh) a working-files claim. Idempotent: re-calling with
21
+ * the same (agent_id, file_path) updates last_activity_at + claim_until
22
+ * without erroring.
23
+ */
24
+ start(agentId: string, filePath: string, ttlMinutes: number): void;
25
+ /**
26
+ * Stop a working-files claim. No-op when no row matches (PostToolUse can
27
+ * arrive after a TTL eviction or before the matching PreToolUse on slow Pre).
28
+ */
29
+ stop(agentId: string, filePath: string): void;
30
+ /** Returns number of rows evicted. */
31
+ sweepExpired(): number;
32
+ /** Called when an agent goes offline (MQTT LWT). Returns rows deleted. */
33
+ clearForAgent(agentId: string): number;
34
+ /**
35
+ * Background sweeper. unref() so it doesn't keep the loop alive at shutdown.
36
+ * Idempotent — second call is a no-op until stopSweeper().
37
+ */
38
+ startSweeper(intervalMs?: number): void;
39
+ stopSweeper(): void;
40
+ /** Read in-flight files map: file_path → set<agent_id>, excluding caller. */
41
+ getIndex(filePaths: string[], excludeAgentId: string): Map<string, Set<string>>;
42
+ }
@@ -0,0 +1,111 @@
1
+ import { getDb } from "./database.js";
2
+ import { silentLogger } from "./logger.js";
3
+ /**
4
+ * Tracks files an agent is currently editing (between PreToolUse and
5
+ * PostToolUse hooks). Distinct from file_activity — that's an append-only
6
+ * historical log; this is current state with TTL.
7
+ *
8
+ * Lifecycle:
9
+ * PreToolUse → start(agent, file, ttlMin) → UPSERT row
10
+ * PostToolUse → stop(agent, file) → DELETE row
11
+ * Sweeper → sweepExpired() → DELETE rows past claim_until
12
+ * Agent LWT → clearForAgent(agent) → DELETE all rows for agent
13
+ */
14
+ export class WorkingFilesTracker {
15
+ sweeperHandle = null;
16
+ log;
17
+ metrics;
18
+ constructor(logger, metrics) {
19
+ this.log = logger || silentLogger;
20
+ this.metrics = metrics;
21
+ }
22
+ /**
23
+ * Start (or refresh) a working-files claim. Idempotent: re-calling with
24
+ * the same (agent_id, file_path) updates last_activity_at + claim_until
25
+ * without erroring.
26
+ */
27
+ start(agentId, filePath, ttlMinutes) {
28
+ const db = getDb();
29
+ const existing = db.prepare("SELECT 1 FROM working_files WHERE agent_id = ? AND file_path = ?")
30
+ .get(agentId, filePath);
31
+ db.prepare(`INSERT INTO working_files (agent_id, file_path, started_at, last_activity_at, claim_until)
32
+ VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now', '+' || CAST(? AS TEXT) || ' minutes'))
33
+ ON CONFLICT(agent_id, file_path) DO UPDATE SET
34
+ last_activity_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
35
+ claim_until = strftime('%Y-%m-%dT%H:%M:%SZ', 'now', '+' || CAST(? AS TEXT) || ' minutes')`).run(agentId, filePath, ttlMinutes, ttlMinutes);
36
+ this.metrics?.workingFilesStarts.inc({ result: existing ? "updated" : "inserted" });
37
+ }
38
+ /**
39
+ * Stop a working-files claim. No-op when no row matches (PostToolUse can
40
+ * arrive after a TTL eviction or before the matching PreToolUse on slow Pre).
41
+ */
42
+ stop(agentId, filePath) {
43
+ const db = getDb();
44
+ db.prepare("DELETE FROM working_files WHERE agent_id = ? AND file_path = ?")
45
+ .run(agentId, filePath);
46
+ }
47
+ /** Returns number of rows evicted. */
48
+ sweepExpired() {
49
+ const db = getDb();
50
+ const result = db.prepare("DELETE FROM working_files WHERE claim_until < strftime('%Y-%m-%dT%H:%M:%SZ', 'now')").run();
51
+ const evicted = Number(result.changes ?? 0);
52
+ if (this.metrics) {
53
+ const count = db.prepare("SELECT COUNT(*) AS c FROM working_files").get().c;
54
+ this.metrics.workingFilesActive.set(count);
55
+ }
56
+ return evicted;
57
+ }
58
+ /** Called when an agent goes offline (MQTT LWT). Returns rows deleted. */
59
+ clearForAgent(agentId) {
60
+ const db = getDb();
61
+ const result = db.prepare("DELETE FROM working_files WHERE agent_id = ?").run(agentId);
62
+ return Number(result.changes ?? 0);
63
+ }
64
+ /**
65
+ * Background sweeper. unref() so it doesn't keep the loop alive at shutdown.
66
+ * Idempotent — second call is a no-op until stopSweeper().
67
+ */
68
+ startSweeper(intervalMs = 60000) {
69
+ if (this.sweeperHandle)
70
+ return;
71
+ this.sweeperHandle = setInterval(() => {
72
+ try {
73
+ const evicted = this.sweepExpired();
74
+ if (evicted > 0)
75
+ this.log.info({ evicted }, "working_files sweep");
76
+ }
77
+ catch (err) {
78
+ this.log.warn({ err }, "working_files sweep failed");
79
+ }
80
+ }, intervalMs);
81
+ if (typeof this.sweeperHandle.unref === "function")
82
+ this.sweeperHandle.unref();
83
+ }
84
+ stopSweeper() {
85
+ if (this.sweeperHandle) {
86
+ clearInterval(this.sweeperHandle);
87
+ this.sweeperHandle = null;
88
+ }
89
+ }
90
+ /** Read in-flight files map: file_path → set<agent_id>, excluding caller. */
91
+ getIndex(filePaths, excludeAgentId) {
92
+ const index = new Map();
93
+ if (filePaths.length === 0)
94
+ return index;
95
+ const db = getDb();
96
+ const placeholders = filePaths.map(() => "?").join(",");
97
+ const rows = db.prepare(`SELECT DISTINCT file_path, agent_id FROM working_files
98
+ WHERE file_path IN (${placeholders})
99
+ AND agent_id != ?
100
+ AND claim_until > strftime('%Y-%m-%dT%H:%M:%SZ', 'now')`).all(...filePaths, excludeAgentId);
101
+ for (const r of rows) {
102
+ let set = index.get(r.file_path);
103
+ if (!set) {
104
+ set = new Set();
105
+ index.set(r.file_path, set);
106
+ }
107
+ set.add(r.agent_id);
108
+ }
109
+ return index;
110
+ }
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-coordinator",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "mcpName": "io.github.swoofer/mcp-coordinator",
5
5
  "description": "Embedded MQTT broker + MCP server for multi-agent coordination",
6
6
  "type": "module",
@@ -77,6 +77,23 @@
77
77
  "typescript": "^5.7.0",
78
78
  "vitest": "^4.1.0"
79
79
  },
80
+ "optionalDependencies": {
81
+ "tree-sitter": "^0.21.1",
82
+ "tree-sitter-typescript": "^0.21.2",
83
+ "tree-sitter-javascript": "^0.21.4",
84
+ "tree-sitter-python": "^0.21.0",
85
+ "tree-sitter-go": "^0.21.0",
86
+ "tree-sitter-rust": "^0.21.2",
87
+ "tree-sitter-java": "^0.21.0",
88
+ "tree-sitter-c-sharp": "^0.21.3",
89
+ "tree-sitter-c": "^0.21.0",
90
+ "tree-sitter-cpp": "^0.22.0",
91
+ "tree-sitter-ruby": "^0.21.0",
92
+ "tree-sitter-php": "^0.22.0",
93
+ "tree-sitter-kotlin": "^0.3.0",
94
+ "tree-sitter-swift": "^0.6.0",
95
+ "tree-sitter-bash": "^0.21.0"
96
+ },
80
97
  "engines": {
81
98
  "node": ">=20"
82
99
  }