@optave/codegraph 1.1.0 → 1.4.1
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/LICENSE +190 -190
- package/README.md +498 -311
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-hcl.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +90 -69
- package/src/builder.js +161 -162
- package/src/cli.js +284 -224
- package/src/config.js +103 -55
- package/src/constants.js +41 -28
- package/src/cycles.js +125 -104
- package/src/db.js +129 -117
- package/src/embedder.js +253 -59
- package/src/export.js +150 -138
- package/src/index.js +50 -39
- package/src/logger.js +24 -20
- package/src/mcp.js +311 -139
- package/src/native.js +68 -0
- package/src/parser.js +2214 -573
- package/src/queries.js +334 -128
- package/src/resolve.js +171 -0
- package/src/watcher.js +81 -53
package/src/config.js
CHANGED
|
@@ -1,55 +1,103 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { debug, warn } from './logger.js';
|
|
5
|
+
|
|
6
|
+
export const CONFIG_FILES = ['.codegraphrc.json', '.codegraphrc', 'codegraph.config.json'];
|
|
7
|
+
|
|
8
|
+
export const DEFAULTS = {
|
|
9
|
+
include: [],
|
|
10
|
+
exclude: [],
|
|
11
|
+
ignoreDirs: [],
|
|
12
|
+
extensions: [],
|
|
13
|
+
aliases: {},
|
|
14
|
+
build: {
|
|
15
|
+
incremental: true,
|
|
16
|
+
dbPath: '.codegraph/graph.db',
|
|
17
|
+
},
|
|
18
|
+
query: {
|
|
19
|
+
defaultDepth: 3,
|
|
20
|
+
defaultLimit: 20,
|
|
21
|
+
},
|
|
22
|
+
embeddings: { model: 'minilm', llmProvider: null },
|
|
23
|
+
llm: { provider: null, model: null, baseUrl: null, apiKey: null, apiKeyCommand: null },
|
|
24
|
+
search: { defaultMinScore: 0.2, rrfK: 60, topK: 15 },
|
|
25
|
+
ci: { failOnCycles: false, impactThreshold: null },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load project configuration from a .codegraphrc.json or similar file.
|
|
30
|
+
* Returns merged config with defaults.
|
|
31
|
+
*/
|
|
32
|
+
export function loadConfig(cwd) {
|
|
33
|
+
cwd = cwd || process.cwd();
|
|
34
|
+
for (const name of CONFIG_FILES) {
|
|
35
|
+
const filePath = path.join(cwd, name);
|
|
36
|
+
if (fs.existsSync(filePath)) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
39
|
+
const config = JSON.parse(raw);
|
|
40
|
+
debug(`Loaded config from ${filePath}`);
|
|
41
|
+
return resolveSecrets(applyEnvOverrides(mergeConfig(DEFAULTS, config)));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
debug(`Failed to parse config ${filePath}: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return resolveSecrets(applyEnvOverrides({ ...DEFAULTS }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ENV_LLM_MAP = {
|
|
51
|
+
CODEGRAPH_LLM_PROVIDER: 'provider',
|
|
52
|
+
CODEGRAPH_LLM_API_KEY: 'apiKey',
|
|
53
|
+
CODEGRAPH_LLM_MODEL: 'model',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function applyEnvOverrides(config) {
|
|
57
|
+
for (const [envKey, field] of Object.entries(ENV_LLM_MAP)) {
|
|
58
|
+
if (process.env[envKey] !== undefined) {
|
|
59
|
+
config.llm[field] = process.env[envKey];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return config;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveSecrets(config) {
|
|
66
|
+
const cmd = config.llm.apiKeyCommand;
|
|
67
|
+
if (typeof cmd !== 'string' || cmd.trim() === '') return config;
|
|
68
|
+
|
|
69
|
+
const parts = cmd.trim().split(/\s+/);
|
|
70
|
+
const [executable, ...args] = parts;
|
|
71
|
+
try {
|
|
72
|
+
const result = execFileSync(executable, args, {
|
|
73
|
+
encoding: 'utf-8',
|
|
74
|
+
timeout: 10_000,
|
|
75
|
+
maxBuffer: 64 * 1024,
|
|
76
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
77
|
+
}).trim();
|
|
78
|
+
if (result) {
|
|
79
|
+
config.llm.apiKey = result;
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
warn(`apiKeyCommand failed: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mergeConfig(defaults, overrides) {
|
|
88
|
+
const result = { ...defaults };
|
|
89
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
90
|
+
if (
|
|
91
|
+
value &&
|
|
92
|
+
typeof value === 'object' &&
|
|
93
|
+
!Array.isArray(value) &&
|
|
94
|
+
defaults[key] &&
|
|
95
|
+
typeof defaults[key] === 'object'
|
|
96
|
+
) {
|
|
97
|
+
result[key] = { ...defaults[key], ...value };
|
|
98
|
+
} else {
|
|
99
|
+
result[key] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
package/src/constants.js
CHANGED
|
@@ -1,28 +1,41 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
'
|
|
6
|
-
'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'.
|
|
11
|
-
'.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { SUPPORTED_EXTENSIONS } from './parser.js';
|
|
3
|
+
|
|
4
|
+
export const IGNORE_DIRS = new Set([
|
|
5
|
+
'node_modules',
|
|
6
|
+
'.git',
|
|
7
|
+
'dist',
|
|
8
|
+
'build',
|
|
9
|
+
'.next',
|
|
10
|
+
'.nuxt',
|
|
11
|
+
'.svelte-kit',
|
|
12
|
+
'coverage',
|
|
13
|
+
'.codegraph',
|
|
14
|
+
'__pycache__',
|
|
15
|
+
'.tox',
|
|
16
|
+
'vendor',
|
|
17
|
+
'.venv',
|
|
18
|
+
'venv',
|
|
19
|
+
'env',
|
|
20
|
+
'.env',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// Re-export as an indirect binding to avoid TDZ in the circular
|
|
24
|
+
// parser.js ↔ constants.js import (no value read at evaluation time).
|
|
25
|
+
export { SUPPORTED_EXTENSIONS as EXTENSIONS };
|
|
26
|
+
|
|
27
|
+
export function shouldIgnore(dirName) {
|
|
28
|
+
return IGNORE_DIRS.has(dirName) || dirName.startsWith('.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isSupportedFile(filePath) {
|
|
32
|
+
return SUPPORTED_EXTENSIONS.has(path.extname(filePath));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Normalize a file path to always use forward slashes.
|
|
37
|
+
* Ensures cross-platform consistency in the SQLite database.
|
|
38
|
+
*/
|
|
39
|
+
export function normalizePath(filePath) {
|
|
40
|
+
return filePath.split(path.sep).join('/');
|
|
41
|
+
}
|
package/src/cycles.js
CHANGED
|
@@ -1,104 +1,125 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return
|
|
104
|
-
}
|
|
1
|
+
import { loadNative } from './native.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect circular dependencies in the codebase using Tarjan's SCC algorithm.
|
|
5
|
+
* Dispatches to native Rust implementation when available, falls back to JS.
|
|
6
|
+
* @param {object} db - Open SQLite database
|
|
7
|
+
* @param {object} opts - { fileLevel: true }
|
|
8
|
+
* @returns {string[][]} Array of cycles, each cycle is an array of file paths
|
|
9
|
+
*/
|
|
10
|
+
export function findCycles(db, opts = {}) {
|
|
11
|
+
const fileLevel = opts.fileLevel !== false;
|
|
12
|
+
|
|
13
|
+
// Build adjacency list from SQLite (stays in JS — only the algorithm can move to Rust)
|
|
14
|
+
let edges;
|
|
15
|
+
if (fileLevel) {
|
|
16
|
+
edges = db
|
|
17
|
+
.prepare(`
|
|
18
|
+
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
19
|
+
FROM edges e
|
|
20
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
21
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
22
|
+
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type')
|
|
23
|
+
`)
|
|
24
|
+
.all();
|
|
25
|
+
} else {
|
|
26
|
+
edges = db
|
|
27
|
+
.prepare(`
|
|
28
|
+
SELECT DISTINCT
|
|
29
|
+
(n1.name || '|' || n1.file) AS source,
|
|
30
|
+
(n2.name || '|' || n2.file) AS target
|
|
31
|
+
FROM edges e
|
|
32
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
33
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
34
|
+
WHERE n1.kind IN ('function', 'method', 'class')
|
|
35
|
+
AND n2.kind IN ('function', 'method', 'class')
|
|
36
|
+
AND e.kind = 'calls'
|
|
37
|
+
AND n1.id != n2.id
|
|
38
|
+
`)
|
|
39
|
+
.all();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Try native Rust implementation
|
|
43
|
+
const native = loadNative();
|
|
44
|
+
if (native) {
|
|
45
|
+
return native.detectCycles(edges);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fallback: JS Tarjan
|
|
49
|
+
return findCyclesJS(edges);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pure-JS Tarjan's SCC implementation.
|
|
54
|
+
*/
|
|
55
|
+
export function findCyclesJS(edges) {
|
|
56
|
+
const graph = new Map();
|
|
57
|
+
for (const { source, target } of edges) {
|
|
58
|
+
if (!graph.has(source)) graph.set(source, []);
|
|
59
|
+
graph.get(source).push(target);
|
|
60
|
+
if (!graph.has(target)) graph.set(target, []);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Tarjan's strongly connected components algorithm
|
|
64
|
+
let index = 0;
|
|
65
|
+
const stack = [];
|
|
66
|
+
const onStack = new Set();
|
|
67
|
+
const indices = new Map();
|
|
68
|
+
const lowlinks = new Map();
|
|
69
|
+
const sccs = [];
|
|
70
|
+
|
|
71
|
+
function strongconnect(v) {
|
|
72
|
+
indices.set(v, index);
|
|
73
|
+
lowlinks.set(v, index);
|
|
74
|
+
index++;
|
|
75
|
+
stack.push(v);
|
|
76
|
+
onStack.add(v);
|
|
77
|
+
|
|
78
|
+
for (const w of graph.get(v) || []) {
|
|
79
|
+
if (!indices.has(w)) {
|
|
80
|
+
strongconnect(w);
|
|
81
|
+
lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
|
|
82
|
+
} else if (onStack.has(w)) {
|
|
83
|
+
lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (lowlinks.get(v) === indices.get(v)) {
|
|
88
|
+
const scc = [];
|
|
89
|
+
let w;
|
|
90
|
+
do {
|
|
91
|
+
w = stack.pop();
|
|
92
|
+
onStack.delete(w);
|
|
93
|
+
scc.push(w);
|
|
94
|
+
} while (w !== v);
|
|
95
|
+
if (scc.length > 1) sccs.push(scc);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const node of graph.keys()) {
|
|
100
|
+
if (!indices.has(node)) strongconnect(node);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return sccs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Format cycles for human-readable output.
|
|
108
|
+
*/
|
|
109
|
+
export function formatCycles(cycles) {
|
|
110
|
+
if (cycles.length === 0) {
|
|
111
|
+
return 'No circular dependencies detected.';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const lines = [`Found ${cycles.length} circular dependency cycle(s):\n`];
|
|
115
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
116
|
+
const cycle = cycles[i];
|
|
117
|
+
lines.push(` Cycle ${i + 1} (${cycle.length} files):`);
|
|
118
|
+
for (const file of cycle) {
|
|
119
|
+
lines.push(` -> ${file}`);
|
|
120
|
+
}
|
|
121
|
+
lines.push(` -> ${cycle[0]} (back to start)`);
|
|
122
|
+
lines.push('');
|
|
123
|
+
}
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|