@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.
- package/README.md +50 -16
- package/dist/cli/ai-context.js +2 -2
- package/dist/cli/analyze.d.ts +22 -0
- package/dist/cli/analyze.js +111 -8
- package/dist/cli/compress-stats.d.ts +29 -0
- package/dist/cli/compress-stats.js +97 -0
- package/dist/cli/graphstore.d.ts +6 -2
- package/dist/cli/graphstore.js +24 -2
- package/dist/cli/index.js +17 -6
- package/dist/cli/profile-heap.d.ts +35 -0
- package/dist/cli/profile-heap.js +126 -0
- package/dist/cli/setup.d.ts +13 -0
- package/dist/cli/setup.js +75 -29
- package/dist/cli/skill-gen.d.ts +14 -2
- package/dist/cli/skill-gen.js +53 -20
- package/dist/cli/tool.js +4 -0
- package/dist/config/ignore-service.js +1 -1
- package/dist/core/embeddings/embedding-pipeline.js +24 -7
- package/dist/core/group/bridge-db.js +111 -24
- package/dist/core/group/extractors/grpc-patterns/proto.js +1 -12
- package/dist/core/ingestion/call-processor.js +2 -2
- package/dist/core/ingestion/cobol/cobol-preprocessor.js +1 -1
- package/dist/core/ingestion/cobol/jcl-parser.d.ts +1 -1
- package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
- package/dist/core/ingestion/cobol-processor.d.ts +1 -1
- package/dist/core/ingestion/cobol-processor.js +1 -1
- package/dist/core/ingestion/heritage-extractors/generic.js +1 -1
- package/dist/core/ingestion/heritage-processor.js +1 -1
- package/dist/core/ingestion/import-processor.js +1 -1
- package/dist/core/ingestion/mro-processor.js +1 -1
- package/dist/core/ingestion/parsing-processor.js +1 -1
- package/dist/core/ingestion/type-extractors/c-cpp.js +1 -1
- package/dist/core/ingestion/type-extractors/python.js +1 -1
- package/dist/core/ingestion/type-extractors/shared.js +0 -3
- package/dist/core/lbug/content-read.d.ts +46 -0
- package/dist/core/lbug/content-read.js +64 -0
- package/dist/core/lbug/csv-generator.d.ts +2 -6
- package/dist/core/lbug/csv-generator.js +45 -12
- package/dist/core/lbug/lbug-adapter.d.ts +4 -1
- package/dist/core/lbug/lbug-adapter.js +157 -25
- package/dist/core/lbug/pool-adapter.js +51 -44
- package/dist/core/lbug/schema.d.ts +7 -7
- package/dist/core/lbug/schema.js +18 -0
- package/dist/core/run-analyze.d.ts +13 -0
- package/dist/core/run-analyze.js +91 -4
- package/dist/core/search/bm25-index.js +153 -12
- package/dist/core/wiki/generator.js +4 -4
- package/dist/mcp/local/local-backend.js +22 -5
- package/dist/mcp/resources.js +2 -3
- package/dist/server/api.js +4 -3
- package/dist/storage/repo-manager.d.ts +39 -0
- package/dist/storage/repo-manager.js +19 -0
- package/hooks/claude/codragraph-hook.cjs +108 -5
- package/hooks/claude/pre-tool-use.sh +6 -1
- package/package.json +4 -4
- package/scripts/build-tree-sitter-proto.cjs +15 -3
- package/scripts/patch-tree-sitter-swift.cjs +17 -4
- package/skills/codragraph-api-surface.md +110 -0
- package/skills/codragraph-cli.md +5 -5
- package/skills/codragraph-config-audit.md +146 -0
- package/skills/codragraph-cross-repo-impact.md +135 -0
- package/skills/codragraph-data-lineage.md +137 -0
- package/skills/codragraph-dead-code.md +119 -0
- package/skills/codragraph-debugging.md +1 -1
- package/skills/codragraph-exploring.md +1 -1
- package/skills/codragraph-gh-actions-debug.md +162 -0
- package/skills/codragraph-gh-issue-workflow.md +178 -0
- package/skills/codragraph-gh-pr-workflow.md +176 -0
- package/skills/codragraph-gh-release-workflow.md +187 -0
- package/skills/codragraph-git-bisect.md +176 -0
- package/skills/codragraph-git-force-push.md +147 -0
- package/skills/codragraph-git-history-rewrite.md +174 -0
- package/skills/codragraph-git-rebase-vs-merge.md +138 -0
- package/skills/codragraph-git-recovery.md +181 -0
- package/skills/codragraph-git-worktree.md +145 -0
- package/skills/codragraph-guide.md +1 -1
- package/skills/codragraph-impact-analysis.md +1 -1
- package/skills/codragraph-migration-tracking.md +130 -0
- package/skills/codragraph-notebook-context.md +136 -0
- package/skills/codragraph-observability-coverage.md +125 -0
- package/skills/codragraph-onboarding.md +129 -0
- package/skills/codragraph-perf-hotspots.md +132 -0
- package/skills/codragraph-pr-review.md +1 -1
- package/skills/codragraph-project-switcher.md +116 -0
- package/skills/codragraph-refactoring.md +1 -1
- package/skills/codragraph-security-audit.md +144 -0
- package/skills/codragraph-sql-tracing.md +122 -0
- package/skills/codragraph-supply-chain-audit.md +153 -0
- 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
|
+
}
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
37
|
+
const stdout = execFileSync(cmd, ['codragraph'], {
|
|
28
38
|
encoding: 'utf-8',
|
|
29
39
|
timeout: 5000,
|
|
30
40
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
.
|
|
34
|
-
|
|
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
|
|
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 {
|
|
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', '
|
|
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', '
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
console.log('');
|
|
585
|
+
return result;
|
|
586
|
+
};
|
|
587
|
+
export const setupCommand = async () => {
|
|
588
|
+
await runSetup();
|
|
543
589
|
};
|
package/dist/cli/skill-gen.d.ts
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
}>;
|
package/dist/cli/skill-gen.js
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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,
|
|
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
|
|
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:
|
|
89
|
-
startLine: row.startLine ?? row[
|
|
90
|
-
endLine: row.endLine ?? row[
|
|
91
|
-
isExported: hasExportedColumn ? (row.isExported ?? row[
|
|
92
|
-
description: row.description ?? (hasExportedColumn ? row[
|
|
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[
|
|
96
|
-
returnType: row.returnType ?? row[
|
|
112
|
+
parameterCount: row.parameterCount ?? row[10],
|
|
113
|
+
returnType: row.returnType ?? row[11],
|
|
97
114
|
}
|
|
98
115
|
: {}),
|
|
99
116
|
});
|