@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.
- package/README.md +78 -9
- package/dist/index.js +353 -26
- 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,
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
279
|
+
return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "0.7.26";
|
|
274
280
|
} catch {
|
|
275
|
-
return "0.7.
|
|
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
|
|
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(
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
41
|
+
"@swarmvaultai/engine": "0.7.26",
|
|
42
42
|
"commander": "^14.0.1"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|