skillwiki 0.9.34 → 0.9.35

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.
@@ -63,7 +63,8 @@ var ExitCode = {
63
63
  LOG_APPEND_LOCK_HELD: 49,
64
64
  FLEET_MANIFEST_INVALID: 50,
65
65
  SENSITIVE_CONTENT_DETECTED: 51,
66
- FLEET_SATELLITE_HEALTH_FAILED: 52
66
+ FLEET_SATELLITE_HEALTH_FAILED: 52,
67
+ PROTECTED_SNAPSHOTTER_WRITE_BLOCKED: 53
67
68
  };
68
69
 
69
70
  // ../shared/src/json-output.ts
@@ -4398,7 +4399,7 @@ function formatKnownContext(input) {
4398
4399
  const selfAliases = collectSelfAliases(input.manifest, input.hostId);
4399
4400
  const outbound = collectOutboundAccess(input.manifest, input.hostId);
4400
4401
  const maintenanceLines = formatMaintenanceLines(host);
4401
- const guidance = input.hostId === "macos-dev" ? "use declared SSH aliases for remote work when needed; do not assume undeclared hosts have reciprocal SSH access." : `this session is already on \`${input.hostId}\`; do not SSH to self aliases unless the user explicitly asks. Do not assume outbound SSH to other fleet hosts is configured.`;
4402
+ const guidance = host.role === "snapshotter" && host.protected === true ? `this session is already on \`${input.hostId}\`; this is a protected snapshotter host. Do not mutate the vault or repo-local project workspaces from this session except explicitly approved snapshot maintenance. Use read-only investigation here and route authoring to a leaf host.` : input.hostId === "macos-dev" ? "use declared SSH aliases for remote work when needed; do not assume undeclared hosts have reciprocal SSH access." : `this session is already on \`${input.hostId}\`; do not SSH to self aliases unless the user explicitly asks. Do not assume outbound SSH to other fleet hosts is configured.`;
4402
4403
  return [
4403
4404
  "## Runtime Host Context",
4404
4405
  "",
@@ -8928,6 +8929,7 @@ export {
8928
8929
  FLEET_REL_PATH,
8929
8930
  runFleetValidate,
8930
8931
  runFleetContext,
8932
+ loadFleetManifestAndHost,
8931
8933
  loadFleetManifest,
8932
8934
  resolveFleetHostId,
8933
8935
  SATELLITE_STALE_MS,
package/dist/cli.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  isFailedRunStatus,
23
23
  isValidRemoteDeleteCap,
24
24
  loadFleetManifest,
25
+ loadFleetManifestAndHost,
25
26
  normalizeRemoteRoot,
26
27
  ok,
27
28
  parseDotenvFile,
@@ -74,7 +75,7 @@ import {
74
75
  triggerAutoUpdate,
75
76
  writeCache,
76
77
  writeDotenv
77
- } from "./chunk-KUDZOG52.js";
78
+ } from "./chunk-TK4ZIQNE.js";
78
79
  import {
79
80
  normalizeDistTag
80
81
  } from "./chunk-E6UWZ3S3.js";
@@ -4754,6 +4755,36 @@ async function postCommit(vault, exitCode) {
4754
4755
  clearLastOp(vault);
4755
4756
  }
4756
4757
 
4758
+ // src/utils/protected-vault-write-guard.ts
4759
+ async function guardProtectedVaultWrite(input) {
4760
+ const load = await loadFleetManifestAndHost({
4761
+ vault: input.vault,
4762
+ hostId: input.hostId,
4763
+ env: input.env ?? process.env,
4764
+ home: input.home ?? process.env.HOME ?? "",
4765
+ cwd: input.cwd ?? process.cwd(),
4766
+ osHostname: input.osHostname ?? process.env.HOSTNAME,
4767
+ user: input.user ?? process.env.USER
4768
+ });
4769
+ if (!load?.hostId || load.identityStatus !== "known") {
4770
+ return { blocked: false };
4771
+ }
4772
+ const host = load.manifest.hosts[load.hostId];
4773
+ if (!host || host.role !== "snapshotter" || host.protected !== true) {
4774
+ return { blocked: false };
4775
+ }
4776
+ return {
4777
+ blocked: true,
4778
+ exitCode: ExitCode.PROTECTED_SNAPSHOTTER_WRITE_BLOCKED,
4779
+ result: err("PROTECTED_SNAPSHOTTER_WRITE_BLOCKED", {
4780
+ host_id: load.hostId,
4781
+ command: input.command,
4782
+ reason: `refusing vault mutation from protected snapshotter host '${load.hostId}'`,
4783
+ guidance: "Use a leaf authoring host for vault/project writes. Keep protected snapshotter sessions read-only except explicit snapshot maintenance."
4784
+ })
4785
+ };
4786
+ }
4787
+
4757
4788
  // src/cli.ts
4758
4789
  var pkg = readCliPackageJson();
4759
4790
  var program = new Command();
@@ -4765,6 +4796,21 @@ async function emit(r, vault, opts) {
4765
4796
  if (vault && opts?.postCommit !== false) await postCommit(vault, r.exitCode);
4766
4797
  process.exit(r.exitCode);
4767
4798
  }
4799
+ async function emitGuardedVaultWrite(vault, command, run, opts) {
4800
+ const guard = await guardProtectedVaultWrite({
4801
+ vault,
4802
+ command,
4803
+ env: process.env,
4804
+ home: process.env.HOME ?? "",
4805
+ cwd: process.cwd(),
4806
+ osHostname: process.env.HOSTNAME,
4807
+ user: process.env.USER
4808
+ });
4809
+ if (guard.blocked) {
4810
+ return emit({ exitCode: guard.exitCode, result: guard.result }, void 0, { postCommit: false });
4811
+ }
4812
+ return emit(await run(), vault, opts);
4813
+ }
4768
4814
  program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
4769
4815
  program.command("fetch-guard <url>").description("check if a URL passes fetch guard rules and sanitize secrets").action(async (url) => emit(await runFetchGuard({ url })));
4770
4816
  program.command("validate <file>").description("validate vault page frontmatter against its detected schema").option("--apply", "auto-update vault index.md and log.md after successful validation", false).option("--vault <dir>", "vault root directory (required with --apply)").option("--wiki <name>", "wiki profile name").action(async (file, opts) => {
@@ -4774,17 +4820,20 @@ program.command("validate <file>").description("validate vault page frontmatter
4774
4820
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
4775
4821
  else vault = v.vault;
4776
4822
  }
4823
+ if (opts.apply && vault) {
4824
+ return emitGuardedVaultWrite(vault, "validate --apply", () => runValidate({ file, apply: true, vault }), void 0);
4825
+ }
4777
4826
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
4778
4827
  });
4779
4828
  program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4780
4829
  const out = opts.out ?? join25(vault, ".skillwiki", "graph.json");
4781
- emit(await runGraphBuild({ vault, out }), vault);
4830
+ return emitGuardedVaultWrite(vault, "graph build", () => runGraphBuild({ vault, out }));
4782
4831
  });
4783
4832
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
4784
4833
  canvasCmd.command("generate [vault]").description("generate .canvas from graph.json").option("--graph-path <path>", "explicit path to graph.json").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4785
4834
  const v = await resolveVaultArg(vault, opts.wiki);
4786
4835
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
4787
- else emit(await runCanvasGenerate({ vault: v.vault, graphPath: opts.graphPath }), v.vault);
4836
+ else return emitGuardedVaultWrite(v.vault, "canvas generate", () => runCanvasGenerate({ vault: v.vault, graphPath: opts.graphPath }));
4788
4837
  });
4789
4838
  program.command("overlap [vault]").description("detect typed-knowledge pages that share the same raw sources").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4790
4839
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -4886,12 +4935,21 @@ program.command("topic-map-check [vault]").description("check whether a topic ma
4886
4935
  program.command("stale [vault]").description("identify stale transcripts and incomplete work items").option("--archive", "move stale items to _archive/", false).option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 3).option("--force-scan", "infer kind/project from filename and content when frontmatter is missing", false).option("--project <slug>", "scope to a single project").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4887
4936
  const v = await resolveVaultArg(vault, opts.wiki);
4888
4937
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
4889
- else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive, forceScan: !!opts.forceScan, project: opts.project }), v.vault);
4938
+ else if (opts.archive) return emitGuardedVaultWrite(
4939
+ v.vault,
4940
+ "stale --archive",
4941
+ () => runStale({ vault: v.vault, days: opts.days, archive: true, forceScan: !!opts.forceScan, project: opts.project })
4942
+ );
4943
+ else emit(await runStale({ vault: v.vault, days: opts.days, archive: false, forceScan: !!opts.forceScan, project: opts.project }), v.vault);
4890
4944
  });
4891
4945
  program.command("claim <transcript> [vault]").description("claim an unclaimed transcript by creating a work item with source: link").option("--project <slug>", "project slug (overrides transcript frontmatter)").option("--slug <slug>", "work-item slug (defaults to transcript filename without date/kind prefix)").option("--wiki <name>", "wiki profile name").action(async (transcript, vault, opts) => {
4892
4946
  const v = await resolveVaultArg(vault, opts.wiki);
4893
4947
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
4894
- else emit(await runClaim({ vault: v.vault, transcript, project: opts.project, slug: opts.slug }), v.vault);
4948
+ else return emitGuardedVaultWrite(
4949
+ v.vault,
4950
+ "claim",
4951
+ () => runClaim({ vault: v.vault, transcript, project: opts.project, slug: opts.slug })
4952
+ );
4895
4953
  });
4896
4954
  program.command("pagesize [vault]").description("report page sizes and flag oversized pages").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4897
4955
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -4901,23 +4959,47 @@ program.command("pagesize [vault]").description("report page sizes and flag over
4901
4959
  program.command("log-rotate [vault]").description("rotate or trim the vault log file").option("--threshold <n>", "entry count threshold", (s) => parseInt(s, 10), 500).option("--apply", "actually rotate", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4902
4960
  const v = await resolveVaultArg(vault, opts.wiki);
4903
4961
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
4904
- else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }), v.vault);
4962
+ else if (opts.apply) return emitGuardedVaultWrite(
4963
+ v.vault,
4964
+ "log-rotate --apply",
4965
+ () => runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: true })
4966
+ );
4967
+ else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: false }), v.vault);
4905
4968
  });
4906
4969
  program.command("log-append [vault]").description("append a single entry to the vault log under a short advisory lock").requiredOption("--content <text>", "log entry text to append").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4907
4970
  const v = await resolveVaultArg(vault, opts.wiki);
4908
4971
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
4909
- else emit(await runLogAppend({ vault: v.vault, content: opts.content }), v.vault);
4972
+ else return emitGuardedVaultWrite(
4973
+ v.vault,
4974
+ "log-append",
4975
+ () => runLogAppend({ vault: v.vault, content: opts.content })
4976
+ );
4910
4977
  });
4911
4978
  program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix supported lint violations").option("--only <bucket>", "run only the specified lint bucket").option("--summary", "emit bounded bucket counts instead of full item arrays", false).option("--examples <n>", "example count per bucket in summary mode", (s) => parseInt(s, 10), 3).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4912
4979
  const v = await resolveVaultArg(vault, opts.wiki);
4913
4980
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
4981
+ else if (opts.fix) return emitGuardedVaultWrite(
4982
+ v.vault,
4983
+ "lint --fix",
4984
+ () => runLint({
4985
+ vault: v.vault,
4986
+ source: vault ? "flag" : void 0,
4987
+ days: opts.days,
4988
+ lines: opts.lines,
4989
+ logThreshold: opts.logThreshold,
4990
+ fix: true,
4991
+ only: opts.only,
4992
+ summary: !!opts.summary,
4993
+ examplesLimit: opts.summary ? opts.examples : void 0
4994
+ })
4995
+ );
4914
4996
  else if (opts.summary) emit(await runLint({
4915
4997
  vault: v.vault,
4916
4998
  source: vault ? "flag" : void 0,
4917
4999
  days: opts.days,
4918
5000
  lines: opts.lines,
4919
5001
  logThreshold: opts.logThreshold,
4920
- fix: opts.fix ?? false,
5002
+ fix: false,
4921
5003
  only: opts.only,
4922
5004
  summary: true,
4923
5005
  examplesLimit: opts.examples
@@ -4928,7 +5010,7 @@ program.command("lint [vault]").description("run all vault health checks").optio
4928
5010
  days: opts.days,
4929
5011
  lines: opts.lines,
4930
5012
  logThreshold: opts.logThreshold,
4931
- fix: opts.fix ?? false,
5013
+ fix: false,
4932
5014
  only: opts.only
4933
5015
  }), v.vault);
4934
5016
  });
@@ -4977,24 +5059,56 @@ program.command("status [vault]").description("output vault diagnostics").option
4977
5059
  program.command("archive <page> [vault]").description("archive a typed-knowledge or raw page").option("--wiki <name>", "wiki profile name").option("--cascade", "scan vault for references (wikilinks + sources arrays); preview by default", false).option("--apply", "with --cascade: mutate sources arrays and archive (without --apply, --cascade is preview-only)", false).option("--remote <remote>", "rclone remote root to prune the archived source path, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete the archived source path from the remote after local archive", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "1").action(async (page, vault, opts) => {
4978
5060
  const v = await resolveVaultArg(vault, opts.wiki);
4979
5061
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
4980
- else emit(await runArchive({
5062
+ else if (opts.cascade && !opts.apply) emit(await runArchive({
4981
5063
  vault: v.vault,
4982
5064
  page,
4983
- cascade: !!opts.cascade,
4984
- apply: !!opts.apply,
5065
+ cascade: true,
5066
+ apply: false,
4985
5067
  remote: opts.remote,
4986
5068
  remoteDelete: !!opts.remoteDelete,
4987
5069
  maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
4988
5070
  }), v.vault);
5071
+ else return emitGuardedVaultWrite(
5072
+ v.vault,
5073
+ "archive",
5074
+ () => runArchive({
5075
+ vault: v.vault,
5076
+ page,
5077
+ cascade: !!opts.cascade,
5078
+ apply: !!opts.apply,
5079
+ remote: opts.remote,
5080
+ remoteDelete: !!opts.remoteDelete,
5081
+ maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
5082
+ })
5083
+ );
4989
5084
  });
4990
5085
  program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--new <date>", "list raw files ingested on/after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4991
5086
  const v = await resolveVaultArg(vault, opts.wiki);
4992
5087
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
4993
- else emit(await runDrift({ vault: v.vault, apply: opts.apply, newSince: opts.new }), v.vault);
5088
+ else if (opts.apply) return emitGuardedVaultWrite(
5089
+ v.vault,
5090
+ "drift --apply",
5091
+ () => runDrift({ vault: v.vault, apply: true, newSince: opts.new })
5092
+ );
5093
+ else emit(await runDrift({ vault: v.vault, apply: false, newSince: opts.new }), v.vault);
4994
5094
  });
4995
5095
  program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--canonical-policy <policy>", "canonical policy: stable-path or scan-order", "stable-path").option("--manifest-out <path>", "write raw dedup delete manifest before applying").option("--manifest-in <path>", "read existing raw dedup delete manifest for remote pruning").option("--remote <remote>", "rclone remote root, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete manifest duplicate paths from the remote", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "50").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4996
5096
  const v = await resolveVaultArg(vault, opts.wiki);
4997
5097
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5098
+ else if (opts.apply || opts.manifestOut) return emitGuardedVaultWrite(
5099
+ v.vault,
5100
+ opts.apply ? "dedup --apply" : "dedup --manifest-out",
5101
+ () => runDedup({
5102
+ vault: v.vault,
5103
+ apply: opts.apply,
5104
+ canonicalPolicy: opts.canonicalPolicy,
5105
+ manifestOut: opts.manifestOut,
5106
+ manifestIn: opts.manifestIn,
5107
+ remote: opts.remote,
5108
+ remoteDelete: !!opts.remoteDelete,
5109
+ maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
5110
+ })
5111
+ );
4998
5112
  else emit(await runDedup({
4999
5113
  vault: v.vault,
5000
5114
  apply: opts.apply,
@@ -5009,12 +5123,22 @@ program.command("dedup [vault]").description("detect duplicate raw sources by sh
5009
5123
  program.command("migrate-citations [vault]").description("migrate ^[raw/...] markers to paragraph-end citations").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5010
5124
  const v = await resolveVaultArg(vault, opts.wiki);
5011
5125
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5012
- else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
5126
+ else if (opts.dryRun) emit(await runMigrateCitations({ vault: v.vault, dryRun: true }), v.vault);
5127
+ else return emitGuardedVaultWrite(
5128
+ v.vault,
5129
+ "migrate-citations",
5130
+ () => runMigrateCitations({ vault: v.vault, dryRun: false })
5131
+ );
5013
5132
  });
5014
5133
  program.command("frontmatter-fix [vault]").description("fix common frontmatter issues on typed-knowledge pages").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5015
5134
  const v = await resolveVaultArg(vault, opts.wiki);
5016
5135
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5017
- else emit(await runFrontmatterFix({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
5136
+ else if (opts.dryRun) emit(await runFrontmatterFix({ vault: v.vault, dryRun: true }), v.vault);
5137
+ else return emitGuardedVaultWrite(
5138
+ v.vault,
5139
+ "frontmatter-fix",
5140
+ () => runFrontmatterFix({ vault: v.vault, dryRun: false })
5141
+ );
5018
5142
  });
5019
5143
  program.command("update").description("update skillwiki CLI from npm dist-tag").option("--tag <tag>", "npm dist-tag", "latest").action(async (opts) => emit(await runUpdate({
5020
5144
  home: process.env.HOME ?? "",
@@ -5033,13 +5157,23 @@ program.command("transcripts [vault]").description("list transcript files in raw
5033
5157
  program.command("project-index <slug> [vault]").description("generate a knowledge index for a project workspace").option("--apply", "write knowledge.md to the project directory", false).option("--wiki <name>", "wiki profile name").action(async (slug, vault, opts) => {
5034
5158
  const v = await resolveVaultArg(vault, opts.wiki);
5035
5159
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5036
- else emit(await runProjectIndex({ vault: v.vault, slug, apply: !!opts.apply }), v.vault);
5160
+ else if (opts.apply) return emitGuardedVaultWrite(
5161
+ v.vault,
5162
+ "project-index --apply",
5163
+ () => runProjectIndex({ vault: v.vault, slug, apply: true })
5164
+ );
5165
+ else emit(await runProjectIndex({ vault: v.vault, slug, apply: false }), v.vault);
5037
5166
  });
5038
5167
  var compoundCmd = program.command("compound").description("manage project compound entries");
5039
5168
  compoundCmd.command("promote [vault]").description("promote retros with Generalize?: yes to compound entries").requiredOption("--project <slug>", "project slug (e.g., llm-wiki)").option("--dry-run", "preview promotions without writing files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5040
5169
  const v = await resolveVaultArg(vault, opts.wiki);
5041
5170
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5042
- else emit(await runCompound({ vault: v.vault, project: opts.project, dryRun: !!opts.dryRun }), v.vault);
5171
+ else if (opts.dryRun) emit(await runCompound({ vault: v.vault, project: opts.project, dryRun: true }), v.vault);
5172
+ else return emitGuardedVaultWrite(
5173
+ v.vault,
5174
+ "compound promote",
5175
+ () => runCompound({ vault: v.vault, project: opts.project, dryRun: false })
5176
+ );
5043
5177
  });
5044
5178
  compoundCmd.command("list [vault]").description("list compound entries for a project").requiredOption("--project <slug>", "project slug (e.g., llm-wiki)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5045
5179
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -5049,12 +5183,21 @@ compoundCmd.command("list [vault]").description("list compound entries for a pro
5049
5183
  compoundCmd.command("delete <entry> [vault]").description("delete a compound entry and regenerate knowledge index").requiredOption("--project <slug>", "project slug (e.g., llm-wiki)").option("--wiki <name>", "wiki profile name").action(async (entry, vault, opts) => {
5050
5184
  const v = await resolveVaultArg(vault, opts.wiki);
5051
5185
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5052
- else emit(await runCompoundDelete({ vault: v.vault, project: opts.project, entry }), v.vault);
5186
+ else return emitGuardedVaultWrite(
5187
+ v.vault,
5188
+ "compound delete",
5189
+ () => runCompoundDelete({ vault: v.vault, project: opts.project, entry })
5190
+ );
5053
5191
  });
5054
5192
  program.command("tag-sync [vault]").description("mirror frontmatter enum values to nested Obsidian tags").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5055
5193
  const v = await resolveVaultArg(vault, opts.wiki);
5056
5194
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5057
- else emit(await runTagSync({ vault: v.vault, dryRun: !!opts.dryRun }), v.vault);
5195
+ else if (opts.dryRun) emit(await runTagSync({ vault: v.vault, dryRun: true }), v.vault);
5196
+ else return emitGuardedVaultWrite(
5197
+ v.vault,
5198
+ "tag-sync",
5199
+ () => runTagSync({ vault: v.vault, dryRun: false })
5200
+ );
5058
5201
  });
5059
5202
  var syncCmd = program.command("sync").description("manage vault sync");
5060
5203
  syncCmd.command("status [vault]").description("check vault git sync status").option("--wiki <name>", "wiki profile name").option("--include-stashes", "enumerate all stashes in output", false).action(async (vault, opts) => {
@@ -5065,25 +5208,41 @@ syncCmd.command("status [vault]").description("check vault git sync status").opt
5065
5208
  syncCmd.command("push [vault]").description("lint, commit, and push vault changes to remote").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5066
5209
  const v = await resolveVaultArg(vault, opts.wiki);
5067
5210
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5068
- else emit(await runSyncPush({ vault: v.vault }));
5211
+ else return emitGuardedVaultWrite(
5212
+ v.vault,
5213
+ "sync push",
5214
+ () => runSyncPush({ vault: v.vault })
5215
+ );
5069
5216
  });
5070
5217
  syncCmd.command("pull [vault]").description("pull remote vault changes and lint").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5071
5218
  const v = await resolveVaultArg(vault, opts.wiki);
5072
5219
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5073
- else emit(await runSyncPull({ vault: v.vault }), v.vault);
5220
+ else return emitGuardedVaultWrite(
5221
+ v.vault,
5222
+ "sync pull",
5223
+ () => runSyncPull({ vault: v.vault })
5224
+ );
5074
5225
  });
5075
5226
  syncCmd.command("lock [vault]").description("acquire advisory lock on vault").option("--summary <text>", "lock description", "skillwiki sync").option("--ttl-minutes <n>", "lock time-to-live in minutes", "30").option("--force", "overwrite existing lock", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5076
5227
  const v = await resolveVaultArg(vault, opts.wiki);
5077
5228
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5078
5229
  else {
5079
5230
  const ttl = parseInt(opts.ttlMinutes, 10) || 30;
5080
- emit(runSyncLock({ vault: v.vault, summary: opts.summary, ttlMinutes: ttl, force: !!opts.force, sessionId: getCliSessionId() }));
5231
+ return emitGuardedVaultWrite(
5232
+ v.vault,
5233
+ "sync lock",
5234
+ async () => runSyncLock({ vault: v.vault, summary: opts.summary, ttlMinutes: ttl, force: !!opts.force, sessionId: getCliSessionId() })
5235
+ );
5081
5236
  }
5082
5237
  });
5083
5238
  syncCmd.command("unlock [vault]").description("release advisory lock on vault").option("--force", "release lock regardless of holder", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5084
5239
  const v = await resolveVaultArg(vault, opts.wiki);
5085
5240
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5086
- else emit(runSyncUnlock({ vault: v.vault, force: !!opts.force, sessionId: getCliSessionId() }));
5241
+ else return emitGuardedVaultWrite(
5242
+ v.vault,
5243
+ "sync unlock",
5244
+ async () => runSyncUnlock({ vault: v.vault, force: !!opts.force, sessionId: getCliSessionId() })
5245
+ );
5087
5246
  });
5088
5247
  syncCmd.command("peers [vault]").description("list active locks and recent wiki-sync stashes").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5089
5248
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -5118,41 +5277,65 @@ backupCmd.command("restore [vault]").description("restore vault from S3-compatib
5118
5277
  }
5119
5278
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
5120
5279
  const dotenv = await parseDotenvFile(configPath(home));
5121
- emit(await runBackupRestore({
5122
- vault: v.vault,
5123
- bucket: opts.bucket ?? dotenv["BACKUP_BUCKET"] ?? "",
5124
- endpoint: opts.endpoint ?? dotenv["BACKUP_ENDPOINT"] ?? "",
5125
- region: opts.region ?? dotenv["BACKUP_REGION"] ?? "us-east-1",
5126
- accessKeyId: dotenv["BACKUP_ACCESS_KEY_ID"] ?? "",
5127
- secretAccessKey: dotenv["BACKUP_SECRET_ACCESS_KEY"] ?? "",
5128
- target: opts.target
5129
- }), v.vault);
5280
+ return emitGuardedVaultWrite(
5281
+ v.vault,
5282
+ "backup restore",
5283
+ () => runBackupRestore({
5284
+ vault: v.vault,
5285
+ bucket: opts.bucket ?? dotenv["BACKUP_BUCKET"] ?? "",
5286
+ endpoint: opts.endpoint ?? dotenv["BACKUP_ENDPOINT"] ?? "",
5287
+ region: opts.region ?? dotenv["BACKUP_REGION"] ?? "us-east-1",
5288
+ accessKeyId: dotenv["BACKUP_ACCESS_KEY_ID"] ?? "",
5289
+ secretAccessKey: dotenv["BACKUP_SECRET_ACCESS_KEY"] ?? "",
5290
+ target: opts.target
5291
+ })
5292
+ );
5130
5293
  });
5131
5294
  program.command("seed [vault]").description("populate a vault with example content").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5132
5295
  const v = await resolveVaultArg(vault, opts.wiki);
5133
5296
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5134
- else emit(await runSeed({ vault: v.vault }), v.vault);
5297
+ else return emitGuardedVaultWrite(
5298
+ v.vault,
5299
+ "seed",
5300
+ () => runSeed({ vault: v.vault })
5301
+ );
5135
5302
  });
5136
5303
  program.command("observe [vault]").description("create a raw transcript observation entry").requiredOption("--text <text>", "observation text").option("--kind <kind>", "observation kind (note|bug|task|idea|session-log)", "task").option("--project <slug>", "associated project slug (required for task/bug claim detection)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5137
5304
  const v = await resolveVaultArg(vault, opts.wiki);
5138
5305
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5139
- else emit(await runObserve({
5140
- vault: v.vault,
5141
- text: opts.text,
5142
- kind: opts.kind,
5143
- project: opts.project
5144
- }), v.vault);
5306
+ else return emitGuardedVaultWrite(
5307
+ v.vault,
5308
+ "observe",
5309
+ () => runObserve({
5310
+ vault: v.vault,
5311
+ text: opts.text,
5312
+ kind: opts.kind,
5313
+ project: opts.project
5314
+ })
5315
+ );
5145
5316
  });
5146
5317
  program.command("session-brief [vault]").description("render or refresh the bounded startup session brief").option("--project <slug>", "project slug, or auto for deterministic detection", "auto").option("--write", "write meta/latest-session-brief.md and local cache files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5147
5318
  const v = await resolveVaultArg(vault, opts.wiki);
5148
5319
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5320
+ else if (opts.write) return emitGuardedVaultWrite(
5321
+ v.vault,
5322
+ "session-brief --write",
5323
+ () => runSessionBrief({
5324
+ vault: v.vault,
5325
+ project: opts.project,
5326
+ write: true,
5327
+ cwd: process.cwd(),
5328
+ env: { SKILLWIKI_PROJECT: process.env.SKILLWIKI_PROJECT }
5329
+ }),
5330
+ { postCommit: true }
5331
+ );
5149
5332
  else emit(await runSessionBrief({
5150
5333
  vault: v.vault,
5151
5334
  project: opts.project,
5152
- write: !!opts.write,
5335
+ write: false,
5153
5336
  cwd: process.cwd(),
5154
5337
  env: { SKILLWIKI_PROJECT: process.env.SKILLWIKI_PROJECT }
5155
- }), v.vault, { postCommit: !!opts.write });
5338
+ }), v.vault, { postCommit: false });
5156
5339
  });
5157
5340
  var memoryCmd = program.command("memory").description("inspect derived agent memory caches");
5158
5341
  memoryCmd.command("topics [vault]").description("list topic-oriented memory from the optional derived cache").option("--project <slug>", "filter topics by project slug").option("--limit <n>", "maximum topics to return", (s) => parseInt(s, 10), 10).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
@@ -5167,12 +5350,23 @@ memoryCmd.command("topics [vault]").description("list topic-oriented memory from
5167
5350
  memoryCmd.command("index [vault]").description("build the derived project memory topic cache").requiredOption("--project <slug>", "project slug").option("--check", "report whether the local project memory cache is missing or stale without writing", false).option("--if-stale", "rebuild the local project memory cache only when it is missing or stale", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5168
5351
  const v = await resolveVaultArg(vault, opts.wiki);
5169
5352
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5170
- else emit(await runMemoryIndex({
5353
+ else if (opts.check) emit(await runMemoryIndex({
5171
5354
  vault: v.vault,
5172
5355
  project: opts.project,
5173
- check: !!opts.check,
5356
+ check: true,
5174
5357
  ifStale: !!opts.ifStale
5175
- }), v.vault, { postCommit: !opts.check });
5358
+ }), v.vault, { postCommit: false });
5359
+ else return emitGuardedVaultWrite(
5360
+ v.vault,
5361
+ opts.ifStale ? "memory index --if-stale" : "memory index",
5362
+ () => runMemoryIndex({
5363
+ vault: v.vault,
5364
+ project: opts.project,
5365
+ check: false,
5366
+ ifStale: !!opts.ifStale
5367
+ }),
5368
+ { postCommit: true }
5369
+ );
5176
5370
  });
5177
5371
  memoryCmd.command("recall [vault]").description("recall bounded source summaries for a memory topic").requiredOption("--project <slug>", "project slug").requiredOption("--topic <slug>", "memory topic slug").option("--scope <scope>", "memory scope: project, global, cross-agent, or all").option("--limit <n>", "maximum sources to return", (s) => parseInt(s, 10), 10).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5178
5372
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -5198,25 +5392,53 @@ memoryCmd.command("review [vault]").description("review deterministic memory gap
5198
5392
  memoryCmd.command("import [vault]").description("preview or apply local memory imports into raw captures").requiredOption("--from <path>", "source file or directory to scan").requiredOption("--project <slug>", "project slug").option("--dry-run", "preview only; do not write captures", false).option("--apply", "write validated raw captures for ready entries", false).option("--max-bytes <n>", "reject source files above this size", (s) => parseInt(s, 10), 2e5).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
5199
5393
  const v = await resolveVaultArg(vault, opts.wiki);
5200
5394
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
5395
+ else if (!!opts.apply && !opts.dryRun) return emitGuardedVaultWrite(
5396
+ v.vault,
5397
+ "memory import --apply",
5398
+ () => runMemoryImport({
5399
+ vault: v.vault,
5400
+ from: opts.from,
5401
+ project: opts.project,
5402
+ apply: true,
5403
+ maxBytes: opts.maxBytes
5404
+ }),
5405
+ { postCommit: true }
5406
+ );
5201
5407
  else emit(await runMemoryImport({
5202
5408
  vault: v.vault,
5203
5409
  from: opts.from,
5204
5410
  project: opts.project,
5205
- apply: !!opts.apply && !opts.dryRun,
5411
+ apply: false,
5206
5412
  maxBytes: opts.maxBytes
5207
- }), v.vault, { postCommit: !!opts.apply && !opts.dryRun });
5413
+ }), v.vault, { postCommit: false });
5208
5414
  });
5209
5415
  program.command("ingest <source>").description("ingest a source URL or local file into the vault").requiredOption("--vault <path>", "vault root directory").requiredOption("--type <type>", "typed-knowledge type (entity|concept|comparison|query)").requiredOption("--title <title>", "page title").option("--tags <csv>", "comma-separated tags").option("--provenance <provenance>", "provenance (research|project)").option("--dry-run", "preview without writing files", false).action(async (source, opts) => {
5210
5416
  const tags = typeof opts.tags === "string" ? opts.tags.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : [];
5211
- emit(await runIngest({
5212
- source,
5213
- vault: opts.vault,
5214
- type: opts.type,
5215
- title: opts.title,
5216
- tags,
5217
- provenance: opts.provenance,
5218
- dryRun: !!opts.dryRun
5219
- }), opts.vault);
5417
+ if (opts.dryRun) {
5418
+ emit(await runIngest({
5419
+ source,
5420
+ vault: opts.vault,
5421
+ type: opts.type,
5422
+ title: opts.title,
5423
+ tags,
5424
+ provenance: opts.provenance,
5425
+ dryRun: true
5426
+ }), opts.vault);
5427
+ return;
5428
+ }
5429
+ return emitGuardedVaultWrite(
5430
+ opts.vault,
5431
+ "ingest",
5432
+ () => runIngest({
5433
+ source,
5434
+ vault: opts.vault,
5435
+ type: opts.type,
5436
+ title: opts.title,
5437
+ tags,
5438
+ provenance: opts.provenance,
5439
+ dryRun: false
5440
+ })
5441
+ );
5220
5442
  });
5221
5443
  var fleetCmd = program.command("fleet").description("manage fleet topology metadata");
5222
5444
  fleetCmd.command("validate <file>").description("validate a fleet manifest").action(async (file) => {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-KUDZOG52.js";
4
+ } from "./chunk-TK4ZIQNE.js";
5
5
  import "./chunk-E6UWZ3S3.js";
6
6
 
7
7
  // src/mcp-entry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.34",
3
+ "version": "0.9.35",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.34",
3
+ "version": "0.9.35",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.34",
3
+ "version": "0.9.35",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 18 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.9.34",
3
+ "version": "0.9.35",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",