skillwiki 0.2.0-beta.1 → 0.2.0-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
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { readFileSync } from "fs";
4
5
  import { Command } from "commander";
5
6
 
6
7
  // src/utils/output.ts
@@ -9,9 +10,14 @@ function printJson(r) {
9
10
  }
10
11
  function printHuman(r) {
11
12
  if (r.ok) {
12
- process.stdout.write(`OK
13
+ if (typeof r.data === "object" && r.data !== null && "humanHint" in r.data) {
14
+ process.stdout.write(`${r.data.humanHint}
15
+ `);
16
+ } else {
17
+ process.stdout.write(`OK
13
18
  ${formatData(r.data)}
14
19
  `);
20
+ }
15
21
  } else {
16
22
  process.stdout.write(`ERR ${r.error}
17
23
  ${r.detail !== void 0 ? formatData(r.detail) + "\n" : ""}`);
@@ -53,7 +59,15 @@ var ExitCode = {
53
59
  LINT_HAS_WARNINGS: 22,
54
60
  LINT_HAS_ERRORS: 23,
55
61
  ENV_WRITE_CONFLICT: 24,
56
- NO_VAULT_CONFIGURED: 25
62
+ NO_VAULT_CONFIGURED: 25,
63
+ INVALID_CONFIG_KEY: 26,
64
+ CONFIG_WRITE_FAILED: 27,
65
+ DOCTOR_HAS_WARNINGS: 28,
66
+ DOCTOR_HAS_ERRORS: 29,
67
+ ARCHIVE_TARGET_NOT_FOUND: 30,
68
+ ARCHIVE_ALREADY_ARCHIVED: 31,
69
+ DRIFT_DETECTED: 32,
70
+ RAW_DEDUP_DETECTED: 33
57
71
  };
58
72
 
59
73
  // ../shared/src/json-output.ts
@@ -231,7 +245,7 @@ async function runHash(input) {
231
245
  const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
232
246
  return {
233
247
  exitCode: ExitCode.OK,
234
- result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength })
248
+ result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
235
249
  };
236
250
  }
237
251
 
@@ -261,7 +275,7 @@ function runFetchGuardSync(input) {
261
275
  result: err("HOST_BLOCKED", { sanitized_url: sanitized, host: u.hostname })
262
276
  };
263
277
  }
264
- return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized }) };
278
+ return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized, humanHint: `ALLOWED: ${sanitized}` }) };
265
279
  }
266
280
  function sanitizeUrl(u) {
267
281
  const clone = new URL(u.toString());
@@ -301,17 +315,18 @@ async function runValidate(input) {
301
315
  }
302
316
  const det = detectSchema(fm.data);
303
317
  if (!det.schema) {
304
- return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [] }) };
318
+ return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
305
319
  }
306
320
  const parsed = SCHEMAS[det.schema].safeParse(fm.data);
307
321
  if (!parsed.success) {
308
322
  const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
309
323
  return {
310
324
  exitCode: ExitCode.INVALID_FRONTMATTER,
311
- result: ok({ schema: det.schema, valid: false, errors })
325
+ result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
326
+ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
312
327
  };
313
328
  }
314
- return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [] }) };
329
+ return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [], humanHint: `VALID (${det.schema})` }) };
315
330
  }
316
331
 
317
332
  // src/commands/graph.ts
@@ -397,7 +412,8 @@ async function runGraphBuild(input) {
397
412
  }
398
413
  return {
399
414
  exitCode: ExitCode.OK,
400
- result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count })
415
+ result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count, humanHint: `nodes: ${scan.data.typedKnowledge.length}, edges: ${edge_count}
416
+ written: ${input.out}` })
401
417
  };
402
418
  }
403
419
  function computeAdamicAdar(adj) {
@@ -472,22 +488,19 @@ async function runOverlap(input) {
472
488
  }
473
489
  return { id, members, score };
474
490
  });
475
- return { exitCode: ExitCode.OK, result: ok({ clusters }) };
491
+ const humanHint = clusters.length === 0 ? "no overlap clusters found" : clusters.map((c) => `cluster (${c.members.length} pages, score ${c.score}): ${c.members.join(", ")}`).join("\n");
492
+ return { exitCode: ExitCode.OK, result: ok({ clusters, humanHint }) };
476
493
  }
477
494
 
478
495
  // src/utils/wiki-path.ts
479
496
  import { join as join2 } from "path";
480
497
 
481
498
  // src/utils/dotenv.ts
482
- import { readFile as readFile4 } from "fs/promises";
483
- var WHITELIST = /* @__PURE__ */ new Set(["WIKI_PATH", "WIKI_LANG"]);
484
- async function parseDotenvFile(path) {
485
- let text;
486
- try {
487
- text = await readFile4(path, "utf8");
488
- } catch {
489
- return {};
490
- }
499
+ import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
500
+ import { dirname as dirname2 } from "path";
501
+ var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
502
+ var _whitelist = new Set(CONFIG_KEYS);
503
+ function parseDotenvText(text) {
491
504
  const out = {};
492
505
  for (const rawLine of text.split(/\r?\n/)) {
493
506
  const line = rawLine.trim();
@@ -496,12 +509,65 @@ async function parseDotenvFile(path) {
496
509
  if (eq <= 0) continue;
497
510
  const key = line.slice(0, eq).trim();
498
511
  const value = line.slice(eq + 1).trim();
499
- if (!WHITELIST.has(key)) continue;
512
+ if (!_whitelist.has(key)) continue;
500
513
  if (value.length === 0) continue;
501
514
  out[key] = value;
502
515
  }
503
516
  return out;
504
517
  }
518
+ async function parseDotenvFile(path) {
519
+ let text;
520
+ try {
521
+ text = await readFile4(path, "utf8");
522
+ } catch {
523
+ return {};
524
+ }
525
+ return parseDotenvText(text);
526
+ }
527
+ async function writeDotenv(filePath, entries, originalContent) {
528
+ const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
529
+ await mkdir2(dirname2(filePath), { recursive: true });
530
+ await writeFile2(filePath, lines.join("\n") + "\n", "utf8");
531
+ }
532
+ function freshLines(entries) {
533
+ const out = [];
534
+ for (const [key, value] of Object.entries(entries)) {
535
+ if (value !== void 0) out.push(`${key}=${value}`);
536
+ }
537
+ return out;
538
+ }
539
+ function updateLines(originalContent, entries) {
540
+ let rawLines = originalContent.split(/\r?\n/);
541
+ if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
542
+ rawLines = rawLines.slice(0, -1);
543
+ }
544
+ const keysToWrite = new Set(Object.keys(entries));
545
+ const out = [];
546
+ for (const line of rawLines) {
547
+ const trimmed = line.trim();
548
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
549
+ out.push(line);
550
+ continue;
551
+ }
552
+ const eq = trimmed.indexOf("=");
553
+ if (eq <= 0) {
554
+ out.push(line);
555
+ continue;
556
+ }
557
+ const key = trimmed.slice(0, eq).trim();
558
+ if (keysToWrite.has(key)) {
559
+ out.push(`${key}=${entries[key]}`);
560
+ keysToWrite.delete(key);
561
+ } else {
562
+ out.push(line);
563
+ }
564
+ }
565
+ for (const key of keysToWrite) {
566
+ const value = entries[key];
567
+ if (value !== void 0) out.push(`${key}=${value}`);
568
+ }
569
+ return out;
570
+ }
505
571
 
506
572
  // src/utils/wiki-path.ts
507
573
  async function resolveInitTimePath(input) {
@@ -609,7 +675,11 @@ async function runOrphans(input) {
609
675
  }
610
676
  }
611
677
  }
612
- return { exitCode: ExitCode.OK, result: ok({ orphans, bridges }) };
678
+ const hintLines = [];
679
+ if (orphans.length > 0) hintLines.push(`orphans: ${orphans.length}`, ...orphans.map((o) => ` ${o}`));
680
+ if (bridges.length > 0) hintLines.push(`bridges: ${bridges.length}`, ...bridges.map((b) => ` ${b.path}`));
681
+ if (hintLines.length === 0) hintLines.push("no orphans or bridges");
682
+ return { exitCode: ExitCode.OK, result: ok({ orphans, bridges, humanHint: hintLines.join("\n") }) };
613
683
  }
614
684
  function simulateRemoval(adj, removed) {
615
685
  const seen = /* @__PURE__ */ new Set();
@@ -630,7 +700,7 @@ function simulateRemoval(adj, removed) {
630
700
 
631
701
  // src/commands/audit.ts
632
702
  import { readFile as readFile5, stat as stat2 } from "fs/promises";
633
- import { dirname as dirname2, resolve, join as join3 } from "path";
703
+ import { dirname as dirname3, resolve, join as join3 } from "path";
634
704
 
635
705
  // src/parsers/citations.ts
636
706
  var FENCE2 = /```[\s\S]*?```/g;
@@ -657,7 +727,7 @@ async function runAudit(input) {
657
727
  if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
658
728
  const split = splitFrontmatter(text);
659
729
  const body = split.ok ? split.data.body : text;
660
- const vault = await findVaultRoot(dirname2(resolve(input.file)));
730
+ const vault = await findVaultRoot(dirname3(resolve(input.file)));
661
731
  if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
662
732
  const markers = extractCitationMarkers(body);
663
733
  const resolved = await Promise.all(markers.map(async (m) => {
@@ -672,13 +742,20 @@ async function runAudit(input) {
672
742
  const referenced = new Set(resolved.map((m) => m.target));
673
743
  const unused_sources = sources.filter((s) => !referenced.has(s));
674
744
  const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
745
+ const broken = resolved.filter((m) => !m.resolved);
746
+ const hintLines = [];
747
+ hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
748
+ if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
749
+ if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
750
+ if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
751
+ const humanHint = hintLines.join("\n");
675
752
  if (resolved.some((m) => !m.resolved)) {
676
- return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
753
+ return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
677
754
  }
678
755
  if (unused_sources.length > 0 || missing_from_sources.length > 0) {
679
- return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
756
+ return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
680
757
  }
681
- return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources } }) };
758
+ return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
682
759
  }
683
760
  async function findVaultRoot(start) {
684
761
  let cur = start;
@@ -688,7 +765,7 @@ async function findVaultRoot(start) {
688
765
  return cur;
689
766
  } catch {
690
767
  }
691
- const parent = dirname2(cur);
768
+ const parent = dirname3(cur);
692
769
  if (parent === cur) return null;
693
770
  cur = parent;
694
771
  }
@@ -700,10 +777,10 @@ import { readdir as readdir2, stat as stat4 } from "fs/promises";
700
777
  import { join as join4 } from "path";
701
778
 
702
779
  // src/utils/install-fs.ts
703
- import { copyFile, mkdir as mkdir2, rename, writeFile as writeFile2, stat as stat3 } from "fs/promises";
704
- import { dirname as dirname3 } from "path";
780
+ import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile3, stat as stat3 } from "fs/promises";
781
+ import { dirname as dirname4 } from "path";
705
782
  async function atomicCopyWithBackup(src, dst) {
706
- await mkdir2(dirname3(dst), { recursive: true });
783
+ await mkdir3(dirname4(dst), { recursive: true });
707
784
  let backupPath = null;
708
785
  try {
709
786
  await stat3(dst);
@@ -721,9 +798,9 @@ async function atomicCopyWithBackup(src, dst) {
721
798
  return ok({ copied: true, backupPath });
722
799
  }
723
800
  async function writeManifest(path, m) {
724
- await mkdir2(dirname3(path), { recursive: true });
801
+ await mkdir3(dirname4(path), { recursive: true });
725
802
  const enriched = { installed_at: (/* @__PURE__ */ new Date()).toISOString(), ...m };
726
- await writeFile2(path, JSON.stringify(enriched, null, 2));
803
+ await writeFile3(path, JSON.stringify(enriched, null, 2));
727
804
  }
728
805
 
729
806
  // src/commands/install.ts
@@ -758,7 +835,12 @@ async function runInstall(input) {
758
835
  }
759
836
  const manifest_path = join4(input.target, "wiki-manifest.json");
760
837
  if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
761
- return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path }) };
838
+ const hintLines = [
839
+ `installed: ${installed.length}`,
840
+ input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
841
+ `manifest: ${manifest_path}`
842
+ ];
843
+ return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path, humanHint: hintLines.join("\n") }) };
762
844
  }
763
845
 
764
846
  // src/commands/path.ts
@@ -770,7 +852,7 @@ async function runPath(input) {
770
852
  home: input.home,
771
853
  explain: input.explain
772
854
  });
773
- return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {} }) };
855
+ return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {}, humanHint: `${r2.path} (via ${r2.source})` }) };
774
856
  }
775
857
  const r = await resolveRuntimePath({
776
858
  flag: input.flag,
@@ -779,7 +861,7 @@ async function runPath(input) {
779
861
  explain: input.explain
780
862
  });
781
863
  if (!r.ok) return { exitCode: ExitCode.NO_VAULT_CONFIGURED, result: r };
782
- return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {} }) };
864
+ 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})` }) };
783
865
  }
784
866
 
785
867
  // src/utils/lang.ts
@@ -833,14 +915,42 @@ async function runLang(input) {
833
915
  value: resolved.value,
834
916
  source: resolved.source,
835
917
  canonical: resolved.canonical,
836
- ...chain ? { chain } : {}
918
+ ...chain ? { chain } : {},
919
+ humanHint: `${resolved.value} (via ${resolved.source})`
837
920
  })
838
921
  };
839
922
  }
840
923
 
841
924
  // src/commands/init.ts
842
- import { mkdir as mkdir3, readFile as readFile6, stat as stat5, writeFile as writeFile3 } from "fs/promises";
843
- import { join as join7, dirname as dirname4 } from "path";
925
+ import { mkdir as mkdir4, readFile as readFile6, readdir as readdir3, writeFile as writeFile4 } from "fs/promises";
926
+ import { join as join7 } from "path";
927
+
928
+ // src/parsers/taxonomy.ts
929
+ import yaml2 from "js-yaml";
930
+ var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
931
+ function extractTaxonomy(schemaText) {
932
+ const m = schemaText.match(FENCE_RE);
933
+ if (!m) return err("NO_TAXONOMY_BLOCK", { message: "No fenced YAML taxonomy block found in SCHEMA.md" });
934
+ let parsed;
935
+ try {
936
+ parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
937
+ } catch (e) {
938
+ return err("INVALID_FRONTMATTER", { message: e.message });
939
+ }
940
+ if (parsed === null || typeof parsed !== "object") {
941
+ return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
942
+ }
943
+ const tax = parsed.taxonomy;
944
+ if (!Array.isArray(tax)) {
945
+ return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
946
+ }
947
+ if (!tax.every((x) => typeof x === "string")) {
948
+ return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
949
+ }
950
+ return ok(tax);
951
+ }
952
+
953
+ // src/commands/init.ts
844
954
  var DEFAULT_TAXONOMY = [
845
955
  "research",
846
956
  "comparison",
@@ -865,25 +975,59 @@ var VAULT_DIRS = [
865
975
  "meta",
866
976
  "projects"
867
977
  ];
978
+ function extractDomainFromSchema(text) {
979
+ const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
980
+ if (!m) return "";
981
+ const d = m[1].trim();
982
+ return d.startsWith("##") ? "" : d;
983
+ }
984
+ async function discoverTagsFromPages(target, knownSlugs) {
985
+ const knownSet = new Set(knownSlugs);
986
+ const discovered = /* @__PURE__ */ new Set();
987
+ for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
988
+ let entries;
989
+ try {
990
+ entries = (await readdir3(join7(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
991
+ } catch {
992
+ continue;
993
+ }
994
+ for (const file of entries) {
995
+ try {
996
+ const text = await readFile6(join7(target, dir, file), "utf8");
997
+ const fm = extractFrontmatter(text);
998
+ if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
999
+ for (const t of fm.data.tags) {
1000
+ if (typeof t === "string" && !knownSet.has(t)) discovered.add(t);
1001
+ }
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ }
1006
+ return [...discovered].sort();
1007
+ }
868
1008
  async function runInit(input) {
869
1009
  const pathRes = await resolveInitTimePath({ flag: input.flag, envValue: input.envValue, home: input.home });
870
1010
  const target = pathRes.path;
871
1011
  const langRes = await resolveLang({ flag: input.lang, envValue: void 0, home: input.home });
872
1012
  const canonicalLang = langRes.canonical;
873
- let hasSchema = false;
1013
+ let oldSchemaText;
874
1014
  try {
875
- await stat5(join7(target, "SCHEMA.md"));
876
- hasSchema = true;
1015
+ oldSchemaText = await readFile6(join7(target, "SCHEMA.md"), "utf8");
877
1016
  } catch {
878
1017
  }
879
- if (hasSchema && !input.force) {
1018
+ if (oldSchemaText && !input.force) {
880
1019
  return {
881
1020
  exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
882
1021
  result: err("INIT_TARGET_NOT_EMPTY", { target })
883
1022
  };
884
1023
  }
885
1024
  const envPath = join7(input.home, ".skillwiki", ".env");
886
- const existingEnv = await parseDotenvFile(envPath);
1025
+ let existingEnvRaw = "";
1026
+ try {
1027
+ existingEnvRaw = await readFile6(envPath, "utf8");
1028
+ } catch {
1029
+ }
1030
+ const existingEnv = parseDotenvText(existingEnvRaw);
887
1031
  const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
888
1032
  if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
889
1033
  return {
@@ -899,73 +1043,125 @@ async function runInit(input) {
899
1043
  }
900
1044
  const created = [];
901
1045
  try {
902
- await mkdir3(target, { recursive: true });
1046
+ await mkdir4(target, { recursive: true });
903
1047
  for (const d of VAULT_DIRS) {
904
- await mkdir3(join7(target, d), { recursive: true });
1048
+ await mkdir4(join7(target, d), { recursive: true });
905
1049
  created.push(d + "/");
906
1050
  }
907
1051
  } catch (e) {
908
1052
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
909
1053
  }
910
1054
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
911
- const taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
912
- const taxonomyYaml = taxonomy.map((t) => ` - ${t}`).join("\n");
1055
+ let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
1056
+ let domain = input.domain;
1057
+ let oldTaxonomy = [];
1058
+ if (oldSchemaText) {
1059
+ if (!domain) {
1060
+ const oldDomain = extractDomainFromSchema(oldSchemaText);
1061
+ if (oldDomain) domain = oldDomain;
1062
+ }
1063
+ const oldTax = extractTaxonomy(oldSchemaText);
1064
+ if (oldTax.ok) oldTaxonomy = oldTax.data;
1065
+ }
1066
+ const taxonomySet = new Set(taxonomy);
1067
+ for (const t of oldTaxonomy) {
1068
+ if (!taxonomySet.has(t)) {
1069
+ taxonomy.push(t);
1070
+ taxonomySet.add(t);
1071
+ }
1072
+ }
1073
+ const discovered = await discoverTagsFromPages(target, taxonomy);
1074
+ const discovered_tags = discovered.length;
1075
+ const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
913
1076
  try {
914
1077
  const schemaTpl = await readFile6(join7(input.templates, "SCHEMA.md"), "utf8");
915
- const schema = schemaTpl.replace("{{DOMAIN}}", input.domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", taxonomyYaml);
916
- await writeFile3(join7(target, "SCHEMA.md"), schema, "utf8");
1078
+ const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
1079
+ await writeFile4(join7(target, "SCHEMA.md"), schema, "utf8");
917
1080
  created.push("SCHEMA.md");
918
1081
  } catch (e) {
919
1082
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
920
1083
  }
921
- try {
922
- const idxTpl = await readFile6(join7(input.templates, "index.md"), "utf8");
923
- const idx = idxTpl.replace("{{INIT_DATE}}", today);
924
- await writeFile3(join7(target, "index.md"), idx, "utf8");
925
- created.push("index.md");
926
- } catch (e) {
927
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "index.md", message: String(e) }) };
928
- }
929
- try {
930
- const logTpl = await readFile6(join7(input.templates, "log.md"), "utf8");
931
- const log = logTpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", input.domain).replace("{{WIKI_LANG}}", canonicalLang);
932
- await writeFile3(join7(target, "log.md"), log, "utf8");
933
- created.push("log.md");
934
- } catch (e) {
935
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "log.md", message: String(e) }) };
1084
+ const preserved = [];
1085
+ async function writeOrPreserve(fileName, render) {
1086
+ try {
1087
+ const existing = await readFile6(join7(target, fileName), "utf8");
1088
+ if (existing.split("\n").length > 10) {
1089
+ preserved.push(fileName);
1090
+ return void 0;
1091
+ }
1092
+ } catch {
1093
+ }
1094
+ try {
1095
+ await writeFile4(join7(target, fileName), await render(), "utf8");
1096
+ created.push(fileName);
1097
+ return void 0;
1098
+ } catch (e) {
1099
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
1100
+ }
936
1101
  }
937
- try {
938
- await mkdir3(dirname4(envPath), { recursive: true });
939
- const envBody = `WIKI_PATH=${target}
940
- WIKI_LANG=${canonicalLang}
941
- `;
942
- await writeFile3(envPath, envBody, "utf8");
943
- } catch (e) {
944
- return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
1102
+ const err1 = await writeOrPreserve("index.md", async () => {
1103
+ const tpl = await readFile6(join7(input.templates, "index.md"), "utf8");
1104
+ return tpl.replace("{{INIT_DATE}}", today);
1105
+ });
1106
+ if (err1) return err1;
1107
+ const err22 = await writeOrPreserve("log.md", async () => {
1108
+ const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
1109
+ return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
1110
+ });
1111
+ if (err22) return err22;
1112
+ const isTempPath = target.startsWith("/tmp/") || target === "/tmp" || target.startsWith("/var/tmp/") || target === "/var/tmp" || target.startsWith("/private/tmp/");
1113
+ const skipEnv = !!input.noEnv || isTempPath;
1114
+ let envWritten = "";
1115
+ if (!skipEnv) {
1116
+ try {
1117
+ await writeDotenv(envPath, { WIKI_PATH: target, WIKI_LANG: canonicalLang }, existingEnvRaw);
1118
+ envWritten = envPath;
1119
+ } catch (e) {
1120
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
1121
+ }
945
1122
  }
946
1123
  const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
1124
+ const humanHint = [
1125
+ `vault: ${target}`,
1126
+ `domain: ${domain}`,
1127
+ `lang: ${canonicalLang}`,
1128
+ `created: ${created.length}, preserved: ${preserved.length}`,
1129
+ `discovered tags: ${discovered_tags}`,
1130
+ skipEnv ? "env: skipped" : `env: ${envWritten}`
1131
+ ].join("\n");
947
1132
  return {
948
1133
  exitCode: ExitCode.OK,
949
1134
  result: ok({
950
1135
  vault: target,
951
- domain: input.domain,
1136
+ domain,
952
1137
  taxonomy,
953
1138
  lang: canonicalLang,
954
1139
  created,
955
- env_written: envPath,
956
- imported_from_hermes: importedFromHermes
1140
+ preserved,
1141
+ env_written: envWritten,
1142
+ env_skipped: skipEnv,
1143
+ imported_from_hermes: importedFromHermes,
1144
+ discovered_tags,
1145
+ humanHint
957
1146
  })
958
1147
  };
959
1148
  }
960
1149
 
1150
+ // src/utils/slug.ts
1151
+ function buildSlugMap(pages) {
1152
+ const map = /* @__PURE__ */ new Map();
1153
+ for (const p of pages) {
1154
+ const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
1155
+ map.set(slug.toLowerCase(), slug);
1156
+ }
1157
+ return map;
1158
+ }
1159
+
961
1160
  // src/commands/links.ts
962
1161
  async function runLinks(input) {
963
1162
  const scan = await scanVault(input.vault);
964
1163
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
965
- const slugs = /* @__PURE__ */ new Set();
966
- for (const p of scan.data.typedKnowledge) {
967
- slugs.add(p.relPath.replace(/\.md$/, "").split("/").pop());
968
- }
1164
+ const slugs = buildSlugMap(scan.data.typedKnowledge);
969
1165
  const broken = [];
970
1166
  for (const p of scan.data.typedKnowledge) {
971
1167
  const text = await readPage(p);
@@ -974,48 +1170,22 @@ async function runLinks(input) {
974
1170
  const lines = body.split("\n");
975
1171
  for (const slug of extractBodyWikilinks(body)) {
976
1172
  const tail = slug.split("/").pop();
977
- if (!slugs.has(tail)) {
1173
+ if (!slugs.has(tail.toLowerCase())) {
978
1174
  const line = lines.findIndex((l) => l.includes(`[[${slug}`));
979
1175
  broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
980
1176
  }
981
1177
  }
982
1178
  }
983
1179
  if (broken.length > 0) {
984
- return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken }) };
1180
+ return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken, humanHint: `broken: ${broken.length}
1181
+ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }) };
985
1182
  }
986
- return { exitCode: ExitCode.OK, result: ok({ broken }) };
1183
+ return { exitCode: ExitCode.OK, result: ok({ broken, humanHint: "no broken wikilinks" }) };
987
1184
  }
988
1185
 
989
1186
  // src/commands/tag-audit.ts
990
1187
  import { readFile as readFile7 } from "fs/promises";
991
1188
  import { join as join8 } from "path";
992
-
993
- // src/parsers/taxonomy.ts
994
- import yaml2 from "js-yaml";
995
- var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
996
- function extractTaxonomy(schemaText) {
997
- const m = schemaText.match(FENCE_RE);
998
- if (!m) return ok([]);
999
- let parsed;
1000
- try {
1001
- parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
1002
- } catch (e) {
1003
- return err("INVALID_FRONTMATTER", { message: e.message });
1004
- }
1005
- if (parsed === null || typeof parsed !== "object") {
1006
- return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
1007
- }
1008
- const tax = parsed.taxonomy;
1009
- if (!Array.isArray(tax)) {
1010
- return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
1011
- }
1012
- if (!tax.every((x) => typeof x === "string")) {
1013
- return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
1014
- }
1015
- return ok(tax);
1016
- }
1017
-
1018
- // src/commands/tag-audit.ts
1019
1189
  async function runTagAudit(input) {
1020
1190
  const scan = await scanVault(input.vault);
1021
1191
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -1037,9 +1207,9 @@ async function runTagAudit(input) {
1037
1207
  }
1038
1208
  }
1039
1209
  if (violations.length > 0) {
1040
- return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data }) };
1210
+ return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data, humanHint: violations.map((v) => `${v.page}: "${v.tag}" not in taxonomy`).join("\n") }) };
1041
1211
  }
1042
- return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data }) };
1212
+ return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
1043
1213
  }
1044
1214
 
1045
1215
  // src/commands/index-check.ts
@@ -1053,7 +1223,11 @@ async function runIndexCheck(input) {
1053
1223
  indexText = await readFile8(join9(input.vault, "index.md"), "utf8");
1054
1224
  } catch {
1055
1225
  }
1056
- const indexSlugs = new Set(extractBodyWikilinks(indexText).map((s) => s.split("/").pop()));
1226
+ const indexSlugsLower = /* @__PURE__ */ new Map();
1227
+ for (const s of extractBodyWikilinks(indexText)) {
1228
+ const tail = s.split("/").pop();
1229
+ indexSlugsLower.set(tail.toLowerCase(), tail);
1230
+ }
1057
1231
  const fileSlugs = /* @__PURE__ */ new Map();
1058
1232
  for (const p of scan.data.typedKnowledge) {
1059
1233
  const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
@@ -1061,16 +1235,21 @@ async function runIndexCheck(input) {
1061
1235
  }
1062
1236
  const missing_from_index = [];
1063
1237
  for (const [slug, relPath] of fileSlugs.entries()) {
1064
- if (!indexSlugs.has(slug)) missing_from_index.push(relPath);
1238
+ if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
1065
1239
  }
1240
+ const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
1066
1241
  const ghost_entries = [];
1067
- for (const slug of indexSlugs) {
1068
- if (!fileSlugs.has(slug)) ghost_entries.push(slug);
1242
+ for (const [lower, orig] of indexSlugsLower) {
1243
+ if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
1069
1244
  }
1245
+ const hintLines = [];
1246
+ if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
1247
+ if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
1248
+ if (hintLines.length === 0) hintLines.push("index OK");
1070
1249
  if (missing_from_index.length > 0 || ghost_entries.length > 0) {
1071
- return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries }) };
1250
+ return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
1072
1251
  }
1073
- return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries }) };
1252
+ return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
1074
1253
  }
1075
1254
 
1076
1255
  // src/commands/stale.ts
@@ -1110,8 +1289,8 @@ async function runStale(input) {
1110
1289
  stale.push({ page: p.relPath, page_updated: updated, newest_source_ingested: newest, gap_days: gap });
1111
1290
  }
1112
1291
  }
1113
- if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale }) };
1114
- return { exitCode: ExitCode.OK, result: ok({ stale }) };
1292
+ if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale, humanHint: stale.map((s) => `${s.page} (${s.gap_days}d stale)`).join("\n") }) };
1293
+ return { exitCode: ExitCode.OK, result: ok({ stale, humanHint: "no stale pages" }) };
1115
1294
  }
1116
1295
 
1117
1296
  // src/commands/pagesize.ts
@@ -1126,17 +1305,17 @@ async function runPagesize(input) {
1126
1305
  const count = body.split("\n").length;
1127
1306
  if (count > input.lines) oversized.push({ page: p.relPath, lines: count });
1128
1307
  }
1129
- if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized }) };
1130
- return { exitCode: ExitCode.OK, result: ok({ oversized }) };
1308
+ if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized, humanHint: oversized.map((p) => `${p.page}: ${p.lines} lines`).join("\n") }) };
1309
+ return { exitCode: ExitCode.OK, result: ok({ oversized, humanHint: "all pages within size limit" }) };
1131
1310
  }
1132
1311
 
1133
1312
  // src/commands/log-rotate.ts
1134
- import { readFile as readFile10, rename as rename2, writeFile as writeFile4, stat as stat6 } from "fs/promises";
1313
+ import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as stat5 } from "fs/promises";
1135
1314
  import { join as join11 } from "path";
1136
1315
  var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
1137
1316
  async function runLogRotate(input) {
1138
1317
  try {
1139
- await stat6(join11(input.vault, "SCHEMA.md"));
1318
+ await stat5(join11(input.vault, "SCHEMA.md"));
1140
1319
  } catch {
1141
1320
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
1142
1321
  }
@@ -1150,12 +1329,12 @@ async function runLogRotate(input) {
1150
1329
  const matches = [...logText.matchAll(ENTRY_RE)];
1151
1330
  const entries = matches.length;
1152
1331
  if (entries < input.threshold) {
1153
- return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false }) };
1332
+ return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
1154
1333
  }
1155
1334
  if (!input.apply) {
1156
1335
  return {
1157
1336
  exitCode: ExitCode.LOG_ROTATE_NEEDED,
1158
- result: ok({ entries, threshold: input.threshold, rotated: false })
1337
+ result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)` })
1159
1338
  };
1160
1339
  }
1161
1340
  const newestYear = matches[matches.length - 1][1];
@@ -1172,17 +1351,86 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
1172
1351
 
1173
1352
  - Previous log moved to ${rotatedName}
1174
1353
  `;
1175
- await writeFile4(logPath, fresh, "utf8");
1354
+ await writeFile5(logPath, fresh, "utf8");
1176
1355
  } catch (e) {
1177
1356
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
1178
1357
  }
1179
- return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName }) };
1358
+ return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
1359
+ }
1360
+
1361
+ // src/commands/topic-map-check.ts
1362
+ var DEFAULT_THRESHOLD = 200;
1363
+ async function runTopicMapCheck(input) {
1364
+ const threshold = input.threshold ?? DEFAULT_THRESHOLD;
1365
+ const scan = await scanVault(input.vault);
1366
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1367
+ const page_count = scan.data.typedKnowledge.length;
1368
+ const recommended = page_count >= threshold;
1369
+ return {
1370
+ exitCode: ExitCode.OK,
1371
+ result: ok({
1372
+ recommended,
1373
+ page_count,
1374
+ threshold,
1375
+ humanHint: recommended ? `topic map recommended (${page_count} pages >= ${threshold} threshold)` : `topic map not needed (${page_count} pages < ${threshold} threshold)`
1376
+ })
1377
+ };
1378
+ }
1379
+
1380
+ // src/commands/index-link-format.ts
1381
+ import { readFile as readFile11 } from "fs/promises";
1382
+ import { join as join12 } from "path";
1383
+ var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
1384
+ async function runIndexLinkFormat(input) {
1385
+ let text = "";
1386
+ try {
1387
+ text = await readFile11(join12(input.vault, "index.md"), "utf8");
1388
+ } catch {
1389
+ }
1390
+ const markdown_links = [];
1391
+ for (const [i, line] of text.split("\n").entries()) {
1392
+ if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
1393
+ }
1394
+ const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
1395
+ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
1396
+ return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
1397
+ }
1398
+
1399
+ // src/commands/dedup.ts
1400
+ async function runDedup(input) {
1401
+ const scan = await scanVault(input.vault);
1402
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1403
+ const hashMap = /* @__PURE__ */ new Map();
1404
+ let totalFiles = 0;
1405
+ for (const raw of scan.data.raw) {
1406
+ const fm = extractFrontmatter(await readPage(raw));
1407
+ if (!fm.ok) continue;
1408
+ const sha = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
1409
+ if (!sha || sha.length !== 64) continue;
1410
+ totalFiles++;
1411
+ const existing = hashMap.get(sha);
1412
+ if (existing) existing.push(raw.relPath);
1413
+ else hashMap.set(sha, [raw.relPath]);
1414
+ }
1415
+ const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files }));
1416
+ const exitCode = duplicates.length > 0 ? ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
1417
+ const hintLines = [`scanned: ${totalFiles} raw files`];
1418
+ if (duplicates.length > 0) {
1419
+ hintLines.push(`duplicates: ${duplicates.length}`);
1420
+ for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
1421
+ } else {
1422
+ hintLines.push("0 duplicates");
1423
+ }
1424
+ return {
1425
+ exitCode,
1426
+ result: ok({ scanned: totalFiles, duplicates, humanHint: hintLines.join("\n") })
1427
+ };
1180
1428
  }
1181
1429
 
1182
1430
  // src/commands/lint.ts
1183
- var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_drift", "tag_not_in_taxonomy"];
1184
- var WARNING_ORDER = ["index_incomplete", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
1185
- var INFO_ORDER = ["bridges", "low_confidence_single_source"];
1431
+ var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
1432
+ var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
1433
+ var INFO_ORDER = ["bridges", "low_confidence_single_source", "topic_map_recommended"];
1186
1434
  async function runLint(input) {
1187
1435
  const buckets = {};
1188
1436
  const links = await runLinks({ vault: input.vault });
@@ -1202,6 +1450,10 @@ async function runLint(input) {
1202
1450
  ghost_entries: idx.result.data.ghost_entries
1203
1451
  }];
1204
1452
  }
1453
+ const linkFmt = await runIndexLinkFormat({ vault: input.vault });
1454
+ if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
1455
+ buckets.index_link_format = linkFmt.result.data.markdown_links;
1456
+ }
1205
1457
  const stale = await runStale({ vault: input.vault, days: input.days });
1206
1458
  if (stale.result.ok && stale.result.data.stale.length > 0) buckets.stale_page = stale.result.data.stale;
1207
1459
  const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
@@ -1215,6 +1467,12 @@ async function runLint(input) {
1215
1467
  if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
1216
1468
  if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
1217
1469
  }
1470
+ const topicMap = await runTopicMapCheck({ vault: input.vault });
1471
+ if (topicMap.result.ok && topicMap.result.data.recommended) {
1472
+ buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
1473
+ }
1474
+ const dedup = await runDedup({ vault: input.vault });
1475
+ if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
1218
1476
  const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1219
1477
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1220
1478
  const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
@@ -1226,19 +1484,322 @@ async function runLint(input) {
1226
1484
  let exitCode = ExitCode.OK;
1227
1485
  if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
1228
1486
  else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
1487
+ const hintLines = [];
1488
+ if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
1489
+ if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
1490
+ if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
1491
+ const allBuckets = [...errorOut, ...warningOut, ...infoOut];
1492
+ for (const b of allBuckets) {
1493
+ hintLines.push(` ${b.kind}: ${b.items.length}`);
1494
+ }
1495
+ if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
1229
1496
  return {
1230
1497
  exitCode,
1231
1498
  result: ok({
1232
1499
  vault: { path: input.vault, source: input.source ?? "resolved" },
1233
1500
  summary,
1234
- by_severity: { error: errorOut, warning: warningOut, info: infoOut }
1501
+ by_severity: { error: errorOut, warning: warningOut, info: infoOut },
1502
+ humanHint: hintLines.join("\n")
1235
1503
  })
1236
1504
  };
1237
1505
  }
1238
1506
 
1507
+ // src/commands/config.ts
1508
+ import { readFile as readFile12 } from "fs/promises";
1509
+ import { existsSync } from "fs";
1510
+ import { join as join13 } from "path";
1511
+ function validateKey(key) {
1512
+ return CONFIG_KEYS.includes(key);
1513
+ }
1514
+ function configPath(home) {
1515
+ return join13(home, ".skillwiki", ".env");
1516
+ }
1517
+ async function runConfigGet(input) {
1518
+ if (!validateKey(input.key)) {
1519
+ return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
1520
+ }
1521
+ const map = await parseDotenvFile(configPath(input.home));
1522
+ const value = map[input.key] ?? "";
1523
+ return { exitCode: ExitCode.OK, result: ok({ key: input.key, value, humanHint: value }) };
1524
+ }
1525
+ async function runConfigSet(input) {
1526
+ if (!validateKey(input.key)) {
1527
+ return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
1528
+ }
1529
+ const filePath = configPath(input.home);
1530
+ try {
1531
+ let originalContent;
1532
+ try {
1533
+ originalContent = await readFile12(filePath, "utf8");
1534
+ } catch {
1535
+ }
1536
+ const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
1537
+ const merged = { ...existing, [input.key]: input.value };
1538
+ await writeDotenv(filePath, merged, originalContent);
1539
+ return { exitCode: ExitCode.OK, result: ok({ key: input.key, value: input.value, written: true, humanHint: `${input.key}=${input.value}` }) };
1540
+ } catch (e) {
1541
+ return { exitCode: ExitCode.CONFIG_WRITE_FAILED, result: err("CONFIG_WRITE_FAILED", { key: input.key, error: String(e) }) };
1542
+ }
1543
+ }
1544
+ async function runConfigList(input) {
1545
+ const map = await parseDotenvFile(configPath(input.home));
1546
+ const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
1547
+ return { exitCode: ExitCode.OK, result: ok({ entries, humanHint: entries.map((e) => `${e.key}=${e.value}`).join("\n") }) };
1548
+ }
1549
+ async function runConfigPath(input) {
1550
+ const filePath = configPath(input.home);
1551
+ return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync(filePath), humanHint: filePath }) };
1552
+ }
1553
+
1554
+ // src/commands/doctor.ts
1555
+ import { existsSync as existsSync2, readdirSync, statSync } from "fs";
1556
+ import { join as join14 } from "path";
1557
+ import { execSync } from "child_process";
1558
+ function check(status, id, label, detail) {
1559
+ return { id, label, status, detail };
1560
+ }
1561
+ function checkNodeVersion() {
1562
+ const major = parseInt(process.version.slice(1).split(".")[0], 10);
1563
+ if (major >= 20) {
1564
+ return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
1565
+ }
1566
+ return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
1567
+ }
1568
+ function checkCliOnPath(argv) {
1569
+ if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
1570
+ return check("warn", "cli_on_path", "skillwiki on PATH", "Running via node cli.js (dev mode) \u2014 PATH check skipped");
1571
+ }
1572
+ if (argv.length >= 2 && argv[1] === "skillwiki") {
1573
+ return check("pass", "cli_on_path", "skillwiki on PATH", "Running as skillwiki \u2014 already on PATH");
1574
+ }
1575
+ try {
1576
+ execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
1577
+ return check("pass", "cli_on_path", "skillwiki on PATH", "skillwiki found on PATH");
1578
+ } catch {
1579
+ return check("warn", "cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
1580
+ }
1581
+ }
1582
+ async function checkConfigFile(home) {
1583
+ const cfgPath = configPath(home);
1584
+ if (!existsSync2(cfgPath)) {
1585
+ return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
1586
+ }
1587
+ try {
1588
+ const map = await parseDotenvFile(cfgPath);
1589
+ const keys = Object.keys(map);
1590
+ return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
1591
+ } catch (e) {
1592
+ return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
1593
+ }
1594
+ }
1595
+ function checkWikiPathExists(resolvedPath) {
1596
+ if (resolvedPath === void 0) {
1597
+ return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
1598
+ }
1599
+ if (existsSync2(resolvedPath) && statSync(resolvedPath).isDirectory()) {
1600
+ return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
1601
+ }
1602
+ return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
1603
+ }
1604
+ function checkVaultStructure(resolvedPath) {
1605
+ if (resolvedPath === void 0) {
1606
+ return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
1607
+ }
1608
+ if (!existsSync2(resolvedPath)) {
1609
+ return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
1610
+ }
1611
+ const missing = [];
1612
+ if (!existsSync2(join14(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
1613
+ for (const dir of ["raw", "entities", "concepts", "meta"]) {
1614
+ if (!existsSync2(join14(resolvedPath, dir))) missing.push(dir + "/");
1615
+ }
1616
+ if (missing.length === 0) {
1617
+ return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
1618
+ }
1619
+ return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
1620
+ }
1621
+ function checkSkillsInstalled(home) {
1622
+ const skillsDir = join14(home, ".claude", "skills");
1623
+ if (!existsSync2(skillsDir)) {
1624
+ return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
1625
+ }
1626
+ const found = findSkillMd(skillsDir);
1627
+ if (found.length > 0) {
1628
+ return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found`);
1629
+ }
1630
+ return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
1631
+ }
1632
+ function findSkillMd(dir) {
1633
+ const results = [];
1634
+ let entries;
1635
+ try {
1636
+ entries = readdirSync(dir, { withFileTypes: true });
1637
+ } catch {
1638
+ return results;
1639
+ }
1640
+ for (const entry of entries) {
1641
+ if (entry.isFile() && entry.name === "SKILL.md") {
1642
+ results.push(join14(dir, entry.name));
1643
+ } else if (entry.isDirectory()) {
1644
+ results.push(...findSkillMd(join14(dir, entry.name)));
1645
+ }
1646
+ }
1647
+ return results;
1648
+ }
1649
+ async function runDoctor(input) {
1650
+ const checks = [];
1651
+ checks.push(checkNodeVersion());
1652
+ checks.push(checkCliOnPath(input.argv));
1653
+ checks.push(await checkConfigFile(input.home));
1654
+ const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
1655
+ if (resolved.ok) {
1656
+ checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
1657
+ } else {
1658
+ checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
1659
+ }
1660
+ const resolvedPath = resolved.ok ? resolved.data.path : void 0;
1661
+ checks.push(checkWikiPathExists(resolvedPath));
1662
+ checks.push(checkVaultStructure(resolvedPath));
1663
+ checks.push(checkSkillsInstalled(input.home));
1664
+ const summary = {
1665
+ pass: checks.filter((c) => c.status === "pass").length,
1666
+ warn: checks.filter((c) => c.status === "warn").length,
1667
+ error: checks.filter((c) => c.status === "error").length
1668
+ };
1669
+ const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
1670
+ const statusIcon = { pass: "\u2713", warn: "\u26A0", error: "\u2717" };
1671
+ const lines = checks.map((c) => {
1672
+ const icon = statusIcon[c.status];
1673
+ const padded = c.label.padEnd(24);
1674
+ return ` ${icon} ${padded} ${c.detail}`;
1675
+ });
1676
+ lines.push("");
1677
+ lines.push(`${summary.pass} pass \xB7 ${summary.warn} warn \xB7 ${summary.error} error`);
1678
+ const humanHint = lines.join("\n");
1679
+ return { exitCode, result: ok({ checks, summary, humanHint }) };
1680
+ }
1681
+
1682
+ // src/commands/archive.ts
1683
+ import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
1684
+ import { join as join15, dirname as dirname6 } from "path";
1685
+ async function runArchive(input) {
1686
+ const scan = await scanVault(input.vault);
1687
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1688
+ let relPath;
1689
+ if (input.page.includes("/")) {
1690
+ relPath = scan.data.typedKnowledge.find((p) => p.relPath === input.page)?.relPath;
1691
+ } else {
1692
+ relPath = scan.data.typedKnowledge.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
1693
+ }
1694
+ if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
1695
+ if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
1696
+ const archivePath = join15("_archive", relPath);
1697
+ await mkdir5(dirname6(join15(input.vault, archivePath)), { recursive: true });
1698
+ let indexUpdated = false;
1699
+ const indexPath = join15(input.vault, "index.md");
1700
+ try {
1701
+ const idx = await readFile13(indexPath, "utf8");
1702
+ const slug = relPath.replace(/\.md$/, "").split("/").pop();
1703
+ const originalLines = idx.split("\n");
1704
+ const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
1705
+ if (filtered.length !== originalLines.length) {
1706
+ await writeFile6(indexPath, filtered.join("\n"), "utf8");
1707
+ indexUpdated = true;
1708
+ }
1709
+ } catch (e) {
1710
+ if (e?.code !== "ENOENT") throw e;
1711
+ }
1712
+ await rename3(join15(input.vault, relPath), join15(input.vault, archivePath));
1713
+ return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
1714
+ }
1715
+
1716
+ // src/commands/drift.ts
1717
+ import { createHash as createHash2 } from "crypto";
1718
+
1719
+ // src/utils/fetch.ts
1720
+ async function controlledFetch(url, opts) {
1721
+ let current = url;
1722
+ for (let hop = 0; hop <= opts.maxRedirects; hop++) {
1723
+ const guard = runFetchGuardSync({ url: current });
1724
+ if (!guard.result.ok) return guard.result;
1725
+ const ctrl = new AbortController();
1726
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
1727
+ let res;
1728
+ try {
1729
+ res = await fetch(current, { redirect: "manual", signal: ctrl.signal });
1730
+ } catch (e) {
1731
+ clearTimeout(timer);
1732
+ if (e?.name === "AbortError") return err("FETCH_TIMEOUT", { url: current });
1733
+ return err("FETCH_FAILED", { message: String(e) });
1734
+ }
1735
+ clearTimeout(timer);
1736
+ if (res.status >= 300 && res.status < 400) {
1737
+ const loc = res.headers.get("location");
1738
+ if (!loc) return err("FETCH_FAILED", { reason: "redirect without Location" });
1739
+ current = new URL(loc, current).toString();
1740
+ continue;
1741
+ }
1742
+ const declared = Number(res.headers.get("content-length") ?? "0");
1743
+ if (declared > opts.maxBytes) return err("FETCH_TOO_LARGE", { declared, limit: opts.maxBytes });
1744
+ const buf = new Uint8Array(await res.arrayBuffer());
1745
+ if (buf.byteLength > opts.maxBytes) return err("FETCH_TOO_LARGE", { actual: buf.byteLength, limit: opts.maxBytes });
1746
+ return ok({ url: current, status: res.status, body: new TextDecoder().decode(buf), bytes: buf.byteLength });
1747
+ }
1748
+ return err("FETCH_FAILED", { reason: "too many redirects", limit: opts.maxRedirects });
1749
+ }
1750
+
1751
+ // src/commands/drift.ts
1752
+ var FETCH_OPTS = { timeoutMs: 1e4, maxBytes: 5e6, maxRedirects: 5 };
1753
+ async function runDrift(input) {
1754
+ const doFetch = input.fetchFn ?? controlledFetch;
1755
+ const scan = await scanVault(input.vault);
1756
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1757
+ const results = [];
1758
+ for (const raw of scan.data.raw) {
1759
+ const fm = extractFrontmatter(await readPage(raw));
1760
+ if (!fm.ok) continue;
1761
+ const sourceUrl = typeof fm.data.source_url === "string" ? fm.data.source_url : null;
1762
+ const storedHash = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
1763
+ if (!sourceUrl || !storedHash) continue;
1764
+ const resp = await doFetch(sourceUrl, FETCH_OPTS);
1765
+ if (!resp.ok) {
1766
+ results.push({
1767
+ raw_path: raw.relPath,
1768
+ source_url: sourceUrl,
1769
+ stored_sha256: storedHash,
1770
+ current_sha256: null,
1771
+ status: "fetch_failed",
1772
+ fetch_error: resp.error
1773
+ });
1774
+ continue;
1775
+ }
1776
+ const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
1777
+ const drifted2 = currentHash !== storedHash;
1778
+ results.push({
1779
+ raw_path: raw.relPath,
1780
+ source_url: sourceUrl,
1781
+ stored_sha256: storedHash,
1782
+ current_sha256: currentHash,
1783
+ status: drifted2 ? "drifted" : "unchanged"
1784
+ });
1785
+ }
1786
+ const drifted = results.filter((r) => r.status === "drifted");
1787
+ const fetchFailed = results.filter((r) => r.status === "fetch_failed");
1788
+ const unchanged = results.filter((r) => r.status === "unchanged").length;
1789
+ const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
1790
+ const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
1791
+ if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
1792
+ if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
1793
+ return {
1794
+ exitCode,
1795
+ result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
1796
+ };
1797
+ }
1798
+
1239
1799
  // src/cli.ts
1800
+ var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
1240
1801
  var program = new Command();
1241
- program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version("0.1.0");
1802
+ program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
1242
1803
  program.option("--human", "render terminal-readable output instead of JSON");
1243
1804
  function emit(r) {
1244
1805
  if (program.opts().human) printHuman(r.result);
@@ -1257,7 +1818,7 @@ program.command("orphans [vault]").action(async (vault) => emit(await runOrphans
1257
1818
  })));
1258
1819
  program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
1259
1820
  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) => {
1260
- const skillsRoot = opts.skillsRoot ?? new URL("../../skills/", import.meta.url).pathname;
1821
+ const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
1261
1822
  emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
1262
1823
  });
1263
1824
  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) => {
@@ -1279,7 +1840,7 @@ program.command("lang").option("--lang <code>", "explicit language override").op
1279
1840
  explain: !!opts.explain
1280
1841
  }));
1281
1842
  });
1282
- 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).action(async (opts) => {
1843
+ 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) => {
1283
1844
  const templates = new URL("../templates/", import.meta.url).pathname;
1284
1845
  const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
1285
1846
  emit(await runInit({
@@ -1290,7 +1851,8 @@ program.command("init").option("--target <dir>", "explicit target directory").re
1290
1851
  domain: opts.domain,
1291
1852
  taxonomy,
1292
1853
  lang: opts.lang,
1293
- force: !!opts.force
1854
+ force: !!opts.force,
1855
+ noEnv: opts.env === false
1294
1856
  }));
1295
1857
  });
1296
1858
  async function resolveVaultArg(arg) {
@@ -1344,6 +1906,31 @@ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => p
1344
1906
  logThreshold: opts.logThreshold
1345
1907
  }));
1346
1908
  });
1909
+ var configCmd = program.command("config").description("manage skillwiki configuration");
1910
+ configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
1911
+ configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));
1912
+ configCmd.command("list").description("list all config key=value pairs").action(async () => emit(await runConfigList({ home: process.env.HOME ?? "" })));
1913
+ configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
1914
+ program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
1915
+ home: process.env.HOME ?? "",
1916
+ envValue: process.env.WIKI_PATH,
1917
+ argv: process.argv
1918
+ })));
1919
+ program.command("archive <page> [vault]").description("archive a typed-knowledge page").action(async (page, vault) => {
1920
+ const v = await resolveVaultArg(vault);
1921
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1922
+ else emit(await runArchive({ vault: v.vault, page }));
1923
+ });
1924
+ program.command("drift [vault]").description("detect content drift in raw sources").action(async (vault) => {
1925
+ const v = await resolveVaultArg(vault);
1926
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1927
+ else emit(await runDrift({ vault: v.vault }));
1928
+ });
1929
+ program.command("dedup [vault]").description("detect duplicate raw sources by sha256").action(async (vault) => {
1930
+ const v = await resolveVaultArg(vault);
1931
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1932
+ else emit(await runDedup({ vault: v.vault }));
1933
+ });
1347
1934
  program.parseAsync(process.argv).catch((e) => {
1348
1935
  process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
1349
1936
  process.exit(1);