@swarmvaultai/cli 0.7.24 → 0.7.26

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 +78 -9
  2. package/dist/index.js +353 -26
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@swarmvaultai/cli` is the global command-line entry point for SwarmVault.
4
4
 
5
- It gives you the `swarmvault` command for building a local-first knowledge vault from files, reStructuredText and DOCX documents, URLs, browser clips, saved query outputs, and guided exploration runs.
5
+ It gives you the `swarmvault` command for building a local-first knowledge vault from files, audio transcripts, YouTube URLs, reStructuredText and DOCX documents, browser clips, saved query outputs, and guided exploration runs.
6
6
 
7
7
  ## Install
8
8
 
@@ -24,6 +24,7 @@ mkdir my-vault
24
24
  cd my-vault
25
25
  swarmvault init --obsidian --profile personal-research
26
26
  swarmvault init --obsidian --profile reader,timeline
27
+ swarmvault demo
27
28
  swarmvault source add https://github.com/karpathy/micrograd
28
29
  swarmvault source add https://example.com/docs/getting-started
29
30
  swarmvault source add ./exports/customer-call.srt --guide
@@ -32,20 +33,25 @@ swarmvault source list
32
33
  swarmvault source reload --all
33
34
  sed -n '1,120p' swarmvault.schema.md
34
35
  swarmvault ingest ./notes.md
36
+ swarmvault ingest ./customer-call.mp3
37
+ swarmvault ingest https://www.youtube.com/watch?v=dQw4w9WgXcQ
35
38
  swarmvault ingest ./repo
36
39
  swarmvault add https://arxiv.org/abs/2401.12345
37
- swarmvault compile
40
+ swarmvault compile --max-tokens 120000
41
+ swarmvault diff
38
42
  swarmvault benchmark
39
- swarmvault query "What keeps recurring?"
43
+ swarmvault query "What keeps recurring?" --commit
40
44
  swarmvault query "Turn this into slides" --format slides
41
45
  swarmvault explore "What should I research next?" --steps 3
42
46
  swarmvault lint --deep
47
+ swarmvault graph blast ./src/index.ts
43
48
  swarmvault graph query "Which nodes bridge the biggest clusters?"
44
49
  swarmvault graph explain "concept:drift"
45
50
  swarmvault watch status
46
51
  swarmvault watch --repo --once
47
52
  swarmvault hook install
48
53
  swarmvault graph serve
54
+ swarmvault graph export --report ./exports/report.html
49
55
  swarmvault graph export --html ./exports/graph.html
50
56
  swarmvault graph export --cypher ./exports/graph.cypher
51
57
  swarmvault graph push neo4j --dry-run
@@ -84,6 +90,23 @@ Quick-start a scratch vault from a local directory in one command.
84
90
 
85
91
  Use this when you want the fastest repo or docs-tree walkthrough without first deciding on managed-source registration.
86
92
 
93
+ ### `swarmvault demo [--port <port>] [--no-serve]`
94
+
95
+ Create a temporary sample vault with bundled sources, compile it immediately, and launch the graph viewer unless you pass `--no-serve`.
96
+
97
+ - writes the demo vault under the system temp directory
98
+ - requires no API keys or extra setup
99
+ - is the fastest way to inspect the full init + ingest + compile + graph workflow on a clean machine
100
+ - respects `--port` when you want a specific viewer port
101
+
102
+ ### `swarmvault diff`
103
+
104
+ Compare the current `state/graph.json` against the last committed graph in git.
105
+
106
+ - when a prior committed graph exists, prints added and removed nodes, pages, and edges
107
+ - when no git baseline exists, falls back to a summary of the current graph state
108
+ - supports `--json` for structured automation output
109
+
87
110
  ### `swarmvault source add|list|reload|review|guide|session|delete`
88
111
 
89
112
  Manage recurring source roots through a registry-backed workflow.
@@ -121,7 +144,7 @@ Source-scoped artifacts are intentionally split by role:
121
144
  | Source guide | `source guide`, `source add --guide`, `ingest --guide` | Guided walkthrough with approval-bundled updates in `wiki/outputs/source-guides/` |
122
145
  | Source session | `source session`, `source add --guide`, `ingest --guide` | Resumable workflow state in `wiki/outputs/source-sessions/` and `state/source-sessions/` |
123
146
 
124
- ### `swarmvault ingest <path-or-url>`
147
+ ### `swarmvault ingest <path-or-url> [--commit]`
125
148
 
126
149
  Ingest a local file path, directory path, or URL into immutable source storage and write manifests to `state/manifests/`.
127
150
 
@@ -130,7 +153,8 @@ Ingest a local file path, directory path, or URL into immutable source storage a
130
153
  - repo-aware directory ingest records `repoRelativePath` and later compile writes `state/code-index.json`
131
154
  - use `source add` instead when the same local directory, public GitHub repo root, or docs hub should stay registered and reloadable
132
155
  - URL ingest still localizes remote image references by default
133
- - local file and archive ingest supports markdown, text, reStructuredText, HTML, PDF, Word, RTF, OpenDocument, EPUB, CSV/TSV, Excel, PowerPoint, Jupyter notebooks, BibTeX, Org-mode, AsciiDoc, transcripts, Slack exports, email, calendar, structured config/data, developer manifests, images, and code
156
+ - YouTube URLs short-circuit to direct transcript capture instead of generic HTML fetch
157
+ - local file and archive ingest supports markdown, text, reStructuredText, HTML, PDF, Word, RTF, OpenDocument, EPUB, CSV/TSV, Excel, PowerPoint, Jupyter notebooks, BibTeX, Org-mode, AsciiDoc, transcripts, Slack exports, email, calendar, audio, structured config/data, developer manifests, images, and code
134
158
  - add `--guide` when you want a resumable source session, source brief, source review, source guide, and approval-bundled canonical page edits when `profile.guidedSessionMode` is `canonical_review`, with `wiki/insights/` fallback for `insights_only`
135
159
  - set `profile.guidedIngestDefault: true` when guided mode should be the default for `ingest`, and use `--no-guide` to force a plain ingest for one run
136
160
  - code-aware directory ingest currently covers JavaScript, JSX, TypeScript, TSX, Bash/shell scripts, Python, Go, Rust, Java, Kotlin, Scala, Dart, Lua, Zig, C#, C, C++, PHP, Ruby, PowerShell, Elixir, OCaml, Objective-C, ReScript, Solidity, HTML, CSS, and Vue single-file components
@@ -149,11 +173,16 @@ Useful flags:
149
173
  - `--no-gitignore`
150
174
  - `--no-include-assets`
151
175
  - `--max-asset-size <bytes>`
176
+ - `--commit`
152
177
 
153
178
  Repo ingest defaults to `first_party` material. The extra `--include-*` flags opt dependency trees, resource bundles, and generated output back in when you actually want them in the vault.
154
179
 
155
180
  Large repo ingest now emits low-noise progress on materially large batches, and parser compatibility failures stay local to the affected source instead of aborting unrelated analysis.
156
181
 
182
+ Audio files use `tasks.audioProvider` when you configure a provider with `audio` capability. When no such provider is configured, SwarmVault still ingests the source and records an explicit extraction warning instead of failing. YouTube transcript ingest does not require a model provider.
183
+
184
+ When `--commit` is set, SwarmVault stages `wiki/` and `state/` changes and creates a git commit when the vault root is inside a git worktree. Outside git, it becomes a no-op instead of failing.
185
+
157
186
  ### `swarmvault add <url>`
158
187
 
159
188
  Capture supported URLs through a normalized markdown layer before ingesting them into the vault.
@@ -171,7 +200,7 @@ Capture supported URLs through a normalized markdown layer before ingesting them
171
200
 
172
201
  Import supported files from the configured inbox directory. This is meant for browser-clipper style markdown bundles, HTML clip bundles, and other capture workflows. Local image and asset references are preserved and copied into canonical storage under `raw/assets/`.
173
202
 
174
- ### `swarmvault compile [--approve]`
203
+ ### `swarmvault compile [--approve] [--commit] [--max-tokens <n>]`
175
204
 
176
205
  Compile the current manifests into:
177
206
 
@@ -187,6 +216,14 @@ New concept and entity pages are staged into `wiki/candidates/` first. A later m
187
216
 
188
217
  With `--approve`, compile writes a staged review bundle into `state/approvals/` without applying active wiki changes.
189
218
 
219
+ Useful flags:
220
+
221
+ - `--approve`
222
+ - `--commit`
223
+ - `--max-tokens <n>`
224
+
225
+ `--max-tokens <n>` keeps the generated wiki inside a bounded token budget by dropping lower-priority pages from final `wiki/` output and reporting token-budget stats in the compile result. `--commit` immediately commits `wiki/` and `state/` changes when the vault lives in a git repo.
226
+
190
227
  ### `swarmvault benchmark [--question "<text>" ...]`
191
228
 
192
229
  Measure graph-guided context reduction against a naive full-corpus read.
@@ -218,7 +255,7 @@ Inspect and resolve staged concept and entity candidates.
218
255
 
219
256
  Targets can be page ids or relative paths under `wiki/candidates/`.
220
257
 
221
- ### `swarmvault query "<question>" [--no-save] [--format markdown|report|slides|chart|image]`
258
+ ### `swarmvault query "<question>" [--no-save] [--commit] [--format markdown|report|slides|chart|image]`
222
259
 
223
260
  Query the compiled vault. The query layer also reads `swarmvault.schema.md`, so answers follow the vault’s own structure and grounding rules.
224
261
 
@@ -233,6 +270,8 @@ Saved outputs also carry related page, node, and source metadata so SwarmVault c
233
270
 
234
271
  Human-authored pages in `wiki/insights/` are also indexed into search and query context, but SwarmVault does not rewrite them after initialization.
235
272
 
273
+ By default, query uses the local SQLite search index. When an embedding-capable provider is available and `search.hybrid` is not disabled, semantic page matches are fused into the same candidate set before answer generation. `tasks.embeddingProvider` is the explicit way to choose that backend, but SwarmVault can also fall back to a `queryProvider` with embeddings support. Set `search.rerank: true` when you want the configured `queryProvider` to rerank the merged top hits. `--commit` immediately commits saved `wiki/` and `state/` changes when the vault root is inside a git repo.
274
+
236
275
  ### `swarmvault explore "<question>" [--steps <n>] [--format markdown|report|slides|chart|image]`
237
276
 
238
277
  Run a save-first multi-step research loop.
@@ -299,6 +338,9 @@ Run SwarmVault as a local MCP server over stdio. This exposes the vault to compa
299
338
  - `ingest_input`
300
339
  - `compile_vault`
301
340
  - `lint_vault`
341
+ - `blast_radius`
342
+
343
+ `compile_vault` also accepts `maxTokens` for bounded wiki output, and `blast_radius` traces reverse import impact for a file or module target.
302
344
 
303
345
  The MCP surface also exposes `swarmvault://schema`, `swarmvault://sessions`, `swarmvault://sessions/{path}`, and includes `schemaPath` in `workspace_info`.
304
346
 
@@ -306,6 +348,8 @@ The MCP surface also exposes `swarmvault://schema`, `swarmvault://sessions`, `sw
306
348
 
307
349
  Start the local graph workspace backed by `state/graph.json`, `/api/search`, `/api/page`, and local graph query/path/explain endpoints.
308
350
 
351
+ It also exposes `/api/bookmarklet` and `/api/clip`, so a running local viewer can ingest the current browser page through a bookmarklet without leaving the browser.
352
+
309
353
  ### `swarmvault graph query "<question>" [--dfs] [--budget <n>]`
310
354
 
311
355
  Run a deterministic local graph traversal seeded from local search, graph labels, and matching group patterns.
@@ -322,21 +366,32 @@ Inspect graph metadata, community membership, neighbors, provenance, and group-p
322
366
 
323
367
  List the most connected bridge-heavy nodes in the current graph.
324
368
 
325
- ### `swarmvault graph export --html|--html-standalone|--svg|--graphml|--cypher|--json|--obsidian|--canvas <output>`
369
+ ### `swarmvault graph blast <target> [--depth <n>]`
370
+
371
+ Trace the reverse-import blast radius of changing a file or module.
372
+
373
+ - accepts a file path, module label, or module id
374
+ - follows reverse `imports` edges through the compiled graph
375
+ - reports affected modules by depth so you can estimate downstream impact before editing
376
+
377
+ ### `swarmvault graph export --html|--html-standalone|--report|--svg|--graphml|--cypher|--json|--obsidian|--canvas <output>`
326
378
 
327
379
  Export the current graph as one or more shareable formats:
328
380
 
329
381
  - `--html` for the full self-contained read-only graph workspace
330
382
  - `--html-standalone` for a lighter vis.js export with node search, legend, and sidebar inspection
383
+ - `--report` for a self-contained HTML graph report with stats, key nodes, communities, and warnings
331
384
  - `--svg` for a static shareable diagram
332
385
  - `--graphml` for graph-tool interoperability
333
386
  - `--cypher` for Neo4j-style import scripts
334
387
  - `--json` for a deterministic machine-readable graph package
335
- - `--obsidian` for an Obsidian-friendly markdown vault with one note per node plus community notes
388
+ - `--obsidian` for an Obsidian-friendly markdown vault that preserves wiki folders, appends graph connections, emits orphan-node stubs and community notes, copies assets, and writes a minimal `.obsidian/` config
336
389
  - `--canvas` for an Obsidian canvas grouped by community
337
390
 
338
391
  You can combine multiple flags in one run to write several exports at once.
339
392
 
393
+ Set `graph.communityResolution` in `swarmvault.config.json` when you want to pin the Louvain clustering resolution used by graph reports and Obsidian community output instead of relying on the adaptive default.
394
+
340
395
  ### `swarmvault graph push neo4j`
341
396
 
342
397
  Push the compiled graph directly into Neo4j over Bolt/Aura instead of writing an intermediate file.
@@ -463,6 +518,20 @@ Deep lint web augmentation uses a separate `webSearch` config block. Example:
463
518
  }
464
519
  ```
465
520
 
521
+ Search behavior is configurable separately from provider routing:
522
+
523
+ ```json
524
+ {
525
+ "search": {
526
+ "hybrid": true,
527
+ "rerank": false
528
+ }
529
+ }
530
+ ```
531
+
532
+ - `search.hybrid` defaults to enabled and merges full-text hits with semantic page matches when an embedding-capable provider is available
533
+ - `search.rerank` optionally asks the current `queryProvider` to rerank the merged top hits before query answers are generated
534
+
466
535
  ## Troubleshooting
467
536
 
468
537
  - If you are running from a source checkout and `graph serve` says the viewer build is missing, run `pnpm build` in the repository first
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync } from "fs";
5
+ import { readFile as readFile2 } from "fs/promises";
5
6
  import process2 from "process";
6
7
  import { createInterface } from "readline/promises";
7
8
  import {
@@ -9,17 +10,21 @@ import {
9
10
  addInput,
10
11
  addManagedSource,
11
12
  archiveCandidate,
13
+ autoCommitWikiChanges,
12
14
  benchmarkVault,
15
+ blastRadiusVault,
13
16
  compileVault,
14
17
  deleteManagedSource,
15
18
  explainGraphVault,
16
19
  exploreVault,
17
20
  exportGraphFormat,
18
21
  exportGraphHtml,
22
+ exportGraphReportHtml,
19
23
  exportObsidianCanvas,
20
24
  exportObsidianVault,
21
25
  getGitHookStatus,
22
26
  getWatchStatus,
27
+ graphDiff,
23
28
  guideManagedSource,
24
29
  guideSourceScope,
25
30
  importInbox,
@@ -266,13 +271,14 @@ function normalizeVersion(version) {
266
271
  // src/index.ts
267
272
  var program = new Command();
268
273
  var CLI_VERSION = readCliVersion();
269
- program.name("swarmvault").description("SwarmVault is a local-first knowledge compiler with graph outputs and optional provider-backed workflows.").version(CLI_VERSION).option("--json", "Emit structured JSON output", false);
274
+ var activeCommand = null;
275
+ program.name("swarmvault").description("SwarmVault is a local-first knowledge compiler with graph outputs and optional provider-backed workflows.").version(CLI_VERSION).enablePositionalOptions().option("--json", "Emit structured JSON output", false);
270
276
  function readCliVersion() {
271
277
  try {
272
278
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
273
- return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "0.7.24";
279
+ return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "0.7.26";
274
280
  } catch {
275
- return "0.7.24";
281
+ return "0.7.26";
276
282
  }
277
283
  }
278
284
  function parsePositiveInt(value, fallback) {
@@ -296,7 +302,7 @@ function slugForCli(value) {
296
302
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "source";
297
303
  }
298
304
  function isJson() {
299
- return program.opts().json === true;
305
+ return activeCommand?.opts().json === true || program.opts().json === true;
300
306
  }
301
307
  function emitJson(data) {
302
308
  process2.stdout.write(`${JSON.stringify(data)}
@@ -429,7 +435,7 @@ program.command("init").description("Initialize a SwarmVault workspace in the cu
429
435
  log("Initialized SwarmVault workspace.");
430
436
  }
431
437
  });
432
- program.command("ingest").description("Ingest a local file path, directory path, or URL into the raw SwarmVault workspace.").argument("<input>", "Local file path, directory path, or URL").option("--review", "Stage a source review artifact after ingest and compile", false).option("--guide", "Stage a guided source integration bundle after ingest and compile (default: from config)").option("--no-guide", "Skip guided mode even if enabled in config").option("--answers-file <path>", "JSON file with guided-session answers keyed by question id or listed in prompt order").option("--include-assets", "Download remote image assets when ingesting URLs", true).option("--no-include-assets", "Skip downloading remote image assets when ingesting URLs").option("--max-asset-size <bytes>", "Maximum number of bytes to fetch for a single remote image asset").option("--repo-root <path>", "Override the detected repo root when ingesting a directory").option("--include <glob...>", "Only ingest files matching one or more glob patterns").option("--exclude <glob...>", "Skip files matching one or more glob patterns").option("--max-files <n>", "Maximum number of files to ingest from a directory").option("--include-third-party", "Also ingest repo files classified as third-party", false).option("--include-resources", "Also ingest repo files classified as resources", false).option("--include-generated", "Also ingest repo files classified as generated output", false).option("--no-gitignore", "Ignore .gitignore rules when ingesting a directory").action(
438
+ program.command("ingest").description("Ingest a local file path, directory path, or URL into the raw SwarmVault workspace.").argument("<input>", "Local file path, directory path, or URL").option("--review", "Stage a source review artifact after ingest and compile", false).option("--guide", "Stage a guided source integration bundle after ingest and compile (default: from config)").option("--no-guide", "Skip guided mode even if enabled in config").option("--answers-file <path>", "JSON file with guided-session answers keyed by question id or listed in prompt order").option("--include-assets", "Download remote image assets when ingesting URLs", true).option("--no-include-assets", "Skip downloading remote image assets when ingesting URLs").option("--max-asset-size <bytes>", "Maximum number of bytes to fetch for a single remote image asset").option("--repo-root <path>", "Override the detected repo root when ingesting a directory").option("--include <glob...>", "Only ingest files matching one or more glob patterns").option("--exclude <glob...>", "Skip files matching one or more glob patterns").option("--max-files <n>", "Maximum number of files to ingest from a directory").option("--include-third-party", "Also ingest repo files classified as third-party", false).option("--include-resources", "Also ingest repo files classified as resources", false).option("--include-generated", "Also ingest repo files classified as generated output", false).option("--no-gitignore", "Ignore .gitignore rules when ingesting a directory").option("--commit", "Auto-commit wiki and state changes after ingest").action(
433
439
  async (input, options) => {
434
440
  const guideAnswers = readGuideAnswersFile(options.answersFile);
435
441
  const vaultConfig = await loadVaultConfig(process2.cwd()).catch(() => null);
@@ -502,6 +508,10 @@ program.command("ingest").description("Ingest a local file path, directory path,
502
508
  log(`Staged guided session at ${completedGuide2.guidePath}.`);
503
509
  }
504
510
  }
511
+ if (options.commit) {
512
+ const msg = await autoCommitWikiChanges(process2.cwd(), "ingest", input, { force: true });
513
+ if (msg && !isJson()) log(`Committed: ${msg}`);
514
+ }
505
515
  return;
506
516
  }
507
517
  const ingest = await ingestInputDetailed(process2.cwd(), input, commonOptions);
@@ -537,6 +547,10 @@ program.command("ingest").description("Ingest a local file path, directory path,
537
547
  log(`Staged guided session at ${completedGuide.guidePath}.`);
538
548
  }
539
549
  }
550
+ if (options.commit) {
551
+ const msg = await autoCommitWikiChanges(process2.cwd(), "ingest", input, { force: true });
552
+ if (msg && !isJson()) log(`Committed: ${msg}`);
553
+ }
540
554
  }
541
555
  );
542
556
  program.command("add").description("Capture supported URLs into normalized markdown before ingesting them.").argument("<input>", "Supported URL or bare arXiv id").option("--author <name>", "Human author or curator for this capture").option("--contributor <name>", "Additional contributor metadata for this capture").action(async (input, options) => {
@@ -674,8 +688,9 @@ inbox.command("import").description("Import supported files from the configured
674
688
  );
675
689
  }
676
690
  });
677
- program.command("compile").description("Compile manifests into wiki pages, graph JSON, and search index.").option("--approve", "Stage a review bundle without applying active page changes", false).action(async (options) => {
678
- const result = await compileVault(process2.cwd(), { approve: options.approve ?? false });
691
+ program.command("compile").description("Compile manifests into wiki pages, graph JSON, and search index.").option("--approve", "Stage a review bundle without applying active page changes", false).option("--commit", "Auto-commit wiki and state changes after compile").option("--max-tokens <n>", "Cap wiki output by trimming lower-priority pages").action(async (options) => {
692
+ const maxTokens = options.maxTokens ? parsePositiveInt(options.maxTokens, 0) || void 0 : void 0;
693
+ const result = await compileVault(process2.cwd(), { approve: options.approve ?? false, maxTokens });
679
694
  if (isJson()) {
680
695
  emitJson(result);
681
696
  } else {
@@ -684,27 +699,44 @@ program.command("compile").description("Compile manifests into wiki pages, graph
684
699
  } else {
685
700
  log(`Compiled ${result.sourceCount} source(s), ${result.pageCount} page(s). Changed: ${result.changedPages.length}.`);
686
701
  }
702
+ if (result.tokenStats) {
703
+ log(
704
+ `Token budget: ~${result.tokenStats.estimatedTokens} tokens, kept ${result.tokenStats.pagesKept} pages, dropped ${result.tokenStats.pagesDropped}.`
705
+ );
706
+ }
707
+ }
708
+ if (options.commit) {
709
+ const msg = await autoCommitWikiChanges(process2.cwd(), "compile", `${result.sourceCount} sources, ${result.pageCount} pages`, {
710
+ force: true
711
+ });
712
+ if (msg && !isJson()) log(`Committed: ${msg}`);
687
713
  }
688
714
  await maybeEmitHeuristicNotice(["compile"]);
689
715
  });
690
- program.command("query").description("Query the compiled SwarmVault wiki.").argument("<question>", "Question to ask SwarmVault").option("--no-save", "Do not persist the answer to wiki/outputs").addOption(
716
+ program.command("query").description("Query the compiled SwarmVault wiki.").argument("<question>", "Question to ask SwarmVault").option("--no-save", "Do not persist the answer to wiki/outputs").option("--commit", "Auto-commit wiki changes after query").addOption(
691
717
  new Option("--format <format>", "Output format").choices(["markdown", "report", "slides", "chart", "image"]).default("markdown")
692
- ).action(async (question, options) => {
693
- const result = await queryVault(process2.cwd(), {
694
- question,
695
- save: options.save ?? true,
696
- format: options.format
697
- });
698
- if (isJson()) {
699
- emitJson(result);
700
- } else {
701
- log(result.answer);
702
- if (result.savedPath) {
703
- log(`Saved to ${result.savedPath}`);
718
+ ).action(
719
+ async (question, options) => {
720
+ const result = await queryVault(process2.cwd(), {
721
+ question,
722
+ save: options.save ?? true,
723
+ format: options.format
724
+ });
725
+ if (isJson()) {
726
+ emitJson(result);
727
+ } else {
728
+ log(result.answer);
729
+ if (result.savedPath) {
730
+ log(`Saved to ${result.savedPath}`);
731
+ }
732
+ }
733
+ if (options.commit) {
734
+ const msg = await autoCommitWikiChanges(process2.cwd(), "query", question.slice(0, 72), { force: true });
735
+ if (msg && !isJson()) log(`Committed: ${msg}`);
704
736
  }
737
+ await maybeEmitHeuristicNotice(["query"]);
705
738
  }
706
- await maybeEmitHeuristicNotice(["query"]);
707
- });
739
+ );
708
740
  program.command("explore").description("Run a save-first multi-step exploration loop against the vault.").argument("<question>", "Root question to explore").option("--steps <n>", "Maximum number of exploration steps", "3").addOption(
709
741
  new Option("--format <format>", "Output format for step pages").choices(["markdown", "report", "slides", "chart", "image"]).default("markdown")
710
742
  ).action(async (question, options) => {
@@ -760,7 +792,7 @@ program.command("lint").description("Run anti-drift and wiki-health checks.").op
760
792
  log(`[${finding.severity}] ${finding.code}: ${finding.message}${finding.pagePath ? ` (${finding.pagePath})` : ""}`);
761
793
  }
762
794
  });
763
- var graph = program.command("graph").description("Graph-related commands.");
795
+ var graph = program.command("graph").description("Graph-related commands.").enablePositionalOptions();
764
796
  var graphPush = graph.command("push").description("Push the compiled graph into external sinks.");
765
797
  graphPush.command("neo4j").description("Push the compiled graph directly into Neo4j over Bolt/Aura.").option("--uri <bolt-uri>", "Neo4j Bolt or Aura URI").option("--username <user>", "Neo4j username").option("--password-env <env-var>", "Environment variable containing the Neo4j password").option("--database <name>", "Neo4j database name").option("--vault-id <id>", "Stable vault identifier used for shared-database namespacing").option("--batch-size <n>", "Maximum rows to write per Neo4j transaction batch").option("--include-third-party", "Also push third-party repo material", false).option("--include-resources", "Also push resource-like content", false).option("--include-generated", "Also push generated output", false).option("--dry-run", "Show what would be pushed without writing to Neo4j", false).action(
766
798
  async (options) => {
@@ -805,6 +837,7 @@ graph.command("serve").description("Serve the local graph viewer.").option("--po
805
837
  emitJson({ port: server.port, url: `http://localhost:${server.port}` });
806
838
  } else {
807
839
  log(`Graph viewer running at http://localhost:${server.port}`);
840
+ log(`Browser clipper: http://localhost:${server.port}/api/bookmarklet`);
808
841
  }
809
842
  process2.on("SIGINT", async () => {
810
843
  try {
@@ -815,12 +848,13 @@ graph.command("serve").description("Serve the local graph viewer.").option("--po
815
848
  });
816
849
  });
817
850
  graph.command("export").description(
818
- "Export the graph as HTML, SVG, GraphML, Cypher, JSON, Obsidian vault, or Obsidian canvas. Combine flags to write multiple formats in one run."
819
- ).option("--html <output>", "Output HTML file path").option("--html-standalone <output>", "Output lightweight standalone HTML file path (vis.js, no build tooling)").option("--svg <output>", "Output SVG file path").option("--graphml <output>", "Output GraphML file path").option("--cypher <output>", "Output Cypher file path").option("--json <output>", "Output JSON file path").option("--obsidian <output>", "Output Obsidian vault directory path").option("--canvas <output>", "Output Obsidian canvas file path").option("--full", "Disable overview sampling for HTML export", false).action(
851
+ "Export the graph as HTML, report, SVG, GraphML, Cypher, JSON, Obsidian vault, or Obsidian canvas. Combine flags to write multiple formats in one run."
852
+ ).option("--html <output>", "Output HTML file path").option("--html-standalone <output>", "Output lightweight standalone HTML file path (vis.js, no build tooling)").option("--report <output>", "Output self-contained HTML report (graph stats, key nodes, communities)").option("--svg <output>", "Output SVG file path").option("--graphml <output>", "Output GraphML file path").option("--cypher <output>", "Output Cypher file path").option("--json <output>", "Output JSON file path").option("--obsidian <output>", "Output Obsidian vault directory path").option("--canvas <output>", "Output Obsidian canvas file path").option("--full", "Disable overview sampling for HTML export", false).action(
820
853
  async (options) => {
821
854
  const targets = [
822
855
  options.html ? { format: "html", outputPath: options.html } : null,
823
856
  options.htmlStandalone ? { format: "html-standalone", outputPath: options.htmlStandalone } : null,
857
+ options.report ? { format: "report", outputPath: options.report } : null,
824
858
  options.svg ? { format: "svg", outputPath: options.svg } : null,
825
859
  options.graphml ? { format: "graphml", outputPath: options.graphml } : null,
826
860
  options.cypher ? { format: "cypher", outputPath: options.cypher } : null,
@@ -829,13 +863,18 @@ graph.command("export").description(
829
863
  options.canvas ? { format: "canvas", outputPath: options.canvas } : null
830
864
  ].filter((target) => Boolean(target));
831
865
  if (targets.length === 0) {
832
- throw new Error("Pass at least one of --html, --html-standalone, --svg, --graphml, --cypher, --json, --obsidian, or --canvas.");
866
+ throw new Error(
867
+ "Pass at least one of --html, --html-standalone, --report, --svg, --graphml, --cypher, --json, --obsidian, or --canvas."
868
+ );
833
869
  }
834
870
  const results = [];
835
871
  for (const target of targets) {
836
872
  if (target.format === "html") {
837
873
  const outputPath = await exportGraphHtml(process2.cwd(), target.outputPath, { full: options.full ?? false });
838
874
  results.push({ format: target.format, outputPath });
875
+ } else if (target.format === "report") {
876
+ const result = await exportGraphReportHtml(process2.cwd(), target.outputPath);
877
+ results.push({ format: result.format, outputPath: result.outputPath });
839
878
  } else if (target.format === "obsidian") {
840
879
  const result = await exportObsidianVault(process2.cwd(), target.outputPath);
841
880
  results.push({ format: result.format, outputPath: result.outputPath, fileCount: result.fileCount });
@@ -896,6 +935,18 @@ graph.command("god-nodes").description("List the highest-connectivity non-source
896
935
  log(`${node.label} degree=${node.degree ?? 0} bridge=${node.bridgeScore ?? 0}`);
897
936
  }
898
937
  });
938
+ graph.command("blast").description("Show the blast radius of changing a file or module.").argument("<target>", "File path, module label, or module id").option("--depth <n>", "Maximum traversal depth", "3").action(async (target, options) => {
939
+ const depth = parsePositiveInt(options.depth, 3);
940
+ const result = await blastRadiusVault(process2.cwd(), target, { maxDepth: depth });
941
+ if (isJson()) {
942
+ emitJson(result);
943
+ return;
944
+ }
945
+ log(result.summary);
946
+ for (const mod of result.affectedModules) {
947
+ log(` ${" ".repeat(mod.depth - 1)}${mod.label} (depth ${mod.depth})`);
948
+ }
949
+ });
899
950
  var review = program.command("review").description("Review staged compile approval bundles.");
900
951
  review.command("list").description("List staged approval bundles and their resolution status.").action(async () => {
901
952
  const approvals = await listApprovals(process2.cwd());
@@ -1141,6 +1192,269 @@ program.command("install").description("Install SwarmVault instructions for an a
1141
1192
  }
1142
1193
  }
1143
1194
  );
1195
+ program.command("demo").description("Try SwarmVault with a bundled sample vault \u2014 zero config, zero API keys.").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").action(async (options) => {
1196
+ const { mkdtemp, writeFile: writeFile2, mkdir: mkdir2 } = await import("fs/promises");
1197
+ const { tmpdir } = await import("os");
1198
+ const path2 = await import("path");
1199
+ const demoDir = await mkdtemp(path2.join(tmpdir(), "swarmvault-demo-"));
1200
+ if (!isJson()) {
1201
+ log(`Creating demo vault in ${demoDir}`);
1202
+ }
1203
+ const rawDir = path2.join(demoDir, "raw", "sources");
1204
+ await mkdir2(rawDir, { recursive: true });
1205
+ await writeFile2(
1206
+ path2.join(rawDir, "llm-wiki-pattern.md"),
1207
+ [
1208
+ "# The LLM Wiki Pattern",
1209
+ "",
1210
+ "Most AI tools answer a question then throw away the work. The LLM Wiki pattern",
1211
+ "keeps a durable wiki between you and raw sources. The LLM does the bookkeeping \u2014",
1212
+ "updating cross-references, noting contradictions, maintaining consistency \u2014 while",
1213
+ "you do the thinking.",
1214
+ "",
1215
+ "## Three Layers",
1216
+ "",
1217
+ "1. **Raw sources** \u2014 immutable documents: books, articles, papers, transcripts, code.",
1218
+ "2. **The wiki** \u2014 LLM-generated markdown: summaries, entity pages, concept pages, cross-references.",
1219
+ "3. **The schema** \u2014 rules for how the wiki is organized and what matters in your domain.",
1220
+ "",
1221
+ "## Why This Beats RAG",
1222
+ "",
1223
+ "RAG re-derives knowledge on every query. The wiki compiles knowledge once and compounds",
1224
+ "it over time. Good answers get filed back as new pages, so exploration builds on itself.",
1225
+ "",
1226
+ "## Key Operations",
1227
+ "",
1228
+ "- **Ingest** \u2014 read a source, write summaries, update cross-references",
1229
+ "- **Query** \u2014 search the wiki, synthesize answers, file results back",
1230
+ "- **Lint** \u2014 health-check for contradictions, orphans, stale claims",
1231
+ ""
1232
+ ].join("\n")
1233
+ );
1234
+ await writeFile2(
1235
+ path2.join(rawDir, "knowledge-graphs.md"),
1236
+ [
1237
+ "# Knowledge Graphs for AI",
1238
+ "",
1239
+ "A knowledge graph represents information as typed nodes and edges with provenance.",
1240
+ "Unlike flat document stores, graphs let you traverse relationships, detect communities,",
1241
+ "and find surprising connections between concepts.",
1242
+ "",
1243
+ "## Node Types",
1244
+ "",
1245
+ "- **Sources** \u2014 the raw documents that feed the graph",
1246
+ "- **Concepts** \u2014 abstract ideas extracted from sources",
1247
+ "- **Entities** \u2014 named things: people, tools, organizations",
1248
+ "- **Modules** \u2014 code units with import/export relationships",
1249
+ "",
1250
+ "## Edge Semantics",
1251
+ "",
1252
+ "Every edge carries an evidence class: `extracted` (directly found), `inferred`",
1253
+ "(derived by reasoning), or `conflicted` (contradictory evidence). This provenance",
1254
+ "tracking prevents hallucination from compounding silently.",
1255
+ "",
1256
+ "## Contradiction Detection",
1257
+ "",
1258
+ "When two sources make conflicting claims, the graph flags the contradiction rather",
1259
+ "than silently picking one. This is critical for research wikis where outdated claims",
1260
+ "from older papers may conflict with newer findings.",
1261
+ "",
1262
+ "RAG systems do not track contradictions because they re-derive everything per query.",
1263
+ "A compiled wiki with a graph layer can detect and surface them automatically.",
1264
+ ""
1265
+ ].join("\n")
1266
+ );
1267
+ await writeFile2(
1268
+ path2.join(rawDir, "local-first-tools.md"),
1269
+ [
1270
+ "# Local-First AI Tools",
1271
+ "",
1272
+ "Local-first means your data stays on your machine by default. No cloud dependency,",
1273
+ "no API keys required for basic operation. Privacy is the default, not an option.",
1274
+ "",
1275
+ "## Advantages",
1276
+ "",
1277
+ "- **Privacy** \u2014 sensitive documents never leave your machine",
1278
+ "- **Speed** \u2014 no network latency for search and graph operations",
1279
+ "- **Reliability** \u2014 works offline, no service outages",
1280
+ "- **Cost** \u2014 no per-query API charges for basic workflows",
1281
+ "",
1282
+ "## The Heuristic Provider",
1283
+ "",
1284
+ "A heuristic provider uses deterministic text analysis \u2014 keyword extraction, TF-IDF,",
1285
+ "structural parsing \u2014 instead of LLM inference. It produces lower-quality summaries",
1286
+ "but runs instantly with zero setup. This makes it ideal for first-run experiences",
1287
+ "and air-gapped environments.",
1288
+ "",
1289
+ "For sharper concept extraction, pair with a local LLM via Ollama. This keeps",
1290
+ "everything on-device while getting model-backed analysis.",
1291
+ ""
1292
+ ].join("\n")
1293
+ );
1294
+ await initVault(demoDir, {});
1295
+ await ingestDirectory(demoDir, rawDir, {});
1296
+ await compileVault(demoDir, {});
1297
+ const { paths } = await loadVaultConfig(demoDir);
1298
+ let graphStats = "";
1299
+ try {
1300
+ const raw = await readFile2(paths.graphPath, "utf-8");
1301
+ const graph2 = JSON.parse(raw);
1302
+ graphStats = ` (${graph2.nodes.length} nodes, ${graph2.edges.length} edges)`;
1303
+ } catch {
1304
+ }
1305
+ if (!isJson()) {
1306
+ log("");
1307
+ log(`Demo vault created${graphStats}.`);
1308
+ log("");
1309
+ log("What just happened:");
1310
+ log(" 1. Created 3 sample sources about LLM wikis, knowledge graphs, and local-first tools");
1311
+ log(" 2. Ingested and compiled them into a knowledge graph");
1312
+ log(" 3. Generated wiki pages with cross-references and a graph report");
1313
+ log("");
1314
+ log(`Vault location: ${demoDir}`);
1315
+ }
1316
+ if (options.serve !== false) {
1317
+ const port = options.port ? parsePositiveInt(options.port, 0) || void 0 : void 0;
1318
+ const server = await startGraphServer(demoDir, port, { full: false });
1319
+ if (isJson()) {
1320
+ emitJson({ demoDir, graphStats: graphStats.trim(), port: server.port, url: `http://localhost:${server.port}` });
1321
+ } else {
1322
+ log(`Graph viewer running at http://localhost:${server.port}`);
1323
+ log("");
1324
+ log("Try next:");
1325
+ log(` cd ${demoDir}`);
1326
+ log(' swarmvault query "How does contradiction detection work?"');
1327
+ log(" swarmvault lint");
1328
+ }
1329
+ process2.on("SIGINT", async () => {
1330
+ try {
1331
+ await server.close();
1332
+ } catch {
1333
+ }
1334
+ process2.exit(0);
1335
+ });
1336
+ } else if (isJson()) {
1337
+ emitJson({ demoDir, graphStats: graphStats.trim() });
1338
+ } else {
1339
+ log("");
1340
+ log("Try next:");
1341
+ log(` cd ${demoDir}`);
1342
+ log(" swarmvault graph serve");
1343
+ log(' swarmvault query "How does contradiction detection work?"');
1344
+ }
1345
+ });
1346
+ program.command("diff").description("Show what changed in the knowledge graph since the last compile.").action(async () => {
1347
+ const rootDir = process2.cwd();
1348
+ const { paths } = await loadVaultConfig(rootDir);
1349
+ let currentGraph;
1350
+ try {
1351
+ const raw = await readFile2(paths.graphPath, "utf-8");
1352
+ currentGraph = JSON.parse(raw);
1353
+ } catch {
1354
+ if (isJson()) {
1355
+ emitJson({ error: "No compiled graph found. Run swarmvault compile first." });
1356
+ } else {
1357
+ log("No compiled graph found. Run swarmvault compile first.");
1358
+ }
1359
+ return;
1360
+ }
1361
+ let previousGraph;
1362
+ const { execFileSync } = await import("child_process");
1363
+ try {
1364
+ const relPath = paths.graphPath.replace(`${rootDir}/`, "");
1365
+ const previousRaw = execFileSync("git", ["show", `HEAD:${relPath}`], {
1366
+ cwd: rootDir,
1367
+ encoding: "utf-8",
1368
+ stdio: ["pipe", "pipe", "pipe"]
1369
+ });
1370
+ previousGraph = JSON.parse(previousRaw);
1371
+ } catch {
1372
+ }
1373
+ if (!previousGraph) {
1374
+ if (isJson()) {
1375
+ emitJson({
1376
+ status: "no-baseline",
1377
+ current: {
1378
+ nodes: currentGraph.nodes.length,
1379
+ edges: currentGraph.edges.length,
1380
+ pages: currentGraph.pages.length,
1381
+ communities: currentGraph.communities?.length ?? 0
1382
+ }
1383
+ });
1384
+ } else {
1385
+ log("No previous graph found (not in a git repo or no prior commit).");
1386
+ log("");
1387
+ log("Current graph:");
1388
+ log(` ${currentGraph.nodes.length} nodes, ${currentGraph.edges.length} edges, ${currentGraph.pages.length} pages`);
1389
+ if (currentGraph.communities?.length) {
1390
+ log(` ${currentGraph.communities.length} communities`);
1391
+ }
1392
+ const conflicted = currentGraph.edges.filter((e) => e.status === "conflicted");
1393
+ if (conflicted.length) {
1394
+ log(` ${conflicted.length} conflicted edges`);
1395
+ }
1396
+ }
1397
+ return;
1398
+ }
1399
+ const diff = graphDiff(previousGraph, currentGraph);
1400
+ if (isJson()) {
1401
+ emitJson(diff);
1402
+ return;
1403
+ }
1404
+ if (diff.summary === "No changes") {
1405
+ log("No changes since last commit.");
1406
+ return;
1407
+ }
1408
+ log(diff.summary);
1409
+ log("");
1410
+ if (diff.addedNodes.length) {
1411
+ log("Added nodes:");
1412
+ for (const node of diff.addedNodes) {
1413
+ log(` + [${node.type}] ${node.label}`);
1414
+ }
1415
+ log("");
1416
+ }
1417
+ if (diff.removedNodes.length) {
1418
+ log("Removed nodes:");
1419
+ for (const node of diff.removedNodes) {
1420
+ log(` - [${node.type}] ${node.label}`);
1421
+ }
1422
+ log("");
1423
+ }
1424
+ if (diff.addedPages.length) {
1425
+ log("Added pages:");
1426
+ for (const page of diff.addedPages) {
1427
+ log(` + [${page.kind}] ${page.title} (${page.path})`);
1428
+ }
1429
+ log("");
1430
+ }
1431
+ if (diff.removedPages.length) {
1432
+ log("Removed pages:");
1433
+ for (const page of diff.removedPages) {
1434
+ log(` - [${page.kind}] ${page.title} (${page.path})`);
1435
+ }
1436
+ log("");
1437
+ }
1438
+ if (diff.addedEdges.length) {
1439
+ log(`Added edges: ${diff.addedEdges.length}`);
1440
+ for (const edge of diff.addedEdges.slice(0, 20)) {
1441
+ log(` + ${edge.source} -[${edge.relation}]-> ${edge.target} (${edge.evidenceClass})`);
1442
+ }
1443
+ if (diff.addedEdges.length > 20) {
1444
+ log(` ... and ${diff.addedEdges.length - 20} more`);
1445
+ }
1446
+ log("");
1447
+ }
1448
+ if (diff.removedEdges.length) {
1449
+ log(`Removed edges: ${diff.removedEdges.length}`);
1450
+ for (const edge of diff.removedEdges.slice(0, 20)) {
1451
+ log(` - ${edge.source} -[${edge.relation}]-> ${edge.target}`);
1452
+ }
1453
+ if (diff.removedEdges.length > 20) {
1454
+ log(` ... and ${diff.removedEdges.length - 20} more`);
1455
+ }
1456
+ }
1457
+ });
1144
1458
  program.command("scan").description("Quick-start: initialize, ingest, compile, and serve a graph viewer in one command.").argument("<directory>", "Directory to scan").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").action(async (directory, options) => {
1145
1459
  const rootDir = process2.cwd();
1146
1460
  await initVault(rootDir, {});
@@ -1174,6 +1488,19 @@ program.command("scan").description("Quick-start: initialize, ingest, compile, a
1174
1488
  emitJson({ ...result, compiled });
1175
1489
  }
1176
1490
  });
1491
+ function enableStructuredJsonOnSubcommands(command) {
1492
+ for (const subcommand of command.commands) {
1493
+ const hasJsonOption = subcommand.options.some((option) => option.attributeName() === "json");
1494
+ if (!hasJsonOption) {
1495
+ subcommand.option("--json", "Emit structured JSON output", false);
1496
+ }
1497
+ enableStructuredJsonOnSubcommands(subcommand);
1498
+ }
1499
+ }
1500
+ enableStructuredJsonOnSubcommands(program);
1501
+ program.hook("preAction", (_command, actionCommand) => {
1502
+ activeCommand = actionCommand;
1503
+ });
1177
1504
  program.parseAsync(process2.argv).catch((error) => {
1178
1505
  const message = error instanceof Error ? error.message : String(error);
1179
1506
  if (isJson()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmvaultai/cli",
3
- "version": "0.7.24",
3
+ "version": "0.7.26",
4
4
  "description": "Global CLI for SwarmVault.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -38,7 +38,7 @@
38
38
  "node": ">=24.0.0"
39
39
  },
40
40
  "dependencies": {
41
- "@swarmvaultai/engine": "0.7.24",
41
+ "@swarmvaultai/engine": "0.7.26",
42
42
  "commander": "^14.0.1"
43
43
  },
44
44
  "devDependencies": {