@stupidloud/codegraph 0.7.20 → 0.9.5

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 (155) hide show
  1. package/README.md +127 -106
  2. package/dist/bin/codegraph.d.ts +4 -0
  3. package/dist/bin/codegraph.d.ts.map +1 -1
  4. package/dist/bin/codegraph.js +327 -8
  5. package/dist/bin/codegraph.js.map +1 -1
  6. package/dist/bin/node-version-check.d.ts +17 -0
  7. package/dist/bin/node-version-check.d.ts.map +1 -1
  8. package/dist/bin/node-version-check.js +37 -0
  9. package/dist/bin/node-version-check.js.map +1 -1
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +1 -11
  12. package/dist/config.js.map +1 -1
  13. package/dist/db/index.d.ts +30 -1
  14. package/dist/db/index.d.ts.map +1 -1
  15. package/dist/db/index.js +75 -25
  16. package/dist/db/index.js.map +1 -1
  17. package/dist/db/queries.d.ts +16 -0
  18. package/dist/db/queries.d.ts.map +1 -1
  19. package/dist/db/queries.js +80 -27
  20. package/dist/db/queries.js.map +1 -1
  21. package/dist/db/sqlite-adapter.d.ts +17 -23
  22. package/dist/db/sqlite-adapter.d.ts.map +1 -1
  23. package/dist/db/sqlite-adapter.js +51 -174
  24. package/dist/db/sqlite-adapter.js.map +1 -1
  25. package/dist/extraction/grammars.d.ts +7 -1
  26. package/dist/extraction/grammars.d.ts.map +1 -1
  27. package/dist/extraction/grammars.js +42 -2
  28. package/dist/extraction/grammars.js.map +1 -1
  29. package/dist/extraction/index.d.ts +9 -14
  30. package/dist/extraction/index.d.ts.map +1 -1
  31. package/dist/extraction/index.js +131 -124
  32. package/dist/extraction/index.js.map +1 -1
  33. package/dist/extraction/languages/index.d.ts.map +1 -1
  34. package/dist/extraction/languages/index.js +4 -0
  35. package/dist/extraction/languages/index.js.map +1 -1
  36. package/dist/extraction/languages/lua.d.ts +3 -0
  37. package/dist/extraction/languages/lua.d.ts.map +1 -0
  38. package/dist/extraction/languages/lua.js +150 -0
  39. package/dist/extraction/languages/lua.js.map +1 -0
  40. package/dist/extraction/languages/luau.d.ts +3 -0
  41. package/dist/extraction/languages/luau.d.ts.map +1 -0
  42. package/dist/extraction/languages/luau.js +37 -0
  43. package/dist/extraction/languages/luau.js.map +1 -0
  44. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  45. package/dist/extraction/tree-sitter.js +38 -0
  46. package/dist/extraction/tree-sitter.js.map +1 -1
  47. package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
  48. package/dist/extraction/wasm/tree-sitter-luau.wasm +0 -0
  49. package/dist/extraction/wasm-runtime-flags.d.ts +38 -0
  50. package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -0
  51. package/dist/extraction/wasm-runtime-flags.js +105 -0
  52. package/dist/extraction/wasm-runtime-flags.js.map +1 -0
  53. package/dist/graph/traversal.d.ts.map +1 -1
  54. package/dist/graph/traversal.js +71 -36
  55. package/dist/graph/traversal.js.map +1 -1
  56. package/dist/index.d.ts +11 -5
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +28 -18
  59. package/dist/index.js.map +1 -1
  60. package/dist/installer/config-writer.d.ts.map +1 -1
  61. package/dist/installer/config-writer.js +3 -1
  62. package/dist/installer/config-writer.js.map +1 -1
  63. package/dist/installer/index.d.ts +66 -2
  64. package/dist/installer/index.d.ts.map +1 -1
  65. package/dist/installer/index.js +195 -5
  66. package/dist/installer/index.js.map +1 -1
  67. package/dist/installer/instructions-template.d.ts +2 -2
  68. package/dist/installer/instructions-template.d.ts.map +1 -1
  69. package/dist/installer/instructions-template.js +4 -2
  70. package/dist/installer/instructions-template.js.map +1 -1
  71. package/dist/installer/targets/claude.d.ts +26 -6
  72. package/dist/installer/targets/claude.d.ts.map +1 -1
  73. package/dist/installer/targets/claude.js +165 -10
  74. package/dist/installer/targets/claude.js.map +1 -1
  75. package/dist/installer/targets/cursor.d.ts.map +1 -1
  76. package/dist/installer/targets/cursor.js +57 -3
  77. package/dist/installer/targets/cursor.js.map +1 -1
  78. package/dist/installer/targets/hermes.d.ts +18 -0
  79. package/dist/installer/targets/hermes.d.ts.map +1 -0
  80. package/dist/installer/targets/hermes.js +305 -0
  81. package/dist/installer/targets/hermes.js.map +1 -0
  82. package/dist/installer/targets/registry.d.ts.map +1 -1
  83. package/dist/installer/targets/registry.js +2 -0
  84. package/dist/installer/targets/registry.js.map +1 -1
  85. package/dist/installer/targets/types.d.ts +1 -1
  86. package/dist/installer/targets/types.d.ts.map +1 -1
  87. package/dist/mcp/index.d.ts +12 -0
  88. package/dist/mcp/index.d.ts.map +1 -1
  89. package/dist/mcp/index.js +213 -18
  90. package/dist/mcp/index.js.map +1 -1
  91. package/dist/mcp/server-instructions.d.ts +1 -1
  92. package/dist/mcp/server-instructions.d.ts.map +1 -1
  93. package/dist/mcp/server-instructions.js +15 -0
  94. package/dist/mcp/server-instructions.js.map +1 -1
  95. package/dist/mcp/tools.d.ts +25 -1
  96. package/dist/mcp/tools.d.ts.map +1 -1
  97. package/dist/mcp/tools.js +221 -30
  98. package/dist/mcp/tools.js.map +1 -1
  99. package/dist/mcp/transport.d.ts +17 -0
  100. package/dist/mcp/transport.d.ts.map +1 -1
  101. package/dist/mcp/transport.js +63 -0
  102. package/dist/mcp/transport.js.map +1 -1
  103. package/dist/resolution/frameworks/drupal.d.ts +51 -0
  104. package/dist/resolution/frameworks/drupal.d.ts.map +1 -0
  105. package/dist/resolution/frameworks/drupal.js +335 -0
  106. package/dist/resolution/frameworks/drupal.js.map +1 -0
  107. package/dist/resolution/frameworks/index.d.ts +2 -0
  108. package/dist/resolution/frameworks/index.d.ts.map +1 -1
  109. package/dist/resolution/frameworks/index.js +9 -1
  110. package/dist/resolution/frameworks/index.js.map +1 -1
  111. package/dist/resolution/frameworks/nestjs.d.ts +26 -0
  112. package/dist/resolution/frameworks/nestjs.d.ts.map +1 -0
  113. package/dist/resolution/frameworks/nestjs.js +374 -0
  114. package/dist/resolution/frameworks/nestjs.js.map +1 -0
  115. package/dist/resolution/index.d.ts.map +1 -1
  116. package/dist/resolution/index.js +40 -7
  117. package/dist/resolution/index.js.map +1 -1
  118. package/dist/resolution/lru-cache.d.ts +24 -0
  119. package/dist/resolution/lru-cache.d.ts.map +1 -0
  120. package/dist/resolution/lru-cache.js +62 -0
  121. package/dist/resolution/lru-cache.js.map +1 -0
  122. package/dist/sync/git-hooks.d.ts +45 -0
  123. package/dist/sync/git-hooks.d.ts.map +1 -0
  124. package/dist/sync/git-hooks.js +223 -0
  125. package/dist/sync/git-hooks.js.map +1 -0
  126. package/dist/sync/index.d.ts +4 -0
  127. package/dist/sync/index.d.ts.map +1 -1
  128. package/dist/sync/index.js +12 -1
  129. package/dist/sync/index.js.map +1 -1
  130. package/dist/sync/watch-policy.d.ts +48 -0
  131. package/dist/sync/watch-policy.d.ts.map +1 -0
  132. package/dist/sync/watch-policy.js +124 -0
  133. package/dist/sync/watch-policy.js.map +1 -0
  134. package/dist/sync/watcher.d.ts +2 -4
  135. package/dist/sync/watcher.d.ts.map +1 -1
  136. package/dist/sync/watcher.js +14 -6
  137. package/dist/sync/watcher.js.map +1 -1
  138. package/dist/types.d.ts +1 -1
  139. package/dist/types.d.ts.map +1 -1
  140. package/dist/types.js +11 -0
  141. package/dist/types.js.map +1 -1
  142. package/dist/utils.js +1 -1
  143. package/package.json +4 -4
  144. package/scripts/add-lang/bench.sh +60 -0
  145. package/scripts/add-lang/check-grammar.mjs +75 -0
  146. package/scripts/add-lang/dump-ast.mjs +103 -0
  147. package/scripts/add-lang/verify-extraction.mjs +70 -0
  148. package/scripts/agent-eval/audit.sh +68 -0
  149. package/scripts/agent-eval/itrun.sh +1 -1
  150. package/scripts/agent-eval/run-all.sh +67 -0
  151. package/scripts/build-bundle.sh +118 -0
  152. package/scripts/npm-shim.js +246 -0
  153. package/scripts/pack-npm.sh +95 -0
  154. package/scripts/patch-tree-sitter-dart.js +0 -112
  155. package/scripts/release.sh +0 -68
@@ -8,6 +8,7 @@
8
8
  * Usage:
9
9
  * codegraph Run interactive installer (when no args)
10
10
  * codegraph install Run interactive installer
11
+ * codegraph uninstall Remove CodeGraph from your agents
11
12
  * codegraph init [path] Initialize CodeGraph in a project
12
13
  * codegraph uninit [path] Remove CodeGraph from a project
13
14
  * codegraph index [path] Index all files in the project
@@ -16,6 +17,9 @@
16
17
  * codegraph query <search> Search for symbols
17
18
  * codegraph files [options] Show project file structure
18
19
  * codegraph context <task> Build context for a task
20
+ * codegraph callers <symbol> Find what calls a function/method
21
+ * codegraph callees <symbol> Find what a function/method calls
22
+ * codegraph impact <symbol> Analyze what code is affected by changing a symbol
19
23
  * codegraph affected [files] Find test files affected by changes
20
24
  */
21
25
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -60,6 +64,7 @@ const directory_1 = require("../directory");
60
64
  const shimmer_progress_1 = require("../ui/shimmer-progress");
61
65
  const glyphs_1 = require("../ui/glyphs");
62
66
  const node_version_check_1 = require("./node-version-check");
67
+ const wasm_runtime_flags_1 = require("../extraction/wasm-runtime-flags");
63
68
  // Lazy-load heavy modules (CodeGraph, runInstaller) to keep CLI startup fast.
64
69
  async function loadCodeGraph() {
65
70
  try {
@@ -94,6 +99,22 @@ if (nodeMajor >= 25) {
94
99
  }
95
100
  // Override active — banner shown for visibility, continuing.
96
101
  }
102
+ // Enforce the supported Node floor. `engines` in package.json only *warns* on
103
+ // install (unless engine-strict), so hard-block here to actually keep users off
104
+ // unsupported versions. Mirrors the 25+ block above. See package.json `engines`.
105
+ if (nodeMajor < node_version_check_1.MIN_NODE_MAJOR) {
106
+ process.stderr.write((0, node_version_check_1.buildNodeTooOldBanner)(nodeVersion) + '\n');
107
+ if (!process.env.CODEGRAPH_ALLOW_UNSAFE_NODE) {
108
+ process.exit(1);
109
+ }
110
+ // Override active — banner shown for visibility, continuing.
111
+ }
112
+ // Re-exec with V8's `--liftoff-only` if it isn't already set, so tree-sitter's
113
+ // large WASM grammars never hit the turboshaft Zone OOM (`Fatal process out of
114
+ // memory: Zone`) on Node >= 22. No-op under the bundled launcher, which already
115
+ // passes the flag. Must run before any grammar (in the parse worker, which
116
+ // inherits this process's flags) is compiled. See ../extraction/wasm-runtime-flags.
117
+ (0, wasm_runtime_flags_1.relaunchWithWasmRuntimeFlagsIfNeeded)(__filename);
97
118
  // Check if running with no arguments - run installer
98
119
  if (process.argv.length === 2) {
99
120
  Promise.resolve().then(() => __importStar(require('../installer'))).then(({ runInstaller }) => runInstaller()).catch((err) => {
@@ -402,6 +423,11 @@ function main() {
402
423
  }
403
424
  }
404
425
  catch { /* non-fatal */ }
426
+ try {
427
+ const { offerWatchFallback } = await Promise.resolve().then(() => __importStar(require('../installer')));
428
+ await offerWatchFallback(clack, projectPath);
429
+ }
430
+ catch { /* non-fatal */ }
405
431
  clack.outro('');
406
432
  return;
407
433
  }
@@ -448,6 +474,11 @@ function main() {
448
474
  else {
449
475
  clack.log.info('Run "codegraph index" to index the project');
450
476
  }
477
+ try {
478
+ const { offerWatchFallback } = await Promise.resolve().then(() => __importStar(require('../installer')));
479
+ await offerWatchFallback(clack, projectPath);
480
+ }
481
+ catch { /* non-fatal */ }
451
482
  clack.outro('Done');
452
483
  cg.destroy();
453
484
  }
@@ -486,6 +517,15 @@ function main() {
486
517
  const { default: CodeGraph } = await loadCodeGraph();
487
518
  const cg = CodeGraph.openSync(projectPath);
488
519
  cg.uninitialize();
520
+ // Clean up any git sync hooks we installed (no-op if none / not a repo).
521
+ try {
522
+ const { removeGitSyncHook } = await Promise.resolve().then(() => __importStar(require('../sync/git-hooks')));
523
+ const removed = removeGitSyncHook(projectPath);
524
+ if (removed.installed.length > 0) {
525
+ info(`Removed git ${removed.installed.join(', ')} sync hook${removed.installed.length > 1 ? 's' : ''}`);
526
+ }
527
+ }
528
+ catch { /* non-fatal */ }
489
529
  success(`Removed CodeGraph from ${projectPath}`);
490
530
  }
491
531
  catch (err) {
@@ -637,6 +677,7 @@ function main() {
637
677
  const stats = cg.getStats();
638
678
  const changes = cg.getChangedFiles();
639
679
  const backend = cg.getBackend();
680
+ const journalMode = cg.getJournalMode();
640
681
  // JSON output mode
641
682
  if (options.json) {
642
683
  console.log(JSON.stringify({
@@ -647,6 +688,7 @@ function main() {
647
688
  edgeCount: stats.edgeCount,
648
689
  dbSizeBytes: stats.dbSizeBytes,
649
690
  backend,
691
+ journalMode,
650
692
  nodesByKind: stats.nodesByKind,
651
693
  languages: Object.entries(stats.filesByLanguage).filter(([, count]) => count > 0).map(([lang]) => lang),
652
694
  pendingChanges: {
@@ -668,14 +710,18 @@ function main() {
668
710
  console.log(` Nodes: ${formatNumber(stats.nodeCount)}`);
669
711
  console.log(` Edges: ${formatNumber(stats.edgeCount)}`);
670
712
  console.log(` DB Size: ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`);
671
- // Surface the active SQLite backend so users can spot the silent
672
- // WASM fallback (5-10x slower). better-sqlite3 is in
673
- // `optionalDependencies`, so `npm install` succeeds without it
674
- // when the native build fails.
675
- const backendLabel = backend === 'native'
676
- ? chalk.green('native')
677
- : chalk.yellow(`wasm ${(0, glyphs_1.getGlyphs)().dash} slower fallback; run \`npm rebuild better-sqlite3\``);
713
+ // Surface the active SQLite backend (node:sqlite Node's built-in real
714
+ // SQLite, full WAL + FTS5, no native build).
715
+ const backendLabel = chalk.green(`node:sqlite ${(0, glyphs_1.getGlyphs)().dash} built-in (full WAL)`);
678
716
  console.log(` Backend: ${backendLabel}`);
717
+ // Effective journal mode: 'wal' means concurrent reads never block on a
718
+ // writer; anything else means they can ("database is locked"). node:sqlite
719
+ // supports WAL everywhere, so a non-wal mode means the filesystem can't
720
+ // (network mounts, WSL2 /mnt). See issue #238.
721
+ const journalLabel = journalMode === 'wal'
722
+ ? chalk.green('wal')
723
+ : chalk.yellow(`${journalMode || 'unknown'} ${(0, glyphs_1.getGlyphs)().dash} WAL inactive; reads can block on writes`);
724
+ console.log(` Journal: ${journalLabel}`);
679
725
  console.log();
680
726
  // Node breakdown
681
727
  console.log(chalk.bold('Nodes by Kind:'));
@@ -988,8 +1034,14 @@ function main() {
988
1034
  .description('Start CodeGraph as an MCP server for AI assistants')
989
1035
  .option('-p, --path <path>', 'Project path (optional for MCP mode, uses rootUri from client)')
990
1036
  .option('--mcp', 'Run as MCP server (stdio transport)')
1037
+ .option('--no-watch', 'Disable the file watcher (no auto-sync; useful on slow filesystems like WSL2 /mnt drives)')
991
1038
  .action(async (options) => {
992
1039
  const projectPath = options.path ? resolveProjectPath(options.path) : undefined;
1040
+ // Commander sets watch=false when --no-watch is passed. Route it through
1041
+ // the same env-var chokepoint the watcher and MCP server already honor.
1042
+ if (options.watch === false) {
1043
+ process.env.CODEGRAPH_NO_WATCH = '1';
1044
+ }
993
1045
  try {
994
1046
  if (options.mcp) {
995
1047
  // Start MCP server - it handles initialization lazily based on rootUri from client
@@ -1189,6 +1241,241 @@ function main() {
1189
1241
  process.exit(1);
1190
1242
  }
1191
1243
  });
1244
+ /**
1245
+ * codegraph callers <symbol>
1246
+ *
1247
+ * CLI parity with the MCP graph tools (codegraph_callers/callees/impact) so the
1248
+ * traversal queries work in scripts, CI, and git hooks without a running MCP
1249
+ * server.
1250
+ */
1251
+ program
1252
+ .command('callers <symbol>')
1253
+ .description('Find all functions/methods that call a specific symbol')
1254
+ .option('-p, --path <path>', 'Project path')
1255
+ .option('-l, --limit <number>', 'Maximum results', '20')
1256
+ .option('-j, --json', 'Output as JSON')
1257
+ .action(async (symbol, options) => {
1258
+ const projectPath = resolveProjectPath(options.path);
1259
+ try {
1260
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1261
+ error(`CodeGraph not initialized in ${projectPath}`);
1262
+ process.exit(1);
1263
+ }
1264
+ const { default: CodeGraph } = await loadCodeGraph();
1265
+ const cg = await CodeGraph.open(projectPath);
1266
+ const limit = parseInt(options.limit || '20', 10);
1267
+ const matches = cg.searchNodes(symbol, { limit: 50 });
1268
+ if (matches.length === 0) {
1269
+ info(`Symbol "${symbol}" not found`);
1270
+ cg.destroy();
1271
+ return;
1272
+ }
1273
+ const seen = new Set();
1274
+ const allCallers = [];
1275
+ for (const match of matches) {
1276
+ const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
1277
+ if (!exactMatch && matches.length > 1)
1278
+ continue;
1279
+ for (const c of cg.getCallers(match.node.id)) {
1280
+ if (!seen.has(c.node.id)) {
1281
+ seen.add(c.node.id);
1282
+ allCallers.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
1283
+ }
1284
+ }
1285
+ }
1286
+ // Fallback: if exact filter removed everything, use the top match
1287
+ if (allCallers.length === 0 && matches[0]) {
1288
+ for (const c of cg.getCallers(matches[0].node.id)) {
1289
+ if (!seen.has(c.node.id)) {
1290
+ seen.add(c.node.id);
1291
+ allCallers.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
1292
+ }
1293
+ }
1294
+ }
1295
+ const limited = allCallers.slice(0, limit);
1296
+ if (options.json) {
1297
+ console.log(JSON.stringify({ symbol, callers: limited }, null, 2));
1298
+ }
1299
+ else if (limited.length === 0) {
1300
+ info(`No callers found for "${symbol}"`);
1301
+ }
1302
+ else {
1303
+ console.log(chalk.bold(`\nCallers of "${symbol}" (${limited.length}):\n`));
1304
+ for (const node of limited) {
1305
+ const loc = node.startLine ? `:${node.startLine}` : '';
1306
+ console.log(chalk.cyan(node.kind.padEnd(12)) +
1307
+ chalk.white(node.name));
1308
+ console.log(chalk.dim(` ${node.filePath}${loc}`));
1309
+ console.log();
1310
+ }
1311
+ }
1312
+ cg.destroy();
1313
+ }
1314
+ catch (err) {
1315
+ error(`callers failed: ${err instanceof Error ? err.message : String(err)}`);
1316
+ process.exit(1);
1317
+ }
1318
+ });
1319
+ /**
1320
+ * codegraph callees <symbol>
1321
+ */
1322
+ program
1323
+ .command('callees <symbol>')
1324
+ .description('Find all functions/methods that a specific symbol calls')
1325
+ .option('-p, --path <path>', 'Project path')
1326
+ .option('-l, --limit <number>', 'Maximum results', '20')
1327
+ .option('-j, --json', 'Output as JSON')
1328
+ .action(async (symbol, options) => {
1329
+ const projectPath = resolveProjectPath(options.path);
1330
+ try {
1331
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1332
+ error(`CodeGraph not initialized in ${projectPath}`);
1333
+ process.exit(1);
1334
+ }
1335
+ const { default: CodeGraph } = await loadCodeGraph();
1336
+ const cg = await CodeGraph.open(projectPath);
1337
+ const limit = parseInt(options.limit || '20', 10);
1338
+ const matches = cg.searchNodes(symbol, { limit: 50 });
1339
+ if (matches.length === 0) {
1340
+ info(`Symbol "${symbol}" not found`);
1341
+ cg.destroy();
1342
+ return;
1343
+ }
1344
+ const seen = new Set();
1345
+ const allCallees = [];
1346
+ for (const match of matches) {
1347
+ const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
1348
+ if (!exactMatch && matches.length > 1)
1349
+ continue;
1350
+ for (const c of cg.getCallees(match.node.id)) {
1351
+ if (!seen.has(c.node.id)) {
1352
+ seen.add(c.node.id);
1353
+ allCallees.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
1354
+ }
1355
+ }
1356
+ }
1357
+ if (allCallees.length === 0 && matches[0]) {
1358
+ for (const c of cg.getCallees(matches[0].node.id)) {
1359
+ if (!seen.has(c.node.id)) {
1360
+ seen.add(c.node.id);
1361
+ allCallees.push({ name: c.node.name, kind: c.node.kind, filePath: c.node.filePath, startLine: c.node.startLine });
1362
+ }
1363
+ }
1364
+ }
1365
+ const limited = allCallees.slice(0, limit);
1366
+ if (options.json) {
1367
+ console.log(JSON.stringify({ symbol, callees: limited }, null, 2));
1368
+ }
1369
+ else if (limited.length === 0) {
1370
+ info(`No callees found for "${symbol}"`);
1371
+ }
1372
+ else {
1373
+ console.log(chalk.bold(`\nCallees of "${symbol}" (${limited.length}):\n`));
1374
+ for (const node of limited) {
1375
+ const loc = node.startLine ? `:${node.startLine}` : '';
1376
+ console.log(chalk.cyan(node.kind.padEnd(12)) +
1377
+ chalk.white(node.name));
1378
+ console.log(chalk.dim(` ${node.filePath}${loc}`));
1379
+ console.log();
1380
+ }
1381
+ }
1382
+ cg.destroy();
1383
+ }
1384
+ catch (err) {
1385
+ error(`callees failed: ${err instanceof Error ? err.message : String(err)}`);
1386
+ process.exit(1);
1387
+ }
1388
+ });
1389
+ /**
1390
+ * codegraph impact <symbol>
1391
+ */
1392
+ program
1393
+ .command('impact <symbol>')
1394
+ .description('Analyze what code is affected by changing a symbol')
1395
+ .option('-p, --path <path>', 'Project path')
1396
+ .option('-d, --depth <number>', 'Traversal depth', '2')
1397
+ .option('-j, --json', 'Output as JSON')
1398
+ .action(async (symbol, options) => {
1399
+ const projectPath = resolveProjectPath(options.path);
1400
+ try {
1401
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1402
+ error(`CodeGraph not initialized in ${projectPath}`);
1403
+ process.exit(1);
1404
+ }
1405
+ const { default: CodeGraph } = await loadCodeGraph();
1406
+ const cg = await CodeGraph.open(projectPath);
1407
+ const depth = Math.min(Math.max(parseInt(options.depth || '2', 10), 1), 10);
1408
+ const matches = cg.searchNodes(symbol, { limit: 50 });
1409
+ if (matches.length === 0) {
1410
+ info(`Symbol "${symbol}" not found`);
1411
+ cg.destroy();
1412
+ return;
1413
+ }
1414
+ // Merge impact subgraphs across all exact-matching symbols
1415
+ const mergedNodes = new Map();
1416
+ const seenEdges = new Set();
1417
+ let edgeCount = 0;
1418
+ for (const match of matches) {
1419
+ const exactMatch = match.node.name === symbol || match.node.name.endsWith(`.${symbol}`) || match.node.name.endsWith(`::${symbol}`);
1420
+ if (!exactMatch && matches.length > 1)
1421
+ continue;
1422
+ const impact = cg.getImpactRadius(match.node.id, depth);
1423
+ for (const [id, n] of impact.nodes) {
1424
+ mergedNodes.set(id, { name: n.name, kind: n.kind, filePath: n.filePath, startLine: n.startLine });
1425
+ }
1426
+ for (const e of impact.edges) {
1427
+ const key = `${e.source}->${e.target}:${e.kind}`;
1428
+ if (!seenEdges.has(key)) {
1429
+ seenEdges.add(key);
1430
+ edgeCount++;
1431
+ }
1432
+ }
1433
+ }
1434
+ // Fallback to top match if exact filter removed everything
1435
+ if (mergedNodes.size === 0 && matches[0]) {
1436
+ const impact = cg.getImpactRadius(matches[0].node.id, depth);
1437
+ for (const [id, n] of impact.nodes) {
1438
+ mergedNodes.set(id, { name: n.name, kind: n.kind, filePath: n.filePath, startLine: n.startLine });
1439
+ }
1440
+ edgeCount = impact.edges.length;
1441
+ }
1442
+ if (options.json) {
1443
+ console.log(JSON.stringify({
1444
+ symbol,
1445
+ depth,
1446
+ nodeCount: mergedNodes.size,
1447
+ edgeCount,
1448
+ affected: Array.from(mergedNodes.values()),
1449
+ }, null, 2));
1450
+ }
1451
+ else if (mergedNodes.size === 0) {
1452
+ info(`No affected symbols found for "${symbol}"`);
1453
+ }
1454
+ else {
1455
+ console.log(chalk.bold(`\nImpact of changing "${symbol}" — ${mergedNodes.size} affected symbols:\n`));
1456
+ // Group by file
1457
+ const byFile = new Map();
1458
+ for (const node of mergedNodes.values()) {
1459
+ const list = byFile.get(node.filePath) || [];
1460
+ list.push({ name: node.name, kind: node.kind, startLine: node.startLine });
1461
+ byFile.set(node.filePath, list);
1462
+ }
1463
+ for (const [file, nodes] of byFile) {
1464
+ console.log(chalk.cyan(file));
1465
+ for (const node of nodes) {
1466
+ const loc = node.startLine ? `:${node.startLine}` : '';
1467
+ console.log(` ${chalk.dim(node.kind.padEnd(12))}${node.name}${chalk.dim(loc)}`);
1468
+ }
1469
+ console.log();
1470
+ }
1471
+ }
1472
+ cg.destroy();
1473
+ }
1474
+ catch (err) {
1475
+ error(`impact failed: ${err instanceof Error ? err.message : String(err)}`);
1476
+ process.exit(1);
1477
+ }
1478
+ });
1192
1479
  /**
1193
1480
  * codegraph affected [files...]
1194
1481
  *
@@ -1324,7 +1611,7 @@ function main() {
1324
1611
  */
1325
1612
  program
1326
1613
  .command('install')
1327
- .description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode)')
1614
+ .description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)')
1328
1615
  .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt')
1329
1616
  .option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
1330
1617
  .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on')
@@ -1373,6 +1660,38 @@ function main() {
1373
1660
  process.exit(1);
1374
1661
  }
1375
1662
  });
1663
+ /**
1664
+ * codegraph uninstall
1665
+ *
1666
+ * Inverse of `install`. Removes the codegraph MCP server entry,
1667
+ * instructions block, and permissions from every agent (or a
1668
+ * `--target` subset). Prompts global-vs-local when not given. Does NOT
1669
+ * delete the `.codegraph/` index — that's `codegraph uninit`.
1670
+ */
1671
+ program
1672
+ .command('uninstall')
1673
+ .description('Remove codegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)')
1674
+ .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "all". Default: all')
1675
+ .option('-l, --location <where>', 'Uninstall location: "global" or "local". Default: prompt')
1676
+ .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=all')
1677
+ .action(async (opts) => {
1678
+ const { runUninstaller } = await Promise.resolve().then(() => __importStar(require('../installer')));
1679
+ if (opts.location && opts.location !== 'global' && opts.location !== 'local') {
1680
+ error(`--location must be "global" or "local" (got "${opts.location}").`);
1681
+ process.exit(1);
1682
+ }
1683
+ try {
1684
+ await runUninstaller({
1685
+ target: opts.target,
1686
+ location: opts.location,
1687
+ yes: opts.yes,
1688
+ });
1689
+ }
1690
+ catch (err) {
1691
+ error(err instanceof Error ? err.message : String(err));
1692
+ process.exit(1);
1693
+ }
1694
+ });
1376
1695
  // Parse and run
1377
1696
  program.parse();
1378
1697
  } // end main()