@swarmvaultai/cli 3.10.0 → 3.12.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 (3) hide show
  1. package/README.md +63 -2
  2. package/dist/index.js +280 -101
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -56,6 +56,9 @@ swarmvault graph share --svg ./share-card.svg
56
56
  swarmvault graph share --bundle ./share-kit
57
57
  swarmvault benchmark
58
58
  swarmvault query "What keeps recurring?" --commit
59
+ swarmvault chat "What should the next agent know?"
60
+ swarmvault chat --resume <session-id> "What changed?"
61
+ swarmvault export ai --out ./exports/ai
59
62
  swarmvault context build "Ship this feature safely" --target ./src --budget 8000
60
63
  swarmvault task start "Ship this feature safely" --target ./src --agent codex
61
64
  swarmvault retrieval status
@@ -73,6 +76,7 @@ swarmvault update .
73
76
  swarmvault graph cluster
74
77
  swarmvault cluster-only
75
78
  swarmvault graph tree --output ./exports/tree.html
79
+ swarmvault tree --output ./exports/tree.html
76
80
  swarmvault graph query "Which nodes bridge the biggest clusters?"
77
81
  swarmvault graph explain "concept:drift"
78
82
  swarmvault watch status
@@ -84,7 +88,9 @@ swarmvault graph export --html ./exports/graph.html
84
88
  swarmvault graph export --cypher ./exports/graph.cypher
85
89
  swarmvault graph export --neo4j ./exports/graph.cypher
86
90
  swarmvault graph merge ./exports/graph.json ./other-graph.json --out ./exports/merged-graph.json
91
+ swarmvault merge-graphs ./exports/graph.json ./other-graph.json --out ./exports/merged-graph.json
87
92
  swarmvault graph push neo4j --dry-run
93
+ swarmvault clone https://github.com/owner/repo --no-viz
88
94
  ```
89
95
 
90
96
  ## Commands
@@ -110,7 +116,7 @@ Set `SWARMVAULT_OUT=<dir>` when generated artifacts should be isolated from the
110
116
 
111
117
  `--profile` accepts `default`, `personal-research`, or a comma-separated preset list such as `reader,timeline`. For fully custom vault behavior, edit the `profile` block in `swarmvault.config.json`; that deterministic profile layer works alongside the human-written `swarmvault.schema.md`. The `personal-research` preset also sets `profile.guidedIngestDefault: true` and `profile.deepLintDefault: true`, so guided ingest/source and lint flows are on by default until you override them with `--no-guide` or `--no-deep`.
112
118
 
113
- ### `swarmvault scan <directory|github-url> [--port <port>] [--no-serve] [--branch <name>] [--ref <ref>] [--checkout-dir <path>]`
119
+ ### `swarmvault scan <directory|github-url> [--port <port>] [--no-serve] [--no-viz] [--mcp] [--branch <name>] [--ref <ref>] [--checkout-dir <path>]`
114
120
 
115
121
  Quick-start a scratch vault from a local directory or public GitHub repo root URL in one command.
116
122
 
@@ -118,12 +124,22 @@ Quick-start a scratch vault from a local directory or public GitHub repo root UR
118
124
  - ingests the supplied directory as local sources, or registers/syncs the supplied public GitHub repo root URL
119
125
  - compiles the vault immediately
120
126
  - writes `wiki/graph/share-card.md`, `wiki/graph/share-card.svg`, and `wiki/graph/share-kit/`, then prints the paths
121
- - starts `graph serve` unless you pass `--no-serve`
127
+ - starts `graph serve` unless you pass `--no-serve` or `--no-viz`
128
+ - `--no-viz` is a compatibility alias for `--no-serve`
129
+ - `--mcp` starts the MCP stdio server after compile instead of the graph viewer
122
130
  - respects `--port` when you want a specific viewer port
123
131
  - for GitHub repo URLs, supports `--branch`, `--ref`, and `--checkout-dir`
124
132
 
125
133
  Use this when you want the fastest repo or docs-tree walkthrough without first deciding on managed-source registration.
126
134
 
135
+ ### `swarmvault clone <directory|github-url> [--no-viz] [--mcp] [--branch <name>] [--ref <ref>] [--checkout-dir <path>]`
136
+
137
+ Compatibility alias for `swarmvault scan`.
138
+
139
+ - initializes, ingests or registers the input, and compiles in one command
140
+ - supports the same public GitHub repo checkout flags as `scan`
141
+ - accepts `--no-serve`, `--no-viz`, `--mcp`, and `--port`
142
+
127
143
  ### `swarmvault demo [--port <port>] [--no-serve]`
128
144
 
129
145
  Create a temporary sample vault with bundled sources, compile it immediately, and launch the graph viewer unless you pass `--no-serve`.
@@ -142,6 +158,28 @@ Compare the current `state/graph.json` against the last committed graph in git.
142
158
  - when no git baseline exists, falls back to a summary of the current graph state
143
159
  - supports `--json` for structured automation output
144
160
 
161
+ ### `swarmvault chat [question...] [--resume [id]] [--list] [--delete <id>]`
162
+
163
+ Ask the compiled wiki in a persisted multi-turn session.
164
+
165
+ - without a question, opens an interactive TTY chat loop with `/help`, `/sessions`, `/status`, `/clear`, and `/exit`
166
+ - with a question, runs one turn and persists the transcript under `wiki/outputs/chat-sessions/`
167
+ - stores structured session state under `state/chat-sessions/`
168
+ - `--resume <id>` resumes by id or unique prefix; `--resume` alone resumes the most recent session
169
+ - `--list` prints saved sessions and `--delete <id>` removes one
170
+ - `--save-output` also writes each turn as a regular `wiki/outputs/` query page
171
+ - supports `--format markdown|report|slides|chart|image`, `--gap-fill`, and global `--json`
172
+
173
+ ### `swarmvault export ai [--out <dir>] [--max-full-chars <n>] [--no-page-siblings]`
174
+
175
+ Write a static AI handoff pack for agents, crawlers, and documentation systems.
176
+
177
+ - defaults to `wiki/exports/ai/`
178
+ - writes `llms.txt`, `llms-full.txt`, `graph.jsonld`, `manifest.json`, and `ai-readme.md`
179
+ - writes per-page `.txt` and `.json` siblings under `pages/` unless `--no-page-siblings` is passed
180
+ - caps `llms-full.txt` with `--max-full-chars` so huge vaults stay bounded
181
+ - includes SHA-256 hashes and file metadata in `manifest.json`
182
+
145
183
  ### `swarmvault doctor [--repair]`
146
184
 
147
185
  Run a whole-vault health check before handing the workspace to an agent or opening the live viewer.
@@ -440,6 +478,13 @@ Write a collapsible HTML source tree for the current `state/graph.json`.
440
478
  - `--max-children` caps very wide folders or modules with a `+N more` row
441
479
  - `--json` returns the output path, source count, node count, and tree payload
442
480
 
481
+ ### `swarmvault tree [--output <html>] [--root <path>] [--label <name>] [--max-children <n>]`
482
+
483
+ Compatibility alias for `swarmvault graph tree`.
484
+
485
+ - writes the same source/module/symbol tree
486
+ - returns the same JSON shape as `graph tree`
487
+
443
488
  ### `swarmvault graph merge <graph...> --out <path> [--label <name>]`
444
489
 
445
490
  Merge multiple graph JSON files into one namespaced graph artifact.
@@ -450,6 +495,13 @@ Merge multiple graph JSON files into one namespaced graph artifact.
450
495
  - maps explicit extracted/inferred/ambiguous edge evidence into SwarmVault edge semantics
451
496
  - `--json` returns the merged graph, input summaries, and warnings
452
497
 
498
+ ### `swarmvault merge-graphs <graph...> --out <path> [--label <name>]`
499
+
500
+ Compatibility alias for `swarmvault graph merge`.
501
+
502
+ - accepts the same SwarmVault and NetworkX/node-link graph inputs
503
+ - returns the same JSON shape as `graph merge`
504
+
453
505
  ### `swarmvault graph status [path]`
454
506
 
455
507
  Read-only graph freshness check for tracked repo roots or one explicit repo path.
@@ -461,6 +513,15 @@ Read-only graph freshness check for tracked repo roots or one explicit repo path
461
513
  - recommends `swarmvault compile` when graph/report artifacts are missing, non-code files changed, or a semantic refresh is pending
462
514
  - supports global `--json` for automation
463
515
 
516
+ ### `swarmvault watch [path] [--once] [--code-only] [--lint]`
517
+
518
+ Watch or refresh one explicit repo root without first writing `watch.repoRoots`.
519
+
520
+ - positional `[path]` is treated as a repo-root override and turns on repo mode
521
+ - `--once` runs one refresh cycle and exits
522
+ - `--code-only` limits the refresh to parser-backed repo graph artifacts
523
+ - repeated `--root <path>` remains available when you need multiple roots
524
+
464
525
  ### `swarmvault graph stats`
465
526
 
466
527
  Summarize the current compiled graph without opening the viewer.
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  addManagedSource,
13
13
  addWatchedRoot,
14
14
  archiveCandidate,
15
+ askChatSession,
15
16
  autoCommitWikiChanges,
16
17
  benchmarkVault,
17
18
  blastRadiusVault,
@@ -20,6 +21,7 @@ import {
20
21
  compileVault,
21
22
  consolidateVault,
22
23
  createSupersessionEdge,
24
+ deleteChatSession,
23
25
  deleteContextPack,
24
26
  deleteManagedSource,
25
27
  doctorRetrieval,
@@ -27,6 +29,7 @@ import {
27
29
  downloadWhisperModel,
28
30
  explainGraphVault,
29
31
  exploreVault,
32
+ exportAiPack,
30
33
  exportGraphFormat,
31
34
  exportGraphHtml,
32
35
  exportGraphReportHtml,
@@ -51,6 +54,7 @@ import {
51
54
  lintVault,
52
55
  listApprovals,
53
56
  listCandidates,
57
+ listChatSessions,
54
58
  listContextPacks,
55
59
  listGodNodes,
56
60
  listManagedSourceRecords,
@@ -315,9 +319,9 @@ program.name("swarmvault").description("SwarmVault is a local-first knowledge co
315
319
  function readCliVersion() {
316
320
  try {
317
321
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
318
- return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "3.10.0";
322
+ return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "3.12.0";
319
323
  } catch {
320
- return "3.10.0";
324
+ return "3.12.0";
321
325
  }
322
326
  }
323
327
  function parsePositiveInt(value, fallback) {
@@ -545,6 +549,209 @@ async function runGraphClusterCommand(options, rootDir = process2.cwd()) {
545
549
  `Refreshed ${result.communityCount} communities across ${result.nodeCount} nodes and ${result.edgeCount} edges. Report: ${result.reportPath}`
546
550
  );
547
551
  }
552
+ async function runGraphTreeCommand(options) {
553
+ const rootDir = options.root ? path2.resolve(process2.cwd(), options.root) : process2.cwd();
554
+ const result = await exportGraphTree(rootDir, options.output, {
555
+ label: options.label,
556
+ maxChildren: parsePositiveInt(options.maxChildren, 250)
557
+ });
558
+ if (isJson()) {
559
+ emitJson(result);
560
+ return;
561
+ }
562
+ log(`Graph tree: ${result.outputPath}`);
563
+ log(`Sources: ${result.sourceCount}; nodes: ${result.nodeCount}.`);
564
+ }
565
+ async function runGraphMergeCommand(graphPaths, options) {
566
+ const result = await mergeGraphFiles(
567
+ graphPaths.map((inputPath) => path2.resolve(process2.cwd(), inputPath)),
568
+ path2.resolve(process2.cwd(), options.out),
569
+ { label: options.label }
570
+ );
571
+ if (isJson()) {
572
+ emitJson(result);
573
+ return;
574
+ }
575
+ log(
576
+ `Merged ${result.inputGraphs.length} graph${result.inputGraphs.length === 1 ? "" : "s"} into ${result.outputPath}. Nodes ${result.graph.nodes.length}, edges ${result.graph.edges.length}.`
577
+ );
578
+ for (const warning of result.warnings) {
579
+ log(`Warning: ${warning}`);
580
+ }
581
+ }
582
+ async function runScanCommand(input, options) {
583
+ const rootDir = process2.cwd();
584
+ await initVault(rootDir, {});
585
+ if (!isJson()) {
586
+ log("Initialized workspace.");
587
+ }
588
+ const result = isHttpUrlInput(input) ? await addManagedSource(rootDir, input, {
589
+ compile: true,
590
+ brief: false,
591
+ branch: options.branch,
592
+ ref: options.ref,
593
+ checkoutDir: options.checkoutDir
594
+ }) : await ingestDirectory(rootDir, input, {});
595
+ if (!isJson()) {
596
+ if ("source" in result) {
597
+ log(
598
+ `Registered ${result.source.kind} source ${result.source.id}. Imported ${result.source.lastSyncCounts?.importedCount ?? 0}, updated ${result.source.lastSyncCounts?.updatedCount ?? 0}.`
599
+ );
600
+ } else {
601
+ log(`Ingested ${result.imported.length} file(s).`);
602
+ }
603
+ }
604
+ const compiled = "compile" in result && result.compile ? result.compile : await compileVault(rootDir, {});
605
+ const { paths } = await loadVaultConfig(rootDir);
606
+ const shareCardPath = path2.join(paths.wikiDir, "graph", "share-card.md");
607
+ const shareCardSvgPath = path2.join(paths.wikiDir, "graph", "share-card.svg");
608
+ const shareKitPath = path2.join(paths.wikiDir, "graph", "share-kit");
609
+ if (!isJson()) {
610
+ log(`Compiled ${compiled.sourceCount} source(s), ${compiled.pageCount} page(s).`);
611
+ log(`Share card: ${shareCardPath}`);
612
+ log(`Visual card: ${shareCardSvgPath}`);
613
+ log(`Share kit: ${shareKitPath}`);
614
+ log("Post text: swarmvault graph share --post");
615
+ }
616
+ if (options.mcp) {
617
+ process2.stderr.write(`${JSON.stringify({ status: "running", transport: "stdio", compiled: compiled.sourceCount })}
618
+ `);
619
+ const controller = await startMcpServer(rootDir);
620
+ process2.on("SIGINT", async () => {
621
+ try {
622
+ await controller.close();
623
+ } catch {
624
+ }
625
+ process2.exit(0);
626
+ });
627
+ return;
628
+ }
629
+ if (options.serve !== false && options.viz !== false) {
630
+ const port = options.port ? parsePositiveInt(options.port, 0) || void 0 : void 0;
631
+ const server = await startGraphServer(rootDir, port, { full: false });
632
+ if (isJson()) {
633
+ emitJson({
634
+ ...result,
635
+ compiled,
636
+ shareCardPath,
637
+ shareCardSvgPath,
638
+ shareKitPath,
639
+ port: server.port,
640
+ url: `http://localhost:${server.port}`
641
+ });
642
+ } else {
643
+ log(`Graph viewer running at http://localhost:${server.port}`);
644
+ }
645
+ process2.on("SIGINT", async () => {
646
+ try {
647
+ await server.close();
648
+ } catch {
649
+ }
650
+ process2.exit(0);
651
+ });
652
+ } else if (isJson()) {
653
+ emitJson({ ...result, compiled, shareCardPath, shareCardSvgPath, shareKitPath });
654
+ }
655
+ }
656
+ async function resolveChatResumeId(resume) {
657
+ if (!resume) {
658
+ return void 0;
659
+ }
660
+ if (typeof resume === "string") {
661
+ return resume;
662
+ }
663
+ const sessions = await listChatSessions(process2.cwd());
664
+ return sessions[0]?.id;
665
+ }
666
+ function logChatSessions(sessions) {
667
+ if (!sessions.length) {
668
+ log("No chat sessions yet.");
669
+ return;
670
+ }
671
+ for (const session of sessions) {
672
+ log(`${session.id} ${session.turnCount} turn${session.turnCount === 1 ? "" : "s"} ${session.title}`);
673
+ log(` updated: ${session.updatedAt}`);
674
+ log(` markdown: ${session.markdownPath}`);
675
+ }
676
+ }
677
+ async function runChatQuestion(question, options) {
678
+ const sessionId = await resolveChatResumeId(options.resume);
679
+ return askChatSession(process2.cwd(), {
680
+ question,
681
+ sessionId,
682
+ saveOutput: options.saveOutput ?? false,
683
+ gapFill: options.gapFill ?? false,
684
+ format: options.format,
685
+ maxHistoryTurns: parsePositiveInt(options.maxHistoryTurns, 6)
686
+ });
687
+ }
688
+ async function runInteractiveChat(options) {
689
+ if (isJson()) {
690
+ throw new Error("Interactive chat is not available with --json. Pass a question for one-shot JSON output.");
691
+ }
692
+ if (!process2.stdin.isTTY) {
693
+ throw new Error("Pass a chat question, or run `swarmvault chat` in an interactive terminal.");
694
+ }
695
+ let sessionId = await resolveChatResumeId(options.resume);
696
+ log(sessionId ? `Resuming chat session ${sessionId}.` : "Starting a new chat session.");
697
+ log("Type /help for commands or /exit to quit.");
698
+ const reader = createInterface({ input: process2.stdin, output: process2.stdout });
699
+ try {
700
+ while (true) {
701
+ const input = (await reader.question("swarmvault> ")).trim();
702
+ if (!input) {
703
+ continue;
704
+ }
705
+ if (input === "/exit" || input === "/quit") {
706
+ break;
707
+ }
708
+ if (input === "/help") {
709
+ log(
710
+ [
711
+ "/help Show commands",
712
+ "/sessions List chat sessions",
713
+ "/status Show vault health summary",
714
+ "/clear Start a fresh session",
715
+ "/exit Quit"
716
+ ].join("\n")
717
+ );
718
+ continue;
719
+ }
720
+ if (input === "/sessions") {
721
+ logChatSessions(await listChatSessions(process2.cwd()));
722
+ continue;
723
+ }
724
+ if (input === "/status") {
725
+ const report = await doctorVault(process2.cwd(), {});
726
+ log(`Vault health: ${report.ok ? "ok" : "needs attention"} (${report.recommendations.length} recommendation(s))`);
727
+ for (const recommendation of report.recommendations.slice(0, 5)) {
728
+ log(`- ${recommendation.label}: ${recommendation.command ?? recommendation.summary}`);
729
+ }
730
+ continue;
731
+ }
732
+ if (input === "/clear") {
733
+ sessionId = void 0;
734
+ log("Started a fresh chat session.");
735
+ continue;
736
+ }
737
+ const result = await askChatSession(process2.cwd(), {
738
+ question: input,
739
+ sessionId,
740
+ saveOutput: options.saveOutput ?? false,
741
+ gapFill: options.gapFill ?? false,
742
+ format: options.format,
743
+ maxHistoryTurns: parsePositiveInt(options.maxHistoryTurns, 6)
744
+ });
745
+ sessionId = result.session.id;
746
+ log(result.answer);
747
+ log(`Session: ${result.session.id}`);
748
+ log(`Saved transcript: ${result.markdownPath}`);
749
+ await maybeEmitHeuristicNotice(["chat"]);
750
+ }
751
+ } finally {
752
+ reader.close();
753
+ }
754
+ }
548
755
  program.hook("postAction", async (_thisCommand, actionCommand) => {
549
756
  const notices = await collectCliNotices({
550
757
  commandPath: getCommandPath(actionCommand),
@@ -923,6 +1130,44 @@ program.command("query").description("Query the compiled SwarmVault wiki.").argu
923
1130
  await maybeEmitHeuristicNotice(["query"]);
924
1131
  }
925
1132
  );
1133
+ program.command("chat").description("Ask the compiled wiki in a persisted multi-turn chat session.").argument("[question...]", "Question to ask in a chat session").option("--resume [id]", "Resume a chat session by id/prefix, or the most recent session when no id is supplied").option("--list", "List saved chat sessions", false).option("--delete <id>", "Delete a saved chat session by id/prefix").option("--save-output", "Also persist each answer as a regular wiki/outputs query page", false).option("--gap-fill", "Pull external web-search evidence when the local wiki has gaps (requires webSearch.tasks.queryProvider).").option("--max-history-turns <n>", "Number of prior turns to include as conversational context", "6").addOption(
1134
+ new Option("--format <format>", "Answer format for generated turns").choices(["markdown", "report", "slides", "chart", "image"]).default("markdown")
1135
+ ).action(
1136
+ async (questionParts, options) => {
1137
+ if (options.list) {
1138
+ const sessions = await listChatSessions(process2.cwd());
1139
+ if (isJson()) {
1140
+ emitJson(sessions);
1141
+ } else {
1142
+ logChatSessions(sessions);
1143
+ }
1144
+ return;
1145
+ }
1146
+ if (options.delete) {
1147
+ const deleted = await deleteChatSession(process2.cwd(), options.delete);
1148
+ if (isJson()) {
1149
+ emitJson(deleted);
1150
+ } else {
1151
+ log(`Deleted chat session ${deleted.id}`);
1152
+ }
1153
+ return;
1154
+ }
1155
+ const question = (questionParts ?? []).join(" ").trim();
1156
+ if (!question) {
1157
+ await runInteractiveChat(options);
1158
+ return;
1159
+ }
1160
+ const result = await runChatQuestion(question, options);
1161
+ if (isJson()) {
1162
+ emitJson(result);
1163
+ } else {
1164
+ log(result.answer);
1165
+ log(`Session: ${result.session.id}`);
1166
+ log(`Saved transcript: ${result.markdownPath}`);
1167
+ }
1168
+ await maybeEmitHeuristicNotice(["chat"]);
1169
+ }
1170
+ );
926
1171
  var context = program.command("context").description("Build and manage token-bounded agent context packs.");
927
1172
  context.command("build").description("Build a cited, token-bounded context pack for an agent task.").argument("<goal>", "Task, question, or goal the agent needs context for").option("--target <target>", "Optional page, node, path, project, or label to anchor the pack").option("--budget <tokens>", "Approximate token budget for included context", String(8e3)).option("--task <id>", "Attach the context pack to an agent task").option("--memory <id>", "Compatibility alias for --task").addOption(new Option("--format <format>", "Output format").choices(["markdown", "json", "llms"]).default("markdown")).action(
928
1173
  async (goal, options) => {
@@ -1190,6 +1435,23 @@ program.command("benchmark").description("Measure graph-guided context reduction
1190
1435
  }
1191
1436
  }
1192
1437
  });
1438
+ var exportCommand = program.command("export").description("Export portable SwarmVault artifacts.");
1439
+ exportCommand.command("ai").description("Export static AI handoff files for agents, crawlers, and documentation systems.").option("--out <dir>", "Output directory", path2.join("wiki", "exports", "ai")).option("--max-full-chars <n>", "Maximum characters to include in llms-full.txt", "5000000").option("--no-page-siblings", "Skip per-page .txt and .json sibling files").action(async (options) => {
1440
+ const result = await exportAiPack(process2.cwd(), {
1441
+ outDir: options.out,
1442
+ maxFullChars: parsePositiveInt(options.maxFullChars, 5e6),
1443
+ pageSiblings: options.pageSiblings ?? true
1444
+ });
1445
+ if (isJson()) {
1446
+ emitJson(result);
1447
+ return;
1448
+ }
1449
+ log(`Exported AI handoff pack to ${result.outputDir}`);
1450
+ log(`Files: ${result.files.length}; pages: ${result.pageCount}; nodes: ${result.nodeCount}; edges: ${result.edgeCount}`);
1451
+ if (result.truncatedFullText) {
1452
+ log("llms-full.txt was truncated; rerun with --max-full-chars for a larger export.");
1453
+ }
1454
+ });
1193
1455
  program.command("lint").description("Run anti-drift and wiki-health checks.").option("--deep", "Run LLM-powered advisory lint (default: from config)").option("--no-deep", "Skip deep lint even if enabled in config").option("--web", "Augment deep lint with configured web search", false).option("--conflicts", "Filter to contradiction findings only", false).option("--decay", "Filter to decay-related findings only", false).option("--tiers", "Filter to consolidation-tier findings only", false).action(async (options) => {
1194
1456
  const lintConfig = await loadVaultConfig(process2.cwd()).catch(() => null);
1195
1457
  const deepEnabled = options.decay || options.tiers ? false : options.deep ?? lintConfig?.config.profile.deepLintDefault ?? false;
@@ -1215,36 +1477,8 @@ program.command("lint").description("Run anti-drift and wiki-health checks.").op
1215
1477
  var graph = program.command("graph").description("Graph-related commands.").enablePositionalOptions();
1216
1478
  var graphPush = graph.command("push").description("Push the compiled graph into external sinks.");
1217
1479
  graph.command("update").alias("refresh").description("Refresh code-derived graph artifacts from tracked repo roots or one explicit repo path.").argument("[path]", "Optional repo root to refresh instead of configured/tracked roots").option("--lint", "Run lint after the refresh cycle", false).option("--force", "Allow graph updates even when node or edge counts shrink sharply", false).action(runGraphUpdateCommand);
1218
- graph.command("tree").description("Write a collapsible source/module/symbol tree for the compiled graph.").option("--output <html>", "Output HTML path (default: wiki/graph/tree.html)").option("--root <path>", "Vault root to read instead of the current directory").option("--label <name>", "Tree title").option("--max-children <n>", "Maximum children to render per tree node", "250").action(async (options) => {
1219
- const rootDir = options.root ? path2.resolve(process2.cwd(), options.root) : process2.cwd();
1220
- const result = await exportGraphTree(rootDir, options.output, {
1221
- label: options.label,
1222
- maxChildren: parsePositiveInt(options.maxChildren, 250)
1223
- });
1224
- if (isJson()) {
1225
- emitJson(result);
1226
- return;
1227
- }
1228
- log(`Graph tree: ${result.outputPath}`);
1229
- log(`Sources: ${result.sourceCount}; nodes: ${result.nodeCount}.`);
1230
- });
1231
- graph.command("merge").description("Merge SwarmVault or node-link JSON graph files into one namespaced graph artifact.").argument("<graphs...>", "Graph JSON files to merge").requiredOption("--out <path>", "Output graph JSON path").option("--label <name>", "Label/prefix to use when merging one graph").action(async (graphPaths, options) => {
1232
- const result = await mergeGraphFiles(
1233
- graphPaths.map((inputPath) => path2.resolve(process2.cwd(), inputPath)),
1234
- path2.resolve(process2.cwd(), options.out),
1235
- { label: options.label }
1236
- );
1237
- if (isJson()) {
1238
- emitJson(result);
1239
- return;
1240
- }
1241
- log(
1242
- `Merged ${result.inputGraphs.length} graph${result.inputGraphs.length === 1 ? "" : "s"} into ${result.outputPath}. Nodes ${result.graph.nodes.length}, edges ${result.graph.edges.length}.`
1243
- );
1244
- for (const warning of result.warnings) {
1245
- log(`Warning: ${warning}`);
1246
- }
1247
- });
1480
+ graph.command("tree").description("Write a collapsible source/module/symbol tree for the compiled graph.").option("--output <html>", "Output HTML path (default: wiki/graph/tree.html)").option("--root <path>", "Vault root to read instead of the current directory").option("--label <name>", "Tree title").option("--max-children <n>", "Maximum children to render per tree node", "250").action(runGraphTreeCommand);
1481
+ graph.command("merge").description("Merge SwarmVault or node-link JSON graph files into one namespaced graph artifact.").argument("<graphs...>", "Graph JSON files to merge").requiredOption("--out <path>", "Output graph JSON path").option("--label <name>", "Label/prefix to use when merging one graph").action(runGraphMergeCommand);
1248
1482
  graph.command("status").description("Read-only check for graph/report presence and tracked repo changes.").argument("[path]", "Optional repo root to check instead of configured/tracked roots").action(showGraphStatusCommand);
1249
1483
  graph.command("stats").description("Summarize compiled graph counts, node types, evidence classes, and relation mix.").action(async () => {
1250
1484
  const stats = await graphStatsVault(process2.cwd());
@@ -1691,14 +1925,16 @@ async function confirmInteractive(message) {
1691
1925
  rl.close();
1692
1926
  }
1693
1927
  }
1694
- var watch = program.command("watch").description("Watch the inbox directory and optionally tracked repos, or run one refresh cycle immediately.").option("--lint", "Run lint after each compile cycle", false).option("--repo", "Also refresh tracked repo sources and watch their repo roots", false).option("--once", "Run one import/refresh cycle immediately instead of starting a watcher", false).option("--code-only", "Only re-extract code sources (AST-only, no LLM re-analysis)", false).option("--debounce <ms>", "Debounce window in milliseconds", "900").option("--root <path>", "Watch this repo root instead of config/auto-discovery (repeat for multiple)", collectRepeated, []).option("--force", "Allow graph updates even when node or edge counts shrink sharply", false).action(
1695
- async (options) => {
1928
+ var watch = program.command("watch").description("Watch the inbox directory and optionally tracked repos, or run one refresh cycle immediately.").argument("[path]", "Optional repo root to watch or refresh instead of config/auto-discovery").option("--lint", "Run lint after each compile cycle", false).option("--repo", "Also refresh tracked repo sources and watch their repo roots", false).option("--once", "Run one import/refresh cycle immediately instead of starting a watcher", false).option("--code-only", "Only re-extract code sources (AST-only, no LLM re-analysis)", false).option("--debounce <ms>", "Debounce window in milliseconds", "900").option("--root <path>", "Watch this repo root instead of config/auto-discovery (repeat for multiple)", collectRepeated, []).option("--force", "Allow graph updates even when node or edge counts shrink sharply", false).action(
1929
+ async (targetPath, options) => {
1696
1930
  const debounceMs = parsePositiveInt(options.debounce, 900);
1697
- const overrideRoots = options.root && options.root.length > 0 ? options.root : void 0;
1931
+ const rootOverrides = [...targetPath ? [targetPath] : [], ...options.root ?? []];
1932
+ const overrideRoots = rootOverrides.length > 0 ? rootOverrides : void 0;
1933
+ const repoMode = options.repo || Boolean(targetPath);
1698
1934
  if (options.once) {
1699
1935
  const result = await runWatchCycle(process2.cwd(), {
1700
1936
  lint: options.lint ?? false,
1701
- repo: options.repo ?? false,
1937
+ repo: repoMode,
1702
1938
  codeOnly: options.codeOnly ?? false,
1703
1939
  debounceMs,
1704
1940
  force: options.force ?? false,
@@ -1708,7 +1944,7 @@ var watch = program.command("watch").description("Watch the inbox directory and
1708
1944
  emitJson(result);
1709
1945
  } else {
1710
1946
  log(
1711
- `Refreshed inbox${options.repo ? " and tracked repos" : ""}. Imported ${result.importedCount}, repo imported ${result.repoImportedCount}, repo updated ${result.repoUpdatedCount}, repo removed ${result.repoRemovedCount}.`
1947
+ `Refreshed inbox${repoMode ? " and tracked repos" : ""}. Imported ${result.importedCount}, repo imported ${result.repoImportedCount}, repo updated ${result.repoUpdatedCount}, repo removed ${result.repoRemovedCount}.`
1712
1948
  );
1713
1949
  }
1714
1950
  return;
@@ -1716,16 +1952,16 @@ var watch = program.command("watch").description("Watch the inbox directory and
1716
1952
  const { paths } = await loadVaultConfig(process2.cwd());
1717
1953
  const controller = await watchVault(process2.cwd(), {
1718
1954
  lint: options.lint ?? false,
1719
- repo: options.repo ?? false,
1955
+ repo: repoMode,
1720
1956
  codeOnly: options.codeOnly ?? false,
1721
1957
  debounceMs,
1722
1958
  force: options.force ?? false,
1723
1959
  overrideRoots
1724
1960
  });
1725
1961
  if (isJson()) {
1726
- emitJson({ status: "watching", inboxDir: paths.inboxDir, repo: options.repo ?? false });
1962
+ emitJson({ status: "watching", inboxDir: paths.inboxDir, repo: repoMode });
1727
1963
  } else {
1728
- log(`Watching inbox${options.repo ? " and tracked repos" : ""} for changes. Press Ctrl+C to stop.`);
1964
+ log(`Watching inbox${repoMode ? " and tracked repos" : ""} for changes. Press Ctrl+C to stop.`);
1729
1965
  }
1730
1966
  process2.on("SIGINT", async () => {
1731
1967
  try {
@@ -1785,6 +2021,8 @@ program.command("update").description("Compatibility alias for graph update: ref
1785
2021
  program.command("cluster-only").description("Compatibility alias for graph cluster: recompute graph communities and report artifacts without re-ingesting.").argument("[vault]", "Optional vault root to cluster instead of the current directory").option("--resolution <number>", "Override the Louvain community resolution for this run").action(
1786
2022
  (vaultPath, options) => runGraphClusterCommand(options, vaultPath ? path2.resolve(process2.cwd(), vaultPath) : process2.cwd())
1787
2023
  );
2024
+ program.command("tree").description("Compatibility alias for graph tree: write a collapsible source/module/symbol tree for the compiled graph.").option("--output <html>", "Output HTML path (default: wiki/graph/tree.html)").option("--root <path>", "Vault root to read instead of the current directory").option("--label <name>", "Tree title").option("--max-children <n>", "Maximum children to render per tree node", "250").action(runGraphTreeCommand);
2025
+ program.command("merge-graphs").description("Compatibility alias for graph merge: combine graph JSON files into one namespaced graph artifact.").argument("<graphs...>", "Graph JSON files to merge").requiredOption("--out <path>", "Output graph JSON path").option("--label <name>", "Label/prefix to use when merging one graph").action(runGraphMergeCommand);
1788
2026
  var hook = program.command("hook").description("Install local git hooks that keep tracked repos and the vault in sync.");
1789
2027
  hook.command("install").description("Install post-commit and post-checkout hooks for the nearest git repository.").action(async () => {
1790
2028
  const status = await installGitHooks(process2.cwd());
@@ -2265,67 +2503,8 @@ retrieval.command("doctor").description("Diagnose retrieval index problems and o
2265
2503
  log(`Warning: ${warning}`);
2266
2504
  }
2267
2505
  });
2268
- program.command("scan").description("Quick-start: initialize, ingest, compile, and serve a graph viewer in one command.").argument("<input>", "Directory or public GitHub repo root URL to scan").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").option("--branch <name>", "GitHub branch to clone when scanning a public repo URL").option("--ref <ref>", "Git ref, tag, or commit to check out when scanning a public repo URL").option("--checkout-dir <path>", "Persistent checkout directory for a public GitHub repo scan").action(async (input, options) => {
2269
- const rootDir = process2.cwd();
2270
- await initVault(rootDir, {});
2271
- if (!isJson()) {
2272
- log("Initialized workspace.");
2273
- }
2274
- const result = isHttpUrlInput(input) ? await addManagedSource(rootDir, input, {
2275
- compile: true,
2276
- brief: false,
2277
- branch: options.branch,
2278
- ref: options.ref,
2279
- checkoutDir: options.checkoutDir
2280
- }) : await ingestDirectory(rootDir, input, {});
2281
- if (!isJson()) {
2282
- if ("source" in result) {
2283
- log(
2284
- `Registered ${result.source.kind} source ${result.source.id}. Imported ${result.source.lastSyncCounts?.importedCount ?? 0}, updated ${result.source.lastSyncCounts?.updatedCount ?? 0}.`
2285
- );
2286
- } else {
2287
- log(`Ingested ${result.imported.length} file(s).`);
2288
- }
2289
- }
2290
- const compiled = "compile" in result && result.compile ? result.compile : await compileVault(rootDir, {});
2291
- const { paths } = await loadVaultConfig(rootDir);
2292
- const shareCardPath = path2.join(paths.wikiDir, "graph", "share-card.md");
2293
- const shareCardSvgPath = path2.join(paths.wikiDir, "graph", "share-card.svg");
2294
- const shareKitPath = path2.join(paths.wikiDir, "graph", "share-kit");
2295
- if (!isJson()) {
2296
- log(`Compiled ${compiled.sourceCount} source(s), ${compiled.pageCount} page(s).`);
2297
- log(`Share card: ${shareCardPath}`);
2298
- log(`Visual card: ${shareCardSvgPath}`);
2299
- log(`Share kit: ${shareKitPath}`);
2300
- log("Post text: swarmvault graph share --post");
2301
- }
2302
- if (options.serve !== false) {
2303
- const port = options.port ? parsePositiveInt(options.port, 0) || void 0 : void 0;
2304
- const server = await startGraphServer(rootDir, port, { full: false });
2305
- if (isJson()) {
2306
- emitJson({
2307
- ...result,
2308
- compiled,
2309
- shareCardPath,
2310
- shareCardSvgPath,
2311
- shareKitPath,
2312
- port: server.port,
2313
- url: `http://localhost:${server.port}`
2314
- });
2315
- } else {
2316
- log(`Graph viewer running at http://localhost:${server.port}`);
2317
- }
2318
- process2.on("SIGINT", async () => {
2319
- try {
2320
- await server.close();
2321
- } catch {
2322
- }
2323
- process2.exit(0);
2324
- });
2325
- } else if (isJson()) {
2326
- emitJson({ ...result, compiled, shareCardPath, shareCardSvgPath, shareKitPath });
2327
- }
2328
- });
2506
+ program.command("scan").description("Quick-start: initialize, ingest, compile, and serve a graph viewer in one command.").argument("<input>", "Directory or public GitHub repo root URL to scan").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").option("--no-viz", "Compatibility alias for --no-serve; skip launching the graph viewer after compile").option("--mcp", "Start the MCP stdio server after compile instead of launching the graph viewer", false).option("--branch <name>", "GitHub branch to clone when scanning a public repo URL").option("--ref <ref>", "Git ref, tag, or commit to check out when scanning a public repo URL").option("--checkout-dir <path>", "Persistent checkout directory for a public GitHub repo scan").action(runScanCommand);
2507
+ program.command("clone").description("Compatibility alias for scan: initialize, clone/register a public repo URL, and compile it into the vault.").argument("<input>", "Public GitHub repo URL or local directory to scan").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").option("--no-viz", "Compatibility alias for --no-serve; skip launching the graph viewer after compile").option("--mcp", "Start the MCP stdio server after compile instead of launching the graph viewer", false).option("--branch <name>", "GitHub branch to clone when scanning a public repo URL").option("--ref <ref>", "Git ref, tag, or commit to check out when scanning a public repo URL").option("--checkout-dir <path>", "Persistent checkout directory for a public GitHub repo scan").action(runScanCommand);
2329
2508
  function enableStructuredJsonOnSubcommands(command) {
2330
2509
  for (const subcommand of command.commands) {
2331
2510
  const hasJsonOption = subcommand.options.some((option) => option.attributeName() === "json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmvaultai/cli",
3
- "version": "3.10.0",
3
+ "version": "3.12.0",
4
4
  "description": "Global CLI for SwarmVault.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -44,7 +44,7 @@
44
44
  "prepublishOnly": "node ../../scripts/check-release-sync.mjs && node ../../scripts/check-published-manifests.mjs"
45
45
  },
46
46
  "dependencies": {
47
- "@swarmvaultai/engine": "3.10.0",
47
+ "@swarmvaultai/engine": "3.12.0",
48
48
  "commander": "^14.0.1"
49
49
  },
50
50
  "devDependencies": {