skillwiki 0.8.1-beta.1 → 0.8.1-beta.11

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 join43 } 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
@@ -500,6 +500,7 @@ import { dirname } from "path";
500
500
  import { readFile as readFile3, readdir, stat } from "fs/promises";
501
501
  import { join as join3, relative as relative2, sep as sep2 } from "path";
502
502
  var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
503
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
503
504
  async function scanVault(root) {
504
505
  try {
505
506
  await stat(join3(root, "SCHEMA.md"));
@@ -510,6 +511,7 @@ async function scanVault(root) {
510
511
  const rels = all.map((p) => ({ absPath: p, relPath: relative2(root, p).split(sep2).join("/") }));
511
512
  return ok({
512
513
  root,
514
+ allMarkdown: rels,
513
515
  typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
514
516
  raw: rels.filter((p) => p.relPath.startsWith("raw/")),
515
517
  workItems: rels.filter((p) => /^projects\/[^/]+\/work\/[^/]+\/(spec|plan|log)\.md$/.test(p.relPath)),
@@ -521,8 +523,10 @@ async function walk(dir) {
521
523
  const out = [];
522
524
  for (const e of entries) {
523
525
  const p = join3(dir, e.name);
524
- if (e.isDirectory()) out.push(...await walk(p));
525
- 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);
526
530
  }
527
531
  return out;
528
532
  }
@@ -548,23 +552,137 @@ function extractBodyWikilinks(body) {
548
552
  return out;
549
553
  }
550
554
 
551
- // src/commands/graph.ts
552
- async function runGraphBuild(input) {
553
- const scan = await scanVault(input.vault);
554
- if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
555
+ // src/utils/community.ts
556
+ async function buildWikilinkAdjacency(typedKnowledge) {
555
557
  const adjacency = {};
556
558
  const slugToPath = {};
557
- for (const p of scan.data.typedKnowledge) {
559
+ for (const p of typedKnowledge) {
558
560
  const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
559
561
  slugToPath[slug] = p.relPath;
560
562
  }
561
- for (const p of scan.data.typedKnowledge) {
563
+ for (const p of typedKnowledge) {
562
564
  const text = await readPage(p);
563
565
  const split = splitFrontmatter(text);
564
566
  const body = split.ok ? split.data.body : text;
565
567
  const links = extractBodyWikilinks(body);
566
568
  adjacency[p.relPath] = links.map((slug) => slugToPath[slug.split("/").pop()]).filter((x) => Boolean(x));
567
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);
568
686
  const adamicAdar = computeAdamicAdar(adjacency);
569
687
  const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
570
688
  try {
@@ -2465,9 +2583,22 @@ ${content}
2465
2583
  }
2466
2584
 
2467
2585
  // src/commands/lint.ts
2468
- import { existsSync as existsSync4 } from "fs";
2469
- import { readFile as readFile16, rename as rename6 } from "fs/promises";
2470
- import { join as join21 } 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
+ }
2471
2602
 
2472
2603
  // src/commands/topic-map-check.ts
2473
2604
  var DEFAULT_THRESHOLD = 200;
@@ -2710,28 +2841,100 @@ async function runRawBodyDedup(vault) {
2710
2841
  }
2711
2842
 
2712
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";
2713
2847
  var MAX_PATH_LENGTH = 240;
2848
+ var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
2714
2849
  async function runPathTooLong(input) {
2715
2850
  const scan = await scanVault(input.vault);
2716
2851
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2717
- const allPages = [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound];
2718
- const violations = [];
2719
- for (const page of allPages) {
2720
- if (page.relPath.length > MAX_PATH_LENGTH) {
2721
- violations.push({ relPath: page.relPath, length: page.relPath.length });
2722
- }
2723
- }
2852
+ const violations = findPathTooLongViolations(scan.data.allMarkdown, MAX_PATH_LENGTH);
2724
2853
  if (violations.length > 0) {
2725
2854
  return {
2726
2855
  exitCode: ExitCode.LINT_HAS_ERRORS,
2727
2856
  result: ok({
2728
2857
  violations,
2729
- 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")
2730
2859
  })
2731
2860
  };
2732
2861
  }
2733
2862
  return { exitCode: ExitCode.OK, result: ok({ violations, humanHint: "all paths within length limit" }) };
2734
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
+ }
2735
2938
  function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
2736
2939
  if (relPath.length <= maxLength) return relPath;
2737
2940
  const lastSlash = relPath.lastIndexOf("/");
@@ -2745,15 +2948,62 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
2745
2948
  const maxPrefixLen = maxLength - dirPrefix.length - suffix.length;
2746
2949
  if (maxPrefixLen <= 0) {
2747
2950
  const fallback = dirPrefix + hash + ext;
2748
- if (fallback.length > maxLength) {
2749
- const dirBudget = maxLength - suffix.length;
2750
- return dirPrefix.slice(0, Math.max(0, dirBudget)) + suffix;
2751
- }
2752
- return fallback;
2951
+ return fallback.length <= maxLength ? fallback : relPath;
2753
2952
  }
2754
2953
  const prefix = base.slice(0, maxPrefixLen).replace(/[-_\s]+$/, "");
2755
2954
  return dirPrefix + prefix + suffix;
2756
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
+ }
2757
3007
  function computeShortHash(input) {
2758
3008
  let hash = 2166136261;
2759
3009
  for (let i = 0; i < input.length; i++) {
@@ -2791,6 +3041,7 @@ function buildCliSurface() {
2791
3041
  program2.command("claim").option("--project <slug>").option("--slug <slug>").option("--wiki <name>");
2792
3042
  program2.command("pagesize").option("--lines <n>").option("--wiki <name>");
2793
3043
  program2.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
3044
+ program2.command("log-append").requiredOption("--content <text>").option("--wiki <name>");
2794
3045
  program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--wiki <name>");
2795
3046
  program2.command("config");
2796
3047
  program2.command("doctor");
@@ -2940,8 +3191,16 @@ function extractSourceEntries(rawFm) {
2940
3191
  }
2941
3192
  var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
2942
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"];
2943
- 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];
2944
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);
2945
3204
  const buckets = {};
2946
3205
  const fixed = [];
2947
3206
  const unresolved = [];
@@ -2983,6 +3242,10 @@ async function runLint(input) {
2983
3242
  if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
2984
3243
  if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
2985
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
+ }
2986
3249
  const topicMap = await runTopicMapCheck({ vault: input.vault });
2987
3250
  if (topicMap.result.ok && topicMap.result.data.recommended) {
2988
3251
  buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
@@ -3061,7 +3324,7 @@ async function runLint(input) {
3061
3324
  let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
3062
3325
  rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
3063
3326
  if (!rawPath.startsWith("raw/") && !rawPath.startsWith("_archive/raw/")) continue;
3064
- if (!existsSync4(join21(input.vault, rawPath)) && !existsSync4(join21(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync4(join21(input.vault, "_archive", rawPath)) && !existsSync4(join21(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"))) {
3065
3328
  brokenSourceFlags.push(`${page.relPath}: ${rawPath}`);
3066
3329
  }
3067
3330
  }
@@ -3152,11 +3415,11 @@ async function runLint(input) {
3152
3415
  const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
3153
3416
  if (!slugMatch) continue;
3154
3417
  const slug = slugMatch[1];
3155
- const knowledgePath = join21(input.vault, "projects", slug, "knowledge.md");
3156
- if (!existsSync4(knowledgePath)) continue;
3418
+ const knowledgePath = join22(input.vault, "projects", slug, "knowledge.md");
3419
+ if (!existsSync5(knowledgePath)) continue;
3157
3420
  const pageRef = page.relPath.replace(/\.md$/, "");
3158
3421
  try {
3159
- const knowledgeContent = await readFile16(knowledgePath, "utf8");
3422
+ const knowledgeContent = await readFile17(knowledgePath, "utf8");
3160
3423
  if (!knowledgeContent.includes(`[[${pageRef}]]`)) {
3161
3424
  orphanedProjectPages.push(`${page.relPath}: not in projects/${slug}/knowledge.md`);
3162
3425
  }
@@ -3197,13 +3460,13 @@ async function runLint(input) {
3197
3460
  }
3198
3461
  }
3199
3462
  if (staleSectionFlags.length > 0) buckets.stale_sections = staleSectionFlags;
3200
- if (input.fix && legacyPages.length > 0) {
3463
+ if (shouldFix("legacy_citation_style") && legacyPages.length > 0) {
3201
3464
  const FENCE_RE2 = /```[\s\S]*?```/g;
3202
3465
  const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
3203
3466
  for (const relPath of legacyPages) {
3204
3467
  try {
3205
3468
  const absPath = `${input.vault}/${relPath}`;
3206
- const raw = await readFile16(absPath, "utf8");
3469
+ const raw = await readFile17(absPath, "utf8");
3207
3470
  const split = splitFrontmatter(raw);
3208
3471
  if (!split.ok) {
3209
3472
  unresolved.push(relPath);
@@ -3298,11 +3561,11 @@ ${newBody}`;
3298
3561
  else delete buckets.legacy_citation_style;
3299
3562
  }
3300
3563
  }
3301
- if (input.fix && noOverview.length > 0) {
3564
+ if (shouldFix("missing_overview") && noOverview.length > 0) {
3302
3565
  for (const relPath of noOverview) {
3303
3566
  try {
3304
3567
  const absPath = `${input.vault}/${relPath}`;
3305
- const raw = await readFile16(absPath, "utf8");
3568
+ const raw = await readFile17(absPath, "utf8");
3306
3569
  const split = splitFrontmatter(raw);
3307
3570
  if (!split.ok) {
3308
3571
  unresolved.push(relPath);
@@ -3339,11 +3602,11 @@ ${trimmedBody}`;
3339
3602
  if (remaining.length > 0) buckets.missing_overview = remaining;
3340
3603
  else delete buckets.missing_overview;
3341
3604
  }
3342
- if (input.fix && missingTldrFlags.length > 0) {
3605
+ if (shouldFix("missing_tldr") && missingTldrFlags.length > 0) {
3343
3606
  for (const relPath of missingTldrFlags) {
3344
3607
  try {
3345
3608
  const absPath = `${input.vault}/${relPath}`;
3346
- const raw = await readFile16(absPath, "utf8");
3609
+ const raw = await readFile17(absPath, "utf8");
3347
3610
  const split = splitFrontmatter(raw);
3348
3611
  if (!split.ok) {
3349
3612
  unresolved.push(relPath);
@@ -3386,14 +3649,14 @@ ${lines.join("\n")}`;
3386
3649
  if (remaining.length > 0) buckets.missing_tldr = remaining;
3387
3650
  else delete buckets.missing_tldr;
3388
3651
  }
3389
- if (input.fix && wikilinkCitationFlags.length > 0) {
3652
+ if (shouldFix("wikilink_citation") && wikilinkCitationFlags.length > 0) {
3390
3653
  const WIKILINK_RE = /\[\[raw\/([^\]|]+)(?:\|[^\]]*)?\]\]/g;
3391
3654
  const FENCE_RE2 = /```[\s\S]*?```/g;
3392
3655
  const wikilinkFixed = [];
3393
3656
  for (const relPath of wikilinkCitationFlags) {
3394
3657
  try {
3395
3658
  const absPath = `${input.vault}/${relPath}`;
3396
- const raw = await readFile16(absPath, "utf8");
3659
+ const raw = await readFile17(absPath, "utf8");
3397
3660
  const split = splitFrontmatter(raw);
3398
3661
  if (!split.ok) {
3399
3662
  unresolved.push(relPath);
@@ -3475,12 +3738,12 @@ ${newBody}`;
3475
3738
  else delete buckets.wikilink_citation;
3476
3739
  }
3477
3740
  }
3478
- if (input.fix && fileSourceUrlFlags.length > 0) {
3741
+ if (shouldFix("file_source_url") && fileSourceUrlFlags.length > 0) {
3479
3742
  const FILE_FIXED = [];
3480
3743
  for (const relPath of fileSourceUrlFlags) {
3481
3744
  try {
3482
3745
  const absPath = `${input.vault}/${relPath}`;
3483
- const raw = await readFile16(absPath, "utf8");
3746
+ const raw = await readFile17(absPath, "utf8");
3484
3747
  const parts = raw.split("---", 3);
3485
3748
  if (parts.length < 3) {
3486
3749
  unresolved.push(relPath);
@@ -3515,36 +3778,11 @@ ${newBody}`;
3515
3778
  }
3516
3779
  }
3517
3780
  const pathViolations = buckets.path_too_long;
3518
- if (input.fix && pathViolations && pathViolations.length > 0) {
3519
- const pathFixed = [];
3520
- for (const v of pathViolations) {
3521
- try {
3522
- const absPath = `${input.vault}/${v.relPath}`;
3523
- const newRelPath = truncateFilename(v.relPath);
3524
- const newAbsPath = `${input.vault}/${newRelPath}`;
3525
- await rename6(absPath, newAbsPath);
3526
- const oldPathEscaped = v.relPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3527
- for (const page of allPages) {
3528
- if (page.relPath === v.relPath) continue;
3529
- const content = await readFile16(page.absPath, "utf8");
3530
- if (!content.includes(v.relPath)) continue;
3531
- let updated = content;
3532
- const citationRe = new RegExp(`\\^\\[${oldPathEscaped}\\]`, "g");
3533
- updated = updated.replace(citationRe, `^[${newRelPath}]`);
3534
- const wikilinkRe = new RegExp(`\\[\\[${oldPathEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
3535
- updated = updated.replace(wikilinkRe, (_m, alias) => `[[${newRelPath}${alias ?? ""}]]`);
3536
- if (updated !== content) {
3537
- const w = await safeWritePage(page.absPath, updated);
3538
- if (!w.ok) {
3539
- unresolved.push(`${page.relPath} (rewire)`);
3540
- }
3541
- }
3542
- }
3543
- pathFixed.push(v.relPath);
3544
- } catch {
3545
- unresolved.push(v.relPath);
3546
- }
3547
- }
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));
3548
3786
  fixed.push(...pathFixed);
3549
3787
  if (pathFixed.length > 0) {
3550
3788
  const fixedSet = new Set(pathFixed);
@@ -3558,13 +3796,6 @@ ${newBody}`;
3558
3796
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
3559
3797
  const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
3560
3798
  if (input.only) {
3561
- const allKnown = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
3562
- if (!allKnown.includes(input.only)) {
3563
- return {
3564
- exitCode: ExitCode.USAGE,
3565
- result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${allKnown.join(", ")}` }
3566
- };
3567
- }
3568
3799
  const match = [...errorOut, ...warningOut, ...infoOut].filter((b) => b.kind === input.only);
3569
3800
  const severity = ERROR_ORDER.includes(input.only) ? "error" : WARNING_ORDER.includes(input.only) ? "warning" : "info";
3570
3801
  const filtered = severity === "error" ? { error: match, warning: [], info: [] } : severity === "warning" ? { error: [], warning: match, info: [] } : { error: [], warning: [], info: match };
@@ -3628,14 +3859,14 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
3628
3859
  }
3629
3860
 
3630
3861
  // src/commands/config.ts
3631
- import { readFile as readFile17 } from "fs/promises";
3632
- import { existsSync as existsSync5 } from "fs";
3633
- import { join as join22 } from "path";
3862
+ import { readFile as readFile18 } from "fs/promises";
3863
+ import { existsSync as existsSync6 } from "fs";
3864
+ import { join as join23 } from "path";
3634
3865
  function validateKey(key) {
3635
3866
  return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
3636
3867
  }
3637
3868
  function configPath(home) {
3638
- return join22(home, ".skillwiki", ".env");
3869
+ return join23(home, ".skillwiki", ".env");
3639
3870
  }
3640
3871
  async function runConfigGet(input) {
3641
3872
  if (!validateKey(input.key)) {
@@ -3653,7 +3884,7 @@ async function runConfigSet(input) {
3653
3884
  try {
3654
3885
  let originalContent;
3655
3886
  try {
3656
- originalContent = await readFile17(filePath, "utf8");
3887
+ originalContent = await readFile18(filePath, "utf8");
3657
3888
  } catch {
3658
3889
  }
3659
3890
  const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
@@ -3685,18 +3916,18 @@ async function runConfigList(input) {
3685
3916
  }
3686
3917
  async function runConfigPath(input) {
3687
3918
  const filePath = configPath(input.home);
3688
- return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync5(filePath), humanHint: filePath }) };
3919
+ return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync6(filePath), humanHint: filePath }) };
3689
3920
  }
3690
3921
 
3691
3922
  // src/commands/doctor.ts
3692
- import { existsSync as existsSync8, lstatSync, readlinkSync, readdirSync, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
3693
- import { join as join26, 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";
3694
3925
  import { execSync as execSync2 } from "child_process";
3695
3926
  import { platform as platform2 } from "os";
3696
3927
 
3697
3928
  // src/utils/auto-update.ts
3698
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
3699
- import { join as join23, 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";
3700
3931
  import { spawn } from "child_process";
3701
3932
 
3702
3933
  // src/utils/update-consts.ts
@@ -3707,7 +3938,7 @@ var CLI_DISABLE_FLAG = "--no-update-notifier";
3707
3938
 
3708
3939
  // src/utils/auto-update.ts
3709
3940
  function cachePath(home) {
3710
- return join23(home, ".skillwiki", CACHE_FILENAME);
3941
+ return join24(home, ".skillwiki", CACHE_FILENAME);
3711
3942
  }
3712
3943
  function readCacheRaw(home) {
3713
3944
  try {
@@ -3726,7 +3957,7 @@ function readCache(home) {
3726
3957
  }
3727
3958
  function writeCache(home, cache) {
3728
3959
  const p = cachePath(home);
3729
- mkdirSync3(dirname8(p), { recursive: true });
3960
+ mkdirSync3(dirname9(p), { recursive: true });
3730
3961
  writeFileSync4(p, JSON.stringify(cache, null, 2));
3731
3962
  }
3732
3963
  function latestFromCache(home, currentVersion) {
@@ -3745,7 +3976,7 @@ function triggerAutoUpdate(home, currentVersion) {
3745
3976
  const { isStale: isStale2 } = readCache(home);
3746
3977
  if (!isStale2) return;
3747
3978
  const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
3748
- if (!existsSync6(bgScript)) return;
3979
+ if (!existsSync7(bgScript)) return;
3749
3980
  const child = spawn(process.execPath, [bgScript, home, currentVersion], {
3750
3981
  detached: true,
3751
3982
  stdio: "ignore"
@@ -3757,12 +3988,12 @@ function triggerAutoUpdate(home, currentVersion) {
3757
3988
 
3758
3989
  // src/utils/plugin-registry.ts
3759
3990
  import { readFileSync as readFileSync5 } from "fs";
3760
- import { join as join24 } from "path";
3761
- var REGISTRY_PATH = join24(".claude", "plugins", "installed_plugins.json");
3991
+ import { join as join25 } from "path";
3992
+ var REGISTRY_PATH = join25(".claude", "plugins", "installed_plugins.json");
3762
3993
  var PLUGIN_KEY = "skillwiki@llm-wiki";
3763
3994
  function readInstalledPlugins(home) {
3764
3995
  try {
3765
- const raw = readFileSync5(join24(home, REGISTRY_PATH), "utf8");
3996
+ const raw = readFileSync5(join25(home, REGISTRY_PATH), "utf8");
3766
3997
  return JSON.parse(raw);
3767
3998
  } catch {
3768
3999
  return null;
@@ -3779,8 +4010,8 @@ function findPlugin(home, key = PLUGIN_KEY) {
3779
4010
  // src/utils/s3-mount-health.ts
3780
4011
  import { execSync } from "child_process";
3781
4012
  import { platform } from "os";
3782
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readFileSync as readFile18 } from "fs";
3783
- import { join as join25 } 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";
3784
4015
  var OS = platform();
3785
4016
  function findRcloneMountPid() {
3786
4017
  try {
@@ -3938,7 +4169,7 @@ function detectFuseMount(vaultPath) {
3938
4169
  return null;
3939
4170
  }
3940
4171
  function writeTest(dir) {
3941
- const testFile = join25(dir, `.doctor-write-test-${process.pid}.tmp`);
4172
+ const testFile = join26(dir, `.doctor-write-test-${process.pid}.tmp`);
3942
4173
  const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
3943
4174
  const start = Date.now();
3944
4175
  try {
@@ -3949,7 +4180,7 @@ function writeTest(dir) {
3949
4180
  const writeMs = Date.now() - start;
3950
4181
  const readStart = Date.now();
3951
4182
  try {
3952
- const back = readFile18(testFile, "utf8");
4183
+ const back = readFile19(testFile, "utf8");
3953
4184
  const readMs = Date.now() - readStart;
3954
4185
  if (back !== payload) {
3955
4186
  try {
@@ -3971,6 +4202,37 @@ function writeTest(dir) {
3971
4202
  }
3972
4203
  return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
3973
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
+ }
3974
4236
  var FLAG_THRESHOLDS = {
3975
4237
  "--vfs-write-back": { min: 15, unit: "s", label: "VFS write-back window" },
3976
4238
  "--vfs-write-wait": { min: 10, unit: "s", label: "VFS write-wait" },
@@ -3992,14 +4254,14 @@ function checkNodeVersion() {
3992
4254
  function detectCliChannels(argv, home) {
3993
4255
  const channels = [];
3994
4256
  if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
3995
- const devPath = resolve4(argv[1]);
4257
+ const devPath = resolve5(argv[1]);
3996
4258
  channels.push({ name: "dev", path: devPath, isDevLink: true });
3997
4259
  }
3998
4260
  try {
3999
4261
  const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
4000
4262
  if (whichOut) {
4001
4263
  const isDev = isDevSymlink(whichOut);
4002
- if (!channels.some((c) => c.path === resolve4(whichOut))) {
4264
+ if (!channels.some((c) => c.path === resolve5(whichOut))) {
4003
4265
  channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
4004
4266
  }
4005
4267
  }
@@ -4007,13 +4269,13 @@ function detectCliChannels(argv, home) {
4007
4269
  }
4008
4270
  const plugin = findPlugin(home);
4009
4271
  if (plugin) {
4010
- const pluginBin = join26(plugin.installPath, "bin", "skillwiki");
4011
- if (existsSync8(pluginBin)) {
4272
+ const pluginBin = join27(plugin.installPath, "bin", "skillwiki");
4273
+ if (existsSync9(pluginBin)) {
4012
4274
  channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
4013
4275
  }
4014
4276
  }
4015
- const installBin = join26(home, ".claude", "skills", "bin", "skillwiki");
4016
- if (existsSync8(installBin)) {
4277
+ const installBin = join27(home, ".claude", "skills", "bin", "skillwiki");
4278
+ if (existsSync9(installBin)) {
4017
4279
  channels.push({ name: "install", path: installBin, isDevLink: false });
4018
4280
  }
4019
4281
  return channels;
@@ -4022,7 +4284,7 @@ function isDevSymlink(binPath) {
4022
4284
  try {
4023
4285
  const st = lstatSync(binPath);
4024
4286
  if (st.isSymbolicLink()) {
4025
- const target = resolve4(binPath, "..", readlinkSync(binPath));
4287
+ const target = resolve5(binPath, "..", readlinkSync(binPath));
4026
4288
  return target.includes("packages/cli") || target.includes("packages\\cli");
4027
4289
  }
4028
4290
  } catch {
@@ -4065,7 +4327,7 @@ function checkCliChannels(argv, home) {
4065
4327
  }
4066
4328
  async function checkConfigFile(home) {
4067
4329
  const cfgPath = configPath(home);
4068
- if (!existsSync8(cfgPath)) {
4330
+ if (!existsSync9(cfgPath)) {
4069
4331
  return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
4070
4332
  }
4071
4333
  try {
@@ -4080,7 +4342,7 @@ function checkWikiPathExists(resolvedPath) {
4080
4342
  if (resolvedPath === void 0) {
4081
4343
  return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
4082
4344
  }
4083
- if (existsSync8(resolvedPath) && statSync3(resolvedPath).isDirectory()) {
4345
+ if (existsSync9(resolvedPath) && statSync3(resolvedPath).isDirectory()) {
4084
4346
  return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
4085
4347
  }
4086
4348
  return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
@@ -4089,13 +4351,13 @@ function checkVaultStructure(resolvedPath) {
4089
4351
  if (resolvedPath === void 0) {
4090
4352
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
4091
4353
  }
4092
- if (!existsSync8(resolvedPath)) {
4354
+ if (!existsSync9(resolvedPath)) {
4093
4355
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
4094
4356
  }
4095
4357
  const missing = [];
4096
- if (!existsSync8(join26(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
4358
+ if (!existsSync9(join27(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
4097
4359
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
4098
- if (!existsSync8(join26(resolvedPath, dir))) missing.push(dir + "/");
4360
+ if (!existsSync9(join27(resolvedPath, dir))) missing.push(dir + "/");
4099
4361
  }
4100
4362
  if (missing.length === 0) {
4101
4363
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -4103,23 +4365,23 @@ function checkVaultStructure(resolvedPath) {
4103
4365
  return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
4104
4366
  }
4105
4367
  function checkSkillsInstalled(home, cwd) {
4106
- const srcDir = cwd ? join26(cwd, "packages", "skills") : void 0;
4107
- if (srcDir && existsSync8(srcDir)) {
4108
- const found = findSkillMd(srcDir);
4368
+ const srcDir = cwd ? join27(cwd, "packages", "skills") : void 0;
4369
+ if (srcDir && existsSync9(srcDir)) {
4370
+ const found = findInstalledSkillMd(srcDir);
4109
4371
  if (found.length > 0) {
4110
4372
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
4111
4373
  }
4112
4374
  }
4113
4375
  const plugin = findPlugin(home);
4114
4376
  if (plugin) {
4115
- const found = findSkillMd(plugin.installPath);
4377
+ const found = findInstalledSkillMd(plugin.installPath);
4116
4378
  if (found.length > 0) {
4117
4379
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
4118
4380
  }
4119
4381
  }
4120
- const skillsDir = join26(home, ".claude", "skills");
4121
- if (existsSync8(skillsDir)) {
4122
- const found = findSkillMd(skillsDir);
4382
+ const skillsDir = join27(home, ".claude", "skills");
4383
+ if (existsSync9(skillsDir)) {
4384
+ const found = findInstalledSkillMd(skillsDir);
4123
4385
  if (found.length > 0) {
4124
4386
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
4125
4387
  }
@@ -4128,10 +4390,10 @@ function checkSkillsInstalled(home, cwd) {
4128
4390
  }
4129
4391
  function checkDuplicateSkills(home) {
4130
4392
  const plugin = findPlugin(home);
4131
- const skillsDir = join26(home, ".claude", "skills");
4393
+ const skillsDir = join27(home, ".claude", "skills");
4132
4394
  const agentSkillDirs = [
4133
- { label: "~/.codex/skills/", path: join26(home, ".codex", "skills") },
4134
- { label: "~/.agents/skills/", path: join26(home, ".agents", "skills") }
4395
+ { label: "~/.codex/skills/", path: join27(home, ".codex", "skills") },
4396
+ { label: "~/.agents/skills/", path: join27(home, ".agents", "skills") }
4135
4397
  ];
4136
4398
  if (!plugin) {
4137
4399
  return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
@@ -4208,8 +4470,8 @@ async function checkProfiles(home) {
4208
4470
  }
4209
4471
  async function checkProjectLocalOverride(cwd) {
4210
4472
  const dir = cwd ?? process.cwd();
4211
- const envPath = join26(dir, ".skillwiki", ".env");
4212
- if (existsSync8(envPath)) {
4473
+ const envPath = join27(dir, ".skillwiki", ".env");
4474
+ if (existsSync9(envPath)) {
4213
4475
  return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
4214
4476
  }
4215
4477
  return check("pass", "project_local", "Project-local config", "None");
@@ -4218,7 +4480,7 @@ function checkVaultGitRemote(resolvedPath) {
4218
4480
  if (resolvedPath === void 0) {
4219
4481
  return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
4220
4482
  }
4221
- if (!existsSync8(join26(resolvedPath, ".git"))) {
4483
+ if (!existsSync9(join27(resolvedPath, ".git"))) {
4222
4484
  return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
4223
4485
  }
4224
4486
  try {
@@ -4241,9 +4503,9 @@ function checkObsidianTemplates(resolvedPath) {
4241
4503
  return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
4242
4504
  }
4243
4505
  const missing = [];
4244
- if (!existsSync8(join26(resolvedPath, "_Templates"))) missing.push("_Templates/");
4245
- if (!existsSync8(join26(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
4246
- if (!existsSync8(join26(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");
4247
4509
  if (missing.length === 0) {
4248
4510
  return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
4249
4511
  }
@@ -4253,8 +4515,8 @@ function checkDotStoreClean(resolvedPath) {
4253
4515
  if (resolvedPath === void 0) {
4254
4516
  return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
4255
4517
  }
4256
- const rawDir = join26(resolvedPath, "raw");
4257
- if (!existsSync8(rawDir)) {
4518
+ const rawDir = join27(resolvedPath, "raw");
4519
+ if (!existsSync9(rawDir)) {
4258
4520
  return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
4259
4521
  }
4260
4522
  const found = [];
@@ -4269,7 +4531,7 @@ function checkDotStoreClean(resolvedPath) {
4269
4531
  if (entry.name === ".DS_Store") {
4270
4532
  found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
4271
4533
  } else if (entry.isDirectory()) {
4272
- walk2(join26(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
4534
+ walk2(join27(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
4273
4535
  }
4274
4536
  }
4275
4537
  })(rawDir, "");
@@ -4282,7 +4544,7 @@ function checkSyncLastPush(resolvedPath) {
4282
4544
  if (resolvedPath === void 0) {
4283
4545
  return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
4284
4546
  }
4285
- if (!existsSync8(join26(resolvedPath, ".git"))) {
4547
+ if (!existsSync9(join27(resolvedPath, ".git"))) {
4286
4548
  return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
4287
4549
  }
4288
4550
  let timestamp;
@@ -4323,8 +4585,8 @@ function checkS3MountPerf(resolvedPath) {
4323
4585
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
4324
4586
  }
4325
4587
  const mountPoint = fuse.mountPoint;
4326
- const conceptsDir = join26(resolvedPath, "concepts");
4327
- if (!existsSync8(conceptsDir)) {
4588
+ const conceptsDir = join27(resolvedPath, "concepts");
4589
+ if (!existsSync9(conceptsDir)) {
4328
4590
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
4329
4591
  }
4330
4592
  const start = Date.now();
@@ -4363,6 +4625,73 @@ function checkS3MountPerf(resolvedPath) {
4363
4625
  `S3 FUSE mount, cache warm (rg scan: ${elapsed.toFixed(3)}s)`
4364
4626
  );
4365
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
+ }
4366
4695
  function checkRcloneFlagAudit(resolvedPath) {
4367
4696
  if (!resolvedPath) {
4368
4697
  return check("pass", "rclone_flags", "rclone VFS flags", "No vault path \u2014 check skipped");
@@ -4386,11 +4715,8 @@ function checkRcloneFlagAudit(resolvedPath) {
4386
4715
  warnings.push(`${flag} not set (default may be unsafe)`);
4387
4716
  continue;
4388
4717
  }
4389
- const value = parseFloat(raw);
4390
- if (isNaN(value)) continue;
4391
- let inSeconds = value;
4392
- if (raw.endsWith("h")) inSeconds = value * 3600;
4393
- else if (raw.endsWith("m")) inSeconds = value * 60;
4718
+ const inSeconds = parseDurationSeconds(raw);
4719
+ if (inSeconds === null) continue;
4394
4720
  const thresholdSec = threshold.unit === "h" ? threshold.min * 3600 : threshold.unit === "m" ? threshold.min * 60 : threshold.min;
4395
4721
  if (inSeconds < thresholdSec) {
4396
4722
  warnings.push(`${flag}=${raw} (recommended \u2265${threshold.min}${threshold.unit})`);
@@ -4442,8 +4768,8 @@ function checkWriteTest(resolvedPath) {
4442
4768
  if (!fuse) {
4443
4769
  return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
4444
4770
  }
4445
- const conceptsDir = join26(resolvedPath, "concepts");
4446
- if (!existsSync8(conceptsDir)) {
4771
+ const conceptsDir = join27(resolvedPath, "concepts");
4772
+ if (!existsSync9(conceptsDir)) {
4447
4773
  return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
4448
4774
  }
4449
4775
  const result = writeTest(conceptsDir);
@@ -4529,7 +4855,7 @@ function checkVfsCacheHealth(resolvedPath) {
4529
4855
  }
4530
4856
  function readVaultSyncConfig(home) {
4531
4857
  try {
4532
- const content = readFileSync7(join26(home, ".skillwiki", ".env"), "utf8");
4858
+ const content = readFileSync7(join27(home, ".skillwiki", ".env"), "utf8");
4533
4859
  let installed = false;
4534
4860
  let role;
4535
4861
  for (const line of content.split(/\r?\n/)) {
@@ -4563,12 +4889,12 @@ function vaultSyncChecks(input) {
4563
4889
  ];
4564
4890
  }
4565
4891
  const isMac = os === "darwin";
4566
- const logDir = input.logDir ?? (isMac ? join26(home, "Library", "Logs") : join26(home, ".local", "state", "vault-sync", "log"));
4567
- const shareDir = input.shareDir ?? (isMac ? join26(home, "Library", "Application Support", "vault-sync", "bin") : join26(home, ".local", "share", "vault-sync", "bin"));
4568
- const filterPath = input.filterPath ?? join26(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");
4569
4895
  const snapshotPath = input.snapshotScriptPath ?? "/root/.hermes/scripts/wiki-snapshot-v3.sh";
4570
- const pushScriptPath = join26(shareDir, "wiki-push.sh");
4571
- const c1 = existsSync8(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`);
4572
4898
  let c2;
4573
4899
  try {
4574
4900
  if (isMac) {
@@ -4619,7 +4945,7 @@ function vaultSyncChecks(input) {
4619
4945
  "Scheduler check failed \u2014 run vault-sync-install"
4620
4946
  );
4621
4947
  }
4622
- const logFile = join26(logDir, "wiki-push.log");
4948
+ const logFile = join27(logDir, "wiki-push.log");
4623
4949
  let c3;
4624
4950
  try {
4625
4951
  const logContent = readFileSync7(logFile, "utf8");
@@ -4678,7 +5004,7 @@ function vaultSyncChecks(input) {
4678
5004
  }
4679
5005
  }
4680
5006
  } catch {
4681
- c3 = existsSync8(logDir) ? check(
5007
+ c3 = existsSync9(logDir) ? check(
4682
5008
  "warn",
4683
5009
  "vault_sync_last_push_age",
4684
5010
  "Vault sync last push recency",
@@ -4690,7 +5016,7 @@ function vaultSyncChecks(input) {
4690
5016
  `Log directory not found at ${logDir}`
4691
5017
  );
4692
5018
  }
4693
- const fetchLogFile = join26(logDir, "wiki-fetch.log");
5019
+ const fetchLogFile = join27(logDir, "wiki-fetch.log");
4694
5020
  let cFetch;
4695
5021
  try {
4696
5022
  const logContent = readFileSync7(fetchLogFile, "utf8");
@@ -4737,7 +5063,7 @@ function vaultSyncChecks(input) {
4737
5063
  }
4738
5064
  let c4;
4739
5065
  try {
4740
- if (!existsSync8(filterPath)) {
5066
+ if (!existsSync9(filterPath)) {
4741
5067
  c4 = check(
4742
5068
  "error",
4743
5069
  "vault_sync_filter_present",
@@ -4786,7 +5112,7 @@ function vaultSyncChecks(input) {
4786
5112
  );
4787
5113
  } else {
4788
5114
  try {
4789
- if (!existsSync8(snapshotPath)) {
5115
+ if (!existsSync9(snapshotPath)) {
4790
5116
  c5 = check(
4791
5117
  "error",
4792
5118
  "vault_sync_snapshot_guard",
@@ -4832,13 +5158,17 @@ function findSkillMd(dir) {
4832
5158
  }
4833
5159
  for (const entry of entries) {
4834
5160
  if (entry.isFile() && entry.name === "SKILL.md") {
4835
- results.push(join26(dir, entry.name));
5161
+ results.push(join27(dir, entry.name));
4836
5162
  } else if (entry.isDirectory()) {
4837
- results.push(...findSkillMd(join26(dir, entry.name)));
5163
+ results.push(...findSkillMd(join27(dir, entry.name)));
4838
5164
  }
4839
5165
  }
4840
5166
  return results;
4841
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
+ }
4842
5172
  function findSkillNames(dir) {
4843
5173
  const results = [];
4844
5174
  let entries;
@@ -4848,12 +5178,61 @@ function findSkillNames(dir) {
4848
5178
  return results;
4849
5179
  }
4850
5180
  for (const entry of entries) {
4851
- if (entry.isDirectory() && existsSync8(join26(dir, entry.name, "SKILL.md"))) {
5181
+ if (entry.isDirectory() && existsSync9(join27(dir, entry.name, "SKILL.md"))) {
4852
5182
  results.push(entry.name);
4853
5183
  }
4854
5184
  }
4855
5185
  return results;
4856
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
+ }
4857
5236
  async function runDoctor(input) {
4858
5237
  const checks = [];
4859
5238
  const vsConfig = readVaultSyncConfig(input.home);
@@ -4876,6 +5255,7 @@ async function runDoctor(input) {
4876
5255
  checks.push(checkSyncLastPush(resolvedPath));
4877
5256
  checks.push(checkDotStoreClean(resolvedPath));
4878
5257
  checks.push(checkS3MountPerf(resolvedPath));
5258
+ checks.push(checkS3MountFreshness(resolvedPath));
4879
5259
  checks.push(checkRcloneFlagAudit(resolvedPath));
4880
5260
  checks.push(checkRcloneVersion(resolvedPath, vsConfig.installed));
4881
5261
  checks.push(checkWriteTest(resolvedPath));
@@ -4889,6 +5269,7 @@ async function runDoctor(input) {
4889
5269
  vaultSyncInstalled: vsConfig.installed,
4890
5270
  vaultSyncRole: vsConfig.role
4891
5271
  }));
5272
+ checks.push(...await vaultMetrics(resolvedPath));
4892
5273
  const summary = {
4893
5274
  pass: checks.filter((c) => c.status === "pass").length,
4894
5275
  info: checks.filter((c) => c.status === "info").length,
@@ -4912,8 +5293,8 @@ async function runDoctor(input) {
4912
5293
  }
4913
5294
 
4914
5295
  // src/commands/archive.ts
4915
- import { rename as rename7, mkdir as mkdir8, readFile as readFile19, writeFile as writeFile10 } from "fs/promises";
4916
- import { join as join27, 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";
4917
5298
  function countWikilinks(body, slug) {
4918
5299
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4919
5300
  const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
@@ -4941,7 +5322,7 @@ async function runArchive(input) {
4941
5322
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
4942
5323
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
4943
5324
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
4944
- const archivePath = join27("_archive", relPath).replace(/\\/g, "/");
5325
+ const archivePath = join28("_archive", relPath).replace(/\\/g, "/");
4945
5326
  let cascade;
4946
5327
  if (input.cascade) {
4947
5328
  const wikilinkRefs = [];
@@ -4965,7 +5346,7 @@ async function runArchive(input) {
4965
5346
  const indexRefs = [];
4966
5347
  if (!isRaw) {
4967
5348
  try {
4968
- const idx = await readFile19(join27(input.vault, "index.md"), "utf8");
5349
+ const idx = await readFile20(join28(input.vault, "index.md"), "utf8");
4969
5350
  idx.split("\n").forEach((line, i) => {
4970
5351
  if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
4971
5352
  });
@@ -4991,8 +5372,8 @@ async function runArchive(input) {
4991
5372
  }
4992
5373
  if (input.cascade && input.apply && cascade) {
4993
5374
  for (const ref of cascade.source_array_refs) {
4994
- const absPath = join27(input.vault, ref.page);
4995
- const text = await readFile19(absPath, "utf8");
5375
+ const absPath = join28(input.vault, ref.page);
5376
+ const text = await readFile20(absPath, "utf8");
4996
5377
  const split = splitFrontmatter(text);
4997
5378
  if (!split.ok) continue;
4998
5379
  const before = split.data.rawFrontmatter;
@@ -5009,12 +5390,12 @@ ${fmRewritten}
5009
5390
  }
5010
5391
  }
5011
5392
  }
5012
- await mkdir8(dirname9(join27(input.vault, archivePath)), { recursive: true });
5393
+ await mkdir9(dirname10(join28(input.vault, archivePath)), { recursive: true });
5013
5394
  let indexUpdated = false;
5014
5395
  if (!isRaw) {
5015
- const indexPath = join27(input.vault, "index.md");
5396
+ const indexPath = join28(input.vault, "index.md");
5016
5397
  try {
5017
- const idx = await readFile19(indexPath, "utf8");
5398
+ const idx = await readFile20(indexPath, "utf8");
5018
5399
  const originalLines = idx.split("\n");
5019
5400
  const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
5020
5401
  if (filtered.length !== originalLines.length) {
@@ -5025,7 +5406,7 @@ ${fmRewritten}
5025
5406
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
5026
5407
  }
5027
5408
  }
5028
- await rename7(join27(input.vault, relPath), join27(input.vault, archivePath));
5409
+ await rename7(join28(input.vault, relPath), join28(input.vault, archivePath));
5029
5410
  appendLastOp(input.vault, {
5030
5411
  operation: input.cascade ? "archive-cascade" : "archive",
5031
5412
  summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
@@ -5412,14 +5793,14 @@ ${newBody}`;
5412
5793
  // src/commands/update.ts
5413
5794
  import { execSync as execSync3 } from "child_process";
5414
5795
  import { readFileSync as readFileSync8 } from "fs";
5415
- import { join as join28 } from "path";
5796
+ import { join as join29 } from "path";
5416
5797
  function resolveGlobalSkillsRoot() {
5417
5798
  try {
5418
5799
  const globalRoot = execSync3("npm root -g", {
5419
5800
  encoding: "utf8",
5420
5801
  timeout: 5e3
5421
5802
  }).trim();
5422
- return join28(globalRoot, "skillwiki", "skills");
5803
+ return join29(globalRoot, "skillwiki", "skills");
5423
5804
  } catch {
5424
5805
  return null;
5425
5806
  }
@@ -5445,7 +5826,7 @@ async function runUpdate(input) {
5445
5826
  );
5446
5827
  const currentVersion = pkg2.version;
5447
5828
  const tag = input.distTag ?? "latest";
5448
- const target = join28(input.home, ".claude", "skills");
5829
+ const target = join29(input.home, ".claude", "skills");
5449
5830
  let latest;
5450
5831
  try {
5451
5832
  latest = execSync3(`npm view skillwiki@${tag} version`, {
@@ -5515,16 +5896,16 @@ async function runUpdate(input) {
5515
5896
 
5516
5897
  // src/commands/self-update.ts
5517
5898
  import { execSync as execSync4 } from "child_process";
5518
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
5519
- import { join as join29 } from "path";
5899
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
5900
+ import { join as join30 } from "path";
5520
5901
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
5521
5902
  async function runSelfUpdate(input) {
5522
5903
  const currentVersion = JSON.parse(
5523
5904
  readFileSync9(new URL("../../package.json", import.meta.url), "utf8")
5524
5905
  ).version;
5525
5906
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
5526
- const localPkgPath = join29(sourceRoot, "packages", "cli", "package.json");
5527
- const hasLocalSource = existsSync9(localPkgPath);
5907
+ const localPkgPath = join30(sourceRoot, "packages", "cli", "package.json");
5908
+ const hasLocalSource = existsSync10(localPkgPath);
5528
5909
  if (input.check) {
5529
5910
  let availableVersion = null;
5530
5911
  let source;
@@ -5655,10 +6036,10 @@ async function runSelfUpdate(input) {
5655
6036
  }
5656
6037
 
5657
6038
  // src/commands/transcripts.ts
5658
- import { readdir as readdir5, stat as stat7, readFile as readFile20 } from "fs/promises";
5659
- import { join as join30 } from "path";
6039
+ import { readdir as readdir5, stat as stat7, readFile as readFile21 } from "fs/promises";
6040
+ import { join as join31 } from "path";
5660
6041
  async function runTranscripts(input) {
5661
- const dir = join30(input.vault, "raw", "transcripts");
6042
+ const dir = join31(input.vault, "raw", "transcripts");
5662
6043
  let entries;
5663
6044
  try {
5664
6045
  entries = await readdir5(dir, { withFileTypes: true });
@@ -5668,8 +6049,8 @@ async function runTranscripts(input) {
5668
6049
  const transcripts = [];
5669
6050
  for (const entry of entries) {
5670
6051
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
5671
- const filePath = join30(dir, entry.name);
5672
- const content = await readFile20(filePath, "utf8");
6052
+ const filePath = join31(dir, entry.name);
6053
+ const content = await readFile21(filePath, "utf8");
5673
6054
  const fm = extractFrontmatter(content);
5674
6055
  if (!fm.ok) continue;
5675
6056
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
@@ -5686,12 +6067,12 @@ async function runTranscripts(input) {
5686
6067
  }
5687
6068
 
5688
6069
  // src/commands/project-index.ts
5689
- import { readdir as readdir6, readFile as readFile21, writeFile as writeFile11, mkdir as mkdir9 } from "fs/promises";
5690
- import { join as join31, 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";
5691
6072
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
5692
6073
  async function runProjectIndex(input) {
5693
6074
  const slug = input.slug;
5694
- const projectDir = join31(input.vault, "projects", slug);
6075
+ const projectDir = join32(input.vault, "projects", slug);
5695
6076
  try {
5696
6077
  await readdir6(projectDir);
5697
6078
  } catch {
@@ -5702,15 +6083,15 @@ async function runProjectIndex(input) {
5702
6083
  }
5703
6084
  const wikilinkPattern = `[[${slug}]]`;
5704
6085
  const entries = [];
5705
- const compoundDir = join31(input.vault, "projects", slug, "compound");
6086
+ const compoundDir = join32(input.vault, "projects", slug, "compound");
5706
6087
  try {
5707
6088
  const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
5708
6089
  for (const entry of compoundFiles) {
5709
6090
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
5710
- const filePath = join31(compoundDir, entry.name);
6091
+ const filePath = join32(compoundDir, entry.name);
5711
6092
  let text;
5712
6093
  try {
5713
- text = await readFile21(filePath, "utf8");
6094
+ text = await readFile22(filePath, "utf8");
5714
6095
  } catch {
5715
6096
  continue;
5716
6097
  }
@@ -5727,16 +6108,16 @@ async function runProjectIndex(input) {
5727
6108
  for (const dir of LAYER2_DIRS) {
5728
6109
  let files;
5729
6110
  try {
5730
- files = await readdir6(join31(input.vault, dir), { withFileTypes: true });
6111
+ files = await readdir6(join32(input.vault, dir), { withFileTypes: true });
5731
6112
  } catch {
5732
6113
  continue;
5733
6114
  }
5734
6115
  for (const entry of files) {
5735
6116
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
5736
- const filePath = join31(input.vault, dir, entry.name);
6117
+ const filePath = join32(input.vault, dir, entry.name);
5737
6118
  let text;
5738
6119
  try {
5739
- text = await readFile21(filePath, "utf8");
6120
+ text = await readFile22(filePath, "utf8");
5740
6121
  } catch {
5741
6122
  continue;
5742
6123
  }
@@ -5757,11 +6138,11 @@ async function runProjectIndex(input) {
5757
6138
  const tb = typeOrder[b.type] ?? 99;
5758
6139
  return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
5759
6140
  });
5760
- const indexPath = join31(projectDir, "knowledge.md");
6141
+ const indexPath = join32(projectDir, "knowledge.md");
5761
6142
  let existing = false;
5762
6143
  let stale = false;
5763
6144
  try {
5764
- const existingText = await readFile21(indexPath, "utf8");
6145
+ const existingText = await readFile22(indexPath, "utf8");
5765
6146
  existing = true;
5766
6147
  const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
5767
6148
  const existingPages = new Set(existingEntries.map((l) => {
@@ -5801,7 +6182,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
5801
6182
  }
5802
6183
  if (input.apply) {
5803
6184
  try {
5804
- await mkdir9(dirname10(indexPath), { recursive: true });
6185
+ await mkdir10(dirname11(indexPath), { recursive: true });
5805
6186
  await writeFile11(indexPath, body, "utf8");
5806
6187
  } catch (e) {
5807
6188
  return {
@@ -5830,10 +6211,10 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
5830
6211
  }
5831
6212
 
5832
6213
  // src/commands/compound.ts
5833
- import { writeFile as writeFile12, mkdir as mkdir10, readdir as readdir7, unlink as unlink3 } from "fs/promises";
5834
- import { join as join32 } from "path";
5835
- import { existsSync as existsSync10 } from "fs";
5836
- import { readFile as readFile22 } 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";
5837
6218
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
5838
6219
  var FIELD_RE = {
5839
6220
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -5931,17 +6312,17 @@ function extractRetroFields(date, cycleName, block) {
5931
6312
  };
5932
6313
  }
5933
6314
  async function runCompound(input) {
5934
- const logPath = join32(input.vault, "log.md");
6315
+ const logPath = join33(input.vault, "log.md");
5935
6316
  let logText;
5936
6317
  try {
5937
- logText = await readFile22(logPath, "utf8");
6318
+ logText = await readFile23(logPath, "utf8");
5938
6319
  } catch {
5939
6320
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
5940
6321
  }
5941
6322
  const entries = parseRetroEntries(logText);
5942
6323
  const promoted = [];
5943
6324
  const skipped = [];
5944
- const compoundDir = join32(input.vault, "projects", input.project, "compound");
6325
+ const compoundDir = join33(input.vault, "projects", input.project, "compound");
5945
6326
  for (const entry of entries) {
5946
6327
  const generalizeValue = entry.generalize.trim();
5947
6328
  if (!/^yes/i.test(generalizeValue)) {
@@ -5949,8 +6330,8 @@ async function runCompound(input) {
5949
6330
  continue;
5950
6331
  }
5951
6332
  const slug = slugify(entry.cycleName);
5952
- const compoundPath = join32(compoundDir, `${slug}.md`);
5953
- if (existsSync10(compoundPath)) {
6333
+ const compoundPath = join33(compoundDir, `${slug}.md`);
6334
+ if (existsSync11(compoundPath)) {
5954
6335
  skipped.push(entry.date);
5955
6336
  continue;
5956
6337
  }
@@ -5988,8 +6369,8 @@ async function runCompound(input) {
5988
6369
  ].join("\n");
5989
6370
  const content = frontmatter + "\n" + body;
5990
6371
  if (!input.dryRun) {
5991
- if (!existsSync10(compoundDir)) {
5992
- await mkdir10(compoundDir, { recursive: true });
6372
+ if (!existsSync11(compoundDir)) {
6373
+ await mkdir11(compoundDir, { recursive: true });
5993
6374
  }
5994
6375
  await writeFile12(compoundPath, content, "utf8");
5995
6376
  }
@@ -6010,23 +6391,23 @@ async function runCompound(input) {
6010
6391
  };
6011
6392
  }
6012
6393
  async function runCompoundDelete(input) {
6013
- const projectDir = join32(input.vault, "projects", input.project);
6014
- if (!existsSync10(projectDir)) {
6394
+ const projectDir = join33(input.vault, "projects", input.project);
6395
+ if (!existsSync11(projectDir)) {
6015
6396
  return {
6016
6397
  exitCode: ExitCode.PROJECT_NOT_FOUND,
6017
6398
  result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
6018
6399
  };
6019
6400
  }
6020
6401
  const entryName = input.entry.replace(/\.md$/, "");
6021
- const compoundPath = join32(projectDir, "compound", `${entryName}.md`);
6022
- if (!existsSync10(compoundPath)) {
6402
+ const compoundPath = join33(projectDir, "compound", `${entryName}.md`);
6403
+ if (!existsSync11(compoundPath)) {
6023
6404
  return {
6024
6405
  exitCode: ExitCode.FILE_NOT_FOUND,
6025
6406
  result: err("FILE_NOT_FOUND", { path: compoundPath })
6026
6407
  };
6027
6408
  }
6028
6409
  try {
6029
- await unlink3(compoundPath);
6410
+ await unlink4(compoundPath);
6030
6411
  } catch (e) {
6031
6412
  return {
6032
6413
  exitCode: ExitCode.WRITE_FAILED,
@@ -6052,8 +6433,8 @@ knowledge.md regenerated`
6052
6433
  };
6053
6434
  }
6054
6435
  async function runCompoundList(input) {
6055
- const compoundDir = join32(input.vault, "projects", input.project, "compound");
6056
- if (!existsSync10(compoundDir)) {
6436
+ const compoundDir = join33(input.vault, "projects", input.project, "compound");
6437
+ if (!existsSync11(compoundDir)) {
6057
6438
  return {
6058
6439
  exitCode: ExitCode.OK,
6059
6440
  result: ok({
@@ -6083,10 +6464,10 @@ could not read compound directory`
6083
6464
  const entries = [];
6084
6465
  for (const dirent of dirents) {
6085
6466
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
6086
- const filePath = join32(compoundDir, dirent.name);
6467
+ const filePath = join33(compoundDir, dirent.name);
6087
6468
  let text;
6088
6469
  try {
6089
- text = await readFile22(filePath, "utf8");
6470
+ text = await readFile23(filePath, "utf8");
6090
6471
  } catch {
6091
6472
  continue;
6092
6473
  }
@@ -6115,9 +6496,9 @@ no compound entries found`;
6115
6496
  }
6116
6497
 
6117
6498
  // src/commands/observe.ts
6118
- import { mkdir as mkdir11, writeFile as writeFile13 } from "fs/promises";
6119
- import { existsSync as existsSync11, statSync as statSync4 } from "fs";
6120
- import { join as join33 } 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";
6121
6502
  import { createHash as createHash4 } from "crypto";
6122
6503
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
6123
6504
  function slugify2(text) {
@@ -6140,15 +6521,15 @@ async function runObserve(input) {
6140
6521
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
6141
6522
  };
6142
6523
  }
6143
- if (!existsSync11(input.vault) || !statSync4(input.vault).isDirectory()) {
6524
+ if (!existsSync12(input.vault) || !statSync4(input.vault).isDirectory()) {
6144
6525
  return {
6145
6526
  exitCode: ExitCode.VAULT_PATH_INVALID,
6146
6527
  result: err("VAULT_PATH_INVALID", { path: input.vault })
6147
6528
  };
6148
6529
  }
6149
- const transcriptsDir = join33(input.vault, "raw", "transcripts");
6530
+ const transcriptsDir = join34(input.vault, "raw", "transcripts");
6150
6531
  try {
6151
- await mkdir11(transcriptsDir, { recursive: true });
6532
+ await mkdir12(transcriptsDir, { recursive: true });
6152
6533
  } catch {
6153
6534
  return {
6154
6535
  exitCode: ExitCode.VAULT_PATH_INVALID,
@@ -6158,7 +6539,7 @@ async function runObserve(input) {
6158
6539
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6159
6540
  const slug = slugify2(input.text);
6160
6541
  const fileName = `${today}-observation-${slug}.md`;
6161
- const filePath = join33(transcriptsDir, fileName);
6542
+ const filePath = join34(transcriptsDir, fileName);
6162
6543
  const body = `
6163
6544
  ${input.text.trim()}
6164
6545
  `;
@@ -6198,8 +6579,8 @@ ${input.text.trim()}
6198
6579
  }
6199
6580
 
6200
6581
  // src/commands/ingest.ts
6201
- import { readFile as readFile23, writeFile as writeFile14, mkdir as mkdir12 } from "fs/promises";
6202
- import { join as join34 } from "path";
6582
+ import { readFile as readFile24, writeFile as writeFile14, mkdir as mkdir13 } from "fs/promises";
6583
+ import { join as join35 } from "path";
6203
6584
  import { createHash as createHash5 } from "crypto";
6204
6585
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
6205
6586
  var TYPE_DIR = {
@@ -6358,7 +6739,7 @@ async function runIngest(input) {
6358
6739
  sourceContent = fetchResult.data.body;
6359
6740
  } else {
6360
6741
  try {
6361
- sourceContent = await readFile23(input.source, "utf8");
6742
+ sourceContent = await readFile24(input.source, "utf8");
6362
6743
  } catch {
6363
6744
  return {
6364
6745
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -6373,8 +6754,8 @@ async function runIngest(input) {
6373
6754
  const rawRelPath = `raw/articles/${slug}.md`;
6374
6755
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
6375
6756
  const typedRelPath = `${typedDir}/${slug}.md`;
6376
- const rawAbsPath = join34(input.vault, rawRelPath);
6377
- const typedAbsPath = join34(input.vault, typedRelPath);
6757
+ const rawAbsPath = join35(input.vault, rawRelPath);
6758
+ const typedAbsPath = join35(input.vault, typedRelPath);
6378
6759
  const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
6379
6760
  const typedContent = buildTypedContent(
6380
6761
  input.title,
@@ -6437,7 +6818,7 @@ async function runIngest(input) {
6437
6818
  };
6438
6819
  }
6439
6820
  try {
6440
- await mkdir12(join34(input.vault, "raw", "articles"), { recursive: true });
6821
+ await mkdir13(join35(input.vault, "raw", "articles"), { recursive: true });
6441
6822
  await writeFile14(rawAbsPath, rawContent, "utf8");
6442
6823
  } catch (e) {
6443
6824
  return {
@@ -6446,7 +6827,7 @@ async function runIngest(input) {
6446
6827
  };
6447
6828
  }
6448
6829
  try {
6449
- await mkdir12(join34(input.vault, typedDir), { recursive: true });
6830
+ await mkdir13(join35(input.vault, typedDir), { recursive: true });
6450
6831
  await writeFile14(typedAbsPath, typedContent, "utf8");
6451
6832
  } catch (e) {
6452
6833
  return {
@@ -6625,12 +7006,12 @@ ${body}`;
6625
7006
  }
6626
7007
 
6627
7008
  // src/commands/sync.ts
6628
- import { existsSync as existsSync13 } from "fs";
6629
- import { join as join36 } from "path";
7009
+ import { existsSync as existsSync14 } from "fs";
7010
+ import { join as join37 } from "path";
6630
7011
 
6631
7012
  // src/utils/sync-lock.ts
6632
- import { existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync10, renameSync, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "fs";
6633
- import { join as join35 } 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";
6634
7015
  import { createHash as createHash6 } from "crypto";
6635
7016
  function getSessionId() {
6636
7017
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
@@ -6638,11 +7019,11 @@ function getSessionId() {
6638
7019
  return process.pid.toString();
6639
7020
  }
6640
7021
  function lockPath(vault) {
6641
- return join35(vault, ".skillwiki", "sync.lock");
7022
+ return join36(vault, ".skillwiki", "sync.lock");
6642
7023
  }
6643
7024
  function readLock(vault) {
6644
7025
  const path = lockPath(vault);
6645
- if (!existsSync12(path)) return null;
7026
+ if (!existsSync13(path)) return null;
6646
7027
  try {
6647
7028
  const raw = readFileSync10(path, "utf8");
6648
7029
  return JSON.parse(raw);
@@ -6657,8 +7038,8 @@ function isStale(lock, now) {
6657
7038
  }
6658
7039
  function acquireLock(vault, opts = {}) {
6659
7040
  const path = lockPath(vault);
6660
- const dir = join35(vault, ".skillwiki");
6661
- if (!existsSync12(dir)) {
7041
+ const dir = join36(vault, ".skillwiki");
7042
+ if (!existsSync13(dir)) {
6662
7043
  mkdirSync4(dir, { recursive: true });
6663
7044
  }
6664
7045
  const sessionId = opts.sessionId ?? getSessionId();
@@ -6703,7 +7084,7 @@ function writeLockedFile(path, lock) {
6703
7084
  }
6704
7085
  function releaseLock(vault, opts = {}) {
6705
7086
  const path = lockPath(vault);
6706
- if (!existsSync12(path)) {
7087
+ if (!existsSync13(path)) {
6707
7088
  return { released: false };
6708
7089
  }
6709
7090
  const sessionId = opts.sessionId ?? getSessionId();
@@ -6732,7 +7113,7 @@ function releaseLock(vault, opts = {}) {
6732
7113
  function runSyncStatus(input) {
6733
7114
  const vault = input.vault;
6734
7115
  const includeStashes = input.includeStashes ?? false;
6735
- if (!existsSync13(join36(vault, ".git"))) {
7116
+ if (!existsSync14(join37(vault, ".git"))) {
6736
7117
  return {
6737
7118
  exitCode: ExitCode.VAULT_PATH_INVALID,
6738
7119
  result: ok({
@@ -6746,6 +7127,7 @@ function runSyncStatus(input) {
6746
7127
  })
6747
7128
  };
6748
7129
  }
7130
+ enableGitLongPathsOnWindows(vault);
6749
7131
  const porcelain = git(vault, ["status", "--porcelain"]);
6750
7132
  const dirty = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
6751
7133
  const revOutput = git(vault, ["rev-list", "--left-right", "--count", "origin/HEAD...HEAD"]);
@@ -6809,12 +7191,24 @@ function runSyncStatus(input) {
6809
7191
  }
6810
7192
  async function runSyncPush(input) {
6811
7193
  const vault = input.vault;
6812
- if (!existsSync13(join36(vault, ".git"))) {
7194
+ if (!existsSync14(join37(vault, ".git"))) {
6813
7195
  return {
6814
7196
  exitCode: ExitCode.VAULT_PATH_INVALID,
6815
7197
  result: err("NOT_A_GIT_REPO", { path: vault })
6816
7198
  };
6817
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
+ }
6818
7212
  const porcelain = git(vault, ["status", "--porcelain"]);
6819
7213
  const dirtyFiles = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0) : [];
6820
7214
  if (dirtyFiles.length === 0) {
@@ -6824,6 +7218,7 @@ async function runSyncPush(input) {
6824
7218
  files_committed: 0,
6825
7219
  commit_message: "",
6826
7220
  pushed: false,
7221
+ path_fixes: pathFixes,
6827
7222
  humanHint: "nothing to commit, working tree clean"
6828
7223
  })
6829
7224
  };
@@ -6878,7 +7273,8 @@ async function runSyncPush(input) {
6878
7273
  files_committed: dirtyFiles.length,
6879
7274
  commit_message: commitMessage,
6880
7275
  pushed: false,
6881
- 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)}`
6882
7278
  })
6883
7279
  };
6884
7280
  }
@@ -6888,7 +7284,8 @@ async function runSyncPush(input) {
6888
7284
  files_committed: dirtyFiles.length,
6889
7285
  commit_message: commitMessage,
6890
7286
  pushed,
6891
- 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)` : ""}`
6892
7289
  })
6893
7290
  };
6894
7291
  }
@@ -6911,14 +7308,19 @@ function enumerateStashes(vault) {
6911
7308
  }
6912
7309
  return stashes;
6913
7310
  }
7311
+ function enableGitLongPathsOnWindows(vault) {
7312
+ if (process.platform !== "win32") return;
7313
+ git(vault, ["config", "core.longpaths", "true"]);
7314
+ }
6914
7315
  async function runSyncPull(input) {
6915
7316
  const vault = input.vault;
6916
- if (!existsSync13(join36(vault, ".git"))) {
7317
+ if (!existsSync14(join37(vault, ".git"))) {
6917
7318
  return {
6918
7319
  exitCode: ExitCode.VAULT_PATH_INVALID,
6919
7320
  result: err("NOT_A_GIT_REPO", { path: vault })
6920
7321
  };
6921
7322
  }
7323
+ enableGitLongPathsOnWindows(vault);
6922
7324
  let fetched = false;
6923
7325
  try {
6924
7326
  gitStrict(vault, ["fetch", "origin"]);
@@ -7007,6 +7409,8 @@ async function runSyncPull(input) {
7007
7409
  };
7008
7410
  }
7009
7411
  }
7412
+ const pathFix = await fixPathTooLong({ vault });
7413
+ const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
7010
7414
  let lintErrors = 0;
7011
7415
  let lintWarnings = 0;
7012
7416
  const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
@@ -7018,6 +7422,7 @@ async function runSyncPull(input) {
7018
7422
  if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
7019
7423
  else hintParts.push("already up to date");
7020
7424
  if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
7425
+ if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
7021
7426
  if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
7022
7427
  if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
7023
7428
  const exitCode = lintErrors > 0 ? ExitCode.LINT_HAS_ERRORS : lintWarnings > 0 ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
@@ -7081,7 +7486,7 @@ function runSyncPeers(input) {
7081
7486
  }
7082
7487
  function runSyncLock(input) {
7083
7488
  const vault = input.vault;
7084
- if (!existsSync13(vault)) {
7489
+ if (!existsSync14(vault)) {
7085
7490
  return {
7086
7491
  exitCode: ExitCode.VAULT_PATH_INVALID,
7087
7492
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -7116,7 +7521,7 @@ function runSyncLock(input) {
7116
7521
  }
7117
7522
  function runSyncUnlock(input) {
7118
7523
  const vault = input.vault;
7119
- if (!existsSync13(vault)) {
7524
+ if (!existsSync14(vault)) {
7120
7525
  return {
7121
7526
  exitCode: ExitCode.VAULT_PATH_INVALID,
7122
7527
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -7150,7 +7555,7 @@ function runSyncUnlock(input) {
7150
7555
 
7151
7556
  // src/commands/backup.ts
7152
7557
  import { statSync as statSync5, readdirSync as readdirSync2, readFileSync as readFileSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
7153
- import { join as join37, relative as relative3, dirname as dirname11 } from "path";
7558
+ import { join as join38, relative as relative3, dirname as dirname12 } from "path";
7154
7559
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
7155
7560
 
7156
7561
  // src/utils/s3-client.ts
@@ -7170,11 +7575,11 @@ function createS3Client(config) {
7170
7575
  }
7171
7576
 
7172
7577
  // src/commands/backup.ts
7173
- 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"]);
7174
7579
  function* walkMarkdown(dir, base) {
7175
7580
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
7176
- if (SKIP_DIRS.has(entry.name)) continue;
7177
- const full = join37(dir, entry.name);
7581
+ if (SKIP_DIRS2.has(entry.name)) continue;
7582
+ const full = join38(dir, entry.name);
7178
7583
  if (entry.isDirectory()) {
7179
7584
  yield* walkMarkdown(full, base);
7180
7585
  } else if (entry.name.endsWith(".md")) {
@@ -7197,7 +7602,7 @@ async function runBackupSync(input) {
7197
7602
  let failed = 0;
7198
7603
  const files = [...walkMarkdown(input.vault, input.vault)];
7199
7604
  for (const relPath of files) {
7200
- const absPath = join37(input.vault, relPath);
7605
+ const absPath = join38(input.vault, relPath);
7201
7606
  const localStat = statSync5(absPath);
7202
7607
  let needsUpload = true;
7203
7608
  try {
@@ -7273,7 +7678,7 @@ async function runBackupRestore(input) {
7273
7678
  const objects = list.Contents ?? [];
7274
7679
  for (const obj of objects) {
7275
7680
  if (!obj.Key) continue;
7276
- const localPath = join37(target, obj.Key);
7681
+ const localPath = join38(target, obj.Key);
7277
7682
  try {
7278
7683
  const localStat = statSync5(localPath);
7279
7684
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -7286,7 +7691,7 @@ async function runBackupRestore(input) {
7286
7691
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
7287
7692
  const body = await resp.Body?.transformToByteArray();
7288
7693
  if (body) {
7289
- mkdirSync5(dirname11(localPath), { recursive: true });
7694
+ mkdirSync5(dirname12(localPath), { recursive: true });
7290
7695
  writeFileSync7(localPath, Buffer.from(body));
7291
7696
  downloaded++;
7292
7697
  }
@@ -7319,11 +7724,11 @@ async function runBackupRestore(input) {
7319
7724
  }
7320
7725
 
7321
7726
  // src/commands/status.ts
7322
- import { existsSync as existsSync14, statSync as statSync6 } from "fs";
7323
- import { readFile as readFile24 } from "fs/promises";
7324
- import { join as join38 } 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";
7325
7730
  async function runStatus(input) {
7326
- if (!existsSync14(input.vault)) {
7731
+ if (!existsSync15(input.vault)) {
7327
7732
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
7328
7733
  }
7329
7734
  const scan = await scanVault(input.vault);
@@ -7348,7 +7753,7 @@ async function runStatus(input) {
7348
7753
  const compound = scan.data.compound.length;
7349
7754
  let schemaVersion = "v1";
7350
7755
  try {
7351
- const schemaContent = await readFile24(join38(input.vault, "SCHEMA.md"), "utf8");
7756
+ const schemaContent = await readFile25(join39(input.vault, "SCHEMA.md"), "utf8");
7352
7757
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
7353
7758
  if (versionMatch) schemaVersion = versionMatch[1];
7354
7759
  } catch {
@@ -7408,8 +7813,8 @@ async function runStatus(input) {
7408
7813
  }
7409
7814
 
7410
7815
  // src/commands/seed.ts
7411
- import { mkdir as mkdir13, writeFile as writeFile15, stat as stat8 } from "fs/promises";
7412
- import { join as join39 } from "path";
7816
+ import { mkdir as mkdir14, writeFile as writeFile15, stat as stat8 } from "fs/promises";
7817
+ import { join as join40 } from "path";
7413
7818
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7414
7819
  var EXAMPLE_PAGES = {
7415
7820
  "entities/example-project.md": `---
@@ -7478,29 +7883,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
7478
7883
  `;
7479
7884
  async function runSeed(input) {
7480
7885
  try {
7481
- await stat8(join39(input.vault, "SCHEMA.md"));
7886
+ await stat8(join40(input.vault, "SCHEMA.md"));
7482
7887
  } catch {
7483
7888
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
7484
7889
  }
7485
7890
  const created = [];
7486
7891
  const skipped = [];
7487
7892
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
7488
- const absPath = join39(input.vault, relPath);
7893
+ const absPath = join40(input.vault, relPath);
7489
7894
  try {
7490
7895
  await stat8(absPath);
7491
7896
  skipped.push(relPath);
7492
7897
  } catch {
7493
- await mkdir13(join39(absPath, ".."), { recursive: true });
7898
+ await mkdir14(join40(absPath, ".."), { recursive: true });
7494
7899
  await writeFile15(absPath, content, "utf8");
7495
7900
  created.push(relPath);
7496
7901
  }
7497
7902
  }
7498
- const rawPath = join39(input.vault, "raw", "articles", "example-source.md");
7903
+ const rawPath = join40(input.vault, "raw", "articles", "example-source.md");
7499
7904
  try {
7500
7905
  await stat8(rawPath);
7501
7906
  skipped.push("raw/articles/example-source.md");
7502
7907
  } catch {
7503
- await mkdir13(join39(rawPath, ".."), { recursive: true });
7908
+ await mkdir14(join40(rawPath, ".."), { recursive: true });
7504
7909
  await writeFile15(rawPath, EXAMPLE_RAW, "utf8");
7505
7910
  created.push("raw/articles/example-source.md");
7506
7911
  }
@@ -7523,9 +7928,9 @@ async function runSeed(input) {
7523
7928
  }
7524
7929
 
7525
7930
  // src/commands/canvas.ts
7526
- import { readFile as readFile25, writeFile as writeFile16 } from "fs/promises";
7527
- import { existsSync as existsSync15 } from "fs";
7528
- import { join as join40 } 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";
7529
7934
  var NODE_WIDTH = 240;
7530
7935
  var NODE_HEIGHT = 60;
7531
7936
  var COLUMN_SPACING = 400;
@@ -7603,8 +8008,8 @@ function buildCanvasEdges(adjacency) {
7603
8008
  return edges;
7604
8009
  }
7605
8010
  async function runCanvasGenerate(input) {
7606
- const graphPath = input.graphPath ?? join40(input.vault, ".skillwiki", "graph.json");
7607
- if (!existsSync15(graphPath)) {
8011
+ const graphPath = input.graphPath ?? join41(input.vault, ".skillwiki", "graph.json");
8012
+ if (!existsSync16(graphPath)) {
7608
8013
  return {
7609
8014
  exitCode: ExitCode.FILE_NOT_FOUND,
7610
8015
  result: err("FILE_NOT_FOUND", {
@@ -7615,7 +8020,7 @@ async function runCanvasGenerate(input) {
7615
8020
  }
7616
8021
  let raw;
7617
8022
  try {
7618
- raw = await readFile25(graphPath, "utf8");
8023
+ raw = await readFile26(graphPath, "utf8");
7619
8024
  } catch (e) {
7620
8025
  return {
7621
8026
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -7641,7 +8046,7 @@ async function runCanvasGenerate(input) {
7641
8046
  const nodes = buildCanvasNodes(paths);
7642
8047
  const edges = buildCanvasEdges(graph.adjacency);
7643
8048
  const canvas = { nodes, edges };
7644
- const outPath = join40(input.vault, "vault-graph.canvas");
8049
+ const outPath = join41(input.vault, "vault-graph.canvas");
7645
8050
  try {
7646
8051
  await writeFile16(outPath, JSON.stringify(canvas, null, 2));
7647
8052
  } catch (e) {
@@ -7663,8 +8068,8 @@ written: ${outPath}`
7663
8068
  }
7664
8069
 
7665
8070
  // src/commands/query.ts
7666
- import { readFile as readFile26, stat as stat9 } from "fs/promises";
7667
- import { join as join41 } from "path";
8071
+ import { readFile as readFile27, stat as stat9 } from "fs/promises";
8072
+ import { join as join42 } from "path";
7668
8073
  var W_KEYWORD = 2;
7669
8074
  var W_SOURCE_OVERLAP = 4;
7670
8075
  var W_WIKILINK = 3;
@@ -7785,7 +8190,7 @@ function computeKeywordScore(terms, title, tags, body) {
7785
8190
  return score;
7786
8191
  }
7787
8192
  async function loadOrBuildGraph(vault) {
7788
- const graphPath = join41(vault, ".skillwiki", "graph.json");
8193
+ const graphPath = join42(vault, ".skillwiki", "graph.json");
7789
8194
  let needsBuild = false;
7790
8195
  try {
7791
8196
  const fileStat = await stat9(graphPath);
@@ -7799,7 +8204,7 @@ async function loadOrBuildGraph(vault) {
7799
8204
  if (buildResult.exitCode !== 0) return null;
7800
8205
  }
7801
8206
  try {
7802
- const raw = await readFile26(graphPath, "utf8");
8207
+ const raw = await readFile27(graphPath, "utf8");
7803
8208
  return JSON.parse(raw);
7804
8209
  } catch {
7805
8210
  return null;
@@ -7807,14 +8212,14 @@ async function loadOrBuildGraph(vault) {
7807
8212
  }
7808
8213
 
7809
8214
  // src/utils/auto-commit.ts
7810
- import { existsSync as existsSync16 } from "fs";
7811
- import { join as join42 } from "path";
8215
+ import { existsSync as existsSync17 } from "fs";
8216
+ import { join as join43 } from "path";
7812
8217
  async function postCommit(vault, exitCode) {
7813
8218
  if (exitCode !== 0) return;
7814
8219
  const home = process.env.HOME ?? "";
7815
8220
  const dotenv = await parseDotenvFile(configPath(home));
7816
8221
  if (dotenv["AUTO_COMMIT"] === "false") return;
7817
- if (!existsSync16(join42(vault, ".git"))) return;
8222
+ if (!existsSync17(join43(vault, ".git"))) return;
7818
8223
  const lastOps = readLastOp(vault);
7819
8224
  if (lastOps.length === 0) return;
7820
8225
  const porcelain = git(vault, ["status", "--porcelain"]);
@@ -7865,7 +8270,7 @@ program.command("validate <file>").description("validate vault page frontmatter
7865
8270
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
7866
8271
  });
7867
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) => {
7868
- const out = opts.out ?? join43(vault, ".skillwiki", "graph.json");
8273
+ const out = opts.out ?? join44(vault, ".skillwiki", "graph.json");
7869
8274
  emit(await runGraphBuild({ vault, out }), vault);
7870
8275
  });
7871
8276
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");