skillwiki 0.5.1 → 0.5.4

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
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  // src/cli.ts
11
11
  import { readFileSync as readFileSync10 } from "fs";
12
- import { join as join38 } from "path";
12
+ import { join as join39 } from "path";
13
13
  import { Command as Command2 } from "commander";
14
14
 
15
15
  // ../shared/src/exit-codes.ts
@@ -59,7 +59,9 @@ var ExitCode = {
59
59
  SYNC_PUSH_FAILED: 42,
60
60
  SYNC_PULL_FAILED: 43,
61
61
  BACKUP_SYNC_FAILED: 44,
62
- BACKUP_RESTORE_CONFLICTS: 45
62
+ BACKUP_RESTORE_CONFLICTS: 45,
63
+ USAGE: 46,
64
+ BODY_TRUNCATION_GUARD: 47
63
65
  };
64
66
 
65
67
  // ../shared/src/json-output.ts
@@ -91,7 +93,8 @@ var TypedKnowledgeSchema = z.object({
91
93
  contradictions: z.array(z.string()).optional(),
92
94
  provenance: z.enum(["research", "project", "mixed"]).optional(),
93
95
  provenance_projects: z.array(wikilink).optional(),
94
- work_items: z.array(wikilink).optional()
96
+ work_items: z.array(wikilink).optional(),
97
+ stale_ttl: z.number().int().positive().optional()
95
98
  }).superRefine((v, ctx) => {
96
99
  if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
97
100
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
@@ -305,11 +308,11 @@ async function runHash(input) {
305
308
  }
306
309
  const split = splitFrontmatter(text);
307
310
  if (!split.ok) return { exitCode: ExitCode.MISSING_CLOSING_DELIMITER, result: split };
308
- const bodyBytes = Buffer.from(split.data.body, "utf8");
309
- const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
311
+ const bodyBytes2 = Buffer.from(split.data.body, "utf8");
312
+ const sha256 = createHash("sha256").update(bodyBytes2).digest("hex");
310
313
  return {
311
314
  exitCode: ExitCode.OK,
312
- result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
315
+ result: ok({ path: input.file, sha256, byte_count: bodyBytes2.byteLength, humanHint: sha256 })
313
316
  };
314
317
  }
315
318
 
@@ -1004,8 +1007,10 @@ function isLegacyCitationStyle(body) {
1004
1007
  return false;
1005
1008
  }
1006
1009
  function hasOrphanedCitations(body) {
1007
- const stripped = stripFences(body.replace(FRONTMATTER, ""));
1010
+ const noFm = body.replace(FRONTMATTER, "");
1011
+ const stripped = stripFences(noFm);
1008
1012
  const lines = stripped.split("\n");
1013
+ const rawLines = noFm.split("\n");
1009
1014
  let inSources = false;
1010
1015
  let sourcesEnded = false;
1011
1016
  let sourcesStartLine = -1;
@@ -1025,10 +1030,13 @@ function hasOrphanedCitations(body) {
1025
1030
  }
1026
1031
  continue;
1027
1032
  }
1028
- const isListItem = /^\s*[-*]\s+/.test(line);
1033
+ const isListItem = /^\s*(?:[-*]|\d+\.)\s+/.test(line);
1029
1034
  const hasMarker = /\^\[raw\//.test(line);
1035
+ const hasBacktickRawPath = /`raw\/[^`]+`/.test(rawLines[i]);
1030
1036
  if (isListItem && hasMarker) {
1031
1037
  lastNonBlankInSources = i;
1038
+ } else if (isListItem && hasBacktickRawPath) {
1039
+ lastNonBlankInSources = i;
1032
1040
  } else if (hasMarker && !isListItem) {
1033
1041
  return true;
1034
1042
  } else {
@@ -1835,6 +1843,42 @@ async function runIndexCheck(input) {
1835
1843
  // src/commands/stale.ts
1836
1844
  import { readdir as readdir4, rename as rename2, mkdir as mkdir6, readFile as readFile10 } from "fs/promises";
1837
1845
  import { join as join13 } from "path";
1846
+
1847
+ // src/parsers/expiry-annotations.ts
1848
+ var HEADING_RE = /^#{1,6}\s+(.+)$/;
1849
+ var ANNOTATION_RE = /^<!--\s*expires:\s*(\d{4}-\d{2}-\d{2})(?:\s+refresh:\s*(weekly|monthly|quarterly))?(?:\s+source:\s*(\S+))?\s*-->$/;
1850
+ var VALID_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1851
+ function isValidDate(s) {
1852
+ if (!VALID_DATE_RE.test(s)) return false;
1853
+ const d = /* @__PURE__ */ new Date(s + "T00:00:00Z");
1854
+ return !Number.isNaN(d.getTime()) && s === d.toISOString().slice(0, 10);
1855
+ }
1856
+ function parseExpiryAnnotations(content, pagePath) {
1857
+ const lines = content.split("\n");
1858
+ const annotations = [];
1859
+ for (let i = 0; i < lines.length; i++) {
1860
+ const match = lines[i].match(ANNOTATION_RE);
1861
+ if (!match) continue;
1862
+ const expires = match[1];
1863
+ if (!isValidDate(expires)) continue;
1864
+ if (i === 0) continue;
1865
+ const prevLine = lines[i - 1];
1866
+ const headingMatch = prevLine.match(HEADING_RE);
1867
+ if (!headingMatch) continue;
1868
+ annotations.push({
1869
+ page: pagePath,
1870
+ heading: headingMatch[1].trim(),
1871
+ line: i + 1,
1872
+ // 1-indexed
1873
+ expires,
1874
+ refresh: match[2],
1875
+ source: match[3]
1876
+ });
1877
+ }
1878
+ return annotations;
1879
+ }
1880
+
1881
+ // src/commands/stale.ts
1838
1882
  function daysSince(isoDate2) {
1839
1883
  return Math.floor((Date.now() - Date.parse(isoDate2)) / 864e5);
1840
1884
  }
@@ -1852,6 +1896,12 @@ async function runStale(input) {
1852
1896
  projectSlugs = (await readdir4(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
1853
1897
  } catch {
1854
1898
  }
1899
+ if (input.project) {
1900
+ if (!projectSlugs.includes(input.project)) {
1901
+ return { exitCode: ExitCode.USAGE, result: { ok: false, error: "UNKNOWN_PROJECT", detail: `Project "${input.project}" not found. Available: ${projectSlugs.join(", ") || "(none)"}` } };
1902
+ }
1903
+ projectSlugs = [input.project];
1904
+ }
1855
1905
  for (const slug of projectSlugs) {
1856
1906
  const workPath = join13(projectsDir, slug, "work");
1857
1907
  let entries;
@@ -1893,9 +1943,10 @@ async function runStale(input) {
1893
1943
  function extractSlug(projectField) {
1894
1944
  return projectField.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^"|"$/g, "");
1895
1945
  }
1946
+ const TERMINAL_STATUSES = /* @__PURE__ */ new Set(["completed", "abandoned", "done", "invalid"]);
1896
1947
  const KIND_FROM_FILENAME = /^(?:\d{4}-\d{2}-\d{2})-(task|bug|idea|note|observation)-.+\.md$/;
1897
1948
  const LOOP_CYCLE_PATTERN = /loop-cycle-/;
1898
- const transcripts = scan.data.raw.filter((p) => p.relPath.startsWith("raw/transcripts/") && p.relPath.endsWith(".md"));
1949
+ let transcripts = scan.data.raw.filter((p) => p.relPath.startsWith("raw/transcripts/") && p.relPath.endsWith(".md"));
1899
1950
  const claimedPaths = /* @__PURE__ */ new Set();
1900
1951
  const transcriptMeta = /* @__PURE__ */ new Map();
1901
1952
  for (const t of transcripts) {
@@ -1904,11 +1955,12 @@ async function runStale(input) {
1904
1955
  const fm = extractFrontmatter(content);
1905
1956
  let kind = fm.ok && typeof fm.data.kind === "string" ? fm.data.kind : "";
1906
1957
  let project = fm.ok && typeof fm.data.project === "string" ? fm.data.project : "";
1958
+ if (input.project && !project.includes(input.project)) continue;
1907
1959
  let inferred = false;
1908
1960
  if (input.forceScan && !kind) {
1909
- const basename = t.relPath.split("/").pop();
1910
- if (!LOOP_CYCLE_PATTERN.test(basename)) {
1911
- const m = basename.match(KIND_FROM_FILENAME);
1961
+ const basename2 = t.relPath.split("/").pop();
1962
+ if (!LOOP_CYCLE_PATTERN.test(basename2)) {
1963
+ const m = basename2.match(KIND_FROM_FILENAME);
1912
1964
  if (m) {
1913
1965
  kind = m[1];
1914
1966
  inferred = true;
@@ -1948,7 +2000,7 @@ async function runStale(input) {
1948
2000
  const overlap = wWords.filter((w) => tWords.has(w)).length;
1949
2001
  if (dirName.includes(tSlug) || tSlug.includes(wSlug) || overlap >= 1) {
1950
2002
  claimedPaths.add(t.relPath);
1951
- if (status === "done" || status === "invalid") {
2003
+ if (TERMINAL_STATUSES.has(status)) {
1952
2004
  staleTranscripts.push({ path: t.relPath, reason: `work item projects/${slug}/work/${dirName} is ${status}` });
1953
2005
  }
1954
2006
  break;
@@ -1958,7 +2010,7 @@ async function runStale(input) {
1958
2010
  for (const [dir, status] of workDirs) {
1959
2011
  if (dir.split("/").pop().startsWith(datePrefix)) {
1960
2012
  claimedPaths.add(t.relPath);
1961
- if (status === "done" || status === "invalid") {
2013
+ if (TERMINAL_STATUSES.has(status)) {
1962
2014
  staleTranscripts.push({ path: t.relPath, reason: `work item ${dir} is ${status}` });
1963
2015
  }
1964
2016
  break;
@@ -2003,10 +2055,8 @@ async function runStale(input) {
2003
2055
  continue;
2004
2056
  }
2005
2057
  const hasSpec = files.includes("spec.md"), hasPlan = files.includes("plan.md"), hasWI = files.includes("work-item.md");
2006
- if (status === "done") {
2007
- doneWorkItems.push({ path: relDir, reason: "completed \u2014 should be archived" });
2008
- } else if (status === "invalid") {
2009
- doneWorkItems.push({ path: relDir, reason: "invalid \u2014 should be archived" });
2058
+ if (TERMINAL_STATUSES.has(status)) {
2059
+ doneWorkItems.push({ path: relDir, reason: `${status || "completed"} \u2014 should be archived` });
2010
2060
  } else if (hasSpec && !hasPlan) {
2011
2061
  incompleteWorkItems.push({ path: relDir, reason: "has spec but no plan" });
2012
2062
  } else if (hasWI && !hasSpec && !hasPlan) {
@@ -2019,16 +2069,53 @@ async function runStale(input) {
2019
2069
  const text = await readFile10(join13(input.vault, page.relPath), "utf8");
2020
2070
  const fm = extractFrontmatter(text);
2021
2071
  if (fm.ok && typeof fm.data.updated === "string") {
2072
+ if (input.project) {
2073
+ const pp = fm.data.provenance_projects;
2074
+ const linked = Array.isArray(pp) && pp.some((p) => String(p).includes(input.project));
2075
+ if (!linked) continue;
2076
+ }
2022
2077
  const age = daysSince(fm.data.updated);
2023
- if (age >= input.days) {
2024
- stale.push({ page: page.relPath, reason: `updated ${age} days ago (threshold: ${input.days})` });
2078
+ const threshold = typeof fm.data.stale_ttl === "number" && fm.data.stale_ttl > 0 ? fm.data.stale_ttl : input.days;
2079
+ if (age >= threshold) {
2080
+ stale.push({ page: page.relPath, reason: `updated ${age} days ago (threshold: ${threshold})` });
2025
2081
  }
2026
2082
  }
2027
2083
  } catch {
2028
2084
  }
2029
2085
  }
2086
+ const staleSections = [];
2087
+ for (const page of scan.data.typedKnowledge) {
2088
+ try {
2089
+ const text = await readFile10(join13(input.vault, page.relPath), "utf8");
2090
+ const projectFilter = input.project;
2091
+ if (projectFilter) {
2092
+ const fm = extractFrontmatter(text);
2093
+ if (fm.ok) {
2094
+ const pp = fm.data.provenance_projects;
2095
+ const linked = Array.isArray(pp) && pp.some((p) => String(p).includes(projectFilter));
2096
+ if (!linked) continue;
2097
+ }
2098
+ }
2099
+ const annotations = parseExpiryAnnotations(text, page.relPath);
2100
+ for (const ann of annotations) {
2101
+ if (daysSince(ann.expires) >= 0) {
2102
+ staleSections.push({
2103
+ page: ann.page,
2104
+ heading: ann.heading,
2105
+ line: ann.line,
2106
+ expires: ann.expires,
2107
+ refresh: ann.refresh,
2108
+ source: ann.source,
2109
+ reason: `section "${ann.heading}" expired on ${ann.expires}`
2110
+ });
2111
+ }
2112
+ }
2113
+ } catch {
2114
+ }
2115
+ }
2116
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2030
2117
  if (input.archive) {
2031
- const archiveDir = join13(input.vault, "_archive", (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
2118
+ const archiveDir = join13(input.vault, "_archive", today);
2032
2119
  await mkdir6(archiveDir, { recursive: true });
2033
2120
  const citedRawPaths = /* @__PURE__ */ new Set();
2034
2121
  for (const page of scan.data.typedKnowledge) {
@@ -2082,7 +2169,7 @@ async function runStale(input) {
2082
2169
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2083
2170
  });
2084
2171
  }
2085
- const total = stale.length + staleTranscripts.length + unclaimedTranscripts.length + incompleteWorkItems.length + doneWorkItems.length;
2172
+ const total = stale.length + staleTranscripts.length + unclaimedTranscripts.length + incompleteWorkItems.length + doneWorkItems.length + staleSections.length;
2086
2173
  const hintLines = [];
2087
2174
  if (stale.length > 0) hintLines.push(`stale_pages: ${stale.length}`, ...stale.map((p) => ` ${p.page}: ${p.reason}`));
2088
2175
  if (staleTranscripts.length > 0) hintLines.push(`stale_transcripts: ${staleTranscripts.length}`, ...staleTranscripts.map((t) => ` ${t.path}: ${t.reason}`));
@@ -2090,6 +2177,7 @@ async function runStale(input) {
2090
2177
  hint: ${t.hint}` : ""}`));
2091
2178
  if (incompleteWorkItems.length > 0) hintLines.push(`incomplete_work_items: ${incompleteWorkItems.length}`, ...incompleteWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
2092
2179
  if (doneWorkItems.length > 0) hintLines.push(`done_work_items: ${doneWorkItems.length}`, ...doneWorkItems.map((w) => ` ${w.path}: ${w.reason}`));
2180
+ if (staleSections.length > 0) hintLines.push(`stale_sections: ${staleSections.length}`, ...staleSections.map((s) => ` ${s.page}#${s.heading}: ${s.reason}`));
2093
2181
  if (archived.length > 0) hintLines.push(`archived: ${archived.length}`, ...archived.map((a) => ` ${a}`));
2094
2182
  if (hintLines.length === 0) hintLines.push("no stale transcripts or incomplete work items");
2095
2183
  return { exitCode: total > 0 ? ExitCode.STALE_PAGE : ExitCode.OK, result: ok({
@@ -2098,6 +2186,7 @@ async function runStale(input) {
2098
2186
  unclaimed_transcripts: unclaimedTranscripts,
2099
2187
  incomplete_work_items: incompleteWorkItems,
2100
2188
  done_work_items: doneWorkItems,
2189
+ stale_sections: staleSections,
2101
2190
  archived,
2102
2191
  humanHint: hintLines.join("\n")
2103
2192
  }) };
@@ -2274,8 +2363,8 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
2274
2363
 
2275
2364
  // src/commands/lint.ts
2276
2365
  import { existsSync as existsSync3 } from "fs";
2277
- import { readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
2278
- import { join as join18 } from "path";
2366
+ import { readFile as readFile15 } from "fs/promises";
2367
+ import { join as join19 } from "path";
2279
2368
 
2280
2369
  // src/commands/topic-map-check.ts
2281
2370
  var DEFAULT_THRESHOLD = 200;
@@ -2402,6 +2491,81 @@ async function runDedup(input) {
2402
2491
  };
2403
2492
  }
2404
2493
 
2494
+ // src/utils/safe-write.ts
2495
+ import { open, readFile as readFile14, rename as rename4, unlink as unlink2, writeFile as writeFile8 } from "fs/promises";
2496
+ import { randomBytes } from "crypto";
2497
+ import { dirname as dirname7, basename, join as join18 } from "path";
2498
+ var DEFAULT_MIN_BODY_RATIO = 0.5;
2499
+ var DEFAULT_MIN_OLD_BODY_BYTES = 200;
2500
+ function bodyBytes(text) {
2501
+ const split = splitFrontmatter(text);
2502
+ if (!split.ok) return Buffer.byteLength(text, "utf8");
2503
+ return Buffer.byteLength(split.data.body, "utf8");
2504
+ }
2505
+ async function readIfExists(absPath) {
2506
+ try {
2507
+ return await readFile14(absPath, "utf8");
2508
+ } catch (e) {
2509
+ if (e.code === "ENOENT") return null;
2510
+ throw e;
2511
+ }
2512
+ }
2513
+ async function safeWritePage(absPath, newContent, opts = {}) {
2514
+ const minRatio = opts.minBodyRatio === void 0 ? DEFAULT_MIN_BODY_RATIO : opts.minBodyRatio;
2515
+ const minOldBytes = opts.minOldBodyBytes ?? DEFAULT_MIN_OLD_BODY_BYTES;
2516
+ let oldContent;
2517
+ try {
2518
+ oldContent = await readIfExists(absPath);
2519
+ } catch (e) {
2520
+ return err("WRITE_FAILED", { path: absPath, phase: "read-existing", message: String(e) });
2521
+ }
2522
+ const isNew = oldContent === null;
2523
+ const oldBodyBytes = isNew ? 0 : bodyBytes(oldContent);
2524
+ const newBodyBytes = bodyBytes(newContent);
2525
+ const bodyRatio = oldBodyBytes > 0 ? newBodyBytes / oldBodyBytes : null;
2526
+ let guardSkippedSmall = false;
2527
+ if (!isNew && minRatio !== null && bodyRatio !== null && bodyRatio < minRatio) {
2528
+ if (oldBodyBytes < minOldBytes) {
2529
+ guardSkippedSmall = true;
2530
+ } else {
2531
+ return err("BODY_TRUNCATION_GUARD", {
2532
+ path: absPath,
2533
+ oldBodyBytes,
2534
+ newBodyBytes,
2535
+ bodyRatio,
2536
+ minBodyRatio: minRatio,
2537
+ hint: "Refusing to write \u2014 new body lost too much content. Likely a parse-modify-serialize bug or a write race. Verify the page source before retrying."
2538
+ });
2539
+ }
2540
+ }
2541
+ if (!isNew && oldContent === newContent) {
2542
+ return ok({ isNew: false, oldBodyBytes, newBodyBytes, bodyRatio, guardSkippedSmall });
2543
+ }
2544
+ const dir = dirname7(absPath);
2545
+ const tmpName = `.${basename(absPath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
2546
+ const tmpPath = join18(dir, tmpName);
2547
+ try {
2548
+ const handle = await open(tmpPath, "w");
2549
+ try {
2550
+ await handle.writeFile(newContent, "utf8");
2551
+ try {
2552
+ await handle.sync();
2553
+ } catch {
2554
+ }
2555
+ } finally {
2556
+ await handle.close();
2557
+ }
2558
+ await rename4(tmpPath, absPath);
2559
+ return ok({ isNew, oldBodyBytes, newBodyBytes, bodyRatio, guardSkippedSmall });
2560
+ } catch (e) {
2561
+ try {
2562
+ await unlink2(tmpPath);
2563
+ } catch {
2564
+ }
2565
+ return err("WRITE_FAILED", { path: absPath, phase: "atomic-write", message: String(e) });
2566
+ }
2567
+ }
2568
+
2405
2569
  // src/commands/raw-body-dedup.ts
2406
2570
  import { createHash as createHash2 } from "crypto";
2407
2571
  async function runRawBodyDedup(vault) {
@@ -2466,11 +2630,11 @@ function buildCliSurface() {
2466
2630
  program2.command("index-check").option("--wiki <name>");
2467
2631
  program2.command("index-link-format").option("--wiki <name>");
2468
2632
  program2.command("topic-map-check").option("--threshold <n>").option("--wiki <name>");
2469
- program2.command("stale").option("--archive").option("--days <n>").option("--force-scan").option("--wiki <name>");
2633
+ program2.command("stale").option("--archive").option("--days <n>").option("--force-scan").option("--project <slug>").option("--refresh").option("--wiki <name>");
2470
2634
  program2.command("claim").option("--project <slug>").option("--slug <slug>").option("--wiki <name>");
2471
2635
  program2.command("pagesize").option("--lines <n>").option("--wiki <name>");
2472
2636
  program2.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
2473
- program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--wiki <name>");
2637
+ program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--wiki <name>");
2474
2638
  program2.command("config");
2475
2639
  program2.command("doctor");
2476
2640
  program2.command("status").option("--wiki <name>");
@@ -2615,8 +2779,8 @@ function extractSourceEntries(rawFm) {
2615
2779
  return entries;
2616
2780
  }
2617
2781
  var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy"];
2618
- var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "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"];
2619
- var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "missing_diagram", "cli_refs"];
2782
+ var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "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"];
2783
+ var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
2620
2784
  async function runLint(input) {
2621
2785
  const buckets = {};
2622
2786
  const fixed = [];
@@ -2725,7 +2889,7 @@ async function runLint(input) {
2725
2889
  let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
2726
2890
  rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
2727
2891
  if (!rawPath.startsWith("raw/") && !rawPath.startsWith("_archive/raw/")) continue;
2728
- if (!existsSync3(join18(input.vault, rawPath)) && !existsSync3(join18(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync3(join18(input.vault, "_archive", rawPath)) && !existsSync3(join18(input.vault, "_archive", rawPath + ".md"))) {
2892
+ if (!existsSync3(join19(input.vault, rawPath)) && !existsSync3(join19(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync3(join19(input.vault, "_archive", rawPath)) && !existsSync3(join19(input.vault, "_archive", rawPath + ".md"))) {
2729
2893
  brokenSourceFlags.push(`${page.relPath}: ${rawPath}`);
2730
2894
  }
2731
2895
  }
@@ -2785,7 +2949,7 @@ async function runLint(input) {
2785
2949
  const text = await readPage(specPage);
2786
2950
  const fm = extractFrontmatter(text);
2787
2951
  if (fm.ok) {
2788
- specStatus = fm.data.status;
2952
+ specStatus = typeof fm.data.status === "string" ? fm.data.status : void 0;
2789
2953
  specStarted = fm.data.started;
2790
2954
  }
2791
2955
  }
@@ -2816,11 +2980,11 @@ async function runLint(input) {
2816
2980
  const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
2817
2981
  if (!slugMatch) continue;
2818
2982
  const slug = slugMatch[1];
2819
- const knowledgePath = join18(input.vault, "projects", slug, "knowledge.md");
2983
+ const knowledgePath = join19(input.vault, "projects", slug, "knowledge.md");
2820
2984
  if (!existsSync3(knowledgePath)) continue;
2821
2985
  const pageRef = page.relPath.replace(/\.md$/, "");
2822
2986
  try {
2823
- const knowledgeContent = await readFile14(knowledgePath, "utf8");
2987
+ const knowledgeContent = await readFile15(knowledgePath, "utf8");
2824
2988
  if (!knowledgeContent.includes(`[[${pageRef}]]`)) {
2825
2989
  orphanedProjectPages.push(`${page.relPath}: not in projects/${slug}/knowledge.md`);
2826
2990
  }
@@ -2840,13 +3004,34 @@ async function runLint(input) {
2840
3004
  }
2841
3005
  }
2842
3006
  if (cliRefFlags.length > 0) buckets.cli_refs = cliRefFlags;
3007
+ const staleSectionFlags = [];
3008
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3009
+ const approachingThreshold = 7;
3010
+ for (const page of scan.data.typedKnowledge) {
3011
+ try {
3012
+ const text = await readPage(page);
3013
+ const annotations = parseExpiryAnnotations(text, page.relPath);
3014
+ for (const ann of annotations) {
3015
+ if (ann.expires < today) {
3016
+ staleSectionFlags.push(`${page.relPath}: section "${ann.heading}" expired on ${ann.expires}`);
3017
+ } else {
3018
+ const daysUntilExpiry = Math.floor((Date.parse(ann.expires) - Date.now()) / 864e5);
3019
+ if (daysUntilExpiry <= approachingThreshold) {
3020
+ staleSectionFlags.push(`${page.relPath}: section "${ann.heading}" expires in ${daysUntilExpiry} day(s) (${ann.expires})`);
3021
+ }
3022
+ }
3023
+ }
3024
+ } catch {
3025
+ }
3026
+ }
3027
+ if (staleSectionFlags.length > 0) buckets.stale_sections = staleSectionFlags;
2843
3028
  if (input.fix && legacyPages.length > 0) {
2844
3029
  const FENCE_RE2 = /```[\s\S]*?```/g;
2845
3030
  const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
2846
3031
  for (const relPath of legacyPages) {
2847
3032
  try {
2848
3033
  const absPath = `${input.vault}/${relPath}`;
2849
- const raw = await readFile14(absPath, "utf8");
3034
+ const raw = await readFile15(absPath, "utf8");
2850
3035
  const split = splitFrontmatter(raw);
2851
3036
  if (!split.ok) {
2852
3037
  unresolved.push(relPath);
@@ -2924,7 +3109,11 @@ async function runLint(input) {
2924
3109
  ${rawFm}
2925
3110
  ---
2926
3111
  ${newBody}`;
2927
- await writeFile8(absPath, newContent, "utf8");
3112
+ const w = await safeWritePage(absPath, newContent);
3113
+ if (!w.ok) {
3114
+ unresolved.push(relPath);
3115
+ continue;
3116
+ }
2928
3117
  fixed.push(relPath);
2929
3118
  } catch {
2930
3119
  unresolved.push(relPath);
@@ -2941,7 +3130,7 @@ ${newBody}`;
2941
3130
  for (const relPath of noOverview) {
2942
3131
  try {
2943
3132
  const absPath = `${input.vault}/${relPath}`;
2944
- const raw = await readFile14(absPath, "utf8");
3133
+ const raw = await readFile15(absPath, "utf8");
2945
3134
  const split = splitFrontmatter(raw);
2946
3135
  if (!split.ok) {
2947
3136
  unresolved.push(relPath);
@@ -2962,7 +3151,11 @@ ${rawFm}
2962
3151
  ${overviewSection}
2963
3152
 
2964
3153
  ${trimmedBody}`;
2965
- await writeFile8(absPath, newContent, "utf8");
3154
+ const w = await safeWritePage(absPath, newContent);
3155
+ if (!w.ok) {
3156
+ unresolved.push(relPath);
3157
+ continue;
3158
+ }
2966
3159
  fixed.push(relPath);
2967
3160
  } catch {
2968
3161
  unresolved.push(relPath);
@@ -2978,7 +3171,7 @@ ${trimmedBody}`;
2978
3171
  for (const relPath of missingTldrFlags) {
2979
3172
  try {
2980
3173
  const absPath = `${input.vault}/${relPath}`;
2981
- const raw = await readFile14(absPath, "utf8");
3174
+ const raw = await readFile15(absPath, "utf8");
2982
3175
  const split = splitFrontmatter(raw);
2983
3176
  if (!split.ok) {
2984
3177
  unresolved.push(relPath);
@@ -3006,7 +3199,11 @@ ${trimmedBody}`;
3006
3199
  const newContent = `---
3007
3200
  ${trimmedFm}---
3008
3201
  ${lines.join("\n")}`;
3009
- await writeFile8(absPath, newContent, "utf8");
3202
+ const w = await safeWritePage(absPath, newContent);
3203
+ if (!w.ok) {
3204
+ unresolved.push(relPath);
3205
+ continue;
3206
+ }
3010
3207
  fixed.push(relPath);
3011
3208
  } catch {
3012
3209
  unresolved.push(relPath);
@@ -3024,7 +3221,7 @@ ${lines.join("\n")}`;
3024
3221
  for (const relPath of wikilinkCitationFlags) {
3025
3222
  try {
3026
3223
  const absPath = `${input.vault}/${relPath}`;
3027
- const raw = await readFile14(absPath, "utf8");
3224
+ const raw = await readFile15(absPath, "utf8");
3028
3225
  const split = splitFrontmatter(raw);
3029
3226
  if (!split.ok) {
3030
3227
  unresolved.push(relPath);
@@ -3088,7 +3285,11 @@ ${lines.join("\n")}`;
3088
3285
  ${rawFm}
3089
3286
  ---
3090
3287
  ${newBody}`;
3091
- await writeFile8(absPath, newContent, "utf8");
3288
+ const w = await safeWritePage(absPath, newContent);
3289
+ if (!w.ok) {
3290
+ unresolved.push(relPath);
3291
+ continue;
3292
+ }
3092
3293
  wikilinkFixed.push(relPath);
3093
3294
  } catch {
3094
3295
  unresolved.push(relPath);
@@ -3106,6 +3307,38 @@ ${newBody}`;
3106
3307
  const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
3107
3308
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
3108
3309
  const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
3310
+ if (input.only) {
3311
+ const allKnown = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
3312
+ if (!allKnown.includes(input.only)) {
3313
+ return {
3314
+ exitCode: ExitCode.USAGE,
3315
+ result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${allKnown.join(", ")}` }
3316
+ };
3317
+ }
3318
+ const match = [...errorOut, ...warningOut, ...infoOut].filter((b) => b.kind === input.only);
3319
+ const severity = ERROR_ORDER.includes(input.only) ? "error" : WARNING_ORDER.includes(input.only) ? "warning" : "info";
3320
+ const filtered = severity === "error" ? { error: match, warning: [], info: [] } : severity === "warning" ? { error: [], warning: match, info: [] } : { error: [], warning: [], info: match };
3321
+ const fSummary = {
3322
+ errors: filtered.error.reduce((n, b) => n + b.items.length, 0),
3323
+ warnings: filtered.warning.reduce((n, b) => n + b.items.length, 0),
3324
+ info: filtered.info.reduce((n, b) => n + b.items.length, 0)
3325
+ };
3326
+ let fExit = ExitCode.OK;
3327
+ if (fSummary.errors > 0) fExit = ExitCode.LINT_HAS_ERRORS;
3328
+ else if (fSummary.warnings > 0 || fSummary.info > 0) fExit = ExitCode.LINT_HAS_WARNINGS;
3329
+ return {
3330
+ exitCode: fExit,
3331
+ result: ok({
3332
+ vault: { path: input.vault, source: input.source ?? "resolved" },
3333
+ summary: fSummary,
3334
+ by_severity: filtered,
3335
+ fixed,
3336
+ unresolved,
3337
+ humanHint: `--only ${input.only}
3338
+ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")}`
3339
+ })
3340
+ };
3341
+ }
3109
3342
  const summary = {
3110
3343
  errors: errorOut.reduce((n, b) => n + b.items.length, 0),
3111
3344
  warnings: warningOut.reduce((n, b) => n + b.items.length, 0),
@@ -3145,14 +3378,14 @@ ${newBody}`;
3145
3378
  }
3146
3379
 
3147
3380
  // src/commands/config.ts
3148
- import { readFile as readFile15 } from "fs/promises";
3381
+ import { readFile as readFile16 } from "fs/promises";
3149
3382
  import { existsSync as existsSync4 } from "fs";
3150
- import { join as join19 } from "path";
3383
+ import { join as join20 } from "path";
3151
3384
  function validateKey(key) {
3152
3385
  return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
3153
3386
  }
3154
3387
  function configPath(home) {
3155
- return join19(home, ".skillwiki", ".env");
3388
+ return join20(home, ".skillwiki", ".env");
3156
3389
  }
3157
3390
  async function runConfigGet(input) {
3158
3391
  if (!validateKey(input.key)) {
@@ -3170,7 +3403,7 @@ async function runConfigSet(input) {
3170
3403
  try {
3171
3404
  let originalContent;
3172
3405
  try {
3173
- originalContent = await readFile15(filePath, "utf8");
3406
+ originalContent = await readFile16(filePath, "utf8");
3174
3407
  } catch {
3175
3408
  }
3176
3409
  const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
@@ -3207,13 +3440,13 @@ async function runConfigPath(input) {
3207
3440
 
3208
3441
  // src/commands/doctor.ts
3209
3442
  import { existsSync as existsSync6, lstatSync, readlinkSync, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
3210
- import { join as join22, resolve as resolve4 } from "path";
3443
+ import { join as join23, resolve as resolve4 } from "path";
3211
3444
  import { execSync } from "child_process";
3212
3445
  import { platform } from "os";
3213
3446
 
3214
3447
  // src/utils/auto-update.ts
3215
3448
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
3216
- import { join as join20, dirname as dirname8 } from "path";
3449
+ import { join as join21, dirname as dirname8 } from "path";
3217
3450
  import { spawn } from "child_process";
3218
3451
 
3219
3452
  // src/utils/update-consts.ts
@@ -3224,7 +3457,7 @@ var CLI_DISABLE_FLAG = "--no-update-notifier";
3224
3457
 
3225
3458
  // src/utils/auto-update.ts
3226
3459
  function cachePath(home) {
3227
- return join20(home, ".skillwiki", CACHE_FILENAME);
3460
+ return join21(home, ".skillwiki", CACHE_FILENAME);
3228
3461
  }
3229
3462
  function readCacheRaw(home) {
3230
3463
  try {
@@ -3274,12 +3507,12 @@ function triggerAutoUpdate(home, currentVersion) {
3274
3507
 
3275
3508
  // src/utils/plugin-registry.ts
3276
3509
  import { readFileSync as readFileSync5 } from "fs";
3277
- import { join as join21 } from "path";
3278
- var REGISTRY_PATH = join21(".claude", "plugins", "installed_plugins.json");
3510
+ import { join as join22 } from "path";
3511
+ var REGISTRY_PATH = join22(".claude", "plugins", "installed_plugins.json");
3279
3512
  var PLUGIN_KEY = "skillwiki@llm-wiki";
3280
3513
  function readInstalledPlugins(home) {
3281
3514
  try {
3282
- const raw = readFileSync5(join21(home, REGISTRY_PATH), "utf8");
3515
+ const raw = readFileSync5(join22(home, REGISTRY_PATH), "utf8");
3283
3516
  return JSON.parse(raw);
3284
3517
  } catch {
3285
3518
  return null;
@@ -3322,12 +3555,12 @@ function detectCliChannels(argv, home) {
3322
3555
  }
3323
3556
  const plugin = findPlugin(home);
3324
3557
  if (plugin) {
3325
- const pluginBin = join22(plugin.installPath, "bin", "skillwiki");
3558
+ const pluginBin = join23(plugin.installPath, "bin", "skillwiki");
3326
3559
  if (existsSync6(pluginBin)) {
3327
3560
  channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
3328
3561
  }
3329
3562
  }
3330
- const installBin = join22(home, ".claude", "skills", "bin", "skillwiki");
3563
+ const installBin = join23(home, ".claude", "skills", "bin", "skillwiki");
3331
3564
  if (existsSync6(installBin)) {
3332
3565
  channels.push({ name: "install", path: installBin, isDevLink: false });
3333
3566
  }
@@ -3408,9 +3641,9 @@ function checkVaultStructure(resolvedPath) {
3408
3641
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
3409
3642
  }
3410
3643
  const missing = [];
3411
- if (!existsSync6(join22(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
3644
+ if (!existsSync6(join23(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
3412
3645
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
3413
- if (!existsSync6(join22(resolvedPath, dir))) missing.push(dir + "/");
3646
+ if (!existsSync6(join23(resolvedPath, dir))) missing.push(dir + "/");
3414
3647
  }
3415
3648
  if (missing.length === 0) {
3416
3649
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -3418,7 +3651,7 @@ function checkVaultStructure(resolvedPath) {
3418
3651
  return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
3419
3652
  }
3420
3653
  function checkSkillsInstalled(home, cwd) {
3421
- const srcDir = cwd ? join22(cwd, "packages", "skills") : void 0;
3654
+ const srcDir = cwd ? join23(cwd, "packages", "skills") : void 0;
3422
3655
  if (srcDir && existsSync6(srcDir)) {
3423
3656
  const found = findSkillMd(srcDir);
3424
3657
  if (found.length > 0) {
@@ -3432,7 +3665,7 @@ function checkSkillsInstalled(home, cwd) {
3432
3665
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
3433
3666
  }
3434
3667
  }
3435
- const skillsDir = join22(home, ".claude", "skills");
3668
+ const skillsDir = join23(home, ".claude", "skills");
3436
3669
  if (existsSync6(skillsDir)) {
3437
3670
  const found = findSkillMd(skillsDir);
3438
3671
  if (found.length > 0) {
@@ -3443,10 +3676,10 @@ function checkSkillsInstalled(home, cwd) {
3443
3676
  }
3444
3677
  function checkDuplicateSkills(home) {
3445
3678
  const plugin = findPlugin(home);
3446
- const skillsDir = join22(home, ".claude", "skills");
3679
+ const skillsDir = join23(home, ".claude", "skills");
3447
3680
  const agentSkillDirs = [
3448
- { label: "~/.codex/skills/", path: join22(home, ".codex", "skills") },
3449
- { label: "~/.agents/skills/", path: join22(home, ".agents", "skills") }
3681
+ { label: "~/.codex/skills/", path: join23(home, ".codex", "skills") },
3682
+ { label: "~/.agents/skills/", path: join23(home, ".agents", "skills") }
3450
3683
  ];
3451
3684
  if (!plugin) {
3452
3685
  return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
@@ -3523,7 +3756,7 @@ async function checkProfiles(home) {
3523
3756
  }
3524
3757
  async function checkProjectLocalOverride(cwd) {
3525
3758
  const dir = cwd ?? process.cwd();
3526
- const envPath = join22(dir, ".skillwiki", ".env");
3759
+ const envPath = join23(dir, ".skillwiki", ".env");
3527
3760
  if (existsSync6(envPath)) {
3528
3761
  return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
3529
3762
  }
@@ -3533,7 +3766,7 @@ function checkVaultGitRemote(resolvedPath) {
3533
3766
  if (resolvedPath === void 0) {
3534
3767
  return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
3535
3768
  }
3536
- if (!existsSync6(join22(resolvedPath, ".git"))) {
3769
+ if (!existsSync6(join23(resolvedPath, ".git"))) {
3537
3770
  return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
3538
3771
  }
3539
3772
  try {
@@ -3556,9 +3789,9 @@ function checkObsidianTemplates(resolvedPath) {
3556
3789
  return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
3557
3790
  }
3558
3791
  const missing = [];
3559
- if (!existsSync6(join22(resolvedPath, "_Templates"))) missing.push("_Templates/");
3560
- if (!existsSync6(join22(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
3561
- if (!existsSync6(join22(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
3792
+ if (!existsSync6(join23(resolvedPath, "_Templates"))) missing.push("_Templates/");
3793
+ if (!existsSync6(join23(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
3794
+ if (!existsSync6(join23(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
3562
3795
  if (missing.length === 0) {
3563
3796
  return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
3564
3797
  }
@@ -3568,7 +3801,7 @@ function checkDotStoreClean(resolvedPath) {
3568
3801
  if (resolvedPath === void 0) {
3569
3802
  return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
3570
3803
  }
3571
- const rawDir = join22(resolvedPath, "raw");
3804
+ const rawDir = join23(resolvedPath, "raw");
3572
3805
  if (!existsSync6(rawDir)) {
3573
3806
  return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
3574
3807
  }
@@ -3584,7 +3817,7 @@ function checkDotStoreClean(resolvedPath) {
3584
3817
  if (entry.name === ".DS_Store") {
3585
3818
  found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
3586
3819
  } else if (entry.isDirectory()) {
3587
- walk2(join22(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
3820
+ walk2(join23(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
3588
3821
  }
3589
3822
  }
3590
3823
  })(rawDir, "");
@@ -3597,7 +3830,7 @@ function checkSyncLastPush(resolvedPath) {
3597
3830
  if (resolvedPath === void 0) {
3598
3831
  return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
3599
3832
  }
3600
- if (!existsSync6(join22(resolvedPath, ".git"))) {
3833
+ if (!existsSync6(join23(resolvedPath, ".git"))) {
3601
3834
  return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
3602
3835
  }
3603
3836
  let timestamp;
@@ -3671,7 +3904,7 @@ function checkS3MountPerf(resolvedPath) {
3671
3904
  if (!mountPoint) {
3672
3905
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
3673
3906
  }
3674
- const conceptsDir = join22(resolvedPath, "concepts");
3907
+ const conceptsDir = join23(resolvedPath, "concepts");
3675
3908
  if (!existsSync6(conceptsDir)) {
3676
3909
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
3677
3910
  }
@@ -3721,9 +3954,9 @@ function findSkillMd(dir) {
3721
3954
  }
3722
3955
  for (const entry of entries) {
3723
3956
  if (entry.isFile() && entry.name === "SKILL.md") {
3724
- results.push(join22(dir, entry.name));
3957
+ results.push(join23(dir, entry.name));
3725
3958
  } else if (entry.isDirectory()) {
3726
- results.push(...findSkillMd(join22(dir, entry.name)));
3959
+ results.push(...findSkillMd(join23(dir, entry.name)));
3727
3960
  }
3728
3961
  }
3729
3962
  return results;
@@ -3737,7 +3970,7 @@ function findSkillNames(dir) {
3737
3970
  return results;
3738
3971
  }
3739
3972
  for (const entry of entries) {
3740
- if (entry.isDirectory() && existsSync6(join22(dir, entry.name, "SKILL.md"))) {
3973
+ if (entry.isDirectory() && existsSync6(join23(dir, entry.name, "SKILL.md"))) {
3741
3974
  results.push(entry.name);
3742
3975
  }
3743
3976
  }
@@ -3791,8 +4024,8 @@ async function runDoctor(input) {
3791
4024
  }
3792
4025
 
3793
4026
  // src/commands/archive.ts
3794
- import { rename as rename4, mkdir as mkdir8, readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
3795
- import { join as join23, dirname as dirname9 } from "path";
4027
+ import { rename as rename5, mkdir as mkdir8, readFile as readFile17, writeFile as writeFile9 } from "fs/promises";
4028
+ import { join as join24, dirname as dirname9 } from "path";
3796
4029
  async function runArchive(input) {
3797
4030
  const scan = await scanVault(input.vault);
3798
4031
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -3808,13 +4041,13 @@ async function runArchive(input) {
3808
4041
  }
3809
4042
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
3810
4043
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
3811
- const archivePath = join23("_archive", relPath).replace(/\\/g, "/");
3812
- await mkdir8(dirname9(join23(input.vault, archivePath)), { recursive: true });
4044
+ const archivePath = join24("_archive", relPath).replace(/\\/g, "/");
4045
+ await mkdir8(dirname9(join24(input.vault, archivePath)), { recursive: true });
3813
4046
  let indexUpdated = false;
3814
4047
  if (!isRaw) {
3815
- const indexPath = join23(input.vault, "index.md");
4048
+ const indexPath = join24(input.vault, "index.md");
3816
4049
  try {
3817
- const idx = await readFile16(indexPath, "utf8");
4050
+ const idx = await readFile17(indexPath, "utf8");
3818
4051
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
3819
4052
  const originalLines = idx.split("\n");
3820
4053
  const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
@@ -3826,7 +4059,7 @@ async function runArchive(input) {
3826
4059
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
3827
4060
  }
3828
4061
  }
3829
- await rename4(join23(input.vault, relPath), join23(input.vault, archivePath));
4062
+ await rename5(join24(input.vault, relPath), join24(input.vault, archivePath));
3830
4063
  appendLastOp(input.vault, {
3831
4064
  operation: "archive",
3832
4065
  summary: `moved ${relPath} to ${archivePath}`,
@@ -3838,7 +4071,6 @@ async function runArchive(input) {
3838
4071
 
3839
4072
  // src/commands/drift.ts
3840
4073
  import { createHash as createHash3 } from "crypto";
3841
- import { writeFile as writeFile10 } from "fs/promises";
3842
4074
 
3843
4075
  // src/utils/fetch.ts
3844
4076
  async function controlledFetch(url, opts) {
@@ -3925,7 +4157,7 @@ async function runDrift(input) {
3925
4157
  ${newFm}
3926
4158
  ---
3927
4159
  ${body}`;
3928
- await writeFile10(raw.absPath, newText, "utf8");
4160
+ await safeWritePage(raw.absPath, newText);
3929
4161
  results.push({
3930
4162
  raw_path: raw.relPath,
3931
4163
  source_url: sourceUrl,
@@ -3968,7 +4200,6 @@ ${body}`;
3968
4200
  }
3969
4201
 
3970
4202
  // src/commands/migrate-citations.ts
3971
- import { writeFile as writeFile11 } from "fs/promises";
3972
4203
  var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
3973
4204
  function moveMarkersToParagraphEnd(body) {
3974
4205
  const lines = body.split("\n");
@@ -4091,7 +4322,11 @@ ${migratedBody}${newFooter}`;
4091
4322
  continue;
4092
4323
  }
4093
4324
  if (!input.dryRun) {
4094
- await writeFile11(page.absPath, newText, "utf8");
4325
+ const w = await safeWritePage(page.absPath, newText);
4326
+ if (!w.ok) {
4327
+ skipped.push(page.relPath);
4328
+ continue;
4329
+ }
4095
4330
  }
4096
4331
  migrated.push(page.relPath);
4097
4332
  }
@@ -4121,7 +4356,6 @@ ${migratedBody}${newFooter}`;
4121
4356
  }
4122
4357
 
4123
4358
  // src/commands/frontmatter-fix.ts
4124
- import { writeFile as writeFile12 } from "fs/promises";
4125
4359
  function isoToday() {
4126
4360
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4127
4361
  }
@@ -4163,7 +4397,11 @@ ${newBody}`;
4163
4397
  continue;
4164
4398
  }
4165
4399
  if (!input.dryRun) {
4166
- await writeFile12(page.absPath, newText, "utf8");
4400
+ const w = await safeWritePage(page.absPath, newText);
4401
+ if (!w.ok) {
4402
+ skipped.push(page.relPath);
4403
+ continue;
4404
+ }
4167
4405
  }
4168
4406
  fixed.push(page.relPath);
4169
4407
  }
@@ -4196,14 +4434,14 @@ ${newBody}`;
4196
4434
  // src/commands/update.ts
4197
4435
  import { execSync as execSync2 } from "child_process";
4198
4436
  import { readFileSync as readFileSync7 } from "fs";
4199
- import { join as join24 } from "path";
4437
+ import { join as join25 } from "path";
4200
4438
  function resolveGlobalSkillsRoot() {
4201
4439
  try {
4202
4440
  const globalRoot = execSync2("npm root -g", {
4203
4441
  encoding: "utf8",
4204
4442
  timeout: 5e3
4205
4443
  }).trim();
4206
- return join24(globalRoot, "skillwiki", "skills");
4444
+ return join25(globalRoot, "skillwiki", "skills");
4207
4445
  } catch {
4208
4446
  return null;
4209
4447
  }
@@ -4229,7 +4467,7 @@ async function runUpdate(input) {
4229
4467
  );
4230
4468
  const currentVersion = pkg2.version;
4231
4469
  const tag = input.distTag ?? "latest";
4232
- const target = join24(input.home, ".claude", "skills");
4470
+ const target = join25(input.home, ".claude", "skills");
4233
4471
  let latest;
4234
4472
  try {
4235
4473
  latest = execSync2(`npm view skillwiki@${tag} version`, {
@@ -4300,14 +4538,14 @@ async function runUpdate(input) {
4300
4538
  // src/commands/self-update.ts
4301
4539
  import { execSync as execSync3 } from "child_process";
4302
4540
  import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
4303
- import { join as join25 } from "path";
4541
+ import { join as join26 } from "path";
4304
4542
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
4305
4543
  async function runSelfUpdate(input) {
4306
4544
  const currentVersion = JSON.parse(
4307
4545
  readFileSync8(new URL("../../package.json", import.meta.url), "utf8")
4308
4546
  ).version;
4309
4547
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
4310
- const localPkgPath = join25(sourceRoot, "packages", "cli", "package.json");
4548
+ const localPkgPath = join26(sourceRoot, "packages", "cli", "package.json");
4311
4549
  const hasLocalSource = existsSync7(localPkgPath);
4312
4550
  if (input.check) {
4313
4551
  let availableVersion = null;
@@ -4439,10 +4677,10 @@ async function runSelfUpdate(input) {
4439
4677
  }
4440
4678
 
4441
4679
  // src/commands/transcripts.ts
4442
- import { readdir as readdir5, stat as stat6, readFile as readFile17 } from "fs/promises";
4443
- import { join as join26 } from "path";
4680
+ import { readdir as readdir5, stat as stat6, readFile as readFile18 } from "fs/promises";
4681
+ import { join as join27 } from "path";
4444
4682
  async function runTranscripts(input) {
4445
- const dir = join26(input.vault, "raw", "transcripts");
4683
+ const dir = join27(input.vault, "raw", "transcripts");
4446
4684
  let entries;
4447
4685
  try {
4448
4686
  entries = await readdir5(dir, { withFileTypes: true });
@@ -4452,8 +4690,8 @@ async function runTranscripts(input) {
4452
4690
  const transcripts = [];
4453
4691
  for (const entry of entries) {
4454
4692
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4455
- const filePath = join26(dir, entry.name);
4456
- const content = await readFile17(filePath, "utf8");
4693
+ const filePath = join27(dir, entry.name);
4694
+ const content = await readFile18(filePath, "utf8");
4457
4695
  const fm = extractFrontmatter(content);
4458
4696
  if (!fm.ok) continue;
4459
4697
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
@@ -4470,12 +4708,12 @@ async function runTranscripts(input) {
4470
4708
  }
4471
4709
 
4472
4710
  // src/commands/project-index.ts
4473
- import { readdir as readdir6, readFile as readFile18, writeFile as writeFile13, mkdir as mkdir9 } from "fs/promises";
4474
- import { join as join27, dirname as dirname10 } from "path";
4711
+ import { readdir as readdir6, readFile as readFile19, writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
4712
+ import { join as join28, dirname as dirname10 } from "path";
4475
4713
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
4476
4714
  async function runProjectIndex(input) {
4477
4715
  const slug = input.slug;
4478
- const projectDir = join27(input.vault, "projects", slug);
4716
+ const projectDir = join28(input.vault, "projects", slug);
4479
4717
  try {
4480
4718
  await readdir6(projectDir);
4481
4719
  } catch {
@@ -4486,15 +4724,15 @@ async function runProjectIndex(input) {
4486
4724
  }
4487
4725
  const wikilinkPattern = `[[${slug}]]`;
4488
4726
  const entries = [];
4489
- const compoundDir = join27(input.vault, "projects", slug, "compound");
4727
+ const compoundDir = join28(input.vault, "projects", slug, "compound");
4490
4728
  try {
4491
4729
  const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
4492
4730
  for (const entry of compoundFiles) {
4493
4731
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4494
- const filePath = join27(compoundDir, entry.name);
4732
+ const filePath = join28(compoundDir, entry.name);
4495
4733
  let text;
4496
4734
  try {
4497
- text = await readFile18(filePath, "utf8");
4735
+ text = await readFile19(filePath, "utf8");
4498
4736
  } catch {
4499
4737
  continue;
4500
4738
  }
@@ -4511,16 +4749,16 @@ async function runProjectIndex(input) {
4511
4749
  for (const dir of LAYER2_DIRS) {
4512
4750
  let files;
4513
4751
  try {
4514
- files = await readdir6(join27(input.vault, dir), { withFileTypes: true });
4752
+ files = await readdir6(join28(input.vault, dir), { withFileTypes: true });
4515
4753
  } catch {
4516
4754
  continue;
4517
4755
  }
4518
4756
  for (const entry of files) {
4519
4757
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4520
- const filePath = join27(input.vault, dir, entry.name);
4758
+ const filePath = join28(input.vault, dir, entry.name);
4521
4759
  let text;
4522
4760
  try {
4523
- text = await readFile18(filePath, "utf8");
4761
+ text = await readFile19(filePath, "utf8");
4524
4762
  } catch {
4525
4763
  continue;
4526
4764
  }
@@ -4541,11 +4779,11 @@ async function runProjectIndex(input) {
4541
4779
  const tb = typeOrder[b.type] ?? 99;
4542
4780
  return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
4543
4781
  });
4544
- const indexPath = join27(projectDir, "knowledge.md");
4782
+ const indexPath = join28(projectDir, "knowledge.md");
4545
4783
  let existing = false;
4546
4784
  let stale = false;
4547
4785
  try {
4548
- const existingText = await readFile18(indexPath, "utf8");
4786
+ const existingText = await readFile19(indexPath, "utf8");
4549
4787
  existing = true;
4550
4788
  const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
4551
4789
  const existingPages = new Set(existingEntries.map((l) => {
@@ -4586,7 +4824,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
4586
4824
  if (input.apply) {
4587
4825
  try {
4588
4826
  await mkdir9(dirname10(indexPath), { recursive: true });
4589
- await writeFile13(indexPath, body, "utf8");
4827
+ await writeFile10(indexPath, body, "utf8");
4590
4828
  } catch (e) {
4591
4829
  return {
4592
4830
  exitCode: ExitCode.WRITE_FAILED,
@@ -4614,10 +4852,10 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
4614
4852
  }
4615
4853
 
4616
4854
  // src/commands/compound.ts
4617
- import { writeFile as writeFile14, mkdir as mkdir10, readdir as readdir7, unlink as unlink2 } from "fs/promises";
4618
- import { join as join28 } from "path";
4855
+ import { writeFile as writeFile11, mkdir as mkdir10, readdir as readdir7, unlink as unlink3 } from "fs/promises";
4856
+ import { join as join29 } from "path";
4619
4857
  import { existsSync as existsSync8 } from "fs";
4620
- import { readFile as readFile19 } from "fs/promises";
4858
+ import { readFile as readFile20 } from "fs/promises";
4621
4859
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
4622
4860
  var FIELD_RE = {
4623
4861
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -4715,17 +4953,17 @@ function extractRetroFields(date, cycleName, block) {
4715
4953
  };
4716
4954
  }
4717
4955
  async function runCompound(input) {
4718
- const logPath = join28(input.vault, "log.md");
4956
+ const logPath = join29(input.vault, "log.md");
4719
4957
  let logText;
4720
4958
  try {
4721
- logText = await readFile19(logPath, "utf8");
4959
+ logText = await readFile20(logPath, "utf8");
4722
4960
  } catch {
4723
4961
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
4724
4962
  }
4725
4963
  const entries = parseRetroEntries(logText);
4726
4964
  const promoted = [];
4727
4965
  const skipped = [];
4728
- const compoundDir = join28(input.vault, "projects", input.project, "compound");
4966
+ const compoundDir = join29(input.vault, "projects", input.project, "compound");
4729
4967
  for (const entry of entries) {
4730
4968
  const generalizeValue = entry.generalize.trim();
4731
4969
  if (!/^yes/i.test(generalizeValue)) {
@@ -4733,7 +4971,7 @@ async function runCompound(input) {
4733
4971
  continue;
4734
4972
  }
4735
4973
  const slug = slugify(entry.cycleName);
4736
- const compoundPath = join28(compoundDir, `${slug}.md`);
4974
+ const compoundPath = join29(compoundDir, `${slug}.md`);
4737
4975
  if (existsSync8(compoundPath)) {
4738
4976
  skipped.push(entry.date);
4739
4977
  continue;
@@ -4775,7 +5013,7 @@ async function runCompound(input) {
4775
5013
  if (!existsSync8(compoundDir)) {
4776
5014
  await mkdir10(compoundDir, { recursive: true });
4777
5015
  }
4778
- await writeFile14(compoundPath, content, "utf8");
5016
+ await writeFile11(compoundPath, content, "utf8");
4779
5017
  }
4780
5018
  promoted.push(`${slug}.md`);
4781
5019
  }
@@ -4794,7 +5032,7 @@ async function runCompound(input) {
4794
5032
  };
4795
5033
  }
4796
5034
  async function runCompoundDelete(input) {
4797
- const projectDir = join28(input.vault, "projects", input.project);
5035
+ const projectDir = join29(input.vault, "projects", input.project);
4798
5036
  if (!existsSync8(projectDir)) {
4799
5037
  return {
4800
5038
  exitCode: ExitCode.PROJECT_NOT_FOUND,
@@ -4802,7 +5040,7 @@ async function runCompoundDelete(input) {
4802
5040
  };
4803
5041
  }
4804
5042
  const entryName = input.entry.replace(/\.md$/, "");
4805
- const compoundPath = join28(projectDir, "compound", `${entryName}.md`);
5043
+ const compoundPath = join29(projectDir, "compound", `${entryName}.md`);
4806
5044
  if (!existsSync8(compoundPath)) {
4807
5045
  return {
4808
5046
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -4810,7 +5048,7 @@ async function runCompoundDelete(input) {
4810
5048
  };
4811
5049
  }
4812
5050
  try {
4813
- await unlink2(compoundPath);
5051
+ await unlink3(compoundPath);
4814
5052
  } catch (e) {
4815
5053
  return {
4816
5054
  exitCode: ExitCode.WRITE_FAILED,
@@ -4836,7 +5074,7 @@ knowledge.md regenerated`
4836
5074
  };
4837
5075
  }
4838
5076
  async function runCompoundList(input) {
4839
- const compoundDir = join28(input.vault, "projects", input.project, "compound");
5077
+ const compoundDir = join29(input.vault, "projects", input.project, "compound");
4840
5078
  if (!existsSync8(compoundDir)) {
4841
5079
  return {
4842
5080
  exitCode: ExitCode.OK,
@@ -4867,10 +5105,10 @@ could not read compound directory`
4867
5105
  const entries = [];
4868
5106
  for (const dirent of dirents) {
4869
5107
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
4870
- const filePath = join28(compoundDir, dirent.name);
5108
+ const filePath = join29(compoundDir, dirent.name);
4871
5109
  let text;
4872
5110
  try {
4873
- text = await readFile19(filePath, "utf8");
5111
+ text = await readFile20(filePath, "utf8");
4874
5112
  } catch {
4875
5113
  continue;
4876
5114
  }
@@ -4899,9 +5137,9 @@ no compound entries found`;
4899
5137
  }
4900
5138
 
4901
5139
  // src/commands/observe.ts
4902
- import { mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
5140
+ import { mkdir as mkdir11, writeFile as writeFile12 } from "fs/promises";
4903
5141
  import { existsSync as existsSync9, statSync as statSync3 } from "fs";
4904
- import { join as join29 } from "path";
5142
+ import { join as join30 } from "path";
4905
5143
  import { createHash as createHash4 } from "crypto";
4906
5144
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
4907
5145
  function slugify2(text) {
@@ -4930,7 +5168,7 @@ async function runObserve(input) {
4930
5168
  result: err("VAULT_PATH_INVALID", { path: input.vault })
4931
5169
  };
4932
5170
  }
4933
- const transcriptsDir = join29(input.vault, "raw", "transcripts");
5171
+ const transcriptsDir = join30(input.vault, "raw", "transcripts");
4934
5172
  try {
4935
5173
  await mkdir11(transcriptsDir, { recursive: true });
4936
5174
  } catch {
@@ -4942,7 +5180,7 @@ async function runObserve(input) {
4942
5180
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4943
5181
  const slug = slugify2(input.text);
4944
5182
  const fileName = `${today}-observation-${slug}.md`;
4945
- const filePath = join29(transcriptsDir, fileName);
5183
+ const filePath = join30(transcriptsDir, fileName);
4946
5184
  const body = `
4947
5185
  ${input.text.trim()}
4948
5186
  `;
@@ -4960,7 +5198,7 @@ ${input.text.trim()}
4960
5198
  frontmatterLines.push("---");
4961
5199
  const content = frontmatterLines.join("\n") + body;
4962
5200
  try {
4963
- await writeFile15(filePath, content, "utf8");
5201
+ await writeFile12(filePath, content, "utf8");
4964
5202
  } catch (e) {
4965
5203
  return {
4966
5204
  exitCode: ExitCode.WRITE_FAILED,
@@ -4982,8 +5220,8 @@ ${input.text.trim()}
4982
5220
  }
4983
5221
 
4984
5222
  // src/commands/ingest.ts
4985
- import { readFile as readFile20, writeFile as writeFile16, mkdir as mkdir12 } from "fs/promises";
4986
- import { join as join30 } from "path";
5223
+ import { readFile as readFile21, writeFile as writeFile13, mkdir as mkdir12 } from "fs/promises";
5224
+ import { join as join31 } from "path";
4987
5225
  import { createHash as createHash5 } from "crypto";
4988
5226
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
4989
5227
  var TYPE_DIR = {
@@ -5142,7 +5380,7 @@ async function runIngest(input) {
5142
5380
  sourceContent = fetchResult.data.body;
5143
5381
  } else {
5144
5382
  try {
5145
- sourceContent = await readFile20(input.source, "utf8");
5383
+ sourceContent = await readFile21(input.source, "utf8");
5146
5384
  } catch {
5147
5385
  return {
5148
5386
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -5157,8 +5395,8 @@ async function runIngest(input) {
5157
5395
  const rawRelPath = `raw/articles/${slug}.md`;
5158
5396
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
5159
5397
  const typedRelPath = `${typedDir}/${slug}.md`;
5160
- const rawAbsPath = join30(input.vault, rawRelPath);
5161
- const typedAbsPath = join30(input.vault, typedRelPath);
5398
+ const rawAbsPath = join31(input.vault, rawRelPath);
5399
+ const typedAbsPath = join31(input.vault, typedRelPath);
5162
5400
  const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
5163
5401
  const typedContent = buildTypedContent(
5164
5402
  input.title,
@@ -5221,8 +5459,8 @@ async function runIngest(input) {
5221
5459
  };
5222
5460
  }
5223
5461
  try {
5224
- await mkdir12(join30(input.vault, "raw", "articles"), { recursive: true });
5225
- await writeFile16(rawAbsPath, rawContent, "utf8");
5462
+ await mkdir12(join31(input.vault, "raw", "articles"), { recursive: true });
5463
+ await writeFile13(rawAbsPath, rawContent, "utf8");
5226
5464
  } catch (e) {
5227
5465
  return {
5228
5466
  exitCode: ExitCode.WRITE_FAILED,
@@ -5230,8 +5468,8 @@ async function runIngest(input) {
5230
5468
  };
5231
5469
  }
5232
5470
  try {
5233
- await mkdir12(join30(input.vault, typedDir), { recursive: true });
5234
- await writeFile16(typedAbsPath, typedContent, "utf8");
5471
+ await mkdir12(join31(input.vault, typedDir), { recursive: true });
5472
+ await writeFile13(typedAbsPath, typedContent, "utf8");
5235
5473
  } catch (e) {
5236
5474
  return {
5237
5475
  exitCode: ExitCode.WRITE_FAILED,
@@ -5262,7 +5500,6 @@ async function runIngest(input) {
5262
5500
  }
5263
5501
 
5264
5502
  // src/commands/tag-sync.ts
5265
- import { writeFile as writeFile17 } from "fs/promises";
5266
5503
  var ENUM_MIRRORS = {
5267
5504
  provenance: ["research", "project", "mixed"],
5268
5505
  confidence: ["high", "medium", "low"]
@@ -5377,7 +5614,11 @@ ${newFm}
5377
5614
  ---
5378
5615
  ${body}`;
5379
5616
  if (!input.dryRun) {
5380
- await writeFile17(page.absPath, newText, "utf8");
5617
+ const w = await safeWritePage(page.absPath, newText);
5618
+ if (!w.ok) {
5619
+ unchanged++;
5620
+ continue;
5621
+ }
5381
5622
  }
5382
5623
  synced.push(page.relPath);
5383
5624
  }
@@ -5407,10 +5648,10 @@ ${body}`;
5407
5648
 
5408
5649
  // src/commands/sync.ts
5409
5650
  import { existsSync as existsSync10 } from "fs";
5410
- import { join as join31 } from "path";
5651
+ import { join as join32 } from "path";
5411
5652
  function runSyncStatus(input) {
5412
5653
  const vault = input.vault;
5413
- if (!existsSync10(join31(vault, ".git"))) {
5654
+ if (!existsSync10(join32(vault, ".git"))) {
5414
5655
  return {
5415
5656
  exitCode: ExitCode.VAULT_PATH_INVALID,
5416
5657
  result: ok({
@@ -5479,7 +5720,7 @@ function runSyncStatus(input) {
5479
5720
  }
5480
5721
  async function runSyncPush(input) {
5481
5722
  const vault = input.vault;
5482
- if (!existsSync10(join31(vault, ".git"))) {
5723
+ if (!existsSync10(join32(vault, ".git"))) {
5483
5724
  return {
5484
5725
  exitCode: ExitCode.VAULT_PATH_INVALID,
5485
5726
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -5564,7 +5805,7 @@ async function runSyncPush(input) {
5564
5805
  }
5565
5806
  async function runSyncPull(input) {
5566
5807
  const vault = input.vault;
5567
- if (!existsSync10(join31(vault, ".git"))) {
5808
+ if (!existsSync10(join32(vault, ".git"))) {
5568
5809
  return {
5569
5810
  exitCode: ExitCode.VAULT_PATH_INVALID,
5570
5811
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -5640,7 +5881,7 @@ async function runSyncPull(input) {
5640
5881
 
5641
5882
  // src/commands/backup.ts
5642
5883
  import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
5643
- import { join as join32, relative as relative3, dirname as dirname11 } from "path";
5884
+ import { join as join33, relative as relative3, dirname as dirname11 } from "path";
5644
5885
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
5645
5886
 
5646
5887
  // src/utils/s3-client.ts
@@ -5664,7 +5905,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
5664
5905
  function* walkMarkdown(dir, base) {
5665
5906
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
5666
5907
  if (SKIP_DIRS.has(entry.name)) continue;
5667
- const full = join32(dir, entry.name);
5908
+ const full = join33(dir, entry.name);
5668
5909
  if (entry.isDirectory()) {
5669
5910
  yield* walkMarkdown(full, base);
5670
5911
  } else if (entry.name.endsWith(".md")) {
@@ -5687,7 +5928,7 @@ async function runBackupSync(input) {
5687
5928
  let failed = 0;
5688
5929
  const files = [...walkMarkdown(input.vault, input.vault)];
5689
5930
  for (const relPath of files) {
5690
- const absPath = join32(input.vault, relPath);
5931
+ const absPath = join33(input.vault, relPath);
5691
5932
  const localStat = statSync4(absPath);
5692
5933
  let needsUpload = true;
5693
5934
  try {
@@ -5763,7 +6004,7 @@ async function runBackupRestore(input) {
5763
6004
  const objects = list.Contents ?? [];
5764
6005
  for (const obj of objects) {
5765
6006
  if (!obj.Key) continue;
5766
- const localPath = join32(target, obj.Key);
6007
+ const localPath = join33(target, obj.Key);
5767
6008
  try {
5768
6009
  const localStat = statSync4(localPath);
5769
6010
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -5810,8 +6051,8 @@ async function runBackupRestore(input) {
5810
6051
 
5811
6052
  // src/commands/status.ts
5812
6053
  import { existsSync as existsSync11, statSync as statSync5 } from "fs";
5813
- import { readFile as readFile21 } from "fs/promises";
5814
- import { join as join33 } from "path";
6054
+ import { readFile as readFile22 } from "fs/promises";
6055
+ import { join as join34 } from "path";
5815
6056
  async function runStatus(input) {
5816
6057
  if (!existsSync11(input.vault)) {
5817
6058
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
@@ -5838,7 +6079,7 @@ async function runStatus(input) {
5838
6079
  const compound = scan.data.compound.length;
5839
6080
  let schemaVersion = "v1";
5840
6081
  try {
5841
- const schemaContent = await readFile21(join33(input.vault, "SCHEMA.md"), "utf8");
6082
+ const schemaContent = await readFile22(join34(input.vault, "SCHEMA.md"), "utf8");
5842
6083
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
5843
6084
  if (versionMatch) schemaVersion = versionMatch[1];
5844
6085
  } catch {
@@ -5898,8 +6139,8 @@ async function runStatus(input) {
5898
6139
  }
5899
6140
 
5900
6141
  // src/commands/seed.ts
5901
- import { mkdir as mkdir13, writeFile as writeFile18, stat as stat7 } from "fs/promises";
5902
- import { join as join34 } from "path";
6142
+ import { mkdir as mkdir13, writeFile as writeFile14, stat as stat7 } from "fs/promises";
6143
+ import { join as join35 } from "path";
5903
6144
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5904
6145
  var EXAMPLE_PAGES = {
5905
6146
  "entities/example-project.md": `---
@@ -5968,30 +6209,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
5968
6209
  `;
5969
6210
  async function runSeed(input) {
5970
6211
  try {
5971
- await stat7(join34(input.vault, "SCHEMA.md"));
6212
+ await stat7(join35(input.vault, "SCHEMA.md"));
5972
6213
  } catch {
5973
6214
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
5974
6215
  }
5975
6216
  const created = [];
5976
6217
  const skipped = [];
5977
6218
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
5978
- const absPath = join34(input.vault, relPath);
6219
+ const absPath = join35(input.vault, relPath);
5979
6220
  try {
5980
6221
  await stat7(absPath);
5981
6222
  skipped.push(relPath);
5982
6223
  } catch {
5983
- await mkdir13(join34(absPath, ".."), { recursive: true });
5984
- await writeFile18(absPath, content, "utf8");
6224
+ await mkdir13(join35(absPath, ".."), { recursive: true });
6225
+ await writeFile14(absPath, content, "utf8");
5985
6226
  created.push(relPath);
5986
6227
  }
5987
6228
  }
5988
- const rawPath = join34(input.vault, "raw", "articles", "example-source.md");
6229
+ const rawPath = join35(input.vault, "raw", "articles", "example-source.md");
5989
6230
  try {
5990
6231
  await stat7(rawPath);
5991
6232
  skipped.push("raw/articles/example-source.md");
5992
6233
  } catch {
5993
- await mkdir13(join34(rawPath, ".."), { recursive: true });
5994
- await writeFile18(rawPath, EXAMPLE_RAW, "utf8");
6234
+ await mkdir13(join35(rawPath, ".."), { recursive: true });
6235
+ await writeFile14(rawPath, EXAMPLE_RAW, "utf8");
5995
6236
  created.push("raw/articles/example-source.md");
5996
6237
  }
5997
6238
  if (created.length > 0) {
@@ -6013,9 +6254,9 @@ async function runSeed(input) {
6013
6254
  }
6014
6255
 
6015
6256
  // src/commands/canvas.ts
6016
- import { readFile as readFile22, writeFile as writeFile19 } from "fs/promises";
6257
+ import { readFile as readFile23, writeFile as writeFile15 } from "fs/promises";
6017
6258
  import { existsSync as existsSync12 } from "fs";
6018
- import { join as join35 } from "path";
6259
+ import { join as join36 } from "path";
6019
6260
  var NODE_WIDTH = 240;
6020
6261
  var NODE_HEIGHT = 60;
6021
6262
  var COLUMN_SPACING = 400;
@@ -6093,7 +6334,7 @@ function buildCanvasEdges(adjacency) {
6093
6334
  return edges;
6094
6335
  }
6095
6336
  async function runCanvasGenerate(input) {
6096
- const graphPath = input.graphPath ?? join35(input.vault, ".skillwiki", "graph.json");
6337
+ const graphPath = input.graphPath ?? join36(input.vault, ".skillwiki", "graph.json");
6097
6338
  if (!existsSync12(graphPath)) {
6098
6339
  return {
6099
6340
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -6105,7 +6346,7 @@ async function runCanvasGenerate(input) {
6105
6346
  }
6106
6347
  let raw;
6107
6348
  try {
6108
- raw = await readFile22(graphPath, "utf8");
6349
+ raw = await readFile23(graphPath, "utf8");
6109
6350
  } catch (e) {
6110
6351
  return {
6111
6352
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -6131,9 +6372,9 @@ async function runCanvasGenerate(input) {
6131
6372
  const nodes = buildCanvasNodes(paths);
6132
6373
  const edges = buildCanvasEdges(graph.adjacency);
6133
6374
  const canvas = { nodes, edges };
6134
- const outPath = join35(input.vault, "vault-graph.canvas");
6375
+ const outPath = join36(input.vault, "vault-graph.canvas");
6135
6376
  try {
6136
- await writeFile19(outPath, JSON.stringify(canvas, null, 2));
6377
+ await writeFile15(outPath, JSON.stringify(canvas, null, 2));
6137
6378
  } catch (e) {
6138
6379
  return {
6139
6380
  exitCode: ExitCode.WRITE_FAILED,
@@ -6153,8 +6394,8 @@ written: ${outPath}`
6153
6394
  }
6154
6395
 
6155
6396
  // src/commands/query.ts
6156
- import { readFile as readFile23, stat as stat8 } from "fs/promises";
6157
- import { join as join36 } from "path";
6397
+ import { readFile as readFile24, stat as stat8 } from "fs/promises";
6398
+ import { join as join37 } from "path";
6158
6399
  var W_KEYWORD = 2;
6159
6400
  var W_SOURCE_OVERLAP = 4;
6160
6401
  var W_WIKILINK = 3;
@@ -6275,7 +6516,7 @@ function computeKeywordScore(terms, title, tags, body) {
6275
6516
  return score;
6276
6517
  }
6277
6518
  async function loadOrBuildGraph(vault) {
6278
- const graphPath = join36(vault, ".skillwiki", "graph.json");
6519
+ const graphPath = join37(vault, ".skillwiki", "graph.json");
6279
6520
  let needsBuild = false;
6280
6521
  try {
6281
6522
  const fileStat = await stat8(graphPath);
@@ -6289,7 +6530,7 @@ async function loadOrBuildGraph(vault) {
6289
6530
  if (buildResult.exitCode !== 0) return null;
6290
6531
  }
6291
6532
  try {
6292
- const raw = await readFile23(graphPath, "utf8");
6533
+ const raw = await readFile24(graphPath, "utf8");
6293
6534
  return JSON.parse(raw);
6294
6535
  } catch {
6295
6536
  return null;
@@ -6298,13 +6539,13 @@ async function loadOrBuildGraph(vault) {
6298
6539
 
6299
6540
  // src/utils/auto-commit.ts
6300
6541
  import { existsSync as existsSync13 } from "fs";
6301
- import { join as join37 } from "path";
6542
+ import { join as join38 } from "path";
6302
6543
  async function postCommit(vault, exitCode) {
6303
6544
  if (exitCode !== 0) return;
6304
6545
  const home = process.env.HOME ?? "";
6305
6546
  const dotenv = await parseDotenvFile(configPath(home));
6306
6547
  if (dotenv["AUTO_COMMIT"] === "false") return;
6307
- if (!existsSync13(join37(vault, ".git"))) return;
6548
+ if (!existsSync13(join38(vault, ".git"))) return;
6308
6549
  const lastOps = readLastOp(vault);
6309
6550
  if (lastOps.length === 0) return;
6310
6551
  const porcelain = git(vault, ["status", "--porcelain"]);
@@ -6355,7 +6596,7 @@ program.command("validate <file>").description("validate vault page frontmatter
6355
6596
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
6356
6597
  });
6357
6598
  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) => {
6358
- const out = opts.out ?? join38(vault, ".skillwiki", "graph.json");
6599
+ const out = opts.out ?? join39(vault, ".skillwiki", "graph.json");
6359
6600
  emit(await runGraphBuild({ vault, out }), vault);
6360
6601
  });
6361
6602
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -6461,10 +6702,10 @@ program.command("topic-map-check [vault]").description("check whether a topic ma
6461
6702
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6462
6703
  else emit(await runTopicMapCheck({ vault: v.vault, threshold: opts.threshold }), v.vault);
6463
6704
  });
6464
- 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("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6705
+ 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) => {
6465
6706
  const v = await resolveVaultArg(vault, opts.wiki);
6466
6707
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6467
- else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive, forceScan: !!opts.forceScan }), v.vault);
6708
+ else emit(await runStale({ vault: v.vault, days: opts.days, archive: !!opts.archive, forceScan: !!opts.forceScan, project: opts.project }), v.vault);
6468
6709
  });
6469
6710
  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) => {
6470
6711
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -6481,7 +6722,7 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
6481
6722
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6482
6723
  else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }), v.vault);
6483
6724
  });
6484
- 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 legacy_citation_style violations").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6725
+ 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 legacy_citation_style violations").option("--only <bucket>", "run only the specified lint bucket").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6485
6726
  const v = await resolveVaultArg(vault, opts.wiki);
6486
6727
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6487
6728
  else emit(await runLint({
@@ -6490,7 +6731,8 @@ program.command("lint [vault]").description("run all vault health checks").optio
6490
6731
  days: opts.days,
6491
6732
  lines: opts.lines,
6492
6733
  logThreshold: opts.logThreshold,
6493
- fix: opts.fix ?? false
6734
+ fix: opts.fix ?? false,
6735
+ only: opts.only
6494
6736
  }), v.vault);
6495
6737
  });
6496
6738
  var configCmd = program.command("config").description("manage skillwiki configuration");
@@ -6597,7 +6839,10 @@ syncCmd.command("pull [vault]").description("pull remote vault changes and lint"
6597
6839
  var backupCmd = program.command("backup").description("manage S3-compatible remote backup");
6598
6840
  backupCmd.command("sync [vault]").description("sync vault to S3-compatible remote backup").option("--dry-run", "list actions without executing").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--prune", "delete orphaned S3 objects not in vault", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6599
6841
  const v = await resolveVaultArg(vault, opts.wiki);
6600
- if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6842
+ if (!v.ok) {
6843
+ emit({ exitCode: v.exitCode, result: v.payload });
6844
+ return;
6845
+ }
6601
6846
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
6602
6847
  const dotenv = await parseDotenvFile(configPath(home));
6603
6848
  emit(await runBackupSync({
@@ -6613,7 +6858,10 @@ backupCmd.command("sync [vault]").description("sync vault to S3-compatible remot
6613
6858
  });
6614
6859
  backupCmd.command("restore [vault]").description("restore vault from S3-compatible remote backup").option("--bucket <name>", "S3 bucket name").option("--endpoint <url>", "S3 endpoint URL").option("--region <region>", "S3 region", "us-east-1").option("--target <dir>", "restore target directory (defaults to vault)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6615
6860
  const v = await resolveVaultArg(vault, opts.wiki);
6616
- if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
6861
+ if (!v.ok) {
6862
+ emit({ exitCode: v.exitCode, result: v.payload });
6863
+ return;
6864
+ }
6617
6865
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
6618
6866
  const dotenv = await parseDotenvFile(configPath(home));
6619
6867
  emit(await runBackupRestore({