code-graph-llm 1.1.0 → 1.3.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.
- package/README.md +2 -0
- package/index.js +72 -81
- package/llm-code-graph.md +4 -2
- package/package.json +5 -2
- package/test/index.test.js +75 -0
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@ A language-agnostic, ultra-compact codebase mapper designed specifically for LLM
|
|
|
5
5
|
## Features
|
|
6
6
|
- **Smart Context Extraction:** Captures JSDoc, Python docstrings, and preceding comments for files and symbols.
|
|
7
7
|
- **Signature Fallback:** Automatically extracts function signatures (parameters/types) if documentation is missing.
|
|
8
|
+
- **Recursive .gitignore Support:** Deeply respects both root and nested `.gitignore` files across the entire project structure.
|
|
9
|
+
- **Smart Flutter/Dart Support:** Optimized to reduce noise by filtering out common widget instantiations while capturing real functional declarations.
|
|
8
10
|
- **Compact & Dense:** Optimized for LLM token efficiency, replacing expensive recursive file scans.
|
|
9
11
|
- **Language-Agnostic:** Optimized regex support for JS/TS, Python, Go, Rust, Java, C#, C/C++, Swift, PHP, Ruby, Dart, and more.
|
|
10
12
|
- **Recursive Ignore Logic:** Deeply respects `.gitignore` and standard excludes (`node_modules`, `.git`).
|
package/index.js
CHANGED
|
@@ -13,46 +13,42 @@ const IGNORE_FILE = '.gitignore';
|
|
|
13
13
|
const DEFAULT_MAP_FILE = 'llm-code-graph.md';
|
|
14
14
|
|
|
15
15
|
const SYMBOL_REGEXES = [
|
|
16
|
-
// Types, Classes, Interfaces
|
|
16
|
+
// Types, Classes, Interfaces (Universal)
|
|
17
17
|
/\b(?:class|interface|type|struct|enum|protocol|extension|trait|module|namespace|object)\s+([a-zA-Z_]\w*)/g,
|
|
18
18
|
|
|
19
|
-
// Explicit Function Keywords
|
|
19
|
+
// Explicit Function Keywords
|
|
20
20
|
/\b(?:function|def|fn|func|fun|method|procedure|sub|routine)\s+([a-zA-Z_]\w*)/g,
|
|
21
21
|
|
|
22
|
-
// C-style
|
|
23
|
-
//
|
|
24
|
-
/\b(?:void|async|public|private|protected|static|virtual|override|readonly|int|float|double|char|bool|string|val|var|let|
|
|
22
|
+
// Method/Var Declarations (C-style, Java, C#, TS, Dart)
|
|
23
|
+
// Refined to require a variable/function name followed by a declaration signal
|
|
24
|
+
/\b(?:void|async|public|private|protected|static|virtual|override|readonly|int|float|double|char|bool|string|val|var|let|final)\s+([a-zA-Z_]\w*)(?=\s*(?:\([^)]*\)|[a-zA-Z_]\w*)\s*(?:\{|=>|;|=))/g,
|
|
25
25
|
|
|
26
|
-
// Exported symbols
|
|
27
|
-
/\bexport\s+(?:default\s+)?(?:const|let|var|function|class|type|interface|enum|async|val)\s+([a-zA-Z_]\w*)/g
|
|
28
|
-
|
|
29
|
-
// Ruby: def name, class Name, module Name (defs covered by Explicit Function Keywords)
|
|
30
|
-
|
|
31
|
-
// PHP: class Name, interface Name, trait Name, function Name
|
|
32
|
-
|
|
33
|
-
// Swift: func name, class Name, struct Name, protocol Name, extension Name
|
|
34
|
-
|
|
35
|
-
// Dart: class Name, void name, var name (void/var covered by C-style pattern)
|
|
26
|
+
// Exported symbols
|
|
27
|
+
/\bexport\s+(?:default\s+)?(?:const|let|var|function|class|type|interface|enum|async|val)\s+([a-zA-Z_]\w*)/g
|
|
36
28
|
];
|
|
37
29
|
|
|
38
|
-
const SUPPORTED_EXTENSIONS = [
|
|
39
|
-
'.js', '.ts', '.jsx', '.tsx',
|
|
40
|
-
'.
|
|
41
|
-
'.
|
|
42
|
-
'.rb', '.php', '.swift', '.kt',
|
|
43
|
-
'.cs', '.dart', '.scala', '.m', '.mm'
|
|
30
|
+
export const SUPPORTED_EXTENSIONS = [
|
|
31
|
+
'.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java',
|
|
32
|
+
'.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.php', '.swift',
|
|
33
|
+
'.kt', '.cs', '.dart', '.scala', '.m', '.mm'
|
|
44
34
|
];
|
|
45
35
|
|
|
46
|
-
function getIgnores(cwd) {
|
|
47
|
-
const ig = ignore().add([
|
|
36
|
+
export function getIgnores(cwd, additionalLines = []) {
|
|
37
|
+
const ig = ignore().add([
|
|
38
|
+
'.git/', 'node_modules/', DEFAULT_MAP_FILE, 'package-lock.json',
|
|
39
|
+
'.idea/', 'build/', 'dist/', 'bin/', 'obj/', '.dart_tool/', '.pub-cache/', '.pub/'
|
|
40
|
+
]);
|
|
48
41
|
const ignorePath = path.join(cwd, IGNORE_FILE);
|
|
49
42
|
if (fs.existsSync(ignorePath)) {
|
|
50
43
|
ig.add(fs.readFileSync(ignorePath, 'utf8'));
|
|
51
44
|
}
|
|
45
|
+
if (additionalLines.length > 0) {
|
|
46
|
+
ig.add(additionalLines);
|
|
47
|
+
}
|
|
52
48
|
return ig;
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
function extractSymbols(content) {
|
|
51
|
+
export function extractSymbols(content) {
|
|
56
52
|
const symbols = [];
|
|
57
53
|
for (const regex of SYMBOL_REGEXES) {
|
|
58
54
|
let match;
|
|
@@ -60,9 +56,8 @@ function extractSymbols(content) {
|
|
|
60
56
|
while ((match = regex.exec(content)) !== null) {
|
|
61
57
|
if (match[1]) {
|
|
62
58
|
const symbolName = match[1];
|
|
63
|
-
if (['if', 'for', 'while', 'switch', 'return', 'await', 'yield'].includes(symbolName)) continue;
|
|
59
|
+
if (['if', 'for', 'while', 'switch', 'return', 'await', 'yield', 'const', 'new'].includes(symbolName)) continue;
|
|
64
60
|
|
|
65
|
-
// 1. Extract preceding comment/docstring
|
|
66
61
|
const linesBefore = content.substring(0, match.index).split('\n');
|
|
67
62
|
let comment = '';
|
|
68
63
|
for (let i = linesBefore.length - 1; i >= 0; i--) {
|
|
@@ -71,20 +66,16 @@ function extractSymbols(content) {
|
|
|
71
66
|
const clean = line.replace(/[\/*#"]/g, '').trim();
|
|
72
67
|
if (clean) comment = clean + (comment ? ' ' + comment : '');
|
|
73
68
|
if (comment.length > 80) break;
|
|
74
|
-
} else if (line === '' && comment === '')
|
|
75
|
-
|
|
76
|
-
} else {
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
69
|
+
} else if (line === '' && comment === '') continue;
|
|
70
|
+
else break;
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
// 2. Backup: Extract Signature (Parameters/Type) if no comment
|
|
82
73
|
let context = comment;
|
|
83
74
|
if (!context) {
|
|
84
|
-
const remainingLine = content.substring(match.index + match[0].length)
|
|
85
|
-
const sigMatch = remainingLine.match(
|
|
86
|
-
if (sigMatch && sigMatch[
|
|
87
|
-
context = sigMatch[
|
|
75
|
+
const remainingLine = content.substring(match.index + match[0].length);
|
|
76
|
+
const sigMatch = remainingLine.match(/^\s*(\([^)]*\)|[^\n{;]*)/);
|
|
77
|
+
if (sigMatch && sigMatch[1].trim()) {
|
|
78
|
+
context = sigMatch[1].trim();
|
|
88
79
|
}
|
|
89
80
|
}
|
|
90
81
|
|
|
@@ -96,27 +87,40 @@ function extractSymbols(content) {
|
|
|
96
87
|
}
|
|
97
88
|
|
|
98
89
|
async function generate(cwd = process.cwd()) {
|
|
99
|
-
const ig = getIgnores(cwd);
|
|
100
90
|
const files = [];
|
|
101
91
|
|
|
102
|
-
function walk(dir) {
|
|
92
|
+
function walk(dir, ig) {
|
|
93
|
+
// Check for local .gitignore and create a new scoped ignore object if found
|
|
94
|
+
let localIg = ig;
|
|
95
|
+
const localIgnorePath = path.join(dir, IGNORE_FILE);
|
|
96
|
+
if (fs.existsSync(localIgnorePath) && dir !== cwd) {
|
|
97
|
+
const content = fs.readFileSync(localIgnorePath, 'utf8');
|
|
98
|
+
const lines = content.split('\n').map(line => {
|
|
99
|
+
line = line.trim();
|
|
100
|
+
if (!line || line.startsWith('#')) return null;
|
|
101
|
+
// Rules in sub-gitignore are relative to that directory
|
|
102
|
+
const relDir = path.relative(cwd, dir).replace(/\\/g, '/');
|
|
103
|
+
return relDir ? `${relDir}/${line}` : line;
|
|
104
|
+
}).filter(Boolean);
|
|
105
|
+
localIg = ignore().add(ig).add(lines);
|
|
106
|
+
}
|
|
107
|
+
|
|
103
108
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
104
109
|
for (const entry of entries) {
|
|
105
110
|
const fullPath = path.join(dir, entry.name);
|
|
106
|
-
|
|
111
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
107
112
|
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
108
|
-
const
|
|
113
|
+
const isDirectory = entry.isDirectory();
|
|
114
|
+
const checkPath = isDirectory ? `${normalizedPath}/` : normalizedPath;
|
|
109
115
|
|
|
110
|
-
if (
|
|
116
|
+
if (localIg.ignores(checkPath)) continue;
|
|
111
117
|
|
|
112
|
-
if (
|
|
113
|
-
walk(fullPath);
|
|
118
|
+
if (isDirectory) {
|
|
119
|
+
walk(fullPath, localIg);
|
|
114
120
|
} else if (entry.isFile()) {
|
|
115
121
|
const ext = path.extname(entry.name);
|
|
116
122
|
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
117
123
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
118
|
-
|
|
119
|
-
// Extract file-level description
|
|
120
124
|
const firstLines = content.split('\n').slice(0, 5);
|
|
121
125
|
let fileDesc = '';
|
|
122
126
|
for (const line of firstLines) {
|
|
@@ -125,23 +129,15 @@ async function generate(cwd = process.cwd()) {
|
|
|
125
129
|
fileDesc += trimmed.replace(/[\/*#]/g, '').trim() + ' ';
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
|
-
|
|
129
132
|
const symbols = extractSymbols(content);
|
|
130
|
-
|
|
131
|
-
// Backup: If no file description, provide a summary
|
|
132
|
-
if (!fileDesc.trim() && symbols.length > 0) {
|
|
133
|
-
fileDesc = `Contains ${symbols.length} symbols.`;
|
|
134
|
-
}
|
|
135
|
-
|
|
133
|
+
if (!fileDesc.trim() && symbols.length > 0) fileDesc = `Contains ${symbols.length} symbols.`;
|
|
136
134
|
files.push({ path: normalizedPath, desc: fileDesc.trim(), symbols });
|
|
137
|
-
} else {
|
|
138
|
-
files.push({ path: normalizedPath, desc: '', symbols: [] });
|
|
139
135
|
}
|
|
140
136
|
}
|
|
141
137
|
}
|
|
142
138
|
}
|
|
143
139
|
|
|
144
|
-
walk(cwd);
|
|
140
|
+
walk(cwd, getIgnores(cwd));
|
|
145
141
|
|
|
146
142
|
const output = files.map(f => {
|
|
147
143
|
const descStr = f.desc ? ` | desc: ${f.desc.substring(0, 100)}` : '';
|
|
@@ -154,54 +150,49 @@ async function generate(cwd = process.cwd()) {
|
|
|
154
150
|
console.log(`[Code-Graph] Updated ${DEFAULT_MAP_FILE}`);
|
|
155
151
|
}
|
|
156
152
|
|
|
157
|
-
|
|
153
|
+
export { generate };
|
|
154
|
+
|
|
155
|
+
export function watch(cwd = process.cwd()) {
|
|
158
156
|
console.log(`[Code-Graph] Watching for changes in ${cwd}...`);
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
let timeout;
|
|
158
|
+
const debouncedGenerate = () => {
|
|
159
|
+
clearTimeout(timeout);
|
|
160
|
+
timeout = setTimeout(() => generate(cwd), 500);
|
|
161
|
+
};
|
|
162
|
+
|
|
161
163
|
const watcher = chokidar.watch(cwd, {
|
|
162
164
|
ignored: (p) => {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
+
if (p === cwd) return false;
|
|
166
|
+
// Watcher ignore is harder for recursive .gitignore without complexity
|
|
167
|
+
// We rely on the generate() call to skip them during walk
|
|
168
|
+
return false;
|
|
165
169
|
},
|
|
166
170
|
persistent: true,
|
|
167
171
|
ignoreInitial: true
|
|
168
172
|
});
|
|
169
173
|
|
|
170
|
-
let timeout;
|
|
171
|
-
const debouncedGenerate = () => {
|
|
172
|
-
clearTimeout(timeout);
|
|
173
|
-
timeout = setTimeout(() => generate(cwd), 500);
|
|
174
|
-
};
|
|
175
|
-
|
|
176
174
|
watcher.on('all', (event, path) => {
|
|
177
|
-
console.log(`[Code-Graph] Change detected: ${event} on ${path}`);
|
|
178
175
|
debouncedGenerate();
|
|
179
176
|
});
|
|
180
177
|
}
|
|
181
178
|
|
|
182
|
-
function installHook(cwd = process.cwd()) {
|
|
179
|
+
export function installHook(cwd = process.cwd()) {
|
|
183
180
|
const hooksDir = path.join(cwd, '.git', 'hooks');
|
|
184
181
|
if (!fs.existsSync(hooksDir)) {
|
|
185
182
|
console.error('[Code-Graph] No .git directory found. Cannot install hook.');
|
|
186
183
|
return;
|
|
187
184
|
}
|
|
188
|
-
|
|
189
185
|
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
190
186
|
const hookContent = `#!/bin/sh\n# Code-Graph pre-commit hook\nnode "${__filename}" generate\ngit add "${DEFAULT_MAP_FILE}"\n`;
|
|
191
|
-
|
|
192
187
|
fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
193
188
|
console.log('[Code-Graph] Installed pre-commit hook.');
|
|
194
189
|
}
|
|
195
190
|
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
if (command === 'generate')
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
watch
|
|
203
|
-
} else if (command === 'install-hook') {
|
|
204
|
-
installHook();
|
|
205
|
-
} else {
|
|
206
|
-
console.log('Usage: code-graph [generate|watch|install-hook]');
|
|
191
|
+
if (process.argv[1] && (process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1].endsWith('index.js'))) {
|
|
192
|
+
const args = process.argv.slice(2);
|
|
193
|
+
const command = args[0] || 'generate';
|
|
194
|
+
if (command === 'generate') generate();
|
|
195
|
+
else if (command === 'watch') watch();
|
|
196
|
+
else if (command === 'install-hook') installHook();
|
|
197
|
+
else console.log('Usage: code-graph [generate|watch|install-hook]');
|
|
207
198
|
}
|
package/llm-code-graph.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
- .gitignore
|
|
5
5
|
- index.js | desc: !usrbinenv node
|
|
6
|
-
- syms: [Name [Dart:], Name [PHP: class Name, interface Name, trait Name,], Name [PHP: class Name, interface Name,], Name [PHP: class Name,], Name [PHP:], Name [Ruby: def name, class Name,], Name [Ruby: def name,], Name [Swift: func name, class Name, struct Name, protocol Name,], Name [Swift: func name, class Name, struct Name,], Name [Swift: func name, class Name,], Name [Swift: func name,], extractSymbols [(content)],
|
|
6
|
+
- syms: [Name [Dart:], Name [PHP: class Name, interface Name, trait Name,], Name [PHP: class Name, interface Name,], Name [PHP: class Name,], Name [PHP:], Name [Ruby: def name, class Name,], Name [Ruby: def name,], Name [Swift: func name, class Name, struct Name, protocol Name,], Name [Swift: func name, class Name, struct Name,], Name [Swift: func name, class Name,], Name [Swift: func name,], SUPPORTED_EXTENSIONS [= [], extractSymbols [(content)], function [generate(cwd = process.cwd())], generate [(cwd = process.cwd()], getIgnores [(cwd)], installHook [(cwd = process.cwd()], is [We must check if p is a directory to append the trailing slash Since chokidar's ignore], name [Dart: class Name, void name,], name [Ruby:], name [Swift:], walk [(dir)], watch [(cwd = process.cwd()]]
|
|
7
7
|
- package.json
|
|
8
|
-
- README.md
|
|
8
|
+
- README.md
|
|
9
|
+
- test/index.test.js | desc: Contains 5 symbols.
|
|
10
|
+
- syms: [noDocFunc [(arg1: string, arg2: number)], py_func [(x)], py_func [Note: Current regex captures '], py_func_2 [This is a python comment], testFunc [This is a test function]]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-graph-llm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Compact, language-agnostic codebase mapper for LLM token efficiency.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -24,5 +24,8 @@
|
|
|
24
24
|
"chokidar": "^3.6.0",
|
|
25
25
|
"ignore": "^5.3.1"
|
|
26
26
|
},
|
|
27
|
-
"type": "module"
|
|
27
|
+
"type": "module",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node --test test/*.test.js"
|
|
30
|
+
}
|
|
28
31
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import {
|
|
7
|
+
extractSymbols,
|
|
8
|
+
getIgnores,
|
|
9
|
+
SUPPORTED_EXTENSIONS,
|
|
10
|
+
generate
|
|
11
|
+
} from '../index.js';
|
|
12
|
+
|
|
13
|
+
test('extractSymbols - JS/TS Docstrings', () => {
|
|
14
|
+
const code = `
|
|
15
|
+
/**
|
|
16
|
+
* This is a test function
|
|
17
|
+
*/
|
|
18
|
+
function testFunc(a, b) {}
|
|
19
|
+
`;
|
|
20
|
+
const symbols = extractSymbols(code);
|
|
21
|
+
assert.ok(symbols.some(s => s.includes('testFunc') && s.includes('This is a test function')));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('extractSymbols - Signature Fallback', () => {
|
|
25
|
+
const code = `
|
|
26
|
+
function noDocFunc(arg1: string, arg2: number) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
const symbols = extractSymbols(code);
|
|
31
|
+
// Matches "noDocFunc [(arg1: string, arg2: number)]"
|
|
32
|
+
assert.ok(symbols.some(s => s.includes('noDocFunc') && s.includes('arg1: string, arg2: number')));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('extractSymbols - Flutter/Dart Noise Reduction', () => {
|
|
36
|
+
const code = `
|
|
37
|
+
const SizedBox(height: 10);
|
|
38
|
+
void realFunction() {}
|
|
39
|
+
`;
|
|
40
|
+
const symbols = extractSymbols(code);
|
|
41
|
+
assert.ok(symbols.some(s => s.includes('realFunction')));
|
|
42
|
+
assert.ok(!symbols.some(s => s.includes('SizedBox')));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('getIgnores - Default Patterns', () => {
|
|
46
|
+
const ig = getIgnores(process.cwd());
|
|
47
|
+
assert.strictEqual(ig.ignores('.git/'), true);
|
|
48
|
+
assert.strictEqual(ig.ignores('node_modules/'), true);
|
|
49
|
+
assert.strictEqual(ig.ignores('.idea/'), true);
|
|
50
|
+
assert.strictEqual(ig.ignores('.dart_tool/'), true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('Recursive Ignore Simulation (Logic Check)', async () => {
|
|
54
|
+
const tempDir = path.join(process.cwd(), 'temp_test_dir');
|
|
55
|
+
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
|
|
56
|
+
fs.mkdirSync(tempDir);
|
|
57
|
+
|
|
58
|
+
const subDir = path.join(tempDir, 'subdir');
|
|
59
|
+
fs.mkdirSync(subDir);
|
|
60
|
+
|
|
61
|
+
// Create a file that should be ignored by subdir/.gitignore
|
|
62
|
+
fs.writeFileSync(path.join(subDir, 'ignored.js'), 'function ignored() {}');
|
|
63
|
+
fs.writeFileSync(path.join(subDir, 'included.js'), 'function included() {}');
|
|
64
|
+
fs.writeFileSync(path.join(subDir, '.gitignore'), 'ignored.js');
|
|
65
|
+
|
|
66
|
+
await generate(tempDir);
|
|
67
|
+
|
|
68
|
+
const mapPath = path.join(tempDir, 'llm-code-graph.md');
|
|
69
|
+
const mapContent = fs.readFileSync(mapPath, 'utf8');
|
|
70
|
+
|
|
71
|
+
assert.ok(mapContent.includes('included.js'));
|
|
72
|
+
assert.ok(!mapContent.includes('ignored.js'));
|
|
73
|
+
|
|
74
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
75
|
+
});
|