skillwiki 0.2.0-beta.8 → 0.2.0

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
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ semverGt
4
+ } from "./chunk-XM5IYZX7.js";
2
5
 
3
6
  // src/cli.ts
4
- import { readFileSync } from "fs";
7
+ import { readFileSync as readFileSync5 } from "fs";
5
8
  import { Command } from "commander";
6
9
 
7
10
  // src/utils/output.ts
@@ -66,7 +69,11 @@ var ExitCode = {
66
69
  DOCTOR_HAS_ERRORS: 29,
67
70
  ARCHIVE_TARGET_NOT_FOUND: 30,
68
71
  ARCHIVE_ALREADY_ARCHIVED: 31,
69
- DRIFT_DETECTED: 32
72
+ DRIFT_DETECTED: 32,
73
+ RAW_DEDUP_DETECTED: 33,
74
+ MIGRATION_APPLIED: 34,
75
+ UNKNOWN_WIKI_PROFILE: 35,
76
+ DEDUP_APPLIED: 36
70
77
  };
71
78
 
72
79
  // ../shared/src/json-output.ts
@@ -106,10 +113,10 @@ var TypedKnowledgeSchema = z.object({
106
113
  });
107
114
  var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
108
115
  var RawSourceSchema = z.object({
109
- title: z.string().min(1),
110
- source_url: z.string().url().nullable(),
116
+ title: z.string().min(1).optional(),
117
+ source_url: z.string().nullable(),
111
118
  ingested: isoDate,
112
- ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]),
119
+ ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
113
120
  sha256: sha256Hex,
114
121
  project: wikilink.optional(),
115
122
  work_item: wikilink.optional(),
@@ -499,6 +506,16 @@ import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from
499
506
  import { dirname as dirname2 } from "path";
500
507
  var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
501
508
  var _whitelist = new Set(CONFIG_KEYS);
509
+ var PROFILE_PATH_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/;
510
+ var PROFILE_LANG_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_LANG$/;
511
+ var PROFILE_DEFAULT_RE = /^WIKI_DEFAULT$/;
512
+ function isValidWikiProfileKey(key) {
513
+ if (key === "WIKI_PATH" || key === "WIKI_LANG") return false;
514
+ return PROFILE_PATH_RE.test(key) || PROFILE_LANG_RE.test(key) || PROFILE_DEFAULT_RE.test(key);
515
+ }
516
+ function profileKey(name, suffix) {
517
+ return `WIKI_${name.toUpperCase().replace(/-/g, "_").replace(/[^A-Z0-9_]/g, "")}_${suffix}`;
518
+ }
502
519
  function parseDotenvText(text) {
503
520
  const out = {};
504
521
  for (const rawLine of text.split(/\r?\n/)) {
@@ -508,7 +525,7 @@ function parseDotenvText(text) {
508
525
  if (eq <= 0) continue;
509
526
  const key = line.slice(0, eq).trim();
510
527
  const value = line.slice(eq + 1).trim();
511
- if (!_whitelist.has(key)) continue;
528
+ if (!_whitelist.has(key) && !isValidWikiProfileKey(key)) continue;
512
529
  if (value.length === 0) continue;
513
530
  out[key] = value;
514
531
  }
@@ -593,6 +610,14 @@ async function resolveInitTimePath(input) {
593
610
  return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
594
611
  }
595
612
  if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
613
+ if (input.cwd) {
614
+ const projCfg = await parseDotenvFile(join2(input.cwd, ".skillwiki", ".env"));
615
+ if (projCfg.WIKI_PATH !== void 0) {
616
+ if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
617
+ return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
618
+ }
619
+ }
620
+ if (input.explain) chain.push({ source: "project-dotenv", matched: false });
596
621
  const fallback = join2(input.home, "wiki");
597
622
  if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
598
623
  return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
@@ -604,15 +629,73 @@ async function resolveRuntimePath(input) {
604
629
  return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
605
630
  }
606
631
  if (input.explain) chain.push({ source: "flag", matched: false });
632
+ const swGlobal = await parseDotenvFile(join2(input.home, ".skillwiki", ".env"));
633
+ const wikiName = input.wiki;
634
+ if (wikiName !== void 0 && wikiName.length > 0) {
635
+ if (wikiName.toLowerCase() === "default") {
636
+ const path2 = swGlobal.WIKI_PATH;
637
+ if (path2 !== void 0) {
638
+ if (input.explain) chain.push({ source: "wiki-profile", matched: true, value: path2 });
639
+ return ok({ path: path2, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} });
640
+ }
641
+ if (input.explain) chain.push({ source: "wiki-profile", matched: false });
642
+ return err("UNKNOWN_WIKI_PROFILE", {
643
+ message: `Wiki profile "default" not found. Set it with: skillwiki config set wiki.path <dir>`
644
+ });
645
+ }
646
+ const key = profileKey(wikiName, "PATH");
647
+ const path = swGlobal[key];
648
+ if (path !== void 0) {
649
+ if (input.explain) chain.push({ source: "wiki-profile", matched: true, value: path });
650
+ return ok({ path, source: "wiki-profile", ...input.explain ? { chain } : {} });
651
+ }
652
+ if (input.explain) chain.push({ source: "wiki-profile", matched: false });
653
+ return err("UNKNOWN_WIKI_PROFILE", {
654
+ message: `Wiki profile "${wikiName}" not found. Set it with: skillwiki config set wiki.${wikiName}.path <dir>`
655
+ });
656
+ }
657
+ if (input.wikiEnv !== void 0 && input.wikiEnv.length > 0) {
658
+ const key = profileKey(input.wikiEnv, "PATH");
659
+ const path = swGlobal[key];
660
+ if (path !== void 0) {
661
+ if (input.explain) chain.push({ source: "wiki-profile", matched: true, value: path });
662
+ return ok({ path, source: "wiki-profile", ...input.explain ? { chain } : {} });
663
+ }
664
+ if (input.explain) chain.push({ source: "wiki-profile", matched: false });
665
+ return err("UNKNOWN_WIKI_PROFILE", {
666
+ message: `Wiki profile "${input.wikiEnv}" not found (from $WIKI env). Set it with: skillwiki config set wiki.${input.wikiEnv}.path <dir>`
667
+ });
668
+ }
669
+ if (input.explain) chain.push({ source: "wiki-profile", matched: false });
607
670
  if (input.envValue !== void 0 && input.envValue.length > 0) {
608
671
  if (input.explain) chain.push({ source: "env", matched: true, value: input.envValue });
609
672
  return ok({ path: input.envValue, source: "env", ...input.explain ? { chain } : {} });
610
673
  }
611
674
  if (input.explain) chain.push({ source: "env", matched: false });
612
- const sw = await parseDotenvFile(join2(input.home, ".skillwiki", ".env"));
613
- if (sw.WIKI_PATH !== void 0) {
614
- if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
615
- return ok({ path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} });
675
+ if (input.cwd) {
676
+ const projCfg = await parseDotenvFile(join2(input.cwd, ".skillwiki", ".env"));
677
+ if (projCfg.WIKI_PATH !== void 0) {
678
+ if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
679
+ return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
680
+ }
681
+ if (input.explain) chain.push({ source: "project-dotenv", matched: false });
682
+ }
683
+ const defaultProfile = swGlobal["WIKI_DEFAULT"];
684
+ if (defaultProfile !== void 0) {
685
+ const key = profileKey(defaultProfile, "PATH");
686
+ const path = swGlobal[key];
687
+ if (path !== void 0) {
688
+ if (input.explain) chain.push({ source: "wiki-default", matched: true, value: path });
689
+ return ok({ path, source: "wiki-default", ...input.explain ? { chain } : {} });
690
+ }
691
+ if (input.explain) chain.push({ source: "wiki-default", matched: false });
692
+ return err("UNKNOWN_WIKI_PROFILE", {
693
+ message: `Default wiki profile "${defaultProfile}" not found. Set it with: skillwiki config set wiki.${defaultProfile}.path <dir>`
694
+ });
695
+ }
696
+ if (swGlobal.WIKI_PATH !== void 0) {
697
+ if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: swGlobal.WIKI_PATH });
698
+ return ok({ path: swGlobal.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} });
616
699
  }
617
700
  if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
618
701
  return err("NO_VAULT_CONFIGURED", {
@@ -626,15 +709,21 @@ async function runOrphans(input) {
626
709
  if (input.vault) {
627
710
  vault = input.vault;
628
711
  } else {
629
- const r = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home ?? "" });
630
- if (!r.ok) return { exitCode: ExitCode.NO_VAULT_CONFIGURED, result: r };
712
+ const r = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home ?? "", wiki: input.wiki });
713
+ if (!r.ok) {
714
+ const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? ExitCode.UNKNOWN_WIKI_PROFILE : ExitCode.NO_VAULT_CONFIGURED;
715
+ return { exitCode, result: r };
716
+ }
631
717
  vault = r.data.path;
632
718
  }
633
719
  const scan = await scanVault(vault);
634
720
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
635
721
  const slugToPath = {};
636
722
  for (const p of scan.data.typedKnowledge) {
637
- slugToPath[p.relPath.replace(/\.md$/, "").split("/").pop()] = p.relPath;
723
+ const rel = p.relPath.replace(/\.md$/, "");
724
+ slugToPath[rel] = p.relPath;
725
+ const filename = rel.split("/").pop();
726
+ if (!(filename in slugToPath)) slugToPath[filename] = p.relPath;
638
727
  }
639
728
  const adj = {};
640
729
  for (const p of scan.data.typedKnowledge) adj[p.relPath] = /* @__PURE__ */ new Set();
@@ -643,7 +732,7 @@ async function runOrphans(input) {
643
732
  const split = splitFrontmatter(text);
644
733
  const body = split.ok ? split.data.body : text;
645
734
  for (const slug of extractBodyWikilinks(body)) {
646
- const tgt = slugToPath[slug.split("/").pop()];
735
+ const tgt = slugToPath[slug] ?? slugToPath[slug.split("/").pop()];
647
736
  if (tgt) {
648
737
  adj[p.relPath].add(tgt);
649
738
  adj[tgt].add(p.relPath);
@@ -703,16 +792,92 @@ import { dirname as dirname3, resolve, join as join3 } from "path";
703
792
 
704
793
  // src/parsers/citations.ts
705
794
  var FENCE2 = /```[\s\S]*?```/g;
795
+ var INLINE_CODE = /`[^`\n]+`/g;
796
+ var MARKER_RE = /\^\[(raw\/[^\]]+)\]/g;
797
+ function stripFences(body) {
798
+ return body.replace(FENCE2, "").replace(INLINE_CODE, "");
799
+ }
800
+ function stripFencedBlocks(body) {
801
+ return body.replace(FENCE2, "");
802
+ }
706
803
  function extractCitationMarkers(body) {
707
- const stripped = body.replace(FENCE2, "");
804
+ const stripped = stripFences(body);
708
805
  const out = [];
709
- const re = /\^\[(raw\/[^\]]+)\]/g;
710
806
  let m;
711
- while ((m = re.exec(stripped)) !== null) {
807
+ while ((m = MARKER_RE.exec(stripped)) !== null) {
712
808
  out.push({ marker: m[0], target: m[1] });
713
809
  }
714
810
  return out;
715
811
  }
812
+ function hasSourcesFooter(body) {
813
+ return /^## Sources\s*$/m.test(stripFencedBlocks(body));
814
+ }
815
+ function isLegacyCitationStyle(body) {
816
+ const markers = extractCitationMarkers(body);
817
+ if (markers.length === 0) return false;
818
+ if (!hasSourcesFooter(body)) return true;
819
+ const lines = stripFences(body).split("\n");
820
+ let inSources = false;
821
+ for (const line of lines) {
822
+ if (/^## Sources\b/.test(line.trim())) {
823
+ inSources = true;
824
+ continue;
825
+ }
826
+ if (inSources) continue;
827
+ const markerOnly = line.replace(MARKER_RE, "").trim();
828
+ if (markerOnly.length === 0 && /\^\[raw\//.test(line)) return true;
829
+ const lastMarkerIdx = line.lastIndexOf("^[raw/");
830
+ if (lastMarkerIdx >= 0) {
831
+ const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE, "").trim();
832
+ if (afterLast.length > 0) return true;
833
+ const beforeFirst = line.slice(0, line.indexOf("^[raw/")).trim();
834
+ if (beforeFirst.length > 0 && !/[.!?]\s*$/.test(beforeFirst)) return true;
835
+ }
836
+ }
837
+ return false;
838
+ }
839
+ function hasOrphanedCitations(body) {
840
+ const stripped = stripFences(body);
841
+ const lines = stripped.split("\n");
842
+ let inSources = false;
843
+ let sourcesEnded = false;
844
+ let sourcesStartLine = -1;
845
+ let lastNonBlankInSources = -1;
846
+ for (let i = 0; i < lines.length; i++) {
847
+ const line = lines[i];
848
+ const trimmed = line.trim();
849
+ if (/^## Sources\b/.test(trimmed)) {
850
+ inSources = true;
851
+ sourcesStartLine = i;
852
+ continue;
853
+ }
854
+ if (!inSources || sourcesEnded) continue;
855
+ if (trimmed.length === 0) {
856
+ if (lastNonBlankInSources >= 0) {
857
+ sourcesEnded = true;
858
+ }
859
+ continue;
860
+ }
861
+ const isListItem = /^\s*[-*]\s+/.test(line);
862
+ const hasMarker = /\^\[raw\//.test(line);
863
+ if (isListItem && hasMarker) {
864
+ lastNonBlankInSources = i;
865
+ } else if (hasMarker && !isListItem) {
866
+ return true;
867
+ } else {
868
+ sourcesEnded = true;
869
+ }
870
+ }
871
+ if (sourcesStartLine === -1) return false;
872
+ if (sourcesEnded) {
873
+ for (let i = lastNonBlankInSources + 1; i < lines.length; i++) {
874
+ if (/\^\[raw\//.test(lines[i])) {
875
+ return true;
876
+ }
877
+ }
878
+ }
879
+ return false;
880
+ }
716
881
 
717
882
  // src/commands/audit.ts
718
883
  async function runAudit(input) {
@@ -737,24 +902,40 @@ async function runAudit(input) {
737
902
  return { ...m, resolved: false };
738
903
  }
739
904
  }));
740
- const sources = fm.data.sources ?? [];
905
+ const sources = (fm.data.sources ?? []).map((s) => s.replace(/^\^\[/, "").replace(/\]$/, ""));
741
906
  const referenced = new Set(resolved.map((m) => m.target));
742
907
  const unused_sources = sources.filter((s) => !referenced.has(s));
743
908
  const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
744
909
  const broken = resolved.filter((m) => !m.resolved);
910
+ const footerMatch = body.match(/\n## Sources\n([\s\S]*)$/);
911
+ let footer_consistency;
912
+ if (footerMatch) {
913
+ const footerTargets = /* @__PURE__ */ new Set();
914
+ const footerRe = /\^\[(raw\/[^\]]+)\]/g;
915
+ let mm;
916
+ while ((mm = footerRe.exec(footerMatch[1])) !== null) footerTargets.add(mm[1]);
917
+ const bodyTargets = new Set(resolved.map((m) => m.target));
918
+ const missing_from_footer = [...bodyTargets].filter((t) => !footerTargets.has(t));
919
+ const extra_in_footer = [...footerTargets].filter((t) => !bodyTargets.has(t));
920
+ footer_consistency = { missing_from_footer, extra_in_footer };
921
+ }
745
922
  const hintLines = [];
746
923
  hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
747
924
  if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
748
925
  if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
926
+ if (footer_consistency) {
927
+ if (footer_consistency.missing_from_footer.length > 0) hintLines.push(`missing_from_footer: ${footer_consistency.missing_from_footer.length}`);
928
+ if (footer_consistency.extra_in_footer.length > 0) hintLines.push(`extra_in_footer: ${footer_consistency.extra_in_footer.length}`);
929
+ }
749
930
  if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
750
931
  const humanHint = hintLines.join("\n");
751
932
  if (resolved.some((m) => !m.resolved)) {
752
- return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
933
+ return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
753
934
  }
754
935
  if (unused_sources.length > 0 || missing_from_sources.length > 0) {
755
- return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
936
+ return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
756
937
  }
757
- return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
938
+ return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, footer_consistency, humanHint }) };
758
939
  }
759
940
  async function findVaultRoot(start) {
760
941
  let cur = start;
@@ -832,6 +1013,20 @@ async function runInstall(input) {
832
1013
  installed.push(dst);
833
1014
  if (r.data.backupPath) backed_up.push(r.data.backupPath);
834
1015
  }
1016
+ const binSrc = join4(input.skillsRoot, "bin", "skillwiki");
1017
+ try {
1018
+ await stat4(binSrc);
1019
+ const binDst = join4(input.target, "bin", "skillwiki");
1020
+ if (!input.dryRun) {
1021
+ const r = await atomicCopyWithBackup(binSrc, binDst);
1022
+ if (!r.ok) return { exitCode: ExitCode.ATOMIC_COPY_FAILED, result: r };
1023
+ installed.push(binDst);
1024
+ if (r.data.backupPath) backed_up.push(r.data.backupPath);
1025
+ } else {
1026
+ installed.push(binDst);
1027
+ }
1028
+ } catch {
1029
+ }
835
1030
  const manifest_path = join4(input.target, "wiki-manifest.json");
836
1031
  if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
837
1032
  const hintLines = [
@@ -857,9 +1052,13 @@ async function runPath(input) {
857
1052
  flag: input.flag,
858
1053
  envValue: input.envValue,
859
1054
  home: input.home,
1055
+ wiki: input.wiki,
860
1056
  explain: input.explain
861
1057
  });
862
- if (!r.ok) return { exitCode: ExitCode.NO_VAULT_CONFIGURED, result: r };
1058
+ if (!r.ok) {
1059
+ const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? ExitCode.UNKNOWN_WIKI_PROFILE : ExitCode.NO_VAULT_CONFIGURED;
1060
+ return { exitCode, result: r };
1061
+ }
863
1062
  return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {}, humanHint: `${r.data.path} (via ${r.data.source})` }) };
864
1063
  }
865
1064
 
@@ -972,8 +1171,10 @@ var VAULT_DIRS = [
972
1171
  "comparisons",
973
1172
  "queries",
974
1173
  "meta",
975
- "projects"
1174
+ "projects",
1175
+ ".obsidian"
976
1176
  ];
1177
+ var ATTACHMENT_FOLDER = "raw/assets";
977
1178
  function extractDomainFromSchema(text) {
978
1179
  const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
979
1180
  if (!m) return "";
@@ -1028,13 +1229,13 @@ async function runInit(input) {
1028
1229
  }
1029
1230
  const existingEnv = parseDotenvText(existingEnvRaw);
1030
1231
  const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
1031
- if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
1232
+ if (!input.profile && existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
1032
1233
  return {
1033
1234
  exitCode: ExitCode.ENV_WRITE_CONFLICT,
1034
1235
  result: err("ENV_WRITE_CONFLICT", { key: "WIKI_PATH", existing: existingEnv.WIKI_PATH, attempted: target })
1035
1236
  };
1036
1237
  }
1037
- if (existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
1238
+ if (!input.profile && existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
1038
1239
  return {
1039
1240
  exitCode: ExitCode.ENV_WRITE_CONFLICT,
1040
1241
  result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
@@ -1103,17 +1304,29 @@ async function runInit(input) {
1103
1304
  return tpl.replace("{{INIT_DATE}}", today);
1104
1305
  });
1105
1306
  if (err1) return err1;
1106
- const err2 = await writeOrPreserve("log.md", async () => {
1307
+ const errObsidian = await writeOrPreserve(".obsidian/app.json", async () => {
1308
+ return JSON.stringify({ attachmentFolderPath: ATTACHMENT_FOLDER }, null, 2) + "\n";
1309
+ });
1310
+ if (errObsidian) return errObsidian;
1311
+ const err22 = await writeOrPreserve("log.md", async () => {
1107
1312
  const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
1108
1313
  return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
1109
1314
  });
1110
- if (err2) return err2;
1111
- const isTempPath = target.startsWith("/tmp/") || target === "/tmp" || target.startsWith("/var/tmp/") || target === "/var/tmp" || target.startsWith("/private/tmp/");
1112
- const skipEnv = !!input.noEnv || isTempPath;
1315
+ if (err22) return err22;
1316
+ const skipEnv = !!input.noEnv;
1113
1317
  let envWritten = "";
1114
1318
  if (!skipEnv) {
1115
1319
  try {
1116
- await writeDotenv(envPath, { WIKI_PATH: target, WIKI_LANG: canonicalLang }, existingEnvRaw);
1320
+ const envEntries = {};
1321
+ if (input.profile) {
1322
+ envEntries[profileKey(input.profile, "PATH")] = target;
1323
+ envEntries[profileKey(input.profile, "LANG")] = canonicalLang;
1324
+ envEntries["WIKI_DEFAULT"] = input.profile;
1325
+ } else {
1326
+ envEntries["WIKI_PATH"] = target;
1327
+ envEntries["WIKI_LANG"] = canonicalLang;
1328
+ }
1329
+ await writeDotenv(envPath, envEntries, existingEnvRaw);
1117
1330
  envWritten = envPath;
1118
1331
  } catch (e) {
1119
1332
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
@@ -1160,7 +1373,8 @@ function buildSlugMap(pages) {
1160
1373
  async function runLinks(input) {
1161
1374
  const scan = await scanVault(input.vault);
1162
1375
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1163
- const slugs = buildSlugMap(scan.data.typedKnowledge);
1376
+ const allPages = [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound];
1377
+ const slugs = buildSlugMap(allPages);
1164
1378
  const broken = [];
1165
1379
  for (const p of scan.data.typedKnowledge) {
1166
1380
  const text = await readPage(p);
@@ -1168,7 +1382,7 @@ async function runLinks(input) {
1168
1382
  const body = split.ok ? split.data.body : text;
1169
1383
  const lines = body.split("\n");
1170
1384
  for (const slug of extractBodyWikilinks(body)) {
1171
- const tail = slug.split("/").pop();
1385
+ const tail = slug.split("/").pop().replace(/\.md$/, "");
1172
1386
  if (!slugs.has(tail.toLowerCase())) {
1173
1387
  const line = lines.findIndex((l) => l.includes(`[[${slug}`));
1174
1388
  broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
@@ -1395,10 +1609,102 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
1395
1609
  return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
1396
1610
  }
1397
1611
 
1612
+ // src/commands/dedup.ts
1613
+ import { readFileSync, writeFileSync, unlinkSync } from "fs";
1614
+ import { join as join13 } from "path";
1615
+ async function runDedup(input) {
1616
+ const scan = await scanVault(input.vault);
1617
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1618
+ const hashMap = /* @__PURE__ */ new Map();
1619
+ let totalFiles = 0;
1620
+ for (const raw of scan.data.raw) {
1621
+ const fm = extractFrontmatter(await readPage(raw));
1622
+ if (!fm.ok) continue;
1623
+ const sha = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
1624
+ if (!sha || sha.length !== 64) continue;
1625
+ totalFiles++;
1626
+ const existing = hashMap.get(sha);
1627
+ if (existing) existing.push(raw.relPath);
1628
+ else hashMap.set(sha, [raw.relPath]);
1629
+ }
1630
+ const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files }));
1631
+ const rewired = [];
1632
+ const removed = [];
1633
+ if (input.apply && duplicates.length > 0) {
1634
+ const replacements = /* @__PURE__ */ new Map();
1635
+ for (const group of duplicates) {
1636
+ const canonical = group.files[0];
1637
+ for (let i = 1; i < group.files.length; i++) {
1638
+ replacements.set(group.files[i], canonical);
1639
+ }
1640
+ }
1641
+ for (const page of scan.data.typedKnowledge) {
1642
+ const text = readFileSync(join13(input.vault, page.relPath), "utf-8");
1643
+ let updated = text;
1644
+ let changed = false;
1645
+ for (const [oldPath, newPath] of replacements) {
1646
+ const oldMarker = `^[${oldPath}]`;
1647
+ const newMarker = `^[${newPath}]`;
1648
+ if (updated.includes(oldMarker)) {
1649
+ updated = updated.replaceAll(oldMarker, newMarker);
1650
+ changed = true;
1651
+ }
1652
+ const oldFm = `- "^[${oldPath}]"`;
1653
+ const newFm = `- "^[${newPath}]"`;
1654
+ if (updated.includes(oldFm)) {
1655
+ updated = updated.replaceAll(oldFm, newFm);
1656
+ changed = true;
1657
+ }
1658
+ }
1659
+ if (changed) {
1660
+ writeFileSync(join13(input.vault, page.relPath), updated);
1661
+ rewired.push(page.relPath);
1662
+ }
1663
+ }
1664
+ for (const [oldPath] of replacements) {
1665
+ const fullPath = join13(input.vault, oldPath);
1666
+ try {
1667
+ unlinkSync(fullPath);
1668
+ removed.push(oldPath);
1669
+ } catch {
1670
+ }
1671
+ }
1672
+ }
1673
+ const exitCode = duplicates.length > 0 ? input.apply ? ExitCode.DEDUP_APPLIED : ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
1674
+ const hintLines = [`scanned: ${totalFiles} raw files`];
1675
+ if (duplicates.length > 0) {
1676
+ hintLines.push(`duplicates: ${duplicates.length}`);
1677
+ for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
1678
+ if (input.apply) {
1679
+ hintLines.push(`rewired: ${rewired.length} pages`);
1680
+ hintLines.push(`removed: ${removed.length} raw files`);
1681
+ }
1682
+ } else {
1683
+ hintLines.push("0 duplicates");
1684
+ }
1685
+ return {
1686
+ exitCode,
1687
+ result: ok({ scanned: totalFiles, duplicates, rewired, removed, humanHint: hintLines.join("\n") })
1688
+ };
1689
+ }
1690
+
1398
1691
  // src/commands/lint.ts
1399
- var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_drift", "tag_not_in_taxonomy"];
1400
- var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
1401
- var INFO_ORDER = ["bridges", "low_confidence_single_source", "topic_map_recommended"];
1692
+ var STRUCT_MIN_BODY_LINES = 60;
1693
+ var STRUCT_MIN_SECTIONS = 3;
1694
+ function hasDuplicateFrontmatter(body) {
1695
+ if (/^---\r?\n/.test(body)) return true;
1696
+ const lines = body.split(/\r?\n/);
1697
+ const limit = Math.min(lines.length, 20);
1698
+ let seenYamlKey = false;
1699
+ for (let i = 0; i < limit; i++) {
1700
+ if (/^\w[\w-]*:/.test(lines[i].trim())) seenYamlKey = true;
1701
+ if (seenYamlKey && lines[i].trim() === "---") return true;
1702
+ }
1703
+ return false;
1704
+ }
1705
+ var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
1706
+ var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "missing_overview"];
1707
+ var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink"];
1402
1708
  async function runLint(input) {
1403
1709
  const buckets = {};
1404
1710
  const links = await runLinks({ vault: input.vault });
@@ -1439,6 +1745,56 @@ async function runLint(input) {
1439
1745
  if (topicMap.result.ok && topicMap.result.data.recommended) {
1440
1746
  buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
1441
1747
  }
1748
+ const dedup = await runDedup({ vault: input.vault });
1749
+ if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
1750
+ const scan = await scanVault(input.vault);
1751
+ const allPages = scan.ok ? [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound] : [];
1752
+ const slugs = scan.ok ? buildSlugMap(allPages) : /* @__PURE__ */ new Map();
1753
+ if (scan.ok) {
1754
+ const legacyPages = [];
1755
+ const orphanedPages = [];
1756
+ const structFlags = [];
1757
+ const dupFrontmatter = [];
1758
+ const noOverview = [];
1759
+ const fmWikilinkFlags = [];
1760
+ for (const page of scan.data.typedKnowledge) {
1761
+ const text = await readPage(page);
1762
+ const split = splitFrontmatter(text);
1763
+ if (!split.ok) continue;
1764
+ const body = split.data.body;
1765
+ const rawFm = split.data.rawFrontmatter;
1766
+ if (hasDuplicateFrontmatter(body)) dupFrontmatter.push(page.relPath);
1767
+ if (isLegacyCitationStyle(body)) legacyPages.push(page.relPath);
1768
+ if (hasOrphanedCitations(body)) orphanedPages.push(page.relPath);
1769
+ const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
1770
+ for (const link of fmLinks) {
1771
+ const target = link.replace(/^\[\[/, "").replace(/(?:\|[^\[\]]*)?\]\]$/, "").trim();
1772
+ const tail = target.split("/").pop().replace(/\.md$/, "");
1773
+ if (!slugs.has(tail.toLowerCase())) {
1774
+ fmWikilinkFlags.push(`${page.relPath}: [[${target}]] does not resolve`);
1775
+ }
1776
+ }
1777
+ const bodyLines = body.split("\n").filter((l) => l.trim().length > 0).length;
1778
+ const hasOverview = /^## Overview/m.test(body);
1779
+ if (!hasOverview) noOverview.push(page.relPath);
1780
+ if (bodyLines < STRUCT_MIN_BODY_LINES) {
1781
+ const hasRelated = /^## (Related|Relationships)/m.test(body);
1782
+ const sectionCount = (body.match(/^## /gm) ?? []).length;
1783
+ if (!hasRelated || sectionCount < STRUCT_MIN_SECTIONS) {
1784
+ const reasons = [];
1785
+ if (!hasRelated) reasons.push("no Related or Relationships");
1786
+ if (sectionCount < STRUCT_MIN_SECTIONS) reasons.push(`only ${sectionCount} sections`);
1787
+ structFlags.push(`${page.relPath}: ${bodyLines} lines, ${reasons.join(", ")}`);
1788
+ }
1789
+ }
1790
+ }
1791
+ if (legacyPages.length > 0) buckets.legacy_citation_style = legacyPages;
1792
+ if (orphanedPages.length > 0) buckets.orphaned_citations = orphanedPages;
1793
+ if (structFlags.length > 0) buckets.page_structure = structFlags;
1794
+ if (dupFrontmatter.length > 0) buckets.duplicate_frontmatter = dupFrontmatter;
1795
+ if (noOverview.length > 0) buckets.missing_overview = noOverview;
1796
+ if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
1797
+ }
1442
1798
  const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1443
1799
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1444
1800
  const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
@@ -1473,12 +1829,12 @@ async function runLint(input) {
1473
1829
  // src/commands/config.ts
1474
1830
  import { readFile as readFile12 } from "fs/promises";
1475
1831
  import { existsSync } from "fs";
1476
- import { join as join13 } from "path";
1832
+ import { join as join14 } from "path";
1477
1833
  function validateKey(key) {
1478
- return CONFIG_KEYS.includes(key);
1834
+ return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
1479
1835
  }
1480
1836
  function configPath(home) {
1481
- return join13(home, ".skillwiki", ".env");
1837
+ return join14(home, ".skillwiki", ".env");
1482
1838
  }
1483
1839
  async function runConfigGet(input) {
1484
1840
  if (!validateKey(input.key)) {
@@ -1510,7 +1866,21 @@ async function runConfigSet(input) {
1510
1866
  async function runConfigList(input) {
1511
1867
  const map = await parseDotenvFile(configPath(input.home));
1512
1868
  const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
1513
- return { exitCode: ExitCode.OK, result: ok({ entries, humanHint: entries.map((e) => `${e.key}=${e.value}`).join("\n") }) };
1869
+ let profiles;
1870
+ if (input.profiles) {
1871
+ const defaultProfile = map["WIKI_DEFAULT"];
1872
+ profiles = [];
1873
+ for (const key of Object.keys(map)) {
1874
+ const m = key.match(/^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/);
1875
+ if (m && key !== "WIKI_PATH") {
1876
+ const name = m[1].toLowerCase().replace(/_/g, "-");
1877
+ profiles.push({ name, path: map[key] ?? "", isDefault: name === defaultProfile });
1878
+ }
1879
+ }
1880
+ profiles.sort((a, b) => a.name.localeCompare(b.name));
1881
+ }
1882
+ const hint = profiles ? profiles.map((p) => `${p.isDefault ? "* " : " "}${p.name} \u2192 ${p.path}`).join("\n") || "(no profiles)" : entries.map((e) => `${e.key}=${e.value}`).join("\n");
1883
+ return { exitCode: ExitCode.OK, result: ok({ entries, profiles, humanHint: hint }) };
1514
1884
  }
1515
1885
  async function runConfigPath(input) {
1516
1886
  const filePath = configPath(input.home);
@@ -1518,9 +1888,72 @@ async function runConfigPath(input) {
1518
1888
  }
1519
1889
 
1520
1890
  // src/commands/doctor.ts
1521
- import { existsSync as existsSync2, readdirSync, statSync } from "fs";
1522
- import { join as join14 } from "path";
1891
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
1892
+ import { join as join16 } from "path";
1523
1893
  import { execSync } from "child_process";
1894
+
1895
+ // src/utils/auto-update.ts
1896
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync } from "fs";
1897
+ import { join as join15, dirname as dirname6 } from "path";
1898
+ import { spawn } from "child_process";
1899
+
1900
+ // src/utils/update-consts.ts
1901
+ var CACHE_FILENAME = ".update-cache.json";
1902
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1903
+ var ENV_DISABLE_KEY = "NO_UPDATE_NOTIFIER";
1904
+ var CLI_DISABLE_FLAG = "--no-update-notifier";
1905
+
1906
+ // src/utils/auto-update.ts
1907
+ function cachePath(home) {
1908
+ return join15(home, ".skillwiki", CACHE_FILENAME);
1909
+ }
1910
+ function readCacheRaw(home) {
1911
+ try {
1912
+ const raw = readFileSync2(cachePath(home), "utf8");
1913
+ return JSON.parse(raw);
1914
+ } catch {
1915
+ return null;
1916
+ }
1917
+ }
1918
+ function readCache(home) {
1919
+ const cache = readCacheRaw(home);
1920
+ if (!cache) return { cache: null, hasUpdate: false, isStale: true };
1921
+ const isStale = Date.now() - cache.lastCheck >= CHECK_INTERVAL_MS;
1922
+ const hasUpdate = !!cache.latestVersion && semverGt(cache.latestVersion, cache.currentVersion);
1923
+ return { cache, hasUpdate, isStale };
1924
+ }
1925
+ function writeCache(home, cache) {
1926
+ const p = cachePath(home);
1927
+ mkdirSync(dirname6(p), { recursive: true });
1928
+ writeFileSync2(p, JSON.stringify(cache, null, 2));
1929
+ }
1930
+ function latestFromCache(home, currentVersion) {
1931
+ const { cache } = readCache(home);
1932
+ if (!cache || !cache.latestVersion) return { hasUpdate: false, latest: null };
1933
+ return {
1934
+ hasUpdate: semverGt(cache.latestVersion, currentVersion),
1935
+ latest: cache.latestVersion
1936
+ };
1937
+ }
1938
+ function isDisabled() {
1939
+ return !!(process.env[ENV_DISABLE_KEY] || process.env.NODE_ENV === "test" || process.argv.includes(CLI_DISABLE_FLAG));
1940
+ }
1941
+ function triggerAutoUpdate(home, currentVersion) {
1942
+ if (isDisabled()) return;
1943
+ const { isStale } = readCache(home);
1944
+ if (!isStale) return;
1945
+ const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
1946
+ if (!existsSync2(bgScript)) return;
1947
+ const child = spawn(process.execPath, [bgScript, home, currentVersion], {
1948
+ detached: true,
1949
+ stdio: "ignore"
1950
+ });
1951
+ child.on("error", () => {
1952
+ });
1953
+ child.unref();
1954
+ }
1955
+
1956
+ // src/commands/doctor.ts
1524
1957
  function check(status, id, label, detail) {
1525
1958
  return { id, label, status, detail };
1526
1959
  }
@@ -1547,7 +1980,7 @@ function checkCliOnPath(argv) {
1547
1980
  }
1548
1981
  async function checkConfigFile(home) {
1549
1982
  const cfgPath = configPath(home);
1550
- if (!existsSync2(cfgPath)) {
1983
+ if (!existsSync3(cfgPath)) {
1551
1984
  return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
1552
1985
  }
1553
1986
  try {
@@ -1562,7 +1995,7 @@ function checkWikiPathExists(resolvedPath) {
1562
1995
  if (resolvedPath === void 0) {
1563
1996
  return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
1564
1997
  }
1565
- if (existsSync2(resolvedPath) && statSync(resolvedPath).isDirectory()) {
1998
+ if (existsSync3(resolvedPath) && statSync(resolvedPath).isDirectory()) {
1566
1999
  return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
1567
2000
  }
1568
2001
  return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
@@ -1571,22 +2004,22 @@ function checkVaultStructure(resolvedPath) {
1571
2004
  if (resolvedPath === void 0) {
1572
2005
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
1573
2006
  }
1574
- if (!existsSync2(resolvedPath)) {
2007
+ if (!existsSync3(resolvedPath)) {
1575
2008
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
1576
2009
  }
1577
2010
  const missing = [];
1578
- if (!existsSync2(join14(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
2011
+ if (!existsSync3(join16(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
1579
2012
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
1580
- if (!existsSync2(join14(resolvedPath, dir))) missing.push(dir + "/");
2013
+ if (!existsSync3(join16(resolvedPath, dir))) missing.push(dir + "/");
1581
2014
  }
1582
2015
  if (missing.length === 0) {
1583
2016
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
1584
2017
  }
1585
- return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
2018
+ return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
1586
2019
  }
1587
2020
  function checkSkillsInstalled(home) {
1588
- const skillsDir = join14(home, ".claude", "skills");
1589
- if (!existsSync2(skillsDir)) {
2021
+ const skillsDir = join16(home, ".claude", "skills");
2022
+ if (!existsSync3(skillsDir)) {
1590
2023
  return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
1591
2024
  }
1592
2025
  const found = findSkillMd(skillsDir);
@@ -1595,6 +2028,70 @@ function checkSkillsInstalled(home) {
1595
2028
  }
1596
2029
  return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
1597
2030
  }
2031
+ function checkNpmUpdate(home, currentVersion) {
2032
+ const { hasUpdate, latest } = latestFromCache(home, currentVersion);
2033
+ if (!latest) {
2034
+ return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (no cache yet)`);
2035
+ }
2036
+ if (hasUpdate) {
2037
+ return check("warn", "npm_update", "npm CLI version", `v${currentVersion} \u2014 update available: v${latest}. Run \`skillwiki update\`.`);
2038
+ }
2039
+ return check("pass", "npm_update", "npm CLI version", `v${currentVersion} (latest: v${latest})`);
2040
+ }
2041
+ function checkPluginVersionDrift(home, currentVersion) {
2042
+ const pluginJsonPath = join16(home, ".claude", "plugins", "cache", "llm-wiki", "plugin.json");
2043
+ if (!existsSync3(pluginJsonPath)) {
2044
+ return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin cache not found \u2014 plugin not installed");
2045
+ }
2046
+ try {
2047
+ const content = readFileSync3(pluginJsonPath, { encoding: "utf8" });
2048
+ const pluginData = JSON.parse(content);
2049
+ const pluginVersion = pluginData.version;
2050
+ if (!pluginVersion) {
2051
+ return check("pass", "plugin_version_drift", "Plugin/CLI version", "Plugin version not found in cache");
2052
+ }
2053
+ if (pluginVersion === currentVersion) {
2054
+ return check("pass", "plugin_version_drift", "Plugin/CLI version", `Both at v${currentVersion}`);
2055
+ }
2056
+ const updateCmd = semverGt(pluginVersion, currentVersion) ? "npm install -g skillwiki@beta" : "claude plugin update skillwiki@llm-wiki";
2057
+ return check(
2058
+ "warn",
2059
+ "plugin_version_drift",
2060
+ "Plugin/CLI version",
2061
+ `Plugin v${pluginVersion} \u2260 CLI v${currentVersion} \u2014 run \`${updateCmd}\``
2062
+ );
2063
+ } catch {
2064
+ return check("pass", "plugin_version_drift", "Plugin/CLI version", "Could not read plugin cache");
2065
+ }
2066
+ }
2067
+ async function checkProfiles(home) {
2068
+ const map = await parseDotenvFile(configPath(home));
2069
+ const profiles = [];
2070
+ for (const key of Object.keys(map)) {
2071
+ if (key.startsWith("WIKI_") && key.endsWith("_PATH") && key !== "WIKI_PATH") {
2072
+ const name = key.slice(5, -5).toLowerCase().replace(/_/g, "-");
2073
+ profiles.push(name);
2074
+ }
2075
+ }
2076
+ if (profiles.length === 0) {
2077
+ return check("pass", "wiki_profiles", "Wiki profiles", "No named profiles configured");
2078
+ }
2079
+ const defaultProfile = map["WIKI_DEFAULT"] ?? "(none)";
2080
+ return check(
2081
+ "pass",
2082
+ "wiki_profiles",
2083
+ "Wiki profiles",
2084
+ `${profiles.length} profile(s): ${profiles.join(", ")}; default: ${defaultProfile}`
2085
+ );
2086
+ }
2087
+ async function checkProjectLocalOverride(cwd) {
2088
+ const dir = cwd ?? process.cwd();
2089
+ const envPath = join16(dir, ".skillwiki", ".env");
2090
+ if (existsSync3(envPath)) {
2091
+ return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
2092
+ }
2093
+ return check("pass", "project_local", "Project-local config", "None");
2094
+ }
1598
2095
  function findSkillMd(dir) {
1599
2096
  const results = [];
1600
2097
  let entries;
@@ -1605,9 +2102,9 @@ function findSkillMd(dir) {
1605
2102
  }
1606
2103
  for (const entry of entries) {
1607
2104
  if (entry.isFile() && entry.name === "SKILL.md") {
1608
- results.push(join14(dir, entry.name));
2105
+ results.push(join16(dir, entry.name));
1609
2106
  } else if (entry.isDirectory()) {
1610
- results.push(...findSkillMd(join14(dir, entry.name)));
2107
+ results.push(...findSkillMd(join16(dir, entry.name)));
1611
2108
  }
1612
2109
  }
1613
2110
  return results;
@@ -1617,6 +2114,8 @@ async function runDoctor(input) {
1617
2114
  checks.push(checkNodeVersion());
1618
2115
  checks.push(checkCliOnPath(input.argv));
1619
2116
  checks.push(await checkConfigFile(input.home));
2117
+ checks.push(await checkProfiles(input.home));
2118
+ checks.push(await checkProjectLocalOverride(input.cwd));
1620
2119
  const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
1621
2120
  if (resolved.ok) {
1622
2121
  checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
@@ -1627,6 +2126,8 @@ async function runDoctor(input) {
1627
2126
  checks.push(checkWikiPathExists(resolvedPath));
1628
2127
  checks.push(checkVaultStructure(resolvedPath));
1629
2128
  checks.push(checkSkillsInstalled(input.home));
2129
+ checks.push(checkNpmUpdate(input.home, input.currentVersion));
2130
+ checks.push(checkPluginVersionDrift(input.home, input.currentVersion));
1630
2131
  const summary = {
1631
2132
  pass: checks.filter((c) => c.status === "pass").length,
1632
2133
  warn: checks.filter((c) => c.status === "warn").length,
@@ -1647,7 +2148,7 @@ async function runDoctor(input) {
1647
2148
 
1648
2149
  // src/commands/archive.ts
1649
2150
  import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
1650
- import { join as join15, dirname as dirname6 } from "path";
2151
+ import { join as join17, dirname as dirname7 } from "path";
1651
2152
  async function runArchive(input) {
1652
2153
  const scan = await scanVault(input.vault);
1653
2154
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -1659,10 +2160,10 @@ async function runArchive(input) {
1659
2160
  }
1660
2161
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
1661
2162
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
1662
- const archivePath = join15("_archive", relPath);
1663
- await mkdir5(dirname6(join15(input.vault, archivePath)), { recursive: true });
2163
+ const archivePath = join17("_archive", relPath);
2164
+ await mkdir5(dirname7(join17(input.vault, archivePath)), { recursive: true });
1664
2165
  let indexUpdated = false;
1665
- const indexPath = join15(input.vault, "index.md");
2166
+ const indexPath = join17(input.vault, "index.md");
1666
2167
  try {
1667
2168
  const idx = await readFile13(indexPath, "utf8");
1668
2169
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
@@ -1675,12 +2176,13 @@ async function runArchive(input) {
1675
2176
  } catch (e) {
1676
2177
  if (e?.code !== "ENOENT") throw e;
1677
2178
  }
1678
- await rename3(join15(input.vault, relPath), join15(input.vault, archivePath));
2179
+ await rename3(join17(input.vault, relPath), join17(input.vault, archivePath));
1679
2180
  return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
1680
2181
  }
1681
2182
 
1682
2183
  // src/commands/drift.ts
1683
2184
  import { createHash as createHash2 } from "crypto";
2185
+ import { writeFile as writeFile7 } from "fs/promises";
1684
2186
 
1685
2187
  // src/utils/fetch.ts
1686
2188
  async function controlledFetch(url, opts) {
@@ -1722,11 +2224,15 @@ async function runDrift(input) {
1722
2224
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1723
2225
  const results = [];
1724
2226
  for (const raw of scan.data.raw) {
1725
- const fm = extractFrontmatter(await readPage(raw));
1726
- if (!fm.ok) continue;
1727
- const sourceUrl = typeof fm.data.source_url === "string" ? fm.data.source_url : null;
1728
- const storedHash = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
1729
- if (!sourceUrl || !storedHash) continue;
2227
+ const text = await readPage(raw);
2228
+ const split = splitFrontmatter(text);
2229
+ if (!split.ok) continue;
2230
+ const { rawFrontmatter, body } = split.data;
2231
+ const sourceUrlMatch = rawFrontmatter.match(/^source_url:\s*(.+)$/m);
2232
+ const storedHashMatch = rawFrontmatter.match(/^sha256:\s*([a-f0-9]+)$/m);
2233
+ if (!sourceUrlMatch || !storedHashMatch) continue;
2234
+ const sourceUrl = sourceUrlMatch[1].trim();
2235
+ const storedHash = storedHashMatch[1];
1730
2236
  const resp = await doFetch(sourceUrl, FETCH_OPTS);
1731
2237
  if (!resp.ok) {
1732
2238
  results.push({
@@ -1741,29 +2247,318 @@ async function runDrift(input) {
1741
2247
  }
1742
2248
  const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
1743
2249
  const drifted2 = currentHash !== storedHash;
1744
- results.push({
1745
- raw_path: raw.relPath,
1746
- source_url: sourceUrl,
1747
- stored_sha256: storedHash,
1748
- current_sha256: currentHash,
1749
- status: drifted2 ? "drifted" : "unchanged"
1750
- });
2250
+ if (drifted2 && input.apply) {
2251
+ const newFm = rawFrontmatter.replace(/^sha256:\s*[a-f0-9]+$/m, `sha256: ${currentHash}`);
2252
+ const newText = `---
2253
+ ${newFm}
2254
+ ---
2255
+ ${body}`;
2256
+ await writeFile7(raw.absPath, newText, "utf8");
2257
+ results.push({
2258
+ raw_path: raw.relPath,
2259
+ source_url: sourceUrl,
2260
+ stored_sha256: storedHash,
2261
+ current_sha256: currentHash,
2262
+ status: "updated"
2263
+ });
2264
+ } else {
2265
+ results.push({
2266
+ raw_path: raw.relPath,
2267
+ source_url: sourceUrl,
2268
+ stored_sha256: storedHash,
2269
+ current_sha256: currentHash,
2270
+ status: drifted2 ? "drifted" : "unchanged"
2271
+ });
2272
+ }
1751
2273
  }
1752
2274
  const drifted = results.filter((r) => r.status === "drifted");
1753
2275
  const fetchFailed = results.filter((r) => r.status === "fetch_failed");
2276
+ const updated = results.filter((r) => r.status === "updated");
1754
2277
  const unchanged = results.filter((r) => r.status === "unchanged").length;
1755
2278
  const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
1756
2279
  const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
1757
2280
  if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
1758
2281
  if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
2282
+ if (updated.length > 0) hintLines.push(`updated: ${updated.length}`, ...updated.map((u) => ` ${u.raw_path}`));
1759
2283
  return {
1760
2284
  exitCode,
1761
- result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
2285
+ result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, updated, unchanged, humanHint: hintLines.join("\n") })
2286
+ };
2287
+ }
2288
+
2289
+ // src/commands/migrate-citations.ts
2290
+ import { writeFile as writeFile8 } from "fs/promises";
2291
+ var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
2292
+ function moveMarkersToParagraphEnd(body) {
2293
+ const lines = body.split("\n");
2294
+ const result = [];
2295
+ for (let i = 0; i < lines.length; i++) {
2296
+ const line = lines[i];
2297
+ if (line.trimStart().startsWith("```")) {
2298
+ result.push(line);
2299
+ if (!line.trimEnd().endsWith("```") || line.trim() === "```") {
2300
+ for (let j = i + 1; j < lines.length; j++) {
2301
+ result.push(lines[j]);
2302
+ if (lines[j].trimStart().startsWith("```")) {
2303
+ i = j;
2304
+ break;
2305
+ }
2306
+ }
2307
+ }
2308
+ continue;
2309
+ }
2310
+ if (/^## Sources\b/.test(line.trim())) {
2311
+ result.push(line);
2312
+ continue;
2313
+ }
2314
+ const markers = [...line.matchAll(MARKER_RE2)];
2315
+ if (markers.length === 0) {
2316
+ result.push(line);
2317
+ continue;
2318
+ }
2319
+ const proseOnly = line.replace(MARKER_RE2, "").trim();
2320
+ if (proseOnly.length === 0) {
2321
+ const markerStr = " " + markers.map((m) => m[0]).join(" ");
2322
+ let merged = false;
2323
+ for (let k = result.length - 1; k >= 0; k--) {
2324
+ if (result[k].trim().length > 0) {
2325
+ result[k] = result[k].trimEnd() + markerStr;
2326
+ merged = true;
2327
+ break;
2328
+ }
2329
+ }
2330
+ if (!merged) result.push(line);
2331
+ continue;
2332
+ }
2333
+ const lastMarkerIdx = line.lastIndexOf("^[raw/");
2334
+ const afterLast = line.slice(lastMarkerIdx).replace(MARKER_RE2, "").trim();
2335
+ const firstMarkerIdx = line.indexOf("^[raw/");
2336
+ const beforeFirst = line.slice(0, firstMarkerIdx).trim();
2337
+ const alreadyAtEnd = afterLast.length === 0 && (beforeFirst.length === 0 || /[.!?]\s*$/.test(beforeFirst));
2338
+ if (alreadyAtEnd) {
2339
+ result.push(line);
2340
+ continue;
2341
+ }
2342
+ let cleaned = line.replace(/\s*\^\[raw\/[^\]]+\]\s*/g, " ").trimEnd();
2343
+ cleaned = cleaned.replace(/ +/g, " ").trimEnd();
2344
+ const markerStrings = markers.map((m) => m[0]);
2345
+ if (cleaned.length > 0 && /[.!?]$/.test(cleaned)) {
2346
+ cleaned += " " + markerStrings.join(" ");
2347
+ } else if (cleaned.length > 0) {
2348
+ cleaned += ". " + markerStrings.join(" ");
2349
+ } else {
2350
+ cleaned = markerStrings.join(" ");
2351
+ }
2352
+ result.push(cleaned);
2353
+ }
2354
+ return result.join("\n");
2355
+ }
2356
+ function buildSourcesFooter(targets) {
2357
+ return "\n## Sources\n" + targets.map((t) => `- ^[${t}]`).join("\n") + "\n";
2358
+ }
2359
+ function reorderSourcesFm(rawFm, targets) {
2360
+ const sourcesLineRe = /^sources:\s*\[([^\]]*)\]\s*$/m;
2361
+ const match = rawFm.match(sourcesLineRe);
2362
+ if (!match) return rawFm;
2363
+ const existing = match[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter((s) => s.length > 0);
2364
+ const targetSet = new Set(targets);
2365
+ const reordered = [
2366
+ ...targets,
2367
+ ...existing.filter((s) => !targetSet.has(s))
2368
+ ];
2369
+ const newLine = `sources: [${reordered.join(", ")}]`;
2370
+ return rawFm.replace(sourcesLineRe, newLine);
2371
+ }
2372
+ function removeExistingFooter(body) {
2373
+ const footerRe = /\n## Sources\n[\s\S]*$/;
2374
+ return body.replace(footerRe, "");
2375
+ }
2376
+ async function runMigrateCitations(input) {
2377
+ const scan = await scanVault(input.vault);
2378
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2379
+ const migrated = [];
2380
+ const skipped = [];
2381
+ let unchanged = 0;
2382
+ for (const page of scan.data.typedKnowledge) {
2383
+ const text = await readPage(page);
2384
+ const split = splitFrontmatter(text);
2385
+ if (!split.ok) continue;
2386
+ const { rawFrontmatter, body } = split.data;
2387
+ const markers = extractCitationMarkers(body);
2388
+ if (markers.length === 0) {
2389
+ unchanged++;
2390
+ continue;
2391
+ }
2392
+ const seen = /* @__PURE__ */ new Set();
2393
+ const uniqueTargets = [];
2394
+ for (const m of markers) {
2395
+ if (!seen.has(m.target)) {
2396
+ seen.add(m.target);
2397
+ uniqueTargets.push(m.target);
2398
+ }
2399
+ }
2400
+ const bodyWithoutFooter = removeExistingFooter(body);
2401
+ const migratedBody = moveMarkersToParagraphEnd(bodyWithoutFooter);
2402
+ const newFooter = buildSourcesFooter(uniqueTargets);
2403
+ const newFm = reorderSourcesFm(rawFrontmatter, uniqueTargets);
2404
+ const newText = `---
2405
+ ${newFm}
2406
+ ---
2407
+ ${migratedBody}${newFooter}`;
2408
+ if (newText === text) {
2409
+ skipped.push(page.relPath);
2410
+ continue;
2411
+ }
2412
+ if (!input.dryRun) {
2413
+ await writeFile8(page.absPath, newText, "utf8");
2414
+ }
2415
+ migrated.push(page.relPath);
2416
+ }
2417
+ const exitCode = migrated.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
2418
+ const hintLines = [`scanned: ${migrated.length + skipped.length + unchanged}`];
2419
+ if (migrated.length > 0) hintLines.push(`migrated: ${migrated.length}`);
2420
+ if (skipped.length > 0) hintLines.push(`skipped (already clean): ${skipped.length}`);
2421
+ if (unchanged > 0) hintLines.push(`unchanged (no markers): ${unchanged}`);
2422
+ return {
2423
+ exitCode,
2424
+ result: ok({
2425
+ scanned: migrated.length + skipped.length + unchanged,
2426
+ migrated,
2427
+ skipped,
2428
+ unchanged,
2429
+ humanHint: hintLines.join("\n")
2430
+ })
2431
+ };
2432
+ }
2433
+
2434
+ // src/commands/frontmatter-fix.ts
2435
+ import { writeFile as writeFile9 } from "fs/promises";
2436
+ function isoToday() {
2437
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2438
+ }
2439
+ function fixFrontmatter(rawFm) {
2440
+ const additions = [];
2441
+ if (!/^created:/m.test(rawFm)) additions.push(`created: ${isoToday()}`);
2442
+ if (!/^updated:/m.test(rawFm)) additions.push(`updated: ${isoToday()}`);
2443
+ if (!/^tags:/m.test(rawFm)) additions.push("tags: []");
2444
+ if (!/^sources:/m.test(rawFm)) additions.push("sources: []");
2445
+ if (!/^provenance:/m.test(rawFm)) additions.push("provenance: research");
2446
+ if (additions.length === 0) return rawFm;
2447
+ return rawFm.trimEnd() + "\n" + additions.join("\n") + "\n";
2448
+ }
2449
+ function removeOrphanTagsLines(body) {
2450
+ return body.split("\n").filter((line) => !/^tags:\s*\[/.test(line.trim())).join("\n");
2451
+ }
2452
+ async function runFrontmatterFix(input) {
2453
+ const scan = await scanVault(input.vault);
2454
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2455
+ const fixed = [];
2456
+ const skipped = [];
2457
+ let unchanged = 0;
2458
+ for (const page of scan.data.typedKnowledge) {
2459
+ const text = await readPage(page);
2460
+ const split = splitFrontmatter(text);
2461
+ if (!split.ok) {
2462
+ skipped.push(page.relPath);
2463
+ continue;
2464
+ }
2465
+ const { rawFrontmatter, body } = split.data;
2466
+ const newFm = fixFrontmatter(rawFrontmatter);
2467
+ const newBody = removeOrphanTagsLines(body);
2468
+ const newText = `---
2469
+ ${newFm}
2470
+ ---
2471
+ ${newBody}`;
2472
+ if (newText === text) {
2473
+ unchanged++;
2474
+ continue;
2475
+ }
2476
+ if (!input.dryRun) {
2477
+ await writeFile9(page.absPath, newText, "utf8");
2478
+ }
2479
+ fixed.push(page.relPath);
2480
+ }
2481
+ const exitCode = fixed.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
2482
+ const hintLines = [`scanned: ${fixed.length + skipped.length + unchanged}`];
2483
+ if (fixed.length > 0) hintLines.push(`fixed: ${fixed.length}`);
2484
+ if (skipped.length > 0) hintLines.push(`skipped (parse error): ${skipped.length}`);
2485
+ if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
2486
+ if (input.dryRun && fixed.length > 0) hintLines.push("(dry run \u2014 no files written)");
2487
+ return {
2488
+ exitCode,
2489
+ result: ok({
2490
+ scanned: fixed.length + skipped.length + unchanged,
2491
+ fixed,
2492
+ skipped,
2493
+ unchanged,
2494
+ humanHint: hintLines.join("\n")
2495
+ })
2496
+ };
2497
+ }
2498
+
2499
+ // src/commands/update.ts
2500
+ import { execSync as execSync2 } from "child_process";
2501
+ import { readFileSync as readFileSync4 } from "fs";
2502
+ async function runUpdate(input) {
2503
+ const pkg2 = JSON.parse(
2504
+ readFileSync4(new URL("../../package.json", import.meta.url), "utf8")
2505
+ );
2506
+ const currentVersion = pkg2.version;
2507
+ const tag = input.distTag ?? "beta";
2508
+ let latest;
2509
+ try {
2510
+ latest = execSync2(`npm view skillwiki@${tag} version`, {
2511
+ encoding: "utf8",
2512
+ timeout: 15e3
2513
+ }).trim();
2514
+ } catch (e) {
2515
+ return {
2516
+ exitCode: ExitCode.PREFLIGHT_FAILED,
2517
+ result: err("PREFLIGHT_FAILED", { message: `Failed to query npm registry: ${String(e)}` })
2518
+ };
2519
+ }
2520
+ const cache = {
2521
+ lastCheck: Date.now(),
2522
+ latestVersion: latest,
2523
+ currentVersion
2524
+ };
2525
+ if (latest === currentVersion) {
2526
+ writeCache(input.home, cache);
2527
+ return {
2528
+ exitCode: ExitCode.OK,
2529
+ result: ok({
2530
+ previousVersion: currentVersion,
2531
+ newVersion: null,
2532
+ wasAlreadyLatest: true,
2533
+ humanHint: `Already on latest ${tag}: v${currentVersion}`
2534
+ })
2535
+ };
2536
+ }
2537
+ try {
2538
+ execSync2(`npm install -g skillwiki@${tag}`, {
2539
+ stdio: "pipe",
2540
+ timeout: 6e4
2541
+ });
2542
+ } catch (e) {
2543
+ return {
2544
+ exitCode: ExitCode.PREFLIGHT_FAILED,
2545
+ result: err("PREFLIGHT_FAILED", { message: `npm install failed: ${String(e)}` })
2546
+ };
2547
+ }
2548
+ writeCache(input.home, { ...cache, updateAppliedAt: Date.now() });
2549
+ return {
2550
+ exitCode: ExitCode.OK,
2551
+ result: ok({
2552
+ previousVersion: currentVersion,
2553
+ newVersion: latest,
2554
+ wasAlreadyLatest: false,
2555
+ humanHint: `Updated skillwiki ${currentVersion} \u2192 ${latest}`
2556
+ })
1762
2557
  };
1763
2558
  }
1764
2559
 
1765
2560
  // src/cli.ts
1766
- var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
2561
+ var pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
1767
2562
  var program = new Command();
1768
2563
  program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
1769
2564
  program.option("--human", "render terminal-readable output instead of JSON");
@@ -1775,19 +2570,20 @@ function emit(r) {
1775
2570
  program.command("hash <file>").action(async (file) => emit(await runHash({ file })));
1776
2571
  program.command("fetch-guard <url>").action(async (url) => emit(await runFetchGuard({ url })));
1777
2572
  program.command("validate <file>").action(async (file) => emit(await runValidate({ file })));
1778
- program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path", ".skillwiki/graph.json").action(async (vault, opts) => emit(await runGraphBuild({ vault, out: opts.out })));
2573
+ program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path", ".skillwiki/graph.json").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => emit(await runGraphBuild({ vault, out: opts.out })));
1779
2574
  program.command("overlap <vault>").action(async (vault) => emit(await runOverlap({ vault })));
1780
- program.command("orphans [vault]").action(async (vault) => emit(await runOrphans({
2575
+ program.command("orphans [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => emit(await runOrphans({
1781
2576
  vault,
1782
2577
  envValue: process.env.WIKI_PATH,
1783
- home: process.env.HOME ?? ""
2578
+ home: process.env.HOME ?? "",
2579
+ wiki: opts.wiki
1784
2580
  })));
1785
2581
  program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
1786
2582
  program.command("install").option("--target <dir>", "target install directory", `${process.env.HOME ?? ""}/.claude/skills/`).option("--dry-run", "preview only", false).option("--skills-root <dir>", "source skills directory (defaults to packaged)").action(async (opts) => {
1787
2583
  const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
1788
2584
  emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
1789
2585
  });
1790
- program.command("path").option("--vault <dir>", "explicit vault override (runtime)").option("--target <dir>", "explicit target override (init-time)").option("--init-time", "use init-time chain instead of runtime", false).option("--explain", "include resolution chain in output", false).action(async (opts) => {
2586
+ program.command("path").option("--vault <dir>", "explicit vault override (runtime)").option("--target <dir>", "explicit target override (init-time)").option("--wiki <name>", "wiki profile name").option("--init-time", "use init-time chain instead of runtime", false).option("--explain", "include resolution chain in output", false).action(async (opts) => {
1791
2587
  const initTime = !!opts.initTime;
1792
2588
  const flag = initTime ? opts.target : opts.vault;
1793
2589
  emit(await runPath({
@@ -1795,6 +2591,7 @@ program.command("path").option("--vault <dir>", "explicit vault override (runtim
1795
2591
  envValue: process.env.WIKI_PATH,
1796
2592
  home: process.env.HOME ?? "",
1797
2593
  initTime,
2594
+ wiki: opts.wiki,
1798
2595
  explain: !!opts.explain
1799
2596
  }));
1800
2597
  });
@@ -1806,7 +2603,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
1806
2603
  explain: !!opts.explain
1807
2604
  }));
1808
2605
  });
1809
- program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").action(async (opts) => {
2606
+ program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").option("--profile <name>", "write as named wiki profile instead of WIKI_PATH").action(async (opts) => {
1810
2607
  const templates = new URL("../templates/", import.meta.url).pathname;
1811
2608
  const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
1812
2609
  emit(await runInit({
@@ -1818,51 +2615,57 @@ program.command("init").option("--target <dir>", "explicit target directory").re
1818
2615
  taxonomy,
1819
2616
  lang: opts.lang,
1820
2617
  force: !!opts.force,
1821
- noEnv: opts.env === false
2618
+ noEnv: opts.env === false,
2619
+ profile: opts.profile
1822
2620
  }));
1823
2621
  });
1824
- async function resolveVaultArg(arg) {
2622
+ async function resolveVaultArg(arg, wiki) {
1825
2623
  if (arg) return { ok: true, vault: arg };
1826
2624
  const r = await resolveRuntimePath({
1827
2625
  flag: void 0,
1828
2626
  envValue: process.env.WIKI_PATH,
1829
- home: process.env.HOME ?? ""
2627
+ wikiEnv: process.env.WIKI,
2628
+ home: process.env.HOME ?? "",
2629
+ wiki
1830
2630
  });
1831
- if (!r.ok) return { ok: false, exitCode: 25, payload: r };
2631
+ if (!r.ok) {
2632
+ const exitCode = r.error === "UNKNOWN_WIKI_PROFILE" ? 35 : 25;
2633
+ return { ok: false, exitCode, payload: r };
2634
+ }
1832
2635
  return { ok: true, vault: r.data.path };
1833
2636
  }
1834
- program.command("links [vault]").action(async (vault) => {
1835
- const v = await resolveVaultArg(vault);
2637
+ program.command("links [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2638
+ const v = await resolveVaultArg(vault, opts.wiki);
1836
2639
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1837
2640
  else emit(await runLinks({ vault: v.vault }));
1838
2641
  });
1839
- program.command("tag-audit [vault]").action(async (vault) => {
1840
- const v = await resolveVaultArg(vault);
2642
+ program.command("tag-audit [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2643
+ const v = await resolveVaultArg(vault, opts.wiki);
1841
2644
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1842
2645
  else emit(await runTagAudit({ vault: v.vault }));
1843
2646
  });
1844
- program.command("index-check [vault]").action(async (vault) => {
1845
- const v = await resolveVaultArg(vault);
2647
+ program.command("index-check [vault]").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2648
+ const v = await resolveVaultArg(vault, opts.wiki);
1846
2649
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1847
2650
  else emit(await runIndexCheck({ vault: v.vault }));
1848
2651
  });
1849
- program.command("stale [vault]").option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 90).action(async (vault, opts) => {
1850
- const v = await resolveVaultArg(vault);
2652
+ program.command("stale [vault]").option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 90).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2653
+ const v = await resolveVaultArg(vault, opts.wiki);
1851
2654
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1852
2655
  else emit(await runStale({ vault: v.vault, days: opts.days }));
1853
2656
  });
1854
- program.command("pagesize [vault]").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).action(async (vault, opts) => {
1855
- const v = await resolveVaultArg(vault);
2657
+ program.command("pagesize [vault]").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2658
+ const v = await resolveVaultArg(vault, opts.wiki);
1856
2659
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1857
2660
  else emit(await runPagesize({ vault: v.vault, lines: opts.lines }));
1858
2661
  });
1859
- program.command("log-rotate [vault]").option("--threshold <n>", "entry count threshold", (s) => parseInt(s, 10), 500).option("--apply", "actually rotate", false).action(async (vault, opts) => {
1860
- const v = await resolveVaultArg(vault);
2662
+ program.command("log-rotate [vault]").option("--threshold <n>", "entry count threshold", (s) => parseInt(s, 10), 500).option("--apply", "actually rotate", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2663
+ const v = await resolveVaultArg(vault, opts.wiki);
1861
2664
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1862
2665
  else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }));
1863
2666
  });
1864
- program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).action(async (vault, opts) => {
1865
- const v = await resolveVaultArg(vault);
2667
+ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2668
+ const v = await resolveVaultArg(vault, opts.wiki);
1866
2669
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1867
2670
  else emit(await runLint({
1868
2671
  vault: v.vault,
@@ -1875,23 +2678,45 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
1875
2678
  var configCmd = program.command("config").description("manage skillwiki configuration");
1876
2679
  configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
1877
2680
  configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));
1878
- configCmd.command("list").description("list all config key=value pairs").action(async () => emit(await runConfigList({ home: process.env.HOME ?? "" })));
2681
+ configCmd.command("list").option("--profiles", "show wiki profiles summary", false).description("list all config key=value pairs").action(async (opts) => emit(await runConfigList({ home: process.env.HOME ?? "", profiles: !!opts.profiles })));
1879
2682
  configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
1880
2683
  program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
1881
2684
  home: process.env.HOME ?? "",
1882
2685
  envValue: process.env.WIKI_PATH,
1883
- argv: process.argv
2686
+ argv: process.argv,
2687
+ currentVersion: pkg.version,
2688
+ cwd: process.cwd()
1884
2689
  })));
1885
- program.command("archive <page> [vault]").description("archive a typed-knowledge page").action(async (page, vault) => {
1886
- const v = await resolveVaultArg(vault);
2690
+ program.command("archive <page> [vault]").description("archive a typed-knowledge page").option("--wiki <name>", "wiki profile name").action(async (page, vault, opts) => {
2691
+ const v = await resolveVaultArg(vault, opts.wiki);
1887
2692
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1888
2693
  else emit(await runArchive({ vault: v.vault, page }));
1889
2694
  });
1890
- program.command("drift [vault]").description("detect content drift in raw sources").action(async (vault) => {
1891
- const v = await resolveVaultArg(vault);
2695
+ program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2696
+ const v = await resolveVaultArg(vault, opts.wiki);
2697
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
2698
+ else emit(await runDrift({ vault: v.vault, apply: opts.apply }));
2699
+ });
2700
+ program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2701
+ const v = await resolveVaultArg(vault, opts.wiki);
2702
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
2703
+ else emit(await runDedup({ vault: v.vault, apply: opts.apply }));
2704
+ });
2705
+ program.command("migrate-citations [vault]").description("migrate ^[raw/...] markers to paragraph-end citations").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2706
+ const v = await resolveVaultArg(vault, opts.wiki);
1892
2707
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1893
- else emit(await runDrift({ vault: v.vault }));
2708
+ else emit(await runMigrateCitations({ vault: v.vault, dryRun: !!opts.dryRun }));
1894
2709
  });
2710
+ program.command("frontmatter-fix [vault]").description("fix common frontmatter issues on typed-knowledge pages").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
2711
+ const v = await resolveVaultArg(vault, opts.wiki);
2712
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
2713
+ else emit(await runFrontmatterFix({ vault: v.vault, dryRun: !!opts.dryRun }));
2714
+ });
2715
+ program.command("update").description("update skillwiki CLI to the latest version").option("--tag <tag>", "npm dist-tag", "beta").action(async (opts) => emit(await runUpdate({
2716
+ home: process.env.HOME ?? "",
2717
+ distTag: opts.tag
2718
+ })));
2719
+ triggerAutoUpdate(process.env.HOME ?? "", pkg.version);
1895
2720
  program.parseAsync(process.argv).catch((e) => {
1896
2721
  process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
1897
2722
  process.exit(1);