@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.
- package/README.md +34 -0
- package/dist/_shared/cgdb/schema-constants.d.ts +16 -0
- package/dist/_shared/cgdb/schema-constants.d.ts.map +1 -0
- package/dist/_shared/cgdb/schema-constants.js +67 -0
- package/dist/_shared/cgdb/schema-constants.js.map +1 -0
- package/dist/_shared/index.d.ts +2 -2
- package/dist/_shared/index.js +1 -1
- package/dist/cli/analyze.d.ts +22 -0
- package/dist/cli/analyze.js +109 -6
- 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 +45 -23
- package/dist/cli/index-repo.js +3 -3
- package/dist/cli/index.js +16 -2
- 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 +22 -11
- package/dist/cli/skill-gen.d.ts +14 -2
- package/dist/cli/skill-gen.js +52 -19
- package/dist/cli/tool.js +4 -0
- package/dist/cli/wiki.js +3 -3
- package/dist/core/augmentation/engine.js +7 -7
- package/dist/core/cgdb/cgdb-adapter.d.ts +176 -0
- package/dist/core/cgdb/cgdb-adapter.js +1320 -0
- package/dist/core/cgdb/content-read.d.ts +46 -0
- package/dist/core/cgdb/content-read.js +64 -0
- package/dist/core/cgdb/csv-generator.d.ts +29 -0
- package/dist/core/cgdb/csv-generator.js +492 -0
- package/dist/core/cgdb/pool-adapter.d.ts +93 -0
- package/dist/core/cgdb/pool-adapter.js +550 -0
- package/dist/core/cgdb/schema.d.ts +62 -0
- package/dist/core/cgdb/schema.js +502 -0
- package/dist/core/embeddings/embedding-pipeline.js +27 -10
- package/dist/core/graphstore/cgdb-row-source.d.ts +19 -0
- package/dist/core/graphstore/cgdb-row-source.js +141 -0
- package/dist/core/graphstore/index.d.ts +1 -1
- package/dist/core/graphstore/index.js +3 -3
- package/dist/core/group/bridge-db.d.ts +2 -2
- package/dist/core/group/bridge-db.js +123 -36
- package/dist/core/group/bridge-schema.d.ts +4 -4
- package/dist/core/group/bridge-schema.js +4 -4
- package/dist/core/group/cross-impact.js +3 -3
- package/dist/core/group/sync.js +4 -4
- 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 +153 -21
- 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 +114 -27
- package/dist/core/search/bm25-index.d.ts +3 -3
- package/dist/core/search/bm25-index.js +75 -23
- package/dist/core/search/hybrid-search.js +2 -2
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +4 -4
- package/dist/core/wiki/graph-queries.d.ts +2 -2
- package/dist/core/wiki/graph-queries.js +5 -5
- package/dist/mcp/core/cgdb-adapter.d.ts +5 -0
- package/dist/mcp/core/cgdb-adapter.js +5 -0
- package/dist/mcp/core/embedder.js +1 -1
- package/dist/mcp/local/local-backend.d.ts +2 -2
- package/dist/mcp/local/local-backend.js +36 -19
- package/dist/mcp/server.js +3 -3
- package/dist/mcp/tools.js +1 -1
- package/dist/server/analyze-worker.js +2 -2
- package/dist/server/api.js +34 -33
- package/dist/storage/repo-manager.d.ts +42 -3
- package/dist/storage/repo-manager.js +23 -4
- package/hooks/claude/codragraph-hook.cjs +98 -5
- package/package.json +4 -4
- package/scripts/build-tree-sitter-proto.cjs +15 -3
- package/scripts/build.js +8 -9
- package/scripts/patch-tree-sitter-swift.cjs +17 -4
- package/skills/codragraph-api-surface.md +110 -0
- 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-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-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-project-switcher.md +116 -0
- 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
- package/vendor/tree-sitter-proto/bindings/node/index.js +3 -3
- package/vendor/tree-sitter-proto/src/node-types.json +1 -1
package/dist/cli/graphstore.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
240
|
-
await
|
|
241
|
-
for (const f of [repo.
|
|
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
|
|
260
|
+
await initCgdb(repo.cgdbPath);
|
|
250
261
|
try {
|
|
251
|
-
const sink = await
|
|
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
|
|
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
|
|
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
|
|
298
|
+
await initCgdb(into);
|
|
288
299
|
try {
|
|
289
|
-
const sink = await
|
|
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
|
|
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
|
-
//
|
|
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
|
|
387
|
-
const { executeQuery } = await import('../core/
|
|
388
|
-
const { SCHEMA_QUERIES } = await import('../core/
|
|
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
|
|
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
|
|
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) {
|
package/dist/cli/index-repo.js
CHANGED
|
@@ -49,7 +49,7 @@ export const indexCommand = async (inputPathParts, options) => {
|
|
|
49
49
|
process.exitCode = 1;
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
-
const { storagePath,
|
|
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
|
|
63
|
+
// ── Verify cgdb database exists ───────────────────────────────────
|
|
64
64
|
try {
|
|
65
|
-
await fs.access(
|
|
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
|
+
}
|
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
|
@@ -519,12 +519,17 @@ async function installCodexSkills(result) {
|
|
|
519
519
|
result.errors.push(`Codex skills: ${err.message}`);
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
console.log('');
|
|
585
|
+
return result;
|
|
586
|
+
};
|
|
587
|
+
export const setupCommand = async () => {
|
|
588
|
+
await runSetup();
|
|
578
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
|