skillwiki 0.8.0 → 0.8.1-beta.10

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 readFileSync12 } from "fs";
12
- import { join as join41 } from "path";
12
+ import { join as join44 } from "path";
13
13
  import { Command as Command2 } from "commander";
14
14
 
15
15
  // ../shared/src/exit-codes.ts
@@ -62,7 +62,8 @@ var ExitCode = {
62
62
  BACKUP_RESTORE_CONFLICTS: 45,
63
63
  USAGE: 46,
64
64
  BODY_TRUNCATION_GUARD: 47,
65
- SYNC_LOCK_HELD: 48
65
+ SYNC_LOCK_HELD: 48,
66
+ LOG_APPEND_LOCK_HELD: 49
66
67
  };
67
68
 
68
69
  // ../shared/src/json-output.ts
@@ -499,6 +500,7 @@ import { dirname } from "path";
499
500
  import { readFile as readFile3, readdir, stat } from "fs/promises";
500
501
  import { join as join3, relative as relative2, sep as sep2 } from "path";
501
502
  var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
503
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
502
504
  async function scanVault(root) {
503
505
  try {
504
506
  await stat(join3(root, "SCHEMA.md"));
@@ -509,6 +511,7 @@ async function scanVault(root) {
509
511
  const rels = all.map((p) => ({ absPath: p, relPath: relative2(root, p).split(sep2).join("/") }));
510
512
  return ok({
511
513
  root,
514
+ allMarkdown: rels,
512
515
  typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
513
516
  raw: rels.filter((p) => p.relPath.startsWith("raw/")),
514
517
  workItems: rels.filter((p) => /^projects\/[^/]+\/work\/[^/]+\/(spec|plan|log)\.md$/.test(p.relPath)),
@@ -520,8 +523,10 @@ async function walk(dir) {
520
523
  const out = [];
521
524
  for (const e of entries) {
522
525
  const p = join3(dir, e.name);
523
- if (e.isDirectory()) out.push(...await walk(p));
524
- else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
526
+ if (e.isDirectory()) {
527
+ if (SKIP_DIRS.has(e.name)) continue;
528
+ out.push(...await walk(p));
529
+ } else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
525
530
  }
526
531
  return out;
527
532
  }
@@ -547,23 +552,137 @@ function extractBodyWikilinks(body) {
547
552
  return out;
548
553
  }
549
554
 
550
- // src/commands/graph.ts
551
- async function runGraphBuild(input) {
552
- const scan = await scanVault(input.vault);
553
- if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
555
+ // src/utils/community.ts
556
+ async function buildWikilinkAdjacency(typedKnowledge) {
554
557
  const adjacency = {};
555
558
  const slugToPath = {};
556
- for (const p of scan.data.typedKnowledge) {
559
+ for (const p of typedKnowledge) {
557
560
  const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
558
561
  slugToPath[slug] = p.relPath;
559
562
  }
560
- for (const p of scan.data.typedKnowledge) {
563
+ for (const p of typedKnowledge) {
561
564
  const text = await readPage(p);
562
565
  const split = splitFrontmatter(text);
563
566
  const body = split.ok ? split.data.body : text;
564
567
  const links = extractBodyWikilinks(body);
565
568
  adjacency[p.relPath] = links.map((slug) => slugToPath[slug.split("/").pop()]).filter((x) => Boolean(x));
566
569
  }
570
+ return adjacency;
571
+ }
572
+ function toUndirectedWeighted(adj) {
573
+ const g = /* @__PURE__ */ new Map();
574
+ const ensure = (n) => {
575
+ let m = g.get(n);
576
+ if (!m) {
577
+ m = /* @__PURE__ */ new Map();
578
+ g.set(n, m);
579
+ }
580
+ return m;
581
+ };
582
+ for (const node of Object.keys(adj)) ensure(node);
583
+ for (const [a, nbrs] of Object.entries(adj)) {
584
+ for (const b of nbrs) {
585
+ if (a === b) continue;
586
+ ensure(a).set(b, 1);
587
+ ensure(b).set(a, 1);
588
+ }
589
+ }
590
+ return g;
591
+ }
592
+ function louvain(g) {
593
+ const nodes = [...g.keys()].sort();
594
+ const comm = /* @__PURE__ */ new Map();
595
+ nodes.forEach((n, i) => comm.set(n, i));
596
+ const k = /* @__PURE__ */ new Map();
597
+ let m2 = 0;
598
+ for (const n of nodes) {
599
+ let deg = 0;
600
+ for (const w of g.get(n).values()) deg += w;
601
+ k.set(n, deg);
602
+ m2 += deg;
603
+ }
604
+ if (m2 === 0) return comm;
605
+ const sumTot = /* @__PURE__ */ new Map();
606
+ for (const n of nodes) {
607
+ const c = comm.get(n);
608
+ sumTot.set(c, (sumTot.get(c) ?? 0) + k.get(n));
609
+ }
610
+ let improved = true;
611
+ while (improved) {
612
+ improved = false;
613
+ for (const n of nodes) {
614
+ const cur = comm.get(n);
615
+ const kn = k.get(n);
616
+ sumTot.set(cur, sumTot.get(cur) - kn);
617
+ const wToComm = /* @__PURE__ */ new Map();
618
+ for (const [nb, w] of g.get(n)) {
619
+ if (nb === n) continue;
620
+ const c = comm.get(nb);
621
+ wToComm.set(c, (wToComm.get(c) ?? 0) + w);
622
+ }
623
+ const gainFor = (c) => (wToComm.get(c) ?? 0) - (sumTot.get(c) ?? 0) * kn / m2;
624
+ const curGain = gainFor(cur);
625
+ let bestComm = cur;
626
+ let bestDelta = 0;
627
+ for (const c of wToComm.keys()) {
628
+ const delta = gainFor(c) - curGain;
629
+ if (delta > bestDelta) {
630
+ bestDelta = delta;
631
+ bestComm = c;
632
+ }
633
+ }
634
+ comm.set(n, bestComm);
635
+ sumTot.set(bestComm, (sumTot.get(bestComm) ?? 0) + kn);
636
+ if (bestComm !== cur) improved = true;
637
+ }
638
+ }
639
+ return comm;
640
+ }
641
+ function communityCohesion(members, g) {
642
+ const n = members.length;
643
+ if (n < 2) return 1;
644
+ const set = new Set(members);
645
+ let internal = 0;
646
+ for (const a of members) {
647
+ for (const [b, w] of g.get(a) ?? /* @__PURE__ */ new Map()) {
648
+ if (a < b && set.has(b)) internal += w;
649
+ }
650
+ }
651
+ return internal / (n * (n - 1) / 2);
652
+ }
653
+ function findSparseCommunities(adj, opts = {}) {
654
+ const minSize = opts.minSize ?? 3;
655
+ const maxCohesion = opts.maxCohesion ?? 0.15;
656
+ const g = toUndirectedWeighted(adj);
657
+ const comm = louvain(g);
658
+ const groups = /* @__PURE__ */ new Map();
659
+ for (const [node, c] of comm) {
660
+ const arr = groups.get(c);
661
+ if (arr) arr.push(node);
662
+ else groups.set(c, [node]);
663
+ }
664
+ const out = [];
665
+ for (const members of groups.values()) {
666
+ if (members.length < minSize) continue;
667
+ const cohesion = communityCohesion(members, g);
668
+ if (cohesion < maxCohesion) {
669
+ out.push({
670
+ members: [...members].sort(),
671
+ size: members.length,
672
+ cohesion: Math.round(cohesion * 1e3) / 1e3,
673
+ action: members.length <= 5 ? "merge into adjacent community" : "split into smaller topics"
674
+ });
675
+ }
676
+ }
677
+ out.sort((a, b) => a.cohesion - b.cohesion);
678
+ return out;
679
+ }
680
+
681
+ // src/commands/graph.ts
682
+ async function runGraphBuild(input) {
683
+ const scan = await scanVault(input.vault);
684
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
685
+ const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge);
567
686
  const adamicAdar = computeAdamicAdar(adjacency);
568
687
  const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
569
688
  try {
@@ -2362,10 +2481,124 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
2362
2481
  return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
2363
2482
  }
2364
2483
 
2484
+ // src/commands/log-append.ts
2485
+ import { readFile as readFile13, rename as rename4, writeFile as writeFile8, stat as stat6 } from "fs/promises";
2486
+ import { join as join17 } from "path";
2487
+
2488
+ // src/utils/log-lock.ts
2489
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
2490
+ import { join as join16 } from "path";
2491
+ function logLockPath(vault) {
2492
+ return join16(vault, ".skillwiki", "log-append.lock");
2493
+ }
2494
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2495
+ async function acquireLogLock(vault, opts = {}) {
2496
+ const retryMs = opts.retryMs ?? 2e3;
2497
+ const pollMs = opts.pollMs ?? 50;
2498
+ const staleMs = opts.staleMs ?? 1e4;
2499
+ const path = logLockPath(vault);
2500
+ const dir = join16(vault, ".skillwiki");
2501
+ if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
2502
+ const deadline = Date.now() + retryMs;
2503
+ const content = JSON.stringify({ pid: process.pid, acquired: (/* @__PURE__ */ new Date()).toISOString() }) + "\n";
2504
+ for (; ; ) {
2505
+ try {
2506
+ writeFileSync2(path, content, { flag: "wx" });
2507
+ return { ok: true };
2508
+ } catch (e) {
2509
+ const err3 = e;
2510
+ if (err3.code !== "EEXIST") throw err3;
2511
+ }
2512
+ try {
2513
+ const age = Date.now() - statSync2(path).mtimeMs;
2514
+ if (age > staleMs) {
2515
+ unlinkSync2(path);
2516
+ continue;
2517
+ }
2518
+ } catch {
2519
+ continue;
2520
+ }
2521
+ if (Date.now() >= deadline) return { ok: false };
2522
+ await sleep(pollMs);
2523
+ }
2524
+ }
2525
+ function releaseLogLock(vault) {
2526
+ try {
2527
+ unlinkSync2(logLockPath(vault));
2528
+ } catch {
2529
+ }
2530
+ }
2531
+
2532
+ // src/commands/log-append.ts
2533
+ var ENTRY_RE2 = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
2534
+ async function runLogAppend(input) {
2535
+ try {
2536
+ await stat6(join17(input.vault, "SCHEMA.md"));
2537
+ } catch {
2538
+ return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
2539
+ }
2540
+ const content = (input.content ?? "").trim();
2541
+ if (content.length === 0) {
2542
+ return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--content must be a non-empty log entry" }) };
2543
+ }
2544
+ const acquired = await acquireLogLock(input.vault);
2545
+ if (!acquired.ok) {
2546
+ return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
2547
+ }
2548
+ const logPath = join17(input.vault, "log.md");
2549
+ try {
2550
+ let logText;
2551
+ try {
2552
+ logText = await readFile13(logPath, "utf8");
2553
+ } catch {
2554
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
2555
+ }
2556
+ const entriesBefore = [...logText.matchAll(ENTRY_RE2)].length;
2557
+ const body = logText.replace(/\s+$/, "");
2558
+ const next = `${body}
2559
+
2560
+ ${content}
2561
+ `;
2562
+ try {
2563
+ const tmp = logPath + ".tmp";
2564
+ await writeFile8(tmp, next, "utf8");
2565
+ await rename4(tmp, logPath);
2566
+ } catch (e) {
2567
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
2568
+ }
2569
+ appendLastOp(input.vault, {
2570
+ operation: "log-append",
2571
+ summary: `appended log entry (${entriesBefore}->${entriesBefore + 1})`,
2572
+ files: ["log.md"],
2573
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2574
+ });
2575
+ const entriesAfter = entriesBefore + 1;
2576
+ return {
2577
+ exitCode: ExitCode.OK,
2578
+ result: ok({ entries_before: entriesBefore, entries_after: entriesAfter, appended: true, humanHint: `appended log entry (${entriesBefore}->${entriesAfter})` })
2579
+ };
2580
+ } finally {
2581
+ releaseLogLock(input.vault);
2582
+ }
2583
+ }
2584
+
2365
2585
  // src/commands/lint.ts
2366
- import { existsSync as existsSync3 } from "fs";
2367
- import { readFile as readFile15, rename as rename5 } from "fs/promises";
2368
- import { join as join19 } from "path";
2586
+ import { existsSync as existsSync5 } from "fs";
2587
+ import { readFile as readFile17 } from "fs/promises";
2588
+ import { join as join22 } from "path";
2589
+
2590
+ // src/commands/sparse-community.ts
2591
+ async function runSparseCommunity(input) {
2592
+ const scan = await scanVault(input.vault);
2593
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2594
+ const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge);
2595
+ const communities = findSparseCommunities(adjacency, {
2596
+ minSize: input.minSize,
2597
+ maxCohesion: input.maxCohesion
2598
+ });
2599
+ const humanHint = communities.length === 0 ? "no sparse communities" : communities.map((c) => ` cohesion ${c.cohesion} (${c.size} pages): ${c.action}`).join("\n");
2600
+ return { exitCode: ExitCode.OK, result: ok({ communities, humanHint }) };
2601
+ }
2369
2602
 
2370
2603
  // src/commands/topic-map-check.ts
2371
2604
  var DEFAULT_THRESHOLD = 200;
@@ -2387,13 +2620,13 @@ async function runTopicMapCheck(input) {
2387
2620
  }
2388
2621
 
2389
2622
  // src/commands/index-link-format.ts
2390
- import { readFile as readFile13 } from "fs/promises";
2391
- import { join as join16 } from "path";
2623
+ import { readFile as readFile14 } from "fs/promises";
2624
+ import { join as join18 } from "path";
2392
2625
  var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
2393
2626
  async function runIndexLinkFormat(input) {
2394
2627
  let text = "";
2395
2628
  try {
2396
- text = await readFile13(join16(input.vault, "index.md"), "utf8");
2629
+ text = await readFile14(join18(input.vault, "index.md"), "utf8");
2397
2630
  } catch {
2398
2631
  }
2399
2632
  const markdown_links = [];
@@ -2406,8 +2639,8 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
2406
2639
  }
2407
2640
 
2408
2641
  // src/commands/dedup.ts
2409
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
2410
- import { join as join17 } from "path";
2642
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
2643
+ import { join as join19 } from "path";
2411
2644
  async function runDedup(input) {
2412
2645
  const scan = await scanVault(input.vault);
2413
2646
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -2435,7 +2668,7 @@ async function runDedup(input) {
2435
2668
  }
2436
2669
  }
2437
2670
  for (const page of scan.data.typedKnowledge) {
2438
- const text = readFileSync3(join17(input.vault, page.relPath), "utf-8");
2671
+ const text = readFileSync3(join19(input.vault, page.relPath), "utf-8");
2439
2672
  let updated = text;
2440
2673
  let changed = false;
2441
2674
  for (const [oldPath, newPath] of replacements) {
@@ -2453,14 +2686,14 @@ async function runDedup(input) {
2453
2686
  }
2454
2687
  }
2455
2688
  if (changed) {
2456
- writeFileSync2(join17(input.vault, page.relPath), updated);
2689
+ writeFileSync3(join19(input.vault, page.relPath), updated);
2457
2690
  rewired.push(page.relPath);
2458
2691
  }
2459
2692
  }
2460
2693
  for (const [oldPath] of replacements) {
2461
- const fullPath = join17(input.vault, oldPath);
2694
+ const fullPath = join19(input.vault, oldPath);
2462
2695
  try {
2463
- unlinkSync2(fullPath);
2696
+ unlinkSync3(fullPath);
2464
2697
  removed.push(oldPath);
2465
2698
  } catch {
2466
2699
  }
@@ -2493,9 +2726,9 @@ async function runDedup(input) {
2493
2726
  }
2494
2727
 
2495
2728
  // src/utils/safe-write.ts
2496
- import { open, readFile as readFile14, rename as rename4, unlink as unlink2, writeFile as writeFile8 } from "fs/promises";
2729
+ import { open, readFile as readFile15, rename as rename5, unlink as unlink2, writeFile as writeFile9 } from "fs/promises";
2497
2730
  import { randomBytes } from "crypto";
2498
- import { dirname as dirname7, basename, join as join18 } from "path";
2731
+ import { dirname as dirname7, basename, join as join20 } from "path";
2499
2732
  var DEFAULT_MIN_BODY_RATIO = 0.5;
2500
2733
  var DEFAULT_MIN_OLD_BODY_BYTES = 200;
2501
2734
  function bodyBytes(text) {
@@ -2505,7 +2738,7 @@ function bodyBytes(text) {
2505
2738
  }
2506
2739
  async function readIfExists(absPath) {
2507
2740
  try {
2508
- return await readFile14(absPath, "utf8");
2741
+ return await readFile15(absPath, "utf8");
2509
2742
  } catch (e) {
2510
2743
  if (e.code === "ENOENT") return null;
2511
2744
  throw e;
@@ -2544,7 +2777,7 @@ async function safeWritePage(absPath, newContent, opts = {}) {
2544
2777
  }
2545
2778
  const dir = dirname7(absPath);
2546
2779
  const tmpName = `.${basename(absPath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
2547
- const tmpPath = join18(dir, tmpName);
2780
+ const tmpPath = join20(dir, tmpName);
2548
2781
  try {
2549
2782
  const handle = await open(tmpPath, "w");
2550
2783
  try {
@@ -2556,7 +2789,7 @@ async function safeWritePage(absPath, newContent, opts = {}) {
2556
2789
  } finally {
2557
2790
  await handle.close();
2558
2791
  }
2559
- await rename4(tmpPath, absPath);
2792
+ await rename5(tmpPath, absPath);
2560
2793
  return ok({ isNew, oldBodyBytes, newBodyBytes, bodyRatio, guardSkippedSmall });
2561
2794
  } catch (e) {
2562
2795
  try {
@@ -2608,28 +2841,100 @@ async function runRawBodyDedup(vault) {
2608
2841
  }
2609
2842
 
2610
2843
  // src/commands/path-too-long.ts
2844
+ import { existsSync as existsSync4 } from "fs";
2845
+ import { mkdir as mkdir8, readFile as readFile16, rename as rename6, unlink as unlink3 } from "fs/promises";
2846
+ import { dirname as dirname8, join as join21, posix, resolve as resolve4 } from "path";
2611
2847
  var MAX_PATH_LENGTH = 240;
2848
+ var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
2612
2849
  async function runPathTooLong(input) {
2613
2850
  const scan = await scanVault(input.vault);
2614
2851
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2615
- const allPages = [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound];
2616
- const violations = [];
2617
- for (const page of allPages) {
2618
- if (page.relPath.length > MAX_PATH_LENGTH) {
2619
- violations.push({ relPath: page.relPath, length: page.relPath.length });
2620
- }
2621
- }
2852
+ const violations = findPathTooLongViolations(scan.data.allMarkdown, MAX_PATH_LENGTH);
2622
2853
  if (violations.length > 0) {
2623
2854
  return {
2624
2855
  exitCode: ExitCode.LINT_HAS_ERRORS,
2625
2856
  result: ok({
2626
2857
  violations,
2627
- humanHint: violations.map((v) => `${v.relPath}: ${v.length} chars (max ${MAX_PATH_LENGTH})`).join("\n")
2858
+ humanHint: violations.map((v) => `${v.relPath}: ${v.length} chars (max ${MAX_PATH_LENGTH}) -> ${v.suggestedRelPath}`).join("\n")
2628
2859
  })
2629
2860
  };
2630
2861
  }
2631
2862
  return { exitCode: ExitCode.OK, result: ok({ violations, humanHint: "all paths within length limit" }) };
2632
2863
  }
2864
+ async function fixPathTooLong(input) {
2865
+ const scan = await scanVault(input.vault);
2866
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2867
+ const maxFixLength = maxFixPathLength(input.vault);
2868
+ const violations = findPathTooLongViolations(scan.data.allMarkdown, maxFixLength);
2869
+ const fixed = [];
2870
+ const unresolved = [];
2871
+ for (const violation of violations) {
2872
+ const target = await resolveFixTarget(input.vault, violation.relPath, violation.suggestedRelPath, maxFixLength);
2873
+ if (!target || target.relPath === violation.relPath || target.relPath.length > maxFixLength) {
2874
+ unresolved.push(violation.relPath);
2875
+ continue;
2876
+ }
2877
+ try {
2878
+ if (target.mode === "dedupe") {
2879
+ await unlink3(join21(input.vault, violation.relPath));
2880
+ } else {
2881
+ await mkdir8(dirname8(join21(input.vault, target.relPath)), { recursive: true });
2882
+ await rename6(join21(input.vault, violation.relPath), join21(input.vault, target.relPath));
2883
+ }
2884
+ fixed.push({ from: violation.relPath, to: target.relPath });
2885
+ } catch {
2886
+ unresolved.push(violation.relPath);
2887
+ }
2888
+ }
2889
+ const rewired = [];
2890
+ if (fixed.length > 0) {
2891
+ const afterScan = await scanVault(input.vault);
2892
+ if (afterScan.ok) {
2893
+ for (const page of afterScan.data.allMarkdown) {
2894
+ if (!shouldRewriteReferences(page.relPath)) continue;
2895
+ try {
2896
+ const original = await readFile16(page.absPath, "utf8");
2897
+ let updated = original;
2898
+ for (const fix of fixed) {
2899
+ updated = replacePathReferences(updated, fix.from, fix.to);
2900
+ }
2901
+ if (updated !== original) {
2902
+ const write = await safeWritePage(page.absPath, updated);
2903
+ if (write.ok) rewired.push(page.relPath);
2904
+ else unresolved.push(`${page.relPath} (rewire)`);
2905
+ }
2906
+ } catch {
2907
+ unresolved.push(`${page.relPath} (rewire)`);
2908
+ }
2909
+ }
2910
+ }
2911
+ }
2912
+ const hintLines = [
2913
+ `fixed: ${fixed.length}`,
2914
+ `rewired: ${rewired.length}`,
2915
+ `unresolved: ${unresolved.length}`
2916
+ ];
2917
+ for (const f of fixed) hintLines.push(` ${f.from} -> ${f.to}`);
2918
+ for (const u of unresolved) hintLines.push(` unresolved: ${u}`);
2919
+ return {
2920
+ exitCode: unresolved.length > 0 ? ExitCode.LINT_HAS_ERRORS : ExitCode.OK,
2921
+ result: ok({ fixed, unresolved, rewired, humanHint: hintLines.join("\n") })
2922
+ };
2923
+ }
2924
+ function findPathTooLongViolations(pages, maxLength) {
2925
+ return pages.filter((page) => page.relPath.length > maxLength).map((page) => ({
2926
+ relPath: page.relPath,
2927
+ length: page.relPath.length,
2928
+ suggestedRelPath: truncateFilename(page.relPath, maxLength)
2929
+ }));
2930
+ }
2931
+ function maxFixPathLength(vault) {
2932
+ if (process.platform !== "win32") return MAX_PATH_LENGTH;
2933
+ const root = resolve4(vault);
2934
+ const separatorBudget = root.endsWith("\\") || root.endsWith("/") ? 0 : 1;
2935
+ const absoluteSafeRelLength = WINDOWS_ABSOLUTE_PATH_LIMIT - root.length - separatorBudget;
2936
+ return Math.max(1, Math.min(MAX_PATH_LENGTH, absoluteSafeRelLength));
2937
+ }
2633
2938
  function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
2634
2939
  if (relPath.length <= maxLength) return relPath;
2635
2940
  const lastSlash = relPath.lastIndexOf("/");
@@ -2643,15 +2948,62 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
2643
2948
  const maxPrefixLen = maxLength - dirPrefix.length - suffix.length;
2644
2949
  if (maxPrefixLen <= 0) {
2645
2950
  const fallback = dirPrefix + hash + ext;
2646
- if (fallback.length > maxLength) {
2647
- const dirBudget = maxLength - suffix.length;
2648
- return dirPrefix.slice(0, Math.max(0, dirBudget)) + suffix;
2649
- }
2650
- return fallback;
2951
+ return fallback.length <= maxLength ? fallback : relPath;
2651
2952
  }
2652
2953
  const prefix = base.slice(0, maxPrefixLen).replace(/[-_\s]+$/, "");
2653
2954
  return dirPrefix + prefix + suffix;
2654
2955
  }
2956
+ async function resolveFixTarget(vault, original, preferred, maxLength) {
2957
+ for (const candidate of candidateRelPaths(preferred, maxLength)) {
2958
+ if (candidate === original || candidate.length > maxLength) continue;
2959
+ const candidatePath = join21(vault, candidate);
2960
+ if (!existsSync4(candidatePath)) return { relPath: candidate, mode: "rename" };
2961
+ if (await hasSameContent(join21(vault, original), candidatePath)) {
2962
+ return { relPath: candidate, mode: "dedupe" };
2963
+ }
2964
+ }
2965
+ return null;
2966
+ }
2967
+ function candidateRelPaths(preferred, maxLength) {
2968
+ const candidates = [preferred];
2969
+ if (preferred.length > maxLength) return candidates;
2970
+ const dir = posix.dirname(preferred) === "." ? "" : posix.dirname(preferred);
2971
+ const filename = posix.basename(preferred);
2972
+ const ext = filename.endsWith(".md") ? ".md" : "";
2973
+ const base = ext ? filename.slice(0, -3) : filename;
2974
+ const dirPrefix = dir ? `${dir}/` : "";
2975
+ for (let i = 2; i < 100; i++) {
2976
+ const suffix = `-${i}${ext}`;
2977
+ const prefixBudget = maxLength - dirPrefix.length - suffix.length;
2978
+ if (prefixBudget <= 0) break;
2979
+ candidates.push(`${dirPrefix}${base.slice(0, prefixBudget).replace(/[-_\s]+$/, "")}${suffix}`);
2980
+ }
2981
+ return candidates;
2982
+ }
2983
+ async function hasSameContent(a, b) {
2984
+ try {
2985
+ const [left, right] = await Promise.all([readFile16(a), readFile16(b)]);
2986
+ return left.equals(right);
2987
+ } catch {
2988
+ return false;
2989
+ }
2990
+ }
2991
+ function shouldRewriteReferences(relPath) {
2992
+ if (relPath.startsWith("raw/")) return false;
2993
+ if (relPath.startsWith("_archive/")) return false;
2994
+ return true;
2995
+ }
2996
+ function replacePathReferences(content, oldRelPath, newRelPath) {
2997
+ let updated = content.replaceAll(oldRelPath, newRelPath);
2998
+ const oldStem = posix.basename(oldRelPath).replace(/\.md$/, "");
2999
+ const newStem = posix.basename(newRelPath).replace(/\.md$/, "");
3000
+ if (oldStem !== newStem) {
3001
+ const oldStemEscaped = oldStem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3002
+ const stemWikilinkRe = new RegExp(`\\[\\[${oldStemEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
3003
+ updated = updated.replace(stemWikilinkRe, (_match, alias) => `[[${newStem}${alias ?? ""}]]`);
3004
+ }
3005
+ return updated;
3006
+ }
2655
3007
  function computeShortHash(input) {
2656
3008
  let hash = 2166136261;
2657
3009
  for (let i = 0; i < input.length; i++) {
@@ -2689,11 +3041,12 @@ function buildCliSurface() {
2689
3041
  program2.command("claim").option("--project <slug>").option("--slug <slug>").option("--wiki <name>");
2690
3042
  program2.command("pagesize").option("--lines <n>").option("--wiki <name>");
2691
3043
  program2.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
3044
+ program2.command("log-append").requiredOption("--content <text>").option("--wiki <name>");
2692
3045
  program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--wiki <name>");
2693
3046
  program2.command("config");
2694
3047
  program2.command("doctor");
2695
3048
  program2.command("status").option("--wiki <name>");
2696
- program2.command("archive").option("--wiki <name>");
3049
+ program2.command("archive").option("--wiki <name>").option("--cascade").option("--apply");
2697
3050
  program2.command("drift").option("--apply").option("--new <date>").option("--wiki <name>");
2698
3051
  program2.command("dedup").option("--apply").option("--wiki <name>");
2699
3052
  program2.command("migrate-citations").option("--dry-run").option("--wiki <name>");
@@ -2726,6 +3079,9 @@ function buildCliSurface() {
2726
3079
  syncCmd2.command("status").option("--wiki <name>");
2727
3080
  syncCmd2.command("push").option("--wiki <name>");
2728
3081
  syncCmd2.command("pull").option("--wiki <name>");
3082
+ syncCmd2.command("lock").option("--summary <text>").option("--ttl-minutes <n>").option("--force").option("--wiki <name>");
3083
+ syncCmd2.command("unlock").option("--force").option("--wiki <name>");
3084
+ syncCmd2.command("peers").option("--wiki <name>");
2729
3085
  const backupCmd2 = program2.commands.find((c) => c.name() === "backup");
2730
3086
  backupCmd2.command("sync").option("--dry-run").option("--bucket <name>").option("--endpoint <url>").option("--region <region>").option("--prune").option("--wiki <name>");
2731
3087
  backupCmd2.command("restore").option("--bucket <name>").option("--endpoint <url>").option("--region <region>").option("--target <dir>").option("--wiki <name>");
@@ -2835,8 +3191,16 @@ function extractSourceEntries(rawFm) {
2835
3191
  }
2836
3192
  var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
2837
3193
  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"];
2838
- var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
3194
+ var INFO_ORDER = ["bridges", "sparse_community", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
3195
+ var KNOWN_BUCKETS = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
2839
3196
  async function runLint(input) {
3197
+ if (input.only && !KNOWN_BUCKETS.includes(input.only)) {
3198
+ return {
3199
+ exitCode: ExitCode.USAGE,
3200
+ result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${KNOWN_BUCKETS.join(", ")}` }
3201
+ };
3202
+ }
3203
+ const shouldFix = (bucket) => !!input.fix && (!input.only || input.only === bucket);
2840
3204
  const buckets = {};
2841
3205
  const fixed = [];
2842
3206
  const unresolved = [];
@@ -2878,6 +3242,10 @@ async function runLint(input) {
2878
3242
  if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
2879
3243
  if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
2880
3244
  }
3245
+ const sparse = await runSparseCommunity({ vault: input.vault });
3246
+ if (sparse.result.ok && sparse.result.data.communities.length > 0) {
3247
+ buckets.sparse_community = sparse.result.data.communities;
3248
+ }
2881
3249
  const topicMap = await runTopicMapCheck({ vault: input.vault });
2882
3250
  if (topicMap.result.ok && topicMap.result.data.recommended) {
2883
3251
  buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
@@ -2956,7 +3324,7 @@ async function runLint(input) {
2956
3324
  let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
2957
3325
  rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
2958
3326
  if (!rawPath.startsWith("raw/") && !rawPath.startsWith("_archive/raw/")) continue;
2959
- 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"))) {
3327
+ if (!existsSync5(join22(input.vault, rawPath)) && !existsSync5(join22(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync5(join22(input.vault, "_archive", rawPath)) && !existsSync5(join22(input.vault, "_archive", rawPath + ".md"))) {
2960
3328
  brokenSourceFlags.push(`${page.relPath}: ${rawPath}`);
2961
3329
  }
2962
3330
  }
@@ -3047,11 +3415,11 @@ async function runLint(input) {
3047
3415
  const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
3048
3416
  if (!slugMatch) continue;
3049
3417
  const slug = slugMatch[1];
3050
- const knowledgePath = join19(input.vault, "projects", slug, "knowledge.md");
3051
- if (!existsSync3(knowledgePath)) continue;
3418
+ const knowledgePath = join22(input.vault, "projects", slug, "knowledge.md");
3419
+ if (!existsSync5(knowledgePath)) continue;
3052
3420
  const pageRef = page.relPath.replace(/\.md$/, "");
3053
3421
  try {
3054
- const knowledgeContent = await readFile15(knowledgePath, "utf8");
3422
+ const knowledgeContent = await readFile17(knowledgePath, "utf8");
3055
3423
  if (!knowledgeContent.includes(`[[${pageRef}]]`)) {
3056
3424
  orphanedProjectPages.push(`${page.relPath}: not in projects/${slug}/knowledge.md`);
3057
3425
  }
@@ -3062,7 +3430,7 @@ async function runLint(input) {
3062
3430
  if (orphanedProjectPages.length > 0) buckets.orphaned_project_pages = orphanedProjectPages;
3063
3431
  const cliRefFlags = [];
3064
3432
  const cliSurface = buildCliSurface();
3065
- const allScanPages = [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound];
3433
+ const allScanPages = [...scan.data.typedKnowledge];
3066
3434
  for (const page of allScanPages) {
3067
3435
  const text = await readPage(page);
3068
3436
  const violations = validateCliRefs(text, page.relPath, cliSurface);
@@ -3092,13 +3460,13 @@ async function runLint(input) {
3092
3460
  }
3093
3461
  }
3094
3462
  if (staleSectionFlags.length > 0) buckets.stale_sections = staleSectionFlags;
3095
- if (input.fix && legacyPages.length > 0) {
3463
+ if (shouldFix("legacy_citation_style") && legacyPages.length > 0) {
3096
3464
  const FENCE_RE2 = /```[\s\S]*?```/g;
3097
3465
  const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
3098
3466
  for (const relPath of legacyPages) {
3099
3467
  try {
3100
3468
  const absPath = `${input.vault}/${relPath}`;
3101
- const raw = await readFile15(absPath, "utf8");
3469
+ const raw = await readFile17(absPath, "utf8");
3102
3470
  const split = splitFrontmatter(raw);
3103
3471
  if (!split.ok) {
3104
3472
  unresolved.push(relPath);
@@ -3193,11 +3561,11 @@ ${newBody}`;
3193
3561
  else delete buckets.legacy_citation_style;
3194
3562
  }
3195
3563
  }
3196
- if (input.fix && noOverview.length > 0) {
3564
+ if (shouldFix("missing_overview") && noOverview.length > 0) {
3197
3565
  for (const relPath of noOverview) {
3198
3566
  try {
3199
3567
  const absPath = `${input.vault}/${relPath}`;
3200
- const raw = await readFile15(absPath, "utf8");
3568
+ const raw = await readFile17(absPath, "utf8");
3201
3569
  const split = splitFrontmatter(raw);
3202
3570
  if (!split.ok) {
3203
3571
  unresolved.push(relPath);
@@ -3234,11 +3602,11 @@ ${trimmedBody}`;
3234
3602
  if (remaining.length > 0) buckets.missing_overview = remaining;
3235
3603
  else delete buckets.missing_overview;
3236
3604
  }
3237
- if (input.fix && missingTldrFlags.length > 0) {
3605
+ if (shouldFix("missing_tldr") && missingTldrFlags.length > 0) {
3238
3606
  for (const relPath of missingTldrFlags) {
3239
3607
  try {
3240
3608
  const absPath = `${input.vault}/${relPath}`;
3241
- const raw = await readFile15(absPath, "utf8");
3609
+ const raw = await readFile17(absPath, "utf8");
3242
3610
  const split = splitFrontmatter(raw);
3243
3611
  if (!split.ok) {
3244
3612
  unresolved.push(relPath);
@@ -3281,14 +3649,14 @@ ${lines.join("\n")}`;
3281
3649
  if (remaining.length > 0) buckets.missing_tldr = remaining;
3282
3650
  else delete buckets.missing_tldr;
3283
3651
  }
3284
- if (input.fix && wikilinkCitationFlags.length > 0) {
3652
+ if (shouldFix("wikilink_citation") && wikilinkCitationFlags.length > 0) {
3285
3653
  const WIKILINK_RE = /\[\[raw\/([^\]|]+)(?:\|[^\]]*)?\]\]/g;
3286
3654
  const FENCE_RE2 = /```[\s\S]*?```/g;
3287
3655
  const wikilinkFixed = [];
3288
3656
  for (const relPath of wikilinkCitationFlags) {
3289
3657
  try {
3290
3658
  const absPath = `${input.vault}/${relPath}`;
3291
- const raw = await readFile15(absPath, "utf8");
3659
+ const raw = await readFile17(absPath, "utf8");
3292
3660
  const split = splitFrontmatter(raw);
3293
3661
  if (!split.ok) {
3294
3662
  unresolved.push(relPath);
@@ -3370,12 +3738,12 @@ ${newBody}`;
3370
3738
  else delete buckets.wikilink_citation;
3371
3739
  }
3372
3740
  }
3373
- if (input.fix && fileSourceUrlFlags.length > 0) {
3741
+ if (shouldFix("file_source_url") && fileSourceUrlFlags.length > 0) {
3374
3742
  const FILE_FIXED = [];
3375
3743
  for (const relPath of fileSourceUrlFlags) {
3376
3744
  try {
3377
3745
  const absPath = `${input.vault}/${relPath}`;
3378
- const raw = await readFile15(absPath, "utf8");
3746
+ const raw = await readFile17(absPath, "utf8");
3379
3747
  const parts = raw.split("---", 3);
3380
3748
  if (parts.length < 3) {
3381
3749
  unresolved.push(relPath);
@@ -3410,36 +3778,11 @@ ${newBody}`;
3410
3778
  }
3411
3779
  }
3412
3780
  const pathViolations = buckets.path_too_long;
3413
- if (input.fix && pathViolations && pathViolations.length > 0) {
3414
- const pathFixed = [];
3415
- for (const v of pathViolations) {
3416
- try {
3417
- const absPath = `${input.vault}/${v.relPath}`;
3418
- const newRelPath = truncateFilename(v.relPath);
3419
- const newAbsPath = `${input.vault}/${newRelPath}`;
3420
- await rename5(absPath, newAbsPath);
3421
- const oldPathEscaped = v.relPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3422
- for (const page of allPages) {
3423
- if (page.relPath === v.relPath) continue;
3424
- const content = await readFile15(page.absPath, "utf8");
3425
- if (!content.includes(v.relPath)) continue;
3426
- let updated = content;
3427
- const citationRe = new RegExp(`\\^\\[${oldPathEscaped}\\]`, "g");
3428
- updated = updated.replace(citationRe, `^[${newRelPath}]`);
3429
- const wikilinkRe = new RegExp(`\\[\\[${oldPathEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
3430
- updated = updated.replace(wikilinkRe, (_m, alias) => `[[${newRelPath}${alias ?? ""}]]`);
3431
- if (updated !== content) {
3432
- const w = await safeWritePage(page.absPath, updated);
3433
- if (!w.ok) {
3434
- unresolved.push(`${page.relPath} (rewire)`);
3435
- }
3436
- }
3437
- }
3438
- pathFixed.push(v.relPath);
3439
- } catch {
3440
- unresolved.push(v.relPath);
3441
- }
3442
- }
3781
+ if (shouldFix("path_too_long") && pathViolations && pathViolations.length > 0) {
3782
+ const pathFix = await fixPathTooLong({ vault: input.vault });
3783
+ const pathFixed = pathFix.result.ok ? pathFix.result.data.fixed.map((f) => f.from) : [];
3784
+ if (pathFix.result.ok) unresolved.push(...pathFix.result.data.unresolved);
3785
+ else unresolved.push(...pathViolations.map((v) => v.relPath));
3443
3786
  fixed.push(...pathFixed);
3444
3787
  if (pathFixed.length > 0) {
3445
3788
  const fixedSet = new Set(pathFixed);
@@ -3453,13 +3796,6 @@ ${newBody}`;
3453
3796
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
3454
3797
  const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
3455
3798
  if (input.only) {
3456
- const allKnown = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
3457
- if (!allKnown.includes(input.only)) {
3458
- return {
3459
- exitCode: ExitCode.USAGE,
3460
- result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${allKnown.join(", ")}` }
3461
- };
3462
- }
3463
3799
  const match = [...errorOut, ...warningOut, ...infoOut].filter((b) => b.kind === input.only);
3464
3800
  const severity = ERROR_ORDER.includes(input.only) ? "error" : WARNING_ORDER.includes(input.only) ? "warning" : "info";
3465
3801
  const filtered = severity === "error" ? { error: match, warning: [], info: [] } : severity === "warning" ? { error: [], warning: match, info: [] } : { error: [], warning: [], info: match };
@@ -3523,14 +3859,14 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
3523
3859
  }
3524
3860
 
3525
3861
  // src/commands/config.ts
3526
- import { readFile as readFile16 } from "fs/promises";
3527
- import { existsSync as existsSync4 } from "fs";
3528
- import { join as join20 } from "path";
3862
+ import { readFile as readFile18 } from "fs/promises";
3863
+ import { existsSync as existsSync6 } from "fs";
3864
+ import { join as join23 } from "path";
3529
3865
  function validateKey(key) {
3530
3866
  return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
3531
3867
  }
3532
3868
  function configPath(home) {
3533
- return join20(home, ".skillwiki", ".env");
3869
+ return join23(home, ".skillwiki", ".env");
3534
3870
  }
3535
3871
  async function runConfigGet(input) {
3536
3872
  if (!validateKey(input.key)) {
@@ -3548,7 +3884,7 @@ async function runConfigSet(input) {
3548
3884
  try {
3549
3885
  let originalContent;
3550
3886
  try {
3551
- originalContent = await readFile16(filePath, "utf8");
3887
+ originalContent = await readFile18(filePath, "utf8");
3552
3888
  } catch {
3553
3889
  }
3554
3890
  const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
@@ -3580,18 +3916,18 @@ async function runConfigList(input) {
3580
3916
  }
3581
3917
  async function runConfigPath(input) {
3582
3918
  const filePath = configPath(input.home);
3583
- return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync4(filePath), humanHint: filePath }) };
3919
+ return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync6(filePath), humanHint: filePath }) };
3584
3920
  }
3585
3921
 
3586
3922
  // src/commands/doctor.ts
3587
- import { existsSync as existsSync7, lstatSync, readlinkSync, readdirSync, statSync as statSync2, readFileSync as readFileSync7 } from "fs";
3588
- import { join as join24, resolve as resolve4 } from "path";
3923
+ import { existsSync as existsSync9, lstatSync, readlinkSync, readdirSync, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
3924
+ import { join as join27, resolve as resolve5 } from "path";
3589
3925
  import { execSync as execSync2 } from "child_process";
3590
3926
  import { platform as platform2 } from "os";
3591
3927
 
3592
3928
  // src/utils/auto-update.ts
3593
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
3594
- import { join as join21, dirname as dirname8 } from "path";
3929
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
3930
+ import { join as join24, dirname as dirname9 } from "path";
3595
3931
  import { spawn } from "child_process";
3596
3932
 
3597
3933
  // src/utils/update-consts.ts
@@ -3602,7 +3938,7 @@ var CLI_DISABLE_FLAG = "--no-update-notifier";
3602
3938
 
3603
3939
  // src/utils/auto-update.ts
3604
3940
  function cachePath(home) {
3605
- return join21(home, ".skillwiki", CACHE_FILENAME);
3941
+ return join24(home, ".skillwiki", CACHE_FILENAME);
3606
3942
  }
3607
3943
  function readCacheRaw(home) {
3608
3944
  try {
@@ -3621,8 +3957,8 @@ function readCache(home) {
3621
3957
  }
3622
3958
  function writeCache(home, cache) {
3623
3959
  const p = cachePath(home);
3624
- mkdirSync2(dirname8(p), { recursive: true });
3625
- writeFileSync3(p, JSON.stringify(cache, null, 2));
3960
+ mkdirSync3(dirname9(p), { recursive: true });
3961
+ writeFileSync4(p, JSON.stringify(cache, null, 2));
3626
3962
  }
3627
3963
  function latestFromCache(home, currentVersion) {
3628
3964
  const { cache } = readCache(home);
@@ -3640,7 +3976,7 @@ function triggerAutoUpdate(home, currentVersion) {
3640
3976
  const { isStale: isStale2 } = readCache(home);
3641
3977
  if (!isStale2) return;
3642
3978
  const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
3643
- if (!existsSync5(bgScript)) return;
3979
+ if (!existsSync7(bgScript)) return;
3644
3980
  const child = spawn(process.execPath, [bgScript, home, currentVersion], {
3645
3981
  detached: true,
3646
3982
  stdio: "ignore"
@@ -3652,12 +3988,12 @@ function triggerAutoUpdate(home, currentVersion) {
3652
3988
 
3653
3989
  // src/utils/plugin-registry.ts
3654
3990
  import { readFileSync as readFileSync5 } from "fs";
3655
- import { join as join22 } from "path";
3656
- var REGISTRY_PATH = join22(".claude", "plugins", "installed_plugins.json");
3991
+ import { join as join25 } from "path";
3992
+ var REGISTRY_PATH = join25(".claude", "plugins", "installed_plugins.json");
3657
3993
  var PLUGIN_KEY = "skillwiki@llm-wiki";
3658
3994
  function readInstalledPlugins(home) {
3659
3995
  try {
3660
- const raw = readFileSync5(join22(home, REGISTRY_PATH), "utf8");
3996
+ const raw = readFileSync5(join25(home, REGISTRY_PATH), "utf8");
3661
3997
  return JSON.parse(raw);
3662
3998
  } catch {
3663
3999
  return null;
@@ -3674,8 +4010,8 @@ function findPlugin(home, key = PLUGIN_KEY) {
3674
4010
  // src/utils/s3-mount-health.ts
3675
4011
  import { execSync } from "child_process";
3676
4012
  import { platform } from "os";
3677
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, readFileSync as readFile17 } from "fs";
3678
- import { join as join23 } from "path";
4013
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readFileSync as readFile19 } from "fs";
4014
+ import { join as join26 } from "path";
3679
4015
  var OS = platform();
3680
4016
  function findRcloneMountPid() {
3681
4017
  try {
@@ -3833,39 +4169,70 @@ function detectFuseMount(vaultPath) {
3833
4169
  return null;
3834
4170
  }
3835
4171
  function writeTest(dir) {
3836
- const testFile = join23(dir, `.doctor-write-test-${process.pid}.tmp`);
4172
+ const testFile = join26(dir, `.doctor-write-test-${process.pid}.tmp`);
3837
4173
  const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
3838
4174
  const start = Date.now();
3839
4175
  try {
3840
- writeFileSync4(testFile, payload, "utf8");
4176
+ writeFileSync5(testFile, payload, "utf8");
3841
4177
  } catch (e) {
3842
4178
  return { success: false, writeMs: Date.now() - start, readMs: 0, size: 0, error: `write failed: ${e.message}` };
3843
4179
  }
3844
4180
  const writeMs = Date.now() - start;
3845
4181
  const readStart = Date.now();
3846
4182
  try {
3847
- const back = readFile17(testFile, "utf8");
4183
+ const back = readFile19(testFile, "utf8");
3848
4184
  const readMs = Date.now() - readStart;
3849
4185
  if (back !== payload) {
3850
4186
  try {
3851
- unlinkSync3(testFile);
4187
+ unlinkSync4(testFile);
3852
4188
  } catch {
3853
4189
  }
3854
4190
  return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
3855
4191
  }
3856
4192
  } catch (e) {
3857
4193
  try {
3858
- unlinkSync3(testFile);
4194
+ unlinkSync4(testFile);
3859
4195
  } catch {
3860
4196
  }
3861
4197
  return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
3862
4198
  }
3863
4199
  try {
3864
- unlinkSync3(testFile);
4200
+ unlinkSync4(testFile);
3865
4201
  } catch {
3866
4202
  }
3867
4203
  return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
3868
4204
  }
4205
+ var DURATION_UNIT_SECONDS = {
4206
+ ms: 1 / 1e3,
4207
+ s: 1,
4208
+ m: 60,
4209
+ h: 3600,
4210
+ d: 86400,
4211
+ w: 604800
4212
+ };
4213
+ function parseDurationSeconds(raw) {
4214
+ const input = raw.trim().toLowerCase();
4215
+ if (!input) return null;
4216
+ if (/^\d+(?:\.\d+)?$/.test(input)) {
4217
+ const num = parseFloat(input);
4218
+ return Number.isFinite(num) ? num : null;
4219
+ }
4220
+ const re = /(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/g;
4221
+ let total = 0;
4222
+ let consumed = 0;
4223
+ for (const match of input.matchAll(re)) {
4224
+ const full = match[0];
4225
+ const value = parseFloat(match[1]);
4226
+ const unit = match[2];
4227
+ if (!Number.isFinite(value)) return null;
4228
+ const factor = DURATION_UNIT_SECONDS[unit];
4229
+ if (factor === void 0) return null;
4230
+ total += value * factor;
4231
+ consumed += full.length;
4232
+ }
4233
+ if (consumed !== input.length) return null;
4234
+ return total;
4235
+ }
3869
4236
  var FLAG_THRESHOLDS = {
3870
4237
  "--vfs-write-back": { min: 15, unit: "s", label: "VFS write-back window" },
3871
4238
  "--vfs-write-wait": { min: 10, unit: "s", label: "VFS write-wait" },
@@ -3887,14 +4254,14 @@ function checkNodeVersion() {
3887
4254
  function detectCliChannels(argv, home) {
3888
4255
  const channels = [];
3889
4256
  if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
3890
- const devPath = resolve4(argv[1]);
4257
+ const devPath = resolve5(argv[1]);
3891
4258
  channels.push({ name: "dev", path: devPath, isDevLink: true });
3892
4259
  }
3893
4260
  try {
3894
4261
  const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
3895
4262
  if (whichOut) {
3896
4263
  const isDev = isDevSymlink(whichOut);
3897
- if (!channels.some((c) => c.path === resolve4(whichOut))) {
4264
+ if (!channels.some((c) => c.path === resolve5(whichOut))) {
3898
4265
  channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
3899
4266
  }
3900
4267
  }
@@ -3902,13 +4269,13 @@ function detectCliChannels(argv, home) {
3902
4269
  }
3903
4270
  const plugin = findPlugin(home);
3904
4271
  if (plugin) {
3905
- const pluginBin = join24(plugin.installPath, "bin", "skillwiki");
3906
- if (existsSync7(pluginBin)) {
4272
+ const pluginBin = join27(plugin.installPath, "bin", "skillwiki");
4273
+ if (existsSync9(pluginBin)) {
3907
4274
  channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
3908
4275
  }
3909
4276
  }
3910
- const installBin = join24(home, ".claude", "skills", "bin", "skillwiki");
3911
- if (existsSync7(installBin)) {
4277
+ const installBin = join27(home, ".claude", "skills", "bin", "skillwiki");
4278
+ if (existsSync9(installBin)) {
3912
4279
  channels.push({ name: "install", path: installBin, isDevLink: false });
3913
4280
  }
3914
4281
  return channels;
@@ -3917,7 +4284,7 @@ function isDevSymlink(binPath) {
3917
4284
  try {
3918
4285
  const st = lstatSync(binPath);
3919
4286
  if (st.isSymbolicLink()) {
3920
- const target = resolve4(binPath, "..", readlinkSync(binPath));
4287
+ const target = resolve5(binPath, "..", readlinkSync(binPath));
3921
4288
  return target.includes("packages/cli") || target.includes("packages\\cli");
3922
4289
  }
3923
4290
  } catch {
@@ -3960,7 +4327,7 @@ function checkCliChannels(argv, home) {
3960
4327
  }
3961
4328
  async function checkConfigFile(home) {
3962
4329
  const cfgPath = configPath(home);
3963
- if (!existsSync7(cfgPath)) {
4330
+ if (!existsSync9(cfgPath)) {
3964
4331
  return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
3965
4332
  }
3966
4333
  try {
@@ -3975,7 +4342,7 @@ function checkWikiPathExists(resolvedPath) {
3975
4342
  if (resolvedPath === void 0) {
3976
4343
  return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
3977
4344
  }
3978
- if (existsSync7(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
4345
+ if (existsSync9(resolvedPath) && statSync3(resolvedPath).isDirectory()) {
3979
4346
  return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
3980
4347
  }
3981
4348
  return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
@@ -3984,13 +4351,13 @@ function checkVaultStructure(resolvedPath) {
3984
4351
  if (resolvedPath === void 0) {
3985
4352
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
3986
4353
  }
3987
- if (!existsSync7(resolvedPath)) {
4354
+ if (!existsSync9(resolvedPath)) {
3988
4355
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
3989
4356
  }
3990
4357
  const missing = [];
3991
- if (!existsSync7(join24(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
4358
+ if (!existsSync9(join27(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
3992
4359
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
3993
- if (!existsSync7(join24(resolvedPath, dir))) missing.push(dir + "/");
4360
+ if (!existsSync9(join27(resolvedPath, dir))) missing.push(dir + "/");
3994
4361
  }
3995
4362
  if (missing.length === 0) {
3996
4363
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -3998,23 +4365,23 @@ function checkVaultStructure(resolvedPath) {
3998
4365
  return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
3999
4366
  }
4000
4367
  function checkSkillsInstalled(home, cwd) {
4001
- const srcDir = cwd ? join24(cwd, "packages", "skills") : void 0;
4002
- if (srcDir && existsSync7(srcDir)) {
4003
- const found = findSkillMd(srcDir);
4368
+ const srcDir = cwd ? join27(cwd, "packages", "skills") : void 0;
4369
+ if (srcDir && existsSync9(srcDir)) {
4370
+ const found = findInstalledSkillMd(srcDir);
4004
4371
  if (found.length > 0) {
4005
4372
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
4006
4373
  }
4007
4374
  }
4008
4375
  const plugin = findPlugin(home);
4009
4376
  if (plugin) {
4010
- const found = findSkillMd(plugin.installPath);
4377
+ const found = findInstalledSkillMd(plugin.installPath);
4011
4378
  if (found.length > 0) {
4012
4379
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
4013
4380
  }
4014
4381
  }
4015
- const skillsDir = join24(home, ".claude", "skills");
4016
- if (existsSync7(skillsDir)) {
4017
- const found = findSkillMd(skillsDir);
4382
+ const skillsDir = join27(home, ".claude", "skills");
4383
+ if (existsSync9(skillsDir)) {
4384
+ const found = findInstalledSkillMd(skillsDir);
4018
4385
  if (found.length > 0) {
4019
4386
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
4020
4387
  }
@@ -4023,10 +4390,10 @@ function checkSkillsInstalled(home, cwd) {
4023
4390
  }
4024
4391
  function checkDuplicateSkills(home) {
4025
4392
  const plugin = findPlugin(home);
4026
- const skillsDir = join24(home, ".claude", "skills");
4393
+ const skillsDir = join27(home, ".claude", "skills");
4027
4394
  const agentSkillDirs = [
4028
- { label: "~/.codex/skills/", path: join24(home, ".codex", "skills") },
4029
- { label: "~/.agents/skills/", path: join24(home, ".agents", "skills") }
4395
+ { label: "~/.codex/skills/", path: join27(home, ".codex", "skills") },
4396
+ { label: "~/.agents/skills/", path: join27(home, ".agents", "skills") }
4030
4397
  ];
4031
4398
  if (!plugin) {
4032
4399
  return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
@@ -4103,8 +4470,8 @@ async function checkProfiles(home) {
4103
4470
  }
4104
4471
  async function checkProjectLocalOverride(cwd) {
4105
4472
  const dir = cwd ?? process.cwd();
4106
- const envPath = join24(dir, ".skillwiki", ".env");
4107
- if (existsSync7(envPath)) {
4473
+ const envPath = join27(dir, ".skillwiki", ".env");
4474
+ if (existsSync9(envPath)) {
4108
4475
  return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
4109
4476
  }
4110
4477
  return check("pass", "project_local", "Project-local config", "None");
@@ -4113,7 +4480,7 @@ function checkVaultGitRemote(resolvedPath) {
4113
4480
  if (resolvedPath === void 0) {
4114
4481
  return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
4115
4482
  }
4116
- if (!existsSync7(join24(resolvedPath, ".git"))) {
4483
+ if (!existsSync9(join27(resolvedPath, ".git"))) {
4117
4484
  return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
4118
4485
  }
4119
4486
  try {
@@ -4136,9 +4503,9 @@ function checkObsidianTemplates(resolvedPath) {
4136
4503
  return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
4137
4504
  }
4138
4505
  const missing = [];
4139
- if (!existsSync7(join24(resolvedPath, "_Templates"))) missing.push("_Templates/");
4140
- if (!existsSync7(join24(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
4141
- if (!existsSync7(join24(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
4506
+ if (!existsSync9(join27(resolvedPath, "_Templates"))) missing.push("_Templates/");
4507
+ if (!existsSync9(join27(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
4508
+ if (!existsSync9(join27(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
4142
4509
  if (missing.length === 0) {
4143
4510
  return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
4144
4511
  }
@@ -4148,8 +4515,8 @@ function checkDotStoreClean(resolvedPath) {
4148
4515
  if (resolvedPath === void 0) {
4149
4516
  return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
4150
4517
  }
4151
- const rawDir = join24(resolvedPath, "raw");
4152
- if (!existsSync7(rawDir)) {
4518
+ const rawDir = join27(resolvedPath, "raw");
4519
+ if (!existsSync9(rawDir)) {
4153
4520
  return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
4154
4521
  }
4155
4522
  const found = [];
@@ -4164,7 +4531,7 @@ function checkDotStoreClean(resolvedPath) {
4164
4531
  if (entry.name === ".DS_Store") {
4165
4532
  found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
4166
4533
  } else if (entry.isDirectory()) {
4167
- walk2(join24(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
4534
+ walk2(join27(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
4168
4535
  }
4169
4536
  }
4170
4537
  })(rawDir, "");
@@ -4177,7 +4544,7 @@ function checkSyncLastPush(resolvedPath) {
4177
4544
  if (resolvedPath === void 0) {
4178
4545
  return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
4179
4546
  }
4180
- if (!existsSync7(join24(resolvedPath, ".git"))) {
4547
+ if (!existsSync9(join27(resolvedPath, ".git"))) {
4181
4548
  return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
4182
4549
  }
4183
4550
  let timestamp;
@@ -4218,8 +4585,8 @@ function checkS3MountPerf(resolvedPath) {
4218
4585
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
4219
4586
  }
4220
4587
  const mountPoint = fuse.mountPoint;
4221
- const conceptsDir = join24(resolvedPath, "concepts");
4222
- if (!existsSync7(conceptsDir)) {
4588
+ const conceptsDir = join27(resolvedPath, "concepts");
4589
+ if (!existsSync9(conceptsDir)) {
4223
4590
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
4224
4591
  }
4225
4592
  const start = Date.now();
@@ -4258,6 +4625,73 @@ function checkS3MountPerf(resolvedPath) {
4258
4625
  `S3 FUSE mount, cache warm (rg scan: ${elapsed.toFixed(3)}s)`
4259
4626
  );
4260
4627
  }
4628
+ var MAX_DIR_CACHE_TIME_SECONDS = 15 * 60;
4629
+ function formatDurationForHumans(seconds) {
4630
+ if (!Number.isFinite(seconds)) return `${seconds}s`;
4631
+ if (seconds >= 3600) return `${(seconds / 3600).toFixed(1)}h`;
4632
+ if (seconds >= 60) return `${(seconds / 60).toFixed(1)}m`;
4633
+ if (seconds >= 1) return `${seconds.toFixed(1)}s`;
4634
+ return `${Math.round(seconds * 1e3)}ms`;
4635
+ }
4636
+ function checkS3MountFreshness(resolvedPath) {
4637
+ if (!resolvedPath) {
4638
+ return check("pass", "s3_mount_freshness", "S3 visibility freshness", "No vault path \u2014 check skipped");
4639
+ }
4640
+ const fuse = detectFuseMount(resolvedPath);
4641
+ if (!fuse) {
4642
+ return check("pass", "s3_mount_freshness", "S3 visibility freshness", "local disk \u2014 check skipped");
4643
+ }
4644
+ const pid = findRcloneMountPid();
4645
+ if (pid === null) {
4646
+ return check(
4647
+ "warn",
4648
+ "s3_mount_freshness",
4649
+ "S3 visibility freshness",
4650
+ `S3 FUSE mount (${fuse.mountPoint}) but no rclone process found \u2014 cannot audit --dir-cache-time`
4651
+ );
4652
+ }
4653
+ const flags = parseRcloneFlags(pid);
4654
+ if (flags.size === 0) {
4655
+ return check(
4656
+ "warn",
4657
+ "s3_mount_freshness",
4658
+ "S3 visibility freshness",
4659
+ `rclone PID ${pid} found but could not parse flags`
4660
+ );
4661
+ }
4662
+ const raw = flags.get("--dir-cache-time");
4663
+ if (!raw) {
4664
+ return check(
4665
+ "pass",
4666
+ "s3_mount_freshness",
4667
+ "S3 visibility freshness",
4668
+ "PID " + pid + ": --dir-cache-time not set (rclone default 5m, within <=15m SLA)"
4669
+ );
4670
+ }
4671
+ const seconds = parseDurationSeconds(raw);
4672
+ if (seconds === null) {
4673
+ return check(
4674
+ "warn",
4675
+ "s3_mount_freshness",
4676
+ "S3 visibility freshness",
4677
+ `PID ${pid}: could not parse --dir-cache-time=${raw}`
4678
+ );
4679
+ }
4680
+ if (seconds > MAX_DIR_CACHE_TIME_SECONDS) {
4681
+ return check(
4682
+ "warn",
4683
+ "s3_mount_freshness",
4684
+ "S3 visibility freshness",
4685
+ `PID ${pid}: --dir-cache-time=${raw} (${formatDurationForHumans(seconds)}) exceeds 15m SLA \u2014 external S3 changes may remain invisible`
4686
+ );
4687
+ }
4688
+ return check(
4689
+ "pass",
4690
+ "s3_mount_freshness",
4691
+ "S3 visibility freshness",
4692
+ `PID ${pid}: --dir-cache-time=${raw} (${formatDurationForHumans(seconds)}), within <=15m SLA`
4693
+ );
4694
+ }
4261
4695
  function checkRcloneFlagAudit(resolvedPath) {
4262
4696
  if (!resolvedPath) {
4263
4697
  return check("pass", "rclone_flags", "rclone VFS flags", "No vault path \u2014 check skipped");
@@ -4281,11 +4715,8 @@ function checkRcloneFlagAudit(resolvedPath) {
4281
4715
  warnings.push(`${flag} not set (default may be unsafe)`);
4282
4716
  continue;
4283
4717
  }
4284
- const value = parseFloat(raw);
4285
- if (isNaN(value)) continue;
4286
- let inSeconds = value;
4287
- if (raw.endsWith("h")) inSeconds = value * 3600;
4288
- else if (raw.endsWith("m")) inSeconds = value * 60;
4718
+ const inSeconds = parseDurationSeconds(raw);
4719
+ if (inSeconds === null) continue;
4289
4720
  const thresholdSec = threshold.unit === "h" ? threshold.min * 3600 : threshold.unit === "m" ? threshold.min * 60 : threshold.min;
4290
4721
  if (inSeconds < thresholdSec) {
4291
4722
  warnings.push(`${flag}=${raw} (recommended \u2265${threshold.min}${threshold.unit})`);
@@ -4337,8 +4768,8 @@ function checkWriteTest(resolvedPath) {
4337
4768
  if (!fuse) {
4338
4769
  return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
4339
4770
  }
4340
- const conceptsDir = join24(resolvedPath, "concepts");
4341
- if (!existsSync7(conceptsDir)) {
4771
+ const conceptsDir = join27(resolvedPath, "concepts");
4772
+ if (!existsSync9(conceptsDir)) {
4342
4773
  return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
4343
4774
  }
4344
4775
  const result = writeTest(conceptsDir);
@@ -4424,7 +4855,7 @@ function checkVfsCacheHealth(resolvedPath) {
4424
4855
  }
4425
4856
  function readVaultSyncConfig(home) {
4426
4857
  try {
4427
- const content = readFileSync7(join24(home, ".skillwiki", ".env"), "utf8");
4858
+ const content = readFileSync7(join27(home, ".skillwiki", ".env"), "utf8");
4428
4859
  let installed = false;
4429
4860
  let role;
4430
4861
  for (const line of content.split(/\r?\n/)) {
@@ -4458,12 +4889,12 @@ function vaultSyncChecks(input) {
4458
4889
  ];
4459
4890
  }
4460
4891
  const isMac = os === "darwin";
4461
- const logDir = input.logDir ?? (isMac ? join24(home, "Library", "Logs") : join24(home, ".local", "state", "vault-sync", "log"));
4462
- const shareDir = input.shareDir ?? (isMac ? join24(home, "Library", "Application Support", "vault-sync", "bin") : join24(home, ".local", "share", "vault-sync", "bin"));
4463
- const filterPath = input.filterPath ?? join24(home, ".config", "rclone", "wiki-push-filters.txt");
4892
+ const logDir = input.logDir ?? (isMac ? join27(home, "Library", "Logs") : join27(home, ".local", "state", "vault-sync", "log"));
4893
+ const shareDir = input.shareDir ?? (isMac ? join27(home, "Library", "Application Support", "vault-sync", "bin") : join27(home, ".local", "share", "vault-sync", "bin"));
4894
+ const filterPath = input.filterPath ?? join27(home, ".config", "rclone", "wiki-push-filters.txt");
4464
4895
  const snapshotPath = input.snapshotScriptPath ?? "/root/.hermes/scripts/wiki-snapshot-v3.sh";
4465
- const pushScriptPath = join24(shareDir, "wiki-push.sh");
4466
- const c1 = existsSync7(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
4896
+ const pushScriptPath = join27(shareDir, "wiki-push.sh");
4897
+ const c1 = existsSync9(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
4467
4898
  let c2;
4468
4899
  try {
4469
4900
  if (isMac) {
@@ -4514,7 +4945,7 @@ function vaultSyncChecks(input) {
4514
4945
  "Scheduler check failed \u2014 run vault-sync-install"
4515
4946
  );
4516
4947
  }
4517
- const logFile = join24(logDir, "wiki-push.log");
4948
+ const logFile = join27(logDir, "wiki-push.log");
4518
4949
  let c3;
4519
4950
  try {
4520
4951
  const logContent = readFileSync7(logFile, "utf8");
@@ -4573,7 +5004,7 @@ function vaultSyncChecks(input) {
4573
5004
  }
4574
5005
  }
4575
5006
  } catch {
4576
- c3 = existsSync7(logDir) ? check(
5007
+ c3 = existsSync9(logDir) ? check(
4577
5008
  "warn",
4578
5009
  "vault_sync_last_push_age",
4579
5010
  "Vault sync last push recency",
@@ -4585,7 +5016,7 @@ function vaultSyncChecks(input) {
4585
5016
  `Log directory not found at ${logDir}`
4586
5017
  );
4587
5018
  }
4588
- const fetchLogFile = join24(logDir, "wiki-fetch.log");
5019
+ const fetchLogFile = join27(logDir, "wiki-fetch.log");
4589
5020
  let cFetch;
4590
5021
  try {
4591
5022
  const logContent = readFileSync7(fetchLogFile, "utf8");
@@ -4632,7 +5063,7 @@ function vaultSyncChecks(input) {
4632
5063
  }
4633
5064
  let c4;
4634
5065
  try {
4635
- if (!existsSync7(filterPath)) {
5066
+ if (!existsSync9(filterPath)) {
4636
5067
  c4 = check(
4637
5068
  "error",
4638
5069
  "vault_sync_filter_present",
@@ -4681,7 +5112,7 @@ function vaultSyncChecks(input) {
4681
5112
  );
4682
5113
  } else {
4683
5114
  try {
4684
- if (!existsSync7(snapshotPath)) {
5115
+ if (!existsSync9(snapshotPath)) {
4685
5116
  c5 = check(
4686
5117
  "error",
4687
5118
  "vault_sync_snapshot_guard",
@@ -4727,13 +5158,17 @@ function findSkillMd(dir) {
4727
5158
  }
4728
5159
  for (const entry of entries) {
4729
5160
  if (entry.isFile() && entry.name === "SKILL.md") {
4730
- results.push(join24(dir, entry.name));
5161
+ results.push(join27(dir, entry.name));
4731
5162
  } else if (entry.isDirectory()) {
4732
- results.push(...findSkillMd(join24(dir, entry.name)));
5163
+ results.push(...findSkillMd(join27(dir, entry.name)));
4733
5164
  }
4734
5165
  }
4735
5166
  return results;
4736
5167
  }
5168
+ function findInstalledSkillMd(dir) {
5169
+ const directSkills = findSkillNames(dir).map((name) => join27(dir, name, "SKILL.md"));
5170
+ return directSkills.length > 0 ? directSkills : findSkillMd(dir);
5171
+ }
4737
5172
  function findSkillNames(dir) {
4738
5173
  const results = [];
4739
5174
  let entries;
@@ -4743,12 +5178,61 @@ function findSkillNames(dir) {
4743
5178
  return results;
4744
5179
  }
4745
5180
  for (const entry of entries) {
4746
- if (entry.isDirectory() && existsSync7(join24(dir, entry.name, "SKILL.md"))) {
5181
+ if (entry.isDirectory() && existsSync9(join27(dir, entry.name, "SKILL.md"))) {
4747
5182
  results.push(entry.name);
4748
5183
  }
4749
5184
  }
4750
5185
  return results;
4751
5186
  }
5187
+ var METRIC_TYPES = ["entities", "concepts", "comparisons", "queries", "meta"];
5188
+ async function vaultMetrics(resolvedPath) {
5189
+ const ids = [
5190
+ ["vault_metric_pages", "Vault pages by type"],
5191
+ ["vault_metric_orphans", "Vault orphan rate"],
5192
+ ["vault_metric_bridges", "Vault bridge count"],
5193
+ ["vault_metric_cohesion", "Mean community cohesion"],
5194
+ ["vault_metric_log_size", "Vault log size"]
5195
+ ];
5196
+ const noVault = () => ids.map(([id, label]) => check("info", id, label, "no vault configured"));
5197
+ if (!resolvedPath) return noVault();
5198
+ const scan = await scanVault(resolvedPath);
5199
+ if (!scan.ok) return noVault();
5200
+ const tk = scan.data.typedKnowledge;
5201
+ const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
5202
+ const adj = await buildWikilinkAdjacency(tk);
5203
+ const g = toUndirectedWeighted(adj);
5204
+ const nodes = [...g.keys()];
5205
+ const total = nodes.length;
5206
+ const orphanCount = nodes.filter((n) => g.get(n).size === 0).length;
5207
+ const orphanRate = total > 0 ? Math.round(orphanCount / total * 1e3) / 10 : 0;
5208
+ const comm = louvain(g);
5209
+ const groups = /* @__PURE__ */ new Map();
5210
+ for (const [node, c] of comm) {
5211
+ const arr = groups.get(c);
5212
+ if (arr) arr.push(node);
5213
+ else groups.set(c, [node]);
5214
+ }
5215
+ const cohesions = [...groups.values()].filter((m) => m.length >= 2).map((m) => communityCohesion(m, g));
5216
+ const meanCohesion = cohesions.length > 0 ? Math.round(cohesions.reduce((a, b) => a + b, 0) / cohesions.length * 1e3) / 1e3 : 0;
5217
+ let bridges = 0;
5218
+ for (const n of nodes) {
5219
+ const nbrComms = /* @__PURE__ */ new Set();
5220
+ for (const nb of g.get(n).keys()) nbrComms.add(comm.get(nb));
5221
+ if (nbrComms.size >= 3) bridges++;
5222
+ }
5223
+ let logLines = 0;
5224
+ try {
5225
+ logLines = readFileSync7(join27(resolvedPath, "log.md"), "utf8").split("\n").length;
5226
+ } catch {
5227
+ }
5228
+ return [
5229
+ check("info", "vault_metric_pages", "Vault pages by type", `${total} typed (${perType})`),
5230
+ check("info", "vault_metric_orphans", "Vault orphan rate", `${orphanRate}% (${orphanCount}/${total} degree-0)`),
5231
+ check("info", "vault_metric_bridges", "Vault bridge count", `${bridges} page(s) link >= 3 communities`),
5232
+ check("info", "vault_metric_cohesion", "Mean community cohesion", `${meanCohesion} across ${cohesions.length} communities (size >= 2)`),
5233
+ check("info", "vault_metric_log_size", "Vault log size", `${logLines} lines`)
5234
+ ];
5235
+ }
4752
5236
  async function runDoctor(input) {
4753
5237
  const checks = [];
4754
5238
  const vsConfig = readVaultSyncConfig(input.home);
@@ -4771,6 +5255,7 @@ async function runDoctor(input) {
4771
5255
  checks.push(checkSyncLastPush(resolvedPath));
4772
5256
  checks.push(checkDotStoreClean(resolvedPath));
4773
5257
  checks.push(checkS3MountPerf(resolvedPath));
5258
+ checks.push(checkS3MountFreshness(resolvedPath));
4774
5259
  checks.push(checkRcloneFlagAudit(resolvedPath));
4775
5260
  checks.push(checkRcloneVersion(resolvedPath, vsConfig.installed));
4776
5261
  checks.push(checkWriteTest(resolvedPath));
@@ -4784,6 +5269,7 @@ async function runDoctor(input) {
4784
5269
  vaultSyncInstalled: vsConfig.installed,
4785
5270
  vaultSyncRole: vsConfig.role
4786
5271
  }));
5272
+ checks.push(...await vaultMetrics(resolvedPath));
4787
5273
  const summary = {
4788
5274
  pass: checks.filter((c) => c.status === "pass").length,
4789
5275
  info: checks.filter((c) => c.status === "info").length,
@@ -4807,8 +5293,8 @@ async function runDoctor(input) {
4807
5293
  }
4808
5294
 
4809
5295
  // src/commands/archive.ts
4810
- import { rename as rename6, mkdir as mkdir8, readFile as readFile18, writeFile as writeFile9 } from "fs/promises";
4811
- import { join as join25, dirname as dirname9 } from "path";
5296
+ import { rename as rename7, mkdir as mkdir9, readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
5297
+ import { join as join28, dirname as dirname10 } from "path";
4812
5298
  function countWikilinks(body, slug) {
4813
5299
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4814
5300
  const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
@@ -4836,7 +5322,7 @@ async function runArchive(input) {
4836
5322
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
4837
5323
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
4838
5324
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
4839
- const archivePath = join25("_archive", relPath).replace(/\\/g, "/");
5325
+ const archivePath = join28("_archive", relPath).replace(/\\/g, "/");
4840
5326
  let cascade;
4841
5327
  if (input.cascade) {
4842
5328
  const wikilinkRefs = [];
@@ -4860,7 +5346,7 @@ async function runArchive(input) {
4860
5346
  const indexRefs = [];
4861
5347
  if (!isRaw) {
4862
5348
  try {
4863
- const idx = await readFile18(join25(input.vault, "index.md"), "utf8");
5349
+ const idx = await readFile20(join28(input.vault, "index.md"), "utf8");
4864
5350
  idx.split("\n").forEach((line, i) => {
4865
5351
  if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
4866
5352
  });
@@ -4886,8 +5372,8 @@ async function runArchive(input) {
4886
5372
  }
4887
5373
  if (input.cascade && input.apply && cascade) {
4888
5374
  for (const ref of cascade.source_array_refs) {
4889
- const absPath = join25(input.vault, ref.page);
4890
- const text = await readFile18(absPath, "utf8");
5375
+ const absPath = join28(input.vault, ref.page);
5376
+ const text = await readFile20(absPath, "utf8");
4891
5377
  const split = splitFrontmatter(text);
4892
5378
  if (!split.ok) continue;
4893
5379
  const before = split.data.rawFrontmatter;
@@ -4898,29 +5384,29 @@ async function runArchive(input) {
4898
5384
  );
4899
5385
  if (fmRewritten === before) continue;
4900
5386
  if (!arraysEqual(ref.sources_after, ref.sources_before)) {
4901
- await writeFile9(absPath, `---
5387
+ await writeFile10(absPath, `---
4902
5388
  ${fmRewritten}
4903
5389
  ---${split.data.body}`, "utf8");
4904
5390
  }
4905
5391
  }
4906
5392
  }
4907
- await mkdir8(dirname9(join25(input.vault, archivePath)), { recursive: true });
5393
+ await mkdir9(dirname10(join28(input.vault, archivePath)), { recursive: true });
4908
5394
  let indexUpdated = false;
4909
5395
  if (!isRaw) {
4910
- const indexPath = join25(input.vault, "index.md");
5396
+ const indexPath = join28(input.vault, "index.md");
4911
5397
  try {
4912
- const idx = await readFile18(indexPath, "utf8");
5398
+ const idx = await readFile20(indexPath, "utf8");
4913
5399
  const originalLines = idx.split("\n");
4914
5400
  const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
4915
5401
  if (filtered.length !== originalLines.length) {
4916
- await writeFile9(indexPath, filtered.join("\n"), "utf8");
5402
+ await writeFile10(indexPath, filtered.join("\n"), "utf8");
4917
5403
  indexUpdated = true;
4918
5404
  }
4919
5405
  } catch (e) {
4920
5406
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
4921
5407
  }
4922
5408
  }
4923
- await rename6(join25(input.vault, relPath), join25(input.vault, archivePath));
5409
+ await rename7(join28(input.vault, relPath), join28(input.vault, archivePath));
4924
5410
  appendLastOp(input.vault, {
4925
5411
  operation: input.cascade ? "archive-cascade" : "archive",
4926
5412
  summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
@@ -5307,14 +5793,14 @@ ${newBody}`;
5307
5793
  // src/commands/update.ts
5308
5794
  import { execSync as execSync3 } from "child_process";
5309
5795
  import { readFileSync as readFileSync8 } from "fs";
5310
- import { join as join26 } from "path";
5796
+ import { join as join29 } from "path";
5311
5797
  function resolveGlobalSkillsRoot() {
5312
5798
  try {
5313
5799
  const globalRoot = execSync3("npm root -g", {
5314
5800
  encoding: "utf8",
5315
5801
  timeout: 5e3
5316
5802
  }).trim();
5317
- return join26(globalRoot, "skillwiki", "skills");
5803
+ return join29(globalRoot, "skillwiki", "skills");
5318
5804
  } catch {
5319
5805
  return null;
5320
5806
  }
@@ -5340,7 +5826,7 @@ async function runUpdate(input) {
5340
5826
  );
5341
5827
  const currentVersion = pkg2.version;
5342
5828
  const tag = input.distTag ?? "latest";
5343
- const target = join26(input.home, ".claude", "skills");
5829
+ const target = join29(input.home, ".claude", "skills");
5344
5830
  let latest;
5345
5831
  try {
5346
5832
  latest = execSync3(`npm view skillwiki@${tag} version`, {
@@ -5410,16 +5896,16 @@ async function runUpdate(input) {
5410
5896
 
5411
5897
  // src/commands/self-update.ts
5412
5898
  import { execSync as execSync4 } from "child_process";
5413
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
5414
- import { join as join27 } from "path";
5899
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
5900
+ import { join as join30 } from "path";
5415
5901
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
5416
5902
  async function runSelfUpdate(input) {
5417
5903
  const currentVersion = JSON.parse(
5418
5904
  readFileSync9(new URL("../../package.json", import.meta.url), "utf8")
5419
5905
  ).version;
5420
5906
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
5421
- const localPkgPath = join27(sourceRoot, "packages", "cli", "package.json");
5422
- const hasLocalSource = existsSync8(localPkgPath);
5907
+ const localPkgPath = join30(sourceRoot, "packages", "cli", "package.json");
5908
+ const hasLocalSource = existsSync10(localPkgPath);
5423
5909
  if (input.check) {
5424
5910
  let availableVersion = null;
5425
5911
  let source;
@@ -5550,10 +6036,10 @@ async function runSelfUpdate(input) {
5550
6036
  }
5551
6037
 
5552
6038
  // src/commands/transcripts.ts
5553
- import { readdir as readdir5, stat as stat6, readFile as readFile19 } from "fs/promises";
5554
- import { join as join28 } from "path";
6039
+ import { readdir as readdir5, stat as stat7, readFile as readFile21 } from "fs/promises";
6040
+ import { join as join31 } from "path";
5555
6041
  async function runTranscripts(input) {
5556
- const dir = join28(input.vault, "raw", "transcripts");
6042
+ const dir = join31(input.vault, "raw", "transcripts");
5557
6043
  let entries;
5558
6044
  try {
5559
6045
  entries = await readdir5(dir, { withFileTypes: true });
@@ -5563,13 +6049,13 @@ async function runTranscripts(input) {
5563
6049
  const transcripts = [];
5564
6050
  for (const entry of entries) {
5565
6051
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
5566
- const filePath = join28(dir, entry.name);
5567
- const content = await readFile19(filePath, "utf8");
6052
+ const filePath = join31(dir, entry.name);
6053
+ const content = await readFile21(filePath, "utf8");
5568
6054
  const fm = extractFrontmatter(content);
5569
6055
  if (!fm.ok) continue;
5570
6056
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
5571
6057
  if (input.since && ingested && ingested < input.since) continue;
5572
- const s = await stat6(filePath);
6058
+ const s = await stat7(filePath);
5573
6059
  transcripts.push({
5574
6060
  file: `raw/transcripts/${entry.name}`,
5575
6061
  ingested,
@@ -5581,12 +6067,12 @@ async function runTranscripts(input) {
5581
6067
  }
5582
6068
 
5583
6069
  // src/commands/project-index.ts
5584
- import { readdir as readdir6, readFile as readFile20, writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
5585
- import { join as join29, dirname as dirname10 } from "path";
6070
+ import { readdir as readdir6, readFile as readFile22, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
6071
+ import { join as join32, dirname as dirname11 } from "path";
5586
6072
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
5587
6073
  async function runProjectIndex(input) {
5588
6074
  const slug = input.slug;
5589
- const projectDir = join29(input.vault, "projects", slug);
6075
+ const projectDir = join32(input.vault, "projects", slug);
5590
6076
  try {
5591
6077
  await readdir6(projectDir);
5592
6078
  } catch {
@@ -5597,15 +6083,15 @@ async function runProjectIndex(input) {
5597
6083
  }
5598
6084
  const wikilinkPattern = `[[${slug}]]`;
5599
6085
  const entries = [];
5600
- const compoundDir = join29(input.vault, "projects", slug, "compound");
6086
+ const compoundDir = join32(input.vault, "projects", slug, "compound");
5601
6087
  try {
5602
6088
  const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
5603
6089
  for (const entry of compoundFiles) {
5604
6090
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
5605
- const filePath = join29(compoundDir, entry.name);
6091
+ const filePath = join32(compoundDir, entry.name);
5606
6092
  let text;
5607
6093
  try {
5608
- text = await readFile20(filePath, "utf8");
6094
+ text = await readFile22(filePath, "utf8");
5609
6095
  } catch {
5610
6096
  continue;
5611
6097
  }
@@ -5622,16 +6108,16 @@ async function runProjectIndex(input) {
5622
6108
  for (const dir of LAYER2_DIRS) {
5623
6109
  let files;
5624
6110
  try {
5625
- files = await readdir6(join29(input.vault, dir), { withFileTypes: true });
6111
+ files = await readdir6(join32(input.vault, dir), { withFileTypes: true });
5626
6112
  } catch {
5627
6113
  continue;
5628
6114
  }
5629
6115
  for (const entry of files) {
5630
6116
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
5631
- const filePath = join29(input.vault, dir, entry.name);
6117
+ const filePath = join32(input.vault, dir, entry.name);
5632
6118
  let text;
5633
6119
  try {
5634
- text = await readFile20(filePath, "utf8");
6120
+ text = await readFile22(filePath, "utf8");
5635
6121
  } catch {
5636
6122
  continue;
5637
6123
  }
@@ -5652,11 +6138,11 @@ async function runProjectIndex(input) {
5652
6138
  const tb = typeOrder[b.type] ?? 99;
5653
6139
  return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
5654
6140
  });
5655
- const indexPath = join29(projectDir, "knowledge.md");
6141
+ const indexPath = join32(projectDir, "knowledge.md");
5656
6142
  let existing = false;
5657
6143
  let stale = false;
5658
6144
  try {
5659
- const existingText = await readFile20(indexPath, "utf8");
6145
+ const existingText = await readFile22(indexPath, "utf8");
5660
6146
  existing = true;
5661
6147
  const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
5662
6148
  const existingPages = new Set(existingEntries.map((l) => {
@@ -5696,8 +6182,8 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
5696
6182
  }
5697
6183
  if (input.apply) {
5698
6184
  try {
5699
- await mkdir9(dirname10(indexPath), { recursive: true });
5700
- await writeFile10(indexPath, body, "utf8");
6185
+ await mkdir10(dirname11(indexPath), { recursive: true });
6186
+ await writeFile11(indexPath, body, "utf8");
5701
6187
  } catch (e) {
5702
6188
  return {
5703
6189
  exitCode: ExitCode.WRITE_FAILED,
@@ -5725,10 +6211,10 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
5725
6211
  }
5726
6212
 
5727
6213
  // src/commands/compound.ts
5728
- import { writeFile as writeFile11, mkdir as mkdir10, readdir as readdir7, unlink as unlink3 } from "fs/promises";
5729
- import { join as join30 } from "path";
5730
- import { existsSync as existsSync9 } from "fs";
5731
- import { readFile as readFile21 } from "fs/promises";
6214
+ import { writeFile as writeFile12, mkdir as mkdir11, readdir as readdir7, unlink as unlink4 } from "fs/promises";
6215
+ import { join as join33 } from "path";
6216
+ import { existsSync as existsSync11 } from "fs";
6217
+ import { readFile as readFile23 } from "fs/promises";
5732
6218
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
5733
6219
  var FIELD_RE = {
5734
6220
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -5826,17 +6312,17 @@ function extractRetroFields(date, cycleName, block) {
5826
6312
  };
5827
6313
  }
5828
6314
  async function runCompound(input) {
5829
- const logPath = join30(input.vault, "log.md");
6315
+ const logPath = join33(input.vault, "log.md");
5830
6316
  let logText;
5831
6317
  try {
5832
- logText = await readFile21(logPath, "utf8");
6318
+ logText = await readFile23(logPath, "utf8");
5833
6319
  } catch {
5834
6320
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
5835
6321
  }
5836
6322
  const entries = parseRetroEntries(logText);
5837
6323
  const promoted = [];
5838
6324
  const skipped = [];
5839
- const compoundDir = join30(input.vault, "projects", input.project, "compound");
6325
+ const compoundDir = join33(input.vault, "projects", input.project, "compound");
5840
6326
  for (const entry of entries) {
5841
6327
  const generalizeValue = entry.generalize.trim();
5842
6328
  if (!/^yes/i.test(generalizeValue)) {
@@ -5844,8 +6330,8 @@ async function runCompound(input) {
5844
6330
  continue;
5845
6331
  }
5846
6332
  const slug = slugify(entry.cycleName);
5847
- const compoundPath = join30(compoundDir, `${slug}.md`);
5848
- if (existsSync9(compoundPath)) {
6333
+ const compoundPath = join33(compoundDir, `${slug}.md`);
6334
+ if (existsSync11(compoundPath)) {
5849
6335
  skipped.push(entry.date);
5850
6336
  continue;
5851
6337
  }
@@ -5883,10 +6369,10 @@ async function runCompound(input) {
5883
6369
  ].join("\n");
5884
6370
  const content = frontmatter + "\n" + body;
5885
6371
  if (!input.dryRun) {
5886
- if (!existsSync9(compoundDir)) {
5887
- await mkdir10(compoundDir, { recursive: true });
6372
+ if (!existsSync11(compoundDir)) {
6373
+ await mkdir11(compoundDir, { recursive: true });
5888
6374
  }
5889
- await writeFile11(compoundPath, content, "utf8");
6375
+ await writeFile12(compoundPath, content, "utf8");
5890
6376
  }
5891
6377
  promoted.push(`${slug}.md`);
5892
6378
  }
@@ -5905,23 +6391,23 @@ async function runCompound(input) {
5905
6391
  };
5906
6392
  }
5907
6393
  async function runCompoundDelete(input) {
5908
- const projectDir = join30(input.vault, "projects", input.project);
5909
- if (!existsSync9(projectDir)) {
6394
+ const projectDir = join33(input.vault, "projects", input.project);
6395
+ if (!existsSync11(projectDir)) {
5910
6396
  return {
5911
6397
  exitCode: ExitCode.PROJECT_NOT_FOUND,
5912
6398
  result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
5913
6399
  };
5914
6400
  }
5915
6401
  const entryName = input.entry.replace(/\.md$/, "");
5916
- const compoundPath = join30(projectDir, "compound", `${entryName}.md`);
5917
- if (!existsSync9(compoundPath)) {
6402
+ const compoundPath = join33(projectDir, "compound", `${entryName}.md`);
6403
+ if (!existsSync11(compoundPath)) {
5918
6404
  return {
5919
6405
  exitCode: ExitCode.FILE_NOT_FOUND,
5920
6406
  result: err("FILE_NOT_FOUND", { path: compoundPath })
5921
6407
  };
5922
6408
  }
5923
6409
  try {
5924
- await unlink3(compoundPath);
6410
+ await unlink4(compoundPath);
5925
6411
  } catch (e) {
5926
6412
  return {
5927
6413
  exitCode: ExitCode.WRITE_FAILED,
@@ -5947,8 +6433,8 @@ knowledge.md regenerated`
5947
6433
  };
5948
6434
  }
5949
6435
  async function runCompoundList(input) {
5950
- const compoundDir = join30(input.vault, "projects", input.project, "compound");
5951
- if (!existsSync9(compoundDir)) {
6436
+ const compoundDir = join33(input.vault, "projects", input.project, "compound");
6437
+ if (!existsSync11(compoundDir)) {
5952
6438
  return {
5953
6439
  exitCode: ExitCode.OK,
5954
6440
  result: ok({
@@ -5978,10 +6464,10 @@ could not read compound directory`
5978
6464
  const entries = [];
5979
6465
  for (const dirent of dirents) {
5980
6466
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
5981
- const filePath = join30(compoundDir, dirent.name);
6467
+ const filePath = join33(compoundDir, dirent.name);
5982
6468
  let text;
5983
6469
  try {
5984
- text = await readFile21(filePath, "utf8");
6470
+ text = await readFile23(filePath, "utf8");
5985
6471
  } catch {
5986
6472
  continue;
5987
6473
  }
@@ -6010,9 +6496,9 @@ no compound entries found`;
6010
6496
  }
6011
6497
 
6012
6498
  // src/commands/observe.ts
6013
- import { mkdir as mkdir11, writeFile as writeFile12 } from "fs/promises";
6014
- import { existsSync as existsSync10, statSync as statSync3 } from "fs";
6015
- import { join as join31 } from "path";
6499
+ import { mkdir as mkdir12, writeFile as writeFile13 } from "fs/promises";
6500
+ import { existsSync as existsSync12, statSync as statSync4 } from "fs";
6501
+ import { join as join34 } from "path";
6016
6502
  import { createHash as createHash4 } from "crypto";
6017
6503
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
6018
6504
  function slugify2(text) {
@@ -6035,15 +6521,15 @@ async function runObserve(input) {
6035
6521
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
6036
6522
  };
6037
6523
  }
6038
- if (!existsSync10(input.vault) || !statSync3(input.vault).isDirectory()) {
6524
+ if (!existsSync12(input.vault) || !statSync4(input.vault).isDirectory()) {
6039
6525
  return {
6040
6526
  exitCode: ExitCode.VAULT_PATH_INVALID,
6041
6527
  result: err("VAULT_PATH_INVALID", { path: input.vault })
6042
6528
  };
6043
6529
  }
6044
- const transcriptsDir = join31(input.vault, "raw", "transcripts");
6530
+ const transcriptsDir = join34(input.vault, "raw", "transcripts");
6045
6531
  try {
6046
- await mkdir11(transcriptsDir, { recursive: true });
6532
+ await mkdir12(transcriptsDir, { recursive: true });
6047
6533
  } catch {
6048
6534
  return {
6049
6535
  exitCode: ExitCode.VAULT_PATH_INVALID,
@@ -6053,7 +6539,7 @@ async function runObserve(input) {
6053
6539
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6054
6540
  const slug = slugify2(input.text);
6055
6541
  const fileName = `${today}-observation-${slug}.md`;
6056
- const filePath = join31(transcriptsDir, fileName);
6542
+ const filePath = join34(transcriptsDir, fileName);
6057
6543
  const body = `
6058
6544
  ${input.text.trim()}
6059
6545
  `;
@@ -6071,7 +6557,7 @@ ${input.text.trim()}
6071
6557
  frontmatterLines.push("---");
6072
6558
  const content = frontmatterLines.join("\n") + body;
6073
6559
  try {
6074
- await writeFile12(filePath, content, "utf8");
6560
+ await writeFile13(filePath, content, "utf8");
6075
6561
  } catch (e) {
6076
6562
  return {
6077
6563
  exitCode: ExitCode.WRITE_FAILED,
@@ -6093,8 +6579,8 @@ ${input.text.trim()}
6093
6579
  }
6094
6580
 
6095
6581
  // src/commands/ingest.ts
6096
- import { readFile as readFile22, writeFile as writeFile13, mkdir as mkdir12 } from "fs/promises";
6097
- import { join as join32 } from "path";
6582
+ import { readFile as readFile24, writeFile as writeFile14, mkdir as mkdir13 } from "fs/promises";
6583
+ import { join as join35 } from "path";
6098
6584
  import { createHash as createHash5 } from "crypto";
6099
6585
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
6100
6586
  var TYPE_DIR = {
@@ -6253,7 +6739,7 @@ async function runIngest(input) {
6253
6739
  sourceContent = fetchResult.data.body;
6254
6740
  } else {
6255
6741
  try {
6256
- sourceContent = await readFile22(input.source, "utf8");
6742
+ sourceContent = await readFile24(input.source, "utf8");
6257
6743
  } catch {
6258
6744
  return {
6259
6745
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -6268,8 +6754,8 @@ async function runIngest(input) {
6268
6754
  const rawRelPath = `raw/articles/${slug}.md`;
6269
6755
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
6270
6756
  const typedRelPath = `${typedDir}/${slug}.md`;
6271
- const rawAbsPath = join32(input.vault, rawRelPath);
6272
- const typedAbsPath = join32(input.vault, typedRelPath);
6757
+ const rawAbsPath = join35(input.vault, rawRelPath);
6758
+ const typedAbsPath = join35(input.vault, typedRelPath);
6273
6759
  const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
6274
6760
  const typedContent = buildTypedContent(
6275
6761
  input.title,
@@ -6332,8 +6818,8 @@ async function runIngest(input) {
6332
6818
  };
6333
6819
  }
6334
6820
  try {
6335
- await mkdir12(join32(input.vault, "raw", "articles"), { recursive: true });
6336
- await writeFile13(rawAbsPath, rawContent, "utf8");
6821
+ await mkdir13(join35(input.vault, "raw", "articles"), { recursive: true });
6822
+ await writeFile14(rawAbsPath, rawContent, "utf8");
6337
6823
  } catch (e) {
6338
6824
  return {
6339
6825
  exitCode: ExitCode.WRITE_FAILED,
@@ -6341,8 +6827,8 @@ async function runIngest(input) {
6341
6827
  };
6342
6828
  }
6343
6829
  try {
6344
- await mkdir12(join32(input.vault, typedDir), { recursive: true });
6345
- await writeFile13(typedAbsPath, typedContent, "utf8");
6830
+ await mkdir13(join35(input.vault, typedDir), { recursive: true });
6831
+ await writeFile14(typedAbsPath, typedContent, "utf8");
6346
6832
  } catch (e) {
6347
6833
  return {
6348
6834
  exitCode: ExitCode.WRITE_FAILED,
@@ -6520,12 +7006,12 @@ ${body}`;
6520
7006
  }
6521
7007
 
6522
7008
  // src/commands/sync.ts
6523
- import { existsSync as existsSync12 } from "fs";
6524
- import { join as join34 } from "path";
7009
+ import { existsSync as existsSync14 } from "fs";
7010
+ import { join as join37 } from "path";
6525
7011
 
6526
7012
  // src/utils/sync-lock.ts
6527
- import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync10, renameSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
6528
- import { join as join33 } from "path";
7013
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, renameSync, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "fs";
7014
+ import { join as join36 } from "path";
6529
7015
  import { createHash as createHash6 } from "crypto";
6530
7016
  function getSessionId() {
6531
7017
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
@@ -6533,11 +7019,11 @@ function getSessionId() {
6533
7019
  return process.pid.toString();
6534
7020
  }
6535
7021
  function lockPath(vault) {
6536
- return join33(vault, ".skillwiki", "sync.lock");
7022
+ return join36(vault, ".skillwiki", "sync.lock");
6537
7023
  }
6538
7024
  function readLock(vault) {
6539
7025
  const path = lockPath(vault);
6540
- if (!existsSync11(path)) return null;
7026
+ if (!existsSync13(path)) return null;
6541
7027
  try {
6542
7028
  const raw = readFileSync10(path, "utf8");
6543
7029
  return JSON.parse(raw);
@@ -6552,9 +7038,9 @@ function isStale(lock, now) {
6552
7038
  }
6553
7039
  function acquireLock(vault, opts = {}) {
6554
7040
  const path = lockPath(vault);
6555
- const dir = join33(vault, ".skillwiki");
6556
- if (!existsSync11(dir)) {
6557
- mkdirSync3(dir, { recursive: true });
7041
+ const dir = join36(vault, ".skillwiki");
7042
+ if (!existsSync13(dir)) {
7043
+ mkdirSync4(dir, { recursive: true });
6558
7044
  }
6559
7045
  const sessionId = opts.sessionId ?? getSessionId();
6560
7046
  const summary = opts.summary ?? "skillwiki sync";
@@ -6573,7 +7059,7 @@ function acquireLock(vault, opts = {}) {
6573
7059
  };
6574
7060
  try {
6575
7061
  const content = JSON.stringify(lock, null, 2) + "\n";
6576
- writeFileSync5(path, content, { flag: "wx" });
7062
+ writeFileSync6(path, content, { flag: "wx" });
6577
7063
  return { ok: true, lock };
6578
7064
  } catch (e) {
6579
7065
  const err3 = e;
@@ -6593,19 +7079,19 @@ function acquireLock(vault, opts = {}) {
6593
7079
  function writeLockedFile(path, lock) {
6594
7080
  const tmp = path + ".tmp";
6595
7081
  const content = JSON.stringify(lock, null, 2) + "\n";
6596
- writeFileSync5(tmp, content);
7082
+ writeFileSync6(tmp, content);
6597
7083
  renameSync(tmp, path);
6598
7084
  }
6599
7085
  function releaseLock(vault, opts = {}) {
6600
7086
  const path = lockPath(vault);
6601
- if (!existsSync11(path)) {
7087
+ if (!existsSync13(path)) {
6602
7088
  return { released: false };
6603
7089
  }
6604
7090
  const sessionId = opts.sessionId ?? getSessionId();
6605
7091
  const existing = readLock(vault);
6606
7092
  if (opts.force) {
6607
7093
  try {
6608
- unlinkSync4(path);
7094
+ unlinkSync5(path);
6609
7095
  const prior = existing && existing.session_id !== sessionId ? existing : void 0;
6610
7096
  return { released: true, prior };
6611
7097
  } catch {
@@ -6616,7 +7102,7 @@ function releaseLock(vault, opts = {}) {
6616
7102
  return { released: false };
6617
7103
  }
6618
7104
  try {
6619
- unlinkSync4(path);
7105
+ unlinkSync5(path);
6620
7106
  return { released: true };
6621
7107
  } catch {
6622
7108
  return { released: false };
@@ -6627,7 +7113,7 @@ function releaseLock(vault, opts = {}) {
6627
7113
  function runSyncStatus(input) {
6628
7114
  const vault = input.vault;
6629
7115
  const includeStashes = input.includeStashes ?? false;
6630
- if (!existsSync12(join34(vault, ".git"))) {
7116
+ if (!existsSync14(join37(vault, ".git"))) {
6631
7117
  return {
6632
7118
  exitCode: ExitCode.VAULT_PATH_INVALID,
6633
7119
  result: ok({
@@ -6641,6 +7127,7 @@ function runSyncStatus(input) {
6641
7127
  })
6642
7128
  };
6643
7129
  }
7130
+ enableGitLongPathsOnWindows(vault);
6644
7131
  const porcelain = git(vault, ["status", "--porcelain"]);
6645
7132
  const dirty = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
6646
7133
  const revOutput = git(vault, ["rev-list", "--left-right", "--count", "origin/HEAD...HEAD"]);
@@ -6704,12 +7191,24 @@ function runSyncStatus(input) {
6704
7191
  }
6705
7192
  async function runSyncPush(input) {
6706
7193
  const vault = input.vault;
6707
- if (!existsSync12(join34(vault, ".git"))) {
7194
+ if (!existsSync14(join37(vault, ".git"))) {
6708
7195
  return {
6709
7196
  exitCode: ExitCode.VAULT_PATH_INVALID,
6710
7197
  result: err("NOT_A_GIT_REPO", { path: vault })
6711
7198
  };
6712
7199
  }
7200
+ enableGitLongPathsOnWindows(vault);
7201
+ let pathFixes = 0;
7202
+ const pathFix = await fixPathTooLong({ vault });
7203
+ if (pathFix.result.ok && pathFix.result.data.fixed.length > 0) {
7204
+ pathFixes = pathFix.result.data.fixed.length;
7205
+ appendLastOp(vault, {
7206
+ operation: "lint-fix",
7207
+ summary: `fixed ${pathFixes} long path(s)`,
7208
+ files: pathFix.result.data.fixed.flatMap((f) => [f.from, f.to]),
7209
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
7210
+ });
7211
+ }
6713
7212
  const porcelain = git(vault, ["status", "--porcelain"]);
6714
7213
  const dirtyFiles = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0) : [];
6715
7214
  if (dirtyFiles.length === 0) {
@@ -6719,6 +7218,7 @@ async function runSyncPush(input) {
6719
7218
  files_committed: 0,
6720
7219
  commit_message: "",
6721
7220
  pushed: false,
7221
+ path_fixes: pathFixes,
6722
7222
  humanHint: "nothing to commit, working tree clean"
6723
7223
  })
6724
7224
  };
@@ -6773,7 +7273,8 @@ async function runSyncPush(input) {
6773
7273
  files_committed: dirtyFiles.length,
6774
7274
  commit_message: commitMessage,
6775
7275
  pushed: false,
6776
- humanHint: `committed ${dirtyFiles.length} file(s) but push failed: ${String(e)}`
7276
+ path_fixes: pathFixes,
7277
+ humanHint: `committed ${dirtyFiles.length} file(s)${pathFixes > 0 ? ` after ${pathFixes} long-path fix(es)` : ""} but push failed: ${String(e)}`
6777
7278
  })
6778
7279
  };
6779
7280
  }
@@ -6783,7 +7284,8 @@ async function runSyncPush(input) {
6783
7284
  files_committed: dirtyFiles.length,
6784
7285
  commit_message: commitMessage,
6785
7286
  pushed,
6786
- humanHint: `committed and pushed ${dirtyFiles.length} file(s)`
7287
+ path_fixes: pathFixes,
7288
+ humanHint: `committed and pushed ${dirtyFiles.length} file(s)${pathFixes > 0 ? ` after ${pathFixes} long-path fix(es)` : ""}`
6787
7289
  })
6788
7290
  };
6789
7291
  }
@@ -6806,14 +7308,19 @@ function enumerateStashes(vault) {
6806
7308
  }
6807
7309
  return stashes;
6808
7310
  }
7311
+ function enableGitLongPathsOnWindows(vault) {
7312
+ if (process.platform !== "win32") return;
7313
+ git(vault, ["config", "core.longpaths", "true"]);
7314
+ }
6809
7315
  async function runSyncPull(input) {
6810
7316
  const vault = input.vault;
6811
- if (!existsSync12(join34(vault, ".git"))) {
7317
+ if (!existsSync14(join37(vault, ".git"))) {
6812
7318
  return {
6813
7319
  exitCode: ExitCode.VAULT_PATH_INVALID,
6814
7320
  result: err("NOT_A_GIT_REPO", { path: vault })
6815
7321
  };
6816
7322
  }
7323
+ enableGitLongPathsOnWindows(vault);
6817
7324
  let fetched = false;
6818
7325
  try {
6819
7326
  gitStrict(vault, ["fetch", "origin"]);
@@ -6902,6 +7409,8 @@ async function runSyncPull(input) {
6902
7409
  };
6903
7410
  }
6904
7411
  }
7412
+ const pathFix = await fixPathTooLong({ vault });
7413
+ const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
6905
7414
  let lintErrors = 0;
6906
7415
  let lintWarnings = 0;
6907
7416
  const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
@@ -6913,6 +7422,7 @@ async function runSyncPull(input) {
6913
7422
  if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
6914
7423
  else hintParts.push("already up to date");
6915
7424
  if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
7425
+ if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
6916
7426
  if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
6917
7427
  if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
6918
7428
  const exitCode = lintErrors > 0 ? ExitCode.LINT_HAS_ERRORS : lintWarnings > 0 ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
@@ -6976,7 +7486,7 @@ function runSyncPeers(input) {
6976
7486
  }
6977
7487
  function runSyncLock(input) {
6978
7488
  const vault = input.vault;
6979
- if (!existsSync12(vault)) {
7489
+ if (!existsSync14(vault)) {
6980
7490
  return {
6981
7491
  exitCode: ExitCode.VAULT_PATH_INVALID,
6982
7492
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -7011,7 +7521,7 @@ function runSyncLock(input) {
7011
7521
  }
7012
7522
  function runSyncUnlock(input) {
7013
7523
  const vault = input.vault;
7014
- if (!existsSync12(vault)) {
7524
+ if (!existsSync14(vault)) {
7015
7525
  return {
7016
7526
  exitCode: ExitCode.VAULT_PATH_INVALID,
7017
7527
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -7044,8 +7554,8 @@ function runSyncUnlock(input) {
7044
7554
  }
7045
7555
 
7046
7556
  // src/commands/backup.ts
7047
- import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync11, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
7048
- import { join as join35, relative as relative3, dirname as dirname11 } from "path";
7557
+ import { statSync as statSync5, readdirSync as readdirSync2, readFileSync as readFileSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
7558
+ import { join as join38, relative as relative3, dirname as dirname12 } from "path";
7049
7559
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
7050
7560
 
7051
7561
  // src/utils/s3-client.ts
@@ -7065,11 +7575,11 @@ function createS3Client(config) {
7065
7575
  }
7066
7576
 
7067
7577
  // src/commands/backup.ts
7068
- var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_modules", ".skillwiki"]);
7578
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_modules", ".skillwiki"]);
7069
7579
  function* walkMarkdown(dir, base) {
7070
7580
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
7071
- if (SKIP_DIRS.has(entry.name)) continue;
7072
- const full = join35(dir, entry.name);
7581
+ if (SKIP_DIRS2.has(entry.name)) continue;
7582
+ const full = join38(dir, entry.name);
7073
7583
  if (entry.isDirectory()) {
7074
7584
  yield* walkMarkdown(full, base);
7075
7585
  } else if (entry.name.endsWith(".md")) {
@@ -7092,8 +7602,8 @@ async function runBackupSync(input) {
7092
7602
  let failed = 0;
7093
7603
  const files = [...walkMarkdown(input.vault, input.vault)];
7094
7604
  for (const relPath of files) {
7095
- const absPath = join35(input.vault, relPath);
7096
- const localStat = statSync4(absPath);
7605
+ const absPath = join38(input.vault, relPath);
7606
+ const localStat = statSync5(absPath);
7097
7607
  let needsUpload = true;
7098
7608
  try {
7099
7609
  const head = await client.send(new HeadObjectCommand({ Bucket: input.bucket, Key: relPath }));
@@ -7168,9 +7678,9 @@ async function runBackupRestore(input) {
7168
7678
  const objects = list.Contents ?? [];
7169
7679
  for (const obj of objects) {
7170
7680
  if (!obj.Key) continue;
7171
- const localPath = join35(target, obj.Key);
7681
+ const localPath = join38(target, obj.Key);
7172
7682
  try {
7173
- const localStat = statSync4(localPath);
7683
+ const localStat = statSync5(localPath);
7174
7684
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
7175
7685
  conflicts++;
7176
7686
  continue;
@@ -7181,8 +7691,8 @@ async function runBackupRestore(input) {
7181
7691
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
7182
7692
  const body = await resp.Body?.transformToByteArray();
7183
7693
  if (body) {
7184
- mkdirSync4(dirname11(localPath), { recursive: true });
7185
- writeFileSync6(localPath, Buffer.from(body));
7694
+ mkdirSync5(dirname12(localPath), { recursive: true });
7695
+ writeFileSync7(localPath, Buffer.from(body));
7186
7696
  downloaded++;
7187
7697
  }
7188
7698
  } catch {
@@ -7214,11 +7724,11 @@ async function runBackupRestore(input) {
7214
7724
  }
7215
7725
 
7216
7726
  // src/commands/status.ts
7217
- import { existsSync as existsSync13, statSync as statSync5 } from "fs";
7218
- import { readFile as readFile23 } from "fs/promises";
7219
- import { join as join36 } from "path";
7727
+ import { existsSync as existsSync15, statSync as statSync6 } from "fs";
7728
+ import { readFile as readFile25 } from "fs/promises";
7729
+ import { join as join39 } from "path";
7220
7730
  async function runStatus(input) {
7221
- if (!existsSync13(input.vault)) {
7731
+ if (!existsSync15(input.vault)) {
7222
7732
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
7223
7733
  }
7224
7734
  const scan = await scanVault(input.vault);
@@ -7243,7 +7753,7 @@ async function runStatus(input) {
7243
7753
  const compound = scan.data.compound.length;
7244
7754
  let schemaVersion = "v1";
7245
7755
  try {
7246
- const schemaContent = await readFile23(join36(input.vault, "SCHEMA.md"), "utf8");
7756
+ const schemaContent = await readFile25(join39(input.vault, "SCHEMA.md"), "utf8");
7247
7757
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
7248
7758
  if (versionMatch) schemaVersion = versionMatch[1];
7249
7759
  } catch {
@@ -7259,7 +7769,7 @@ async function runStatus(input) {
7259
7769
  let maxTime = 0;
7260
7770
  for (const page of allPages) {
7261
7771
  try {
7262
- const st = statSync5(page.absPath);
7772
+ const st = statSync6(page.absPath);
7263
7773
  if (st.mtimeMs > maxTime) {
7264
7774
  maxTime = st.mtimeMs;
7265
7775
  lastModified = st.mtime.toISOString();
@@ -7303,8 +7813,8 @@ async function runStatus(input) {
7303
7813
  }
7304
7814
 
7305
7815
  // src/commands/seed.ts
7306
- import { mkdir as mkdir13, writeFile as writeFile14, stat as stat7 } from "fs/promises";
7307
- import { join as join37 } from "path";
7816
+ import { mkdir as mkdir14, writeFile as writeFile15, stat as stat8 } from "fs/promises";
7817
+ import { join as join40 } from "path";
7308
7818
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7309
7819
  var EXAMPLE_PAGES = {
7310
7820
  "entities/example-project.md": `---
@@ -7373,30 +7883,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
7373
7883
  `;
7374
7884
  async function runSeed(input) {
7375
7885
  try {
7376
- await stat7(join37(input.vault, "SCHEMA.md"));
7886
+ await stat8(join40(input.vault, "SCHEMA.md"));
7377
7887
  } catch {
7378
7888
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
7379
7889
  }
7380
7890
  const created = [];
7381
7891
  const skipped = [];
7382
7892
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
7383
- const absPath = join37(input.vault, relPath);
7893
+ const absPath = join40(input.vault, relPath);
7384
7894
  try {
7385
- await stat7(absPath);
7895
+ await stat8(absPath);
7386
7896
  skipped.push(relPath);
7387
7897
  } catch {
7388
- await mkdir13(join37(absPath, ".."), { recursive: true });
7389
- await writeFile14(absPath, content, "utf8");
7898
+ await mkdir14(join40(absPath, ".."), { recursive: true });
7899
+ await writeFile15(absPath, content, "utf8");
7390
7900
  created.push(relPath);
7391
7901
  }
7392
7902
  }
7393
- const rawPath = join37(input.vault, "raw", "articles", "example-source.md");
7903
+ const rawPath = join40(input.vault, "raw", "articles", "example-source.md");
7394
7904
  try {
7395
- await stat7(rawPath);
7905
+ await stat8(rawPath);
7396
7906
  skipped.push("raw/articles/example-source.md");
7397
7907
  } catch {
7398
- await mkdir13(join37(rawPath, ".."), { recursive: true });
7399
- await writeFile14(rawPath, EXAMPLE_RAW, "utf8");
7908
+ await mkdir14(join40(rawPath, ".."), { recursive: true });
7909
+ await writeFile15(rawPath, EXAMPLE_RAW, "utf8");
7400
7910
  created.push("raw/articles/example-source.md");
7401
7911
  }
7402
7912
  if (created.length > 0) {
@@ -7418,9 +7928,9 @@ async function runSeed(input) {
7418
7928
  }
7419
7929
 
7420
7930
  // src/commands/canvas.ts
7421
- import { readFile as readFile24, writeFile as writeFile15 } from "fs/promises";
7422
- import { existsSync as existsSync14 } from "fs";
7423
- import { join as join38 } from "path";
7931
+ import { readFile as readFile26, writeFile as writeFile16 } from "fs/promises";
7932
+ import { existsSync as existsSync16 } from "fs";
7933
+ import { join as join41 } from "path";
7424
7934
  var NODE_WIDTH = 240;
7425
7935
  var NODE_HEIGHT = 60;
7426
7936
  var COLUMN_SPACING = 400;
@@ -7498,8 +8008,8 @@ function buildCanvasEdges(adjacency) {
7498
8008
  return edges;
7499
8009
  }
7500
8010
  async function runCanvasGenerate(input) {
7501
- const graphPath = input.graphPath ?? join38(input.vault, ".skillwiki", "graph.json");
7502
- if (!existsSync14(graphPath)) {
8011
+ const graphPath = input.graphPath ?? join41(input.vault, ".skillwiki", "graph.json");
8012
+ if (!existsSync16(graphPath)) {
7503
8013
  return {
7504
8014
  exitCode: ExitCode.FILE_NOT_FOUND,
7505
8015
  result: err("FILE_NOT_FOUND", {
@@ -7510,7 +8020,7 @@ async function runCanvasGenerate(input) {
7510
8020
  }
7511
8021
  let raw;
7512
8022
  try {
7513
- raw = await readFile24(graphPath, "utf8");
8023
+ raw = await readFile26(graphPath, "utf8");
7514
8024
  } catch (e) {
7515
8025
  return {
7516
8026
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -7536,9 +8046,9 @@ async function runCanvasGenerate(input) {
7536
8046
  const nodes = buildCanvasNodes(paths);
7537
8047
  const edges = buildCanvasEdges(graph.adjacency);
7538
8048
  const canvas = { nodes, edges };
7539
- const outPath = join38(input.vault, "vault-graph.canvas");
8049
+ const outPath = join41(input.vault, "vault-graph.canvas");
7540
8050
  try {
7541
- await writeFile15(outPath, JSON.stringify(canvas, null, 2));
8051
+ await writeFile16(outPath, JSON.stringify(canvas, null, 2));
7542
8052
  } catch (e) {
7543
8053
  return {
7544
8054
  exitCode: ExitCode.WRITE_FAILED,
@@ -7558,8 +8068,8 @@ written: ${outPath}`
7558
8068
  }
7559
8069
 
7560
8070
  // src/commands/query.ts
7561
- import { readFile as readFile25, stat as stat8 } from "fs/promises";
7562
- import { join as join39 } from "path";
8071
+ import { readFile as readFile27, stat as stat9 } from "fs/promises";
8072
+ import { join as join42 } from "path";
7563
8073
  var W_KEYWORD = 2;
7564
8074
  var W_SOURCE_OVERLAP = 4;
7565
8075
  var W_WIKILINK = 3;
@@ -7680,10 +8190,10 @@ function computeKeywordScore(terms, title, tags, body) {
7680
8190
  return score;
7681
8191
  }
7682
8192
  async function loadOrBuildGraph(vault) {
7683
- const graphPath = join39(vault, ".skillwiki", "graph.json");
8193
+ const graphPath = join42(vault, ".skillwiki", "graph.json");
7684
8194
  let needsBuild = false;
7685
8195
  try {
7686
- const fileStat = await stat8(graphPath);
8196
+ const fileStat = await stat9(graphPath);
7687
8197
  const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
7688
8198
  if (ageHours > 24) needsBuild = true;
7689
8199
  } catch {
@@ -7694,7 +8204,7 @@ async function loadOrBuildGraph(vault) {
7694
8204
  if (buildResult.exitCode !== 0) return null;
7695
8205
  }
7696
8206
  try {
7697
- const raw = await readFile25(graphPath, "utf8");
8207
+ const raw = await readFile27(graphPath, "utf8");
7698
8208
  return JSON.parse(raw);
7699
8209
  } catch {
7700
8210
  return null;
@@ -7702,14 +8212,14 @@ async function loadOrBuildGraph(vault) {
7702
8212
  }
7703
8213
 
7704
8214
  // src/utils/auto-commit.ts
7705
- import { existsSync as existsSync15 } from "fs";
7706
- import { join as join40 } from "path";
8215
+ import { existsSync as existsSync17 } from "fs";
8216
+ import { join as join43 } from "path";
7707
8217
  async function postCommit(vault, exitCode) {
7708
8218
  if (exitCode !== 0) return;
7709
8219
  const home = process.env.HOME ?? "";
7710
8220
  const dotenv = await parseDotenvFile(configPath(home));
7711
8221
  if (dotenv["AUTO_COMMIT"] === "false") return;
7712
- if (!existsSync15(join40(vault, ".git"))) return;
8222
+ if (!existsSync17(join43(vault, ".git"))) return;
7713
8223
  const lastOps = readLastOp(vault);
7714
8224
  if (lastOps.length === 0) return;
7715
8225
  const porcelain = git(vault, ["status", "--porcelain"]);
@@ -7760,7 +8270,7 @@ program.command("validate <file>").description("validate vault page frontmatter
7760
8270
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
7761
8271
  });
7762
8272
  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) => {
7763
- const out = opts.out ?? join41(vault, ".skillwiki", "graph.json");
8273
+ const out = opts.out ?? join44(vault, ".skillwiki", "graph.json");
7764
8274
  emit(await runGraphBuild({ vault, out }), vault);
7765
8275
  });
7766
8276
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -7886,6 +8396,11 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
7886
8396
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7887
8397
  else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }), v.vault);
7888
8398
  });
8399
+ program.command("log-append [vault]").description("append a single entry to the vault log under a short advisory lock").requiredOption("--content <text>", "log entry text to append").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
8400
+ const v = await resolveVaultArg(vault, opts.wiki);
8401
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
8402
+ else emit(await runLogAppend({ vault: v.vault, content: opts.content }), v.vault);
8403
+ });
7889
8404
  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) => {
7890
8405
  const v = await resolveVaultArg(vault, opts.wiki);
7891
8406
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });