@optave/codegraph 3.1.0 → 3.1.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.
Files changed (47) hide show
  1. package/README.md +5 -5
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +8 -9
  4. package/src/ast-analysis/rules/csharp.js +201 -0
  5. package/src/ast-analysis/rules/go.js +182 -0
  6. package/src/ast-analysis/rules/index.js +82 -0
  7. package/src/ast-analysis/rules/java.js +175 -0
  8. package/src/ast-analysis/rules/javascript.js +246 -0
  9. package/src/ast-analysis/rules/php.js +219 -0
  10. package/src/ast-analysis/rules/python.js +196 -0
  11. package/src/ast-analysis/rules/ruby.js +204 -0
  12. package/src/ast-analysis/rules/rust.js +173 -0
  13. package/src/ast-analysis/shared.js +223 -0
  14. package/src/ast.js +15 -28
  15. package/src/audit.js +4 -5
  16. package/src/boundaries.js +1 -1
  17. package/src/branch-compare.js +84 -79
  18. package/src/builder.js +0 -5
  19. package/src/cfg.js +106 -338
  20. package/src/check.js +3 -3
  21. package/src/cli.js +99 -179
  22. package/src/cochange.js +1 -1
  23. package/src/communities.js +13 -16
  24. package/src/complexity.js +196 -1239
  25. package/src/cycles.js +1 -1
  26. package/src/dataflow.js +269 -694
  27. package/src/db/connection.js +88 -0
  28. package/src/db/migrations.js +312 -0
  29. package/src/db/query-builder.js +280 -0
  30. package/src/db/repository.js +134 -0
  31. package/src/db.js +19 -399
  32. package/src/embedder.js +145 -141
  33. package/src/export.js +1 -1
  34. package/src/flow.js +161 -162
  35. package/src/index.js +34 -1
  36. package/src/kinds.js +49 -0
  37. package/src/manifesto.js +3 -8
  38. package/src/mcp.js +37 -20
  39. package/src/owners.js +132 -132
  40. package/src/queries-cli.js +866 -0
  41. package/src/queries.js +1323 -2267
  42. package/src/result-formatter.js +21 -0
  43. package/src/sequence.js +177 -182
  44. package/src/structure.js +200 -199
  45. package/src/test-filter.js +7 -0
  46. package/src/triage.js +120 -162
  47. package/src/viewer.js +1 -1
package/src/flow.js CHANGED
@@ -6,9 +6,11 @@
6
6
  */
7
7
 
8
8
  import { openReadonlyOrFail } from './db.js';
9
- import { paginateResult, printNdjson } from './paginate.js';
10
- import { findMatchingNodes, isTestFile, kindIcon } from './queries.js';
9
+ import { paginateResult } from './paginate.js';
10
+ import { CORE_SYMBOL_KINDS, findMatchingNodes, kindIcon } from './queries.js';
11
+ import { outputResult } from './result-formatter.js';
11
12
  import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
13
+ import { isTestFile } from './test-filter.js';
12
14
 
13
15
  /**
14
16
  * Determine the entry point type from a node name based on framework prefixes.
@@ -35,46 +37,49 @@ export function entryPointType(name) {
35
37
  */
36
38
  export function listEntryPointsData(dbPath, opts = {}) {
37
39
  const db = openReadonlyOrFail(dbPath);
38
- const noTests = opts.noTests || false;
39
-
40
- // Find all framework-prefixed nodes
41
- const prefixConditions = FRAMEWORK_ENTRY_PREFIXES.map(() => 'n.name LIKE ?').join(' OR ');
42
- const prefixParams = FRAMEWORK_ENTRY_PREFIXES.map((p) => `${p}%`);
43
-
44
- let rows = db
45
- .prepare(
46
- `SELECT n.name, n.kind, n.file, n.line, n.role
47
- FROM nodes n
48
- WHERE (
49
- (${prefixConditions})
50
- OR n.role = 'entry'
51
- )
52
- AND n.kind NOT IN ('file', 'directory')
53
- ORDER BY n.name`,
54
- )
55
- .all(...prefixParams);
56
-
57
- if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
58
-
59
- const entries = rows.map((r) => ({
60
- name: r.name,
61
- kind: r.kind,
62
- file: r.file,
63
- line: r.line,
64
- role: r.role,
65
- type: entryPointType(r.name) || (r.role === 'entry' ? 'exported' : null),
66
- }));
67
-
68
- const byType = {};
69
- for (const e of entries) {
70
- const t = e.type || 'other';
71
- if (!byType[t]) byType[t] = [];
72
- byType[t].push(e);
73
- }
40
+ try {
41
+ const noTests = opts.noTests || false;
42
+
43
+ // Find all framework-prefixed nodes
44
+ const prefixConditions = FRAMEWORK_ENTRY_PREFIXES.map(() => 'n.name LIKE ?').join(' OR ');
45
+ const prefixParams = FRAMEWORK_ENTRY_PREFIXES.map((p) => `${p}%`);
46
+
47
+ let rows = db
48
+ .prepare(
49
+ `SELECT n.name, n.kind, n.file, n.line, n.role
50
+ FROM nodes n
51
+ WHERE (
52
+ (${prefixConditions})
53
+ OR n.role = 'entry'
54
+ )
55
+ AND n.kind NOT IN ('file', 'directory')
56
+ ORDER BY n.name`,
57
+ )
58
+ .all(...prefixParams);
59
+
60
+ if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
61
+
62
+ const entries = rows.map((r) => ({
63
+ name: r.name,
64
+ kind: r.kind,
65
+ file: r.file,
66
+ line: r.line,
67
+ role: r.role,
68
+ type: entryPointType(r.name) || (r.role === 'entry' ? 'exported' : null),
69
+ }));
70
+
71
+ const byType = {};
72
+ for (const e of entries) {
73
+ const t = e.type || 'other';
74
+ if (!byType[t]) byType[t] = [];
75
+ byType[t].push(e);
76
+ }
74
77
 
75
- db.close();
76
- const base = { entries, byType, count: entries.length };
77
- return paginateResult(base, 'entries', { limit: opts.limit, offset: opts.offset });
78
+ const base = { entries, byType, count: entries.length };
79
+ return paginateResult(base, 'entries', { limit: opts.limit, offset: opts.offset });
80
+ } finally {
81
+ db.close();
82
+ }
78
83
  }
79
84
 
80
85
  /**
@@ -91,132 +96,136 @@ export function listEntryPointsData(dbPath, opts = {}) {
91
96
  */
92
97
  export function flowData(name, dbPath, opts = {}) {
93
98
  const db = openReadonlyOrFail(dbPath);
94
- const maxDepth = opts.depth || 10;
95
- const noTests = opts.noTests || false;
96
-
97
- // Phase 1: Direct LIKE match on full name
98
- let matchNode = findMatchingNodes(db, name, opts)[0] ?? null;
99
+ try {
100
+ const maxDepth = opts.depth || 10;
101
+ const noTests = opts.noTests || false;
102
+ const flowOpts = { ...opts, kinds: opts.kind ? [opts.kind] : CORE_SYMBOL_KINDS };
103
+
104
+ // Phase 1: Direct LIKE match on full name (use all 10 core symbol kinds,
105
+ // not just FUNCTION_KINDS, so flow can trace from interfaces/types/structs/etc.)
106
+ let matchNode = findMatchingNodes(db, name, flowOpts)[0] ?? null;
107
+
108
+ // Phase 2: Prefix-stripped matching — try adding framework prefixes
109
+ if (!matchNode) {
110
+ for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
111
+ matchNode = findMatchingNodes(db, `${prefix}${name}`, flowOpts)[0] ?? null;
112
+ if (matchNode) break;
113
+ }
114
+ }
99
115
 
100
- // Phase 2: Prefix-stripped matching — try adding framework prefixes
101
- if (!matchNode) {
102
- for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
103
- matchNode = findMatchingNodes(db, `${prefix}${name}`, opts)[0] ?? null;
104
- if (matchNode) break;
116
+ if (!matchNode) {
117
+ return {
118
+ entry: null,
119
+ depth: maxDepth,
120
+ steps: [],
121
+ leaves: [],
122
+ cycles: [],
123
+ totalReached: 0,
124
+ truncated: false,
125
+ };
105
126
  }
106
- }
107
127
 
108
- if (!matchNode) {
109
- db.close();
110
- return {
111
- entry: null,
112
- depth: maxDepth,
113
- steps: [],
114
- leaves: [],
115
- cycles: [],
116
- totalReached: 0,
117
- truncated: false,
128
+ const epType = entryPointType(matchNode.name);
129
+ const entry = {
130
+ name: matchNode.name,
131
+ kind: matchNode.kind,
132
+ file: matchNode.file,
133
+ line: matchNode.line,
134
+ type: epType || 'exported',
135
+ role: matchNode.role,
118
136
  };
119
- }
120
137
 
121
- const epType = entryPointType(matchNode.name);
122
- const entry = {
123
- name: matchNode.name,
124
- kind: matchNode.kind,
125
- file: matchNode.file,
126
- line: matchNode.line,
127
- type: epType || 'exported',
128
- role: matchNode.role,
129
- };
130
-
131
- // Forward BFS through callees
132
- const visited = new Set([matchNode.id]);
133
- let frontier = [matchNode.id];
134
- const steps = [];
135
- const cycles = [];
136
- let truncated = false;
137
-
138
- // Track which nodes are at each depth and their depth for leaf detection
139
- const nodeDepths = new Map();
140
- const idToNode = new Map();
141
- idToNode.set(matchNode.id, entry);
142
-
143
- for (let d = 1; d <= maxDepth; d++) {
144
- const nextFrontier = [];
145
- const levelNodes = [];
146
-
147
- for (const fid of frontier) {
148
- const callees = db
149
- .prepare(
150
- `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.role
151
- FROM edges e JOIN nodes n ON e.target_id = n.id
152
- WHERE e.source_id = ? AND e.kind = 'calls'`,
153
- )
154
- .all(fid);
155
-
156
- for (const c of callees) {
157
- if (noTests && isTestFile(c.file)) continue;
158
-
159
- if (visited.has(c.id)) {
160
- // Cycle detected
161
- const fromNode = idToNode.get(fid);
162
- if (fromNode) {
163
- cycles.push({ from: fromNode.name, to: c.name, depth: d });
138
+ // Forward BFS through callees
139
+ const visited = new Set([matchNode.id]);
140
+ let frontier = [matchNode.id];
141
+ const steps = [];
142
+ const cycles = [];
143
+ let truncated = false;
144
+
145
+ // Track which nodes are at each depth and their depth for leaf detection
146
+ const nodeDepths = new Map();
147
+ const idToNode = new Map();
148
+ idToNode.set(matchNode.id, entry);
149
+
150
+ for (let d = 1; d <= maxDepth; d++) {
151
+ const nextFrontier = [];
152
+ const levelNodes = [];
153
+
154
+ for (const fid of frontier) {
155
+ const callees = db
156
+ .prepare(
157
+ `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.role
158
+ FROM edges e JOIN nodes n ON e.target_id = n.id
159
+ WHERE e.source_id = ? AND e.kind = 'calls'`,
160
+ )
161
+ .all(fid);
162
+
163
+ for (const c of callees) {
164
+ if (noTests && isTestFile(c.file)) continue;
165
+
166
+ if (visited.has(c.id)) {
167
+ // Cycle detected
168
+ const fromNode = idToNode.get(fid);
169
+ if (fromNode) {
170
+ cycles.push({ from: fromNode.name, to: c.name, depth: d });
171
+ }
172
+ continue;
164
173
  }
165
- continue;
166
- }
167
174
 
168
- visited.add(c.id);
169
- nextFrontier.push(c.id);
170
- const nodeInfo = { name: c.name, kind: c.kind, file: c.file, line: c.line };
171
- levelNodes.push(nodeInfo);
172
- nodeDepths.set(c.id, d);
173
- idToNode.set(c.id, nodeInfo);
175
+ visited.add(c.id);
176
+ nextFrontier.push(c.id);
177
+ const nodeInfo = { name: c.name, kind: c.kind, file: c.file, line: c.line };
178
+ levelNodes.push(nodeInfo);
179
+ nodeDepths.set(c.id, d);
180
+ idToNode.set(c.id, nodeInfo);
181
+ }
174
182
  }
175
- }
176
183
 
177
- if (levelNodes.length > 0) {
178
- steps.push({ depth: d, nodes: levelNodes });
179
- }
184
+ if (levelNodes.length > 0) {
185
+ steps.push({ depth: d, nodes: levelNodes });
186
+ }
180
187
 
181
- frontier = nextFrontier;
182
- if (frontier.length === 0) break;
188
+ frontier = nextFrontier;
189
+ if (frontier.length === 0) break;
183
190
 
184
- if (d === maxDepth && frontier.length > 0) {
185
- truncated = true;
191
+ if (d === maxDepth && frontier.length > 0) {
192
+ truncated = true;
193
+ }
186
194
  }
187
- }
188
195
 
189
- // Identify leaves: visited nodes that have no outgoing 'calls' edges to other visited nodes
190
- // (or no outgoing calls at all)
191
- const leaves = [];
192
- for (const [id, depth] of nodeDepths) {
193
- const outgoing = db
194
- .prepare(
195
- `SELECT DISTINCT n.id
196
- FROM edges e JOIN nodes n ON e.target_id = n.id
197
- WHERE e.source_id = ? AND e.kind = 'calls'`,
198
- )
199
- .all(id);
196
+ // Identify leaves: visited nodes that have no outgoing 'calls' edges to other visited nodes
197
+ // (or no outgoing calls at all)
198
+ const leaves = [];
199
+ for (const [id, depth] of nodeDepths) {
200
+ const outgoing = db
201
+ .prepare(
202
+ `SELECT DISTINCT n.id
203
+ FROM edges e JOIN nodes n ON e.target_id = n.id
204
+ WHERE e.source_id = ? AND e.kind = 'calls'`,
205
+ )
206
+ .all(id);
200
207
 
201
- if (outgoing.length === 0) {
202
- const node = idToNode.get(id);
203
- if (node) {
204
- leaves.push({ ...node, depth });
208
+ if (outgoing.length === 0) {
209
+ const node = idToNode.get(id);
210
+ if (node) {
211
+ leaves.push({ ...node, depth });
212
+ }
205
213
  }
206
214
  }
207
- }
208
215
 
209
- db.close();
210
- const base = {
211
- entry,
212
- depth: maxDepth,
213
- steps,
214
- leaves,
215
- cycles,
216
- totalReached: visited.size - 1, // exclude the entry node itself
217
- truncated,
218
- };
219
- return paginateResult(base, 'steps', { limit: opts.limit, offset: opts.offset });
216
+ const base = {
217
+ entry,
218
+ depth: maxDepth,
219
+ steps,
220
+ leaves,
221
+ cycles,
222
+ totalReached: visited.size - 1, // exclude the entry node itself
223
+ truncated,
224
+ };
225
+ return paginateResult(base, 'steps', { limit: opts.limit, offset: opts.offset });
226
+ } finally {
227
+ db.close();
228
+ }
220
229
  }
221
230
 
222
231
  /**
@@ -229,14 +238,7 @@ export function flow(name, dbPath, opts = {}) {
229
238
  limit: opts.limit,
230
239
  offset: opts.offset,
231
240
  });
232
- if (opts.ndjson) {
233
- printNdjson(data, 'entries');
234
- return;
235
- }
236
- if (opts.json) {
237
- console.log(JSON.stringify(data, null, 2));
238
- return;
239
- }
241
+ if (outputResult(data, 'entries', opts)) return;
240
242
  if (data.count === 0) {
241
243
  console.log('No entry points found. Run "codegraph build" first.');
242
244
  return;
@@ -253,10 +255,7 @@ export function flow(name, dbPath, opts = {}) {
253
255
  }
254
256
 
255
257
  const data = flowData(name, dbPath, opts);
256
- if (opts.json) {
257
- console.log(JSON.stringify(data, null, 2));
258
- return;
259
- }
258
+ if (outputResult(data, 'steps', opts)) return;
260
259
 
261
260
  if (!data.entry) {
262
261
  console.log(`No matching entry point or function found for "${name}".`);
package/src/index.js CHANGED
@@ -77,12 +77,24 @@ export {
77
77
  } from './dataflow.js';
78
78
  // Database utilities
79
79
  export {
80
+ countEdges,
81
+ countFiles,
82
+ countNodes,
83
+ fanInJoinSQL,
84
+ fanOutJoinSQL,
80
85
  findDbPath,
86
+ findNodesForTriage,
87
+ findNodesWithFanIn,
81
88
  getBuildMeta,
82
89
  initSchema,
90
+ iterateFunctionNodes,
91
+ kindInClause,
92
+ listFunctionNodes,
93
+ NodeQuery,
83
94
  openDb,
84
95
  openReadonlyOrFail,
85
96
  setBuildMeta,
97
+ testFilterSQL,
86
98
  } from './db.js';
87
99
  // Embeddings
88
100
  export {
@@ -140,7 +152,6 @@ export {
140
152
  FALSE_POSITIVE_CALLER_THRESHOLD,
141
153
  FALSE_POSITIVE_NAMES,
142
154
  fileDepsData,
143
- fileExports,
144
155
  fnDepsData,
145
156
  fnImpactData,
146
157
  impactAnalysisData,
@@ -158,6 +169,24 @@ export {
158
169
  VALID_ROLES,
159
170
  whereData,
160
171
  } from './queries.js';
172
+ // Query CLI display wrappers
173
+ export {
174
+ children,
175
+ context,
176
+ diffImpact,
177
+ explain,
178
+ fileDeps,
179
+ fileExports,
180
+ fnDeps,
181
+ fnImpact,
182
+ impactAnalysis,
183
+ moduleMap,
184
+ queryName,
185
+ roles,
186
+ stats,
187
+ symbolPath,
188
+ where,
189
+ } from './queries-cli.js';
161
190
  // Registry (multi-repo)
162
191
  export {
163
192
  listRepos,
@@ -169,6 +198,8 @@ export {
169
198
  saveRegistry,
170
199
  unregisterRepo,
171
200
  } from './registry.js';
201
+ // Result formatting
202
+ export { outputResult } from './result-formatter.js';
172
203
  // Sequence diagram generation
173
204
  export { sequence, sequenceData, sequenceToMermaid } from './sequence.js';
174
205
  // Snapshot management
@@ -192,6 +223,8 @@ export {
192
223
  moduleBoundariesData,
193
224
  structureData,
194
225
  } from './structure.js';
226
+ // Test file detection
227
+ export { isTestFile, TEST_PATTERN } from './test-filter.js';
195
228
  // Triage — composite risk audit
196
229
  export { triage, triageData } from './triage.js';
197
230
  // Interactive HTML viewer
package/src/kinds.js ADDED
@@ -0,0 +1,49 @@
1
+ // ── Symbol kind constants ───────────────────────────────────────────
2
+ // Original 10 kinds — used as default query scope
3
+ export const CORE_SYMBOL_KINDS = [
4
+ 'function',
5
+ 'method',
6
+ 'class',
7
+ 'interface',
8
+ 'type',
9
+ 'struct',
10
+ 'enum',
11
+ 'trait',
12
+ 'record',
13
+ 'module',
14
+ ];
15
+
16
+ // Sub-declaration kinds (Phase 1)
17
+ export const EXTENDED_SYMBOL_KINDS = [
18
+ 'parameter',
19
+ 'property',
20
+ 'constant',
21
+ // Phase 2 (reserved, not yet extracted):
22
+ // 'constructor', 'namespace', 'decorator', 'getter', 'setter',
23
+ ];
24
+
25
+ // Full set for --kind validation and MCP enum
26
+ export const EVERY_SYMBOL_KIND = [...CORE_SYMBOL_KINDS, ...EXTENDED_SYMBOL_KINDS];
27
+
28
+ // Backward compat: ALL_SYMBOL_KINDS stays as the core 10
29
+ export const ALL_SYMBOL_KINDS = CORE_SYMBOL_KINDS;
30
+
31
+ // ── Edge kind constants ─────────────────────────────────────────────
32
+ // Core edge kinds — coupling and dependency relationships
33
+ export const CORE_EDGE_KINDS = [
34
+ 'imports',
35
+ 'imports-type',
36
+ 'reexports',
37
+ 'calls',
38
+ 'extends',
39
+ 'implements',
40
+ 'contains',
41
+ ];
42
+
43
+ // Structural edge kinds — parent/child and type relationships
44
+ export const STRUCTURAL_EDGE_KINDS = ['parameter_of', 'receiver'];
45
+
46
+ // Full set for MCP enum and validation
47
+ export const EVERY_EDGE_KIND = [...CORE_EDGE_KINDS, ...STRUCTURAL_EDGE_KINDS];
48
+
49
+ export const VALID_ROLES = ['entry', 'core', 'utility', 'adapter', 'dead', 'leaf'];
package/src/manifesto.js CHANGED
@@ -3,7 +3,8 @@ import { loadConfig } from './config.js';
3
3
  import { findCycles } from './cycles.js';
4
4
  import { openReadonlyOrFail } from './db.js';
5
5
  import { debug } from './logger.js';
6
- import { paginateResult, printNdjson } from './paginate.js';
6
+ import { paginateResult } from './paginate.js';
7
+ import { outputResult } from './result-formatter.js';
7
8
 
8
9
  // ─── Rule Definitions ─────────────────────────────────────────────────
9
10
 
@@ -434,13 +435,7 @@ export function manifestoData(customDbPath, opts = {}) {
434
435
  export function manifesto(customDbPath, opts = {}) {
435
436
  const data = manifestoData(customDbPath, opts);
436
437
 
437
- if (opts.ndjson) {
438
- printNdjson(data, 'violations');
439
- if (!data.passed) process.exit(1);
440
- return;
441
- }
442
- if (opts.json) {
443
- console.log(JSON.stringify(data, null, 2));
438
+ if (outputResult(data, 'violations', opts)) {
444
439
  if (!data.passed) process.exit(1);
445
440
  return;
446
441
  }
package/src/mcp.js CHANGED
@@ -836,26 +836,27 @@ export async function startMCPServer(customDbPath, options = {}) {
836
836
  process.exit(1);
837
837
  }
838
838
 
839
- // Lazy import query functions to avoid circular deps at module load
840
- const {
841
- impactAnalysisData,
842
- moduleMapData,
843
- fileDepsData,
844
- exportsData,
845
- fnDepsData,
846
- fnImpactData,
847
- pathData,
848
- contextData,
849
- childrenData,
850
- explainData,
851
- whereData,
852
- diffImpactData,
853
- listFunctionsData,
854
- rolesData,
855
- } = await import('./queries.js');
839
+ // Connect transport FIRST so the server can receive the client's
840
+ // `initialize` request while heavy modules (queries, better-sqlite3)
841
+ // are still loading. These are lazy-loaded on the first tool call
842
+ // and cached for subsequent calls.
843
+ let _queries;
844
+ let _Database;
856
845
 
857
- const require = createRequire(import.meta.url);
858
- const Database = require('better-sqlite3');
846
+ async function getQueries() {
847
+ if (!_queries) {
848
+ _queries = await import('./queries.js');
849
+ }
850
+ return _queries;
851
+ }
852
+
853
+ function getDatabase() {
854
+ if (!_Database) {
855
+ const require = createRequire(import.meta.url);
856
+ _Database = require('better-sqlite3');
857
+ }
858
+ return _Database;
859
+ }
859
860
 
860
861
  const server = new Server(
861
862
  { name: 'codegraph', version: '1.0.0' },
@@ -868,8 +869,24 @@ export async function startMCPServer(customDbPath, options = {}) {
868
869
 
869
870
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
870
871
  const { name, arguments: args } = request.params;
871
-
872
872
  try {
873
+ const {
874
+ impactAnalysisData,
875
+ moduleMapData,
876
+ fileDepsData,
877
+ exportsData,
878
+ fnDepsData,
879
+ fnImpactData,
880
+ pathData,
881
+ contextData,
882
+ childrenData,
883
+ explainData,
884
+ whereData,
885
+ diffImpactData,
886
+ listFunctionsData,
887
+ rolesData,
888
+ } = await getQueries();
889
+ const Database = getDatabase();
873
890
  if (!multiRepo && args.repo) {
874
891
  throw new Error(
875
892
  'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to access other repositories.',