@swarmvaultai/cli 0.1.25 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -0
  2. package/dist/index.js +258 -57
  3. package/package.json +5 -4
package/README.md CHANGED
@@ -78,10 +78,15 @@ Useful flags:
78
78
  - `--include <glob...>`
79
79
  - `--exclude <glob...>`
80
80
  - `--max-files <n>`
81
+ - `--include-third-party`
82
+ - `--include-resources`
83
+ - `--include-generated`
81
84
  - `--no-gitignore`
82
85
  - `--no-include-assets`
83
86
  - `--max-asset-size <bytes>`
84
87
 
88
+ 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.
89
+
85
90
  ### `swarmvault add <url>`
86
91
 
87
92
  Capture supported URLs through a normalized markdown layer before ingesting them into the vault.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync } from "fs";
5
- import process from "process";
5
+ import process2 from "process";
6
6
  import {
7
7
  acceptApproval,
8
8
  addInput,
@@ -42,45 +42,245 @@ import {
42
42
  watchVault
43
43
  } from "@swarmvaultai/engine";
44
44
  import { Command, Option } from "commander";
45
+
46
+ // src/notices.ts
47
+ import { spawn } from "child_process";
48
+ import { mkdir, readFile, writeFile } from "fs/promises";
49
+ import os from "os";
50
+ import path from "path";
51
+ var NOTICE_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
52
+ var NOTICE_TIMEOUT_MS = 2e3;
53
+ var STAR_URL = "https://github.com/swarmclawai/swarmvault";
54
+ var NPM_PACKAGE = "@swarmvaultai/cli";
55
+ var SUPPRESSED_COMMANDS = /* @__PURE__ */ new Set(["graph serve", "mcp", "schedule serve", "watch"]);
56
+ function resolveCliStatePath(env = process.env) {
57
+ const override = env.SWARMVAULT_CLI_STATE_PATH?.trim();
58
+ if (override) {
59
+ return path.resolve(override);
60
+ }
61
+ const homeDir = os.homedir();
62
+ if (!homeDir) {
63
+ return null;
64
+ }
65
+ return path.join(homeDir, ".swarmvault", "cli-state.json");
66
+ }
67
+ function shouldEmitCliNotices(options) {
68
+ const env = options.env ?? process.env;
69
+ if (options.json) {
70
+ return false;
71
+ }
72
+ if (env.SWARMVAULT_NO_NOTICES === "1") {
73
+ return false;
74
+ }
75
+ if (Boolean(env.CI) && env.CI !== "0") {
76
+ return false;
77
+ }
78
+ if (!(options.stdoutIsTTY ?? process.stdout.isTTY) || !(options.stderrIsTTY ?? process.stderr.isTTY)) {
79
+ return false;
80
+ }
81
+ const commandKey = options.commandPath.join(" ").trim();
82
+ return !SUPPRESSED_COMMANDS.has(commandKey);
83
+ }
84
+ async function collectCliNotices(options) {
85
+ if (!shouldEmitCliNotices(options)) {
86
+ return [];
87
+ }
88
+ const env = options.env ?? process.env;
89
+ const statePath = options.statePath ?? resolveCliStatePath(env);
90
+ if (!statePath) {
91
+ return [];
92
+ }
93
+ const state = await readNoticeState(statePath);
94
+ const nextState = { ...state };
95
+ const notices = [];
96
+ const now = options.now ?? /* @__PURE__ */ new Date();
97
+ const nowMs = now.getTime();
98
+ if (!state.starPromptShown) {
99
+ notices.push(`If SwarmVault is useful, star the repo: ${STAR_URL}`);
100
+ nextState.starPromptShown = true;
101
+ }
102
+ const lastCheckMs = state.lastUpdateCheckAt ? Date.parse(state.lastUpdateCheckAt) : Number.NaN;
103
+ const shouldCheckUpdates = !Number.isFinite(lastCheckMs) || nowMs - lastCheckMs >= NOTICE_CACHE_TTL_MS;
104
+ if (shouldCheckUpdates) {
105
+ const fetchLatestVersion = options.fetchLatestVersion ?? (() => fetchLatestCliVersion(env));
106
+ const latestVersion = await fetchLatestVersion().catch(() => null);
107
+ nextState.lastUpdateCheckAt = now.toISOString();
108
+ if (latestVersion) {
109
+ nextState.lastSeenLatestVersion = latestVersion;
110
+ if (isVersionNewer(latestVersion, options.currentVersion)) {
111
+ notices.unshift(
112
+ `Update available: ${latestVersion} (current ${options.currentVersion}). Upgrade with: npm install -g ${NPM_PACKAGE}@latest`
113
+ );
114
+ }
115
+ }
116
+ }
117
+ await writeNoticeState(statePath, nextState);
118
+ return notices;
119
+ }
120
+ async function readNoticeState(statePath) {
121
+ try {
122
+ const raw = await readFile(statePath, "utf8");
123
+ const parsed = JSON.parse(raw);
124
+ return typeof parsed === "object" && parsed ? parsed : {};
125
+ } catch {
126
+ return {};
127
+ }
128
+ }
129
+ async function writeNoticeState(statePath, state) {
130
+ try {
131
+ await mkdir(path.dirname(statePath), { recursive: true });
132
+ await writeFile(statePath, `${JSON.stringify(state, null, 2)}
133
+ `, "utf8");
134
+ } catch {
135
+ }
136
+ }
137
+ async function fetchLatestCliVersion(env) {
138
+ return await new Promise((resolve) => {
139
+ const child = spawn("npm", ["view", NPM_PACKAGE, "version", "--json"], {
140
+ env: {
141
+ ...process.env,
142
+ ...env,
143
+ npm_config_audit: "false",
144
+ npm_config_fund: "false",
145
+ npm_config_update_notifier: "false"
146
+ },
147
+ stdio: ["ignore", "pipe", "ignore"]
148
+ });
149
+ const chunks = [];
150
+ let settled = false;
151
+ const finish = (value) => {
152
+ if (settled) {
153
+ return;
154
+ }
155
+ settled = true;
156
+ clearTimeout(timeoutId);
157
+ resolve(value);
158
+ };
159
+ child.stdout.on("data", (chunk) => {
160
+ chunks.push(chunk);
161
+ });
162
+ child.on("error", () => {
163
+ finish(null);
164
+ });
165
+ child.on("exit", (code) => {
166
+ if (code !== 0) {
167
+ finish(null);
168
+ return;
169
+ }
170
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
171
+ if (!raw) {
172
+ finish(null);
173
+ return;
174
+ }
175
+ try {
176
+ const parsed = JSON.parse(raw);
177
+ finish(typeof parsed === "string" && parsed.trim() ? parsed.trim() : null);
178
+ } catch {
179
+ finish(raw.replace(/^"+|"+$/g, "").trim() || null);
180
+ }
181
+ });
182
+ const timeoutId = setTimeout(() => {
183
+ child.kill("SIGKILL");
184
+ finish(null);
185
+ }, NOTICE_TIMEOUT_MS);
186
+ });
187
+ }
188
+ function isVersionNewer(candidate2, current) {
189
+ return compareVersions(candidate2, current) > 0;
190
+ }
191
+ function compareVersions(left, right) {
192
+ const leftParts = normalizeVersion(left);
193
+ const rightParts = normalizeVersion(right);
194
+ const maxLength = Math.max(leftParts.length, rightParts.length);
195
+ for (let index = 0; index < maxLength; index += 1) {
196
+ const leftValue = leftParts[index] ?? 0;
197
+ const rightValue = rightParts[index] ?? 0;
198
+ if (leftValue !== rightValue) {
199
+ return leftValue - rightValue;
200
+ }
201
+ }
202
+ return 0;
203
+ }
204
+ function normalizeVersion(version) {
205
+ const match = version.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
206
+ if (!match) {
207
+ return [0];
208
+ }
209
+ return match.slice(1).map((segment) => Number.parseInt(segment ?? "0", 10) || 0);
210
+ }
211
+
212
+ // src/index.ts
45
213
  var program = new Command();
46
214
  var CLI_VERSION = readCliVersion();
47
215
  program.name("swarmvault").description("SwarmVault is a local-first LLM wiki compiler with graph outputs and pluggable providers.").version(CLI_VERSION).option("--json", "Emit structured JSON output", false);
48
216
  function readCliVersion() {
49
217
  try {
50
218
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
51
- return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "0.1.25";
219
+ return typeof packageJson.version === "string" && packageJson.version.trim() ? packageJson.version : "0.1.26";
52
220
  } catch {
53
- return "0.1.25";
221
+ return "0.1.26";
54
222
  }
55
223
  }
56
224
  function isJson() {
57
225
  return program.opts().json === true;
58
226
  }
59
227
  function emitJson(data) {
60
- process.stdout.write(`${JSON.stringify(data)}
228
+ process2.stdout.write(`${JSON.stringify(data)}
61
229
  `);
62
230
  }
63
231
  function log(message) {
64
232
  if (isJson()) {
65
- process.stderr.write(`${message}
233
+ process2.stderr.write(`${message}
66
234
  `);
67
235
  } else {
68
- process.stdout.write(`${message}
236
+ process2.stdout.write(`${message}
69
237
  `);
70
238
  }
71
239
  }
240
+ function emitNotice(message) {
241
+ process2.stderr.write(`[swarmvault] ${message}
242
+ `);
243
+ }
244
+ function getCommandPath(command) {
245
+ const names = [];
246
+ let current = command;
247
+ while (current) {
248
+ const name = current.name();
249
+ if (name && name !== "swarmvault") {
250
+ names.unshift(name);
251
+ }
252
+ current = current.parent ?? null;
253
+ }
254
+ return names;
255
+ }
256
+ program.hook("postAction", async (_thisCommand, actionCommand) => {
257
+ const notices = await collectCliNotices({
258
+ commandPath: getCommandPath(actionCommand),
259
+ currentVersion: CLI_VERSION,
260
+ json: isJson()
261
+ });
262
+ for (const notice of notices) {
263
+ emitNotice(notice);
264
+ }
265
+ });
72
266
  program.command("init").description("Initialize a SwarmVault workspace in the current directory.").option("--obsidian", "Generate a minimal .obsidian workspace alongside the vault", false).action(async (options) => {
73
- await initVault(process.cwd(), { obsidian: options.obsidian ?? false });
267
+ await initVault(process2.cwd(), { obsidian: options.obsidian ?? false });
74
268
  if (isJson()) {
75
- emitJson({ status: "initialized", rootDir: process.cwd(), obsidian: options.obsidian ?? false });
269
+ emitJson({ status: "initialized", rootDir: process2.cwd(), obsidian: options.obsidian ?? false });
76
270
  } else {
77
271
  log("Initialized SwarmVault workspace.");
78
272
  }
79
273
  });
80
- 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("--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("--no-gitignore", "Ignore .gitignore rules when ingesting a directory").action(
274
+ 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("--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(
81
275
  async (input, options) => {
82
276
  const maxAssetSize = typeof options.maxAssetSize === "string" && options.maxAssetSize.trim() ? Number.parseInt(options.maxAssetSize, 10) : void 0;
83
277
  const maxFiles = typeof options.maxFiles === "string" && options.maxFiles.trim() ? Number.parseInt(options.maxFiles, 10) : void 0;
278
+ const extractClasses = [
279
+ "first_party",
280
+ ...options.includeThirdParty ? ["third_party"] : [],
281
+ ...options.includeResources ? ["resource"] : [],
282
+ ...options.includeGenerated ? ["generated"] : []
283
+ ];
84
284
  const commonOptions = {
85
285
  includeAssets: options.includeAssets,
86
286
  maxAssetSize: Number.isFinite(maxAssetSize) ? maxAssetSize : void 0,
@@ -88,10 +288,11 @@ program.command("ingest").description("Ingest a local file path, directory path,
88
288
  include: options.include,
89
289
  exclude: options.exclude,
90
290
  maxFiles: Number.isFinite(maxFiles) ? maxFiles : void 0,
91
- gitignore: options.gitignore
291
+ gitignore: options.gitignore,
292
+ extractClasses
92
293
  };
93
294
  const directoryResult = !/^https?:\/\//i.test(input) ? await import("fs/promises").then(
94
- (fs) => fs.stat(input).then((stat) => stat.isDirectory() ? ingestDirectory(process.cwd(), input, commonOptions) : null).catch(() => null)
295
+ (fs) => fs.stat(input).then((stat) => stat.isDirectory() ? ingestDirectory(process2.cwd(), input, commonOptions) : null).catch(() => null)
95
296
  ) : null;
96
297
  if (directoryResult) {
97
298
  if (isJson()) {
@@ -103,7 +304,7 @@ program.command("ingest").description("Ingest a local file path, directory path,
103
304
  }
104
305
  return;
105
306
  }
106
- const manifest = await ingestInput(process.cwd(), input, commonOptions);
307
+ const manifest = await ingestInput(process2.cwd(), input, commonOptions);
107
308
  if (isJson()) {
108
309
  emitJson(manifest);
109
310
  } else {
@@ -112,7 +313,7 @@ program.command("ingest").description("Ingest a local file path, directory path,
112
313
  }
113
314
  );
114
315
  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) => {
115
- const result = await addInput(process.cwd(), input, {
316
+ const result = await addInput(process2.cwd(), input, {
116
317
  author: options.author,
117
318
  contributor: options.contributor
118
319
  });
@@ -124,7 +325,7 @@ program.command("add").description("Capture supported URLs into normalized markd
124
325
  });
125
326
  var inbox = program.command("inbox").description("Inbox and capture workflows.");
126
327
  inbox.command("import").description("Import supported files from the configured inbox directory.").argument("[dir]", "Optional inbox directory override").action(async (dir) => {
127
- const result = await importInbox(process.cwd(), dir);
328
+ const result = await importInbox(process2.cwd(), dir);
128
329
  if (isJson()) {
129
330
  emitJson(result);
130
331
  } else {
@@ -134,7 +335,7 @@ inbox.command("import").description("Import supported files from the configured
134
335
  }
135
336
  });
136
337
  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) => {
137
- const result = await compileVault(process.cwd(), { approve: options.approve ?? false });
338
+ const result = await compileVault(process2.cwd(), { approve: options.approve ?? false });
138
339
  if (isJson()) {
139
340
  emitJson(result);
140
341
  } else {
@@ -148,7 +349,7 @@ program.command("compile").description("Compile manifests into wiki pages, graph
148
349
  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(
149
350
  new Option("--format <format>", "Output format").choices(["markdown", "report", "slides", "chart", "image"]).default("markdown")
150
351
  ).action(async (question, options) => {
151
- const result = await queryVault(process.cwd(), {
352
+ const result = await queryVault(process2.cwd(), {
152
353
  question,
153
354
  save: options.save ?? true,
154
355
  format: options.format
@@ -166,7 +367,7 @@ program.command("explore").description("Run a save-first multi-step exploration
166
367
  new Option("--format <format>", "Output format for step pages").choices(["markdown", "report", "slides", "chart", "image"]).default("markdown")
167
368
  ).action(async (question, options) => {
168
369
  const stepCount = Number.parseInt(options.steps ?? "3", 10);
169
- const result = await exploreVault(process.cwd(), {
370
+ const result = await exploreVault(process2.cwd(), {
170
371
  question,
171
372
  steps: Number.isFinite(stepCount) ? stepCount : 3,
172
373
  format: options.format
@@ -179,7 +380,7 @@ program.command("explore").description("Run a save-first multi-step exploration
179
380
  }
180
381
  });
181
382
  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) => {
182
- const result = await benchmarkVault(process.cwd(), {
383
+ const result = await benchmarkVault(process2.cwd(), {
183
384
  questions: options.question
184
385
  });
185
386
  if (isJson()) {
@@ -191,7 +392,7 @@ program.command("benchmark").description("Measure graph-guided context reduction
191
392
  }
192
393
  });
193
394
  program.command("lint").description("Run anti-drift and wiki-health checks.").option("--deep", "Run LLM-powered advisory lint", false).option("--web", "Augment deep lint with configured web search", false).action(async (options) => {
194
- const findings = await lintVault(process.cwd(), {
395
+ const findings = await lintVault(process2.cwd(), {
195
396
  deep: options.deep ?? false,
196
397
  web: options.web ?? false
197
398
  });
@@ -210,15 +411,15 @@ program.command("lint").description("Run anti-drift and wiki-health checks.").op
210
411
  var graph = program.command("graph").description("Graph-related commands.");
211
412
  graph.command("serve").description("Serve the local graph viewer.").option("--port <port>", "Port override").action(async (options) => {
212
413
  const port = options.port ? Number.parseInt(options.port, 10) : void 0;
213
- const server = await startGraphServer(process.cwd(), port);
414
+ const server = await startGraphServer(process2.cwd(), port);
214
415
  if (isJson()) {
215
416
  emitJson({ port: server.port, url: `http://localhost:${server.port}` });
216
417
  } else {
217
418
  log(`Graph viewer running at http://localhost:${server.port}`);
218
419
  }
219
- process.on("SIGINT", async () => {
420
+ process2.on("SIGINT", async () => {
220
421
  await server.close();
221
- process.exit(0);
422
+ process2.exit(0);
222
423
  });
223
424
  });
224
425
  graph.command("export").description("Export the graph as HTML, SVG, GraphML, or Cypher.").option("--html <output>", "Output HTML file path").option("--svg <output>", "Output SVG file path").option("--graphml <output>", "Output GraphML file path").option("--cypher <output>", "Output Cypher file path").action(async (options) => {
@@ -232,7 +433,7 @@ graph.command("export").description("Export the graph as HTML, SVG, GraphML, or
232
433
  throw new Error("Pass exactly one of --html, --svg, --graphml, or --cypher.");
233
434
  }
234
435
  const target = targets[0];
235
- const outputPath = target.format === "html" ? await exportGraphHtml(process.cwd(), target.outputPath) : (await exportGraphFormat(process.cwd(), target.format, target.outputPath)).outputPath;
436
+ const outputPath = target.format === "html" ? await exportGraphHtml(process2.cwd(), target.outputPath) : (await exportGraphFormat(process2.cwd(), target.format, target.outputPath)).outputPath;
236
437
  if (isJson()) {
237
438
  emitJson({ format: target.format, outputPath });
238
439
  } else {
@@ -241,7 +442,7 @@ graph.command("export").description("Export the graph as HTML, SVG, GraphML, or
241
442
  });
242
443
  graph.command("query").description("Traverse the compiled graph deterministically from local search seeds.").argument("<question>", "Question or graph search seed").option("--dfs", "Prefer a depth-first traversal instead of breadth-first", false).option("--budget <n>", "Maximum number of graph nodes to summarize").action(async (question, options) => {
243
444
  const budget = options.budget ? Number.parseInt(options.budget, 10) : void 0;
244
- const result = await queryGraphVault(process.cwd(), question, {
445
+ const result = await queryGraphVault(process2.cwd(), question, {
245
446
  traversal: options.dfs ? "dfs" : "bfs",
246
447
  budget: Number.isFinite(budget) ? budget : void 0
247
448
  });
@@ -252,7 +453,7 @@ graph.command("query").description("Traverse the compiled graph deterministicall
252
453
  log(result.summary);
253
454
  });
254
455
  graph.command("path").description("Find the shortest graph path between two nodes or pages.").argument("<from>", "Source node/page label or id").argument("<to>", "Target node/page label or id").action(async (from, to) => {
255
- const result = await pathGraphVault(process.cwd(), from, to);
456
+ const result = await pathGraphVault(process2.cwd(), from, to);
256
457
  if (isJson()) {
257
458
  emitJson(result);
258
459
  return;
@@ -260,7 +461,7 @@ graph.command("path").description("Find the shortest graph path between two node
260
461
  log(result.summary);
261
462
  });
262
463
  graph.command("explain").description("Explain a graph node, its page, community, and neighbors.").argument("<target>", "Node/page label or id").action(async (target) => {
263
- const result = await explainGraphVault(process.cwd(), target);
464
+ const result = await explainGraphVault(process2.cwd(), target);
264
465
  if (isJson()) {
265
466
  emitJson(result);
266
467
  return;
@@ -269,7 +470,7 @@ graph.command("explain").description("Explain a graph node, its page, community,
269
470
  });
270
471
  graph.command("god-nodes").description("List the highest-connectivity non-source graph nodes.").option("--limit <n>", "Maximum number of nodes to return", "10").action(async (options) => {
271
472
  const limit = Number.parseInt(options.limit ?? "10", 10);
272
- const result = await listGodNodes(process.cwd(), Number.isFinite(limit) ? limit : 10);
473
+ const result = await listGodNodes(process2.cwd(), Number.isFinite(limit) ? limit : 10);
273
474
  if (isJson()) {
274
475
  emitJson(result);
275
476
  return;
@@ -280,7 +481,7 @@ graph.command("god-nodes").description("List the highest-connectivity non-source
280
481
  });
281
482
  var review = program.command("review").description("Review staged compile approval bundles.");
282
483
  review.command("list").description("List staged approval bundles and their resolution status.").action(async () => {
283
- const approvals = await listApprovals(process.cwd());
484
+ const approvals = await listApprovals(process2.cwd());
284
485
  if (isJson()) {
285
486
  emitJson(approvals);
286
487
  return;
@@ -296,7 +497,7 @@ review.command("list").description("List staged approval bundles and their resol
296
497
  }
297
498
  });
298
499
  review.command("show").description("Show the entries inside a staged approval bundle.").argument("<approvalId>", "Approval bundle identifier").action(async (approvalId) => {
299
- const approval = await readApproval(process.cwd(), approvalId);
500
+ const approval = await readApproval(process2.cwd(), approvalId);
300
501
  if (isJson()) {
301
502
  emitJson(approval);
302
503
  return;
@@ -307,7 +508,7 @@ review.command("show").description("Show the entries inside a staged approval bu
307
508
  }
308
509
  });
309
510
  review.command("accept").description("Accept all pending entries, or selected entries, from a staged approval bundle.").argument("<approvalId>", "Approval bundle identifier").argument("[targets...]", "Optional page ids or paths to accept").action(async (approvalId, targets) => {
310
- const result = await acceptApproval(process.cwd(), approvalId, targets);
511
+ const result = await acceptApproval(process2.cwd(), approvalId, targets);
311
512
  if (isJson()) {
312
513
  emitJson(result);
313
514
  } else {
@@ -315,7 +516,7 @@ review.command("accept").description("Accept all pending entries, or selected en
315
516
  }
316
517
  });
317
518
  review.command("reject").description("Reject all pending entries, or selected entries, from a staged approval bundle.").argument("<approvalId>", "Approval bundle identifier").argument("[targets...]", "Optional page ids or paths to reject").action(async (approvalId, targets) => {
318
- const result = await rejectApproval(process.cwd(), approvalId, targets);
519
+ const result = await rejectApproval(process2.cwd(), approvalId, targets);
319
520
  if (isJson()) {
320
521
  emitJson(result);
321
522
  } else {
@@ -324,7 +525,7 @@ review.command("reject").description("Reject all pending entries, or selected en
324
525
  });
325
526
  var candidate = program.command("candidate").description("Candidate page workflows.");
326
527
  candidate.command("list").description("List staged concept and entity candidates.").action(async () => {
327
- const candidates = await listCandidates(process.cwd());
528
+ const candidates = await listCandidates(process2.cwd());
328
529
  if (isJson()) {
329
530
  emitJson(candidates);
330
531
  return;
@@ -338,7 +539,7 @@ candidate.command("list").description("List staged concept and entity candidates
338
539
  }
339
540
  });
340
541
  candidate.command("promote").description("Promote a candidate into its active concept or entity path.").argument("<target>", "Candidate page id or path").action(async (target) => {
341
- const result = await promoteCandidate(process.cwd(), target);
542
+ const result = await promoteCandidate(process2.cwd(), target);
342
543
  if (isJson()) {
343
544
  emitJson(result);
344
545
  } else {
@@ -346,7 +547,7 @@ candidate.command("promote").description("Promote a candidate into its active co
346
547
  }
347
548
  });
348
549
  candidate.command("archive").description("Archive a candidate by removing it from the active candidate set.").argument("<target>", "Candidate page id or path").action(async (target) => {
349
- const result = await archiveCandidate(process.cwd(), target);
550
+ const result = await archiveCandidate(process2.cwd(), target);
350
551
  if (isJson()) {
351
552
  emitJson(result);
352
553
  } else {
@@ -356,7 +557,7 @@ candidate.command("archive").description("Archive a candidate by removing it fro
356
557
  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("--debounce <ms>", "Debounce window in milliseconds", "900").action(async (options) => {
357
558
  const debounceMs = Number.parseInt(options.debounce ?? "900", 10);
358
559
  if (options.once) {
359
- const result = await runWatchCycle(process.cwd(), {
560
+ const result = await runWatchCycle(process2.cwd(), {
360
561
  lint: options.lint ?? false,
361
562
  repo: options.repo ?? false,
362
563
  debounceMs: Number.isFinite(debounceMs) ? debounceMs : 900
@@ -370,8 +571,8 @@ var watch = program.command("watch").description("Watch the inbox directory and
370
571
  }
371
572
  return;
372
573
  }
373
- const { paths } = await loadVaultConfig(process.cwd());
374
- const controller = await watchVault(process.cwd(), {
574
+ const { paths } = await loadVaultConfig(process2.cwd());
575
+ const controller = await watchVault(process2.cwd(), {
375
576
  lint: options.lint ?? false,
376
577
  repo: options.repo ?? false,
377
578
  debounceMs: Number.isFinite(debounceMs) ? debounceMs : 900
@@ -381,13 +582,13 @@ var watch = program.command("watch").description("Watch the inbox directory and
381
582
  } else {
382
583
  log(`Watching inbox${options.repo ? " and tracked repos" : ""} for changes. Press Ctrl+C to stop.`);
383
584
  }
384
- process.on("SIGINT", async () => {
585
+ process2.on("SIGINT", async () => {
385
586
  await controller.close();
386
- process.exit(0);
587
+ process2.exit(0);
387
588
  });
388
589
  });
389
590
  watch.command("status").description("Show the latest watch run plus pending semantic refresh entries.").action(async () => {
390
- const result = await getWatchStatus(process.cwd());
591
+ const result = await getWatchStatus(process2.cwd());
391
592
  if (isJson()) {
392
593
  emitJson(result);
393
594
  return;
@@ -399,7 +600,7 @@ watch.command("status").description("Show the latest watch run plus pending sema
399
600
  }
400
601
  });
401
602
  program.command("watch-status").description("Show the latest watch run plus pending semantic refresh entries.").action(async () => {
402
- const result = await getWatchStatus(process.cwd());
603
+ const result = await getWatchStatus(process2.cwd());
403
604
  if (isJson()) {
404
605
  emitJson(result);
405
606
  return;
@@ -412,7 +613,7 @@ program.command("watch-status").description("Show the latest watch run plus pend
412
613
  });
413
614
  var hook = program.command("hook").description("Install local git hooks that keep tracked repos and the vault in sync.");
414
615
  hook.command("install").description("Install post-commit and post-checkout hooks for the nearest git repository.").action(async () => {
415
- const status = await installGitHooks(process.cwd());
616
+ const status = await installGitHooks(process2.cwd());
416
617
  if (isJson()) {
417
618
  emitJson(status);
418
619
  return;
@@ -420,7 +621,7 @@ hook.command("install").description("Install post-commit and post-checkout hooks
420
621
  log(`Installed hooks in ${status.repoRoot}`);
421
622
  });
422
623
  hook.command("uninstall").description("Remove the SwarmVault-managed git hook blocks from the nearest git repository.").action(async () => {
423
- const status = await uninstallGitHooks(process.cwd());
624
+ const status = await uninstallGitHooks(process2.cwd());
424
625
  if (isJson()) {
425
626
  emitJson(status);
426
627
  return;
@@ -428,7 +629,7 @@ hook.command("uninstall").description("Remove the SwarmVault-managed git hook bl
428
629
  log(`Removed SwarmVault hook blocks from ${status.repoRoot ?? "the current workspace"}`);
429
630
  });
430
631
  hook.command("status").description("Show whether SwarmVault-managed git hooks are installed.").action(async () => {
431
- const status = await getGitHookStatus(process.cwd());
632
+ const status = await getGitHookStatus(process2.cwd());
432
633
  if (isJson()) {
433
634
  emitJson(status);
434
635
  return;
@@ -443,7 +644,7 @@ hook.command("status").description("Show whether SwarmVault-managed git hooks ar
443
644
  });
444
645
  var schedule = program.command("schedule").description("Run scheduled vault maintenance jobs.");
445
646
  schedule.command("list").description("List configured schedule jobs and their next run state.").action(async () => {
446
- const schedules = await listSchedules(process.cwd());
647
+ const schedules = await listSchedules(process2.cwd());
447
648
  if (isJson()) {
448
649
  emitJson(schedules);
449
650
  return;
@@ -459,7 +660,7 @@ schedule.command("list").description("List configured schedule jobs and their ne
459
660
  }
460
661
  });
461
662
  schedule.command("run").description("Run one configured schedule job immediately.").argument("<jobId>", "Schedule identifier").action(async (jobId) => {
462
- const result = await runSchedule(process.cwd(), jobId);
663
+ const result = await runSchedule(process2.cwd(), jobId);
463
664
  if (isJson()) {
464
665
  emitJson(result);
465
666
  return;
@@ -470,46 +671,46 @@ schedule.command("run").description("Run one configured schedule job immediately
470
671
  });
471
672
  schedule.command("serve").description("Run the local schedule loop.").option("--poll <ms>", "Polling interval in milliseconds", "30000").action(async (options) => {
472
673
  const pollMs = Number.parseInt(options.poll ?? "30000", 10);
473
- const controller = await serveSchedules(process.cwd(), Number.isFinite(pollMs) ? pollMs : 3e4);
674
+ const controller = await serveSchedules(process2.cwd(), Number.isFinite(pollMs) ? pollMs : 3e4);
474
675
  if (isJson()) {
475
676
  emitJson({ status: "serving", pollMs: Number.isFinite(pollMs) ? pollMs : 3e4 });
476
677
  } else {
477
678
  log("Serving schedules. Press Ctrl+C to stop.");
478
679
  }
479
- process.on("SIGINT", async () => {
680
+ process2.on("SIGINT", async () => {
480
681
  await controller.close();
481
- process.exit(0);
682
+ process2.exit(0);
482
683
  });
483
684
  });
484
685
  program.command("mcp").description("Run SwarmVault as a local MCP server over stdio.").action(async () => {
485
686
  if (isJson()) {
486
- process.stderr.write(`${JSON.stringify({ status: "running", transport: "stdio" })}
687
+ process2.stderr.write(`${JSON.stringify({ status: "running", transport: "stdio" })}
487
688
  `);
488
689
  }
489
- const controller = await startMcpServer(process.cwd());
490
- process.on("SIGINT", async () => {
690
+ const controller = await startMcpServer(process2.cwd());
691
+ process2.on("SIGINT", async () => {
491
692
  await controller.close();
492
- process.exit(0);
693
+ process2.exit(0);
493
694
  });
494
695
  });
495
696
  program.command("install").description("Install SwarmVault instructions for an agent in the current project.").requiredOption("--agent <agent>", "codex, claude, cursor, goose, pi, gemini, or opencode").option("--hook", "Also install the recommended Claude pre-search hook when agent=claude", false).action(async (options) => {
496
697
  if (options.hook && options.agent !== "claude") {
497
698
  throw new Error("--hook is only supported for --agent claude");
498
699
  }
499
- const target = await installAgent(process.cwd(), options.agent, { claudeHook: options.hook ?? false });
700
+ const target = await installAgent(process2.cwd(), options.agent, { claudeHook: options.hook ?? false });
500
701
  if (isJson()) {
501
702
  emitJson({ agent: options.agent, target, hook: options.hook ?? false });
502
703
  } else {
503
704
  log(`Installed rules into ${target}`);
504
705
  }
505
706
  });
506
- program.parseAsync(process.argv).catch((error) => {
707
+ program.parseAsync(process2.argv).catch((error) => {
507
708
  const message = error instanceof Error ? error.message : String(error);
508
709
  if (isJson()) {
509
710
  emitJson({ error: message });
510
711
  } else {
511
- process.stderr.write(`${message}
712
+ process2.stderr.write(`${message}
512
713
  `);
513
714
  }
514
- process.exit(1);
715
+ process2.exit(1);
515
716
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmvaultai/cli",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Global CLI for SwarmVault.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,15 +39,16 @@
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsup src/index.ts --format esm --dts",
42
- "test": "node -e \"process.exit(0)\"",
42
+ "test": "vitest run",
43
43
  "typecheck": "tsc --noEmit"
44
44
  },
45
45
  "dependencies": {
46
- "@swarmvaultai/engine": "0.1.25",
46
+ "@swarmvaultai/engine": "0.1.26",
47
47
  "commander": "^14.0.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/node": "^24.6.0",
51
- "tsup": "^8.5.0"
51
+ "tsup": "^8.5.0",
52
+ "vitest": "^3.2.4"
52
53
  }
53
54
  }