@kentwynn/kgraph 0.1.25 → 0.1.27

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,13 +1,15 @@
1
- export function extractPythonSymbols(sourceText, filePath) {
2
- const lines = sourceText.split('\n');
1
+ import { parseSource } from './tree-sitter-parser.js';
2
+ export async function extractPythonSymbols(sourceText, filePath) {
3
3
  const symbols = [];
4
4
  const dependencies = [];
5
5
  const relationships = [];
6
6
  const warnings = [];
7
- // Stack tracks nested class scope: [{name, indent}]
8
- const classStack = [];
9
- const addSymbol = (name, kind, lineNum, parentName) => {
10
- const id = [filePath, kind, parentName, name, lineNum]
7
+ if (!sourceText.trim()) {
8
+ return { symbols, dependencies, relationships, warnings };
9
+ }
10
+ const tree = await parseSource(sourceText, 'python');
11
+ const addSymbol = (name, kind, startLine, endLine, exported, parentName) => {
12
+ const id = [filePath, kind, parentName, name, startLine]
11
13
  .filter(Boolean)
12
14
  .join('#');
13
15
  symbols.push({
@@ -15,9 +17,9 @@ export function extractPythonSymbols(sourceText, filePath) {
15
17
  name,
16
18
  kind,
17
19
  filePath,
18
- startLine: lineNum,
19
- endLine: lineNum,
20
- exported: false,
20
+ startLine,
21
+ endLine,
22
+ exported,
21
23
  parentName,
22
24
  });
23
25
  relationships.push({
@@ -29,70 +31,89 @@ export function extractPythonSymbols(sourceText, filePath) {
29
31
  confidence: 'high',
30
32
  });
31
33
  };
32
- for (let i = 0; i < lines.length; i++) {
33
- const line = lines[i];
34
- const lineNum = i + 1;
35
- const trimmed = line.trimStart();
36
- if (!trimmed || trimmed.startsWith('#'))
37
- continue;
38
- const indent = line.length - trimmed.length;
39
- // Pop classes we've exited: any class whose indent >= current line's indent
40
- // (unless this line IS the class that opened at that indent — handled by re-pushing below)
41
- while (classStack.length > 0 &&
42
- classStack[classStack.length - 1].indent >= indent) {
43
- classStack.pop();
44
- }
45
- // class definition
46
- const classMatch = trimmed.match(/^class\s+([A-Za-z_]\w*)/);
47
- if (classMatch) {
48
- const name = classMatch[1];
49
- const parent = classStack[classStack.length - 1];
50
- addSymbol(name, 'class', lineNum, parent?.name);
51
- classStack.push({ name, indent });
52
- continue;
53
- }
54
- // function / method definition (sync or async)
55
- const funcMatch = trimmed.match(/^(?:async\s+)?def\s+([A-Za-z_]\w*)/);
56
- if (funcMatch) {
57
- const name = funcMatch[1];
58
- const parent = classStack[classStack.length - 1];
59
- const kind = parent !== undefined ? 'method' : 'function';
60
- addSymbol(name, kind, lineNum, parent?.name);
61
- continue;
62
- }
63
- // import module
64
- const importMatch = trimmed.match(/^import\s+([\w.]+)/);
65
- if (importMatch) {
66
- const specifier = importMatch[1];
67
- dependencies.push({ fromFile: filePath, specifier, kind: 'package' });
68
- addSymbol(specifier, 'import', lineNum);
69
- relationships.push({
70
- sourceType: 'file',
71
- sourceId: filePath,
72
- targetType: 'package',
73
- targetId: specifier,
74
- relationshipType: 'import',
75
- confidence: 'high',
76
- });
77
- continue;
34
+ function walk(node, parentClassName) {
35
+ switch (node.type) {
36
+ case 'class_definition': {
37
+ const nameNode = node.childForFieldName('name');
38
+ if (nameNode) {
39
+ const startLine = node.startPosition.row + 1;
40
+ const endLine = node.endPosition.row + 1;
41
+ addSymbol(nameNode.text, 'class', startLine, endLine, false, parentClassName);
42
+ // Walk children for nested classes/methods
43
+ const body = node.childForFieldName('body');
44
+ if (body) {
45
+ for (const child of body.namedChildren) {
46
+ walk(child, nameNode.text);
47
+ }
48
+ }
49
+ }
50
+ return; // Don't recurse further from here
51
+ }
52
+ case 'function_definition': {
53
+ const nameNode = node.childForFieldName('name');
54
+ if (nameNode) {
55
+ const startLine = node.startPosition.row + 1;
56
+ const endLine = node.endPosition.row + 1;
57
+ const kind = parentClassName
58
+ ? 'method'
59
+ : 'function';
60
+ addSymbol(nameNode.text, kind, startLine, endLine, false, parentClassName);
61
+ }
62
+ return; // Don't recurse into function bodies
63
+ }
64
+ case 'import_statement': {
65
+ // import os / import os.path
66
+ const startLine = node.startPosition.row + 1;
67
+ for (const child of node.namedChildren) {
68
+ if (child.type === 'dotted_name' || child.type === 'aliased_import') {
69
+ const specifier = child.type === 'aliased_import'
70
+ ? (child.childForFieldName('name')?.text ?? child.text)
71
+ : child.text;
72
+ dependencies.push({
73
+ fromFile: filePath,
74
+ specifier,
75
+ kind: 'package',
76
+ });
77
+ addSymbol(specifier, 'import', startLine, startLine, false);
78
+ relationships.push({
79
+ sourceType: 'file',
80
+ sourceId: filePath,
81
+ targetType: 'package',
82
+ targetId: specifier,
83
+ relationshipType: 'import',
84
+ confidence: 'high',
85
+ });
86
+ }
87
+ }
88
+ return;
89
+ }
90
+ case 'import_from_statement': {
91
+ // from X import Y
92
+ const moduleNode = node.childForFieldName('module_name');
93
+ if (moduleNode) {
94
+ const specifier = moduleNode.text;
95
+ const kind = specifier.startsWith('.')
96
+ ? 'local'
97
+ : 'package';
98
+ dependencies.push({ fromFile: filePath, specifier, kind });
99
+ relationships.push({
100
+ sourceType: 'file',
101
+ sourceId: filePath,
102
+ targetType: kind === 'local' ? 'file' : 'package',
103
+ targetId: specifier,
104
+ relationshipType: 'import',
105
+ confidence: kind === 'local' ? 'medium' : 'high',
106
+ });
107
+ }
108
+ return;
109
+ }
78
110
  }
79
- // from X import Y
80
- const fromMatch = trimmed.match(/^from\s+(\S+)\s+import/);
81
- if (fromMatch) {
82
- const specifier = fromMatch[1];
83
- const kind = specifier.startsWith('.')
84
- ? 'local'
85
- : 'package';
86
- dependencies.push({ fromFile: filePath, specifier, kind });
87
- relationships.push({
88
- sourceType: 'file',
89
- sourceId: filePath,
90
- targetType: kind === 'local' ? 'file' : 'package',
91
- targetId: specifier,
92
- relationshipType: 'import',
93
- confidence: kind === 'local' ? 'medium' : 'high',
94
- });
111
+ // Recurse into children for top-level statements
112
+ for (const child of node.namedChildren) {
113
+ walk(child, parentClassName);
95
114
  }
96
115
  }
116
+ walk(tree.rootNode);
117
+ tree.delete();
97
118
  return { symbols, dependencies, relationships, warnings };
98
119
  }
@@ -2,6 +2,7 @@ import fg from 'fast-glob';
2
2
  import crypto from 'node:crypto';
3
3
  import { readFile, stat } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import { estimateTokens } from '../session/token-estimator.js';
5
6
  import { extractCSymbols } from './c-symbol-extractor.js';
6
7
  import { extractCSharpSymbols } from './csharp-symbol-extractor.js';
7
8
  import { buildFastGlobIgnore, detectLanguage, isPreciseLanguage, readGitignorePatterns, shouldExclude, } from './file-classifier.js';
@@ -10,10 +11,9 @@ import { extractJvmSymbols } from './jvm-symbol-extractor.js';
10
11
  import { extractPythonSymbols } from './python-symbol-extractor.js';
11
12
  import { extractRustSymbols } from './rust-symbol-extractor.js';
12
13
  import { extractTsSymbols } from './ts-symbol-extractor.js';
13
- import { estimateTokens } from '../session/token-estimator.js';
14
14
  const C_EXTS = new Set(['.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx']);
15
15
  const JVM_EXTS = new Set(['.java', '.kt', '.kts']);
16
- function extractSymbols(text, repoPath) {
16
+ async function extractSymbols(text, repoPath) {
17
17
  const ext = path.extname(repoPath);
18
18
  if (ext === '.py' || ext === '.pyw' || ext === '.pyi') {
19
19
  return extractPythonSymbols(text, repoPath);
@@ -79,7 +79,7 @@ export async function scanRepository(rootPath, config, previous) {
79
79
  warnings: [],
80
80
  };
81
81
  if (isPreciseLanguage(repoPath, config)) {
82
- const extracted = extractSymbols(text, repoPath);
82
+ const extracted = await extractSymbols(text, repoPath);
83
83
  symbols.push(...extracted.symbols);
84
84
  dependencies.push(...extracted.dependencies);
85
85
  relationships.push(...extracted.relationships.filter((relationship) => relationship.relationshipType !== 'import'));
@@ -1,2 +1,2 @@
1
1
  import type { SymbolExtractionResult } from './ts-symbol-extractor.js';
2
- export declare function extractRustSymbols(sourceText: string, filePath: string): SymbolExtractionResult;
2
+ export declare function extractRustSymbols(sourceText: string, filePath: string): Promise<SymbolExtractionResult>;
@@ -1,38 +1,15 @@
1
- export function extractRustSymbols(sourceText, filePath) {
2
- const lines = sourceText.split('\n');
1
+ import { parseSource } from './tree-sitter-parser.js';
2
+ export async function extractRustSymbols(sourceText, filePath) {
3
3
  const symbols = [];
4
4
  const dependencies = [];
5
5
  const relationships = [];
6
6
  const warnings = [];
7
- // Track current impl block: { typeName, indent }
8
- const implStack = [];
9
- let braceDepth = 0;
10
- const addSymbol = (name, kind, lineNum, parentName) => {
11
- const id = [filePath, kind, parentName, name, lineNum]
12
- .filter(Boolean)
13
- .join('#');
14
- // Rust: pub = exported
15
- symbols.push({
16
- id,
17
- name,
18
- kind,
19
- filePath,
20
- startLine: lineNum,
21
- endLine: lineNum,
22
- exported: false, // set by caller
23
- parentName,
24
- });
25
- relationships.push({
26
- sourceType: 'file',
27
- sourceId: filePath,
28
- targetType: 'symbol',
29
- targetId: id,
30
- relationshipType: 'contains',
31
- confidence: 'high',
32
- });
33
- };
34
- const addSymbolExported = (name, kind, lineNum, exported, parentName) => {
35
- const id = [filePath, kind, parentName, name, lineNum]
7
+ if (!sourceText.trim()) {
8
+ return { symbols, dependencies, relationships, warnings };
9
+ }
10
+ const tree = await parseSource(sourceText, 'rust');
11
+ const addSymbol = (name, kind, startLine, endLine, exported, parentName) => {
12
+ const id = [filePath, kind, parentName, name, startLine]
36
13
  .filter(Boolean)
37
14
  .join('#');
38
15
  symbols.push({
@@ -40,8 +17,8 @@ export function extractRustSymbols(sourceText, filePath) {
40
17
  name,
41
18
  kind,
42
19
  filePath,
43
- startLine: lineNum,
44
- endLine: lineNum,
20
+ startLine,
21
+ endLine,
45
22
  exported,
46
23
  parentName,
47
24
  });
@@ -54,66 +31,94 @@ export function extractRustSymbols(sourceText, filePath) {
54
31
  confidence: 'high',
55
32
  });
56
33
  };
57
- for (let i = 0; i < lines.length; i++) {
58
- const line = lines[i];
59
- const lineNum = i + 1;
60
- const trimmed = line.trim();
61
- if (!trimmed || trimmed.startsWith('//'))
62
- continue;
63
- // Track brace depth for impl block scoping
64
- braceDepth +=
65
- (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
66
- // Pop impl blocks we've exited
67
- while (implStack.length > 0 &&
68
- braceDepth < implStack[implStack.length - 1].braceDepth) {
69
- implStack.pop();
70
- }
71
- // use statement: use crate::path or use external::path
72
- const useMatch = trimmed.match(/^use\s+([\w:]+)/);
73
- if (useMatch) {
74
- const specifier = useMatch[1];
75
- const kind = specifier.startsWith('crate::') ||
76
- specifier.startsWith('super::') ||
77
- specifier.startsWith('self::')
78
- ? 'local'
79
- : 'package';
80
- dependencies.push({ fromFile: filePath, specifier, kind });
81
- relationships.push({
82
- sourceType: 'file',
83
- sourceId: filePath,
84
- targetType: kind === 'local' ? 'file' : 'package',
85
- targetId: specifier,
86
- relationshipType: 'import',
87
- confidence: 'high',
88
- });
89
- continue;
90
- }
91
- // impl block: impl TypeName or impl Trait for TypeName
92
- const implMatch = trimmed.match(/^impl(?:<[^>]*>)?\s+(?:\w+\s+for\s+)?(\w+)/);
93
- if (implMatch) {
94
- implStack.push({ typeName: implMatch[1], braceDepth });
95
- continue;
96
- }
97
- // struct / enum / trait definition
98
- const typeMatch = trimmed.match(/^(pub\s+)?(?:struct|enum|trait)\s+(\w+)/);
99
- if (typeMatch) {
100
- addSymbolExported(typeMatch[2], 'class', lineNum, !!typeMatch[1]);
101
- continue;
34
+ function hasPub(node) {
35
+ return node.namedChildren.some((c) => c.type === 'visibility_modifier');
36
+ }
37
+ function extractUseSpecifier(node) {
38
+ // use_declaration has a child tree of scoped_identifier / scoped_use_list / use_wildcard
39
+ // We want the text without 'use' and ';'
40
+ for (const child of node.namedChildren) {
41
+ if (child.type !== 'visibility_modifier') {
42
+ return child.text;
43
+ }
102
44
  }
103
- // fn definition (inside or outside impl)
104
- const fnMatch = trimmed.match(/^(pub\s+)?(?:async\s+)?fn\s+(\w+)/);
105
- if (fnMatch) {
106
- const exported = !!fnMatch[1];
107
- const name = fnMatch[2];
108
- const parent = implStack[implStack.length - 1];
109
- if (parent) {
110
- addSymbolExported(name, 'method', lineNum, exported, parent.typeName);
45
+ return '';
46
+ }
47
+ function walk(node, implTypeName) {
48
+ switch (node.type) {
49
+ case 'use_declaration': {
50
+ const specifier = extractUseSpecifier(node);
51
+ if (specifier) {
52
+ const kind = specifier.startsWith('crate::') ||
53
+ specifier.startsWith('super::') ||
54
+ specifier.startsWith('self::')
55
+ ? 'local'
56
+ : 'package';
57
+ dependencies.push({
58
+ fromFile: filePath,
59
+ specifier,
60
+ kind,
61
+ });
62
+ relationships.push({
63
+ sourceType: 'file',
64
+ sourceId: filePath,
65
+ targetType: kind === 'local' ? 'file' : 'package',
66
+ targetId: specifier,
67
+ relationshipType: 'import',
68
+ confidence: 'high',
69
+ });
70
+ }
71
+ return;
72
+ }
73
+ case 'struct_item':
74
+ case 'enum_item':
75
+ case 'trait_item': {
76
+ const nameNode = node.childForFieldName('name');
77
+ if (nameNode) {
78
+ addSymbol(nameNode.text, 'class', node.startPosition.row + 1, node.endPosition.row + 1, hasPub(node));
79
+ }
80
+ return;
111
81
  }
112
- else {
113
- addSymbolExported(name, 'function', lineNum, exported);
82
+ case 'impl_item': {
83
+ // impl TypeName { ... } or impl Trait for TypeName { ... }
84
+ const typeNode = node.childForFieldName('type');
85
+ const typeName = typeNode?.type === 'type_identifier' ? typeNode.text : typeNode?.text;
86
+ const body = node.childForFieldName('body') ??
87
+ node.namedChildren.find((c) => c.type === 'declaration_list');
88
+ if (body && typeName) {
89
+ for (const child of body.namedChildren) {
90
+ walk(child, typeName);
91
+ }
92
+ }
93
+ return;
114
94
  }
115
- continue;
95
+ case 'function_item': {
96
+ const nameNode = node.childForFieldName('name');
97
+ if (nameNode) {
98
+ const exported = hasPub(node);
99
+ if (implTypeName) {
100
+ addSymbol(nameNode.text, 'method', node.startPosition.row + 1, node.endPosition.row + 1, exported, implTypeName);
101
+ }
102
+ else {
103
+ addSymbol(nameNode.text, 'function', node.startPosition.row + 1, node.endPosition.row + 1, exported);
104
+ }
105
+ }
106
+ return;
107
+ }
108
+ case 'function_signature_item': {
109
+ // trait method signatures
110
+ const nameNode = node.childForFieldName('name');
111
+ if (nameNode && implTypeName) {
112
+ addSymbol(nameNode.text, 'method', node.startPosition.row + 1, node.endPosition.row + 1, false, implTypeName);
113
+ }
114
+ return;
115
+ }
116
+ }
117
+ for (const child of node.namedChildren) {
118
+ walk(child, implTypeName);
116
119
  }
117
120
  }
121
+ walk(tree.rootNode);
122
+ tree.delete();
118
123
  return { symbols, dependencies, relationships, warnings };
119
124
  }
@@ -0,0 +1,5 @@
1
+ import { Language, Tree } from 'web-tree-sitter';
2
+ type GrammarKey = 'python' | 'java' | 'kotlin' | 'go' | 'rust' | 'c' | 'cpp' | 'c_sharp';
3
+ export declare function loadLanguage(grammarKey: GrammarKey): Promise<Language>;
4
+ export declare function parseSource(sourceText: string, grammarKey: GrammarKey): Promise<Tree>;
5
+ export {};
@@ -0,0 +1,55 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { Language, Parser } from 'web-tree-sitter';
4
+ const require = createRequire(import.meta.url);
5
+ let initPromise = null;
6
+ const languageCache = new Map();
7
+ const GRAMMAR_PACKAGES = {
8
+ python: { pkg: 'tree-sitter-python', wasm: 'tree-sitter-python.wasm' },
9
+ java: { pkg: 'tree-sitter-java', wasm: 'tree-sitter-java.wasm' },
10
+ kotlin: {
11
+ pkg: '@tree-sitter-grammars/tree-sitter-kotlin',
12
+ wasm: 'tree-sitter-kotlin.wasm',
13
+ },
14
+ go: { pkg: 'tree-sitter-go', wasm: 'tree-sitter-go.wasm' },
15
+ rust: { pkg: 'tree-sitter-rust', wasm: 'tree-sitter-rust.wasm' },
16
+ c: { pkg: 'tree-sitter-c', wasm: 'tree-sitter-c.wasm' },
17
+ cpp: { pkg: 'tree-sitter-cpp', wasm: 'tree-sitter-cpp.wasm' },
18
+ c_sharp: {
19
+ pkg: 'tree-sitter-c-sharp',
20
+ wasm: 'tree-sitter-c_sharp.wasm',
21
+ },
22
+ };
23
+ async function ensureInit() {
24
+ if (!initPromise) {
25
+ initPromise = Parser.init();
26
+ }
27
+ return initPromise;
28
+ }
29
+ function resolveWasmPath(grammarKey) {
30
+ const { pkg, wasm } = GRAMMAR_PACKAGES[grammarKey];
31
+ const pkgDir = path.dirname(require.resolve(`${pkg}/package.json`));
32
+ return path.join(pkgDir, wasm);
33
+ }
34
+ export async function loadLanguage(grammarKey) {
35
+ const cached = languageCache.get(grammarKey);
36
+ if (cached)
37
+ return cached;
38
+ await ensureInit();
39
+ const wasmPath = resolveWasmPath(grammarKey);
40
+ const language = await Language.load(wasmPath);
41
+ languageCache.set(grammarKey, language);
42
+ return language;
43
+ }
44
+ export async function parseSource(sourceText, grammarKey) {
45
+ await ensureInit();
46
+ const language = await loadLanguage(grammarKey);
47
+ const parser = new Parser();
48
+ parser.setLanguage(language);
49
+ const tree = parser.parse(sourceText);
50
+ parser.delete();
51
+ if (!tree) {
52
+ throw new Error(`Failed to parse source with grammar ${grammarKey}`);
53
+ }
54
+ return tree;
55
+ }
@@ -12,8 +12,8 @@ export interface DomainHint {
12
12
  paths?: string[];
13
13
  tags?: string[];
14
14
  }
15
- export type IntegrationName = "claude-code" | "cline" | "codex" | "copilot" | "cursor" | "gemini" | "windsurf";
16
- export type IntegrationMode = "smart" | "always" | "manual" | "off";
15
+ export type IntegrationName = 'claude-code' | 'cline' | 'codex' | 'copilot' | 'cursor' | 'gemini' | 'windsurf';
16
+ export type IntegrationMode = 'smart' | 'always' | 'manual' | 'off';
17
17
  export interface IntegrationConfig {
18
18
  name: IntegrationName;
19
19
  enabled: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,11 +39,21 @@
39
39
  },
40
40
  "homepage": "https://github.com/kentwynn/KGraph#readme",
41
41
  "dependencies": {
42
+ "@clack/prompts": "^1.3.0",
43
+ "@tree-sitter-grammars/tree-sitter-kotlin": "^1.1.0",
42
44
  "chalk": "^5.6.2",
43
45
  "commander": "^12.1.0",
44
46
  "fast-glob": "^3.3.2",
45
47
  "figlet": "^1.11.0",
48
+ "tree-sitter-c": "^0.24.1",
49
+ "tree-sitter-c-sharp": "^0.23.5",
50
+ "tree-sitter-cpp": "^0.23.4",
51
+ "tree-sitter-go": "^0.25.0",
52
+ "tree-sitter-java": "^0.23.5",
53
+ "tree-sitter-python": "^0.25.0",
54
+ "tree-sitter-rust": "^0.24.0",
46
55
  "typescript": "^5.9.3",
56
+ "web-tree-sitter": "^0.26.8",
47
57
  "yaml": "^2.5.1"
48
58
  },
49
59
  "devDependencies": {