@kentwynn/kgraph 0.2.22 → 0.2.24

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.
@@ -3,22 +3,44 @@ import crypto from 'node:crypto';
3
3
  import { readFile, stat } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { estimateTokens } from '../session/token-estimator.js';
6
+ import { extractBroadSymbols, supportsBroadExtraction, } from './broad-symbol-extractor.js';
6
7
  import { extractCSymbols } from './c-symbol-extractor.js';
7
8
  import { extractCSharpSymbols } from './csharp-symbol-extractor.js';
8
9
  import { buildFastGlobIgnore, detectLanguage, isPreciseLanguage, readGitignorePatterns, shouldExclude, } from './file-classifier.js';
9
- import { getChangedFilesSince, isGitRepo } from './git-utils.js';
10
+ import { getChangedFilesSince, getGitIgnoredFiles, isGitRepo, } from './git-utils.js';
10
11
  import { extractGoSymbols } from './go-symbol-extractor.js';
11
12
  import { extractJvmSymbols } from './jvm-symbol-extractor.js';
13
+ import { extractPhpSymbols } from './php-symbol-extractor.js';
12
14
  import { extractPythonSymbols } from './python-symbol-extractor.js';
15
+ import { extractRubySymbols } from './ruby-symbol-extractor.js';
13
16
  import { extractRustSymbols } from './rust-symbol-extractor.js';
17
+ import { extractShellSymbols } from './shell-symbol-extractor.js';
18
+ import { extractSqlSymbols } from './sql-symbol-extractor.js';
14
19
  import { extractTsSymbols } from './ts-symbol-extractor.js';
15
20
  const C_EXTS = new Set(['.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx']);
16
21
  const JVM_EXTS = new Set(['.java', '.kt', '.kts']);
22
+ const TS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
17
23
  async function extractSymbols(text, repoPath) {
18
24
  const ext = path.extname(repoPath);
25
+ const language = detectLanguage(repoPath);
26
+ if (supportsBroadExtraction(language)) {
27
+ return extractBroadSymbols(text, repoPath, language);
28
+ }
19
29
  if (ext === '.py' || ext === '.pyw' || ext === '.pyi') {
20
30
  return extractPythonSymbols(text, repoPath);
21
31
  }
32
+ if (ext === '.php') {
33
+ return extractPhpSymbols(text, repoPath);
34
+ }
35
+ if (ext === '.rb' || ext === '.rake' || language === 'ruby') {
36
+ return extractRubySymbols(text, repoPath);
37
+ }
38
+ if (['.sh', '.bash', '.zsh', '.fish'].includes(ext) || language === 'shell') {
39
+ return extractShellSymbols(text, repoPath);
40
+ }
41
+ if (ext === '.sql') {
42
+ return extractSqlSymbols(text, repoPath);
43
+ }
22
44
  if (ext === '.go') {
23
45
  return extractGoSymbols(text, repoPath);
24
46
  }
@@ -34,19 +56,26 @@ async function extractSymbols(text, repoPath) {
34
56
  if (ext === '.cs') {
35
57
  return extractCSharpSymbols(text, repoPath);
36
58
  }
37
- return extractTsSymbols(text, repoPath);
59
+ if (TS_EXTS.has(ext)) {
60
+ return extractTsSymbols(text, repoPath);
61
+ }
62
+ return { symbols: [], dependencies: [], relationships: [], warnings: [] };
38
63
  }
39
64
  export async function scanRepository(rootPath, config, previous) {
40
65
  const gitignorePatterns = await readGitignorePatterns(rootPath);
41
66
  const allExcludes = [...config.exclude, ...gitignorePatterns];
42
67
  const mergedConfig = { ...config, exclude: allExcludes };
43
- const entries = await fg(config.include, {
68
+ const rawEntries = await fg(config.include, {
44
69
  cwd: rootPath,
45
70
  dot: true,
46
71
  onlyFiles: true,
47
72
  unique: true,
48
73
  ignore: buildFastGlobIgnore(allExcludes),
49
74
  });
75
+ const gitIgnored = (await isGitRepo(rootPath))
76
+ ? await getGitIgnoredFiles(rootPath, rawEntries)
77
+ : new Set();
78
+ const entries = rawEntries.filter((entry) => !gitIgnored.has(entry));
50
79
  // Build lookup maps from previous scan for incremental skip
51
80
  const prevFileByPath = new Map((previous?.files ?? []).map((f) => [f.path, f]));
52
81
  const prevSymbolsByFile = new Map();
@@ -190,7 +219,7 @@ export async function scanRepository(rootPath, config, previous) {
190
219
  }
191
220
  resolveLocalDependencies(dependencies, files);
192
221
  relationships.push(...buildImportRelationships(dependencies));
193
- relationships.push(...detectMovedFiles(previous?.files ?? [], files));
222
+ relationships.push(...detectMovedFiles((previous?.files ?? []).filter((file) => !shouldExclude(file.path, mergedConfig)), files));
194
223
  return {
195
224
  files,
196
225
  symbols,
@@ -223,6 +252,15 @@ const SOURCE_EXTENSIONS = [
223
252
  '.hpp',
224
253
  '.hxx',
225
254
  '.cs',
255
+ '.php',
256
+ '.swift',
257
+ '.rb',
258
+ '.rake',
259
+ '.lua',
260
+ '.dart',
261
+ '.ex',
262
+ '.exs',
263
+ '.scala',
226
264
  ];
227
265
  function resolveLocalDependencies(dependencies, files) {
228
266
  const filePaths = new Set(files.map((file) => file.path));
@@ -237,6 +275,9 @@ function resolveLocalDependencyPath(fromFile, specifier, filePaths) {
237
275
  if (!specifier.startsWith('.')) {
238
276
  return undefined;
239
277
  }
278
+ if (path.posix.extname(fromFile) === '.py') {
279
+ return resolvePythonRelativeImportPath(fromFile, specifier, filePaths);
280
+ }
240
281
  const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), specifier));
241
282
  const candidates = path.posix.extname(base)
242
283
  ? [base]
@@ -246,6 +287,26 @@ function resolveLocalDependencyPath(fromFile, specifier, filePaths) {
246
287
  ];
247
288
  return candidates.find((candidate) => filePaths.has(candidate));
248
289
  }
290
+ function resolvePythonRelativeImportPath(fromFile, specifier, filePaths) {
291
+ const match = specifier.match(/^(\.+)(.*)$/);
292
+ if (!match) {
293
+ return undefined;
294
+ }
295
+ const [, dots, moduleName] = match;
296
+ const packageParts = path.posix.dirname(fromFile).split('/');
297
+ const levelsUp = Math.max(0, dots.length - 1);
298
+ const baseParts = levelsUp > 0 ? packageParts.slice(0, -levelsUp) : packageParts;
299
+ const modulePath = moduleName
300
+ .split('.')
301
+ .filter(Boolean)
302
+ .join('/');
303
+ const base = path.posix.normalize(path.posix.join(...baseParts, modulePath));
304
+ const candidates = [
305
+ `${base}.py`,
306
+ path.posix.join(base, '__init__.py'),
307
+ ];
308
+ return candidates.find((candidate) => filePaths.has(candidate));
309
+ }
249
310
  function buildImportRelationships(dependencies) {
250
311
  return dependencies.map((dependency) => ({
251
312
  sourceType: 'file',
@@ -0,0 +1,2 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractRubySymbols(sourceText: string, filePath: string): Promise<SymbolExtractionResult>;
@@ -0,0 +1,75 @@
1
+ import { emptyExtractionResult, ExtractionContext, } from './extraction-context.js';
2
+ import { parseSource } from './tree-sitter-parser.js';
3
+ export async function extractRubySymbols(sourceText, filePath) {
4
+ if (!sourceText.trim()) {
5
+ return emptyExtractionResult();
6
+ }
7
+ const tree = await parseSource(sourceText, 'ruby');
8
+ const context = new ExtractionContext(filePath);
9
+ function addNamedSymbol(node, kind, parentName) {
10
+ const nameNode = findNameNode(node);
11
+ if (!nameNode)
12
+ return undefined;
13
+ return context.addSymbol({
14
+ name: nameNode.text,
15
+ kind,
16
+ startLine: node.startPosition.row + 1,
17
+ endLine: node.endPosition.row + 1,
18
+ exported: true,
19
+ parentName,
20
+ });
21
+ }
22
+ function walk(node, parentName) {
23
+ switch (node.type) {
24
+ case 'call': {
25
+ const methodName = findCallMethodName(node);
26
+ if (methodName === 'require' || methodName === 'require_relative') {
27
+ const argument = findFirstStringContent(node);
28
+ if (argument) {
29
+ context.addDependency(argument, methodName === 'require_relative' ? 'local' : 'package');
30
+ }
31
+ return;
32
+ }
33
+ break;
34
+ }
35
+ case 'class':
36
+ case 'module': {
37
+ const symbol = addNamedSymbol(node, node.type === 'class' ? 'class' : 'type', parentName);
38
+ const nextParent = symbol?.name ?? parentName;
39
+ for (const child of node.namedChildren) {
40
+ walk(child, nextParent);
41
+ }
42
+ return;
43
+ }
44
+ case 'method':
45
+ case 'singleton_method': {
46
+ const symbol = addNamedSymbol(node, parentName ? 'method' : 'function', parentName);
47
+ if (symbol && parentName) {
48
+ const parent = context.symbols.find((candidate) => candidate.name === parentName &&
49
+ (candidate.kind === 'class' || candidate.kind === 'type'));
50
+ if (parent)
51
+ context.addSymbolContains(parent, symbol);
52
+ }
53
+ return;
54
+ }
55
+ }
56
+ for (const child of node.namedChildren) {
57
+ walk(child, parentName);
58
+ }
59
+ }
60
+ walk(tree.rootNode);
61
+ tree.delete();
62
+ return context.toResult();
63
+ }
64
+ function findNameNode(node) {
65
+ return (node.childForFieldName('name') ??
66
+ node.namedChildren.find((child) => child.type === 'constant') ??
67
+ node.namedChildren.find((child) => child.type === 'identifier'));
68
+ }
69
+ function findCallMethodName(node) {
70
+ return (node.childForFieldName('method')?.text ??
71
+ node.namedChildren.find((child) => child.type === 'identifier')?.text);
72
+ }
73
+ function findFirstStringContent(node) {
74
+ return node.descendantsOfType('string_content')[0]?.text;
75
+ }
@@ -0,0 +1,2 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractShellSymbols(sourceText: string, filePath: string): Promise<SymbolExtractionResult>;
@@ -0,0 +1,78 @@
1
+ import { emptyExtractionResult, ExtractionContext, } from './extraction-context.js';
2
+ import { parseSource } from './tree-sitter-parser.js';
3
+ export async function extractShellSymbols(sourceText, filePath) {
4
+ if (!sourceText.trim()) {
5
+ return emptyExtractionResult();
6
+ }
7
+ const tree = await parseSource(sourceText, 'bash');
8
+ const context = new ExtractionContext(filePath);
9
+ function walk(node, currentFunctionId) {
10
+ switch (node.type) {
11
+ case 'function_definition': {
12
+ const name = findFunctionName(node);
13
+ if (name) {
14
+ const symbol = context.addSymbol({
15
+ name,
16
+ kind: 'function',
17
+ startLine: node.startPosition.row + 1,
18
+ endLine: node.endPosition.row + 1,
19
+ exported: true,
20
+ });
21
+ for (const child of node.namedChildren) {
22
+ walk(child, symbol.id);
23
+ }
24
+ return;
25
+ }
26
+ break;
27
+ }
28
+ case 'command': {
29
+ const commandName = findCommandName(node);
30
+ const firstArgument = findFirstCommandArgument(node);
31
+ if ((commandName === 'source' || commandName === '.') &&
32
+ firstArgument) {
33
+ context.addDependency(firstArgument, 'local');
34
+ return;
35
+ }
36
+ if (currentFunctionId && commandName && isLocalScriptReference(commandName)) {
37
+ context.relationships.push({
38
+ sourceType: 'symbol',
39
+ sourceId: currentFunctionId,
40
+ targetType: 'file',
41
+ targetId: commandName,
42
+ relationshipType: 'calls',
43
+ confidence: 'low',
44
+ });
45
+ }
46
+ break;
47
+ }
48
+ }
49
+ for (const child of node.namedChildren) {
50
+ walk(child, currentFunctionId);
51
+ }
52
+ }
53
+ walk(tree.rootNode);
54
+ tree.delete();
55
+ return context.toResult();
56
+ }
57
+ function findFunctionName(node) {
58
+ return (node.childForFieldName('name')?.text ??
59
+ node.namedChildren.find((child) => child.type === 'word')?.text);
60
+ }
61
+ function findCommandName(node) {
62
+ return (node.childForFieldName('name')?.text ??
63
+ node.namedChildren
64
+ .find((child) => child.type === 'command_name')
65
+ ?.namedChildren.find((child) => child.type === 'word')?.text ??
66
+ node.namedChildren.find((child) => child.type === 'command_name')?.text ??
67
+ node.namedChildren.find((child) => child.type === 'word')?.text);
68
+ }
69
+ function findFirstCommandArgument(node) {
70
+ return node.namedChildren.find((child) => child.type === 'word')?.text;
71
+ }
72
+ function isLocalScriptReference(value) {
73
+ return (value.startsWith('./') ||
74
+ value.startsWith('../') ||
75
+ value.endsWith('.sh') ||
76
+ value.endsWith('.bash') ||
77
+ value.endsWith('.zsh'));
78
+ }
@@ -0,0 +1,2 @@
1
+ import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
+ export declare function extractSqlSymbols(sourceText: string, filePath: string): Promise<SymbolExtractionResult>;
@@ -0,0 +1,166 @@
1
+ import { emptyExtractionResult, ExtractionContext, } from './extraction-context.js';
2
+ export async function extractSqlSymbols(sourceText, filePath) {
3
+ if (!sourceText.trim()) {
4
+ return emptyExtractionResult();
5
+ }
6
+ const context = new ExtractionContext(filePath);
7
+ const withoutComments = stripLineComments(sourceText);
8
+ const statements = splitStatements(withoutComments);
9
+ for (const statement of statements) {
10
+ collectCreateStatement(statement, context);
11
+ collectAlterStatement(statement, context);
12
+ collectQueryReferences(statement, context);
13
+ }
14
+ return context.toResult();
15
+ }
16
+ function collectCreateStatement(statement, context) {
17
+ const normalized = compact(statement.text);
18
+ const createMatch = normalized.match(/^CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:UNIQUE|MATERIALIZED|TEMP|TEMPORARY)\s+)*(TABLE|VIEW|INDEX|FUNCTION|PROCEDURE|TRIGGER|TYPE)\s+(?:IF\s+NOT\s+EXISTS\s+)?("?[\w.]+"?)/i);
19
+ if (!createMatch?.[1] || !createMatch[2]) {
20
+ return;
21
+ }
22
+ const objectKind = createMatch[1].toLowerCase();
23
+ const name = normalizeSqlIdentifier(createMatch[2]);
24
+ const symbol = context.addSymbol({
25
+ name,
26
+ kind: objectKind === 'function' || objectKind === 'procedure'
27
+ ? 'function'
28
+ : 'type',
29
+ startLine: statement.startLine,
30
+ endLine: statement.endLine,
31
+ exported: true,
32
+ parentName: objectKind,
33
+ });
34
+ const tableForTrigger = normalized.match(/\bON\s+("?[\w.]+"?)/i)?.[1];
35
+ if (objectKind === 'trigger' && tableForTrigger) {
36
+ context.relationships.push({
37
+ sourceType: 'symbol',
38
+ sourceId: symbol.id,
39
+ targetType: 'symbol',
40
+ targetId: normalizeSqlIdentifier(tableForTrigger),
41
+ relationshipType: 'mentions',
42
+ confidence: 'high',
43
+ });
44
+ }
45
+ for (const referencedTable of referencedTables(normalized)) {
46
+ context.relationships.push({
47
+ sourceType: 'symbol',
48
+ sourceId: symbol.id,
49
+ targetType: 'symbol',
50
+ targetId: referencedTable,
51
+ relationshipType: 'mentions',
52
+ confidence: 'medium',
53
+ });
54
+ }
55
+ }
56
+ function collectAlterStatement(statement, context) {
57
+ const normalized = compact(statement.text);
58
+ const alterMatch = normalized.match(/^ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?("?[\w.]+"?)/i);
59
+ if (!alterMatch?.[1]) {
60
+ return;
61
+ }
62
+ const tableName = normalizeSqlIdentifier(alterMatch[1]);
63
+ const symbol = context.addSymbol({
64
+ name: `alter ${tableName}`,
65
+ kind: 'type',
66
+ startLine: statement.startLine,
67
+ endLine: statement.endLine,
68
+ exported: true,
69
+ parentName: 'table',
70
+ });
71
+ context.relationships.push({
72
+ sourceType: 'symbol',
73
+ sourceId: symbol.id,
74
+ targetType: 'symbol',
75
+ targetId: tableName,
76
+ relationshipType: 'mentions',
77
+ confidence: 'high',
78
+ });
79
+ for (const referencedTable of referencedTables(normalized)) {
80
+ context.relationships.push({
81
+ sourceType: 'symbol',
82
+ sourceId: symbol.id,
83
+ targetType: 'symbol',
84
+ targetId: referencedTable,
85
+ relationshipType: 'mentions',
86
+ confidence: 'medium',
87
+ });
88
+ }
89
+ }
90
+ function collectQueryReferences(statement, context) {
91
+ const normalized = compact(statement.text);
92
+ const queryMatch = normalized.match(/^(SELECT|INSERT|UPDATE|DELETE)\b/i);
93
+ if (!queryMatch?.[1]) {
94
+ return;
95
+ }
96
+ const symbol = context.addSymbol({
97
+ name: `${queryMatch[1].toLowerCase()} statement ${statement.startLine}`,
98
+ kind: 'type',
99
+ startLine: statement.startLine,
100
+ endLine: statement.endLine,
101
+ });
102
+ for (const table of referencedTables(normalized)) {
103
+ context.relationships.push({
104
+ sourceType: 'symbol',
105
+ sourceId: symbol.id,
106
+ targetType: 'symbol',
107
+ targetId: table,
108
+ relationshipType: 'mentions',
109
+ confidence: 'medium',
110
+ });
111
+ }
112
+ }
113
+ function stripLineComments(sourceText) {
114
+ return sourceText
115
+ .split(/\r?\n/)
116
+ .map((line) => line.replace(/--.*$/, ''))
117
+ .join('\n');
118
+ }
119
+ function splitStatements(sourceText) {
120
+ const statements = [];
121
+ let current = '';
122
+ let startLine = 1;
123
+ let lineNumber = 1;
124
+ for (const line of sourceText.split(/\r?\n/)) {
125
+ if (!current.trim()) {
126
+ startLine = lineNumber;
127
+ }
128
+ current += `${line}\n`;
129
+ if (line.includes(';')) {
130
+ statements.push({
131
+ text: current,
132
+ startLine,
133
+ endLine: lineNumber,
134
+ });
135
+ current = '';
136
+ }
137
+ lineNumber += 1;
138
+ }
139
+ if (current.trim()) {
140
+ statements.push({ text: current, startLine, endLine: lineNumber - 1 });
141
+ }
142
+ return statements;
143
+ }
144
+ function compact(value) {
145
+ return value.replace(/\s+/g, ' ').trim();
146
+ }
147
+ function normalizeSqlIdentifier(value) {
148
+ return value.replace(/^"+|"+$/g, '').replace(/[;,)]$/, '');
149
+ }
150
+ function referencedTables(statement) {
151
+ const names = new Set();
152
+ const patterns = [
153
+ /\bREFERENCES\s+("?[\w.]+"?)/gi,
154
+ /\bFROM\s+("?[\w.]+"?)/gi,
155
+ /\bJOIN\s+("?[\w.]+"?)/gi,
156
+ /\bUPDATE\s+("?[\w.]+"?)/gi,
157
+ /\bINTO\s+("?[\w.]+"?)/gi,
158
+ ];
159
+ for (const pattern of patterns) {
160
+ for (const match of statement.matchAll(pattern)) {
161
+ if (match[1])
162
+ names.add(normalizeSqlIdentifier(match[1]));
163
+ }
164
+ }
165
+ return [...names];
166
+ }
@@ -1,5 +1,4 @@
1
1
  import { Language, Tree } from 'web-tree-sitter';
2
- type GrammarKey = 'python' | 'java' | 'kotlin' | 'go' | 'rust' | 'c' | 'cpp' | 'c_sharp';
2
+ export type GrammarKey = 'python' | 'java' | 'kotlin' | 'go' | 'rust' | 'c' | 'cpp' | 'c_sharp' | 'php' | 'ruby' | 'bash' | 'yaml' | 'json' | 'html' | 'css' | 'lua' | 'dart' | 'elixir' | 'scala';
3
3
  export declare function loadLanguage(grammarKey: GrammarKey): Promise<Language>;
4
4
  export declare function parseSource(sourceText: string, grammarKey: GrammarKey): Promise<Tree>;
5
- export {};
@@ -19,6 +19,20 @@ const GRAMMAR_PACKAGES = {
19
19
  pkg: 'tree-sitter-c-sharp',
20
20
  wasm: 'tree-sitter-c_sharp.wasm',
21
21
  },
22
+ php: { pkg: 'tree-sitter-php', wasm: 'tree-sitter-php.wasm' },
23
+ ruby: { pkg: 'tree-sitter-ruby', wasm: 'tree-sitter-ruby.wasm' },
24
+ bash: { pkg: 'tree-sitter-bash', wasm: 'tree-sitter-bash.wasm' },
25
+ yaml: {
26
+ pkg: '@tree-sitter-grammars/tree-sitter-yaml',
27
+ wasm: 'tree-sitter-yaml.wasm',
28
+ },
29
+ json: { pkg: 'tree-sitter-json', wasm: 'tree-sitter-json.wasm' },
30
+ html: { pkg: 'tree-sitter-html', wasm: 'tree-sitter-html.wasm' },
31
+ css: { pkg: 'tree-sitter-css', wasm: 'tree-sitter-css.wasm' },
32
+ lua: { pkg: 'tree-sitter-lua', wasm: 'tree-sitter-lua.wasm' },
33
+ dart: { pkg: 'tree-sitter-dart', wasm: 'tree-sitter-dart.wasm' },
34
+ elixir: { pkg: 'tree-sitter-elixir', wasm: 'tree-sitter-elixir.wasm' },
35
+ scala: { pkg: 'tree-sitter-scala', wasm: 'tree-sitter-scala.wasm' },
22
36
  };
23
37
  async function ensureInit() {
24
38
  if (!initPromise) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,16 +45,27 @@
45
45
  "dependencies": {
46
46
  "@clack/prompts": "^1.3.0",
47
47
  "@tree-sitter-grammars/tree-sitter-kotlin": "^1.1.0",
48
+ "@tree-sitter-grammars/tree-sitter-yaml": "^0.7.1",
48
49
  "chalk": "^5.6.2",
49
50
  "commander": "^12.1.0",
50
51
  "fast-glob": "^3.3.2",
52
+ "tree-sitter-bash": "^0.25.1",
51
53
  "tree-sitter-c": "^0.24.1",
52
54
  "tree-sitter-c-sharp": "^0.23.5",
53
55
  "tree-sitter-cpp": "^0.23.4",
56
+ "tree-sitter-css": "^0.25.0",
57
+ "tree-sitter-dart": "^1.0.0",
58
+ "tree-sitter-elixir": "^0.3.5",
54
59
  "tree-sitter-go": "^0.25.0",
60
+ "tree-sitter-html": "^0.23.2",
55
61
  "tree-sitter-java": "^0.23.5",
62
+ "tree-sitter-json": "^0.24.8",
63
+ "tree-sitter-lua": "^2.1.3",
64
+ "tree-sitter-php": "^0.24.2",
56
65
  "tree-sitter-python": "^0.25.0",
66
+ "tree-sitter-ruby": "^0.23.1",
57
67
  "tree-sitter-rust": "^0.24.0",
68
+ "tree-sitter-scala": "^0.24.0",
58
69
  "typescript": "^5.9.3",
59
70
  "web-tree-sitter": "^0.26.8",
60
71
  "yaml": "^2.5.1"