@swarmvaultai/cli 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +119 -29
  2. package/package.json +9 -8
  3. package/LICENSE +0 -21
package/dist/index.js CHANGED
@@ -9,11 +9,14 @@ import {
9
9
  acceptApproval,
10
10
  addInput,
11
11
  addManagedSource,
12
+ addWatchedRoot,
12
13
  archiveCandidate,
13
14
  autoCommitWikiChanges,
14
15
  benchmarkVault,
15
16
  blastRadiusVault,
16
17
  compileVault,
18
+ consolidateVault,
19
+ createSupersessionEdge,
17
20
  deleteManagedSource,
18
21
  explainGraphVault,
19
22
  exploreVault,
@@ -40,6 +43,7 @@ import {
40
43
  listManagedSourceRecords,
41
44
  listManifests,
42
45
  listSchedules,
46
+ listWatchedRoots,
43
47
  loadVaultConfig,
44
48
  pathGraphVault,
45
49
  previewCandidatePromotions,
@@ -50,6 +54,7 @@ import {
50
54
  readApproval,
51
55
  rejectApproval,
52
56
  reloadManagedSources,
57
+ removeWatchedRoot,
53
58
  resumeSourceSession,
54
59
  reviewManagedSource,
55
60
  reviewSourceScope,
@@ -278,9 +283,9 @@ program.name("swarmvault").description("SwarmVault is a local-first knowledge co
278
283
  function readCliVersion() {
279
284
  try {
280
285
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
281
- return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "0.9.0";
286
+ return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "0.11.0";
282
287
  } catch {
283
- return "0.9.0";
288
+ return "0.11.0";
284
289
  }
285
290
  }
286
291
  function parsePositiveInt(value, fallback) {
@@ -288,6 +293,9 @@ function parsePositiveInt(value, fallback) {
288
293
  const parsed = Number.parseInt(value, 10);
289
294
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
290
295
  }
296
+ function collectRepeated(value, previous) {
297
+ return [...previous, value];
298
+ }
291
299
  function sourceScopeFromManifests(input, manifests) {
292
300
  if (!manifests.length) {
293
301
  return null;
@@ -306,6 +314,18 @@ function slugForCli(value) {
306
314
  function isJson() {
307
315
  return activeCommand?.opts().json === true || program.opts().json === true;
308
316
  }
317
+ function summarizeRedactions(redactions) {
318
+ if (!redactions || redactions.length === 0) {
319
+ return void 0;
320
+ }
321
+ const totalMatches = redactions.reduce((total, entry) => total + entry.matches.reduce((sum, match) => sum + match.count, 0), 0);
322
+ if (totalMatches === 0) {
323
+ return void 0;
324
+ }
325
+ const secretsLabel = totalMatches === 1 ? "secret" : "secrets";
326
+ const sourceLabel = redactions.length === 1 ? "source" : "sources";
327
+ return `Redacted ${totalMatches} ${secretsLabel} across ${redactions.length} ${sourceLabel} (run --no-redact to disable).`;
328
+ }
309
329
  function emitJson(data) {
310
330
  process2.stdout.write(`${JSON.stringify(data)}
311
331
  `);
@@ -446,7 +466,7 @@ program.command("init").description("Initialize a SwarmVault workspace in the cu
446
466
  log(options.lite ? "Initialized SwarmVault lite workspace." : "Initialized SwarmVault workspace.");
447
467
  }
448
468
  });
449
- 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("--resume <run-id>", "Re-run only the failed files from a prior ingest run id").option("--commit", "Auto-commit wiki and state changes after ingest").action(
469
+ 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("--resume <run-id>", "Re-run only the failed files from a prior ingest run id").option("--commit", "Auto-commit wiki and state changes after ingest").option("--no-redact", "Skip PII/secret redaction for this run (overrides config)").action(
450
470
  async (input, options) => {
451
471
  const guideAnswers = readGuideAnswersFile(options.answersFile);
452
472
  const vaultConfig = await loadVaultConfig(process2.cwd()).catch(() => null);
@@ -468,7 +488,8 @@ program.command("ingest").description("Ingest a local file path, directory path,
468
488
  maxFiles,
469
489
  gitignore: options.gitignore,
470
490
  extractClasses,
471
- resume: options.resume
491
+ resume: options.resume,
492
+ redact: options.redact
472
493
  };
473
494
  const directoryResult = !/^https?:\/\//i.test(input) ? await import("fs/promises").then(
474
495
  (fs) => fs.stat(input).then((stat) => stat.isDirectory() ? ingestDirectory(process2.cwd(), input, commonOptions) : null).catch(() => null)
@@ -528,6 +549,10 @@ program.command("ingest").description("Ingest a local file path, directory path,
528
549
  } else if (completedGuide2?.guidePath) {
529
550
  log(`Staged guided session at ${completedGuide2.guidePath}.`);
530
551
  }
552
+ const redactionLine = summarizeRedactions(directoryResult.redactions);
553
+ if (redactionLine) {
554
+ log(redactionLine);
555
+ }
531
556
  }
532
557
  if (options.commit) {
533
558
  const msg = await autoCommitWikiChanges(process2.cwd(), "ingest", input, { force: true });
@@ -567,6 +592,10 @@ program.command("ingest").description("Ingest a local file path, directory path,
567
592
  } else if (completedGuide?.guidePath) {
568
593
  log(`Staged guided session at ${completedGuide.guidePath}.`);
569
594
  }
595
+ const redactionLine = summarizeRedactions(ingest.redactions);
596
+ if (redactionLine) {
597
+ log(redactionLine);
598
+ }
570
599
  }
571
600
  if (options.commit) {
572
601
  const msg = await autoCommitWikiChanges(process2.cwd(), "ingest", input, { force: true });
@@ -574,10 +603,11 @@ program.command("ingest").description("Ingest a local file path, directory path,
574
603
  }
575
604
  }
576
605
  );
577
- 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) => {
606
+ 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").option("--no-redact", "Skip PII/secret redaction for this capture (overrides config)").action(async (input, options) => {
578
607
  const result = await addInput(process2.cwd(), input, {
579
608
  author: options.author,
580
- contributor: options.contributor
609
+ contributor: options.contributor,
610
+ redact: options.redact
581
611
  });
582
612
  if (isJson()) {
583
613
  emitJson(result);
@@ -734,14 +764,28 @@ program.command("compile").description("Compile manifests into wiki pages, graph
734
764
  }
735
765
  await maybeEmitHeuristicNotice(["compile"]);
736
766
  });
737
- 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(
767
+ program.command("consolidate").description("Roll working-tier insights up into episodic, semantic, and procedural tiers.").option("--dry-run", "Return decisions without writing any files", false).action(async (options) => {
768
+ const result = await consolidateVault(process2.cwd(), { dryRun: options.dryRun ?? false });
769
+ if (isJson()) {
770
+ emitJson(result);
771
+ return;
772
+ }
773
+ log(
774
+ `${options.dryRun ? "Would consolidate" : "Consolidated"} ${result.newPages.length} new tier page(s); ${result.promoted.length} promotion(s).`
775
+ );
776
+ for (const decision of result.decisions) {
777
+ log(decision);
778
+ }
779
+ });
780
+ 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").option("--gap-fill", "Pull external web-search evidence when the local wiki has gaps (requires webSearch.tasks.queryProvider).").addOption(
738
781
  new Option("--format <format>", "Output format").choices(["markdown", "report", "slides", "chart", "image"]).default("markdown")
739
782
  ).action(
740
783
  async (question, options) => {
741
784
  const result = await queryVault(process2.cwd(), {
742
785
  question,
743
786
  save: options.save ?? true,
744
- format: options.format
787
+ format: options.format,
788
+ gapFill: options.gapFill ?? false
745
789
  });
746
790
  if (isJson()) {
747
791
  emitJson(result);
@@ -758,23 +802,26 @@ program.command("query").description("Query the compiled SwarmVault wiki.").argu
758
802
  await maybeEmitHeuristicNotice(["query"]);
759
803
  }
760
804
  );
761
- 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(
805
+ 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").option("--gap-fill", "Pull external web-search evidence when the local wiki has gaps (requires webSearch.tasks.exploreProvider).").addOption(
762
806
  new Option("--format <format>", "Output format for step pages").choices(["markdown", "report", "slides", "chart", "image"]).default("markdown")
763
- ).action(async (question, options) => {
764
- const stepCount = parsePositiveInt(options.steps, 3);
765
- const result = await exploreVault(process2.cwd(), {
766
- question,
767
- steps: stepCount,
768
- format: options.format
769
- });
770
- if (isJson()) {
771
- emitJson(result);
772
- } else {
773
- log(`Exploration hub saved to ${result.hubPath}`);
774
- log(`Completed ${result.stepCount} step(s).`);
807
+ ).action(
808
+ async (question, options) => {
809
+ const stepCount = parsePositiveInt(options.steps, 3);
810
+ const result = await exploreVault(process2.cwd(), {
811
+ question,
812
+ steps: stepCount,
813
+ format: options.format,
814
+ gapFill: options.gapFill ?? false
815
+ });
816
+ if (isJson()) {
817
+ emitJson(result);
818
+ } else {
819
+ log(`Exploration hub saved to ${result.hubPath}`);
820
+ log(`Completed ${result.stepCount} step(s).`);
821
+ }
822
+ await maybeEmitHeuristicNotice(["explore"]);
775
823
  }
776
- await maybeEmitHeuristicNotice(["explore"]);
777
- });
824
+ );
778
825
  program.command("benchmark").description("Measure graph-guided context reduction against a naive full-corpus read.").option("--question <text...>", "Optional custom benchmark question(s)").action(async (options) => {
779
826
  const result = await benchmarkVault(process2.cwd(), {
780
827
  questions: options.question
@@ -793,13 +840,15 @@ program.command("benchmark").description("Measure graph-guided context reduction
793
840
  }
794
841
  }
795
842
  });
796
- program.command("lint").description("Run anti-drift and wiki-health checks.").option("--deep", "Run LLM-powered advisory lint (default: from config)").option("--no-deep", "Skip deep lint even if enabled in config").option("--web", "Augment deep lint with configured web search", false).option("--conflicts", "Filter to contradiction findings only", false).action(async (options) => {
843
+ program.command("lint").description("Run anti-drift and wiki-health checks.").option("--deep", "Run LLM-powered advisory lint (default: from config)").option("--no-deep", "Skip deep lint even if enabled in config").option("--web", "Augment deep lint with configured web search", false).option("--conflicts", "Filter to contradiction findings only", false).option("--decay", "Filter to decay-related findings only", false).option("--tiers", "Filter to consolidation-tier findings only", false).action(async (options) => {
797
844
  const lintConfig = await loadVaultConfig(process2.cwd()).catch(() => null);
798
- const deepEnabled = options.deep ?? lintConfig?.config.profile.deepLintDefault ?? false;
845
+ const deepEnabled = options.decay || options.tiers ? false : options.deep ?? lintConfig?.config.profile.deepLintDefault ?? false;
799
846
  const findings = await lintVault(process2.cwd(), {
800
847
  deep: deepEnabled,
801
848
  web: options.web ?? false,
802
- conflicts: options.conflicts ?? false
849
+ conflicts: options.conflicts ?? false,
850
+ decay: options.decay ?? false,
851
+ tiers: options.tiers ?? false
803
852
  });
804
853
  if (isJson()) {
805
854
  emitJson(findings);
@@ -969,6 +1018,14 @@ graph.command("blast").description("Show the blast radius of changing a file or
969
1018
  log(` ${" ".repeat(mod.depth - 1)}${mod.label} (depth ${mod.depth})`);
970
1019
  }
971
1020
  });
1021
+ graph.command("supersession").description("Record that one page has been replaced by another (writes a superseded_by edge).").argument("<pageId>", "Page id or path of the older page").argument("<replacedById>", "Page id or path of the replacement page").action(async (pageId, replacedById) => {
1022
+ const result = await createSupersessionEdge(process2.cwd(), pageId, replacedById);
1023
+ if (isJson()) {
1024
+ emitJson(result);
1025
+ return;
1026
+ }
1027
+ log(`Superseded ${result.oldPageId} by ${result.newPageId} (edge ${result.edgeId}).`);
1028
+ });
972
1029
  var review = program.command("review").description("Review staged compile approval bundles.");
973
1030
  review.command("list").description("List staged approval bundles and their resolution status.").action(async () => {
974
1031
  const approvals = await listApprovals(process2.cwd());
@@ -1083,14 +1140,16 @@ candidate.command("preview-scores").description("Show promotion scores for stage
1083
1140
  log(`${verdict} ${decision.pageId} score=${decision.score.toFixed(2)} ${decision.reasons.join("; ")}`);
1084
1141
  }
1085
1142
  });
1086
- var watch = program.command("watch").description("Watch the inbox directory and optionally tracked repos, or run one refresh cycle immediately.").option("--lint", "Run lint after each compile cycle", false).option("--repo", "Also refresh tracked repo sources and watch their repo roots", false).option("--once", "Run one import/refresh cycle immediately instead of starting a watcher", false).option("--code-only", "Only re-extract code sources (AST-only, no LLM re-analysis)", false).option("--debounce <ms>", "Debounce window in milliseconds", "900").action(async (options) => {
1143
+ var watch = program.command("watch").description("Watch the inbox directory and optionally tracked repos, or run one refresh cycle immediately.").option("--lint", "Run lint after each compile cycle", false).option("--repo", "Also refresh tracked repo sources and watch their repo roots", false).option("--once", "Run one import/refresh cycle immediately instead of starting a watcher", false).option("--code-only", "Only re-extract code sources (AST-only, no LLM re-analysis)", false).option("--debounce <ms>", "Debounce window in milliseconds", "900").option("--root <path>", "Watch this repo root instead of config/auto-discovery (repeat for multiple)", collectRepeated, []).action(async (options) => {
1087
1144
  const debounceMs = parsePositiveInt(options.debounce, 900);
1145
+ const overrideRoots = options.root && options.root.length > 0 ? options.root : void 0;
1088
1146
  if (options.once) {
1089
1147
  const result = await runWatchCycle(process2.cwd(), {
1090
1148
  lint: options.lint ?? false,
1091
1149
  repo: options.repo ?? false,
1092
1150
  codeOnly: options.codeOnly ?? false,
1093
- debounceMs
1151
+ debounceMs,
1152
+ overrideRoots
1094
1153
  });
1095
1154
  if (isJson()) {
1096
1155
  emitJson(result);
@@ -1106,7 +1165,8 @@ var watch = program.command("watch").description("Watch the inbox directory and
1106
1165
  lint: options.lint ?? false,
1107
1166
  repo: options.repo ?? false,
1108
1167
  codeOnly: options.codeOnly ?? false,
1109
- debounceMs
1168
+ debounceMs,
1169
+ overrideRoots
1110
1170
  });
1111
1171
  if (isJson()) {
1112
1172
  emitJson({ status: "watching", inboxDir: paths.inboxDir, repo: options.repo ?? false });
@@ -1121,6 +1181,36 @@ var watch = program.command("watch").description("Watch the inbox directory and
1121
1181
  process2.exit(0);
1122
1182
  });
1123
1183
  });
1184
+ watch.command("list-roots").description("Print the effective watched repo roots resolved from config and auto-discovery.").action(async () => {
1185
+ const roots = await listWatchedRoots(process2.cwd());
1186
+ if (isJson()) {
1187
+ emitJson({ roots });
1188
+ return;
1189
+ }
1190
+ if (roots.length === 0) {
1191
+ log("No watched repo roots.");
1192
+ return;
1193
+ }
1194
+ for (const entry of roots) {
1195
+ log(entry);
1196
+ }
1197
+ });
1198
+ watch.command("add-root <path>").description("Persist a repo root into swarmvault.config.json watch.repoRoots.").action(async (pathValue) => {
1199
+ const resolved = await addWatchedRoot(process2.cwd(), pathValue);
1200
+ if (isJson()) {
1201
+ emitJson({ added: resolved });
1202
+ return;
1203
+ }
1204
+ log(`Watching ${resolved}`);
1205
+ });
1206
+ watch.command("remove-root <path>").description("Remove a repo root from swarmvault.config.json watch.repoRoots.").action(async (pathValue) => {
1207
+ const removed = await removeWatchedRoot(process2.cwd(), pathValue);
1208
+ if (isJson()) {
1209
+ emitJson({ removed });
1210
+ return;
1211
+ }
1212
+ log(removed ? `Removed ${pathValue}` : `${pathValue} was not in watch.repoRoots.`);
1213
+ });
1124
1214
  async function showWatchStatus() {
1125
1215
  const result = await getWatchStatus(process2.cwd());
1126
1216
  if (isJson()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmvaultai/cli",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Global CLI for SwarmVault.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,18 +37,19 @@
37
37
  "engines": {
38
38
  "node": ">=24.0.0"
39
39
  },
40
+ "scripts": {
41
+ "build": "tsup src/index.ts --format esm --dts",
42
+ "test": "vitest run",
43
+ "typecheck": "tsc --noEmit",
44
+ "prepublishOnly": "node ../../scripts/check-release-sync.mjs && node ../../scripts/check-published-manifests.mjs"
45
+ },
40
46
  "dependencies": {
41
- "@swarmvaultai/engine": "0.9.0",
47
+ "@swarmvaultai/engine": "0.11.0",
42
48
  "commander": "^14.0.1"
43
49
  },
44
50
  "devDependencies": {
45
51
  "@types/node": "^24.6.0",
46
52
  "tsup": "^8.5.0",
47
53
  "vitest": "^3.2.4"
48
- },
49
- "scripts": {
50
- "build": "tsup src/index.ts --format esm --dts",
51
- "test": "vitest run",
52
- "typecheck": "tsc --noEmit"
53
54
  }
54
- }
55
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 SwarmVault
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.