@swarmvaultai/cli 3.14.2 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +39 -9
  2. package/dist/index.js +230 -38
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -105,25 +105,26 @@ Set `SWARMVAULT_OUT=<dir>` when generated artifacts should be isolated from the
105
105
 
106
106
  `--profile` accepts `default`, `personal-research`, or a comma-separated preset list such as `reader,timeline`. For fully custom vault behavior, edit the `profile` block in `swarmvault.config.json`; that deterministic profile layer works alongside the human-written `swarmvault.schema.md`. The `personal-research` preset also sets `profile.guidedIngestDefault: true` and `profile.deepLintDefault: true`, so guided ingest/source and lint flows are on by default until you override them with `--no-guide` or `--no-deep`.
107
107
 
108
- ### `swarmvault quickstart <directory|github-url> [--port <port>] [--no-serve] [--no-viz] [--mcp] [--branch <name>] [--ref <ref>] [--checkout-dir <path>]`
108
+ ### `swarmvault quickstart <file|directory|github-url> [--port <port>] [--no-serve] [--no-viz] [--mcp] [--branch <name>] [--ref <ref>] [--checkout-dir <path>] [--install-agent-rules]`
109
109
 
110
110
  Beginner-friendly alias for `swarmvault scan`.
111
111
 
112
112
  - initializes the current directory as a SwarmVault workspace
113
- - ingests the supplied local directory, or registers/syncs the supplied public GitHub repo root URL
113
+ - ingests the supplied local file or directory, or registers/syncs the supplied public GitHub repo root URL
114
114
  - compiles wiki, graph, search, and share artifacts immediately
115
115
  - prints the generated `raw/`, `wiki/`, `state/graph.json`, and `wiki/graph/` paths in human output
116
116
  - starts `graph serve` unless you pass `--no-serve` or `--no-viz`
117
117
  - keeps the same JSON output contract as `scan`
118
+ - leaves agent rule files alone unless you pass `--install-agent-rules`
118
119
 
119
120
  Use this as the default first-run command in docs and onboarding.
120
121
 
121
- ### `swarmvault scan <directory|github-url> [--port <port>] [--no-serve] [--no-viz] [--mcp] [--branch <name>] [--ref <ref>] [--checkout-dir <path>]`
122
+ ### `swarmvault scan <file|directory|github-url> [--port <port>] [--no-serve] [--no-viz] [--mcp] [--branch <name>] [--ref <ref>] [--checkout-dir <path>] [--install-agent-rules]`
122
123
 
123
- Quick-start a scratch vault from a local directory or public GitHub repo root URL in one command.
124
+ Quick-start a scratch vault from a local file, directory, or public GitHub repo root URL in one command.
124
125
 
125
126
  - initializes the current directory as a SwarmVault workspace
126
- - ingests the supplied directory as local sources, or registers/syncs the supplied public GitHub repo root URL
127
+ - ingests the supplied file or directory as local sources, or registers/syncs the supplied public GitHub repo root URL
127
128
  - compiles the vault immediately
128
129
  - writes `wiki/graph/share-card.md`, `wiki/graph/share-card.svg`, and `wiki/graph/share-kit/`, then prints the paths
129
130
  - starts `graph serve` unless you pass `--no-serve` or `--no-viz`
@@ -131,10 +132,11 @@ Quick-start a scratch vault from a local directory or public GitHub repo root UR
131
132
  - `--mcp` starts the MCP stdio server after compile instead of the graph viewer
132
133
  - respects `--port` when you want a specific viewer port
133
134
  - for GitHub repo URLs, supports `--branch`, `--ref`, and `--checkout-dir`
135
+ - `--install-agent-rules` installs the configured `agents` targets during initialization
134
136
 
135
137
  Use this when you want the fastest repo or docs-tree walkthrough without first deciding on managed-source registration.
136
138
 
137
- ### `swarmvault clone <directory|github-url> [--no-viz] [--mcp] [--branch <name>] [--ref <ref>] [--checkout-dir <path>]`
139
+ ### `swarmvault clone <file|directory|github-url> [--no-viz] [--mcp] [--branch <name>] [--ref <ref>] [--checkout-dir <path>] [--install-agent-rules]`
138
140
 
139
141
  Compatibility alias for `swarmvault scan`.
140
142
 
@@ -268,7 +270,7 @@ Useful flags:
268
270
 
269
271
  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.
270
272
 
271
- 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.
273
+ Interactive file and directory ingest now emits bounded stderr progress, including the active file and processed content size. JSON, MCP, watch, and CI-style flows stay quiet, and parser compatibility failures stay local to the affected source instead of aborting unrelated analysis.
272
274
 
273
275
  Audio and video files use `tasks.audioProvider` when you configure a provider with `audio` capability. Local video extraction shells out to `ffmpeg`; public video URL extraction with `--video` shells out to `yt-dlp`. When no audio provider or extractor binary 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.
274
276
 
@@ -670,7 +672,15 @@ Trace the reverse-import blast radius of changing a file or module.
670
672
  - follows reverse `imports` edges through the compiled graph
671
673
  - reports affected modules by depth so you can estimate downstream impact before editing
672
674
 
673
- ### `swarmvault graph export --html|--html-standalone|--report|--svg|--graphml|--cypher|--json|--obsidian|--canvas <output>`
675
+ ### `swarmvault graph cycles [--relation <name>] [--limit <n>] [--max-depth <n>]`
676
+
677
+ Find deterministic directed cycles in the compiled graph.
678
+
679
+ - defaults to `imports` edges for module cycle checks
680
+ - accepts repeated `--relation` flags to inspect other directed relationships
681
+ - supports global `--json` for automation
682
+
683
+ ### `swarmvault graph export --html|--html-standalone|--report|--svg|--graphml|--cypher|--json|--callflow|--obsidian|--canvas <output>`
674
684
 
675
685
  Export the current graph as one or more shareable formats:
676
686
 
@@ -681,6 +691,7 @@ Export the current graph as one or more shareable formats:
681
691
  - `--graphml` for graph-tool interoperability
682
692
  - `--cypher` for Neo4j-style import scripts
683
693
  - `--json` for a deterministic machine-readable graph package
694
+ - `--callflow` for compact directed relationship HTML
684
695
  - `--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
685
696
  - `--canvas` for an Obsidian canvas grouped by community
686
697
 
@@ -711,10 +722,12 @@ Defaults:
711
722
  - namespaces every remote record by `vaultId` so multiple vaults can safely share one Neo4j database
712
723
  - upserts current graph records and does not prune stale remote data yet
713
724
 
714
- ### `swarmvault install --agent <agent>`
725
+ ### `swarmvault install --agent <agent> [--scope project|user]`
715
726
 
716
727
  Install agent-specific rules into the current project so an agent understands the SwarmVault workspace contract and workflow.
717
728
 
729
+ `init`, `quickstart`, `scan`, and `clone` do not write project-local agent rule files by default. Run `swarmvault install --agent <agent> --scope project` for one target at a time, use `--scope user` for supported user-scope skill installs, or list targets in `swarmvault.config.json` and pass `--install-agent-rules` to `init`, `quickstart`, `scan`, or `clone` when you intentionally want configured targets installed together.
730
+
718
731
  Hook-capable installs:
719
732
 
720
733
  ```bash
@@ -723,6 +736,7 @@ swarmvault install --agent claude --hook
723
736
  swarmvault install --agent gemini --hook
724
737
  swarmvault install --agent opencode --hook
725
738
  swarmvault install --agent copilot --hook
739
+ swarmvault install --agent kilo --hook
726
740
  ```
727
741
 
728
742
  Agent target mapping:
@@ -737,9 +751,13 @@ Agent target mapping:
737
751
  - `claw` writes `.claw/skills/swarmvault/SKILL.md`
738
752
  - `droid` writes `.factory/rules/swarmvault.md`
739
753
  - `kiro` writes `.kiro/skills/swarmvault/SKILL.md` and `.kiro/steering/swarmvault.md`
754
+ - `kilo` project-scope writes `AGENTS.md`; with `--hook`, it also writes `.kilo/plugins/swarmvault.js` and `.kilo/kilo.json` while preserving an existing `.kilo/kilo.jsonc`
740
755
  - `hermes` writes `~/.hermes/skills/swarmvault/SKILL.md` plus `AGENTS.md`
741
756
  - `antigravity` writes `.agents/rules/swarmvault.md` and `.agents/workflows/swarmvault.md`, and removes older fully managed `.agent/` files during reinstall
742
757
  - `vscode` writes `.github/chatmodes/swarmvault.chatmode.md` plus `.github/copilot-instructions.md`
758
+ - `devin` writes `.devin/skills/swarmvault/SKILL.md` plus `.windsurf/rules/swarmvault.md`
759
+
760
+ `swarmvault install status --agent <agent> [--scope project|user] [--hook]` reports the expected install paths and whether they exist without writing files.
743
761
 
744
762
  SwarmVault only owns the managed block inside shared markdown rule files. It keeps the SwarmVault block aligned across targets while preserving any user-owned text before or after the block, so `AGENTS.md` and `CLAUDE.md` do not need to be byte-identical.
745
763
 
@@ -750,6 +768,7 @@ Hook semantics:
750
768
  - `gemini --hook` writes `.gemini/settings.json` plus `.gemini/hooks/swarmvault-graph-first.js` and stays advisory/model-visible
751
769
  - `opencode --hook` writes `.opencode/plugins/swarmvault-graph-first.js` and stays advisory/log-only
752
770
  - `copilot --hook` writes `.github/hooks/swarmvault-graph-first.json` plus `.github/hooks/swarmvault-graph-first.js` and remains decision-based rather than advisory
771
+ - `kilo --hook` writes `.kilo/plugins/swarmvault.js` and registers it in `.kilo/kilo.json`
753
772
 
754
773
  `aider` is intentionally file/config-based in this release rather than hook-based.
755
774
 
@@ -771,6 +790,17 @@ npm install -g @swarmvaultai/cli
771
790
 
772
791
  SwarmVault defaults to a local `heuristic` provider so the CLI works without API keys, but real vaults will usually point at an actual model provider.
773
792
 
793
+ CLI registry commands:
794
+
795
+ ```bash
796
+ swarmvault provider add router --type openrouter --model openrouter/auto --api-key-env OPENROUTER_API_KEY --capability chat --capability structured --task queryProvider
797
+ swarmvault provider list
798
+ swarmvault provider show router
799
+ swarmvault provider remove router --fallback local
800
+ ```
801
+
802
+ `provider add|remove` preserves unrelated config fields and stores secret references through `apiKeyEnv`, not literal API key values.
803
+
774
804
  Example:
775
805
 
776
806
  ```json
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync } from "fs";
5
- import { access, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
5
+ import { access, mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "fs/promises";
6
6
  import path2 from "path";
7
7
  import process2 from "process";
8
8
  import { createInterface } from "readline/promises";
@@ -10,6 +10,7 @@ import {
10
10
  acceptApproval,
11
11
  addInput,
12
12
  addManagedSource,
13
+ addProviderConfig,
13
14
  addWatchedRoot,
14
15
  archiveCandidate,
15
16
  askChatSession,
@@ -37,9 +38,12 @@ import {
37
38
  exportGraphTree,
38
39
  exportObsidianCanvas,
39
40
  exportObsidianVault,
41
+ findGraphCycles,
40
42
  finishMemoryTask,
43
+ getAgentInstallStatus,
41
44
  getGitHookStatus,
42
45
  getGraphStatus,
46
+ getProviderConfigEntry,
43
47
  getRetrievalStatus,
44
48
  getWatchStatus,
45
49
  graphDiff,
@@ -61,6 +65,7 @@ import {
61
65
  listManagedSourceRecords,
62
66
  listManifests,
63
67
  listMemoryTasks,
68
+ listProviderConfigEntries,
64
69
  listSchedules,
65
70
  listWatchedRoots,
66
71
  loadVaultConfig,
@@ -80,6 +85,7 @@ import {
80
85
  registerLocalWhisperProvider,
81
86
  rejectApproval,
82
87
  reloadManagedSources,
88
+ removeProviderConfig,
83
89
  removeWatchedRoot,
84
90
  renderContextPackLlms,
85
91
  renderContextPackMarkdown,
@@ -330,9 +336,9 @@ program.addHelpText(
330
336
  function readCliVersion() {
331
337
  try {
332
338
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
333
- return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "3.14.2";
339
+ return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "3.16.0";
334
340
  } catch {
335
- return "3.14.2";
341
+ return "3.16.0";
336
342
  }
337
343
  }
338
344
  function parsePositiveInt(value, fallback) {
@@ -340,6 +346,60 @@ function parsePositiveInt(value, fallback) {
340
346
  const parsed = Number.parseInt(value, 10);
341
347
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
342
348
  }
349
+ var providerTypes = [
350
+ "heuristic",
351
+ "openai",
352
+ "ollama",
353
+ "anthropic",
354
+ "gemini",
355
+ "openai-compatible",
356
+ "openrouter",
357
+ "groq",
358
+ "together",
359
+ "xai",
360
+ "cerebras",
361
+ "local-whisper",
362
+ "custom"
363
+ ];
364
+ var providerCapabilities = [
365
+ "responses",
366
+ "chat",
367
+ "structured",
368
+ "tools",
369
+ "vision",
370
+ "embeddings",
371
+ "streaming",
372
+ "local",
373
+ "image_generation",
374
+ "audio"
375
+ ];
376
+ var providerTaskKeys = [
377
+ "compileProvider",
378
+ "queryProvider",
379
+ "lintProvider",
380
+ "visionProvider",
381
+ "imageProvider",
382
+ "embeddingProvider",
383
+ "audioProvider"
384
+ ];
385
+ function parseProviderType(value) {
386
+ if (providerTypes.includes(value)) {
387
+ return value;
388
+ }
389
+ throw new Error(`Unknown provider type "${value}". Use one of: ${providerTypes.join(", ")}.`);
390
+ }
391
+ function parseProviderCapability(value) {
392
+ if (providerCapabilities.includes(value)) {
393
+ return value;
394
+ }
395
+ throw new Error(`Unknown provider capability "${value}". Use one of: ${providerCapabilities.join(", ")}.`);
396
+ }
397
+ function parseProviderTask(value) {
398
+ if (providerTaskKeys.includes(value)) {
399
+ return value;
400
+ }
401
+ throw new Error(`Unknown provider task "${value}". Use one of: ${providerTaskKeys.join(", ")}.`);
402
+ }
343
403
  function parsePositiveNumber(value) {
344
404
  if (value === void 0) return void 0;
345
405
  const parsed = Number.parseFloat(value);
@@ -818,7 +878,8 @@ async function runGraphMergeCommand(graphPaths, options) {
818
878
  }
819
879
  async function runScanCommand(input, options) {
820
880
  const rootDir = process2.cwd();
821
- await initVault(rootDir, {});
881
+ const progress = !isJson() && !options.mcp;
882
+ await initVault(rootDir, { installAgentRules: options.installAgentRules ?? false });
822
883
  if (!isJson()) {
823
884
  log("Initialized workspace.");
824
885
  }
@@ -828,14 +889,17 @@ async function runScanCommand(input, options) {
828
889
  branch: options.branch,
829
890
  ref: options.ref,
830
891
  checkoutDir: options.checkoutDir
831
- }) : await ingestDirectory(rootDir, input, {});
892
+ }) : await ingestScanInput(rootDir, input, progress);
832
893
  if (!isJson()) {
833
894
  if ("source" in result) {
834
895
  log(
835
896
  `Registered ${result.source.kind} source ${result.source.id}. Imported ${result.source.lastSyncCounts?.importedCount ?? 0}, updated ${result.source.lastSyncCounts?.updatedCount ?? 0}.`
836
897
  );
837
- } else {
898
+ } else if ("inputDir" in result) {
838
899
  log(`Ingested ${result.imported.length} file(s).`);
900
+ } else {
901
+ const sourceCount = result.created.length + result.updated.length + result.unchanged.length;
902
+ log(`Ingested ${sourceCount} source(s).`);
839
903
  }
840
904
  }
841
905
  const compiled = "compile" in result && result.compile ? result.compile : await compileVault(rootDir, {});
@@ -901,6 +965,17 @@ async function runScanCommand(input, options) {
901
965
  emitJson({ ...result, compiled, shareCardPath, shareCardSvgPath, shareKitPath });
902
966
  }
903
967
  }
968
+ async function ingestScanInput(rootDir, input, progress) {
969
+ const absoluteInput = path2.resolve(rootDir, input);
970
+ const inputStat = await stat(absoluteInput);
971
+ if (inputStat.isDirectory()) {
972
+ return ingestDirectory(rootDir, input, { progress });
973
+ }
974
+ if (inputStat.isFile()) {
975
+ return ingestInputDetailed(rootDir, input, { progress });
976
+ }
977
+ throw new Error(`Input must be a file or directory: ${input}`);
978
+ }
904
979
  async function resolveChatResumeId(resume) {
905
980
  if (!resume) {
906
981
  return void 0;
@@ -1022,7 +1097,7 @@ program.command("next").description("Show the safest next command for this direc
1022
1097
  }
1023
1098
  printNextCommandReport(report);
1024
1099
  });
1025
- program.command("quickstart").description("Beginner path: initialize, ingest, compile, and optionally open the graph viewer in one command.").argument("<input>", "Directory or public GitHub repo root URL to turn into a vault").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").option("--no-viz", "Compatibility alias for --no-serve; skip launching the graph viewer after compile").option("--mcp", "Start the MCP stdio server after compile instead of launching the graph viewer", false).option("--branch <name>", "GitHub branch to clone when the input is a public repo URL").option("--ref <ref>", "Git ref, tag, or commit to check out when the input is a public repo URL").option("--checkout-dir <path>", "Persistent checkout directory for a public GitHub repo input").action(runScanCommand);
1100
+ program.command("quickstart").description("Beginner path: initialize, ingest, compile, and optionally open the graph viewer in one command.").argument("<input>", "Directory or public GitHub repo root URL to turn into a vault").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").option("--no-viz", "Compatibility alias for --no-serve; skip launching the graph viewer after compile").option("--mcp", "Start the MCP stdio server after compile instead of launching the graph viewer", false).option("--branch <name>", "GitHub branch to clone when the input is a public repo URL").option("--ref <ref>", "Git ref, tag, or commit to check out when the input is a public repo URL").option("--checkout-dir <path>", "Persistent checkout directory for a public GitHub repo input").option("--install-agent-rules", "Install configured agent rule files during initialization", false).action(runScanCommand);
1026
1101
  program.command("init").description("Initialize a SwarmVault workspace in the current directory.").option("--obsidian", "Generate a minimal .obsidian workspace alongside the vault", false).option(
1027
1102
  "--profile <profile>",
1028
1103
  "Starter workspace profile or comma-separated preset list (for example: personal-research or reader,timeline)"
@@ -1030,11 +1105,12 @@ program.command("init").description("Initialize a SwarmVault workspace in the cu
1030
1105
  "--lite",
1031
1106
  "Minimal LLM-Wiki starter (raw/, wiki/, wiki/index.md, wiki/log.md, swarmvault.schema.md) without config, state, or agent installs",
1032
1107
  false
1033
- ).action(async (options) => {
1108
+ ).option("--install-agent-rules", "Install configured agent rule files during initialization", false).action(async (options) => {
1034
1109
  await initVault(process2.cwd(), {
1035
1110
  obsidian: options.obsidian ?? false,
1036
1111
  profile: options.profile,
1037
- lite: options.lite ?? false
1112
+ lite: options.lite ?? false,
1113
+ installAgentRules: options.installAgentRules ?? false
1038
1114
  });
1039
1115
  if (isJson()) {
1040
1116
  emitJson({
@@ -1042,7 +1118,8 @@ program.command("init").description("Initialize a SwarmVault workspace in the cu
1042
1118
  rootDir: process2.cwd(),
1043
1119
  obsidian: options.obsidian ?? false,
1044
1120
  profile: options.profile ?? "default",
1045
- lite: options.lite ?? false
1121
+ lite: options.lite ?? false,
1122
+ installAgentRules: options.installAgentRules ?? false
1046
1123
  });
1047
1124
  } else {
1048
1125
  log(options.lite ? "Initialized SwarmVault lite workspace." : "Initialized SwarmVault workspace.");
@@ -1074,11 +1151,10 @@ program.command("ingest").description("Ingest a local file path, directory path,
1074
1151
  video: options.video,
1075
1152
  extractClasses,
1076
1153
  resume: options.resume,
1077
- redact: options.redact
1154
+ redact: options.redact,
1155
+ progress: !isJson()
1078
1156
  };
1079
- const directoryResult = !/^https?:\/\//i.test(input) ? await import("fs/promises").then(
1080
- (fs) => fs.stat(input).then((stat) => stat.isDirectory() ? ingestDirectory(process2.cwd(), input, commonOptions) : null).catch(() => null)
1081
- ) : null;
1157
+ const directoryResult = !/^https?:\/\//i.test(input) ? await stat(input).then((inputStat) => inputStat.isDirectory() ? ingestDirectory(process2.cwd(), input, commonOptions) : null).catch(() => null) : null;
1082
1158
  if (directoryResult) {
1083
1159
  const scope2 = options.review || guideEnabled ? await (async () => {
1084
1160
  const pathModule = await import("path");
@@ -1837,8 +1913,8 @@ graph.command("serve").description("Serve the local graph viewer.").option("--po
1837
1913
  });
1838
1914
  });
1839
1915
  graph.command("export").description(
1840
- "Export the graph as HTML, report, SVG, GraphML, Cypher, JSON, Obsidian vault, or Obsidian canvas. Combine flags to write multiple formats in one run."
1841
- ).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("--neo4j <output>", "Compatibility alias for --cypher, writing a Neo4j Cypher import file").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", "Include the full graph in HTML export (default; queries traverse complete graph)", true).option("--overview", "Use overview sampling for HTML export (smaller file, queries limited to sampled nodes)", false).action(
1916
+ "Export the graph as HTML, report, SVG, GraphML, Cypher, JSON, callflow HTML, Obsidian vault, or Obsidian canvas. Combine flags to write multiple formats in one run."
1917
+ ).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("--neo4j <output>", "Compatibility alias for --cypher, writing a Neo4j Cypher import file").option("--json <output>", "Output JSON file path").option("--callflow <output>", "Output directed callflow HTML file path").option("--obsidian <output>", "Output Obsidian vault directory path").option("--canvas <output>", "Output Obsidian canvas file path").option("--full", "Include the full graph in HTML export (default; queries traverse complete graph)", true).option("--overview", "Use overview sampling for HTML export (smaller file, queries limited to sampled nodes)", false).action(
1842
1918
  async (options) => {
1843
1919
  const useFullGraph = options.overview ? false : options.full ?? true;
1844
1920
  const targets = [
@@ -1850,12 +1926,13 @@ graph.command("export").description(
1850
1926
  options.cypher ? { format: "cypher", outputPath: options.cypher } : null,
1851
1927
  options.neo4j ? { format: "cypher", outputPath: options.neo4j } : null,
1852
1928
  options.json ? { format: "json", outputPath: options.json } : null,
1929
+ options.callflow ? { format: "callflow", outputPath: options.callflow } : null,
1853
1930
  options.obsidian ? { format: "obsidian", outputPath: options.obsidian } : null,
1854
1931
  options.canvas ? { format: "canvas", outputPath: options.canvas } : null
1855
1932
  ].filter((target) => Boolean(target));
1856
1933
  if (targets.length === 0) {
1857
1934
  throw new Error(
1858
- "Pass at least one of --html, --html-standalone, --report, --svg, --graphml, --cypher, --neo4j, --json, --obsidian, or --canvas."
1935
+ "Pass at least one of --html, --html-standalone, --report, --svg, --graphml, --cypher, --neo4j, --json, --callflow, --obsidian, or --canvas."
1859
1936
  );
1860
1937
  }
1861
1938
  const results = [];
@@ -1994,6 +2071,24 @@ graph.command("blast").description("Show the blast radius of changing a file or
1994
2071
  log(` ${" ".repeat(mod.depth - 1)}${mod.label} (depth ${mod.depth})`);
1995
2072
  }
1996
2073
  });
2074
+ graph.command("cycles").description("Find directed cycles in the compiled graph, defaulting to import edges.").option("--relation <name>", "Relation name to follow (repeatable; default: imports)", collectRepeated, []).option("--limit <n>", "Maximum cycles to report", "25").option("--max-depth <n>", "Maximum cycle depth", "25").action(async (options) => {
2075
+ const { paths } = await loadVaultConfig(process2.cwd());
2076
+ const raw = await readFile2(paths.graphPath, "utf-8");
2077
+ const graphArtifact = JSON.parse(raw);
2078
+ const result = findGraphCycles(graphArtifact, {
2079
+ relations: options.relation?.length ? options.relation : ["imports"],
2080
+ limit: parsePositiveInt(options.limit, 25),
2081
+ maxDepth: parsePositiveInt(options.maxDepth, 25)
2082
+ });
2083
+ if (isJson()) {
2084
+ emitJson(result);
2085
+ return;
2086
+ }
2087
+ log(result.summary);
2088
+ for (const cycle of result.cycles) {
2089
+ log(`- ${cycle.labels.join(" -> ")} -> ${cycle.labels[0]} (${cycle.relations.join(", ")})`);
2090
+ }
2091
+ });
1997
2092
  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) => {
1998
2093
  const result = await createSupersessionEdge(process2.cwd(), pageId, replacedById);
1999
2094
  if (isJson()) {
@@ -2177,6 +2272,88 @@ provider.command("setup").description("Interactive setup for a provider (current
2177
2272
  log(`Left tasks.audioProvider = "${registration.previousAudioProvider}" untouched (use --set-audio-provider to override).`);
2178
2273
  }
2179
2274
  });
2275
+ provider.command("add").description("Add or update a named provider in swarmvault.config.json.").argument("<id>", "Provider id").requiredOption("--type <type>", `Provider type: ${providerTypes.join(", ")}`).requiredOption("--model <model>", "Provider model name").option("--base-url <url>", "OpenAI-compatible base URL").option("--api-key-env <name>", "Environment variable that holds the provider API key").option("--capability <capability>", `Provider capability (${providerCapabilities.join(", ")})`, collectRepeated, []).option("--task <task>", `Assign provider to task (${providerTaskKeys.join(", ")})`, collectRepeated, []).option("--api-style <style>", "OpenAI-compatible API style: responses or chat").option("--module <path>", "Custom provider module path").option("--binary-path <path>", "Local provider binary path").option("--model-path <path>", "Local model file path").option("--threads <n>", "Local provider thread count").action(
2276
+ async (id, options) => {
2277
+ const apiStyle = options.apiStyle;
2278
+ if (apiStyle && apiStyle !== "responses" && apiStyle !== "chat") {
2279
+ throw new Error("--api-style must be responses or chat.");
2280
+ }
2281
+ const threads = options.threads ? parsePositiveInt(options.threads, 0) || void 0 : void 0;
2282
+ const result = await addProviderConfig({
2283
+ rootDir: process2.cwd(),
2284
+ providerId: id,
2285
+ provider: {
2286
+ type: parseProviderType(options.type),
2287
+ model: options.model,
2288
+ baseUrl: options.baseUrl,
2289
+ apiKeyEnv: options.apiKeyEnv,
2290
+ capabilities: options.capability?.map(parseProviderCapability),
2291
+ apiStyle,
2292
+ module: options.module,
2293
+ binaryPath: options.binaryPath,
2294
+ modelPath: options.modelPath,
2295
+ threads
2296
+ },
2297
+ tasks: options.task?.map(parseProviderTask)
2298
+ });
2299
+ if (isJson()) {
2300
+ emitJson(result);
2301
+ return;
2302
+ }
2303
+ log(`${result.added ? "Added" : result.updated ? "Updated" : "Kept"} provider ${result.providerId} in ${result.configPath}.`);
2304
+ if (result.updatedTasks.length) {
2305
+ log(`Assigned tasks: ${result.updatedTasks.join(", ")}`);
2306
+ }
2307
+ }
2308
+ );
2309
+ provider.command("list").description("List configured providers and task assignments.").action(async () => {
2310
+ const entries = await listProviderConfigEntries(process2.cwd());
2311
+ if (isJson()) {
2312
+ emitJson(entries);
2313
+ return;
2314
+ }
2315
+ if (!entries.length) {
2316
+ log("No providers configured.");
2317
+ return;
2318
+ }
2319
+ for (const entry of entries) {
2320
+ const tasks = entry.assignedTasks.length ? ` tasks=${entry.assignedTasks.join(",")}` : "";
2321
+ const key = entry.apiKeyEnv ? ` key=${entry.apiKeyEnv}` : "";
2322
+ log(`${entry.id} type=${entry.type} model=${entry.model}${key}${tasks}`);
2323
+ }
2324
+ });
2325
+ provider.command("show").description("Show one configured provider.").argument("<id>", "Provider id").action(async (id) => {
2326
+ const entry = await getProviderConfigEntry(process2.cwd(), id);
2327
+ if (!entry) {
2328
+ throw new Error(`Provider ${id} is not configured.`);
2329
+ }
2330
+ if (isJson()) {
2331
+ emitJson(entry);
2332
+ return;
2333
+ }
2334
+ log(`${entry.id}`);
2335
+ log(`type=${entry.type}`);
2336
+ log(`model=${entry.model}`);
2337
+ if (entry.baseUrl) log(`baseUrl=${entry.baseUrl}`);
2338
+ if (entry.apiKeyEnv) log(`apiKeyEnv=${entry.apiKeyEnv}`);
2339
+ if (entry.capabilities.length) log(`capabilities=${entry.capabilities.join(",")}`);
2340
+ if (entry.assignedTasks.length) log(`tasks=${entry.assignedTasks.join(",")}`);
2341
+ });
2342
+ provider.command("remove").description("Remove a configured provider and reassign its tasks to a fallback provider.").argument("<id>", "Provider id").option("--fallback <id>", "Fallback provider for tasks currently assigned to the removed provider").action(async (id, options) => {
2343
+ const result = await removeProviderConfig({
2344
+ rootDir: process2.cwd(),
2345
+ providerId: id,
2346
+ fallbackProviderId: options.fallback
2347
+ });
2348
+ if (isJson()) {
2349
+ emitJson(result);
2350
+ return;
2351
+ }
2352
+ log(`${result.removed ? "Removed" : "No provider named"} ${id}.`);
2353
+ if (result.updatedTasks.length) {
2354
+ log(`Reassigned tasks: ${result.updatedTasks.join(", ")}`);
2355
+ }
2356
+ });
2180
2357
  async function confirmInteractive(message) {
2181
2358
  if (!process2.stdin.isTTY) return false;
2182
2359
  const rl = createInterface({ input: process2.stdin, output: process2.stderr });
@@ -2390,29 +2567,44 @@ program.command("mcp").description("Run SwarmVault as a local MCP server over st
2390
2567
  process2.exit(0);
2391
2568
  });
2392
2569
  });
2393
- program.command("install").description("Install SwarmVault instructions for an agent in the current project.").requiredOption(
2570
+ var install = program.command("install").description("Install SwarmVault instructions for an agent in the current project.");
2571
+ install.command("status").description("Show whether SwarmVault instructions are installed for an agent.").requiredOption("--agent <agent>", "Agent name").option("--hook", "Include hook/plugin targets in the status check", false).option("--scope <scope>", "Install scope to inspect: project or user", "project").action(async (options) => {
2572
+ const scope = options.scope === "user" ? "user" : "project";
2573
+ const result = await getAgentInstallStatus(process2.cwd(), options.agent, { hook: options.hook ?? false, scope });
2574
+ if (isJson()) {
2575
+ emitJson(result);
2576
+ return;
2577
+ }
2578
+ log(`${result.agent} ${result.installed ? "installed" : "not installed"} (${result.scope}${result.hook ? ", hook" : ""})`);
2579
+ for (const target of result.targets) {
2580
+ log(`${target.exists ? "ok" : "missing"} ${target.path}`);
2581
+ }
2582
+ });
2583
+ install.option(
2394
2584
  "--agent <agent>",
2395
- "claude, codex, cursor, gemini, goose, opencode, copilot, aider, droid, pi, trae, claw, kiro, hermes, antigravity, vscode, amp, augment, adal, bob, cline, codebuddy, command-code, continue, cortex, crush, deepagents, firebender, iflow, junie, kilo-code, kimi, kode, mcpjam, mistral-vibe, mux, neovate, openclaw, openhands, pochi, qoder, qwen-code, replit, roo-code, trae-cn, warp, windsurf, or zencoder"
2396
- ).option("--hook", "Also install hook/plugin guidance when the target agent supports it", false).action(
2397
- async (options) => {
2398
- const hookCapableAgents = /* @__PURE__ */ new Set(["codex", "claude", "opencode", "gemini", "copilot"]);
2399
- if (options.hook && !hookCapableAgents.has(options.agent)) {
2400
- throw new Error("--hook is only supported for --agent codex, claude, opencode, gemini, or copilot");
2585
+ "claude, codex, cursor, gemini, goose, opencode, copilot, aider, droid, pi, trae, claw, kiro, kilo, hermes, antigravity, vscode, amp, augment, adal, bob, cline, codebuddy, command-code, continue, cortex, crush, deepagents, devin, firebender, iflow, junie, kilo-code, kimi, kode, mcpjam, mistral-vibe, mux, neovate, openclaw, openhands, pochi, qoder, qwen-code, replit, roo-code, trae-cn, warp, windsurf, or zencoder"
2586
+ ).option("--hook", "Also install hook/plugin guidance when the target agent supports it", false).option("--scope <scope>", "Install scope: project or user", "project").action(async (options) => {
2587
+ if (!options.agent) {
2588
+ throw new Error("Specify --agent <agent>.");
2589
+ }
2590
+ const hookCapableAgents = /* @__PURE__ */ new Set(["codex", "claude", "opencode", "gemini", "copilot", "kilo"]);
2591
+ if (options.hook && !hookCapableAgents.has(options.agent)) {
2592
+ throw new Error("--hook is only supported for --agent codex, claude, opencode, gemini, copilot, or kilo");
2593
+ }
2594
+ const scope = options.scope === "user" ? "user" : "project";
2595
+ const result = await installAgent(process2.cwd(), options.agent, { hook: options.hook ?? false, scope });
2596
+ if (isJson()) {
2597
+ emitJson({ ...result, hook: options.hook ?? false, scope });
2598
+ } else {
2599
+ log(`Installed rules into ${result.target}`);
2600
+ if (result.targets.length > 1) {
2601
+ log(`Also wrote: ${result.targets.filter((entry) => entry !== result.target).join(", ")}`);
2401
2602
  }
2402
- const result = await installAgent(process2.cwd(), options.agent, { hook: options.hook ?? false });
2403
- if (isJson()) {
2404
- emitJson({ ...result, hook: options.hook ?? false });
2405
- } else {
2406
- log(`Installed rules into ${result.target}`);
2407
- if (result.targets.length > 1) {
2408
- log(`Also wrote: ${result.targets.filter((entry) => entry !== result.target).join(", ")}`);
2409
- }
2410
- for (const warning of result.warnings ?? []) {
2411
- emitNotice(warning);
2412
- }
2603
+ for (const warning of result.warnings ?? []) {
2604
+ emitNotice(warning);
2413
2605
  }
2414
2606
  }
2415
- );
2607
+ });
2416
2608
  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) => {
2417
2609
  const { mkdtemp, writeFile: writeFile3, mkdir: mkdir3 } = await import("fs/promises");
2418
2610
  const { tmpdir } = await import("os");
@@ -2765,8 +2957,8 @@ retrieval.command("doctor").description("Diagnose retrieval index problems and o
2765
2957
  log(`Warning: ${warning}`);
2766
2958
  }
2767
2959
  });
2768
- program.command("scan", { hidden: true }).description("Quick-start: initialize, ingest, compile, and serve a graph viewer in one command.").argument("<input>", "Directory or public GitHub repo root URL to scan").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").option("--no-viz", "Compatibility alias for --no-serve; skip launching the graph viewer after compile").option("--mcp", "Start the MCP stdio server after compile instead of launching the graph viewer", false).option("--branch <name>", "GitHub branch to clone when scanning a public repo URL").option("--ref <ref>", "Git ref, tag, or commit to check out when scanning a public repo URL").option("--checkout-dir <path>", "Persistent checkout directory for a public GitHub repo scan").action(runScanCommand);
2769
- program.command("clone", { hidden: true }).description("Compatibility alias for scan: initialize, clone/register a public repo URL, and compile it into the vault.").argument("<input>", "Public GitHub repo URL or local directory to scan").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").option("--no-viz", "Compatibility alias for --no-serve; skip launching the graph viewer after compile").option("--mcp", "Start the MCP stdio server after compile instead of launching the graph viewer", false).option("--branch <name>", "GitHub branch to clone when scanning a public repo URL").option("--ref <ref>", "Git ref, tag, or commit to check out when scanning a public repo URL").option("--checkout-dir <path>", "Persistent checkout directory for a public GitHub repo scan").action(runScanCommand);
2960
+ program.command("scan", { hidden: true }).description("Quick-start: initialize, ingest, compile, and serve a graph viewer in one command.").argument("<input>", "Directory or public GitHub repo root URL to scan").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").option("--no-viz", "Compatibility alias for --no-serve; skip launching the graph viewer after compile").option("--mcp", "Start the MCP stdio server after compile instead of launching the graph viewer", false).option("--branch <name>", "GitHub branch to clone when scanning a public repo URL").option("--ref <ref>", "Git ref, tag, or commit to check out when scanning a public repo URL").option("--checkout-dir <path>", "Persistent checkout directory for a public GitHub repo scan").option("--install-agent-rules", "Install configured agent rule files during initialization", false).action(runScanCommand);
2961
+ program.command("clone", { hidden: true }).description("Compatibility alias for scan: initialize, clone/register a public repo URL, and compile it into the vault.").argument("<input>", "Public GitHub repo URL or local directory to scan").option("--port <port>", "Port for the graph viewer").option("--no-serve", "Skip launching the graph viewer after compile").option("--no-viz", "Compatibility alias for --no-serve; skip launching the graph viewer after compile").option("--mcp", "Start the MCP stdio server after compile instead of launching the graph viewer", false).option("--branch <name>", "GitHub branch to clone when scanning a public repo URL").option("--ref <ref>", "Git ref, tag, or commit to check out when scanning a public repo URL").option("--checkout-dir <path>", "Persistent checkout directory for a public GitHub repo scan").option("--install-agent-rules", "Install configured agent rule files during initialization", false).action(runScanCommand);
2770
2962
  function enableStructuredJsonOnSubcommands(command) {
2771
2963
  for (const subcommand of command.commands) {
2772
2964
  const hasJsonOption = subcommand.options.some((option) => option.attributeName() === "json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmvaultai/cli",
3
- "version": "3.14.2",
3
+ "version": "3.16.0",
4
4
  "description": "Global CLI for SwarmVault.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -44,7 +44,7 @@
44
44
  "prepublishOnly": "node ../../scripts/check-release-sync.mjs && node ../../scripts/check-published-manifests.mjs"
45
45
  },
46
46
  "dependencies": {
47
- "@swarmvaultai/engine": "3.14.2",
47
+ "@swarmvaultai/engine": "3.16.0",
48
48
  "commander": "^14.0.1"
49
49
  },
50
50
  "devDependencies": {