skillwiki 0.8.5-beta.3 → 0.8.5-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -14,8 +14,7 @@ import {
14
14
  } from "./chunk-TPS5XD2J.js";
15
15
 
16
16
  // src/cli.ts
17
- import { readFileSync as readFileSync12 } from "fs";
18
- import { join as join44 } from "path";
17
+ import { join as join45 } from "path";
19
18
  import { Command as Command2 } from "commander";
20
19
 
21
20
  // ../shared/src/exit-codes.ts
@@ -3048,8 +3047,9 @@ function buildCliSurface() {
3048
3047
  program2.command("pagesize").option("--lines <n>").option("--wiki <name>");
3049
3048
  program2.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
3050
3049
  program2.command("log-append").requiredOption("--content <text>").option("--wiki <name>");
3051
- program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--wiki <name>");
3050
+ program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--summary").option("--examples <n>").option("--wiki <name>");
3052
3051
  program2.command("config");
3052
+ program2.command("health").option("--wiki <name>").option("--sync <mode>").option("--no-fail").option("--out <path>").option("--examples <n>");
3053
3053
  program2.command("doctor");
3054
3054
  program2.command("status").option("--wiki <name>");
3055
3055
  program2.command("archive").option("--wiki <name>").option("--cascade").option("--apply");
@@ -3272,6 +3272,58 @@ var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_source_identi
3272
3272
  var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "file_source_url", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
3273
3273
  var INFO_ORDER = ["bridges", "sparse_community", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
3274
3274
  var KNOWN_BUCKETS = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
3275
+ function shellQuote(value) {
3276
+ return `'${value.replace(/'/g, `'\\''`)}'`;
3277
+ }
3278
+ function formatExample(item) {
3279
+ if (typeof item === "string") return item;
3280
+ if (item === null || item === void 0) return String(item);
3281
+ try {
3282
+ return JSON.stringify(item);
3283
+ } catch {
3284
+ return String(item);
3285
+ }
3286
+ }
3287
+ function summarizeBucket(bucket, severity, vaultPath, examplesLimit) {
3288
+ const safeLimit = Math.max(0, Math.min(examplesLimit, 10));
3289
+ const examples = bucket.items.slice(0, safeLimit).map(formatExample);
3290
+ return {
3291
+ kind: bucket.kind,
3292
+ severity,
3293
+ count: bucket.items.length,
3294
+ examples,
3295
+ examples_limit: safeLimit,
3296
+ sample_truncated: bucket.items.length > examples.length,
3297
+ details_command: `skillwiki lint ${shellQuote(vaultPath)} --only ${bucket.kind}`
3298
+ };
3299
+ }
3300
+ function summarizeLintOutput(output, examplesLimit = 3) {
3301
+ const buckets = [
3302
+ ...output.by_severity.error.map((bucket) => summarizeBucket(bucket, "error", output.vault.path, examplesLimit)),
3303
+ ...output.by_severity.warning.map((bucket) => summarizeBucket(bucket, "warning", output.vault.path, examplesLimit)),
3304
+ ...output.by_severity.info.map((bucket) => summarizeBucket(bucket, "info", output.vault.path, examplesLimit))
3305
+ ];
3306
+ const lines = [];
3307
+ lines.push(`errors: ${output.summary.errors}`);
3308
+ lines.push(`warnings: ${output.summary.warnings}`);
3309
+ lines.push(`info: ${output.summary.info}`);
3310
+ for (const bucket of buckets) {
3311
+ lines.push(` ${bucket.kind}: ${bucket.count}`);
3312
+ if (bucket.examples.length > 0) {
3313
+ lines.push(` e.g. ${bucket.examples[0]}`);
3314
+ }
3315
+ }
3316
+ return {
3317
+ vault: output.vault,
3318
+ summary: output.summary,
3319
+ buckets,
3320
+ details_included: false,
3321
+ truncated: false,
3322
+ fixed: output.fixed,
3323
+ unresolved: output.unresolved,
3324
+ humanHint: lines.join("\n")
3325
+ };
3326
+ }
3275
3327
  async function runLint(input) {
3276
3328
  if (input.only && !KNOWN_BUCKETS.includes(input.only)) {
3277
3329
  return {
@@ -3904,17 +3956,18 @@ ${newBody}`;
3904
3956
  let fExit = ExitCode.OK;
3905
3957
  if (fSummary.errors > 0) fExit = ExitCode.LINT_HAS_ERRORS;
3906
3958
  else if (fSummary.warnings > 0 || fSummary.info > 0) fExit = ExitCode.LINT_HAS_WARNINGS;
3959
+ const output2 = {
3960
+ vault: { path: input.vault, source: input.source ?? "resolved" },
3961
+ summary: fSummary,
3962
+ by_severity: filtered,
3963
+ fixed,
3964
+ unresolved,
3965
+ humanHint: `--only ${input.only}
3966
+ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")}`
3967
+ };
3907
3968
  return {
3908
3969
  exitCode: fExit,
3909
- result: ok({
3910
- vault: { path: input.vault, source: input.source ?? "resolved" },
3911
- summary: fSummary,
3912
- by_severity: filtered,
3913
- fixed,
3914
- unresolved,
3915
- humanHint: `--only ${input.only}
3916
- ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")}`
3917
- })
3970
+ result: ok(input.summary ? summarizeLintOutput(output2, input.examplesLimit) : output2)
3918
3971
  };
3919
3972
  }
3920
3973
  const summary = {
@@ -3942,19 +3995,31 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
3942
3995
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3943
3996
  });
3944
3997
  }
3998
+ const output = {
3999
+ vault: { path: input.vault, source: input.source ?? "resolved" },
4000
+ summary,
4001
+ by_severity: { error: errorOut, warning: warningOut, info: infoOut },
4002
+ fixed,
4003
+ unresolved,
4004
+ humanHint: hintLines.join("\n")
4005
+ };
3945
4006
  return {
3946
4007
  exitCode,
3947
- result: ok({
3948
- vault: { path: input.vault, source: input.source ?? "resolved" },
3949
- summary,
3950
- by_severity: { error: errorOut, warning: warningOut, info: infoOut },
3951
- fixed,
3952
- unresolved,
3953
- humanHint: hintLines.join("\n")
3954
- })
4008
+ result: ok(input.summary ? summarizeLintOutput(output, input.examplesLimit) : output)
3955
4009
  };
3956
4010
  }
3957
4011
 
4012
+ // src/commands/health.ts
4013
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync, writeFileSync as writeFileSync6 } from "fs";
4014
+ import { dirname as dirname10, join as join28, resolve as resolve6 } from "path";
4015
+ import { platform as platform3 } from "os";
4016
+
4017
+ // src/commands/doctor.ts
4018
+ import { existsSync as existsSync10, lstatSync, readlinkSync, readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
4019
+ import { join as join27, resolve as resolve5 } from "path";
4020
+ import { execSync as execSync2 } from "child_process";
4021
+ import { platform as platform2 } from "os";
4022
+
3958
4023
  // src/commands/config.ts
3959
4024
  import { readFile as readFile18 } from "fs/promises";
3960
4025
  import { existsSync as existsSync6 } from "fs";
@@ -4016,12 +4081,6 @@ async function runConfigPath(input) {
4016
4081
  return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync6(filePath), humanHint: filePath }) };
4017
4082
  }
4018
4083
 
4019
- // src/commands/doctor.ts
4020
- import { existsSync as existsSync10, lstatSync, readlinkSync, readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
4021
- import { join as join27, resolve as resolve5 } from "path";
4022
- import { execSync as execSync2 } from "child_process";
4023
- import { platform as platform2 } from "os";
4024
-
4025
4084
  // src/utils/auto-update.ts
4026
4085
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
4027
4086
  import { join as join24, dirname as dirname9 } from "path";
@@ -5624,9 +5683,383 @@ async function runDoctor(input) {
5624
5683
  return { exitCode, result: ok({ checks, summary, humanHint }) };
5625
5684
  }
5626
5685
 
5686
+ // src/commands/health.ts
5687
+ function statusFromCounts(counts) {
5688
+ if ((counts.error ?? 0) > 0) return "error";
5689
+ if ((counts.warn ?? counts.warnings ?? 0) > 0) return "warn";
5690
+ if ((counts.info ?? 0) > 0) return "info";
5691
+ return "pass";
5692
+ }
5693
+ function maxStatus(statuses) {
5694
+ const order = { pass: 0, info: 1, warn: 2, error: 3, unknown: 4 };
5695
+ return statuses.reduce((max, status) => order[status] > order[max] ? status : max, "pass");
5696
+ }
5697
+ function bucketCount(buckets, kind) {
5698
+ return buckets.find((bucket) => bucket.kind === kind)?.count ?? 0;
5699
+ }
5700
+ function bucketEvidence(prefix, buckets, kinds) {
5701
+ return kinds.map((kind) => ({ kind, count: bucketCount(buckets, kind) })).filter((item) => item.count > 0).map((item) => `${prefix}.${item.kind}: ${item.count}`);
5702
+ }
5703
+ function shellQuote2(value) {
5704
+ return `'${value.replace(/'/g, `'\\''`)}'`;
5705
+ }
5706
+ function lintCommand(vaultPath, args) {
5707
+ return `skillwiki lint ${shellQuote2(vaultPath)} ${args}`;
5708
+ }
5709
+ function deriveRiskFlags(lint, vaultSync, syncMode) {
5710
+ const flags = [];
5711
+ const integrityEvidence = bucketEvidence("lint", lint.buckets, [
5712
+ "broken_sources",
5713
+ "invalid_frontmatter",
5714
+ "broken_wikilinks",
5715
+ "raw_source_identity_conflict"
5716
+ ]);
5717
+ if (integrityEvidence.length > 0) {
5718
+ flags.push({
5719
+ id: "content_integrity_risk",
5720
+ status: "error",
5721
+ blocking: true,
5722
+ evidence: integrityEvidence,
5723
+ suggested_commands: [
5724
+ lintCommand(lint.vault.path, "--summary"),
5725
+ lintCommand(lint.vault.path, "--only broken_sources")
5726
+ ]
5727
+ });
5728
+ }
5729
+ const retrievalEvidence = bucketEvidence("lint", lint.buckets, [
5730
+ "tag_not_in_taxonomy",
5731
+ "orphans",
5732
+ "orphaned_project_pages",
5733
+ "missing_overview"
5734
+ ]);
5735
+ if (retrievalEvidence.length > 0) {
5736
+ const hasError = bucketCount(lint.buckets, "tag_not_in_taxonomy") > 0;
5737
+ flags.push({
5738
+ id: "retrieval_quality_risk",
5739
+ status: hasError ? "error" : "warn",
5740
+ blocking: hasError,
5741
+ evidence: retrievalEvidence,
5742
+ suggested_commands: [lintCommand(lint.vault.path, "--summary")]
5743
+ });
5744
+ }
5745
+ const duplicationEvidence = bucketEvidence("lint", lint.buckets, [
5746
+ "raw_dedup",
5747
+ "raw_body_duplicate",
5748
+ "raw_subdirectory_duplicate"
5749
+ ]);
5750
+ if (duplicationEvidence.length > 0) {
5751
+ flags.push({
5752
+ id: "content_duplication_risk",
5753
+ status: "warn",
5754
+ blocking: false,
5755
+ evidence: duplicationEvidence,
5756
+ suggested_commands: [lintCommand(lint.vault.path, "--only raw_dedup")]
5757
+ });
5758
+ }
5759
+ const syncEvidence = vaultSync.checks.filter((check2) => check2.status === "error" || check2.status === "warn").map((check2) => `vault_sync.${check2.id}: ${check2.status}`);
5760
+ if (syncEvidence.length > 0) {
5761
+ flags.push({
5762
+ id: "sync_visibility_risk",
5763
+ status: vaultSync.status === "error" ? "error" : "warn",
5764
+ blocking: syncMode === "required",
5765
+ evidence: syncEvidence,
5766
+ suggested_commands: ["vault-sync-install"]
5767
+ });
5768
+ }
5769
+ const backlogEvidence = bucketEvidence("lint", lint.buckets, [
5770
+ "stale_page",
5771
+ "page_too_large",
5772
+ "work_item_health",
5773
+ "log_rotate_needed"
5774
+ ]);
5775
+ if (backlogEvidence.length > 0) {
5776
+ flags.push({
5777
+ id: "maintenance_backlog",
5778
+ status: "warn",
5779
+ blocking: false,
5780
+ evidence: backlogEvidence,
5781
+ suggested_commands: [lintCommand(lint.vault.path, "--summary")]
5782
+ });
5783
+ }
5784
+ return flags;
5785
+ }
5786
+ function summarizeChecks(checks) {
5787
+ return {
5788
+ pass: checks.filter((check2) => check2.status === "pass").length,
5789
+ info: checks.filter((check2) => check2.status === "info").length,
5790
+ warn: checks.filter((check2) => check2.status === "warn").length,
5791
+ error: checks.filter((check2) => check2.status === "error").length,
5792
+ skipped: checks.filter((check2) => /skipped/i.test(check2.detail)).length
5793
+ };
5794
+ }
5795
+ function classifyLog(path, id, label, okPattern) {
5796
+ if (!existsSync11(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
5797
+ const lines = readFileSync8(path, "utf8").split(/\r?\n/).filter(Boolean);
5798
+ const last = lines[lines.length - 1] ?? "";
5799
+ if (!last) return { id, label, status: "warn", detail: `log file empty: ${path}` };
5800
+ if (/fail|error/i.test(last)) return { id, label, status: "error", detail: last.slice(0, 120) };
5801
+ if (okPattern.test(last)) return { id, label, status: "pass", detail: last.slice(0, 120) };
5802
+ return { id, label, status: "warn", detail: last.slice(0, 120) };
5803
+ }
5804
+ function runVaultSyncHealth(home, syncMode) {
5805
+ if (syncMode === "off") {
5806
+ return {
5807
+ status: "pass",
5808
+ blocking: false,
5809
+ installed: false,
5810
+ summary: { pass: 0, info: 0, warn: 0, error: 0, skipped: 1 },
5811
+ checks: [{ id: "vault_sync_skipped", status: "pass", detail: "vault-sync checks skipped by --sync off" }]
5812
+ };
5813
+ }
5814
+ const isMac = platform3() === "darwin";
5815
+ const shareDir = isMac ? join28(home, "Library", "Application Support", "vault-sync", "bin") : join28(home, ".local", "share", "vault-sync", "bin");
5816
+ const logDir = isMac ? join28(home, "Library", "Logs") : join28(home, ".local", "state", "vault-sync", "log");
5817
+ const filterPath = join28(home, ".config", "rclone", "wiki-push-filters.txt");
5818
+ const checks = [];
5819
+ const pushScript = join28(shareDir, "wiki-push.sh");
5820
+ checks.push(existsSync11(pushScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found: ${pushScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Script missing: ${pushScript}` });
5821
+ if (isMac) {
5822
+ const pushPlist = join28(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
5823
+ const fetchPlist = join28(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
5824
+ checks.push(existsSync11(pushPlist) && existsSync11(fetchPlist) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "launchd unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "launchd unit files missing (read-only mode)" });
5825
+ checks.push({ id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "macOS host \u2014 check skipped" });
5826
+ } else {
5827
+ const pushTimer = join28(home, ".config", "systemd", "user", "wiki-push.timer");
5828
+ const fetchTimer = join28(home, ".config", "systemd", "user", "wiki-fetch.timer");
5829
+ const fuseTimer = join28(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
5830
+ const fuseService = join28(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
5831
+ checks.push(existsSync11(pushTimer) && existsSync11(fetchTimer) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "systemd timer unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "systemd timer unit files missing (read-only mode)" });
5832
+ checks.push(existsSync11(fuseTimer) && existsSync11(fuseService) ? { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "wiki-fuse-refresh unit files present (read-only mode)" } : { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "warn", detail: "wiki-fuse-refresh unit files missing (read-only mode)" });
5833
+ }
5834
+ checks.push(classifyLog(join28(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
5835
+ checks.push(classifyLog(join28(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
5836
+ if (!existsSync11(filterPath)) {
5837
+ checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
5838
+ } else {
5839
+ const content = readFileSync8(filterPath, "utf8");
5840
+ const missing = ["remotely-save/data.json", ".skillwiki/sync.lock", ".claude/settings.local.json"].filter((item) => !content.includes(item));
5841
+ checks.push(missing.length > 0 ? { id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "warn", detail: `Missing excludes: ${missing.join(", ")}` } : { id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "pass", detail: "Required excludes present" });
5842
+ }
5843
+ checks.push({ id: "vault_sync_snapshot_guard", label: "Snapshot script guard", status: "pass", detail: "Not a snapshotter host \u2014 check skipped" });
5844
+ const summary = summarizeChecks(checks);
5845
+ return {
5846
+ status: statusFromCounts(summary),
5847
+ blocking: syncMode === "required",
5848
+ installed: checks.find((check2) => check2.id === "vault_sync_installed")?.status === "pass",
5849
+ summary,
5850
+ checks: checks.map((check2) => ({ id: check2.id, status: check2.status, detail: check2.detail }))
5851
+ };
5852
+ }
5853
+ function deriveQueryReadiness(lint) {
5854
+ const taxonomyErrors = bucketCount(lint.buckets, "tag_not_in_taxonomy");
5855
+ const brokenLinks = bucketCount(lint.buckets, "broken_wikilinks") + bucketCount(lint.buckets, "broken_sources");
5856
+ const orphanCount = bucketCount(lint.buckets, "orphans") + bucketCount(lint.buckets, "orphaned_project_pages");
5857
+ const missingOverview = bucketCount(lint.buckets, "missing_overview");
5858
+ const missingTldr = bucketCount(lint.buckets, "missing_tldr");
5859
+ let score = 100;
5860
+ score -= Math.min(40, taxonomyErrors * 2);
5861
+ score -= Math.min(30, brokenLinks * 10);
5862
+ score -= Math.min(20, orphanCount * 3);
5863
+ score -= Math.min(15, missingOverview);
5864
+ score -= Math.min(5, missingTldr);
5865
+ score = Math.max(0, score);
5866
+ const status = taxonomyErrors > 0 || brokenLinks > 0 ? "error" : score < 80 ? "warn" : "pass";
5867
+ return {
5868
+ status,
5869
+ blocking: false,
5870
+ summary: { score },
5871
+ signals: [
5872
+ { id: "taxonomy_errors", status: taxonomyErrors > 0 ? "error" : "pass", value: taxonomyErrors },
5873
+ { id: "broken_links", status: brokenLinks > 0 ? "error" : "pass", value: brokenLinks },
5874
+ { id: "orphan_count", status: orphanCount > 0 ? "warn" : "pass", value: orphanCount },
5875
+ { id: "missing_overview", status: missingOverview > 0 ? "warn" : "pass", value: missingOverview },
5876
+ { id: "missing_tldr", status: missingTldr > 0 ? "info" : "pass", value: missingTldr }
5877
+ ]
5878
+ };
5879
+ }
5880
+ function selfCheckReport(report) {
5881
+ const errors = [];
5882
+ const lint = report.components.lint;
5883
+ const sumBySeverity = (severity) => lint.buckets.filter((bucket) => bucket.severity === severity).reduce((sum, bucket) => sum + bucket.count, 0);
5884
+ if (lint.summary.errors !== sumBySeverity("error")) {
5885
+ errors.push(`lint.summary.errors=${lint.summary.errors} but error bucket counts sum to ${sumBySeverity("error")}`);
5886
+ }
5887
+ if (lint.summary.warnings !== sumBySeverity("warning")) {
5888
+ errors.push(`lint.summary.warnings=${lint.summary.warnings} but warning bucket counts sum to ${sumBySeverity("warning")}`);
5889
+ }
5890
+ if (lint.summary.info !== sumBySeverity("info")) {
5891
+ errors.push(`lint.summary.info=${lint.summary.info} but info bucket counts sum to ${sumBySeverity("info")}`);
5892
+ }
5893
+ for (const component of ["doctor", "lint", "vault_sync", "query_readiness", "source_freshness"]) {
5894
+ if (!report.coverage[component]) errors.push(`missing coverage entry for ${component}`);
5895
+ }
5896
+ for (const flag of report.risk_flags) {
5897
+ if (flag.evidence.length === 0) errors.push(`risk flag ${flag.id} has no evidence`);
5898
+ }
5899
+ return { status: errors.length > 0 ? "error" : "pass", errors };
5900
+ }
5901
+ function buildHumanHint(report) {
5902
+ const lines = [
5903
+ `vault: ${report.vault.path} (via ${report.vault.source})`,
5904
+ `overall: ${report.overall_status}`,
5905
+ `doctor: ${report.components.doctor.summary.pass} pass, ${report.components.doctor.summary.info} info, ${report.components.doctor.summary.warn} warn, ${report.components.doctor.summary.error} error`,
5906
+ `lint: ${report.components.lint.summary.errors} errors, ${report.components.lint.summary.warnings} warnings, ${report.components.lint.summary.info} info`
5907
+ ];
5908
+ for (const bucket of report.components.lint.buckets) {
5909
+ lines.push(` ${bucket.severity} ${bucket.kind}: ${bucket.count}`);
5910
+ }
5911
+ lines.push(`vault_sync: ${report.components.vault_sync.summary.pass} pass, ${report.components.vault_sync.summary.warn} warn, ${report.components.vault_sync.summary.error} error`);
5912
+ lines.push(`query_readiness: ${report.components.query_readiness.status} (${report.components.query_readiness.summary.score}/100)`);
5913
+ if (report.risk_flags.length > 0) {
5914
+ lines.push("risk flags:");
5915
+ for (const flag of report.risk_flags) {
5916
+ lines.push(` ${flag.id}: ${flag.status}${flag.blocking ? " (blocking)" : ""}`);
5917
+ }
5918
+ }
5919
+ if (report.self_check.status === "error") {
5920
+ lines.push("self-check: error");
5921
+ for (const error of report.self_check.errors) lines.push(` ${error}`);
5922
+ }
5923
+ return lines.join("\n");
5924
+ }
5925
+ function writeReport(out, report) {
5926
+ mkdirSync4(dirname10(out), { recursive: true });
5927
+ const tmp = `${out}.tmp-${process.pid}-${Date.now()}`;
5928
+ writeFileSync6(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
5929
+ renameSync(tmp, out);
5930
+ }
5931
+ async function runHealth(input) {
5932
+ const syncMode = input.sync ?? "optional";
5933
+ const doctor = await runDoctor({
5934
+ home: input.home,
5935
+ envValue: input.envValue,
5936
+ argv: input.argv,
5937
+ currentVersion: input.currentVersion,
5938
+ cwd: input.cwd
5939
+ });
5940
+ const lint = await runLint({
5941
+ vault: input.vault,
5942
+ source: input.vaultSource ?? "resolved",
5943
+ days: 90,
5944
+ lines: 200,
5945
+ logThreshold: 500,
5946
+ summary: true,
5947
+ examplesLimit: input.examplesLimit ?? 3
5948
+ });
5949
+ if (!doctor.result.ok) return { exitCode: doctor.exitCode, result: doctor.result };
5950
+ if (!lint.result.ok) return { exitCode: lint.exitCode, result: lint.result };
5951
+ const doctorStatus = statusFromCounts(doctor.result.data.summary);
5952
+ const vaultSync = runVaultSyncHealth(input.home, syncMode);
5953
+ const queryReadiness = deriveQueryReadiness(lint.result.data);
5954
+ const sourceFreshness = {
5955
+ status: bucketCount(lint.result.data.buckets, "stale_page") > 0 || bucketCount(lint.result.data.buckets, "file_source_url") > 0 ? "warn" : "pass",
5956
+ blocking: false,
5957
+ summary: {
5958
+ stale_pages: bucketCount(lint.result.data.buckets, "stale_page"),
5959
+ file_source_url: bucketCount(lint.result.data.buckets, "file_source_url"),
5960
+ stale_sections: bucketCount(lint.result.data.buckets, "stale_sections")
5961
+ }
5962
+ };
5963
+ const doctorComponent = {
5964
+ status: doctorStatus,
5965
+ blocking: false,
5966
+ summary: doctor.result.data.summary,
5967
+ checks: doctor.result.data.checks.map((check2) => ({ id: check2.id, status: check2.status, detail: check2.detail }))
5968
+ };
5969
+ const lintComponent = {
5970
+ status: statusFromCounts({ error: lint.result.data.summary.errors, warnings: lint.result.data.summary.warnings, info: lint.result.data.summary.info }),
5971
+ blocking: true,
5972
+ summary: lint.result.data.summary,
5973
+ buckets: lint.result.data.buckets,
5974
+ details_included: false,
5975
+ truncated: false
5976
+ };
5977
+ const riskFlags = deriveRiskFlags(lint.result.data, vaultSync, syncMode);
5978
+ const blockingStatuses = [
5979
+ lintComponent.blocking ? lintComponent.status : "pass",
5980
+ vaultSync.blocking ? vaultSync.status : "pass",
5981
+ ...riskFlags.filter((flag) => flag.blocking).map((flag) => flag.status)
5982
+ ];
5983
+ const advisoryStatuses = [
5984
+ doctorComponent.status,
5985
+ vaultSync.status,
5986
+ queryReadiness.status,
5987
+ sourceFreshness.status,
5988
+ ...riskFlags.filter((flag) => !flag.blocking).map((flag) => flag.status)
5989
+ ];
5990
+ const blockingStatus = maxStatus(blockingStatuses);
5991
+ const advisoryStatus = maxStatus(advisoryStatuses);
5992
+ const baseReport = {
5993
+ schema_version: 1,
5994
+ policy_version: 1,
5995
+ tool: { name: "skillwiki", version: input.currentVersion },
5996
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
5997
+ command_kind: "diagnostic",
5998
+ vault: { path: input.vault, source: input.vaultSource ?? "resolved" },
5999
+ overall_status: maxStatus([blockingStatus, advisoryStatus]),
6000
+ blocking_status: blockingStatus,
6001
+ advisory_status: advisoryStatus,
6002
+ coverage: {
6003
+ doctor: { state: "checked", status: doctorStatus },
6004
+ lint: { state: "checked", status: lintComponent.status },
6005
+ vault_sync: syncMode === "off" ? { state: "skipped", status: "pass", reason: "--sync off" } : { state: "checked", status: vaultSync.status },
6006
+ query_readiness: { state: "checked", status: queryReadiness.status },
6007
+ source_freshness: { state: "checked", status: sourceFreshness.status }
6008
+ },
6009
+ components: {
6010
+ doctor: doctorComponent,
6011
+ lint: lintComponent,
6012
+ vault_sync: vaultSync,
6013
+ query_readiness: queryReadiness,
6014
+ source_freshness: sourceFreshness
6015
+ },
6016
+ risk_flags: riskFlags,
6017
+ details_included: false,
6018
+ truncated: false,
6019
+ mutated: false,
6020
+ post_commit_ran: false,
6021
+ report_complete: true,
6022
+ report_written: false,
6023
+ warnings: []
6024
+ };
6025
+ if (input.out) {
6026
+ baseReport.report_path = input.out;
6027
+ }
6028
+ if (input.out && resolve6(input.out).startsWith(resolve6(input.vault) + "/")) {
6029
+ baseReport.warnings.push({
6030
+ id: "report_inside_vault",
6031
+ message: "health report was written inside the vault; this may create sync churn"
6032
+ });
6033
+ }
6034
+ const self_check = selfCheckReport(baseReport);
6035
+ const reportWithoutHint = {
6036
+ ...baseReport,
6037
+ self_check,
6038
+ overall_status: self_check.status === "error" ? "unknown" : baseReport.overall_status,
6039
+ report_complete: self_check.status === "error" ? false : baseReport.report_complete
6040
+ };
6041
+ const report = {
6042
+ ...reportWithoutHint,
6043
+ humanHint: buildHumanHint(reportWithoutHint)
6044
+ };
6045
+ if (input.out) {
6046
+ report.report_written = true;
6047
+ writeReport(input.out, report);
6048
+ }
6049
+ let exitCode = ExitCode.OK;
6050
+ if (!input.noFail) {
6051
+ if (report.self_check.status === "error") exitCode = ExitCode.INTERNAL_ERROR;
6052
+ else if (report.blocking_status === "error") exitCode = ExitCode.LINT_HAS_ERRORS;
6053
+ else if (report.blocking_status === "warn" || report.advisory_status === "warn" || report.advisory_status === "error") {
6054
+ exitCode = ExitCode.LINT_HAS_WARNINGS;
6055
+ }
6056
+ }
6057
+ return { exitCode, result: ok(report) };
6058
+ }
6059
+
5627
6060
  // src/commands/archive.ts
5628
6061
  import { rename as rename7, mkdir as mkdir9, readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
5629
- import { join as join28, dirname as dirname10 } from "path";
6062
+ import { join as join29, dirname as dirname11 } from "path";
5630
6063
  function countWikilinks(body, slug) {
5631
6064
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5632
6065
  const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
@@ -5654,7 +6087,7 @@ async function runArchive(input) {
5654
6087
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
5655
6088
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
5656
6089
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
5657
- const archivePath = join28("_archive", relPath).replace(/\\/g, "/");
6090
+ const archivePath = join29("_archive", relPath).replace(/\\/g, "/");
5658
6091
  let cascade;
5659
6092
  if (input.cascade) {
5660
6093
  const wikilinkRefs = [];
@@ -5678,7 +6111,7 @@ async function runArchive(input) {
5678
6111
  const indexRefs = [];
5679
6112
  if (!isRaw) {
5680
6113
  try {
5681
- const idx = await readFile20(join28(input.vault, "index.md"), "utf8");
6114
+ const idx = await readFile20(join29(input.vault, "index.md"), "utf8");
5682
6115
  idx.split("\n").forEach((line, i) => {
5683
6116
  if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
5684
6117
  });
@@ -5704,7 +6137,7 @@ async function runArchive(input) {
5704
6137
  }
5705
6138
  if (input.cascade && input.apply && cascade) {
5706
6139
  for (const ref of cascade.source_array_refs) {
5707
- const absPath = join28(input.vault, ref.page);
6140
+ const absPath = join29(input.vault, ref.page);
5708
6141
  const text = await readFile20(absPath, "utf8");
5709
6142
  const split = splitFrontmatter(text);
5710
6143
  if (!split.ok) continue;
@@ -5722,10 +6155,10 @@ ${fmRewritten}
5722
6155
  }
5723
6156
  }
5724
6157
  }
5725
- await mkdir9(dirname10(join28(input.vault, archivePath)), { recursive: true });
6158
+ await mkdir9(dirname11(join29(input.vault, archivePath)), { recursive: true });
5726
6159
  let indexUpdated = false;
5727
6160
  if (!isRaw) {
5728
- const indexPath = join28(input.vault, "index.md");
6161
+ const indexPath = join29(input.vault, "index.md");
5729
6162
  try {
5730
6163
  const idx = await readFile20(indexPath, "utf8");
5731
6164
  const originalLines = idx.split("\n");
@@ -5738,7 +6171,7 @@ ${fmRewritten}
5738
6171
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
5739
6172
  }
5740
6173
  }
5741
- await rename7(join28(input.vault, relPath), join28(input.vault, archivePath));
6174
+ await rename7(join29(input.vault, relPath), join29(input.vault, archivePath));
5742
6175
  appendLastOp(input.vault, {
5743
6176
  operation: input.cascade ? "archive-cascade" : "archive",
5744
6177
  summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
@@ -6147,15 +6580,37 @@ ${newBody}`;
6147
6580
 
6148
6581
  // src/commands/update.ts
6149
6582
  import { execSync as execSync3 } from "child_process";
6150
- import { readFileSync as readFileSync8 } from "fs";
6151
- import { join as join29 } from "path";
6583
+ import { join as join30 } from "path";
6584
+
6585
+ // src/utils/package-info.ts
6586
+ import { readFileSync as readFileSync9 } from "fs";
6587
+ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
6588
+ return [
6589
+ new URL("../package.json", baseUrl),
6590
+ new URL("../../package.json", baseUrl)
6591
+ ];
6592
+ }
6593
+ function readCliPackageJson(baseUrl = import.meta.url) {
6594
+ for (const url of packageJsonCandidateUrls(baseUrl)) {
6595
+ try {
6596
+ const pkg2 = JSON.parse(readFileSync9(url, "utf8"));
6597
+ if (typeof pkg2.version === "string") {
6598
+ return { ...pkg2, version: pkg2.version };
6599
+ }
6600
+ } catch {
6601
+ }
6602
+ }
6603
+ throw new Error(`Could not locate skillwiki package.json from ${baseUrl}`);
6604
+ }
6605
+
6606
+ // src/commands/update.ts
6152
6607
  function resolveGlobalSkillsRoot() {
6153
6608
  try {
6154
6609
  const globalRoot = execSync3("npm root -g", {
6155
6610
  encoding: "utf8",
6156
6611
  timeout: 5e3
6157
6612
  }).trim();
6158
- return join29(globalRoot, "skillwiki", "skills");
6613
+ return join30(globalRoot, "skillwiki", "skills");
6159
6614
  } catch {
6160
6615
  return null;
6161
6616
  }
@@ -6176,12 +6631,10 @@ async function refreshInstalledSkills(target) {
6176
6631
  }
6177
6632
  }
6178
6633
  async function runUpdate(input) {
6179
- const pkg2 = JSON.parse(
6180
- readFileSync8(new URL("../../package.json", import.meta.url), "utf8")
6181
- );
6634
+ const pkg2 = readCliPackageJson();
6182
6635
  const currentVersion = pkg2.version;
6183
6636
  const tag = normalizeDistTag(input.distTag);
6184
- const target = join29(input.home, ".claude", "skills");
6637
+ const target = join30(input.home, ".claude", "skills");
6185
6638
  let latest;
6186
6639
  try {
6187
6640
  latest = execSync3(`npm view skillwiki@${tag} version`, {
@@ -6252,24 +6705,22 @@ async function runUpdate(input) {
6252
6705
 
6253
6706
  // src/commands/self-update.ts
6254
6707
  import { execSync as execSync4 } from "child_process";
6255
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
6256
- import { join as join30 } from "path";
6708
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
6709
+ import { join as join31 } from "path";
6257
6710
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
6258
6711
  async function runSelfUpdate(input) {
6259
- const currentVersion = JSON.parse(
6260
- readFileSync9(new URL("../../package.json", import.meta.url), "utf8")
6261
- ).version;
6712
+ const currentVersion = readCliPackageJson().version;
6262
6713
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
6263
6714
  const distTag = normalizeDistTag(input.distTag);
6264
- const localPkgPath = join30(sourceRoot, "packages", "cli", "package.json");
6265
- const hasLocalSource = existsSync11(localPkgPath);
6715
+ const localPkgPath = join31(sourceRoot, "packages", "cli", "package.json");
6716
+ const hasLocalSource = existsSync12(localPkgPath);
6266
6717
  if (input.check) {
6267
6718
  let availableVersion = null;
6268
6719
  let source;
6269
6720
  if (hasLocalSource) {
6270
6721
  source = "local";
6271
6722
  try {
6272
- availableVersion = JSON.parse(readFileSync9(localPkgPath, "utf8")).version ?? null;
6723
+ availableVersion = JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? null;
6273
6724
  } catch {
6274
6725
  availableVersion = null;
6275
6726
  }
@@ -6327,7 +6778,7 @@ async function runSelfUpdate(input) {
6327
6778
  }
6328
6779
  const newVersion = (() => {
6329
6780
  try {
6330
- return JSON.parse(readFileSync9(localPkgPath, "utf8")).version ?? "unknown";
6781
+ return JSON.parse(readFileSync10(localPkgPath, "utf8")).version ?? "unknown";
6331
6782
  } catch {
6332
6783
  return "unknown";
6333
6784
  }
@@ -6394,9 +6845,9 @@ async function runSelfUpdate(input) {
6394
6845
 
6395
6846
  // src/commands/transcripts.ts
6396
6847
  import { readdir as readdir5, stat as stat7, readFile as readFile21 } from "fs/promises";
6397
- import { join as join31 } from "path";
6848
+ import { join as join32 } from "path";
6398
6849
  async function runTranscripts(input) {
6399
- const dir = join31(input.vault, "raw", "transcripts");
6850
+ const dir = join32(input.vault, "raw", "transcripts");
6400
6851
  let entries;
6401
6852
  try {
6402
6853
  entries = await readdir5(dir, { withFileTypes: true });
@@ -6406,7 +6857,7 @@ async function runTranscripts(input) {
6406
6857
  const transcripts = [];
6407
6858
  for (const entry of entries) {
6408
6859
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
6409
- const filePath = join31(dir, entry.name);
6860
+ const filePath = join32(dir, entry.name);
6410
6861
  const content = await readFile21(filePath, "utf8");
6411
6862
  const fm = extractFrontmatter(content);
6412
6863
  if (!fm.ok) continue;
@@ -6425,11 +6876,11 @@ async function runTranscripts(input) {
6425
6876
 
6426
6877
  // src/commands/project-index.ts
6427
6878
  import { readdir as readdir6, readFile as readFile22, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
6428
- import { join as join32, dirname as dirname11 } from "path";
6879
+ import { join as join33, dirname as dirname12 } from "path";
6429
6880
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
6430
6881
  async function runProjectIndex(input) {
6431
6882
  const slug = input.slug;
6432
- const projectDir = join32(input.vault, "projects", slug);
6883
+ const projectDir = join33(input.vault, "projects", slug);
6433
6884
  try {
6434
6885
  await readdir6(projectDir);
6435
6886
  } catch {
@@ -6440,12 +6891,12 @@ async function runProjectIndex(input) {
6440
6891
  }
6441
6892
  const wikilinkPattern = `[[${slug}]]`;
6442
6893
  const entries = [];
6443
- const compoundDir = join32(input.vault, "projects", slug, "compound");
6894
+ const compoundDir = join33(input.vault, "projects", slug, "compound");
6444
6895
  try {
6445
6896
  const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
6446
6897
  for (const entry of compoundFiles) {
6447
6898
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
6448
- const filePath = join32(compoundDir, entry.name);
6899
+ const filePath = join33(compoundDir, entry.name);
6449
6900
  let text;
6450
6901
  try {
6451
6902
  text = await readFile22(filePath, "utf8");
@@ -6465,13 +6916,13 @@ async function runProjectIndex(input) {
6465
6916
  for (const dir of LAYER2_DIRS) {
6466
6917
  let files;
6467
6918
  try {
6468
- files = await readdir6(join32(input.vault, dir), { withFileTypes: true });
6919
+ files = await readdir6(join33(input.vault, dir), { withFileTypes: true });
6469
6920
  } catch {
6470
6921
  continue;
6471
6922
  }
6472
6923
  for (const entry of files) {
6473
6924
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
6474
- const filePath = join32(input.vault, dir, entry.name);
6925
+ const filePath = join33(input.vault, dir, entry.name);
6475
6926
  let text;
6476
6927
  try {
6477
6928
  text = await readFile22(filePath, "utf8");
@@ -6495,7 +6946,7 @@ async function runProjectIndex(input) {
6495
6946
  const tb = typeOrder[b.type] ?? 99;
6496
6947
  return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
6497
6948
  });
6498
- const indexPath = join32(projectDir, "knowledge.md");
6949
+ const indexPath = join33(projectDir, "knowledge.md");
6499
6950
  let existing = false;
6500
6951
  let stale = false;
6501
6952
  try {
@@ -6539,7 +6990,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
6539
6990
  }
6540
6991
  if (input.apply) {
6541
6992
  try {
6542
- await mkdir10(dirname11(indexPath), { recursive: true });
6993
+ await mkdir10(dirname12(indexPath), { recursive: true });
6543
6994
  await writeFile11(indexPath, body, "utf8");
6544
6995
  } catch (e) {
6545
6996
  return {
@@ -6569,8 +7020,8 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
6569
7020
 
6570
7021
  // src/commands/compound.ts
6571
7022
  import { writeFile as writeFile12, mkdir as mkdir11, readdir as readdir7, unlink as unlink4 } from "fs/promises";
6572
- import { join as join33 } from "path";
6573
- import { existsSync as existsSync12 } from "fs";
7023
+ import { join as join34 } from "path";
7024
+ import { existsSync as existsSync13 } from "fs";
6574
7025
  import { readFile as readFile23 } from "fs/promises";
6575
7026
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
6576
7027
  var FIELD_RE = {
@@ -6669,7 +7120,7 @@ function extractRetroFields(date, cycleName, block) {
6669
7120
  };
6670
7121
  }
6671
7122
  async function runCompound(input) {
6672
- const logPath = join33(input.vault, "log.md");
7123
+ const logPath = join34(input.vault, "log.md");
6673
7124
  let logText;
6674
7125
  try {
6675
7126
  logText = await readFile23(logPath, "utf8");
@@ -6679,7 +7130,7 @@ async function runCompound(input) {
6679
7130
  const entries = parseRetroEntries(logText);
6680
7131
  const promoted = [];
6681
7132
  const skipped = [];
6682
- const compoundDir = join33(input.vault, "projects", input.project, "compound");
7133
+ const compoundDir = join34(input.vault, "projects", input.project, "compound");
6683
7134
  for (const entry of entries) {
6684
7135
  const generalizeValue = entry.generalize.trim();
6685
7136
  if (!/^yes/i.test(generalizeValue)) {
@@ -6687,8 +7138,8 @@ async function runCompound(input) {
6687
7138
  continue;
6688
7139
  }
6689
7140
  const slug = slugify(entry.cycleName);
6690
- const compoundPath = join33(compoundDir, `${slug}.md`);
6691
- if (existsSync12(compoundPath)) {
7141
+ const compoundPath = join34(compoundDir, `${slug}.md`);
7142
+ if (existsSync13(compoundPath)) {
6692
7143
  skipped.push(entry.date);
6693
7144
  continue;
6694
7145
  }
@@ -6726,7 +7177,7 @@ async function runCompound(input) {
6726
7177
  ].join("\n");
6727
7178
  const content = frontmatter + "\n" + body;
6728
7179
  if (!input.dryRun) {
6729
- if (!existsSync12(compoundDir)) {
7180
+ if (!existsSync13(compoundDir)) {
6730
7181
  await mkdir11(compoundDir, { recursive: true });
6731
7182
  }
6732
7183
  await writeFile12(compoundPath, content, "utf8");
@@ -6748,16 +7199,16 @@ async function runCompound(input) {
6748
7199
  };
6749
7200
  }
6750
7201
  async function runCompoundDelete(input) {
6751
- const projectDir = join33(input.vault, "projects", input.project);
6752
- if (!existsSync12(projectDir)) {
7202
+ const projectDir = join34(input.vault, "projects", input.project);
7203
+ if (!existsSync13(projectDir)) {
6753
7204
  return {
6754
7205
  exitCode: ExitCode.PROJECT_NOT_FOUND,
6755
7206
  result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
6756
7207
  };
6757
7208
  }
6758
7209
  const entryName = input.entry.replace(/\.md$/, "");
6759
- const compoundPath = join33(projectDir, "compound", `${entryName}.md`);
6760
- if (!existsSync12(compoundPath)) {
7210
+ const compoundPath = join34(projectDir, "compound", `${entryName}.md`);
7211
+ if (!existsSync13(compoundPath)) {
6761
7212
  return {
6762
7213
  exitCode: ExitCode.FILE_NOT_FOUND,
6763
7214
  result: err("FILE_NOT_FOUND", { path: compoundPath })
@@ -6790,8 +7241,8 @@ knowledge.md regenerated`
6790
7241
  };
6791
7242
  }
6792
7243
  async function runCompoundList(input) {
6793
- const compoundDir = join33(input.vault, "projects", input.project, "compound");
6794
- if (!existsSync12(compoundDir)) {
7244
+ const compoundDir = join34(input.vault, "projects", input.project, "compound");
7245
+ if (!existsSync13(compoundDir)) {
6795
7246
  return {
6796
7247
  exitCode: ExitCode.OK,
6797
7248
  result: ok({
@@ -6821,7 +7272,7 @@ could not read compound directory`
6821
7272
  const entries = [];
6822
7273
  for (const dirent of dirents) {
6823
7274
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
6824
- const filePath = join33(compoundDir, dirent.name);
7275
+ const filePath = join34(compoundDir, dirent.name);
6825
7276
  let text;
6826
7277
  try {
6827
7278
  text = await readFile23(filePath, "utf8");
@@ -6854,8 +7305,8 @@ no compound entries found`;
6854
7305
 
6855
7306
  // src/commands/observe.ts
6856
7307
  import { mkdir as mkdir12, writeFile as writeFile13 } from "fs/promises";
6857
- import { existsSync as existsSync13, statSync as statSync4 } from "fs";
6858
- import { join as join34 } from "path";
7308
+ import { existsSync as existsSync14, statSync as statSync4 } from "fs";
7309
+ import { join as join35 } from "path";
6859
7310
  import { createHash as createHash4 } from "crypto";
6860
7311
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
6861
7312
  function slugify2(text) {
@@ -6878,13 +7329,13 @@ async function runObserve(input) {
6878
7329
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
6879
7330
  };
6880
7331
  }
6881
- if (!existsSync13(input.vault) || !statSync4(input.vault).isDirectory()) {
7332
+ if (!existsSync14(input.vault) || !statSync4(input.vault).isDirectory()) {
6882
7333
  return {
6883
7334
  exitCode: ExitCode.VAULT_PATH_INVALID,
6884
7335
  result: err("VAULT_PATH_INVALID", { path: input.vault })
6885
7336
  };
6886
7337
  }
6887
- const transcriptsDir = join34(input.vault, "raw", "transcripts");
7338
+ const transcriptsDir = join35(input.vault, "raw", "transcripts");
6888
7339
  try {
6889
7340
  await mkdir12(transcriptsDir, { recursive: true });
6890
7341
  } catch {
@@ -6896,7 +7347,7 @@ async function runObserve(input) {
6896
7347
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6897
7348
  const slug = slugify2(input.text);
6898
7349
  const fileName = `${today}-observation-${slug}.md`;
6899
- const filePath = join34(transcriptsDir, fileName);
7350
+ const filePath = join35(transcriptsDir, fileName);
6900
7351
  const body = `
6901
7352
  ${input.text.trim()}
6902
7353
  `;
@@ -6937,7 +7388,7 @@ ${input.text.trim()}
6937
7388
 
6938
7389
  // src/commands/ingest.ts
6939
7390
  import { readFile as readFile24, writeFile as writeFile14, mkdir as mkdir13 } from "fs/promises";
6940
- import { join as join35 } from "path";
7391
+ import { join as join36 } from "path";
6941
7392
  import { createHash as createHash5 } from "crypto";
6942
7393
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
6943
7394
  var TYPE_DIR = {
@@ -7111,8 +7562,8 @@ async function runIngest(input) {
7111
7562
  const rawRelPath = `raw/articles/${slug}.md`;
7112
7563
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
7113
7564
  const typedRelPath = `${typedDir}/${slug}.md`;
7114
- const rawAbsPath = join35(input.vault, rawRelPath);
7115
- const typedAbsPath = join35(input.vault, typedRelPath);
7565
+ const rawAbsPath = join36(input.vault, rawRelPath);
7566
+ const typedAbsPath = join36(input.vault, typedRelPath);
7116
7567
  const identity = assessSourceIdentity({
7117
7568
  rawPath: rawRelPath,
7118
7569
  sourceUrl: sourceUrl ?? void 0,
@@ -7194,7 +7645,7 @@ async function runIngest(input) {
7194
7645
  };
7195
7646
  }
7196
7647
  try {
7197
- await mkdir13(join35(input.vault, "raw", "articles"), { recursive: true });
7648
+ await mkdir13(join36(input.vault, "raw", "articles"), { recursive: true });
7198
7649
  await writeFile14(rawAbsPath, rawContent, "utf8");
7199
7650
  } catch (e) {
7200
7651
  return {
@@ -7203,7 +7654,7 @@ async function runIngest(input) {
7203
7654
  };
7204
7655
  }
7205
7656
  try {
7206
- await mkdir13(join35(input.vault, typedDir), { recursive: true });
7657
+ await mkdir13(join36(input.vault, typedDir), { recursive: true });
7207
7658
  await writeFile14(typedAbsPath, typedContent, "utf8");
7208
7659
  } catch (e) {
7209
7660
  return {
@@ -7382,12 +7833,12 @@ ${body}`;
7382
7833
  }
7383
7834
 
7384
7835
  // src/commands/sync.ts
7385
- import { existsSync as existsSync15 } from "fs";
7386
- import { join as join37 } from "path";
7836
+ import { existsSync as existsSync16 } from "fs";
7837
+ import { join as join38 } from "path";
7387
7838
 
7388
7839
  // src/utils/sync-lock.ts
7389
- import { existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync10, renameSync, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "fs";
7390
- import { join as join36 } from "path";
7840
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "fs";
7841
+ import { join as join37 } from "path";
7391
7842
  import { createHash as createHash6 } from "crypto";
7392
7843
  function getSessionId() {
7393
7844
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
@@ -7395,13 +7846,13 @@ function getSessionId() {
7395
7846
  return process.pid.toString();
7396
7847
  }
7397
7848
  function lockPath(vault) {
7398
- return join36(vault, ".skillwiki", "sync.lock");
7849
+ return join37(vault, ".skillwiki", "sync.lock");
7399
7850
  }
7400
7851
  function readLock(vault) {
7401
7852
  const path = lockPath(vault);
7402
- if (!existsSync14(path)) return null;
7853
+ if (!existsSync15(path)) return null;
7403
7854
  try {
7404
- const raw = readFileSync10(path, "utf8");
7855
+ const raw = readFileSync11(path, "utf8");
7405
7856
  return JSON.parse(raw);
7406
7857
  } catch {
7407
7858
  return null;
@@ -7414,9 +7865,9 @@ function isStale(lock, now) {
7414
7865
  }
7415
7866
  function acquireLock(vault, opts = {}) {
7416
7867
  const path = lockPath(vault);
7417
- const dir = join36(vault, ".skillwiki");
7418
- if (!existsSync14(dir)) {
7419
- mkdirSync4(dir, { recursive: true });
7868
+ const dir = join37(vault, ".skillwiki");
7869
+ if (!existsSync15(dir)) {
7870
+ mkdirSync5(dir, { recursive: true });
7420
7871
  }
7421
7872
  const sessionId = opts.sessionId ?? getSessionId();
7422
7873
  const summary = opts.summary ?? "skillwiki sync";
@@ -7435,7 +7886,7 @@ function acquireLock(vault, opts = {}) {
7435
7886
  };
7436
7887
  try {
7437
7888
  const content = JSON.stringify(lock, null, 2) + "\n";
7438
- writeFileSync6(path, content, { flag: "wx" });
7889
+ writeFileSync7(path, content, { flag: "wx" });
7439
7890
  return { ok: true, lock };
7440
7891
  } catch (e) {
7441
7892
  const err3 = e;
@@ -7455,12 +7906,12 @@ function acquireLock(vault, opts = {}) {
7455
7906
  function writeLockedFile(path, lock) {
7456
7907
  const tmp = path + ".tmp";
7457
7908
  const content = JSON.stringify(lock, null, 2) + "\n";
7458
- writeFileSync6(tmp, content);
7459
- renameSync(tmp, path);
7909
+ writeFileSync7(tmp, content);
7910
+ renameSync2(tmp, path);
7460
7911
  }
7461
7912
  function releaseLock(vault, opts = {}) {
7462
7913
  const path = lockPath(vault);
7463
- if (!existsSync14(path)) {
7914
+ if (!existsSync15(path)) {
7464
7915
  return { released: false };
7465
7916
  }
7466
7917
  const sessionId = opts.sessionId ?? getSessionId();
@@ -7489,7 +7940,7 @@ function releaseLock(vault, opts = {}) {
7489
7940
  function runSyncStatus(input) {
7490
7941
  const vault = input.vault;
7491
7942
  const includeStashes = input.includeStashes ?? false;
7492
- if (!existsSync15(join37(vault, ".git"))) {
7943
+ if (!existsSync16(join38(vault, ".git"))) {
7493
7944
  return {
7494
7945
  exitCode: ExitCode.VAULT_PATH_INVALID,
7495
7946
  result: ok({
@@ -7567,7 +8018,7 @@ function runSyncStatus(input) {
7567
8018
  }
7568
8019
  async function runSyncPush(input) {
7569
8020
  const vault = input.vault;
7570
- if (!existsSync15(join37(vault, ".git"))) {
8021
+ if (!existsSync16(join38(vault, ".git"))) {
7571
8022
  return {
7572
8023
  exitCode: ExitCode.VAULT_PATH_INVALID,
7573
8024
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -7690,7 +8141,7 @@ function enableGitLongPathsOnWindows(vault) {
7690
8141
  }
7691
8142
  async function runSyncPull(input) {
7692
8143
  const vault = input.vault;
7693
- if (!existsSync15(join37(vault, ".git"))) {
8144
+ if (!existsSync16(join38(vault, ".git"))) {
7694
8145
  return {
7695
8146
  exitCode: ExitCode.VAULT_PATH_INVALID,
7696
8147
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -7862,7 +8313,7 @@ function runSyncPeers(input) {
7862
8313
  }
7863
8314
  function runSyncLock(input) {
7864
8315
  const vault = input.vault;
7865
- if (!existsSync15(vault)) {
8316
+ if (!existsSync16(vault)) {
7866
8317
  return {
7867
8318
  exitCode: ExitCode.VAULT_PATH_INVALID,
7868
8319
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -7897,7 +8348,7 @@ function runSyncLock(input) {
7897
8348
  }
7898
8349
  function runSyncUnlock(input) {
7899
8350
  const vault = input.vault;
7900
- if (!existsSync15(vault)) {
8351
+ if (!existsSync16(vault)) {
7901
8352
  return {
7902
8353
  exitCode: ExitCode.VAULT_PATH_INVALID,
7903
8354
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -7930,8 +8381,8 @@ function runSyncUnlock(input) {
7930
8381
  }
7931
8382
 
7932
8383
  // src/commands/backup.ts
7933
- import { statSync as statSync5, readdirSync as readdirSync3, readFileSync as readFileSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
7934
- import { join as join38, relative as relative3, dirname as dirname12 } from "path";
8384
+ import { statSync as statSync5, readdirSync as readdirSync3, readFileSync as readFileSync12, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
8385
+ import { join as join39, relative as relative3, dirname as dirname13 } from "path";
7935
8386
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
7936
8387
 
7937
8388
  // src/utils/s3-client.ts
@@ -7955,7 +8406,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node
7955
8406
  function* walkMarkdown(dir, base) {
7956
8407
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
7957
8408
  if (SKIP_DIRS2.has(entry.name)) continue;
7958
- const full = join38(dir, entry.name);
8409
+ const full = join39(dir, entry.name);
7959
8410
  if (entry.isDirectory()) {
7960
8411
  yield* walkMarkdown(full, base);
7961
8412
  } else if (entry.name.endsWith(".md")) {
@@ -7978,7 +8429,7 @@ async function runBackupSync(input) {
7978
8429
  let failed = 0;
7979
8430
  const files = [...walkMarkdown(input.vault, input.vault)];
7980
8431
  for (const relPath of files) {
7981
- const absPath = join38(input.vault, relPath);
8432
+ const absPath = join39(input.vault, relPath);
7982
8433
  const localStat = statSync5(absPath);
7983
8434
  let needsUpload = true;
7984
8435
  try {
@@ -7997,7 +8448,7 @@ async function runBackupSync(input) {
7997
8448
  continue;
7998
8449
  }
7999
8450
  try {
8000
- const body = readFileSync11(absPath);
8451
+ const body = readFileSync12(absPath);
8001
8452
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
8002
8453
  uploaded++;
8003
8454
  } catch {
@@ -8054,7 +8505,7 @@ async function runBackupRestore(input) {
8054
8505
  const objects = list.Contents ?? [];
8055
8506
  for (const obj of objects) {
8056
8507
  if (!obj.Key) continue;
8057
- const localPath = join38(target, obj.Key);
8508
+ const localPath = join39(target, obj.Key);
8058
8509
  try {
8059
8510
  const localStat = statSync5(localPath);
8060
8511
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -8067,8 +8518,8 @@ async function runBackupRestore(input) {
8067
8518
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
8068
8519
  const body = await resp.Body?.transformToByteArray();
8069
8520
  if (body) {
8070
- mkdirSync5(dirname12(localPath), { recursive: true });
8071
- writeFileSync7(localPath, Buffer.from(body));
8521
+ mkdirSync6(dirname13(localPath), { recursive: true });
8522
+ writeFileSync8(localPath, Buffer.from(body));
8072
8523
  downloaded++;
8073
8524
  }
8074
8525
  } catch {
@@ -8100,11 +8551,11 @@ async function runBackupRestore(input) {
8100
8551
  }
8101
8552
 
8102
8553
  // src/commands/status.ts
8103
- import { existsSync as existsSync16, statSync as statSync6 } from "fs";
8554
+ import { existsSync as existsSync17, statSync as statSync6 } from "fs";
8104
8555
  import { readFile as readFile25 } from "fs/promises";
8105
- import { join as join39 } from "path";
8556
+ import { join as join40 } from "path";
8106
8557
  async function runStatus(input) {
8107
- if (!existsSync16(input.vault)) {
8558
+ if (!existsSync17(input.vault)) {
8108
8559
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
8109
8560
  }
8110
8561
  const scan = await scanVault(input.vault);
@@ -8129,7 +8580,7 @@ async function runStatus(input) {
8129
8580
  const compound = scan.data.compound.length;
8130
8581
  let schemaVersion = "v1";
8131
8582
  try {
8132
- const schemaContent = await readFile25(join39(input.vault, "SCHEMA.md"), "utf8");
8583
+ const schemaContent = await readFile25(join40(input.vault, "SCHEMA.md"), "utf8");
8133
8584
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
8134
8585
  if (versionMatch) schemaVersion = versionMatch[1];
8135
8586
  } catch {
@@ -8190,7 +8641,7 @@ async function runStatus(input) {
8190
8641
 
8191
8642
  // src/commands/seed.ts
8192
8643
  import { mkdir as mkdir14, writeFile as writeFile15, stat as stat8 } from "fs/promises";
8193
- import { join as join40 } from "path";
8644
+ import { join as join41 } from "path";
8194
8645
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8195
8646
  var EXAMPLE_PAGES = {
8196
8647
  "entities/example-project.md": `---
@@ -8259,29 +8710,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
8259
8710
  `;
8260
8711
  async function runSeed(input) {
8261
8712
  try {
8262
- await stat8(join40(input.vault, "SCHEMA.md"));
8713
+ await stat8(join41(input.vault, "SCHEMA.md"));
8263
8714
  } catch {
8264
8715
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
8265
8716
  }
8266
8717
  const created = [];
8267
8718
  const skipped = [];
8268
8719
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
8269
- const absPath = join40(input.vault, relPath);
8720
+ const absPath = join41(input.vault, relPath);
8270
8721
  try {
8271
8722
  await stat8(absPath);
8272
8723
  skipped.push(relPath);
8273
8724
  } catch {
8274
- await mkdir14(join40(absPath, ".."), { recursive: true });
8725
+ await mkdir14(join41(absPath, ".."), { recursive: true });
8275
8726
  await writeFile15(absPath, content, "utf8");
8276
8727
  created.push(relPath);
8277
8728
  }
8278
8729
  }
8279
- const rawPath = join40(input.vault, "raw", "articles", "example-source.md");
8730
+ const rawPath = join41(input.vault, "raw", "articles", "example-source.md");
8280
8731
  try {
8281
8732
  await stat8(rawPath);
8282
8733
  skipped.push("raw/articles/example-source.md");
8283
8734
  } catch {
8284
- await mkdir14(join40(rawPath, ".."), { recursive: true });
8735
+ await mkdir14(join41(rawPath, ".."), { recursive: true });
8285
8736
  await writeFile15(rawPath, EXAMPLE_RAW, "utf8");
8286
8737
  created.push("raw/articles/example-source.md");
8287
8738
  }
@@ -8305,8 +8756,8 @@ async function runSeed(input) {
8305
8756
 
8306
8757
  // src/commands/canvas.ts
8307
8758
  import { readFile as readFile26, writeFile as writeFile16 } from "fs/promises";
8308
- import { existsSync as existsSync17 } from "fs";
8309
- import { join as join41 } from "path";
8759
+ import { existsSync as existsSync18 } from "fs";
8760
+ import { join as join42 } from "path";
8310
8761
  var NODE_WIDTH = 240;
8311
8762
  var NODE_HEIGHT = 60;
8312
8763
  var COLUMN_SPACING = 400;
@@ -8384,8 +8835,8 @@ function buildCanvasEdges(adjacency) {
8384
8835
  return edges;
8385
8836
  }
8386
8837
  async function runCanvasGenerate(input) {
8387
- const graphPath = input.graphPath ?? join41(input.vault, ".skillwiki", "graph.json");
8388
- if (!existsSync17(graphPath)) {
8838
+ const graphPath = input.graphPath ?? join42(input.vault, ".skillwiki", "graph.json");
8839
+ if (!existsSync18(graphPath)) {
8389
8840
  return {
8390
8841
  exitCode: ExitCode.FILE_NOT_FOUND,
8391
8842
  result: err("FILE_NOT_FOUND", {
@@ -8422,7 +8873,7 @@ async function runCanvasGenerate(input) {
8422
8873
  const nodes = buildCanvasNodes(paths);
8423
8874
  const edges = buildCanvasEdges(graph.adjacency);
8424
8875
  const canvas = { nodes, edges };
8425
- const outPath = join41(input.vault, "vault-graph.canvas");
8876
+ const outPath = join42(input.vault, "vault-graph.canvas");
8426
8877
  try {
8427
8878
  await writeFile16(outPath, JSON.stringify(canvas, null, 2));
8428
8879
  } catch (e) {
@@ -8445,7 +8896,7 @@ written: ${outPath}`
8445
8896
 
8446
8897
  // src/commands/query.ts
8447
8898
  import { readFile as readFile27, stat as stat9 } from "fs/promises";
8448
- import { join as join42 } from "path";
8899
+ import { join as join43 } from "path";
8449
8900
  var W_KEYWORD = 2;
8450
8901
  var W_SOURCE_OVERLAP = 4;
8451
8902
  var W_WIKILINK = 3;
@@ -8566,7 +9017,7 @@ function computeKeywordScore(terms, title, tags, body) {
8566
9017
  return score;
8567
9018
  }
8568
9019
  async function loadOrBuildGraph(vault) {
8569
- const graphPath = join42(vault, ".skillwiki", "graph.json");
9020
+ const graphPath = join43(vault, ".skillwiki", "graph.json");
8570
9021
  let needsBuild = false;
8571
9022
  try {
8572
9023
  const fileStat = await stat9(graphPath);
@@ -8588,14 +9039,14 @@ async function loadOrBuildGraph(vault) {
8588
9039
  }
8589
9040
 
8590
9041
  // src/utils/auto-commit.ts
8591
- import { existsSync as existsSync18 } from "fs";
8592
- import { join as join43 } from "path";
9042
+ import { existsSync as existsSync19 } from "fs";
9043
+ import { join as join44 } from "path";
8593
9044
  async function postCommit(vault, exitCode) {
8594
9045
  if (exitCode !== 0) return;
8595
9046
  const home = process.env.HOME ?? "";
8596
9047
  const dotenv = await parseDotenvFile(configPath(home));
8597
9048
  if (dotenv["AUTO_COMMIT"] === "false") return;
8598
- if (!existsSync18(join43(vault, ".git"))) return;
9049
+ if (!existsSync19(join44(vault, ".git"))) return;
8599
9050
  const lastOps = readLastOp(vault);
8600
9051
  if (lastOps.length === 0) return;
8601
9052
  const porcelain = git(vault, ["status", "--porcelain"]);
@@ -8624,14 +9075,14 @@ async function postCommit(vault, exitCode) {
8624
9075
  }
8625
9076
 
8626
9077
  // src/cli.ts
8627
- var pkg = JSON.parse(readFileSync12(new URL("../package.json", import.meta.url), "utf8"));
9078
+ var pkg = readCliPackageJson();
8628
9079
  var program = new Command2();
8629
9080
  program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
8630
9081
  program.option("--human", "render terminal-readable output instead of JSON");
8631
- async function emit(r, vault) {
9082
+ async function emit(r, vault, opts) {
8632
9083
  if (program.opts().human) printHuman(r.result);
8633
9084
  else printJson(r.result);
8634
- if (vault) await postCommit(vault, r.exitCode);
9085
+ if (vault && opts?.postCommit !== false) await postCommit(vault, r.exitCode);
8635
9086
  process.exit(r.exitCode);
8636
9087
  }
8637
9088
  program.command("hash <file>").description("compute SHA-256 hash of a vault page body").action(async (file) => emit(await runHash({ file })));
@@ -8646,7 +9097,7 @@ program.command("validate <file>").description("validate vault page frontmatter
8646
9097
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
8647
9098
  });
8648
9099
  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) => {
8649
- const out = opts.out ?? join44(vault, ".skillwiki", "graph.json");
9100
+ const out = opts.out ?? join45(vault, ".skillwiki", "graph.json");
8650
9101
  emit(await runGraphBuild({ vault, out }), vault);
8651
9102
  });
8652
9103
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -8777,9 +9228,20 @@ program.command("log-append [vault]").description("append a single entry to the
8777
9228
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
8778
9229
  else emit(await runLogAppend({ vault: v.vault, content: opts.content }), v.vault);
8779
9230
  });
8780
- 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("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
9231
+ 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) => {
8781
9232
  const v = await resolveVaultArg(vault, opts.wiki);
8782
9233
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9234
+ else if (opts.summary) emit(await runLint({
9235
+ vault: v.vault,
9236
+ source: vault ? "flag" : void 0,
9237
+ days: opts.days,
9238
+ lines: opts.lines,
9239
+ logThreshold: opts.logThreshold,
9240
+ fix: opts.fix ?? false,
9241
+ only: opts.only,
9242
+ summary: true,
9243
+ examplesLimit: opts.examples
9244
+ }), v.vault);
8783
9245
  else emit(await runLint({
8784
9246
  vault: v.vault,
8785
9247
  source: vault ? "flag" : void 0,
@@ -8790,6 +9252,27 @@ program.command("lint [vault]").description("run all vault health checks").optio
8790
9252
  only: opts.only
8791
9253
  }), v.vault);
8792
9254
  });
9255
+ program.command("health [vault]").description("bounded whole-system wiki health report").option("--wiki <name>", "wiki profile name").option("--sync <mode>", "vault-sync policy: optional|required|off", "optional").option("--no-fail", "always exit 0 while reporting status in JSON").option("--out <path>", "write JSON result envelope to this path").option("--examples <n>", "example count per lint bucket", (s) => parseInt(s, 10), 3).action(async (vault, opts) => {
9256
+ const sync = String(opts.sync ?? "optional");
9257
+ if (!["optional", "required", "off"].includes(sync)) {
9258
+ emit({ exitCode: ExitCode.USAGE, result: { ok: false, error: "USAGE", detail: "--sync must be optional, required, or off" } });
9259
+ }
9260
+ const v = await resolveVaultArg(vault, opts.wiki);
9261
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
9262
+ else emit(await runHealth({
9263
+ vault: v.vault,
9264
+ vaultSource: vault ? "flag" : "resolved",
9265
+ home: process.env.HOME ?? "",
9266
+ envValue: process.env.WIKI_PATH,
9267
+ argv: process.argv,
9268
+ currentVersion: pkg.version,
9269
+ cwd: process.cwd(),
9270
+ sync,
9271
+ noFail: opts.fail === false,
9272
+ out: opts.out,
9273
+ examplesLimit: opts.examples
9274
+ }), void 0, { postCommit: false });
9275
+ });
8793
9276
  var configCmd = program.command("config").description("manage skillwiki configuration");
8794
9277
  configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
8795
9278
  configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));