@codragraph/cli 1.6.3 → 2.0.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 (89) hide show
  1. package/README.md +50 -16
  2. package/dist/cli/ai-context.js +2 -2
  3. package/dist/cli/analyze.d.ts +22 -0
  4. package/dist/cli/analyze.js +111 -8
  5. package/dist/cli/compress-stats.d.ts +29 -0
  6. package/dist/cli/compress-stats.js +97 -0
  7. package/dist/cli/graphstore.d.ts +6 -2
  8. package/dist/cli/graphstore.js +24 -2
  9. package/dist/cli/index.js +17 -6
  10. package/dist/cli/profile-heap.d.ts +35 -0
  11. package/dist/cli/profile-heap.js +126 -0
  12. package/dist/cli/setup.d.ts +13 -0
  13. package/dist/cli/setup.js +75 -29
  14. package/dist/cli/skill-gen.d.ts +14 -2
  15. package/dist/cli/skill-gen.js +53 -20
  16. package/dist/cli/tool.js +4 -0
  17. package/dist/config/ignore-service.js +1 -1
  18. package/dist/core/embeddings/embedding-pipeline.js +24 -7
  19. package/dist/core/group/bridge-db.js +111 -24
  20. package/dist/core/group/extractors/grpc-patterns/proto.js +1 -12
  21. package/dist/core/ingestion/call-processor.js +2 -2
  22. package/dist/core/ingestion/cobol/cobol-preprocessor.js +1 -1
  23. package/dist/core/ingestion/cobol/jcl-parser.d.ts +1 -1
  24. package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
  25. package/dist/core/ingestion/cobol-processor.d.ts +1 -1
  26. package/dist/core/ingestion/cobol-processor.js +1 -1
  27. package/dist/core/ingestion/heritage-extractors/generic.js +1 -1
  28. package/dist/core/ingestion/heritage-processor.js +1 -1
  29. package/dist/core/ingestion/import-processor.js +1 -1
  30. package/dist/core/ingestion/mro-processor.js +1 -1
  31. package/dist/core/ingestion/parsing-processor.js +1 -1
  32. package/dist/core/ingestion/type-extractors/c-cpp.js +1 -1
  33. package/dist/core/ingestion/type-extractors/python.js +1 -1
  34. package/dist/core/ingestion/type-extractors/shared.js +0 -3
  35. package/dist/core/lbug/content-read.d.ts +46 -0
  36. package/dist/core/lbug/content-read.js +64 -0
  37. package/dist/core/lbug/csv-generator.d.ts +2 -6
  38. package/dist/core/lbug/csv-generator.js +45 -12
  39. package/dist/core/lbug/lbug-adapter.d.ts +4 -1
  40. package/dist/core/lbug/lbug-adapter.js +157 -25
  41. package/dist/core/lbug/pool-adapter.js +51 -44
  42. package/dist/core/lbug/schema.d.ts +7 -7
  43. package/dist/core/lbug/schema.js +18 -0
  44. package/dist/core/run-analyze.d.ts +13 -0
  45. package/dist/core/run-analyze.js +91 -4
  46. package/dist/core/search/bm25-index.js +153 -12
  47. package/dist/core/wiki/generator.js +4 -4
  48. package/dist/mcp/local/local-backend.js +22 -5
  49. package/dist/mcp/resources.js +2 -3
  50. package/dist/server/api.js +4 -3
  51. package/dist/storage/repo-manager.d.ts +39 -0
  52. package/dist/storage/repo-manager.js +19 -0
  53. package/hooks/claude/codragraph-hook.cjs +108 -5
  54. package/hooks/claude/pre-tool-use.sh +6 -1
  55. package/package.json +4 -4
  56. package/scripts/build-tree-sitter-proto.cjs +15 -3
  57. package/scripts/patch-tree-sitter-swift.cjs +17 -4
  58. package/skills/codragraph-api-surface.md +110 -0
  59. package/skills/codragraph-cli.md +5 -5
  60. package/skills/codragraph-config-audit.md +146 -0
  61. package/skills/codragraph-cross-repo-impact.md +135 -0
  62. package/skills/codragraph-data-lineage.md +137 -0
  63. package/skills/codragraph-dead-code.md +119 -0
  64. package/skills/codragraph-debugging.md +1 -1
  65. package/skills/codragraph-exploring.md +1 -1
  66. package/skills/codragraph-gh-actions-debug.md +162 -0
  67. package/skills/codragraph-gh-issue-workflow.md +178 -0
  68. package/skills/codragraph-gh-pr-workflow.md +176 -0
  69. package/skills/codragraph-gh-release-workflow.md +187 -0
  70. package/skills/codragraph-git-bisect.md +176 -0
  71. package/skills/codragraph-git-force-push.md +147 -0
  72. package/skills/codragraph-git-history-rewrite.md +174 -0
  73. package/skills/codragraph-git-rebase-vs-merge.md +138 -0
  74. package/skills/codragraph-git-recovery.md +181 -0
  75. package/skills/codragraph-git-worktree.md +145 -0
  76. package/skills/codragraph-guide.md +1 -1
  77. package/skills/codragraph-impact-analysis.md +1 -1
  78. package/skills/codragraph-migration-tracking.md +130 -0
  79. package/skills/codragraph-notebook-context.md +136 -0
  80. package/skills/codragraph-observability-coverage.md +125 -0
  81. package/skills/codragraph-onboarding.md +129 -0
  82. package/skills/codragraph-perf-hotspots.md +132 -0
  83. package/skills/codragraph-pr-review.md +1 -1
  84. package/skills/codragraph-project-switcher.md +116 -0
  85. package/skills/codragraph-refactoring.md +1 -1
  86. package/skills/codragraph-security-audit.md +144 -0
  87. package/skills/codragraph-sql-tracing.md +122 -0
  88. package/skills/codragraph-supply-chain-audit.md +153 -0
  89. package/skills/codragraph-test-coverage.md +97 -0
@@ -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
@@ -18,20 +18,38 @@ const __filename = fileURLToPath(import.meta.url);
18
18
  const __dirname = path.dirname(__filename);
19
19
  const execFileAsync = promisify(execFile);
20
20
  /**
21
- * Resolve the absolute path to the `@codragraph/cli` binary if it's installed
21
+ * Resolve the absolute path to the `codragraph` binary if it's installed
22
22
  * globally (or via npm -g / yarn global). Returns null when not found.
23
+ *
24
+ * Note: the npm package is `@codragraph/cli`, but the executable it installs
25
+ * is `codragraph` (see package.json `bin`). PATH lookup must use the bin name.
26
+ *
27
+ * Windows specifics: `where codragraph` returns every PATH entry, in order:
28
+ * the extensionless Unix shim npm creates first (a sh script — Node's
29
+ * spawn/execFile cannot launch it on Windows), then `codragraph.cmd`,
30
+ * then `codragraph.ps1`. We must pick the .cmd / .exe / .bat entry; writing
31
+ * the extensionless path into a downstream MCP config produces a launcher
32
+ * that fails on every spawn.
23
33
  */
24
34
  function resolveCodragraphBin() {
25
35
  try {
26
36
  const cmd = process.platform === 'win32' ? 'where' : 'which';
27
- const resolved = execFileSync(cmd, ['@codragraph/cli'], {
37
+ const stdout = execFileSync(cmd, ['codragraph'], {
28
38
  encoding: 'utf-8',
29
39
  timeout: 5000,
30
40
  stdio: ['ignore', 'pipe', 'ignore'],
31
- })
32
- .split('\n')[0]
33
- .trim();
34
- return resolved || null;
41
+ });
42
+ const lines = stdout
43
+ .split('\n')
44
+ .map((l) => l.trim())
45
+ .filter(Boolean);
46
+ if (process.platform === 'win32') {
47
+ // Prefer a Windows-executable shim. If none is on PATH, return null so
48
+ // the caller falls through to the `cmd /c npx -y @codragraph/cli` path.
49
+ const exe = lines.find((l) => /\.(cmd|exe|bat)$/i.test(l));
50
+ return exe ?? null;
51
+ }
52
+ return lines[0] ?? null;
35
53
  }
36
54
  catch {
37
55
  return null;
@@ -40,28 +58,38 @@ function resolveCodragraphBin() {
40
58
  /**
41
59
  * The MCP server entry for all editors.
42
60
  *
43
- * Prefers the globally-installed `@codragraph/cli` binary (starts in ~1 s) over
44
- * `npx -y codragraph@latest` (cold-cache install of native deps can take
61
+ * Prefers the globally-installed `codragraph` binary (starts in ~1 s) over
62
+ * `npx -y @codragraph/cli@latest` (cold-cache install of native deps can take
45
63
  * >60 s, exceeding Claude Code's 30 s MCP connection timeout).
46
64
  *
47
65
  * Falls back to npx when the binary isn't on PATH — e.g. first-time
48
- * users who ran `npx codragraph analyze` but haven't done `npm i -g`.
66
+ * users who ran `npx @codragraph/cli analyze` but haven't done `npm i -g`.
67
+ *
68
+ * Windows note: even when the bin is on PATH, we launch via `cmd /c codragraph
69
+ * mcp` rather than writing the resolved path. Reason: `where codragraph`
70
+ * returns the extensionless Unix shim before `codragraph.cmd`, and Node's
71
+ * spawn/execFile cannot launch the extensionless shim on Windows. Letting
72
+ * cmd resolve via PATHEXT is the only reliable path that works for npm-,
73
+ * pnpm-, and yarn-installed shims alike.
49
74
  */
50
75
  function getMcpEntry() {
51
76
  const bin = resolveCodragraphBin();
52
77
  if (bin) {
78
+ if (process.platform === 'win32') {
79
+ return { command: 'cmd', args: ['/c', 'codragraph', 'mcp'] };
80
+ }
53
81
  return { command: bin, args: ['mcp'] };
54
82
  }
55
83
  // Fallback: npx (works without a global install, but slow cold-start)
56
84
  if (process.platform === 'win32') {
57
85
  return {
58
86
  command: 'cmd',
59
- args: ['/c', 'npx', '-y', 'codragraph@latest', 'mcp'],
87
+ args: ['/c', 'npx', '-y', '@codragraph/cli@latest', 'mcp'],
60
88
  };
61
89
  }
62
90
  return {
63
91
  command: 'npx',
64
- args: ['-y', 'codragraph@latest', 'mcp'],
92
+ args: ['-y', '@codragraph/cli@latest', 'mcp'],
65
93
  };
66
94
  }
67
95
  /**
@@ -71,12 +99,18 @@ function getMcpEntry() {
71
99
  function getOpenCodeMcpEntry() {
72
100
  const bin = resolveCodragraphBin();
73
101
  if (bin) {
102
+ if (process.platform === 'win32') {
103
+ return { type: 'local', command: ['cmd', '/c', 'codragraph', 'mcp'] };
104
+ }
74
105
  return { type: 'local', command: [bin, 'mcp'] };
75
106
  }
76
107
  if (process.platform === 'win32') {
77
- return { type: 'local', command: ['cmd', '/c', 'npx', '-y', 'codragraph@latest', 'mcp'] };
108
+ return {
109
+ type: 'local',
110
+ command: ['cmd', '/c', 'npx', '-y', '@codragraph/cli@latest', 'mcp'],
111
+ };
78
112
  }
79
- return { type: 'local', command: ['npx', '-y', 'codragraph@latest', 'mcp'] };
113
+ return { type: 'local', command: ['npx', '-y', '@codragraph/cli@latest', 'mcp'] };
80
114
  }
81
115
  /**
82
116
  * Merge codragraph entry into an existing MCP config JSON object.
@@ -239,7 +273,7 @@ async function installClaudeCodeHooks(result) {
239
273
  // Source hooks bundled within the codragraph package (hooks/claude/)
240
274
  const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude');
241
275
  // Copy unified hook script to ~/.claude/hooks/codragraph/
242
- const destHooksDir = path.join(claudeDir, 'hooks', '@codragraph/cli');
276
+ const destHooksDir = path.join(claudeDir, 'hooks', 'codragraph');
243
277
  try {
244
278
  await fs.mkdir(destHooksDir, { recursive: true });
245
279
  const src = path.join(pluginHooksPath, 'codragraph-hook.cjs');
@@ -291,7 +325,7 @@ async function setupOpenCode(result) {
291
325
  }
292
326
  const configPath = path.join(opencodeDir, 'opencode.json');
293
327
  try {
294
- const ok = await mergeJsoncFile(configPath, ['mcp', '@codragraph/cli'], getOpenCodeMcpEntry());
328
+ const ok = await mergeJsoncFile(configPath, ['mcp', 'codragraph'], getOpenCodeMcpEntry());
295
329
  if (ok) {
296
330
  result.configured.push('OpenCode');
297
331
  }
@@ -339,9 +373,10 @@ async function setupCodex(result) {
339
373
  }
340
374
  try {
341
375
  const entry = getMcpEntry();
342
- await execFileAsync('codex', ['mcp', 'add', '@codragraph/cli', '--', entry.command, ...entry.args], {
343
- shell: process.platform === 'win32',
344
- });
376
+ // On Windows, npm-installed CLIs ship as .cmd shims that Node's execFile
377
+ // can only invoke when the file extension is explicit (no PATHEXT lookup).
378
+ const codexBin = process.platform === 'win32' ? 'codex.cmd' : 'codex';
379
+ await execFileAsync(codexBin, ['mcp', 'add', 'codragraph', '--', entry.command, ...entry.args]);
345
380
  result.configured.push('Codex');
346
381
  return;
347
382
  }
@@ -484,12 +519,17 @@ async function installCodexSkills(result) {
484
519
  result.errors.push(`Codex skills: ${err.message}`);
485
520
  }
486
521
  }
487
- // ─── Main command ──────────────────────────────────────────────────
488
- export const setupCommand = async () => {
489
- console.log('');
490
- console.log(' CodraGraph Setup');
491
- console.log(' ==============');
492
- 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
+ }
493
533
  // Ensure global directory exists
494
534
  const globalDir = getGlobalDir();
495
535
  await fs.mkdir(globalDir, { recursive: true });
@@ -534,10 +574,16 @@ export const setupCommand = async () => {
534
574
  console.log(' Summary:');
535
575
  console.log(` MCP configured for: ${result.configured.filter((c) => !c.includes('skills')).join(', ') || 'none'}`);
536
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
+ }
537
584
  console.log('');
538
- console.log(' Next steps:');
539
- console.log(' 1. cd into any git repo');
540
- console.log(' 2. Run: codragraph analyze');
541
- console.log(' 3. Open the repo in your editor — MCP is ready!');
542
- console.log('');
585
+ return result;
586
+ };
587
+ export const setupCommand = async () => {
588
+ await runSetup();
543
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
@@ -103,7 +136,7 @@ export const generateSkillFiles = async (repoPath, projectName, pipelineResult)
103
136
  * @param {string} repoPath - Repository root for path normalization
104
137
  * @returns {CommunityNode[]} Synthetic community nodes built from membership data
105
138
  */
106
- const buildCommunitiesFromMemberships = (memberships, graph, repoPath) => {
139
+ const buildCommunitiesFromMemberships = (memberships, graph, _repoPath) => {
107
140
  // Group memberships by communityId
108
141
  const groups = new Map();
109
142
  for (const m of memberships) {
package/dist/cli/tool.js CHANGED
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { writeSync } from 'node:fs';
18
18
  import { LocalBackend } from '../mcp/local/local-backend.js';
19
+ import { emitTokenStats } from './compress-stats.js';
19
20
  let _backend = null;
20
21
  async function getBackend() {
21
22
  if (_backend)
@@ -68,6 +69,7 @@ export async function queryCommand(queryText, options) {
68
69
  repo: options?.repo,
69
70
  });
70
71
  output(result);
72
+ emitTokenStats(result);
71
73
  }
72
74
  export async function contextCommand(name, options) {
73
75
  if (!name?.trim() && !options?.uid) {
@@ -83,6 +85,7 @@ export async function contextCommand(name, options) {
83
85
  repo: options?.repo,
84
86
  });
85
87
  output(result);
88
+ emitTokenStats(result);
86
89
  }
87
90
  export async function impactCommand(target, options) {
88
91
  if (!target?.trim()) {
@@ -99,6 +102,7 @@ export async function impactCommand(target, options) {
99
102
  repo: options?.repo,
100
103
  });
101
104
  output(result);
105
+ emitTokenStats(result);
102
106
  }
103
107
  catch (err) {
104
108
  // Belt-and-suspenders: catch infrastructure failures (getBackend, callTool transport)
@@ -285,7 +285,7 @@ export const shouldIgnorePath = (filePath) => {
285
285
  // Ignore hidden files (starting with .)
286
286
  if (fileName.startsWith('.') && fileName !== '.') {
287
287
  // But allow some important config files
288
- const allowedDotFiles = ['.env', '.gitignore']; // Already in IGNORED_FILES, so this is redundant
288
+ const _allowedDotFiles = ['.env', '.gitignore']; // Already in IGNORED_FILES, so this is redundant
289
289
  // Actually, let's NOT ignore all dot files - many are important configs
290
290
  // Just rely on the explicit lists above
291
291
  }
@@ -16,6 +16,7 @@ import { extractStructuralNames } from './structural-extractor.js';
16
16
  import { DEFAULT_EMBEDDING_CONFIG, EMBEDDABLE_LABELS, isShortLabel, LABEL_METHOD, LABELS_WITH_EXPORTED, STRUCTURAL_LABELS, collectBestChunks, } from './types.js';
17
17
  import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME, CREATE_VECTOR_INDEX_QUERY, STALE_HASH_SENTINEL, } from '../lbug/schema.js';
18
18
  import { loadVectorExtension } from '../lbug/lbug-adapter.js';
19
+ import { decodeContentField } from '../lbug/content-read.js';
19
20
  const isDev = process.env.NODE_ENV === 'development';
20
21
  /**
21
22
  * Bump this when the embedding text template changes in a way that should
@@ -46,12 +47,17 @@ const queryEmbeddableNodes = async (executeQuery) => {
46
47
  for (const label of EMBEDDABLE_LABELS) {
47
48
  try {
48
49
  let query;
50
+ // RFC 0001 Phase 2: pull contentEncoding alongside content so we
51
+ // hand DECODED text to the embedder. Embedding compressed bytes
52
+ // would silently destroy semantic search quality without any
53
+ // visible error — decode is mandatory at this boundary.
49
54
  if (label === LABEL_METHOD) {
50
55
  // Method has parameterCount and returnType
51
56
  query = `
52
57
  MATCH (n:Method)
53
58
  RETURN n.id AS id, n.name AS name, 'Method' AS label,
54
59
  n.filePath AS filePath, n.content AS content,
60
+ n.contentEncoding AS contentEncoding,
55
61
  n.startLine AS startLine, n.endLine AS endLine,
56
62
  n.isExported AS isExported, n.description AS description,
57
63
  n.parameterCount AS parameterCount, n.returnType AS returnType
@@ -63,6 +69,7 @@ const queryEmbeddableNodes = async (executeQuery) => {
63
69
  MATCH (n:\`${label}\`)
64
70
  RETURN n.id AS id, n.name AS name, '${label}' AS label,
65
71
  n.filePath AS filePath, n.content AS content,
72
+ n.contentEncoding AS contentEncoding,
66
73
  n.startLine AS startLine, n.endLine AS endLine,
67
74
  n.isExported AS isExported, n.description AS description
68
75
  `;
@@ -73,6 +80,7 @@ const queryEmbeddableNodes = async (executeQuery) => {
73
80
  MATCH (n:\`${label}\`)
74
81
  RETURN n.id AS id, n.name AS name, '${label}' AS label,
75
82
  n.filePath AS filePath, n.content AS content,
83
+ n.contentEncoding AS contentEncoding,
76
84
  n.startLine AS startLine, n.endLine AS endLine,
77
85
  n.description AS description
78
86
  `;
@@ -80,20 +88,29 @@ const queryEmbeddableNodes = async (executeQuery) => {
80
88
  const rows = await executeQuery(query);
81
89
  for (const row of rows) {
82
90
  const hasExportedColumn = label === LABEL_METHOD || LABELS_WITH_EXPORTED.has(label);
91
+ // Column layout (every variant of the query above shares the
92
+ // first six positions; later columns differ by label):
93
+ // 0=id, 1=name, 2=label, 3=filePath,
94
+ // 4=content, 5=contentEncoding,
95
+ // 6=startLine, 7=endLine,
96
+ // 8=isExported (Method + LABELS_WITH_EXPORTED only)
97
+ // 8 or 9=description (depending on isExported presence)
98
+ // 10=parameterCount, 11=returnType (Method only)
99
+ const decoded = decodeContentField(row.content ?? row[4], row.contentEncoding ?? row[5]);
83
100
  allNodes.push({
84
101
  id: row.id ?? row[0],
85
102
  name: row.name ?? row[1],
86
103
  label: row.label ?? row[2],
87
104
  filePath: row.filePath ?? row[3],
88
- content: row.content ?? row[4] ?? '',
89
- startLine: row.startLine ?? row[5],
90
- endLine: row.endLine ?? row[6],
91
- isExported: hasExportedColumn ? (row.isExported ?? row[7]) : undefined,
92
- description: row.description ?? (hasExportedColumn ? row[8] : row[7]),
105
+ content: decoded ?? '',
106
+ startLine: row.startLine ?? row[6],
107
+ endLine: row.endLine ?? row[7],
108
+ isExported: hasExportedColumn ? (row.isExported ?? row[8]) : undefined,
109
+ description: row.description ?? (hasExportedColumn ? row[9] : row[8]),
93
110
  ...(label === LABEL_METHOD
94
111
  ? {
95
- parameterCount: row.parameterCount ?? row[9],
96
- returnType: row.returnType ?? row[10],
112
+ parameterCount: row.parameterCount ?? row[10],
113
+ returnType: row.returnType ?? row[11],
97
114
  }
98
115
  : {}),
99
116
  });