@optave/codegraph 1.3.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/README.md +19 -1
- package/package.json +5 -5
- package/src/config.js +45 -3
- package/src/mcp.js +169 -3
- package/src/queries.js +30 -0
- package/src/resolve.js +1 -1
package/README.md
CHANGED
|
@@ -366,6 +366,7 @@ See **[docs/recommended-practices.md](docs/recommended-practices.md)** for integ
|
|
|
366
366
|
- **CI/CD** — PR impact comments, threshold gates, graph caching
|
|
367
367
|
- **AI agents** — MCP server, CLAUDE.md templates, Claude Code hooks
|
|
368
368
|
- **Developer workflow** — watch mode, explore-before-you-edit, semantic search
|
|
369
|
+
- **Secure credentials** — `apiKeyCommand` with 1Password, Bitwarden, Vault, macOS Keychain, `pass`
|
|
369
370
|
|
|
370
371
|
## 🔁 CI / GitHub Actions
|
|
371
372
|
|
|
@@ -395,6 +396,23 @@ Create a `.codegraphrc.json` in your project root to customize behavior:
|
|
|
395
396
|
}
|
|
396
397
|
```
|
|
397
398
|
|
|
399
|
+
### LLM credentials
|
|
400
|
+
|
|
401
|
+
Codegraph supports an `apiKeyCommand` field for secure credential management. Instead of storing API keys in config files or environment variables, you can shell out to a secret manager at runtime:
|
|
402
|
+
|
|
403
|
+
```json
|
|
404
|
+
{
|
|
405
|
+
"llm": {
|
|
406
|
+
"provider": "openai",
|
|
407
|
+
"apiKeyCommand": "op read op://vault/openai/api-key"
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
The command is split on whitespace and executed with `execFileSync` (no shell injection risk). Priority: **command output > `CODEGRAPH_LLM_API_KEY` env var > file config**. On failure, codegraph warns and falls back to the next source.
|
|
413
|
+
|
|
414
|
+
Works with any secret manager: 1Password CLI (`op`), Bitwarden (`bw`), `pass`, HashiCorp Vault, macOS Keychain (`security`), AWS Secrets Manager, etc.
|
|
415
|
+
|
|
398
416
|
## 📖 Programmatic API
|
|
399
417
|
|
|
400
418
|
Codegraph also exports a full API for use in your own tools:
|
|
@@ -449,7 +467,7 @@ const { results: fused } = await multiSearchData(
|
|
|
449
467
|
See **[ROADMAP.md](ROADMAP.md)** for the full development roadmap. Current plan:
|
|
450
468
|
|
|
451
469
|
1. ~~**Rust Core**~~ — **Complete** (v1.3.0) — native tree-sitter parsing via napi-rs, parallel multi-core parsing, incremental re-parsing, import resolution & cycle detection in Rust
|
|
452
|
-
2.
|
|
470
|
+
2. ~~**Foundation Hardening**~~ — **Complete** (v1.4.0) — parser registry, 11-tool MCP server, test coverage 62%→75%, `apiKeyCommand` secret resolution
|
|
453
471
|
3. **Intelligent Embeddings** — LLM-generated descriptions, hybrid search
|
|
454
472
|
4. **Natural Language Queries** — `codegraph ask` command, conversational sessions
|
|
455
473
|
5. **Expanded Language Support** — 8 new languages (12 → 20)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optave/codegraph",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Local code graph CLI — parse codebases with tree-sitter, build dependency graphs, query them",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -61,10 +61,10 @@
|
|
|
61
61
|
"optionalDependencies": {
|
|
62
62
|
"@huggingface/transformers": "^3.8.1",
|
|
63
63
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
64
|
-
"@optave/codegraph-darwin-arm64": "1.
|
|
65
|
-
"@optave/codegraph-darwin-x64": "1.
|
|
66
|
-
"@optave/codegraph-linux-x64-gnu": "1.
|
|
67
|
-
"@optave/codegraph-win32-x64-msvc": "1.
|
|
64
|
+
"@optave/codegraph-darwin-arm64": "1.4.1",
|
|
65
|
+
"@optave/codegraph-darwin-x64": "1.4.1",
|
|
66
|
+
"@optave/codegraph-linux-x64-gnu": "1.4.1",
|
|
67
|
+
"@optave/codegraph-win32-x64-msvc": "1.4.1"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@biomejs/biome": "^2.4.4",
|
package/src/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import { debug } from './logger.js';
|
|
4
|
+
import { debug, warn } from './logger.js';
|
|
4
5
|
|
|
5
6
|
export const CONFIG_FILES = ['.codegraphrc.json', '.codegraphrc', 'codegraph.config.json'];
|
|
6
7
|
|
|
@@ -18,6 +19,10 @@ export const DEFAULTS = {
|
|
|
18
19
|
defaultDepth: 3,
|
|
19
20
|
defaultLimit: 20,
|
|
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 },
|
|
21
26
|
};
|
|
22
27
|
|
|
23
28
|
/**
|
|
@@ -33,13 +38,50 @@ export function loadConfig(cwd) {
|
|
|
33
38
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
34
39
|
const config = JSON.parse(raw);
|
|
35
40
|
debug(`Loaded config from ${filePath}`);
|
|
36
|
-
return mergeConfig(DEFAULTS, config);
|
|
41
|
+
return resolveSecrets(applyEnvOverrides(mergeConfig(DEFAULTS, config)));
|
|
37
42
|
} catch (err) {
|
|
38
43
|
debug(`Failed to parse config ${filePath}: ${err.message}`);
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
|
-
return { ...DEFAULTS };
|
|
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;
|
|
43
85
|
}
|
|
44
86
|
|
|
45
87
|
function mergeConfig(defaults, overrides) {
|
package/src/mcp.js
CHANGED
|
@@ -66,6 +66,93 @@ const TOOLS = [
|
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: 'fn_deps',
|
|
71
|
+
description: 'Show function-level dependency chain: what a function calls and what calls it',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
76
|
+
depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
|
|
77
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
78
|
+
},
|
|
79
|
+
required: ['name'],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'fn_impact',
|
|
84
|
+
description:
|
|
85
|
+
'Show function-level blast radius: all functions transitively affected by changes to a function',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
name: { type: 'string', description: 'Function/method/class name (partial match)' },
|
|
90
|
+
depth: { type: 'number', description: 'Max traversal depth', default: 5 },
|
|
91
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
92
|
+
},
|
|
93
|
+
required: ['name'],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'diff_impact',
|
|
98
|
+
description: 'Analyze git diff to find which functions changed and their transitive callers',
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
staged: { type: 'boolean', description: 'Analyze staged changes only', default: false },
|
|
103
|
+
ref: { type: 'string', description: 'Git ref to diff against (default: HEAD)' },
|
|
104
|
+
depth: { type: 'number', description: 'Transitive caller depth', default: 3 },
|
|
105
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'semantic_search',
|
|
111
|
+
description:
|
|
112
|
+
'Search code symbols by meaning using embeddings (requires prior `codegraph embed`)',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
query: { type: 'string', description: 'Natural language search query' },
|
|
117
|
+
limit: { type: 'number', description: 'Max results to return', default: 15 },
|
|
118
|
+
min_score: { type: 'number', description: 'Minimum similarity score (0-1)', default: 0.2 },
|
|
119
|
+
},
|
|
120
|
+
required: ['query'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'export_graph',
|
|
125
|
+
description: 'Export the dependency graph in DOT (Graphviz), Mermaid, or JSON format',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
format: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
enum: ['dot', 'mermaid', 'json'],
|
|
132
|
+
description: 'Export format',
|
|
133
|
+
},
|
|
134
|
+
file_level: {
|
|
135
|
+
type: 'boolean',
|
|
136
|
+
description: 'File-level graph (true) or function-level (false)',
|
|
137
|
+
default: true,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
required: ['format'],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'list_functions',
|
|
145
|
+
description:
|
|
146
|
+
'List functions, methods, and classes in the codebase, optionally filtered by file or name pattern',
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
file: { type: 'string', description: 'Filter by file path (partial match)' },
|
|
151
|
+
pattern: { type: 'string', description: 'Filter by function name (partial match)' },
|
|
152
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
69
156
|
];
|
|
70
157
|
|
|
71
158
|
export { TOOLS };
|
|
@@ -90,9 +177,16 @@ export async function startMCPServer(customDbPath) {
|
|
|
90
177
|
}
|
|
91
178
|
|
|
92
179
|
// Lazy import query functions to avoid circular deps at module load
|
|
93
|
-
const {
|
|
94
|
-
|
|
95
|
-
|
|
180
|
+
const {
|
|
181
|
+
queryNameData,
|
|
182
|
+
impactAnalysisData,
|
|
183
|
+
moduleMapData,
|
|
184
|
+
fileDepsData,
|
|
185
|
+
fnDepsData,
|
|
186
|
+
fnImpactData,
|
|
187
|
+
diffImpactData,
|
|
188
|
+
listFunctionsData,
|
|
189
|
+
} = await import('./queries.js');
|
|
96
190
|
|
|
97
191
|
const require = createRequire(import.meta.url);
|
|
98
192
|
const Database = require('better-sqlite3');
|
|
@@ -130,6 +224,78 @@ export async function startMCPServer(customDbPath) {
|
|
|
130
224
|
case 'module_map':
|
|
131
225
|
result = moduleMapData(dbPath, args.limit || 20);
|
|
132
226
|
break;
|
|
227
|
+
case 'fn_deps':
|
|
228
|
+
result = fnDepsData(args.name, dbPath, {
|
|
229
|
+
depth: args.depth,
|
|
230
|
+
noTests: args.no_tests,
|
|
231
|
+
});
|
|
232
|
+
break;
|
|
233
|
+
case 'fn_impact':
|
|
234
|
+
result = fnImpactData(args.name, dbPath, {
|
|
235
|
+
depth: args.depth,
|
|
236
|
+
noTests: args.no_tests,
|
|
237
|
+
});
|
|
238
|
+
break;
|
|
239
|
+
case 'diff_impact':
|
|
240
|
+
result = diffImpactData(dbPath, {
|
|
241
|
+
staged: args.staged,
|
|
242
|
+
ref: args.ref,
|
|
243
|
+
depth: args.depth,
|
|
244
|
+
noTests: args.no_tests,
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
case 'semantic_search': {
|
|
248
|
+
const { searchData } = await import('./embedder.js');
|
|
249
|
+
result = await searchData(args.query, dbPath, {
|
|
250
|
+
limit: args.limit,
|
|
251
|
+
minScore: args.min_score,
|
|
252
|
+
});
|
|
253
|
+
if (result === null) {
|
|
254
|
+
return {
|
|
255
|
+
content: [
|
|
256
|
+
{ type: 'text', text: 'Semantic search unavailable. Run `codegraph embed` first.' },
|
|
257
|
+
],
|
|
258
|
+
isError: true,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case 'export_graph': {
|
|
264
|
+
const { exportDOT, exportMermaid, exportJSON } = await import('./export.js');
|
|
265
|
+
const db = new Database(findDbPath(dbPath), { readonly: true });
|
|
266
|
+
const fileLevel = args.file_level !== false;
|
|
267
|
+
switch (args.format) {
|
|
268
|
+
case 'dot':
|
|
269
|
+
result = exportDOT(db, { fileLevel });
|
|
270
|
+
break;
|
|
271
|
+
case 'mermaid':
|
|
272
|
+
result = exportMermaid(db, { fileLevel });
|
|
273
|
+
break;
|
|
274
|
+
case 'json':
|
|
275
|
+
result = exportJSON(db);
|
|
276
|
+
break;
|
|
277
|
+
default:
|
|
278
|
+
db.close();
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: 'text',
|
|
283
|
+
text: `Unknown format: ${args.format}. Use dot, mermaid, or json.`,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
isError: true,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
db.close();
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'list_functions':
|
|
293
|
+
result = listFunctionsData(dbPath, {
|
|
294
|
+
file: args.file,
|
|
295
|
+
pattern: args.pattern,
|
|
296
|
+
noTests: args.no_tests,
|
|
297
|
+
});
|
|
298
|
+
break;
|
|
133
299
|
default:
|
|
134
300
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
135
301
|
}
|
package/src/queries.js
CHANGED
|
@@ -566,6 +566,36 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
566
566
|
};
|
|
567
567
|
}
|
|
568
568
|
|
|
569
|
+
export function listFunctionsData(customDbPath, opts = {}) {
|
|
570
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
571
|
+
const noTests = opts.noTests || false;
|
|
572
|
+
const kinds = ['function', 'method', 'class'];
|
|
573
|
+
const placeholders = kinds.map(() => '?').join(', ');
|
|
574
|
+
|
|
575
|
+
const conditions = [`kind IN (${placeholders})`];
|
|
576
|
+
const params = [...kinds];
|
|
577
|
+
|
|
578
|
+
if (opts.file) {
|
|
579
|
+
conditions.push('file LIKE ?');
|
|
580
|
+
params.push(`%${opts.file}%`);
|
|
581
|
+
}
|
|
582
|
+
if (opts.pattern) {
|
|
583
|
+
conditions.push('name LIKE ?');
|
|
584
|
+
params.push(`%${opts.pattern}%`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let rows = db
|
|
588
|
+
.prepare(
|
|
589
|
+
`SELECT name, kind, file, line FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
|
|
590
|
+
)
|
|
591
|
+
.all(...params);
|
|
592
|
+
|
|
593
|
+
if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
|
|
594
|
+
|
|
595
|
+
db.close();
|
|
596
|
+
return { count: rows.length, functions: rows };
|
|
597
|
+
}
|
|
598
|
+
|
|
569
599
|
// ─── Human-readable output (original formatting) ───────────────────────
|
|
570
600
|
|
|
571
601
|
export function queryName(name, customDbPath, opts = {}) {
|
package/src/resolve.js
CHANGED
|
@@ -12,7 +12,7 @@ import { loadNative } from './native.js';
|
|
|
12
12
|
export function convertAliasesForNative(aliases) {
|
|
13
13
|
if (!aliases) return null;
|
|
14
14
|
return {
|
|
15
|
-
baseUrl: aliases.baseUrl ||
|
|
15
|
+
baseUrl: aliases.baseUrl || '',
|
|
16
16
|
paths: Object.entries(aliases.paths || {}).map(([pattern, targets]) => ({
|
|
17
17
|
pattern,
|
|
18
18
|
targets,
|