@codragraph/cli 1.6.4 → 2.1.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.
Files changed (105) hide show
  1. package/README.md +34 -0
  2. package/dist/_shared/cgdb/schema-constants.d.ts +16 -0
  3. package/dist/_shared/cgdb/schema-constants.d.ts.map +1 -0
  4. package/dist/_shared/cgdb/schema-constants.js +67 -0
  5. package/dist/_shared/cgdb/schema-constants.js.map +1 -0
  6. package/dist/_shared/index.d.ts +2 -2
  7. package/dist/_shared/index.js +1 -1
  8. package/dist/cli/analyze.d.ts +22 -0
  9. package/dist/cli/analyze.js +109 -6
  10. package/dist/cli/compress-stats.d.ts +29 -0
  11. package/dist/cli/compress-stats.js +97 -0
  12. package/dist/cli/graphstore.d.ts +6 -2
  13. package/dist/cli/graphstore.js +45 -23
  14. package/dist/cli/index-repo.js +3 -3
  15. package/dist/cli/index.js +16 -2
  16. package/dist/cli/profile-heap.d.ts +35 -0
  17. package/dist/cli/profile-heap.js +126 -0
  18. package/dist/cli/setup.d.ts +13 -0
  19. package/dist/cli/setup.js +22 -11
  20. package/dist/cli/skill-gen.d.ts +14 -2
  21. package/dist/cli/skill-gen.js +52 -19
  22. package/dist/cli/tool.js +4 -0
  23. package/dist/cli/wiki.js +3 -3
  24. package/dist/core/augmentation/engine.js +7 -7
  25. package/dist/core/cgdb/cgdb-adapter.d.ts +176 -0
  26. package/dist/core/cgdb/cgdb-adapter.js +1320 -0
  27. package/dist/core/cgdb/content-read.d.ts +46 -0
  28. package/dist/core/cgdb/content-read.js +64 -0
  29. package/dist/core/cgdb/csv-generator.d.ts +29 -0
  30. package/dist/core/cgdb/csv-generator.js +492 -0
  31. package/dist/core/cgdb/pool-adapter.d.ts +93 -0
  32. package/dist/core/cgdb/pool-adapter.js +550 -0
  33. package/dist/core/cgdb/schema.d.ts +62 -0
  34. package/dist/core/cgdb/schema.js +502 -0
  35. package/dist/core/embeddings/embedding-pipeline.js +27 -10
  36. package/dist/core/graphstore/cgdb-row-source.d.ts +19 -0
  37. package/dist/core/graphstore/cgdb-row-source.js +141 -0
  38. package/dist/core/graphstore/index.d.ts +1 -1
  39. package/dist/core/graphstore/index.js +3 -3
  40. package/dist/core/group/bridge-db.d.ts +2 -2
  41. package/dist/core/group/bridge-db.js +123 -36
  42. package/dist/core/group/bridge-schema.d.ts +4 -4
  43. package/dist/core/group/bridge-schema.js +4 -4
  44. package/dist/core/group/cross-impact.js +3 -3
  45. package/dist/core/group/sync.js +4 -4
  46. package/dist/core/lbug/content-read.d.ts +46 -0
  47. package/dist/core/lbug/content-read.js +64 -0
  48. package/dist/core/lbug/csv-generator.d.ts +2 -6
  49. package/dist/core/lbug/csv-generator.js +45 -12
  50. package/dist/core/lbug/lbug-adapter.d.ts +4 -1
  51. package/dist/core/lbug/lbug-adapter.js +153 -21
  52. package/dist/core/lbug/schema.d.ts +7 -7
  53. package/dist/core/lbug/schema.js +18 -0
  54. package/dist/core/run-analyze.d.ts +13 -0
  55. package/dist/core/run-analyze.js +114 -27
  56. package/dist/core/search/bm25-index.d.ts +3 -3
  57. package/dist/core/search/bm25-index.js +75 -23
  58. package/dist/core/search/hybrid-search.js +2 -2
  59. package/dist/core/wiki/generator.d.ts +2 -2
  60. package/dist/core/wiki/generator.js +4 -4
  61. package/dist/core/wiki/graph-queries.d.ts +2 -2
  62. package/dist/core/wiki/graph-queries.js +5 -5
  63. package/dist/mcp/core/cgdb-adapter.d.ts +5 -0
  64. package/dist/mcp/core/cgdb-adapter.js +5 -0
  65. package/dist/mcp/core/embedder.js +1 -1
  66. package/dist/mcp/local/local-backend.d.ts +2 -2
  67. package/dist/mcp/local/local-backend.js +36 -19
  68. package/dist/mcp/server.js +3 -3
  69. package/dist/mcp/tools.js +1 -1
  70. package/dist/server/analyze-worker.js +2 -2
  71. package/dist/server/api.js +34 -33
  72. package/dist/storage/repo-manager.d.ts +42 -3
  73. package/dist/storage/repo-manager.js +23 -4
  74. package/hooks/claude/codragraph-hook.cjs +98 -5
  75. package/package.json +4 -4
  76. package/scripts/build-tree-sitter-proto.cjs +15 -3
  77. package/scripts/build.js +8 -9
  78. package/scripts/patch-tree-sitter-swift.cjs +17 -4
  79. package/skills/codragraph-api-surface.md +110 -0
  80. package/skills/codragraph-config-audit.md +146 -0
  81. package/skills/codragraph-cross-repo-impact.md +135 -0
  82. package/skills/codragraph-data-lineage.md +137 -0
  83. package/skills/codragraph-dead-code.md +119 -0
  84. package/skills/codragraph-gh-actions-debug.md +162 -0
  85. package/skills/codragraph-gh-issue-workflow.md +178 -0
  86. package/skills/codragraph-gh-pr-workflow.md +176 -0
  87. package/skills/codragraph-gh-release-workflow.md +187 -0
  88. package/skills/codragraph-git-bisect.md +176 -0
  89. package/skills/codragraph-git-force-push.md +147 -0
  90. package/skills/codragraph-git-history-rewrite.md +174 -0
  91. package/skills/codragraph-git-rebase-vs-merge.md +138 -0
  92. package/skills/codragraph-git-recovery.md +181 -0
  93. package/skills/codragraph-git-worktree.md +145 -0
  94. package/skills/codragraph-migration-tracking.md +130 -0
  95. package/skills/codragraph-notebook-context.md +136 -0
  96. package/skills/codragraph-observability-coverage.md +125 -0
  97. package/skills/codragraph-onboarding.md +129 -0
  98. package/skills/codragraph-perf-hotspots.md +132 -0
  99. package/skills/codragraph-project-switcher.md +116 -0
  100. package/skills/codragraph-security-audit.md +144 -0
  101. package/skills/codragraph-sql-tracing.md +122 -0
  102. package/skills/codragraph-supply-chain-audit.md +153 -0
  103. package/skills/codragraph-test-coverage.md +97 -0
  104. package/vendor/tree-sitter-proto/bindings/node/index.js +3 -3
  105. package/vendor/tree-sitter-proto/src/node-types.json +1 -1
@@ -12,7 +12,7 @@ import fs from 'node:fs/promises';
12
12
  import { FsCAS, createBranch, createCommit, DEFAULT_BRANCH, deleteBranch, diffSemantic, diffSnapshots, gc as runGc, getJson, listBranches, materializeSnapshot, parseObjectId, readCommit, readHead, resolveHeadCommit, setHead, threeWayMerge, walkCommits, writeHeadBranch, writeHeadDetached, } from '@codragraph/graphstore';
13
13
  import { findRepo, loadMeta, saveMeta } from '../storage/repo-manager.js';
14
14
  import { GRAPHSTORE_SUBDIR } from '../core/graphstore/index.js';
15
- import { initLbug, closeLbug } from '../core/lbug/lbug-adapter.js';
15
+ import { initCgdb, closeCgdb } from '../core/cgdb/cgdb-adapter.js';
16
16
  import { recordAnalysisSnapshot } from '../core/graphstore/index.js';
17
17
  import { getCurrentCommit, hasGitDir } from '../storage/git.js';
18
18
  const resolveGraphstore = async (cwd) => {
@@ -84,7 +84,7 @@ export const branchListCommand = async () => {
84
84
  // ──────────────────────────────────────────────────────────────────────
85
85
  // codragraph diff <from> <to>
86
86
  // ──────────────────────────────────────────────────────────────────────
87
- export const diffCommand = async (from, to) => {
87
+ export const diffCommand = async (from, to, opts = {}) => {
88
88
  const ctx = await resolveGraphstore(process.cwd());
89
89
  const fromCommitId = await resolveCommitTarget(ctx, from);
90
90
  const toCommitId = await resolveCommitTarget(ctx, to);
@@ -95,6 +95,17 @@ export const diffCommand = async (from, to) => {
95
95
  from: fromCommit.snapshot,
96
96
  to: toCommit.snapshot,
97
97
  });
98
+ // --json: emit a machine-readable payload for downstream consumers
99
+ // (GitHub Action comment formatter, IDE plugins, etc). Keep human and
100
+ // JSON paths separate — never sneak JSON into the human path's stdout.
101
+ if (opts.json) {
102
+ process.stdout.write(JSON.stringify({
103
+ from: { commit: fromCommitId, message: fromCommit.message },
104
+ to: { commit: toCommitId, message: toCommit.message },
105
+ diff,
106
+ }, null, 2) + '\n');
107
+ return;
108
+ }
98
109
  process.stdout.write(`From: ${fromCommitId.slice(7, 7 + 12)} ${fromCommit.message}\n`);
99
110
  process.stdout.write(`To: ${toCommitId.slice(7, 7 + 12)} ${toCommit.message}\n\n`);
100
111
  let totalAdded = 0;
@@ -153,7 +164,7 @@ export const commitCommand = async (opts = {}) => {
153
164
  const repo = await findRepo(process.cwd());
154
165
  if (!repo)
155
166
  throw new Error('No CodraGraph index found');
156
- await initLbug(repo.lbugPath);
167
+ await initCgdb(repo.cgdbPath);
157
168
  try {
158
169
  const result = await recordAnalysisSnapshot({
159
170
  storagePath: repo.storagePath,
@@ -180,7 +191,7 @@ export const commitCommand = async (opts = {}) => {
180
191
  }
181
192
  }
182
193
  finally {
183
- await closeLbug();
194
+ await closeCgdb();
184
195
  }
185
196
  // Avoid unused warnings while keeping the import live for future callers.
186
197
  void ctx;
@@ -210,10 +221,10 @@ export const branchCreateCommand = async (name, opts = {}) => {
210
221
  // ──────────────────────────────────────────────────────────────────────
211
222
  //
212
223
  // Two modes:
213
- // - default : just move HEAD (no lbug rewrite). Cheap; good for
224
+ // - default : just move HEAD (no cgdb rewrite). Cheap; good for
214
225
  // log/diff inspection from a different vantage point.
215
226
  // - --materialize : also rebuild the live LadybugDB from the target
216
- // snapshot. Destructive of the current lbug state —
227
+ // snapshot. Destructive of the current cgdb state —
217
228
  // run `commit` first if you have unsaved changes.
218
229
  export const checkoutCommand = async (target, opts = {}) => {
219
230
  const ctx = await resolveGraphstore(process.cwd());
@@ -236,9 +247,9 @@ export const checkoutCommand = async (target, opts = {}) => {
236
247
  throw new Error('No CodraGraph index found');
237
248
  const commit = await readCommit(ctx.cas, commitId);
238
249
  process.stdout.write(`materializing snapshot ${commit.snapshot.slice(7, 7 + 12)}...\n`);
239
- // Wipe and reinit lbug.
240
- await closeLbug();
241
- for (const f of [repo.lbugPath, `${repo.lbugPath}.wal`, `${repo.lbugPath}.lock`]) {
250
+ // Wipe and reinit cgdb.
251
+ await closeCgdb();
252
+ for (const f of [repo.cgdbPath, `${repo.cgdbPath}.wal`, `${repo.cgdbPath}.lock`]) {
242
253
  try {
243
254
  await fs.rm(f, { recursive: true, force: true });
244
255
  }
@@ -246,9 +257,9 @@ export const checkoutCommand = async (target, opts = {}) => {
246
257
  /* swallow */
247
258
  }
248
259
  }
249
- await initLbug(repo.lbugPath);
260
+ await initCgdb(repo.cgdbPath);
250
261
  try {
251
- const sink = await createLbugRowSinkForCheckout(repo.lbugPath);
262
+ const sink = await createCgdbRowSinkForCheckout(repo.cgdbPath);
252
263
  const result = await materializeSnapshot({
253
264
  cas: ctx.cas,
254
265
  snapshotId: commit.snapshot,
@@ -258,14 +269,14 @@ export const checkoutCommand = async (target, opts = {}) => {
258
269
  `${result.stats.edgeRowCount} edges\n`);
259
270
  }
260
271
  finally {
261
- await closeLbug();
272
+ await closeCgdb();
262
273
  }
263
274
  }
264
275
  };
265
276
  // ──────────────────────────────────────────────────────────────────────
266
277
  // codragraph materialize <target> --into <path>
267
278
  // ──────────────────────────────────────────────────────────────────────
268
- // Read-only inspection: rebuild a snapshot into a fresh sibling lbug
279
+ // Read-only inspection: rebuild a snapshot into a fresh sibling cgdb
269
280
  // without touching the live one. Useful for "let me query the graph as
270
281
  // it was at commit X" without disturbing current work.
271
282
  export const materializeCommand = async (target, opts) => {
@@ -284,9 +295,9 @@ export const materializeCommand = async (target, opts) => {
284
295
  catch {
285
296
  /* doesn't exist, good */
286
297
  }
287
- await initLbug(into);
298
+ await initCgdb(into);
288
299
  try {
289
- const sink = await createLbugRowSinkForCheckout(into);
300
+ const sink = await createCgdbRowSinkForCheckout(into);
290
301
  const result = await materializeSnapshot({
291
302
  cas: ctx.cas,
292
303
  snapshotId: commit.snapshot,
@@ -299,7 +310,7 @@ export const materializeCommand = async (target, opts) => {
299
310
  `edges: ${result.stats.edgeRowCount}\n`);
300
311
  }
301
312
  finally {
302
- await closeLbug();
313
+ await closeCgdb();
303
314
  }
304
315
  };
305
316
  // ──────────────────────────────────────────────────────────────────────
@@ -375,7 +386,7 @@ const locateSymbolHash = (manifest, symbolId, tableHint) => {
375
386
  return null;
376
387
  };
377
388
  // ──────────────────────────────────────────────────────────────────────
378
- // LbugRowSink — used by checkout --materialize and `materialize` command.
389
+ // CgdbRowSink — used by checkout --materialize and `materialize` command.
379
390
  // ──────────────────────────────────────────────────────────────────────
380
391
  //
381
392
  // Bulk-loads rows back into a fresh LadybugDB instance. Phase 4 keeps it
@@ -383,9 +394,9 @@ const locateSymbolHash = (manifest, symbolId, tableHint) => {
383
394
  // `executeWithReusedStatement` helper. For typical repos (≤100k rows)
384
395
  // this finishes in seconds; CSV-based bulk loading is a Phase 4.5
385
396
  // optimization once we measure where the bottleneck actually is.
386
- const createLbugRowSinkForCheckout = async (lbugPath) => {
387
- const { executeQuery } = await import('../core/lbug/lbug-adapter.js');
388
- const { SCHEMA_QUERIES } = await import('../core/lbug/schema.js');
397
+ const createCgdbRowSinkForCheckout = async (cgdbPath) => {
398
+ const { executeQuery } = await import('../core/cgdb/cgdb-adapter.js');
399
+ const { SCHEMA_QUERIES } = await import('../core/cgdb/schema.js');
389
400
  // Recreate the schema; the path was just wiped, so this is a clean install.
390
401
  for (const ddl of SCHEMA_QUERIES) {
391
402
  try {
@@ -413,7 +424,7 @@ const createLbugRowSinkForCheckout = async (lbugPath) => {
413
424
  endEdges: async () => { },
414
425
  finalize: async () => { },
415
426
  };
416
- void lbugPath;
427
+ void cgdbPath;
417
428
  return sink;
418
429
  };
419
430
  const insertNode = async (executeQuery, table, row) => {
@@ -457,7 +468,7 @@ const cypherLiteral = (v) => {
457
468
  return JSON.stringify(v);
458
469
  if (typeof v === 'number' || typeof v === 'boolean')
459
470
  return String(v);
460
- // Fall back to JSON for arrays / nested objects; the underlying lbug
471
+ // Fall back to JSON for arrays / nested objects; the underlying cgdb
461
472
  // schema is mostly scalar so this is rare.
462
473
  return JSON.stringify(v);
463
474
  };
@@ -581,7 +592,7 @@ const formatBytes = (n) => {
581
592
  // classified modifications, added/removed APIs, and process changes. We
582
593
  // expose it as a separate module-local helper so the CLI handler can
583
594
  // dispatch on the flag.
584
- export const diffSemanticCommand = async (from, to) => {
595
+ export const diffSemanticCommand = async (from, to, opts = {}) => {
585
596
  const ctx = await resolveGraphstore(process.cwd());
586
597
  const fromCommit = await readCommit(ctx.cas, await resolveCommitTarget(ctx, from));
587
598
  const toCommit = await readCommit(ctx.cas, await resolveCommitTarget(ctx, to));
@@ -590,6 +601,17 @@ export const diffSemanticCommand = async (from, to) => {
590
601
  from: fromCommit.snapshot,
591
602
  to: toCommit.snapshot,
592
603
  });
604
+ // --json: same shape as diff (plain) but with the semantic payload. The
605
+ // PR-review GitHub Action consumes this directly to render the Markdown
606
+ // comment without parsing free-form text.
607
+ if (opts.json) {
608
+ process.stdout.write(JSON.stringify({
609
+ from: { ref: from, message: fromCommit.message },
610
+ to: { ref: to, message: toCommit.message },
611
+ semantic: d,
612
+ }, null, 2) + '\n');
613
+ return;
614
+ }
593
615
  process.stdout.write(`From: ${from} (${fromCommit.message})\n`);
594
616
  process.stdout.write(`To: ${to} (${toCommit.message})\n\n`);
595
617
  if (d.addedAPIs.length > 0) {
@@ -49,7 +49,7 @@ export const indexCommand = async (inputPathParts, options) => {
49
49
  process.exitCode = 1;
50
50
  return;
51
51
  }
52
- const { storagePath, lbugPath } = getStoragePaths(repoPath);
52
+ const { storagePath, cgdbPath } = getStoragePaths(repoPath);
53
53
  // ── Verify .codragraph/ exists ──────────────────────────────────────
54
54
  try {
55
55
  await fs.access(storagePath);
@@ -60,9 +60,9 @@ export const indexCommand = async (inputPathParts, options) => {
60
60
  process.exitCode = 1;
61
61
  return;
62
62
  }
63
- // ── Verify lbug database exists ───────────────────────────────────
63
+ // ── Verify cgdb database exists ───────────────────────────────────
64
64
  try {
65
- await fs.access(lbugPath);
65
+ await fs.access(cgdbPath);
66
66
  }
67
67
  catch {
68
68
  console.log(` .codragraph/ folder exists but contains no LadybugDB index.`);
package/dist/cli/index.js CHANGED
@@ -19,6 +19,7 @@ program
19
19
  .option('-f, --force', 'Force full re-index even if up to date')
20
20
  .option('--embeddings', 'Enable embedding generation for semantic search (off by default)')
21
21
  .option('--skills', 'Generate repo-specific skill files from detected communities')
22
+ .option('--skill-targets <list>', 'CSV of editor targets for --skills (claude, cursor, opencode, codex). Default: claude.')
22
23
  .option('--skip-agents-md', 'Skip updating the codragraph section in AGENTS.md and CLAUDE.md')
23
24
  .option('--no-stats', 'Omit volatile file/symbol counts from AGENTS.md and CLAUDE.md')
24
25
  .option('--skip-git', 'Index a folder without requiring a .git directory')
@@ -28,12 +29,24 @@ program
28
29
  'Leaves `-r <name>` ambiguous for the two paths; use -r <path> to disambiguate.')
29
30
  .option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)')
30
31
  .option('--max-file-size <kb>', 'Skip files larger than this (KB). Default: 512. Hard cap: 32768 (tree-sitter limit).')
32
+ .option('--no-setup', 'Skip the first-run editor setup (auto-runs once when ~/.codragraph/registry.json is missing)')
33
+ .option('--compress <encoding>', 'Compress per-row content (RFC 0001 Phase 2). One of: none (default), brotli, zstd. zstd requires Node ≥ 22.15.', 'none')
31
34
  .addHelpText('after', '\nEnvironment variables:\n' +
32
35
  ' CODRAGRAPH_NO_GITIGNORE=1 Skip .gitignore parsing (still reads .codragraphignore)\n' +
33
36
  ' CODRAGRAPH_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n' +
34
37
  '\nTip: `.codragraphignore` supports `.gitignore`-style negation. Add e.g.\n' +
35
38
  ' `!__tests__/` to index a directory that is auto-filtered by default (#771).')
36
39
  .action(createLazyAction(() => import('./analyze.js'), 'analyzeCommand'));
40
+ program
41
+ .command('profile-heap [path]')
42
+ .description('Run analyze with heap-profile instrumentation (RFC 0002 Phase 1). ' +
43
+ 'Writes per-phase v8 heap snapshots + a JSONL RSS timeline under ' +
44
+ '.codragraph/heap-profiles/, then prints a summary table.')
45
+ .option('-f, --force', 'Force full re-index (analyze flag, passed through)')
46
+ .option('--skip-git', 'Index a folder without requiring a .git directory')
47
+ .option('--no-setup', 'Skip first-run editor setup')
48
+ .option('--no-summary', 'Skip the post-run summary table (raw artifacts only)')
49
+ .action(createLazyAction(() => import('./profile-heap.js'), 'profileHeapCommand'));
37
50
  program
38
51
  .command('index [path...]')
39
52
  .description('Register an existing .codragraph/ folder into the global registry (no re-analysis needed)')
@@ -192,12 +205,13 @@ program
192
205
  .command('diff <from> <to>')
193
206
  .description('Structural diff between two graph commits or branches')
194
207
  .option('--semantic', 'Use the semantic differ (added APIs, classified modifications, processes)')
208
+ .option('--json', 'Emit machine-readable JSON instead of human-readable text (for CI / GitHub Action consumers)')
195
209
  .action(async (from, to, opts) => {
196
210
  const mod = await import('./graphstore.js');
197
211
  if (opts.semantic)
198
- await mod.diffSemanticCommand(from, to);
212
+ await mod.diffSemanticCommand(from, to, { json: opts.json });
199
213
  else
200
- await mod.diffCommand(from, to);
214
+ await mod.diffCommand(from, to, { json: opts.json });
201
215
  });
202
216
  program
203
217
  .command('merge <branch>')
@@ -0,0 +1,35 @@
1
+ /**
2
+ * profile-heap — RFC 0002 Phase 1 entry point.
3
+ *
4
+ * A thin wrapper around `analyze` that flips on the heap-profile
5
+ * instrumentation already living in `runFullAnalysis`, then prints a
6
+ * per-phase RSS / heapUsed summary table after the run finishes.
7
+ *
8
+ * Why a dedicated subcommand instead of just documenting the env var?
9
+ * - Discoverability: `codragraph --help` lists it next to `analyze`.
10
+ * - One-shot UX: users (and the maintainer) get a useful summary table
11
+ * without having to spelunk through Chrome DevTools to compare
12
+ * snapshots. The `.heapsnapshot` files are still written for deep
13
+ * dives; the summary just makes the cheap signal (RSS curve, heapUsed
14
+ * curve) visible at a glance.
15
+ * - Phase 1 of RFC 0002 is profile-first by design — we ship the tool
16
+ * before any mitigation. Don't add compression, eviction, or streaming
17
+ * refactors here; that's Phase 2+ once we know which phase is the
18
+ * actual bottleneck.
19
+ *
20
+ * Side effects: writes `.codragraph/heap-profiles/<ts>-<phase>.heapsnapshot`
21
+ * (one per phase boundary, ~100-500MB each) plus a small
22
+ * `profile-summary.jsonl` timeline. Disk usage adds up fast on large
23
+ * repos — clean up between runs if you don't need the raw snapshots.
24
+ */
25
+ import { type AnalyzeOptions } from './analyze.js';
26
+ export interface ProfileHeapOptions extends AnalyzeOptions {
27
+ /**
28
+ * Commander injects this from the `--no-summary` flag — see CLI
29
+ * registration. `--no-summary` ⇒ `summary === false`. The dual-name
30
+ * convention (positive flag name, negated value) is a commander
31
+ * footgun: a `noSummary?: boolean` field would silently never fire.
32
+ */
33
+ summary?: boolean;
34
+ }
35
+ export declare const profileHeapCommand: (inputPath?: string, options?: ProfileHeapOptions) => Promise<void>;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * profile-heap — RFC 0002 Phase 1 entry point.
3
+ *
4
+ * A thin wrapper around `analyze` that flips on the heap-profile
5
+ * instrumentation already living in `runFullAnalysis`, then prints a
6
+ * per-phase RSS / heapUsed summary table after the run finishes.
7
+ *
8
+ * Why a dedicated subcommand instead of just documenting the env var?
9
+ * - Discoverability: `codragraph --help` lists it next to `analyze`.
10
+ * - One-shot UX: users (and the maintainer) get a useful summary table
11
+ * without having to spelunk through Chrome DevTools to compare
12
+ * snapshots. The `.heapsnapshot` files are still written for deep
13
+ * dives; the summary just makes the cheap signal (RSS curve, heapUsed
14
+ * curve) visible at a glance.
15
+ * - Phase 1 of RFC 0002 is profile-first by design — we ship the tool
16
+ * before any mitigation. Don't add compression, eviction, or streaming
17
+ * refactors here; that's Phase 2+ once we know which phase is the
18
+ * actual bottleneck.
19
+ *
20
+ * Side effects: writes `.codragraph/heap-profiles/<ts>-<phase>.heapsnapshot`
21
+ * (one per phase boundary, ~100-500MB each) plus a small
22
+ * `profile-summary.jsonl` timeline. Disk usage adds up fast on large
23
+ * repos — clean up between runs if you don't need the raw snapshots.
24
+ */
25
+ import path from 'path';
26
+ import * as fsSync from 'node:fs';
27
+ import { getGitRoot, hasGitDir } from '../storage/git.js';
28
+ import { analyzeCommand } from './analyze.js';
29
+ export const profileHeapCommand = async (inputPath, options) => {
30
+ // Flip on the instrumentation BEFORE delegating to analyze. The env var
31
+ // is read by `runFullAnalysis` at orchestrator entry, so it must be set
32
+ // here. Setting it on every profile-heap invocation also guarantees that
33
+ // a leftover `unset` from a prior shell session can't disable profiling
34
+ // in this run.
35
+ process.env.CODRAGRAPH_HEAP_PROFILE = '1';
36
+ // Resolve the repo path the same way `analyze` does so we can locate the
37
+ // summary file after the run. Mirroring this avoids touching analyze's
38
+ // resolution logic, which already handles --skip-git, gitRoot, etc.
39
+ let repoPath;
40
+ if (inputPath) {
41
+ repoPath = path.resolve(inputPath);
42
+ }
43
+ else {
44
+ const gitRoot = getGitRoot(process.cwd());
45
+ if (!gitRoot && !options?.skipGit) {
46
+ // Let analyze produce its standard error message + exit code rather
47
+ // than duplicating the message here.
48
+ await analyzeCommand(inputPath, options);
49
+ return;
50
+ }
51
+ repoPath = gitRoot ?? path.resolve(process.cwd());
52
+ }
53
+ if (!hasGitDir(repoPath) && !options?.skipGit) {
54
+ await analyzeCommand(inputPath, options);
55
+ return;
56
+ }
57
+ // Detect whether we're the outer (pre-re-exec) process. analyzeCommand
58
+ // calls ensureHeap() which `execFileSync`s a child with
59
+ // --max-old-space-size=8192 on first invocation; that child runs the
60
+ // instrumented codepath and prints its own summary before exiting. If
61
+ // we don't bail here, the outer process re-reads the just-written
62
+ // summary file and prints it a second time.
63
+ //
64
+ // Capture the flag BEFORE the await so a future change to NODE_OPTIONS
65
+ // mid-flight can't confuse us. (execFileSync's child env doesn't
66
+ // propagate back to process.env, but be defensive.)
67
+ const isInnerProcess = (process.env.NODE_OPTIONS || '').includes('--max-old-space-size');
68
+ await analyzeCommand(inputPath, options);
69
+ // Outer process: the inner already printed the summary on its way out.
70
+ if (!isInnerProcess)
71
+ return;
72
+ // `--no-summary` → commander sets options.summary === false.
73
+ if (options?.summary === false)
74
+ return;
75
+ const summaryPath = path.join(repoPath, '.codragraph', 'heap-profiles', 'profile-summary.jsonl');
76
+ if (!fsSync.existsSync(summaryPath)) {
77
+ // analyze re-execs itself with a larger heap on first invocation; the
78
+ // outer process never reaches the instrumented codepath. Tell the user
79
+ // where to find the artifacts in that case.
80
+ console.log(`\n Heap profile summary not found at ${summaryPath}.\n` +
81
+ ` This is expected on the first call (analyze re-execs with --max-old-space-size).\n` +
82
+ ` Re-run \`codragraph profile-heap\` and the summary will appear in the second pass.\n`);
83
+ return;
84
+ }
85
+ const lines = fsSync
86
+ .readFileSync(summaryPath, 'utf8')
87
+ .split('\n')
88
+ .filter((l) => l.trim().length > 0);
89
+ const entries = [];
90
+ for (const line of lines) {
91
+ try {
92
+ entries.push(JSON.parse(line));
93
+ }
94
+ catch {
95
+ /* skip malformed lines — best-effort */
96
+ }
97
+ }
98
+ if (entries.length === 0) {
99
+ console.log(`\n Heap profile summary at ${summaryPath} is empty.\n`);
100
+ return;
101
+ }
102
+ printSummary(entries, summaryPath);
103
+ };
104
+ function printSummary(entries, summaryPath) {
105
+ const peakRss = entries.reduce((m, e) => (e.rss > m ? e.rss : m), 0);
106
+ const peakHeapUsed = entries.reduce((m, e) => (e.heapUsed > m ? e.heapUsed : m), 0);
107
+ const startTs = entries[0].ts;
108
+ console.log('\n Heap-profile summary');
109
+ console.log(' ────────────────────');
110
+ console.log(' Phase'.padEnd(28) +
111
+ ' Δt(s)'.padEnd(10) +
112
+ ' RSS(MB)'.padEnd(12) +
113
+ ' heapUsed(MB)'.padEnd(16) +
114
+ ' Snapshot');
115
+ for (const e of entries) {
116
+ const dt = ((e.ts - startTs) / 1000).toFixed(1);
117
+ const rssMb = (e.rss / 1024 / 1024).toFixed(0);
118
+ const heapMb = (e.heapUsed / 1024 / 1024).toFixed(0);
119
+ console.log(` ${e.phase.padEnd(26)} ${dt.padStart(6)} ${rssMb.padStart(7)} ${heapMb.padStart(11)} ${e.snapshotFile}`);
120
+ }
121
+ console.log(' ────────────────────');
122
+ console.log(` peak RSS: ${(peakRss / 1024 / 1024).toFixed(0)} MB`);
123
+ console.log(` peak heapUsed: ${(peakHeapUsed / 1024 / 1024).toFixed(0)} MB`);
124
+ console.log(` raw timeline: ${summaryPath}`);
125
+ console.log(` snapshots dir: ${path.dirname(summaryPath)} (open .heapsnapshot files in Chrome DevTools → Memory → Load)\n`);
126
+ }
@@ -5,4 +5,17 @@
5
5
  * Detects installed AI editors and writes the appropriate MCP config
6
6
  * so the CodraGraph MCP server is available in all projects.
7
7
  */
8
+ interface SetupResult {
9
+ configured: string[];
10
+ skipped: string[];
11
+ errors: string[];
12
+ }
13
+ export interface RunSetupOptions {
14
+ /** Suppress the trailing "Next steps" block (used when analyze auto-runs setup). */
15
+ skipNextSteps?: boolean;
16
+ /** Suppress the "CodraGraph Setup" header (used when analyze auto-runs setup). */
17
+ compactHeader?: boolean;
18
+ }
19
+ export declare const runSetup: (options?: RunSetupOptions) => Promise<SetupResult>;
8
20
  export declare const setupCommand: () => Promise<void>;
21
+ export {};
package/dist/cli/setup.js CHANGED
@@ -519,12 +519,17 @@ async function installCodexSkills(result) {
519
519
  result.errors.push(`Codex skills: ${err.message}`);
520
520
  }
521
521
  }
522
- // ─── Main command ──────────────────────────────────────────────────
523
- export const setupCommand = async () => {
524
- console.log('');
525
- console.log(' CodraGraph Setup');
526
- console.log(' ==============');
527
- console.log('');
522
+ export const runSetup = async (options = {}) => {
523
+ if (options.compactHeader) {
524
+ console.log(' CodraGraph: first-run editor setup');
525
+ console.log('');
526
+ }
527
+ else {
528
+ console.log('');
529
+ console.log(' CodraGraph Setup');
530
+ console.log(' ==============');
531
+ console.log('');
532
+ }
528
533
  // Ensure global directory exists
529
534
  const globalDir = getGlobalDir();
530
535
  await fs.mkdir(globalDir, { recursive: true });
@@ -569,10 +574,16 @@ export const setupCommand = async () => {
569
574
  console.log(' Summary:');
570
575
  console.log(` MCP configured for: ${result.configured.filter((c) => !c.includes('skills')).join(', ') || 'none'}`);
571
576
  console.log(` Skills installed to: ${result.configured.filter((c) => c.includes('skills')).length > 0 ? result.configured.filter((c) => c.includes('skills')).join(', ') : 'none'}`);
577
+ if (!options.skipNextSteps) {
578
+ console.log('');
579
+ console.log(' Next steps:');
580
+ console.log(' 1. cd into any git repo');
581
+ console.log(' 2. Run: codragraph analyze');
582
+ console.log(' 3. Open the repo in your editor — MCP is ready!');
583
+ }
572
584
  console.log('');
573
- console.log(' Next steps:');
574
- console.log(' 1. cd into any git repo');
575
- console.log(' 2. Run: codragraph analyze');
576
- console.log(' 3. Open the repo in your editor — MCP is ready!');
577
- console.log('');
585
+ return result;
586
+ };
587
+ export const setupCommand = async () => {
588
+ await runSetup();
578
589
  };
@@ -13,14 +13,26 @@ export interface GeneratedSkillInfo {
13
13
  symbolCount: number;
14
14
  fileCount: number;
15
15
  }
16
+ /**
17
+ * Supported skill targets. Project-relative output paths mirror each editor's
18
+ * convention: Claude / Cursor use `skills/`, OpenCode uses `skill/` (singular)
19
+ * to match its global config layout, Codex uses `skills/`. The trailing
20
+ * `generated/` segment isolates auto-generated skills from human-authored ones.
21
+ */
22
+ export declare const SKILL_TARGETS: readonly ["claude", "cursor", "opencode", "codex"];
23
+ export type SkillTarget = (typeof SKILL_TARGETS)[number];
16
24
  /**
17
25
  * @brief Generate repo-specific skill files from detected communities
18
26
  * @param {string} repoPath - Absolute path to the repository root
19
27
  * @param {string} projectName - Human-readable project name
20
28
  * @param {PipelineResult} pipelineResult - In-memory pipeline data with communities, processes, graph
21
- * @returns {Promise<{ skills: GeneratedSkillInfo[], outputPath: string }>} Generated skill metadata
29
+ * @param {SkillTarget[]} targets - Editor targets to emit to. Defaults to ['claude'].
30
+ * @returns {Promise<{ skills: GeneratedSkillInfo[], outputPath: string, outputPaths: string[] }>}
31
+ * `outputPath` is the Claude path (or first target) for backwards compat;
32
+ * `outputPaths` lists every directory written to.
22
33
  */
23
- export declare const generateSkillFiles: (repoPath: string, projectName: string, pipelineResult: PipelineResult) => Promise<{
34
+ export declare const generateSkillFiles: (repoPath: string, projectName: string, pipelineResult: PipelineResult, targets?: SkillTarget[]) => Promise<{
24
35
  skills: GeneratedSkillInfo[];
25
36
  outputPath: string;
37
+ outputPaths: string[];
26
38
  }>;
@@ -8,6 +8,20 @@
8
8
  */
9
9
  import fs from 'fs/promises';
10
10
  import path from 'path';
11
+ import { estimateTokens } from './compress-stats.js';
12
+ /**
13
+ * Supported skill targets. Project-relative output paths mirror each editor's
14
+ * convention: Claude / Cursor use `skills/`, OpenCode uses `skill/` (singular)
15
+ * to match its global config layout, Codex uses `skills/`. The trailing
16
+ * `generated/` segment isolates auto-generated skills from human-authored ones.
17
+ */
18
+ export const SKILL_TARGETS = ['claude', 'cursor', 'opencode', 'codex'];
19
+ const SKILL_OUTPUT_DIRS = {
20
+ claude: ['.claude', 'skills', 'generated'],
21
+ cursor: ['.cursor', 'skills', 'generated'],
22
+ opencode: ['.opencode', 'skill', 'generated'],
23
+ codex: ['.codex', 'skills', 'generated'],
24
+ };
11
25
  // ============================================================================
12
26
  // MAIN EXPORT
13
27
  // ============================================================================
@@ -16,14 +30,24 @@ import path from 'path';
16
30
  * @param {string} repoPath - Absolute path to the repository root
17
31
  * @param {string} projectName - Human-readable project name
18
32
  * @param {PipelineResult} pipelineResult - In-memory pipeline data with communities, processes, graph
19
- * @returns {Promise<{ skills: GeneratedSkillInfo[], outputPath: string }>} Generated skill metadata
33
+ * @param {SkillTarget[]} targets - Editor targets to emit to. Defaults to ['claude'].
34
+ * @returns {Promise<{ skills: GeneratedSkillInfo[], outputPath: string, outputPaths: string[] }>}
35
+ * `outputPath` is the Claude path (or first target) for backwards compat;
36
+ * `outputPaths` lists every directory written to.
20
37
  */
21
- export const generateSkillFiles = async (repoPath, projectName, pipelineResult) => {
38
+ export const generateSkillFiles = async (repoPath, projectName, pipelineResult, targets = ['claude']) => {
22
39
  const { communityResult, processResult, graph } = pipelineResult;
23
- const outputDir = path.join(repoPath, '.claude', 'skills', 'generated');
40
+ // Resolve all output dirs once. The "primary" path is Claude (if requested)
41
+ // or the first target — kept for AGENTS.md / CLAUDE.md generators that link
42
+ // to skill files relative to .claude/.
43
+ const effectiveTargets = targets.length > 0 ? targets : ['claude'];
44
+ const outputDirs = effectiveTargets.map((t) => path.join(repoPath, ...SKILL_OUTPUT_DIRS[t]));
45
+ const primaryDir = effectiveTargets.includes('claude')
46
+ ? path.join(repoPath, ...SKILL_OUTPUT_DIRS.claude)
47
+ : outputDirs[0];
24
48
  if (!communityResult || !communityResult.memberships.length) {
25
49
  console.log('\n Skills: no communities detected, skipping skill generation');
26
- return { skills: [], outputPath: outputDir };
50
+ return { skills: [], outputPath: primaryDir, outputPaths: outputDirs };
27
51
  }
28
52
  console.log('\n Generating repo-specific skills...');
29
53
  // Step 1: Build communities from memberships (not the filtered communities array).
@@ -42,19 +66,21 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
42
66
  .slice(0, 20);
43
67
  if (significant.length === 0) {
44
68
  console.log('\n Skills: no significant communities found (all below 3-symbol threshold)');
45
- return { skills: [], outputPath: outputDir };
69
+ return { skills: [], outputPath: primaryDir, outputPaths: outputDirs };
46
70
  }
47
71
  // Step 3: Build lookup maps
48
72
  const membershipsByComm = buildMembershipMap(communityResult.memberships);
49
73
  const nodeIdToCommunityLabel = buildNodeCommunityLabelMap(communityResult.memberships, communities);
50
- // Step 4: Clear and recreate output directory
51
- try {
52
- await fs.rm(outputDir, { recursive: true, force: true });
53
- }
54
- catch {
55
- /* may not exist */
74
+ // Step 4: Clear and recreate every output directory we'll write to
75
+ for (const dir of outputDirs) {
76
+ try {
77
+ await fs.rm(dir, { recursive: true, force: true });
78
+ }
79
+ catch {
80
+ /* may not exist */
81
+ }
82
+ await fs.mkdir(dir, { recursive: true });
56
83
  }
57
- await fs.mkdir(outputDir, { recursive: true });
58
84
  // Step 5: Generate skill files
59
85
  const skills = [];
60
86
  const usedNames = new Set();
@@ -76,10 +102,13 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
76
102
  usedNames.add(kebabName);
77
103
  // Generate SKILL.md content
78
104
  const content = renderSkillMarkdown(community, projectName, members, files, entryPoints, flows, connections, kebabName);
79
- // Write file
80
- const skillDir = path.join(outputDir, kebabName);
81
- await fs.mkdir(skillDir, { recursive: true });
82
- await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf-8');
105
+ // Write the same SKILL.md to each requested editor target
106
+ for (const dir of outputDirs) {
107
+ const skillDir = path.join(dir, kebabName);
108
+ await fs.mkdir(skillDir, { recursive: true });
109
+ await fs.writeFile(path.join(skillDir, 'SKILL.md'), content, 'utf-8');
110
+ }
111
+ const skillTokens = estimateTokens(content);
83
112
  const info = {
84
113
  name: kebabName,
85
114
  label: community.label,
@@ -87,10 +116,14 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
87
116
  fileCount: files.length,
88
117
  };
89
118
  skills.push(info);
90
- console.log(` \u2713 ${community.label} (${community.symbolCount} symbols, ${files.length} files)`);
119
+ // Show the @codragraph/compress headline number per skill: how many
120
+ // tokens of distilled context this community boils down to.
121
+ console.log(` \u2713 ${community.label} (${community.symbolCount} symbols, ${files.length} files) ` +
122
+ `\u2192 ~${skillTokens.toLocaleString()} tokens`);
91
123
  }
92
- console.log(`\n ${skills.length} skills generated \u2192 .claude/skills/generated/`);
93
- return { skills, outputPath: outputDir };
124
+ const targetSummary = effectiveTargets.join(', ');
125
+ console.log(`\n ${skills.length} skills generated \u2192 ${targetSummary}`);
126
+ return { skills, outputPath: primaryDir, outputPaths: outputDirs };
94
127
  };
95
128
  // ============================================================================
96
129
  // FALLBACK COMMUNITY BUILDER