skillwiki 0.9.1 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/cli.js +1125 -683
  2. package/package.json +1 -1
  3. package/skills/.claude-plugin/plugin.json +1 -1
  4. package/skills/.codex-plugin/plugin.json +1 -1
  5. package/skills/agents/proj-decide.md +1 -0
  6. package/skills/agents/proj-distill.md +1 -0
  7. package/skills/agents/proj-init.md +1 -0
  8. package/skills/agents/proj-work.md +1 -0
  9. package/skills/agents/wiki-adapter-prd.md +8 -5
  10. package/skills/agents/wiki-add-task.md +8 -5
  11. package/skills/agents/wiki-archive.md +1 -0
  12. package/skills/agents/wiki-audit.md +1 -0
  13. package/skills/agents/wiki-canvas.md +1 -0
  14. package/skills/agents/wiki-crystallize.md +5 -2
  15. package/skills/agents/wiki-ingest.md +8 -5
  16. package/skills/agents/wiki-lint.md +5 -3
  17. package/skills/agents/wiki-query.md +4 -1
  18. package/skills/agents/wiki-reingest.md +6 -3
  19. package/skills/agents/wiki-sync.md +1 -0
  20. package/skills/package.json +1 -1
  21. package/skills/skills/using-skillwiki/SKILL.md +26 -1
  22. package/skills/skills/wiki-adapter-prd/SKILL.md +8 -5
  23. package/skills/skills/wiki-add-task/SKILL.md +9 -6
  24. package/skills/skills/wiki-crystallize/SKILL.md +5 -2
  25. package/skills/skills/wiki-ingest/SKILL.md +8 -6
  26. package/skills/skills/wiki-lint/SKILL.md +5 -3
  27. package/skills/skills/wiki-query/SKILL.md +4 -1
  28. package/skills/using-skillwiki/SKILL.md +26 -1
  29. package/skills/wiki-adapter-prd/SKILL.md +8 -5
  30. package/skills/wiki-add-task/SKILL.md +9 -6
  31. package/skills/wiki-crystallize/SKILL.md +5 -2
  32. package/skills/wiki-ingest/SKILL.md +8 -6
  33. package/skills/wiki-lint/SKILL.md +5 -3
  34. package/skills/wiki-query/SKILL.md +4 -1
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-TPS5XD2J.js";
15
15
 
16
16
  // src/cli.ts
17
- import { join as join47 } from "path";
17
+ import { join as join48 } from "path";
18
18
  import { Command as Command2 } from "commander";
19
19
 
20
20
  // ../shared/src/exit-codes.ts
@@ -69,7 +69,8 @@ var ExitCode = {
69
69
  BODY_TRUNCATION_GUARD: 47,
70
70
  SYNC_LOCK_HELD: 48,
71
71
  LOG_APPEND_LOCK_HELD: 49,
72
- FLEET_MANIFEST_INVALID: 50
72
+ FLEET_MANIFEST_INVALID: 50,
73
+ SENSITIVE_CONTENT_DETECTED: 51
73
74
  };
74
75
 
75
76
  // ../shared/src/json-output.ts
@@ -450,6 +451,148 @@ function sanitizeUrl(u) {
450
451
  // src/commands/validate.ts
451
452
  import { readFile as readFile2, writeFile } from "fs/promises";
452
453
  import { join as join2, resolve, relative, sep } from "path";
454
+
455
+ // src/utils/sensitive-content.ts
456
+ import { createHash as createHash2 } from "crypto";
457
+ var REDACTED_RE = /\[REDACTED:[^\]]+\]/i;
458
+ var SYNTHETIC_RE = /^(?:<[^>]+>|\$\{[^}]+\}|REPLACE_WITH_[A-Z0-9_]+|YOUR_[A-Z0-9_]+|EXAMPLE_[A-Z0-9_]+)$/i;
459
+ var MATCHERS = [
460
+ {
461
+ kind: "private_key",
462
+ re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g
463
+ },
464
+ {
465
+ kind: "authorization_header",
466
+ re: /\bAuthorization["']?\s*:\s*["']?(Bearer\s+[A-Za-z0-9._~+/-]{20,})["']?/gi,
467
+ valueGroup: 1
468
+ },
469
+ {
470
+ kind: "cookie",
471
+ re: /\b(?:Cookie|Set-Cookie)\s*:\s*([^\n]{20,})/gi,
472
+ valueGroup: 1
473
+ },
474
+ {
475
+ kind: "jwt",
476
+ re: /\b([A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,})\b/g,
477
+ valueGroup: 1
478
+ },
479
+ {
480
+ kind: "provider_key",
481
+ re: /\b(sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|gh[pousr]_[A-Za-z0-9_=-]{20,})\b/g,
482
+ valueGroup: 1
483
+ },
484
+ {
485
+ kind: "access_key",
486
+ re: /\b((?:AKIA|ASIA)[A-Z0-9]{16})\b/g,
487
+ valueGroup: 1
488
+ },
489
+ {
490
+ kind: "access_key",
491
+ re: /\b(?:access[-_ ]?key|credential)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
492
+ valueGroup: 1
493
+ },
494
+ {
495
+ kind: "api_key",
496
+ re: /\b(?:api[-_ ]?key)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
497
+ valueGroup: 1
498
+ },
499
+ {
500
+ kind: "password",
501
+ re: /\b(?:pass(?:word|wd)?)["']?\s*[:=]\s*["']?([^\s`"']{8,})["']?/gi,
502
+ valueGroup: 1
503
+ },
504
+ {
505
+ kind: "secret",
506
+ re: /\b(?:secret|client[-_ ]?secret)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
507
+ valueGroup: 1
508
+ },
509
+ {
510
+ kind: "token",
511
+ re: /\b(?:token|session)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
512
+ valueGroup: 1
513
+ }
514
+ ];
515
+ function fingerprint(value) {
516
+ return createHash2("sha256").update(value).digest("hex").slice(0, 12);
517
+ }
518
+ function lineFor(text, offset) {
519
+ return text.slice(0, offset).split(/\r?\n/).length;
520
+ }
521
+ function redactMarker(kind, value) {
522
+ return `[REDACTED:${kind}:${fingerprint(value)}]`;
523
+ }
524
+ function isSyntheticPlaceholder(value) {
525
+ return REDACTED_RE.test(value) || SYNTHETIC_RE.test(value.trim());
526
+ }
527
+ function collectMatches(text) {
528
+ const matches = [];
529
+ for (const matcher of MATCHERS) {
530
+ matcher.re.lastIndex = 0;
531
+ for (const m of text.matchAll(matcher.re)) {
532
+ const whole = m[0];
533
+ const start = m.index ?? 0;
534
+ if (REDACTED_RE.test(whole)) continue;
535
+ const value = matcher.valueGroup ? m[matcher.valueGroup] : whole;
536
+ if (isSyntheticPlaceholder(value)) continue;
537
+ const valueOffset = whole.lastIndexOf(value);
538
+ const valueStart = start + Math.max(0, valueOffset);
539
+ matches.push({
540
+ start,
541
+ end: start + whole.length,
542
+ valueStart,
543
+ valueEnd: valueStart + value.length,
544
+ kind: matcher.kind
545
+ });
546
+ }
547
+ }
548
+ return matches.sort((a, b) => {
549
+ if (a.valueStart !== b.valueStart) return a.valueStart - b.valueStart;
550
+ return b.valueEnd - b.valueStart - (a.valueEnd - a.valueStart);
551
+ });
552
+ }
553
+ function collapseOverlaps(matches) {
554
+ const kept = [];
555
+ for (const match of matches) {
556
+ const overlaps = kept.some((k) => match.valueStart < k.valueEnd && match.valueEnd > k.valueStart);
557
+ if (!overlaps) kept.push(match);
558
+ }
559
+ return kept;
560
+ }
561
+ function scanSensitiveContent(text, opts = {}) {
562
+ return collapseOverlaps(collectMatches(text)).map((match) => {
563
+ const value = text.slice(match.valueStart, match.valueEnd);
564
+ const marker = redactMarker(match.kind, value);
565
+ const rawPreview = text.slice(Math.max(0, match.start - 24), Math.min(text.length, match.end + 24));
566
+ const preview = rawPreview.replace(value, marker);
567
+ return {
568
+ file: opts.file,
569
+ line: lineFor(text, match.valueStart),
570
+ kind: match.kind,
571
+ preview,
572
+ fingerprint: fingerprint(value)
573
+ };
574
+ });
575
+ }
576
+ function redactSensitiveContent(text, opts = {}) {
577
+ const matches = collapseOverlaps(collectMatches(text));
578
+ if (matches.length === 0) return { text, changed: false, findings: [] };
579
+ let out = "";
580
+ let cursor = 0;
581
+ for (const match of matches) {
582
+ const value = text.slice(match.valueStart, match.valueEnd);
583
+ out += text.slice(cursor, match.valueStart);
584
+ out += redactMarker(match.kind, value);
585
+ cursor = match.valueEnd;
586
+ }
587
+ out += text.slice(cursor);
588
+ return {
589
+ text: out,
590
+ changed: out !== text,
591
+ findings: scanSensitiveContent(text, opts)
592
+ };
593
+ }
594
+
595
+ // src/commands/validate.ts
453
596
  var TYPE_TO_SECTION = {
454
597
  entity: "Entities",
455
598
  concept: "Concepts",
@@ -480,6 +623,26 @@ async function runValidate(input) {
480
623
  return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
481
624
  }
482
625
  const det = detectSchema(fm.data);
626
+ const sensitiveFindings = scanSensitiveContent(text, { file: input.file });
627
+ if (sensitiveFindings.length > 0) {
628
+ const errors = sensitiveFindings.map((f) => ({
629
+ path: "sensitive_content",
630
+ message: `${f.kind} at line ${f.line}: ${f.preview}`
631
+ }));
632
+ return {
633
+ exitCode: ExitCode.SENSITIVE_CONTENT_DETECTED,
634
+ result: ok({
635
+ schema: det.schema,
636
+ valid: false,
637
+ errors,
638
+ index_updated: false,
639
+ log_updated: false,
640
+ humanHint: `SENSITIVE CONTENT (${sensitiveFindings.length})
641
+ ${errors.map((e) => ` ${e.message}`).join("\n")}`,
642
+ sensitive_findings: sensitiveFindings
643
+ })
644
+ };
645
+ }
483
646
  if (!det.schema) {
484
647
  return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], index_updated: false, log_updated: false, humanHint: "schema not detected" }) };
485
648
  }
@@ -1159,8 +1322,8 @@ function simulateRemoval(adj, removed) {
1159
1322
  }
1160
1323
 
1161
1324
  // src/commands/audit.ts
1162
- import { readFile as readFile5, stat as stat2 } from "fs/promises";
1163
- import { dirname as dirname3, resolve as resolve2, join as join5 } from "path";
1325
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
1326
+ import { dirname as dirname3, resolve as resolve2, join as join6 } from "path";
1164
1327
 
1165
1328
  // src/parsers/citations.ts
1166
1329
  var FENCE2 = /```[\s\S]*?```/g;
@@ -1178,6 +1341,7 @@ function extractCitationMarkers(body) {
1178
1341
  const out = [];
1179
1342
  let m;
1180
1343
  while ((m = MARKER_RE.exec(stripped)) !== null) {
1344
+ if (m[1] === "raw/...") continue;
1181
1345
  out.push({ marker: m[0], target: m[1] });
1182
1346
  }
1183
1347
  return out;
@@ -1267,6 +1431,41 @@ function hasWikilinkCitations(body) {
1267
1431
  return /\[\[raw\/[^\]]+\]\]/.test(stripped);
1268
1432
  }
1269
1433
 
1434
+ // src/utils/raw-source.ts
1435
+ import { existsSync } from "fs";
1436
+ import { stat as stat2 } from "fs/promises";
1437
+ import { join as join5 } from "path";
1438
+ function normalizeRawSourceTarget(entry) {
1439
+ let target = entry.trim().replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
1440
+ target = target.replace(/^\^\[/, "").replace(/\]$/, "");
1441
+ if (!target.startsWith("raw/") && !target.startsWith("_archive/raw/")) return null;
1442
+ return target;
1443
+ }
1444
+ function rawSourceTargetCandidates(vault, target) {
1445
+ const normalized = normalizeRawSourceTarget(target);
1446
+ if (!normalized) return [];
1447
+ const candidates = [join5(vault, normalized)];
1448
+ if (!normalized.endsWith(".md")) candidates.push(join5(vault, `${normalized}.md`));
1449
+ if (normalized.startsWith("raw/")) {
1450
+ candidates.push(join5(vault, "_archive", normalized));
1451
+ if (!normalized.endsWith(".md")) candidates.push(join5(vault, "_archive", `${normalized}.md`));
1452
+ }
1453
+ return [...new Set(candidates)];
1454
+ }
1455
+ function rawSourceTargetExistsSync(vault, target) {
1456
+ return rawSourceTargetCandidates(vault, target).some((candidate) => existsSync(candidate));
1457
+ }
1458
+ async function rawSourceTargetExists(vault, target) {
1459
+ for (const candidate of rawSourceTargetCandidates(vault, target)) {
1460
+ try {
1461
+ await stat2(candidate);
1462
+ return true;
1463
+ } catch {
1464
+ }
1465
+ }
1466
+ return false;
1467
+ }
1468
+
1270
1469
  // src/commands/audit.ts
1271
1470
  async function runAudit(input) {
1272
1471
  let text;
@@ -1283,12 +1482,7 @@ async function runAudit(input) {
1283
1482
  if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
1284
1483
  const markers = extractCitationMarkers(body);
1285
1484
  const resolved = await Promise.all(markers.map(async (m) => {
1286
- try {
1287
- await stat2(join5(vault, m.target));
1288
- return { ...m, resolved: true };
1289
- } catch {
1290
- return { ...m, resolved: false };
1291
- }
1485
+ return { ...m, resolved: await rawSourceTargetExists(vault, m.target) };
1292
1486
  }));
1293
1487
  const sources = (fm.data.sources ?? []).map((s) => s.replace(/^\^\[/, "").replace(/\]$/, ""));
1294
1488
  const referenced = new Set(resolved.map((m) => m.target));
@@ -1329,7 +1523,7 @@ async function findVaultRoot(start) {
1329
1523
  let cur = start;
1330
1524
  for (let i = 0; i < 20; i++) {
1331
1525
  try {
1332
- await stat2(join5(cur, "SCHEMA.md"));
1526
+ await stat3(join6(cur, "SCHEMA.md"));
1333
1527
  return cur;
1334
1528
  } catch {
1335
1529
  }
@@ -1381,17 +1575,17 @@ async function validateCompoundReferences(vault) {
1381
1575
  }
1382
1576
 
1383
1577
  // src/commands/install.ts
1384
- import { readdir as readdir2, stat as stat4, symlink, unlink, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
1385
- import { join as join6, resolve as resolve3, dirname as dirname5 } from "path";
1578
+ import { readdir as readdir2, stat as stat5, symlink, unlink, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
1579
+ import { join as join7, resolve as resolve3, dirname as dirname5 } from "path";
1386
1580
 
1387
1581
  // src/utils/install-fs.ts
1388
- import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile4, stat as stat3 } from "fs/promises";
1582
+ import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile4, stat as stat4 } from "fs/promises";
1389
1583
  import { dirname as dirname4 } from "path";
1390
1584
  async function atomicCopyWithBackup(src, dst) {
1391
1585
  await mkdir3(dirname4(dst), { recursive: true });
1392
1586
  let backupPath = null;
1393
1587
  try {
1394
- await stat3(dst);
1588
+ await stat4(dst);
1395
1589
  backupPath = `${dst}.bak`;
1396
1590
  await copyFile(dst, backupPath);
1397
1591
  } catch {
@@ -1445,7 +1639,7 @@ async function runInstall(input) {
1445
1639
  const withSkill = [];
1446
1640
  for (const d of dirs) {
1447
1641
  try {
1448
- await stat4(join6(input.skillsRoot, d.name, "SKILL.md"));
1642
+ await stat5(join7(input.skillsRoot, d.name, "SKILL.md"));
1449
1643
  withSkill.push(d.name);
1450
1644
  } catch {
1451
1645
  }
@@ -1462,10 +1656,10 @@ async function runInstall(input) {
1462
1656
  const version_warnings = [];
1463
1657
  const skillMetas = {};
1464
1658
  for (const name of entries) {
1465
- const src = join6(input.skillsRoot, name, "SKILL.md");
1466
- const dst = join6(input.target, name, "SKILL.md");
1659
+ const src = join7(input.skillsRoot, name, "SKILL.md");
1660
+ const dst = join7(input.target, name, "SKILL.md");
1467
1661
  try {
1468
- await stat4(src);
1662
+ await stat5(src);
1469
1663
  } catch {
1470
1664
  return { exitCode: ExitCode.PREFLIGHT_FAILED, result: err("PREFLIGHT_FAILED", { missing: src }) };
1471
1665
  }
@@ -1504,10 +1698,10 @@ async function runInstall(input) {
1504
1698
  if (r.data.backupPath) backed_up.push(r.data.backupPath);
1505
1699
  }
1506
1700
  }
1507
- const binSrc = join6(input.skillsRoot, "bin", "skillwiki");
1701
+ const binSrc = join7(input.skillsRoot, "bin", "skillwiki");
1508
1702
  try {
1509
- await stat4(binSrc);
1510
- const binDst = join6(input.target, "bin", "skillwiki");
1703
+ await stat5(binSrc);
1704
+ const binDst = join7(input.target, "bin", "skillwiki");
1511
1705
  if (!input.dryRun) {
1512
1706
  if (input.symlink) {
1513
1707
  const r = await createSymlink(binSrc, binDst);
@@ -1524,7 +1718,7 @@ async function runInstall(input) {
1524
1718
  }
1525
1719
  } catch {
1526
1720
  }
1527
- const manifest_path = join6(input.target, "wiki-manifest.json");
1721
+ const manifest_path = join7(input.target, "wiki-manifest.json");
1528
1722
  if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up, symlink: input.symlink || void 0, skills: skillMetas });
1529
1723
  const mode = input.symlink ? "symlink (dev mode)" : "copy";
1530
1724
  const hintLines = [
@@ -1566,7 +1760,7 @@ async function runPath(input) {
1566
1760
  }
1567
1761
 
1568
1762
  // src/utils/lang.ts
1569
- import { join as join7 } from "path";
1763
+ import { join as join8 } from "path";
1570
1764
  var ALIASES = {
1571
1765
  english: "en",
1572
1766
  en: "en",
@@ -1589,7 +1783,7 @@ async function resolveLang(input) {
1589
1783
  if (input.envValue !== void 0 && input.envValue.length > 0) {
1590
1784
  return { value: input.envValue, source: "env", canonical: normalizeLang(input.envValue) };
1591
1785
  }
1592
- const dotenv = await parseDotenvFile(join7(input.home, ".skillwiki", ".env"));
1786
+ const dotenv = await parseDotenvFile(join8(input.home, ".skillwiki", ".env"));
1593
1787
  if (dotenv.WIKI_LANG !== void 0) {
1594
1788
  return { value: dotenv.WIKI_LANG, source: "skillwiki-dotenv", canonical: normalizeLang(dotenv.WIKI_LANG) };
1595
1789
  }
@@ -1597,7 +1791,7 @@ async function resolveLang(input) {
1597
1791
  }
1598
1792
 
1599
1793
  // src/commands/lang.ts
1600
- import { join as join8 } from "path";
1794
+ import { join as join9 } from "path";
1601
1795
  async function runLang(input) {
1602
1796
  const resolved = await resolveLang({ flag: input.flag, envValue: input.envValue, home: input.home });
1603
1797
  let chain;
@@ -1606,7 +1800,7 @@ async function runLang(input) {
1606
1800
  { source: "flag", matched: input.flag !== void 0 && input.flag.length > 0, value: input.flag },
1607
1801
  { source: "env", matched: input.envValue !== void 0 && input.envValue.length > 0, value: input.envValue }
1608
1802
  ];
1609
- const sw = await parseDotenvFile(join8(input.home, ".skillwiki", ".env"));
1803
+ const sw = await parseDotenvFile(join9(input.home, ".skillwiki", ".env"));
1610
1804
  chain.push({ source: "skillwiki-dotenv", matched: sw.WIKI_LANG !== void 0, value: sw.WIKI_LANG });
1611
1805
  chain.push({ source: "default", matched: resolved.source === "default", value: "en" });
1612
1806
  }
@@ -1624,7 +1818,7 @@ async function runLang(input) {
1624
1818
 
1625
1819
  // src/commands/init.ts
1626
1820
  import { mkdir as mkdir5, readFile as readFile7, readdir as readdir3, writeFile as writeFile5 } from "fs/promises";
1627
- import { join as join10 } from "path";
1821
+ import { join as join11 } from "path";
1628
1822
 
1629
1823
  // src/parsers/taxonomy.ts
1630
1824
  import yaml2 from "js-yaml";
@@ -1652,16 +1846,16 @@ function extractTaxonomy(schemaText) {
1652
1846
  }
1653
1847
 
1654
1848
  // src/utils/last-op.ts
1655
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
1656
- import { join as join9 } from "path";
1849
+ import { readFileSync as readFileSync2, writeFileSync, mkdirSync, unlinkSync, existsSync as existsSync2 } from "fs";
1850
+ import { join as join10 } from "path";
1657
1851
  var LAST_OP_DIR = ".skillwiki";
1658
1852
  var LAST_OP_FILE = "last-op.json";
1659
1853
  function lastOpPath(vault) {
1660
- return join9(vault, LAST_OP_DIR, LAST_OP_FILE);
1854
+ return join10(vault, LAST_OP_DIR, LAST_OP_FILE);
1661
1855
  }
1662
1856
  function readLastOp(vault) {
1663
1857
  const p = lastOpPath(vault);
1664
- if (!existsSync(p)) return [];
1858
+ if (!existsSync2(p)) return [];
1665
1859
  try {
1666
1860
  const raw = readFileSync2(p, "utf8");
1667
1861
  const parsed = JSON.parse(raw);
@@ -1681,8 +1875,8 @@ function readLastOp(vault) {
1681
1875
  function appendLastOp(vault, entry) {
1682
1876
  const existing = readLastOp(vault);
1683
1877
  existing.push(entry);
1684
- const dir = join9(vault, LAST_OP_DIR);
1685
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1878
+ const dir = join10(vault, LAST_OP_DIR);
1879
+ if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
1686
1880
  writeFileSync(lastOpPath(vault), JSON.stringify(existing, null, 2), "utf8");
1687
1881
  }
1688
1882
  function clearLastOp(vault) {
@@ -1735,13 +1929,13 @@ async function discoverTagsFromPages(target, knownSlugs) {
1735
1929
  for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
1736
1930
  let entries;
1737
1931
  try {
1738
- entries = (await readdir3(join10(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
1932
+ entries = (await readdir3(join11(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
1739
1933
  } catch {
1740
1934
  continue;
1741
1935
  }
1742
1936
  for (const file of entries) {
1743
1937
  try {
1744
- const text = await readFile7(join10(target, dir, file), "utf8");
1938
+ const text = await readFile7(join11(target, dir, file), "utf8");
1745
1939
  const fm = extractFrontmatter(text);
1746
1940
  if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
1747
1941
  for (const t of fm.data.tags) {
@@ -1760,7 +1954,7 @@ async function runInit(input) {
1760
1954
  const canonicalLang = langRes.canonical;
1761
1955
  let oldSchemaText;
1762
1956
  try {
1763
- oldSchemaText = await readFile7(join10(target, "SCHEMA.md"), "utf8");
1957
+ oldSchemaText = await readFile7(join11(target, "SCHEMA.md"), "utf8");
1764
1958
  } catch {
1765
1959
  }
1766
1960
  if (oldSchemaText && !input.force) {
@@ -1769,7 +1963,7 @@ async function runInit(input) {
1769
1963
  result: err("INIT_TARGET_NOT_EMPTY", { target })
1770
1964
  };
1771
1965
  }
1772
- const envPath = join10(input.home, ".skillwiki", ".env");
1966
+ const envPath = join11(input.home, ".skillwiki", ".env");
1773
1967
  let existingEnvRaw = "";
1774
1968
  try {
1775
1969
  existingEnvRaw = await readFile7(envPath, "utf8");
@@ -1795,7 +1989,7 @@ async function runInit(input) {
1795
1989
  try {
1796
1990
  await mkdir5(target, { recursive: true });
1797
1991
  for (const d of VAULT_DIRS) {
1798
- await mkdir5(join10(target, d), { recursive: true });
1992
+ await mkdir5(join11(target, d), { recursive: true });
1799
1993
  created.push(d + "/");
1800
1994
  }
1801
1995
  } catch (e) {
@@ -1824,9 +2018,9 @@ async function runInit(input) {
1824
2018
  const discovered_tags = discovered.length;
1825
2019
  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");
1826
2020
  try {
1827
- const schemaTpl = await readFile7(join10(input.templates, "SCHEMA.md"), "utf8");
2021
+ const schemaTpl = await readFile7(join11(input.templates, "SCHEMA.md"), "utf8");
1828
2022
  const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
1829
- await writeFile5(join10(target, "SCHEMA.md"), schema, "utf8");
2023
+ await writeFile5(join11(target, "SCHEMA.md"), schema, "utf8");
1830
2024
  created.push("SCHEMA.md");
1831
2025
  } catch (e) {
1832
2026
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
@@ -1834,7 +2028,7 @@ async function runInit(input) {
1834
2028
  const preserved = [];
1835
2029
  async function writeOrPreserve(fileName, render) {
1836
2030
  try {
1837
- const existing = await readFile7(join10(target, fileName), "utf8");
2031
+ const existing = await readFile7(join11(target, fileName), "utf8");
1838
2032
  if (existing.split("\n").length > 10) {
1839
2033
  preserved.push(fileName);
1840
2034
  return void 0;
@@ -1842,7 +2036,7 @@ async function runInit(input) {
1842
2036
  } catch {
1843
2037
  }
1844
2038
  try {
1845
- await writeFile5(join10(target, fileName), await render(), "utf8");
2039
+ await writeFile5(join11(target, fileName), await render(), "utf8");
1846
2040
  created.push(fileName);
1847
2041
  return void 0;
1848
2042
  } catch (e) {
@@ -1850,7 +2044,7 @@ async function runInit(input) {
1850
2044
  }
1851
2045
  }
1852
2046
  const err1 = await writeOrPreserve("index.md", async () => {
1853
- const tpl = await readFile7(join10(input.templates, "index.md"), "utf8");
2047
+ const tpl = await readFile7(join11(input.templates, "index.md"), "utf8");
1854
2048
  return tpl.replace("{{INIT_DATE}}", today);
1855
2049
  });
1856
2050
  if (err1) return err1;
@@ -1877,7 +2071,7 @@ async function runInit(input) {
1877
2071
  });
1878
2072
  if (errTemplate) return errTemplate;
1879
2073
  const err22 = await writeOrPreserve("log.md", async () => {
1880
- const tpl = await readFile7(join10(input.templates, "log.md"), "utf8");
2074
+ const tpl = await readFile7(join11(input.templates, "log.md"), "utf8");
1881
2075
  return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
1882
2076
  });
1883
2077
  if (err22) return err22;
@@ -1975,11 +2169,11 @@ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }
1975
2169
 
1976
2170
  // src/commands/tag-audit.ts
1977
2171
  import { readFile as readFile8 } from "fs/promises";
1978
- import { join as join11 } from "path";
2172
+ import { join as join12 } from "path";
1979
2173
  async function runTagAudit(input) {
1980
2174
  const scan = await scanVault(input.vault);
1981
2175
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1982
- const schemaText = await readFile8(join11(input.vault, "SCHEMA.md"), "utf8");
2176
+ const schemaText = await readFile8(join12(input.vault, "SCHEMA.md"), "utf8");
1983
2177
  const tax = extractTaxonomy(schemaText);
1984
2178
  if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
1985
2179
  const allowed = new Set(tax.data);
@@ -2004,13 +2198,13 @@ async function runTagAudit(input) {
2004
2198
 
2005
2199
  // src/commands/index-check.ts
2006
2200
  import { readFile as readFile9 } from "fs/promises";
2007
- import { join as join12 } from "path";
2201
+ import { join as join13 } from "path";
2008
2202
  async function runIndexCheck(input) {
2009
2203
  const scan = await scanVault(input.vault);
2010
2204
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2011
2205
  let indexText = "";
2012
2206
  try {
2013
- indexText = await readFile9(join12(input.vault, "index.md"), "utf8");
2207
+ indexText = await readFile9(join13(input.vault, "index.md"), "utf8");
2014
2208
  } catch {
2015
2209
  }
2016
2210
  const indexSlugsLower = /* @__PURE__ */ new Map();
@@ -2050,7 +2244,7 @@ async function runIndexCheck(input) {
2050
2244
 
2051
2245
  // src/commands/stale.ts
2052
2246
  import { readdir as readdir4, rename as rename2, mkdir as mkdir6, readFile as readFile10 } from "fs/promises";
2053
- import { join as join13 } from "path";
2247
+ import { join as join14 } from "path";
2054
2248
 
2055
2249
  // src/parsers/expiry-annotations.ts
2056
2250
  var HEADING_RE = /^#{1,6}\s+(.+)$/;
@@ -2098,7 +2292,7 @@ async function runStale(input) {
2098
2292
  const archived = [];
2099
2293
  const workDirs = /* @__PURE__ */ new Map();
2100
2294
  const workDirsBySlug = /* @__PURE__ */ new Map();
2101
- const projectsDir = join13(input.vault, "projects");
2295
+ const projectsDir = join14(input.vault, "projects");
2102
2296
  let projectSlugs = [];
2103
2297
  try {
2104
2298
  projectSlugs = (await readdir4(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
@@ -2111,7 +2305,7 @@ async function runStale(input) {
2111
2305
  projectSlugs = [input.project];
2112
2306
  }
2113
2307
  for (const slug of projectSlugs) {
2114
- const workPath = join13(projectsDir, slug, "work");
2308
+ const workPath = join14(projectsDir, slug, "work");
2115
2309
  let entries;
2116
2310
  try {
2117
2311
  entries = await readdir4(workPath, { withFileTypes: true });
@@ -2122,7 +2316,7 @@ async function runStale(input) {
2122
2316
  for (const e of entries) {
2123
2317
  if (!e.isDirectory()) continue;
2124
2318
  const relDir = `projects/${slug}/work/${e.name}`;
2125
- const absDir = join13(workPath, e.name);
2319
+ const absDir = join14(workPath, e.name);
2126
2320
  let status = "";
2127
2321
  let files;
2128
2322
  try {
@@ -2135,7 +2329,7 @@ async function runStale(input) {
2135
2329
  for (const f of files) {
2136
2330
  if (!f.endsWith(".md")) continue;
2137
2331
  try {
2138
- const fm = extractFrontmatter(await readFile10(join13(absDir, f), "utf8"));
2332
+ const fm = extractFrontmatter(await readFile10(join14(absDir, f), "utf8"));
2139
2333
  if (fm.ok && typeof fm.data.status === "string") {
2140
2334
  status = fm.data.status;
2141
2335
  break;
@@ -2159,7 +2353,7 @@ async function runStale(input) {
2159
2353
  const transcriptMeta = /* @__PURE__ */ new Map();
2160
2354
  for (const t of transcripts) {
2161
2355
  try {
2162
- const content = await readFile10(join13(input.vault, t.relPath), "utf8");
2356
+ const content = await readFile10(join14(input.vault, t.relPath), "utf8");
2163
2357
  const fm = extractFrontmatter(content);
2164
2358
  let kind = fm.ok && typeof fm.data.kind === "string" ? fm.data.kind : "";
2165
2359
  let project = fm.ok && typeof fm.data.project === "string" ? fm.data.project : "";
@@ -2227,7 +2421,7 @@ async function runStale(input) {
2227
2421
  }
2228
2422
  }
2229
2423
  for (const [relDir] of workDirs) {
2230
- const specPath = join13(input.vault, relDir, "spec.md");
2424
+ const specPath = join14(input.vault, relDir, "spec.md");
2231
2425
  try {
2232
2426
  const specContent = await readFile10(specPath, "utf8");
2233
2427
  const specFm = extractFrontmatter(specContent);
@@ -2258,7 +2452,7 @@ async function runStale(input) {
2258
2452
  if (daysSince(dateStr) < input.days) continue;
2259
2453
  let files;
2260
2454
  try {
2261
- files = await readdir4(join13(input.vault, relDir));
2455
+ files = await readdir4(join14(input.vault, relDir));
2262
2456
  } catch {
2263
2457
  continue;
2264
2458
  }
@@ -2274,7 +2468,7 @@ async function runStale(input) {
2274
2468
  const stale = [];
2275
2469
  for (const page of scan.data.typedKnowledge) {
2276
2470
  try {
2277
- const text = await readFile10(join13(input.vault, page.relPath), "utf8");
2471
+ const text = await readFile10(join14(input.vault, page.relPath), "utf8");
2278
2472
  const fm = extractFrontmatter(text);
2279
2473
  if (fm.ok && typeof fm.data.updated === "string") {
2280
2474
  if (input.project) {
@@ -2294,7 +2488,7 @@ async function runStale(input) {
2294
2488
  const staleSections = [];
2295
2489
  for (const page of scan.data.typedKnowledge) {
2296
2490
  try {
2297
- const text = await readFile10(join13(input.vault, page.relPath), "utf8");
2491
+ const text = await readFile10(join14(input.vault, page.relPath), "utf8");
2298
2492
  const projectFilter = input.project;
2299
2493
  if (projectFilter) {
2300
2494
  const fm = extractFrontmatter(text);
@@ -2323,11 +2517,11 @@ async function runStale(input) {
2323
2517
  }
2324
2518
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2325
2519
  if (input.archive) {
2326
- const archiveDir = join13(input.vault, "_archive", today);
2520
+ const archiveDir = join14(input.vault, "_archive", today);
2327
2521
  await mkdir6(archiveDir, { recursive: true });
2328
2522
  const citedRawPaths = /* @__PURE__ */ new Set();
2329
2523
  for (const page of scan.data.typedKnowledge) {
2330
- const text = await readFile10(join13(input.vault, page.relPath), "utf8").catch(() => "");
2524
+ const text = await readFile10(join14(input.vault, page.relPath), "utf8").catch(() => "");
2331
2525
  for (const line of text.split("\n")) {
2332
2526
  for (const m of line.matchAll(/\^\[(raw\/[^\]]+)\]/g)) {
2333
2527
  citedRawPaths.add(m[1]);
@@ -2339,9 +2533,9 @@ async function runStale(input) {
2339
2533
  }
2340
2534
  for (const t of staleTranscripts) {
2341
2535
  if (citedRawPaths.has(t.path) || citedRawPaths.has(t.path.replace(/\.md$/, ""))) continue;
2342
- const dest = join13(archiveDir, t.path.split("/").pop());
2536
+ const dest = join14(archiveDir, t.path.split("/").pop());
2343
2537
  try {
2344
- await rename2(join13(input.vault, t.path), dest);
2538
+ await rename2(join14(input.vault, t.path), dest);
2345
2539
  archived.push(t.path);
2346
2540
  } catch {
2347
2541
  }
@@ -2351,18 +2545,18 @@ async function runStale(input) {
2351
2545
  if (parts.length >= 4 && parts[0] === "projects") {
2352
2546
  const slug = parts[1];
2353
2547
  const itemName = parts[3];
2354
- const histDir = join13(input.vault, "projects", slug, "history", "archived-work");
2548
+ const histDir = join14(input.vault, "projects", slug, "history", "archived-work");
2355
2549
  await mkdir6(histDir, { recursive: true });
2356
- const dest = join13(histDir, itemName);
2550
+ const dest = join14(histDir, itemName);
2357
2551
  try {
2358
- await rename2(join13(input.vault, w.path), dest);
2552
+ await rename2(join14(input.vault, w.path), dest);
2359
2553
  archived.push(w.path);
2360
2554
  } catch {
2361
2555
  }
2362
2556
  } else {
2363
- const dest = join13(archiveDir, w.path.replace(/\//g, "_"));
2557
+ const dest = join14(archiveDir, w.path.replace(/\//g, "_"));
2364
2558
  try {
2365
- await rename2(join13(input.vault, w.path), dest);
2559
+ await rename2(join14(input.vault, w.path), dest);
2366
2560
  archived.push(w.path);
2367
2561
  } catch {
2368
2562
  }
@@ -2402,8 +2596,8 @@ async function runStale(input) {
2402
2596
 
2403
2597
  // src/commands/claim.ts
2404
2598
  import { mkdir as mkdir7, writeFile as writeFile6, readFile as readFile11 } from "fs/promises";
2405
- import { existsSync as existsSync2, statSync } from "fs";
2406
- import { join as join14 } from "path";
2599
+ import { existsSync as existsSync3, statSync } from "fs";
2600
+ import { join as join15 } from "path";
2407
2601
  function extractDate(filename) {
2408
2602
  const m = filename.match(/^(\d{4}-\d{2}-\d{2})/);
2409
2603
  return m?.[1] ?? "";
@@ -2415,11 +2609,11 @@ function extractProjectSlug(projectField) {
2415
2609
  return projectField.replace(/^\[\[/, "").replace(/\]\]$/, "").replace(/^"|"$/g, "");
2416
2610
  }
2417
2611
  async function runClaim(input) {
2418
- if (!existsSync2(input.vault) || !statSync(input.vault).isDirectory()) {
2612
+ if (!existsSync3(input.vault) || !statSync(input.vault).isDirectory()) {
2419
2613
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { path: input.vault }) };
2420
2614
  }
2421
- const absTranscript = join14(input.vault, input.transcript);
2422
- if (!existsSync2(absTranscript)) {
2615
+ const absTranscript = join15(input.vault, input.transcript);
2616
+ if (!existsSync3(absTranscript)) {
2423
2617
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.transcript }) };
2424
2618
  }
2425
2619
  const content = await readFile11(absTranscript, "utf8");
@@ -2434,8 +2628,8 @@ async function runClaim(input) {
2434
2628
  result: err("SCHEME_REJECTED", { message: "No project specified. Use --project or set project in transcript frontmatter." })
2435
2629
  };
2436
2630
  }
2437
- const projectDir = join14(input.vault, "projects", projectSlug);
2438
- if (!existsSync2(projectDir) || !statSync(projectDir).isDirectory()) {
2631
+ const projectDir = join15(input.vault, "projects", projectSlug);
2632
+ if (!existsSync3(projectDir) || !statSync(projectDir).isDirectory()) {
2439
2633
  return {
2440
2634
  exitCode: ExitCode.PROJECT_NOT_FOUND,
2441
2635
  result: err("PROJECT_NOT_FOUND", { project: projectSlug })
@@ -2451,10 +2645,10 @@ async function runClaim(input) {
2451
2645
  }
2452
2646
  const workSlug = input.slug || extractSlugFromFilename(filename);
2453
2647
  const dirName = `${date}-${workSlug}`;
2454
- const workDir = join14(projectDir, "work", dirName);
2648
+ const workDir = join15(projectDir, "work", dirName);
2455
2649
  const relWorkDir = `projects/${projectSlug}/work/${dirName}`;
2456
2650
  const relSpecPath = `${relWorkDir}/spec.md`;
2457
- if (existsSync2(workDir)) {
2651
+ if (existsSync3(workDir)) {
2458
2652
  return {
2459
2653
  exitCode: ExitCode.OK,
2460
2654
  result: ok({
@@ -2480,7 +2674,7 @@ async function runClaim(input) {
2480
2674
  `Claimed from ${input.transcript}`,
2481
2675
  ""
2482
2676
  ];
2483
- await writeFile6(join14(workDir, "spec.md"), specLines.join("\n"), "utf8");
2677
+ await writeFile6(join15(workDir, "spec.md"), specLines.join("\n"), "utf8");
2484
2678
  appendLastOp(input.vault, {
2485
2679
  operation: "claim",
2486
2680
  summary: `claimed ${input.transcript} \u2192 ${relWorkDir}`,
@@ -2515,16 +2709,16 @@ async function runPagesize(input) {
2515
2709
  }
2516
2710
 
2517
2711
  // src/commands/log-rotate.ts
2518
- import { readFile as readFile12, rename as rename3, writeFile as writeFile7, stat as stat5 } from "fs/promises";
2519
- import { join as join15 } from "path";
2712
+ import { readFile as readFile12, rename as rename3, writeFile as writeFile7, stat as stat6 } from "fs/promises";
2713
+ import { join as join16 } from "path";
2520
2714
  var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
2521
2715
  async function runLogRotate(input) {
2522
2716
  try {
2523
- await stat5(join15(input.vault, "SCHEMA.md"));
2717
+ await stat6(join16(input.vault, "SCHEMA.md"));
2524
2718
  } catch {
2525
2719
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
2526
2720
  }
2527
- const logPath = join15(input.vault, "log.md");
2721
+ const logPath = join16(input.vault, "log.md");
2528
2722
  let logText;
2529
2723
  try {
2530
2724
  logText = await readFile12(logPath, "utf8");
@@ -2544,7 +2738,7 @@ async function runLogRotate(input) {
2544
2738
  }
2545
2739
  const newestYear = matches[matches.length - 1][1];
2546
2740
  const rotatedName = `log-${newestYear}.md`;
2547
- const rotatedPath = join15(input.vault, rotatedName);
2741
+ const rotatedPath = join16(input.vault, rotatedName);
2548
2742
  try {
2549
2743
  await rename3(logPath, rotatedPath);
2550
2744
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
@@ -2570,14 +2764,14 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
2570
2764
  }
2571
2765
 
2572
2766
  // src/commands/log-append.ts
2573
- import { readFile as readFile13, rename as rename4, writeFile as writeFile8, stat as stat6 } from "fs/promises";
2574
- import { join as join17 } from "path";
2767
+ import { readFile as readFile13, rename as rename4, writeFile as writeFile8, stat as stat7 } from "fs/promises";
2768
+ import { join as join18 } from "path";
2575
2769
 
2576
2770
  // src/utils/log-lock.ts
2577
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
2578
- import { join as join16 } from "path";
2771
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
2772
+ import { join as join17 } from "path";
2579
2773
  function logLockPath(vault) {
2580
- return join16(vault, ".skillwiki", "log-append.lock");
2774
+ return join17(vault, ".skillwiki", "log-append.lock");
2581
2775
  }
2582
2776
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2583
2777
  async function acquireLogLock(vault, opts = {}) {
@@ -2585,8 +2779,8 @@ async function acquireLogLock(vault, opts = {}) {
2585
2779
  const pollMs = opts.pollMs ?? 50;
2586
2780
  const staleMs = opts.staleMs ?? 1e4;
2587
2781
  const path = logLockPath(vault);
2588
- const dir = join16(vault, ".skillwiki");
2589
- if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
2782
+ const dir = join17(vault, ".skillwiki");
2783
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
2590
2784
  const deadline = Date.now() + retryMs;
2591
2785
  const content = JSON.stringify({ pid: process.pid, acquired: (/* @__PURE__ */ new Date()).toISOString() }) + "\n";
2592
2786
  for (; ; ) {
@@ -2621,7 +2815,7 @@ function releaseLogLock(vault) {
2621
2815
  var ENTRY_RE2 = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
2622
2816
  async function runLogAppend(input) {
2623
2817
  try {
2624
- await stat6(join17(input.vault, "SCHEMA.md"));
2818
+ await stat7(join18(input.vault, "SCHEMA.md"));
2625
2819
  } catch {
2626
2820
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
2627
2821
  }
@@ -2633,7 +2827,7 @@ async function runLogAppend(input) {
2633
2827
  if (!acquired.ok) {
2634
2828
  return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
2635
2829
  }
2636
- const logPath = join17(input.vault, "log.md");
2830
+ const logPath = join18(input.vault, "log.md");
2637
2831
  try {
2638
2832
  let logText;
2639
2833
  try {
@@ -2671,9 +2865,10 @@ ${content}
2671
2865
  }
2672
2866
 
2673
2867
  // src/commands/lint.ts
2674
- import { existsSync as existsSync5 } from "fs";
2868
+ import { existsSync as existsSync6 } from "fs";
2675
2869
  import { readFile as readFile17 } from "fs/promises";
2676
- import { join as join22 } from "path";
2870
+ import { createHash as createHash4 } from "crypto";
2871
+ import { join as join23 } from "path";
2677
2872
 
2678
2873
  // src/commands/sparse-community.ts
2679
2874
  async function runSparseCommunity(input) {
@@ -2709,12 +2904,12 @@ async function runTopicMapCheck(input) {
2709
2904
 
2710
2905
  // src/commands/index-link-format.ts
2711
2906
  import { readFile as readFile14 } from "fs/promises";
2712
- import { join as join18 } from "path";
2907
+ import { join as join19 } from "path";
2713
2908
  var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
2714
2909
  async function runIndexLinkFormat(input) {
2715
2910
  let text = "";
2716
2911
  try {
2717
- text = await readFile14(join18(input.vault, "index.md"), "utf8");
2912
+ text = await readFile14(join19(input.vault, "index.md"), "utf8");
2718
2913
  } catch {
2719
2914
  }
2720
2915
  const markdown_links = [];
@@ -2728,7 +2923,7 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
2728
2923
 
2729
2924
  // src/commands/dedup.ts
2730
2925
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
2731
- import { join as join19 } from "path";
2926
+ import { join as join20 } from "path";
2732
2927
  async function runDedup(input) {
2733
2928
  const scan = await scanVault(input.vault);
2734
2929
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -2756,7 +2951,7 @@ async function runDedup(input) {
2756
2951
  }
2757
2952
  }
2758
2953
  for (const page of scan.data.typedKnowledge) {
2759
- const text = readFileSync3(join19(input.vault, page.relPath), "utf-8");
2954
+ const text = readFileSync3(join20(input.vault, page.relPath), "utf-8");
2760
2955
  let updated = text;
2761
2956
  let changed = false;
2762
2957
  for (const [oldPath, newPath] of replacements) {
@@ -2774,12 +2969,12 @@ async function runDedup(input) {
2774
2969
  }
2775
2970
  }
2776
2971
  if (changed) {
2777
- writeFileSync3(join19(input.vault, page.relPath), updated);
2972
+ writeFileSync3(join20(input.vault, page.relPath), updated);
2778
2973
  rewired.push(page.relPath);
2779
2974
  }
2780
2975
  }
2781
2976
  for (const [oldPath] of replacements) {
2782
- const fullPath = join19(input.vault, oldPath);
2977
+ const fullPath = join20(input.vault, oldPath);
2783
2978
  try {
2784
2979
  unlinkSync3(fullPath);
2785
2980
  removed.push(oldPath);
@@ -2816,7 +3011,7 @@ async function runDedup(input) {
2816
3011
  // src/utils/safe-write.ts
2817
3012
  import { open, readFile as readFile15, rename as rename5, unlink as unlink2, writeFile as writeFile9 } from "fs/promises";
2818
3013
  import { randomBytes } from "crypto";
2819
- import { dirname as dirname7, basename, join as join20 } from "path";
3014
+ import { dirname as dirname7, basename, join as join21 } from "path";
2820
3015
  var DEFAULT_MIN_BODY_RATIO = 0.5;
2821
3016
  var DEFAULT_MIN_OLD_BODY_BYTES = 200;
2822
3017
  function bodyBytes(text) {
@@ -2865,7 +3060,7 @@ async function safeWritePage(absPath, newContent, opts = {}) {
2865
3060
  }
2866
3061
  const dir = dirname7(absPath);
2867
3062
  const tmpName = `.${basename(absPath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
2868
- const tmpPath = join20(dir, tmpName);
3063
+ const tmpPath = join21(dir, tmpName);
2869
3064
  try {
2870
3065
  const handle = await open(tmpPath, "w");
2871
3066
  try {
@@ -2889,7 +3084,7 @@ async function safeWritePage(absPath, newContent, opts = {}) {
2889
3084
  }
2890
3085
 
2891
3086
  // src/commands/raw-body-dedup.ts
2892
- import { createHash as createHash2 } from "crypto";
3087
+ import { createHash as createHash3 } from "crypto";
2893
3088
  async function runRawBodyDedup(vault) {
2894
3089
  const scan = await scanVault(vault);
2895
3090
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -2900,7 +3095,7 @@ async function runRawBodyDedup(vault) {
2900
3095
  const split = splitFrontmatter(text);
2901
3096
  if (!split.ok) continue;
2902
3097
  totalFiles++;
2903
- const bodyHash = createHash2("sha256").update(split.data.body).digest("hex");
3098
+ const bodyHash = createHash3("sha256").update(split.data.body).digest("hex");
2904
3099
  const fm = extractFrontmatter(text);
2905
3100
  let fmSha256 = null;
2906
3101
  if (fm.ok && typeof fm.data.sha256 === "string" && fm.data.sha256.length === 64) {
@@ -2929,9 +3124,9 @@ async function runRawBodyDedup(vault) {
2929
3124
  }
2930
3125
 
2931
3126
  // src/commands/path-too-long.ts
2932
- import { existsSync as existsSync4 } from "fs";
3127
+ import { existsSync as existsSync5 } from "fs";
2933
3128
  import { mkdir as mkdir8, readFile as readFile16, rename as rename6, unlink as unlink3 } from "fs/promises";
2934
- import { dirname as dirname8, join as join21, posix, resolve as resolve4 } from "path";
3129
+ import { dirname as dirname8, join as join22, posix, resolve as resolve4 } from "path";
2935
3130
  var MAX_PATH_LENGTH = 240;
2936
3131
  var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
2937
3132
  async function runPathTooLong(input) {
@@ -2964,10 +3159,10 @@ async function fixPathTooLong(input) {
2964
3159
  }
2965
3160
  try {
2966
3161
  if (target.mode === "dedupe") {
2967
- await unlink3(join21(input.vault, violation.relPath));
3162
+ await unlink3(join22(input.vault, violation.relPath));
2968
3163
  } else {
2969
- await mkdir8(dirname8(join21(input.vault, target.relPath)), { recursive: true });
2970
- await rename6(join21(input.vault, violation.relPath), join21(input.vault, target.relPath));
3164
+ await mkdir8(dirname8(join22(input.vault, target.relPath)), { recursive: true });
3165
+ await rename6(join22(input.vault, violation.relPath), join22(input.vault, target.relPath));
2971
3166
  }
2972
3167
  fixed.push({ from: violation.relPath, to: target.relPath });
2973
3168
  } catch {
@@ -3044,9 +3239,9 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
3044
3239
  async function resolveFixTarget(vault, original, preferred, maxLength) {
3045
3240
  for (const candidate of candidateRelPaths(preferred, maxLength)) {
3046
3241
  if (candidate === original || candidate.length > maxLength) continue;
3047
- const candidatePath = join21(vault, candidate);
3048
- if (!existsSync4(candidatePath)) return { relPath: candidate, mode: "rename" };
3049
- if (await hasSameContent(join21(vault, original), candidatePath)) {
3242
+ const candidatePath = join22(vault, candidate);
3243
+ if (!existsSync5(candidatePath)) return { relPath: candidate, mode: "rename" };
3244
+ if (await hasSameContent(join22(vault, original), candidatePath)) {
3050
3245
  return { relPath: candidate, mode: "dedupe" };
3051
3246
  }
3052
3247
  }
@@ -3356,7 +3551,7 @@ function extractSourceEntries(rawFm) {
3356
3551
  }
3357
3552
  return entries;
3358
3553
  }
3359
- var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_source_identity_conflict", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
3554
+ var ERROR_ORDER = ["sensitive_content", "broken_wikilinks", "invalid_frontmatter", "raw_source_identity_conflict", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
3360
3555
  var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "file_source_url", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
3361
3556
  var INFO_ORDER = ["bridges", "sparse_community", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
3362
3557
  var KNOWN_BUCKETS = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
@@ -3385,6 +3580,26 @@ function summarizeBucket(bucket, severity, vaultPath, examplesLimit) {
3385
3580
  details_command: `skillwiki lint ${shellQuote(vaultPath)} --only ${bucket.kind}`
3386
3581
  };
3387
3582
  }
3583
+ function recomputeRawSha256IfPresent(content) {
3584
+ const split = splitFrontmatter(content);
3585
+ if (!split.ok) return content;
3586
+ if (!/^sha256:\s*[0-9a-f]{64}$/m.test(split.data.rawFrontmatter)) return content;
3587
+ const sha256 = createHash4("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
3588
+ const rawFrontmatter = split.data.rawFrontmatter.replace(/^sha256:\s*[0-9a-f]{64}$/m, `sha256: ${sha256}`);
3589
+ return `---
3590
+ ${rawFrontmatter}
3591
+ ---
3592
+ ${split.data.body}`;
3593
+ }
3594
+ function appendLintFixLastOp(vault, fixed) {
3595
+ if (fixed.length === 0) return;
3596
+ appendLastOp(vault, {
3597
+ operation: "lint-fix",
3598
+ summary: `fixed ${fixed.length} page(s)`,
3599
+ files: fixed,
3600
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3601
+ });
3602
+ }
3388
3603
  function summarizeLintOutput(output, examplesLimit = 3) {
3389
3604
  const buckets = [
3390
3605
  ...output.by_severity.error.map((bucket) => summarizeBucket(bucket, "error", output.vault.path, examplesLimit)),
@@ -3486,6 +3701,16 @@ async function runLint(input) {
3486
3701
  const allPages = scan.ok ? [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound] : [];
3487
3702
  const slugs = scan.ok ? buildSlugMap(allPages) : /* @__PURE__ */ new Map();
3488
3703
  if (scan.ok) {
3704
+ const sensitiveFlags = [];
3705
+ for (const page of scan.data.allMarkdown) {
3706
+ try {
3707
+ const text = await readPage(page);
3708
+ const findings = scanSensitiveContent(text, { file: page.relPath });
3709
+ sensitiveFlags.push(...findings);
3710
+ } catch {
3711
+ }
3712
+ }
3713
+ if (sensitiveFlags.length > 0) buckets.sensitive_content = sensitiveFlags;
3489
3714
  const subDirDupes = [];
3490
3715
  const flatStems = /* @__PURE__ */ new Map();
3491
3716
  const deepFiles = [];
@@ -3543,7 +3768,7 @@ async function runLint(input) {
3543
3768
  const noOverview = [];
3544
3769
  const fmWikilinkFlags = [];
3545
3770
  const wikilinkCitationFlags = [];
3546
- const brokenSourceFlags = [];
3771
+ const brokenSourceFlags = /* @__PURE__ */ new Set();
3547
3772
  const missingTldrFlags = [];
3548
3773
  const missingDiagramFlags = [];
3549
3774
  for (const page of scan.data.typedKnowledge) {
@@ -3558,11 +3783,15 @@ async function runLint(input) {
3558
3783
  if (hasWikilinkCitations(body)) wikilinkCitationFlags.push(page.relPath);
3559
3784
  const sourcesEntries = extractSourceEntries(rawFm);
3560
3785
  for (const entry of sourcesEntries) {
3561
- let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
3562
- rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
3563
- if (!rawPath.startsWith("raw/") && !rawPath.startsWith("_archive/raw/")) continue;
3564
- if (!existsSync5(join22(input.vault, rawPath)) && !existsSync5(join22(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync5(join22(input.vault, "_archive", rawPath)) && !existsSync5(join22(input.vault, "_archive", rawPath + ".md"))) {
3565
- brokenSourceFlags.push(`${page.relPath}: ${rawPath}`);
3786
+ const rawPath = normalizeRawSourceTarget(entry);
3787
+ if (!rawPath) continue;
3788
+ if (!rawSourceTargetExistsSync(input.vault, rawPath)) {
3789
+ brokenSourceFlags.add(`${page.relPath}: ${rawPath}`);
3790
+ }
3791
+ }
3792
+ for (const marker of extractCitationMarkers(body)) {
3793
+ if (!rawSourceTargetExistsSync(input.vault, marker.target)) {
3794
+ brokenSourceFlags.add(`${page.relPath}: ${marker.target}`);
3566
3795
  }
3567
3796
  }
3568
3797
  const fmLinks = rawFm.match(/\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g) ?? [];
@@ -3601,7 +3830,7 @@ async function runLint(input) {
3601
3830
  if (noOverview.length > 0) buckets.missing_overview = noOverview;
3602
3831
  if (fmWikilinkFlags.length > 0) buckets.frontmatter_wikilink = fmWikilinkFlags;
3603
3832
  if (wikilinkCitationFlags.length > 0) buckets.wikilink_citation = wikilinkCitationFlags;
3604
- if (brokenSourceFlags.length > 0) buckets.broken_sources = brokenSourceFlags;
3833
+ if (brokenSourceFlags.size > 0) buckets.broken_sources = [...brokenSourceFlags];
3605
3834
  if (missingTldrFlags.length > 0) buckets.missing_tldr = missingTldrFlags;
3606
3835
  if (missingDiagramFlags.length > 0) buckets.missing_diagram = missingDiagramFlags;
3607
3836
  const workItemHealth = [];
@@ -3652,8 +3881,8 @@ async function runLint(input) {
3652
3881
  const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
3653
3882
  if (!slugMatch) continue;
3654
3883
  const slug = slugMatch[1];
3655
- const knowledgePath = join22(input.vault, "projects", slug, "knowledge.md");
3656
- if (!existsSync5(knowledgePath)) continue;
3884
+ const knowledgePath = join23(input.vault, "projects", slug, "knowledge.md");
3885
+ if (!existsSync6(knowledgePath)) continue;
3657
3886
  const pageRef = page.relPath.replace(/\.md$/, "");
3658
3887
  try {
3659
3888
  const knowledgeContent = await readFile17(knowledgePath, "utf8");
@@ -3697,6 +3926,36 @@ async function runLint(input) {
3697
3926
  }
3698
3927
  }
3699
3928
  if (staleSectionFlags.length > 0) buckets.stale_sections = staleSectionFlags;
3929
+ if (shouldFix("sensitive_content") && buckets.sensitive_content) {
3930
+ const sensitiveFixed = [];
3931
+ for (const page of scan.data.allMarkdown) {
3932
+ try {
3933
+ const raw = await readPage(page);
3934
+ const redacted = redactSensitiveContent(raw, { file: page.relPath });
3935
+ if (!redacted.changed) continue;
3936
+ const next = recomputeRawSha256IfPresent(redacted.text);
3937
+ const w = await safeWritePage(page.absPath, next, { minBodyRatio: null });
3938
+ if (!w.ok) {
3939
+ unresolved.push(page.relPath);
3940
+ continue;
3941
+ }
3942
+ sensitiveFixed.push(page.relPath);
3943
+ } catch {
3944
+ unresolved.push(page.relPath);
3945
+ }
3946
+ }
3947
+ fixed.push(...sensitiveFixed);
3948
+ const remainingSensitiveFlags = [];
3949
+ for (const page of scan.data.allMarkdown) {
3950
+ try {
3951
+ const text = await readPage(page);
3952
+ remainingSensitiveFlags.push(...scanSensitiveContent(text, { file: page.relPath }));
3953
+ } catch {
3954
+ }
3955
+ }
3956
+ if (remainingSensitiveFlags.length > 0) buckets.sensitive_content = remainingSensitiveFlags;
3957
+ else delete buckets.sensitive_content;
3958
+ }
3700
3959
  if (shouldFix("legacy_citation_style") && legacyPages.length > 0) {
3701
3960
  const FENCE_RE2 = /```[\s\S]*?```/g;
3702
3961
  const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
@@ -4053,6 +4312,7 @@ ${newBody}`;
4053
4312
  humanHint: `--only ${input.only}
4054
4313
  ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")}`
4055
4314
  };
4315
+ if (input.fix) appendLintFixLastOp(input.vault, fixed);
4056
4316
  return {
4057
4317
  exitCode: fExit,
4058
4318
  result: ok(input.summary ? summarizeLintOutput(output2, input.examplesLimit) : output2)
@@ -4075,14 +4335,7 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
4075
4335
  hintLines.push(` ${b.kind}: ${b.items.length}`);
4076
4336
  }
4077
4337
  if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
4078
- if (input.fix && fixed.length > 0) {
4079
- appendLastOp(input.vault, {
4080
- operation: "lint-fix",
4081
- summary: `fixed ${fixed.length} page(s)`,
4082
- files: fixed,
4083
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4084
- });
4085
- }
4338
+ if (input.fix) appendLintFixLastOp(input.vault, fixed);
4086
4339
  const output = {
4087
4340
  vault: { path: input.vault, source: input.source ?? "resolved" },
4088
4341
  summary,
@@ -4098,25 +4351,25 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
4098
4351
  }
4099
4352
 
4100
4353
  // src/commands/health.ts
4101
- import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync, writeFileSync as writeFileSync6 } from "fs";
4102
- import { dirname as dirname10, join as join28, resolve as resolve6 } from "path";
4354
+ import { existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync, writeFileSync as writeFileSync6 } from "fs";
4355
+ import { dirname as dirname10, join as join30, resolve as resolve6 } from "path";
4103
4356
  import { platform as platform3 } from "os";
4104
4357
 
4105
4358
  // src/commands/doctor.ts
4106
- import { existsSync as existsSync10, lstatSync, readlinkSync, readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
4107
- import { join as join27, resolve as resolve5 } from "path";
4359
+ import { existsSync as existsSync11, lstatSync, readlinkSync, readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
4360
+ import { join as join29, resolve as resolve5 } from "path";
4108
4361
  import { execSync as execSync2 } from "child_process";
4109
4362
  import { platform as platform2 } from "os";
4110
4363
 
4111
4364
  // src/commands/config.ts
4112
4365
  import { readFile as readFile18 } from "fs/promises";
4113
- import { existsSync as existsSync6 } from "fs";
4114
- import { join as join23 } from "path";
4366
+ import { existsSync as existsSync7 } from "fs";
4367
+ import { join as join24 } from "path";
4115
4368
  function validateKey(key) {
4116
4369
  return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
4117
4370
  }
4118
4371
  function configPath(home) {
4119
- return join23(home, ".skillwiki", ".env");
4372
+ return join24(home, ".skillwiki", ".env");
4120
4373
  }
4121
4374
  async function runConfigGet(input) {
4122
4375
  if (!validateKey(input.key)) {
@@ -4166,15 +4419,15 @@ async function runConfigList(input) {
4166
4419
  }
4167
4420
  async function runConfigPath(input) {
4168
4421
  const filePath = configPath(input.home);
4169
- return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync6(filePath), humanHint: filePath }) };
4422
+ return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync7(filePath), humanHint: filePath }) };
4170
4423
  }
4171
4424
 
4172
4425
  // src/utils/auto-update.ts
4173
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
4174
- import { join as join24, dirname as dirname9 } from "path";
4426
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
4427
+ import { join as join25, dirname as dirname9 } from "path";
4175
4428
  import { spawn } from "child_process";
4176
4429
  function cachePath(home) {
4177
- return join24(home, ".skillwiki", CACHE_FILENAME);
4430
+ return join25(home, ".skillwiki", CACHE_FILENAME);
4178
4431
  }
4179
4432
  function readCacheRaw(home) {
4180
4433
  try {
@@ -4218,7 +4471,7 @@ function triggerAutoUpdate(home, currentVersion) {
4218
4471
  if (!isStale2) return;
4219
4472
  const distTag = distTagFromCache(home);
4220
4473
  const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
4221
- if (!existsSync7(bgScript)) return;
4474
+ if (!existsSync8(bgScript)) return;
4222
4475
  const child = spawn(process.execPath, [bgScript, home, currentVersion, distTag], {
4223
4476
  detached: true,
4224
4477
  stdio: "ignore"
@@ -4229,14 +4482,14 @@ function triggerAutoUpdate(home, currentVersion) {
4229
4482
  }
4230
4483
 
4231
4484
  // src/utils/plugin-registry.ts
4232
- import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync5 } from "fs";
4233
- import { join as join25 } from "path";
4234
- var REGISTRY_PATH = join25(".claude", "plugins", "installed_plugins.json");
4235
- var CODEX_CONFIG_PATH = join25(".codex", "config.toml");
4485
+ import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync5 } from "fs";
4486
+ import { join as join26 } from "path";
4487
+ var REGISTRY_PATH = join26(".claude", "plugins", "installed_plugins.json");
4488
+ var CODEX_CONFIG_PATH = join26(".codex", "config.toml");
4236
4489
  var PLUGIN_KEY = "skillwiki@llm-wiki";
4237
4490
  function readInstalledPlugins(home) {
4238
4491
  try {
4239
- const raw = readFileSync5(join25(home, REGISTRY_PATH), "utf8");
4492
+ const raw = readFileSync5(join26(home, REGISTRY_PATH), "utf8");
4240
4493
  return JSON.parse(raw);
4241
4494
  } catch {
4242
4495
  return null;
@@ -4272,8 +4525,8 @@ function findPluginInstallations(home, key = PLUGIN_KEY) {
4272
4525
  function findCodexPlugin(home, key, pluginName, marketplace) {
4273
4526
  const config = readCodexPluginConfig(home, key, marketplace);
4274
4527
  if (!config?.enabled) return null;
4275
- const cacheRoot = join25(home, ".codex", "plugins", "cache", marketplace, pluginName);
4276
- if (!existsSync8(cacheRoot)) return null;
4528
+ const cacheRoot = join26(home, ".codex", "plugins", "cache", marketplace, pluginName);
4529
+ if (!existsSync9(cacheRoot)) return null;
4277
4530
  let versions;
4278
4531
  try {
4279
4532
  versions = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -4288,7 +4541,7 @@ function findCodexPlugin(home, key, pluginName, marketplace) {
4288
4541
  key,
4289
4542
  pluginName,
4290
4543
  marketplace,
4291
- installPath: join25(cacheRoot, version),
4544
+ installPath: join26(cacheRoot, version),
4292
4545
  version,
4293
4546
  sourceType: config.sourceType,
4294
4547
  source: config.source
@@ -4305,7 +4558,7 @@ function parsePluginKey(key) {
4305
4558
  function readCodexPluginConfig(home, key, marketplace) {
4306
4559
  let raw;
4307
4560
  try {
4308
- raw = readFileSync5(join25(home, CODEX_CONFIG_PATH), "utf8");
4561
+ raw = readFileSync5(join26(home, CODEX_CONFIG_PATH), "utf8");
4309
4562
  } catch {
4310
4563
  return null;
4311
4564
  }
@@ -4346,90 +4599,490 @@ function parseTomlScalar(rawValue) {
4346
4599
  return value;
4347
4600
  }
4348
4601
 
4349
- // src/utils/s3-mount-health.ts
4350
- import { execSync } from "child_process";
4351
- import { platform } from "os";
4352
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readFileSync as readFile19 } from "fs";
4353
- import { join as join26 } from "path";
4354
- var OS = platform();
4355
- function findRcloneMountPid() {
4356
- try {
4357
- const out = execSync("pgrep -f 'rclone.*mount'", {
4358
- encoding: "utf8",
4359
- timeout: 2e3,
4360
- stdio: ["pipe", "pipe", "pipe"]
4361
- }).trim();
4362
- const pids = out.split("\n").filter(Boolean);
4363
- if (pids.length === 0) return null;
4364
- return parseInt(pids[0], 10);
4365
- } catch {
4366
- try {
4367
- const out = execSync("ps aux", { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] });
4368
- for (const line of out.split("\n")) {
4369
- if (line.includes("rclone") && line.includes("mount") && !line.includes("grep")) {
4370
- const parts = line.trim().split(/\s+/);
4371
- if (parts.length >= 2) return parseInt(parts[1], 10);
4372
- }
4373
- }
4374
- } catch {
4602
+ // src/commands/fleet.ts
4603
+ import { readFile as readFile19 } from "fs/promises";
4604
+ import { hostname as nodeHostname, userInfo } from "os";
4605
+ import { join as join27 } from "path";
4606
+ import yaml3 from "js-yaml";
4607
+ var FLEET_REL_PATH = join27("projects", "llm-wiki", "architecture", "fleet.yaml");
4608
+ async function runFleetValidate(input) {
4609
+ const loaded = await loadFleetManifest(input.file);
4610
+ if (!loaded.ok) {
4611
+ if (loaded.error === "FILE_NOT_FOUND") {
4612
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
4375
4613
  }
4376
- return null;
4614
+ const errors = fleetLoadErrors(loaded);
4615
+ return invalidFleet(errors);
4377
4616
  }
4617
+ const warnings = fleetWarnings(loaded.manifest);
4618
+ const snapshotter = findSnapshotter(loaded.manifest);
4619
+ return {
4620
+ exitCode: ExitCode.OK,
4621
+ result: ok({
4622
+ valid: true,
4623
+ errors: [],
4624
+ warnings,
4625
+ host_count: Object.keys(loaded.manifest.hosts).length,
4626
+ snapshotter,
4627
+ humanHint: `VALID fleet manifest (${Object.keys(loaded.manifest.hosts).length} hosts; snapshotter: ${snapshotter ?? "none"})`
4628
+ })
4629
+ };
4378
4630
  }
4379
- function parseRcloneFlags(pid) {
4380
- const flags = /* @__PURE__ */ new Map();
4381
- try {
4382
- const args = getRcloneArgs(pid);
4383
- for (let i = 0; i < args.length; i++) {
4384
- const arg = args[i];
4385
- if (arg.startsWith("--") && arg.includes("=")) {
4386
- const eq = arg.indexOf("=");
4387
- flags.set(arg.slice(0, eq), arg.slice(eq + 1));
4388
- } else if (arg.startsWith("--")) {
4389
- const next = args[i + 1];
4390
- if (next && !next.startsWith("-")) {
4391
- flags.set(arg, next);
4392
- i++;
4393
- } else {
4394
- flags.set(arg, "");
4395
- }
4396
- }
4397
- }
4398
- } catch {
4631
+ async function runFleetContext(input) {
4632
+ const env = input.env ?? process.env;
4633
+ const home = input.home ?? env.HOME ?? "";
4634
+ const cwd = input.cwd ?? process.cwd();
4635
+ const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
4636
+ const user = input.user ?? safeEnvValue(env.USER) ?? safeUserName();
4637
+ const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
4638
+ const file = input.file ?? (vault ? join27(vault, FLEET_REL_PATH) : void 0);
4639
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
4640
+ const loaded = file ? await loadFleetManifest(file) : { ok: false, error: "FILE_NOT_FOUND" };
4641
+ if (!loaded.ok) {
4642
+ const warnings = ["fleet manifest unavailable or invalid"];
4643
+ const markdown2 = formatUnknownContext({
4644
+ generatedAt,
4645
+ osHostname,
4646
+ user,
4647
+ cwd,
4648
+ vault,
4649
+ reason: warnings[0],
4650
+ trace: [],
4651
+ warnings
4652
+ });
4653
+ return {
4654
+ exitCode: ExitCode.OK,
4655
+ result: ok({
4656
+ manifest_loaded: false,
4657
+ generated_at: generatedAt,
4658
+ identity_status: "unknown",
4659
+ resolver_trace: [],
4660
+ warnings,
4661
+ markdown: markdown2,
4662
+ humanHint: markdown2
4663
+ })
4664
+ };
4399
4665
  }
4400
- return flags;
4401
- }
4402
- function getRcloneVersion() {
4403
- try {
4404
- const out = execSync("rclone version", {
4405
- encoding: "utf8",
4406
- timeout: 3e3,
4407
- stdio: ["pipe", "pipe", "pipe"]
4666
+ const resolved = await resolveHostId({
4667
+ manifest: loaded.manifest,
4668
+ hostId: input.hostId,
4669
+ env,
4670
+ home,
4671
+ osHostname
4672
+ });
4673
+ if (resolved.hostId && !loaded.manifest.hosts[resolved.hostId]) {
4674
+ const source = resolved.source ?? "unknown";
4675
+ const warnings = [`resolved host id \`${resolved.hostId}\` from ${source} is not in fleet.yaml`];
4676
+ const markdown2 = formatInvalidContext({
4677
+ generatedAt,
4678
+ hostId: resolved.hostId,
4679
+ source,
4680
+ osHostname,
4681
+ user,
4682
+ cwd,
4683
+ vault,
4684
+ trace: resolved.trace,
4685
+ warnings
4408
4686
  });
4409
- const match = out.match(/rclone\s+v(\d+)\.(\d+)\.(\d+)/i);
4410
- if (!match) return null;
4411
4687
  return {
4412
- major: parseInt(match[1], 10),
4413
- minor: parseInt(match[2], 10),
4414
- patch: parseInt(match[3], 10),
4415
- raw: out.split("\n")[0].trim()
4688
+ exitCode: ExitCode.OK,
4689
+ result: ok({
4690
+ manifest_loaded: true,
4691
+ host_id: resolved.hostId,
4692
+ source: resolved.source,
4693
+ generated_at: generatedAt,
4694
+ identity_status: "invalid",
4695
+ resolver_trace: resolved.trace,
4696
+ warnings,
4697
+ markdown: markdown2,
4698
+ humanHint: markdown2
4699
+ })
4416
4700
  };
4417
- } catch {
4418
- return null;
4419
4701
  }
4420
- }
4421
- function extractRcloneFs(args) {
4422
- let foundMount = false;
4423
- for (const arg of args) {
4424
- if (arg === "mount") {
4425
- foundMount = true;
4426
- continue;
4427
- }
4428
- if (foundMount && arg.includes(":") && !arg.startsWith("-") && !arg.startsWith("/")) {
4429
- return arg;
4430
- }
4702
+ if (!resolved.hostId) {
4703
+ const warnings = ["host identity is unresolved"];
4704
+ const markdown2 = formatUnknownContext({
4705
+ generatedAt,
4706
+ osHostname,
4707
+ user,
4708
+ cwd,
4709
+ vault,
4710
+ reason: warnings[0],
4711
+ trace: resolved.trace,
4712
+ warnings
4713
+ });
4714
+ return {
4715
+ exitCode: ExitCode.OK,
4716
+ result: ok({
4717
+ manifest_loaded: true,
4718
+ generated_at: generatedAt,
4719
+ identity_status: "unknown",
4720
+ resolver_trace: resolved.trace,
4721
+ warnings,
4722
+ markdown: markdown2,
4723
+ humanHint: markdown2
4724
+ })
4725
+ };
4431
4726
  }
4432
- return null;
4727
+ const markdown = formatKnownContext({
4728
+ manifest: loaded.manifest,
4729
+ hostId: resolved.hostId,
4730
+ source: resolved.source,
4731
+ generatedAt,
4732
+ osHostname,
4733
+ user,
4734
+ cwd,
4735
+ vault,
4736
+ trace: resolved.trace
4737
+ });
4738
+ return {
4739
+ exitCode: ExitCode.OK,
4740
+ result: ok({
4741
+ manifest_loaded: true,
4742
+ host_id: resolved.hostId,
4743
+ source: resolved.source,
4744
+ generated_at: generatedAt,
4745
+ identity_status: "known",
4746
+ resolver_trace: resolved.trace,
4747
+ warnings: [],
4748
+ markdown,
4749
+ humanHint: markdown
4750
+ })
4751
+ };
4752
+ }
4753
+ async function loadFleetManifest(file) {
4754
+ let text;
4755
+ try {
4756
+ text = await readFile19(file, "utf8");
4757
+ } catch {
4758
+ return { ok: false, error: "FILE_NOT_FOUND" };
4759
+ }
4760
+ let parsed;
4761
+ try {
4762
+ parsed = yaml3.load(text, { schema: yaml3.JSON_SCHEMA });
4763
+ } catch (error) {
4764
+ return { ok: false, error: "INVALID_YAML", detail: error instanceof Error ? error.message : String(error) };
4765
+ }
4766
+ const result = FleetManifestSchema.safeParse(parsed);
4767
+ if (!result.success) {
4768
+ return { ok: false, error: "INVALID_FLEET_MANIFEST", detail: result.error.issues };
4769
+ }
4770
+ return { ok: true, manifest: result.data };
4771
+ }
4772
+ function invalidFleet(errors) {
4773
+ return {
4774
+ exitCode: ExitCode.FLEET_MANIFEST_INVALID,
4775
+ result: ok({
4776
+ valid: false,
4777
+ errors,
4778
+ warnings: [],
4779
+ host_count: 0,
4780
+ humanHint: `INVALID fleet manifest
4781
+ ${errors.map((e) => ` ${e.path || "(root)"}: ${e.message}`).join("\n")}`
4782
+ })
4783
+ };
4784
+ }
4785
+ function fleetLoadErrors(loaded) {
4786
+ if (loaded.error === "INVALID_YAML") {
4787
+ return [{ path: "", message: `invalid YAML: ${String(loaded.detail ?? "parse failed")}` }];
4788
+ }
4789
+ if (loaded.error === "INVALID_FLEET_MANIFEST" && Array.isArray(loaded.detail)) {
4790
+ return loaded.detail.map((issue) => {
4791
+ const zodIssue = issue;
4792
+ return {
4793
+ path: (zodIssue.path ?? []).join("."),
4794
+ message: zodIssue.message ?? "invalid value"
4795
+ };
4796
+ });
4797
+ }
4798
+ return [{ path: "", message: loaded.error }];
4799
+ }
4800
+ function fleetWarnings(manifest) {
4801
+ const warnings = [];
4802
+ for (const [id, host] of Object.entries(manifest.hosts)) {
4803
+ if (host.role === "snapshotter" && host.protected !== true) {
4804
+ warnings.push(`snapshotter host '${id}' is not protected=true`);
4805
+ }
4806
+ }
4807
+ return warnings;
4808
+ }
4809
+ function findSnapshotter(manifest) {
4810
+ return Object.entries(manifest.hosts).find(([, host]) => host.role === "snapshotter")?.[0];
4811
+ }
4812
+ async function resolveHostId(input) {
4813
+ const trace = [];
4814
+ if (input.hostId) {
4815
+ trace.push({ source: "--host-id", status: "matched", value: input.hostId });
4816
+ return { hostId: input.hostId, source: "host-id", trace };
4817
+ }
4818
+ trace.push({ source: "--host-id", status: "unset" });
4819
+ if (input.env.SKILLWIKI_HOST_ID) {
4820
+ trace.push({ source: "SKILLWIKI_HOST_ID", status: "matched", value: input.env.SKILLWIKI_HOST_ID });
4821
+ return { hostId: input.env.SKILLWIKI_HOST_ID, source: "SKILLWIKI_HOST_ID", trace };
4822
+ }
4823
+ trace.push({ source: "SKILLWIKI_HOST_ID", status: "unset" });
4824
+ if (input.env.AGENT_HOST_ID) {
4825
+ trace.push({ source: "AGENT_HOST_ID", status: "matched", value: input.env.AGENT_HOST_ID });
4826
+ return { hostId: input.env.AGENT_HOST_ID, source: "AGENT_HOST_ID", trace };
4827
+ }
4828
+ trace.push({ source: "AGENT_HOST_ID", status: "unset" });
4829
+ if (input.home) {
4830
+ const dotenv = await parseDotenvFile(join27(input.home, ".skillwiki", ".env"));
4831
+ if (dotenv.SKILLWIKI_HOST_ID) {
4832
+ trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "matched", value: dotenv.SKILLWIKI_HOST_ID });
4833
+ return { hostId: dotenv.SKILLWIKI_HOST_ID, source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", trace };
4834
+ }
4835
+ trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "unset" });
4836
+ } else {
4837
+ trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "skipped" });
4838
+ }
4839
+ if (input.env.VS_HOSTNAME) {
4840
+ trace.push({ source: "VS_HOSTNAME", status: "matched", value: input.env.VS_HOSTNAME });
4841
+ return { hostId: input.env.VS_HOSTNAME, source: "VS_HOSTNAME", trace };
4842
+ }
4843
+ trace.push({ source: "VS_HOSTNAME", status: "unset" });
4844
+ const hostname = input.osHostname.trim();
4845
+ if (hostname) {
4846
+ if (input.manifest.hosts[hostname]) {
4847
+ trace.push({ source: "hostname", status: "matched", value: hostname });
4848
+ return { hostId: hostname, source: "hostname", trace };
4849
+ }
4850
+ const byHostname = Object.entries(input.manifest.hosts).find(([, host]) => host.identity.hostnames.includes(hostname));
4851
+ if (byHostname) {
4852
+ trace.push({ source: "hostname", status: "matched", value: hostname });
4853
+ return { hostId: byHostname[0], source: "hostname", trace };
4854
+ }
4855
+ trace.push({ source: "hostname", status: "unmatched", value: hostname });
4856
+ } else {
4857
+ trace.push({ source: "hostname", status: "unset" });
4858
+ }
4859
+ return { trace };
4860
+ }
4861
+ function formatKnownContext(input) {
4862
+ const host = input.manifest.hosts[input.hostId];
4863
+ const protectedValue = host.protected === true ? "true" : "false";
4864
+ const writesTo = host.writes_to.join(", ");
4865
+ const selfAliases = collectSelfAliases(input.manifest, input.hostId);
4866
+ const outbound = collectOutboundAccess(input.manifest, input.hostId);
4867
+ const maintenanceLines = formatMaintenanceLines(host);
4868
+ const guidance = input.hostId === "macos-dev" ? "use declared SSH aliases for remote work when needed; do not assume undeclared hosts have reciprocal SSH access." : `this session is already on \`${input.hostId}\`; do not SSH to self aliases unless the user explicitly asks. Do not assume outbound SSH to other fleet hosts is configured.`;
4869
+ return [
4870
+ "## Runtime Host Context",
4871
+ "",
4872
+ `- Context generated: \`${input.generatedAt}\``,
4873
+ `- Current machine: \`${input.hostId}\`${input.source ? ` (source: \`${input.source}\`)` : ""}`,
4874
+ "- Identity status: `known`",
4875
+ `- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
4876
+ `- Resolver trace: ${formatTrace(input.trace)}`,
4877
+ `- OS hostname: ${formatMaybe(input.osHostname)}`,
4878
+ `- User: ${formatMaybe(input.user)}`,
4879
+ `- Workspace: ${formatMaybe(input.cwd)}`,
4880
+ `- Vault: ${formatMaybe(input.vault)}`,
4881
+ "- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
4882
+ `- Fleet role: \`${host.role}\`; protected: \`${protectedValue}\`; writes_to: \`${writesTo}\``,
4883
+ ...maintenanceLines,
4884
+ `- Self SSH aliases known in fleet: ${formatList(selfAliases)}`,
4885
+ `- Declared outbound SSH from this source: ${formatOutboundAccess(outbound)}`,
4886
+ `- Guidance: ${guidance}`
4887
+ ].join("\n");
4888
+ }
4889
+ function formatUnknownContext(input) {
4890
+ return [
4891
+ "## Runtime Host Context",
4892
+ "",
4893
+ `- Context generated: \`${input.generatedAt}\``,
4894
+ "- Current machine: unknown",
4895
+ "- Identity status: `unknown`",
4896
+ `- Resolver trace: ${formatTrace(input.trace)}`,
4897
+ `- Warnings: ${formatWarnings(input.warnings)}`,
4898
+ `- OS hostname: ${formatMaybe(input.osHostname)}`,
4899
+ `- User: ${formatMaybe(input.user)}`,
4900
+ `- Workspace: ${formatMaybe(input.cwd)}`,
4901
+ `- Vault: ${formatMaybe(input.vault)}`,
4902
+ "- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
4903
+ "- Fleet role: unknown",
4904
+ "- Self SSH aliases known in fleet: unknown",
4905
+ "- Declared outbound SSH from this source: unknown",
4906
+ `- Guidance: ${input.reason}; do not assume local vs remote role. Inspect runtime or ask before SSH/deploy/sync work.`
4907
+ ].join("\n");
4908
+ }
4909
+ function formatInvalidContext(input) {
4910
+ return [
4911
+ "## Runtime Host Context",
4912
+ "",
4913
+ `- Context generated: \`${input.generatedAt}\``,
4914
+ "- Current machine: unknown",
4915
+ "- Identity status: `invalid`",
4916
+ `- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
4917
+ `- Resolver trace: ${formatTrace(input.trace)}`,
4918
+ `- Warnings: ${formatWarnings(input.warnings)}`,
4919
+ `- OS hostname: ${formatMaybe(input.osHostname)}`,
4920
+ `- User: ${formatMaybe(input.user)}`,
4921
+ `- Workspace: ${formatMaybe(input.cwd)}`,
4922
+ `- Vault: ${formatMaybe(input.vault)}`,
4923
+ "- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
4924
+ "- Fleet role: unknown",
4925
+ "- Self SSH aliases known in fleet: unknown",
4926
+ "- Declared outbound SSH from this source: unknown",
4927
+ `- Guidance: do not trust this identity; rerun with \`--host-id\` only if the user confirms \`${input.hostId}\` is the current fleet host id.`
4928
+ ].join("\n");
4929
+ }
4930
+ function collectSelfAliases(manifest, hostId2) {
4931
+ const aliases = [];
4932
+ const host = manifest.hosts[hostId2];
4933
+ const access = host?.access?.from ?? {};
4934
+ for (const profile of Object.values(access)) {
4935
+ for (const alias of profile.ssh_aliases ?? []) aliases.push(alias);
4936
+ }
4937
+ return [...new Set(aliases)];
4938
+ }
4939
+ function collectOutboundAccess(manifest, sourceHostId) {
4940
+ const hosts = [];
4941
+ for (const [targetId, target] of Object.entries(manifest.hosts)) {
4942
+ if (targetId === sourceHostId) continue;
4943
+ const profile = target.access?.from?.[sourceHostId];
4944
+ if (profile && (profile.status === "configured" || profile.status === "local")) {
4945
+ hosts.push({
4946
+ hostId: targetId,
4947
+ sshAliases: [...new Set(profile.ssh_aliases ?? [])],
4948
+ users: [...new Set(profile.users ?? [])]
4949
+ });
4950
+ }
4951
+ }
4952
+ return hosts.sort((left, right) => left.hostId.localeCompare(right.hostId));
4953
+ }
4954
+ function formatMaintenanceLines(host) {
4955
+ const satellite = host.maintenance?.skillwiki_satellite;
4956
+ if (!satellite?.enabled) return [];
4957
+ return [
4958
+ `- Maintenance role: \`skillwiki satellite\`; user: \`${satellite.user}\`; ssh: \`${satellite.ssh_alias}\``,
4959
+ `- Maintenance paths: maintenance vault: \`${satellite.vault_path}\`; repo: \`${satellite.repo_path}\`; scheduler: \`${satellite.scheduler}\`; jobs: ${formatList(satellite.jobs)}`
4960
+ ];
4961
+ }
4962
+ function formatOutboundAccess(values) {
4963
+ if (values.length === 0) return "none";
4964
+ return values.map((value) => {
4965
+ const aliasPart = value.sshAliases.length > 0 ? ` via ${formatList(value.sshAliases)}` : " (no SSH aliases)";
4966
+ const usersPart = value.users.length > 0 ? ` (users: ${formatList(value.users)})` : "";
4967
+ return `\`${value.hostId}\`${aliasPart}${usersPart}`;
4968
+ }).join("; ");
4969
+ }
4970
+ function formatResolution(source, hostId2) {
4971
+ return source ? `\`${source === "host-id" ? "--host-id" : source}\` -> \`${hostId2}\`` : `unknown -> \`${hostId2}\``;
4972
+ }
4973
+ function formatTrace(values) {
4974
+ if (values.length === 0) return "not available";
4975
+ return values.map((value) => {
4976
+ const source = `\`${value.source}\``;
4977
+ if (value.status === "matched") return `${source} matched \`${value.value ?? ""}\``;
4978
+ if (value.status === "unmatched") return `${source} unmatched \`${value.value ?? ""}\``;
4979
+ return `${source} ${value.status}`;
4980
+ }).join("; ");
4981
+ }
4982
+ function formatWarnings(values) {
4983
+ return values.length > 0 ? values.join("; ") : "none";
4984
+ }
4985
+ function formatList(values) {
4986
+ return values.length > 0 ? values.map((v) => `\`${v}\``).join(", ") : "none";
4987
+ }
4988
+ function formatMaybe(value) {
4989
+ return value && value.trim().length > 0 ? `\`${value}\`` : "unknown";
4990
+ }
4991
+ function safeEnvValue(value) {
4992
+ return value && value.trim().length > 0 ? value : void 0;
4993
+ }
4994
+ function safeUserName() {
4995
+ try {
4996
+ return userInfo().username;
4997
+ } catch {
4998
+ return "";
4999
+ }
5000
+ }
5001
+
5002
+ // src/utils/s3-mount-health.ts
5003
+ import { execSync } from "child_process";
5004
+ import { platform } from "os";
5005
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readFileSync as readFile20 } from "fs";
5006
+ import { join as join28 } from "path";
5007
+ var OS = platform();
5008
+ function findRcloneMountPid() {
5009
+ try {
5010
+ const out = execSync("pgrep -f 'rclone.*mount'", {
5011
+ encoding: "utf8",
5012
+ timeout: 2e3,
5013
+ stdio: ["pipe", "pipe", "pipe"]
5014
+ }).trim();
5015
+ const pids = out.split("\n").filter(Boolean);
5016
+ if (pids.length === 0) return null;
5017
+ return parseInt(pids[0], 10);
5018
+ } catch {
5019
+ try {
5020
+ const out = execSync("ps aux", { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] });
5021
+ for (const line of out.split("\n")) {
5022
+ if (line.includes("rclone") && line.includes("mount") && !line.includes("grep")) {
5023
+ const parts = line.trim().split(/\s+/);
5024
+ if (parts.length >= 2) return parseInt(parts[1], 10);
5025
+ }
5026
+ }
5027
+ } catch {
5028
+ }
5029
+ return null;
5030
+ }
5031
+ }
5032
+ function parseRcloneFlags(pid) {
5033
+ const flags = /* @__PURE__ */ new Map();
5034
+ try {
5035
+ const args = getRcloneArgs(pid);
5036
+ for (let i = 0; i < args.length; i++) {
5037
+ const arg = args[i];
5038
+ if (arg.startsWith("--") && arg.includes("=")) {
5039
+ const eq = arg.indexOf("=");
5040
+ flags.set(arg.slice(0, eq), arg.slice(eq + 1));
5041
+ } else if (arg.startsWith("--")) {
5042
+ const next = args[i + 1];
5043
+ if (next && !next.startsWith("-")) {
5044
+ flags.set(arg, next);
5045
+ i++;
5046
+ } else {
5047
+ flags.set(arg, "");
5048
+ }
5049
+ }
5050
+ }
5051
+ } catch {
5052
+ }
5053
+ return flags;
5054
+ }
5055
+ function getRcloneVersion() {
5056
+ try {
5057
+ const out = execSync("rclone version", {
5058
+ encoding: "utf8",
5059
+ timeout: 3e3,
5060
+ stdio: ["pipe", "pipe", "pipe"]
5061
+ });
5062
+ const match = out.match(/rclone\s+v(\d+)\.(\d+)\.(\d+)/i);
5063
+ if (!match) return null;
5064
+ return {
5065
+ major: parseInt(match[1], 10),
5066
+ minor: parseInt(match[2], 10),
5067
+ patch: parseInt(match[3], 10),
5068
+ raw: out.split("\n")[0].trim()
5069
+ };
5070
+ } catch {
5071
+ return null;
5072
+ }
5073
+ }
5074
+ function extractRcloneFs(args) {
5075
+ let foundMount = false;
5076
+ for (const arg of args) {
5077
+ if (arg === "mount") {
5078
+ foundMount = true;
5079
+ continue;
5080
+ }
5081
+ if (foundMount && arg.includes(":") && !arg.startsWith("-") && !arg.startsWith("/")) {
5082
+ return arg;
5083
+ }
5084
+ }
5085
+ return null;
4433
5086
  }
4434
5087
  function getRcloneArgs(pid) {
4435
5088
  try {
@@ -4508,7 +5161,7 @@ function detectFuseMount(vaultPath) {
4508
5161
  return null;
4509
5162
  }
4510
5163
  function writeTest(dir) {
4511
- const testFile = join26(dir, `.doctor-write-test-${process.pid}.tmp`);
5164
+ const testFile = join28(dir, `.doctor-write-test-${process.pid}.tmp`);
4512
5165
  const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
4513
5166
  const start = Date.now();
4514
5167
  try {
@@ -4519,7 +5172,7 @@ function writeTest(dir) {
4519
5172
  const writeMs = Date.now() - start;
4520
5173
  const readStart = Date.now();
4521
5174
  try {
4522
- const back = readFile19(testFile, "utf8");
5175
+ const back = readFile20(testFile, "utf8");
4523
5176
  const readMs = Date.now() - readStart;
4524
5177
  if (back !== payload) {
4525
5178
  try {
@@ -4608,13 +5261,13 @@ function detectCliChannels(argv, home) {
4608
5261
  }
4609
5262
  const plugin = findPlugin(home);
4610
5263
  if (plugin) {
4611
- const pluginBin = join27(plugin.installPath, "bin", "skillwiki");
4612
- if (existsSync10(pluginBin)) {
5264
+ const pluginBin = join29(plugin.installPath, "bin", "skillwiki");
5265
+ if (existsSync11(pluginBin)) {
4613
5266
  channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
4614
5267
  }
4615
5268
  }
4616
- const installBin = join27(home, ".claude", "skills", "bin", "skillwiki");
4617
- if (existsSync10(installBin)) {
5269
+ const installBin = join29(home, ".claude", "skills", "bin", "skillwiki");
5270
+ if (existsSync11(installBin)) {
4618
5271
  channels.push({ name: "install", path: installBin, isDevLink: false });
4619
5272
  }
4620
5273
  return channels;
@@ -4666,7 +5319,7 @@ function checkCliChannels(argv, home) {
4666
5319
  }
4667
5320
  async function checkConfigFile(home) {
4668
5321
  const cfgPath = configPath(home);
4669
- if (!existsSync10(cfgPath)) {
5322
+ if (!existsSync11(cfgPath)) {
4670
5323
  return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
4671
5324
  }
4672
5325
  try {
@@ -4681,7 +5334,7 @@ function checkWikiPathExists(resolvedPath) {
4681
5334
  if (resolvedPath === void 0) {
4682
5335
  return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
4683
5336
  }
4684
- if (existsSync10(resolvedPath) && statSync3(resolvedPath).isDirectory()) {
5337
+ if (existsSync11(resolvedPath) && statSync3(resolvedPath).isDirectory()) {
4685
5338
  return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
4686
5339
  }
4687
5340
  return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
@@ -4690,13 +5343,13 @@ function checkVaultStructure(resolvedPath) {
4690
5343
  if (resolvedPath === void 0) {
4691
5344
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
4692
5345
  }
4693
- if (!existsSync10(resolvedPath)) {
5346
+ if (!existsSync11(resolvedPath)) {
4694
5347
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
4695
5348
  }
4696
5349
  const missing = [];
4697
- if (!existsSync10(join27(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
5350
+ if (!existsSync11(join29(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
4698
5351
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
4699
- if (!existsSync10(join27(resolvedPath, dir))) missing.push(dir + "/");
5352
+ if (!existsSync11(join29(resolvedPath, dir))) missing.push(dir + "/");
4700
5353
  }
4701
5354
  if (missing.length === 0) {
4702
5355
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -4704,8 +5357,8 @@ function checkVaultStructure(resolvedPath) {
4704
5357
  return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
4705
5358
  }
4706
5359
  function checkSkillsInstalled(home, cwd) {
4707
- const srcDir = cwd ? join27(cwd, "packages", "skills") : void 0;
4708
- if (srcDir && existsSync10(srcDir)) {
5360
+ const srcDir = cwd ? join29(cwd, "packages", "skills") : void 0;
5361
+ if (srcDir && existsSync11(srcDir)) {
4709
5362
  const found = findInstalledSkillMd(srcDir);
4710
5363
  if (found.length > 0) {
4711
5364
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
@@ -4718,8 +5371,8 @@ function checkSkillsInstalled(home, cwd) {
4718
5371
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
4719
5372
  }
4720
5373
  }
4721
- const skillsDir = join27(home, ".claude", "skills");
4722
- if (existsSync10(skillsDir)) {
5374
+ const skillsDir = join29(home, ".claude", "skills");
5375
+ if (existsSync11(skillsDir)) {
4723
5376
  const found = findInstalledSkillMd(skillsDir);
4724
5377
  if (found.length > 0) {
4725
5378
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
@@ -4729,10 +5382,10 @@ function checkSkillsInstalled(home, cwd) {
4729
5382
  }
4730
5383
  function checkDuplicateSkills(home) {
4731
5384
  const plugin = findPlugin(home);
4732
- const skillsDir = join27(home, ".claude", "skills");
5385
+ const skillsDir = join29(home, ".claude", "skills");
4733
5386
  const agentSkillDirs = [
4734
- { label: "~/.codex/skills/", path: join27(home, ".codex", "skills") },
4735
- { label: "~/.agents/skills/", path: join27(home, ".agents", "skills") }
5387
+ { label: "~/.codex/skills/", path: join29(home, ".codex", "skills") },
5388
+ { label: "~/.agents/skills/", path: join29(home, ".agents", "skills") }
4736
5389
  ];
4737
5390
  if (!plugin) {
4738
5391
  return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
@@ -4831,8 +5484,8 @@ async function checkProfiles(home) {
4831
5484
  }
4832
5485
  async function checkProjectLocalOverride(cwd) {
4833
5486
  const dir = cwd ?? process.cwd();
4834
- const envPath = join27(dir, ".skillwiki", ".env");
4835
- if (existsSync10(envPath)) {
5487
+ const envPath = join29(dir, ".skillwiki", ".env");
5488
+ if (existsSync11(envPath)) {
4836
5489
  return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
4837
5490
  }
4838
5491
  return check("pass", "project_local", "Project-local config", "None");
@@ -4841,7 +5494,7 @@ function checkVaultGitRemote(resolvedPath) {
4841
5494
  if (resolvedPath === void 0) {
4842
5495
  return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
4843
5496
  }
4844
- if (!existsSync10(join27(resolvedPath, ".git"))) {
5497
+ if (!existsSync11(join29(resolvedPath, ".git"))) {
4845
5498
  return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
4846
5499
  }
4847
5500
  try {
@@ -4864,9 +5517,9 @@ function checkObsidianTemplates(resolvedPath) {
4864
5517
  return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
4865
5518
  }
4866
5519
  const missing = [];
4867
- if (!existsSync10(join27(resolvedPath, "_Templates"))) missing.push("_Templates/");
4868
- if (!existsSync10(join27(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
4869
- if (!existsSync10(join27(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
5520
+ if (!existsSync11(join29(resolvedPath, "_Templates"))) missing.push("_Templates/");
5521
+ if (!existsSync11(join29(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
5522
+ if (!existsSync11(join29(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
4870
5523
  if (missing.length === 0) {
4871
5524
  return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
4872
5525
  }
@@ -4876,8 +5529,8 @@ function checkDotStoreClean(resolvedPath) {
4876
5529
  if (resolvedPath === void 0) {
4877
5530
  return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
4878
5531
  }
4879
- const rawDir = join27(resolvedPath, "raw");
4880
- if (!existsSync10(rawDir)) {
5532
+ const rawDir = join29(resolvedPath, "raw");
5533
+ if (!existsSync11(rawDir)) {
4881
5534
  return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
4882
5535
  }
4883
5536
  const found = [];
@@ -4892,7 +5545,7 @@ function checkDotStoreClean(resolvedPath) {
4892
5545
  if (entry.name === ".DS_Store") {
4893
5546
  found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
4894
5547
  } else if (entry.isDirectory()) {
4895
- walk2(join27(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
5548
+ walk2(join29(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
4896
5549
  }
4897
5550
  }
4898
5551
  })(rawDir, "");
@@ -4905,7 +5558,7 @@ function checkSyncLastPush(resolvedPath) {
4905
5558
  if (resolvedPath === void 0) {
4906
5559
  return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
4907
5560
  }
4908
- if (!existsSync10(join27(resolvedPath, ".git"))) {
5561
+ if (!existsSync11(join29(resolvedPath, ".git"))) {
4909
5562
  return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
4910
5563
  }
4911
5564
  let timestamp;
@@ -4953,7 +5606,7 @@ function checkVaultGitDirty(resolvedPath) {
4953
5606
  if (resolvedPath === void 0) {
4954
5607
  return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
4955
5608
  }
4956
- if (!existsSync10(join27(resolvedPath, ".git"))) {
5609
+ if (!existsSync11(join29(resolvedPath, ".git"))) {
4957
5610
  return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
4958
5611
  }
4959
5612
  try {
@@ -4994,7 +5647,7 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
4994
5647
  if (resolvedPath === void 0) {
4995
5648
  return check("pass", id, label, "No vault path \u2014 check skipped");
4996
5649
  }
4997
- if (!existsSync10(join27(resolvedPath, ".git"))) {
5650
+ if (!existsSync11(join29(resolvedPath, ".git"))) {
4998
5651
  return check("pass", id, label, "No git repo \u2014 check skipped");
4999
5652
  }
5000
5653
  if (!hasOriginMain(resolvedPath)) {
@@ -5014,13 +5667,38 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
5014
5667
  return check("warn", id, label, "Could not compare HEAD with origin/main");
5015
5668
  }
5016
5669
  }
5670
+ async function checkFleetIdentity(input) {
5671
+ if (!input.vaultPath) {
5672
+ return check("pass", "fleet_identity", "Fleet identity", "No vault path \u2014 check skipped");
5673
+ }
5674
+ const r = await runFleetContext({
5675
+ vault: input.vaultPath,
5676
+ env: { ...process.env, WIKI_PATH: input.envValue ?? input.vaultPath },
5677
+ home: input.home,
5678
+ cwd: input.cwd ?? process.cwd(),
5679
+ osHostname: process.env.HOSTNAME,
5680
+ user: process.env.USER
5681
+ });
5682
+ if (!r.result.ok) {
5683
+ return check("warn", "fleet_identity", "Fleet identity", "Could not evaluate fleet identity");
5684
+ }
5685
+ const data = r.result.data;
5686
+ if (!data.manifest_loaded) {
5687
+ return check("pass", "fleet_identity", "Fleet identity", "Fleet manifest unavailable \u2014 check skipped");
5688
+ }
5689
+ if (data.identity_status === "known") {
5690
+ return check("pass", "fleet_identity", "Fleet identity", `Resolved ${data.host_id ?? "unknown"} via ${data.source ?? "unknown"}`);
5691
+ }
5692
+ const detail = data.warnings.length > 0 ? data.warnings.join("; ") : "Fleet identity is unresolved";
5693
+ return check("warn", "fleet_identity", "Fleet identity", detail);
5694
+ }
5017
5695
  function pullLogPaths(home) {
5018
5696
  const paths = platform2() === "darwin" ? [
5019
- join27(home, "Library", "Logs", "wiki-pull.log"),
5020
- join27(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
5697
+ join29(home, "Library", "Logs", "wiki-pull.log"),
5698
+ join29(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
5021
5699
  ] : [
5022
- join27(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
5023
- join27(home, "Library", "Logs", "wiki-pull.log")
5700
+ join29(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
5701
+ join29(home, "Library", "Logs", "wiki-pull.log")
5024
5702
  ];
5025
5703
  return [...new Set(paths)];
5026
5704
  }
@@ -5032,7 +5710,7 @@ function isRecentLogLine(line, nowMs) {
5032
5710
  return nowMs - ts <= 24 * 60 * 60 * 1e3;
5033
5711
  }
5034
5712
  function checkVaultGitPullFailures(home) {
5035
- const path = pullLogPaths(home).find((p) => existsSync10(p));
5713
+ const path = pullLogPaths(home).find((p) => existsSync11(p));
5036
5714
  if (!path) {
5037
5715
  return check("pass", "vault_git_pull_failures", "Vault pull failures", "No wiki-pull.log found \u2014 check skipped");
5038
5716
  }
@@ -5060,8 +5738,8 @@ function checkS3MountPerf(resolvedPath) {
5060
5738
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
5061
5739
  }
5062
5740
  const mountPoint = fuse.mountPoint;
5063
- const conceptsDir = join27(resolvedPath, "concepts");
5064
- if (!existsSync10(conceptsDir)) {
5741
+ const conceptsDir = join29(resolvedPath, "concepts");
5742
+ if (!existsSync11(conceptsDir)) {
5065
5743
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
5066
5744
  }
5067
5745
  const start = Date.now();
@@ -5243,8 +5921,8 @@ function checkWriteTest(resolvedPath) {
5243
5921
  if (!fuse) {
5244
5922
  return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
5245
5923
  }
5246
- const conceptsDir = join27(resolvedPath, "concepts");
5247
- if (!existsSync10(conceptsDir)) {
5924
+ const conceptsDir = join29(resolvedPath, "concepts");
5925
+ if (!existsSync11(conceptsDir)) {
5248
5926
  return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
5249
5927
  }
5250
5928
  const result = writeTest(conceptsDir);
@@ -5330,7 +6008,7 @@ function checkVfsCacheHealth(resolvedPath) {
5330
6008
  }
5331
6009
  function readVaultSyncConfig(home) {
5332
6010
  try {
5333
- const content = readFileSync7(join27(home, ".skillwiki", ".env"), "utf8");
6011
+ const content = readFileSync7(join29(home, ".skillwiki", ".env"), "utf8");
5334
6012
  let installed = false;
5335
6013
  let role;
5336
6014
  for (const line of content.split(/\r?\n/)) {
@@ -5364,12 +6042,12 @@ function vaultSyncChecks(input) {
5364
6042
  ];
5365
6043
  }
5366
6044
  const isMac = os === "darwin";
5367
- const logDir = input.logDir ?? (isMac ? join27(home, "Library", "Logs") : join27(home, ".local", "state", "vault-sync", "log"));
5368
- const shareDir = input.shareDir ?? (isMac ? join27(home, "Library", "Application Support", "vault-sync", "bin") : join27(home, ".local", "share", "vault-sync", "bin"));
5369
- const filterPath = input.filterPath ?? join27(home, ".config", "rclone", "wiki-push-filters.txt");
6045
+ const logDir = input.logDir ?? (isMac ? join29(home, "Library", "Logs") : join29(home, ".local", "state", "vault-sync", "log"));
6046
+ const shareDir = input.shareDir ?? (isMac ? join29(home, "Library", "Application Support", "vault-sync", "bin") : join29(home, ".local", "share", "vault-sync", "bin"));
6047
+ const filterPath = input.filterPath ?? join29(home, ".config", "rclone", "wiki-push-filters.txt");
5370
6048
  const snapshotPath = input.snapshotScriptPath ?? "/root/.hermes/scripts/wiki-snapshot-v3.sh";
5371
- const pushScriptPath = join27(shareDir, "wiki-push.sh");
5372
- const c1 = existsSync10(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
6049
+ const pushScriptPath = join29(shareDir, "wiki-push.sh");
6050
+ const c1 = existsSync11(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
5373
6051
  let c2;
5374
6052
  try {
5375
6053
  if (isMac) {
@@ -5420,7 +6098,7 @@ function vaultSyncChecks(input) {
5420
6098
  "Scheduler check failed \u2014 run vault-sync-install"
5421
6099
  );
5422
6100
  }
5423
- const logFile = join27(logDir, "wiki-push.log");
6101
+ const logFile = join29(logDir, "wiki-push.log");
5424
6102
  let c3;
5425
6103
  try {
5426
6104
  const logContent = readFileSync7(logFile, "utf8");
@@ -5479,7 +6157,7 @@ function vaultSyncChecks(input) {
5479
6157
  }
5480
6158
  }
5481
6159
  } catch {
5482
- c3 = existsSync10(logDir) ? check(
6160
+ c3 = existsSync11(logDir) ? check(
5483
6161
  "warn",
5484
6162
  "vault_sync_last_push_age",
5485
6163
  "Vault sync last push recency",
@@ -5491,7 +6169,7 @@ function vaultSyncChecks(input) {
5491
6169
  `Log directory not found at ${logDir}`
5492
6170
  );
5493
6171
  }
5494
- const fetchLogFile = join27(logDir, "wiki-fetch.log");
6172
+ const fetchLogFile = join29(logDir, "wiki-fetch.log");
5495
6173
  let cFetch;
5496
6174
  try {
5497
6175
  const logContent = readFileSync7(fetchLogFile, "utf8");
@@ -5538,7 +6216,7 @@ function vaultSyncChecks(input) {
5538
6216
  }
5539
6217
  let c4;
5540
6218
  try {
5541
- if (!existsSync10(filterPath)) {
6219
+ if (!existsSync11(filterPath)) {
5542
6220
  c4 = check(
5543
6221
  "error",
5544
6222
  "vault_sync_filter_present",
@@ -5587,7 +6265,7 @@ function vaultSyncChecks(input) {
5587
6265
  );
5588
6266
  } else {
5589
6267
  try {
5590
- if (!existsSync10(snapshotPath)) {
6268
+ if (!existsSync11(snapshotPath)) {
5591
6269
  c5 = check(
5592
6270
  "error",
5593
6271
  "vault_sync_snapshot_guard",
@@ -5633,15 +6311,15 @@ function findSkillMd(dir) {
5633
6311
  }
5634
6312
  for (const entry of entries) {
5635
6313
  if (entry.isFile() && entry.name === "SKILL.md") {
5636
- results.push(join27(dir, entry.name));
6314
+ results.push(join29(dir, entry.name));
5637
6315
  } else if (entry.isDirectory()) {
5638
- results.push(...findSkillMd(join27(dir, entry.name)));
6316
+ results.push(...findSkillMd(join29(dir, entry.name)));
5639
6317
  }
5640
6318
  }
5641
6319
  return results;
5642
6320
  }
5643
6321
  function findInstalledSkillMd(dir) {
5644
- const directSkills = findSkillNames(dir).map((name) => join27(dir, name, "SKILL.md"));
6322
+ const directSkills = findSkillNames(dir).map((name) => join29(dir, name, "SKILL.md"));
5645
6323
  return directSkills.length > 0 ? directSkills : findSkillMd(dir);
5646
6324
  }
5647
6325
  function findSkillNames(dir) {
@@ -5653,7 +6331,7 @@ function findSkillNames(dir) {
5653
6331
  return results;
5654
6332
  }
5655
6333
  for (const entry of entries) {
5656
- if (entry.isDirectory() && existsSync10(join27(dir, entry.name, "SKILL.md"))) {
6334
+ if (entry.isDirectory() && existsSync11(join29(dir, entry.name, "SKILL.md"))) {
5657
6335
  results.push(entry.name);
5658
6336
  }
5659
6337
  }
@@ -5697,7 +6375,7 @@ async function vaultMetrics(resolvedPath) {
5697
6375
  }
5698
6376
  let logLines = 0;
5699
6377
  try {
5700
- logLines = readFileSync7(join27(resolvedPath, "log.md"), "utf8").split("\n").length;
6378
+ logLines = readFileSync7(join29(resolvedPath, "log.md"), "utf8").split("\n").length;
5701
6379
  } catch {
5702
6380
  }
5703
6381
  return [
@@ -5727,6 +6405,12 @@ async function runDoctor(input) {
5727
6405
  checks.push(checkVaultStructure(resolvedPath));
5728
6406
  checks.push(checkObsidianTemplates(resolvedPath));
5729
6407
  checks.push(checkVaultGitRemote(resolvedPath));
6408
+ checks.push(await checkFleetIdentity({
6409
+ vaultPath: resolvedPath,
6410
+ home: input.home,
6411
+ cwd: input.cwd,
6412
+ envValue: input.envValue
6413
+ }));
5730
6414
  checks.push(checkSyncLastPush(resolvedPath));
5731
6415
  checks.push(checkVaultGitDirty(resolvedPath));
5732
6416
  checks.push(checkVaultGitAhead(resolvedPath));
@@ -5881,7 +6565,7 @@ function summarizeChecks(checks) {
5881
6565
  };
5882
6566
  }
5883
6567
  function classifyLog(path, id, label, okPattern) {
5884
- if (!existsSync11(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
6568
+ if (!existsSync12(path)) return { id, label, status: "warn", detail: `log file missing: ${path}` };
5885
6569
  const lines = readFileSync8(path, "utf8").split(/\r?\n/).filter(Boolean);
5886
6570
  const last = lines[lines.length - 1] ?? "";
5887
6571
  if (!last) return { id, label, status: "warn", detail: `log file empty: ${path}` };
@@ -5900,28 +6584,28 @@ function runVaultSyncHealth(home, syncMode) {
5900
6584
  };
5901
6585
  }
5902
6586
  const isMac = platform3() === "darwin";
5903
- const shareDir = isMac ? join28(home, "Library", "Application Support", "vault-sync", "bin") : join28(home, ".local", "share", "vault-sync", "bin");
5904
- const logDir = isMac ? join28(home, "Library", "Logs") : join28(home, ".local", "state", "vault-sync", "log");
5905
- const filterPath = join28(home, ".config", "rclone", "wiki-push-filters.txt");
6587
+ const shareDir = isMac ? join30(home, "Library", "Application Support", "vault-sync", "bin") : join30(home, ".local", "share", "vault-sync", "bin");
6588
+ const logDir = isMac ? join30(home, "Library", "Logs") : join30(home, ".local", "state", "vault-sync", "log");
6589
+ const filterPath = join30(home, ".config", "rclone", "wiki-push-filters.txt");
5906
6590
  const checks = [];
5907
- const pushScript = join28(shareDir, "wiki-push.sh");
5908
- checks.push(existsSync11(pushScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found: ${pushScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Script missing: ${pushScript}` });
6591
+ const pushScript = join30(shareDir, "wiki-push.sh");
6592
+ checks.push(existsSync12(pushScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found: ${pushScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Script missing: ${pushScript}` });
5909
6593
  if (isMac) {
5910
- const pushPlist = join28(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
5911
- const fetchPlist = join28(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
5912
- checks.push(existsSync11(pushPlist) && existsSync11(fetchPlist) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "launchd unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "launchd unit files missing (read-only mode)" });
6594
+ const pushPlist = join30(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
6595
+ const fetchPlist = join30(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
6596
+ checks.push(existsSync12(pushPlist) && existsSync12(fetchPlist) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "launchd unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "launchd unit files missing (read-only mode)" });
5913
6597
  checks.push({ id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "macOS host \u2014 check skipped" });
5914
6598
  } else {
5915
- const pushTimer = join28(home, ".config", "systemd", "user", "wiki-push.timer");
5916
- const fetchTimer = join28(home, ".config", "systemd", "user", "wiki-fetch.timer");
5917
- const fuseTimer = join28(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
5918
- const fuseService = join28(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
5919
- checks.push(existsSync11(pushTimer) && existsSync11(fetchTimer) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "systemd timer unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "systemd timer unit files missing (read-only mode)" });
5920
- checks.push(existsSync11(fuseTimer) && existsSync11(fuseService) ? { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "wiki-fuse-refresh unit files present (read-only mode)" } : { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "warn", detail: "wiki-fuse-refresh unit files missing (read-only mode)" });
5921
- }
5922
- checks.push(classifyLog(join28(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
5923
- checks.push(classifyLog(join28(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
5924
- if (!existsSync11(filterPath)) {
6599
+ const pushTimer = join30(home, ".config", "systemd", "user", "wiki-push.timer");
6600
+ const fetchTimer = join30(home, ".config", "systemd", "user", "wiki-fetch.timer");
6601
+ const fuseTimer = join30(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
6602
+ const fuseService = join30(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
6603
+ checks.push(existsSync12(pushTimer) && existsSync12(fetchTimer) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "systemd timer unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "systemd timer unit files missing (read-only mode)" });
6604
+ checks.push(existsSync12(fuseTimer) && existsSync12(fuseService) ? { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "wiki-fuse-refresh unit files present (read-only mode)" } : { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "warn", detail: "wiki-fuse-refresh unit files missing (read-only mode)" });
6605
+ }
6606
+ checks.push(classifyLog(join30(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
6607
+ checks.push(classifyLog(join30(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
6608
+ if (!existsSync12(filterPath)) {
5925
6609
  checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
5926
6610
  } else {
5927
6611
  const content = readFileSync8(filterPath, "utf8");
@@ -6146,8 +6830,8 @@ async function runHealth(input) {
6146
6830
  }
6147
6831
 
6148
6832
  // src/commands/archive.ts
6149
- import { rename as rename7, mkdir as mkdir9, readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
6150
- import { join as join29, dirname as dirname11 } from "path";
6833
+ import { rename as rename7, mkdir as mkdir9, readFile as readFile21, writeFile as writeFile10 } from "fs/promises";
6834
+ import { join as join31, dirname as dirname11 } from "path";
6151
6835
  function countWikilinks(body, slug) {
6152
6836
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6153
6837
  const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
@@ -6175,7 +6859,7 @@ async function runArchive(input) {
6175
6859
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
6176
6860
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
6177
6861
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
6178
- const archivePath = join29("_archive", relPath).replace(/\\/g, "/");
6862
+ const archivePath = join31("_archive", relPath).replace(/\\/g, "/");
6179
6863
  let cascade;
6180
6864
  if (input.cascade) {
6181
6865
  const wikilinkRefs = [];
@@ -6199,7 +6883,7 @@ async function runArchive(input) {
6199
6883
  const indexRefs = [];
6200
6884
  if (!isRaw) {
6201
6885
  try {
6202
- const idx = await readFile20(join29(input.vault, "index.md"), "utf8");
6886
+ const idx = await readFile21(join31(input.vault, "index.md"), "utf8");
6203
6887
  idx.split("\n").forEach((line, i) => {
6204
6888
  if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
6205
6889
  });
@@ -6225,8 +6909,8 @@ async function runArchive(input) {
6225
6909
  }
6226
6910
  if (input.cascade && input.apply && cascade) {
6227
6911
  for (const ref of cascade.source_array_refs) {
6228
- const absPath = join29(input.vault, ref.page);
6229
- const text = await readFile20(absPath, "utf8");
6912
+ const absPath = join31(input.vault, ref.page);
6913
+ const text = await readFile21(absPath, "utf8");
6230
6914
  const split = splitFrontmatter(text);
6231
6915
  if (!split.ok) continue;
6232
6916
  const before = split.data.rawFrontmatter;
@@ -6243,12 +6927,12 @@ ${fmRewritten}
6243
6927
  }
6244
6928
  }
6245
6929
  }
6246
- await mkdir9(dirname11(join29(input.vault, archivePath)), { recursive: true });
6930
+ await mkdir9(dirname11(join31(input.vault, archivePath)), { recursive: true });
6247
6931
  let indexUpdated = false;
6248
6932
  if (!isRaw) {
6249
- const indexPath = join29(input.vault, "index.md");
6933
+ const indexPath = join31(input.vault, "index.md");
6250
6934
  try {
6251
- const idx = await readFile20(indexPath, "utf8");
6935
+ const idx = await readFile21(indexPath, "utf8");
6252
6936
  const originalLines = idx.split("\n");
6253
6937
  const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
6254
6938
  if (filtered.length !== originalLines.length) {
@@ -6259,7 +6943,7 @@ ${fmRewritten}
6259
6943
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
6260
6944
  }
6261
6945
  }
6262
- await rename7(join29(input.vault, relPath), join29(input.vault, archivePath));
6946
+ await rename7(join31(input.vault, relPath), join31(input.vault, archivePath));
6263
6947
  appendLastOp(input.vault, {
6264
6948
  operation: input.cascade ? "archive-cascade" : "archive",
6265
6949
  summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
@@ -6282,7 +6966,7 @@ ${fmRewritten}
6282
6966
  }
6283
6967
 
6284
6968
  // src/commands/drift.ts
6285
- import { createHash as createHash3 } from "crypto";
6969
+ import { createHash as createHash5 } from "crypto";
6286
6970
 
6287
6971
  // src/utils/fetch.ts
6288
6972
  async function controlledFetch(url, opts) {
@@ -6361,7 +7045,7 @@ async function runDrift(input) {
6361
7045
  });
6362
7046
  continue;
6363
7047
  }
6364
- const currentHash = createHash3("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
7048
+ const currentHash = createHash5("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
6365
7049
  const identity = assessSourceIdentity({
6366
7050
  rawPath: raw.relPath,
6367
7051
  sourceUrl,
@@ -6668,7 +7352,7 @@ ${newBody}`;
6668
7352
 
6669
7353
  // src/commands/update.ts
6670
7354
  import { execSync as execSync3 } from "child_process";
6671
- import { join as join30 } from "path";
7355
+ import { join as join32 } from "path";
6672
7356
 
6673
7357
  // src/utils/package-info.ts
6674
7358
  import { readFileSync as readFileSync9 } from "fs";
@@ -6698,7 +7382,7 @@ function resolveGlobalSkillsRoot() {
6698
7382
  encoding: "utf8",
6699
7383
  timeout: 5e3
6700
7384
  }).trim();
6701
- return join30(globalRoot, "skillwiki", "skills");
7385
+ return join32(globalRoot, "skillwiki", "skills");
6702
7386
  } catch {
6703
7387
  return null;
6704
7388
  }
@@ -6722,7 +7406,7 @@ async function runUpdate(input) {
6722
7406
  const pkg2 = readCliPackageJson();
6723
7407
  const currentVersion = pkg2.version;
6724
7408
  const tag = normalizeDistTag(input.distTag);
6725
- const target = join30(input.home, ".claude", "skills");
7409
+ const target = join32(input.home, ".claude", "skills");
6726
7410
  let latest;
6727
7411
  try {
6728
7412
  latest = execSync3(`npm view skillwiki@${tag} version`, {
@@ -6793,15 +7477,15 @@ async function runUpdate(input) {
6793
7477
 
6794
7478
  // src/commands/self-update.ts
6795
7479
  import { execSync as execSync4 } from "child_process";
6796
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
6797
- import { join as join31 } from "path";
7480
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
7481
+ import { join as join33 } from "path";
6798
7482
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
6799
7483
  async function runSelfUpdate(input) {
6800
7484
  const currentVersion = readCliPackageJson().version;
6801
7485
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
6802
7486
  const distTag = normalizeDistTag(input.distTag);
6803
- const localPkgPath = join31(sourceRoot, "packages", "cli", "package.json");
6804
- const hasLocalSource = existsSync12(localPkgPath);
7487
+ const localPkgPath = join33(sourceRoot, "packages", "cli", "package.json");
7488
+ const hasLocalSource = existsSync13(localPkgPath);
6805
7489
  if (input.check) {
6806
7490
  let availableVersion = null;
6807
7491
  let source;
@@ -6932,10 +7616,10 @@ async function runSelfUpdate(input) {
6932
7616
  }
6933
7617
 
6934
7618
  // src/commands/transcripts.ts
6935
- import { readdir as readdir5, stat as stat7, readFile as readFile21 } from "fs/promises";
6936
- import { join as join32 } from "path";
7619
+ import { readdir as readdir5, stat as stat8, readFile as readFile22 } from "fs/promises";
7620
+ import { join as join34 } from "path";
6937
7621
  async function runTranscripts(input) {
6938
- const dir = join32(input.vault, "raw", "transcripts");
7622
+ const dir = join34(input.vault, "raw", "transcripts");
6939
7623
  let entries;
6940
7624
  try {
6941
7625
  entries = await readdir5(dir, { withFileTypes: true });
@@ -6945,13 +7629,13 @@ async function runTranscripts(input) {
6945
7629
  const transcripts = [];
6946
7630
  for (const entry of entries) {
6947
7631
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
6948
- const filePath = join32(dir, entry.name);
6949
- const content = await readFile21(filePath, "utf8");
7632
+ const filePath = join34(dir, entry.name);
7633
+ const content = await readFile22(filePath, "utf8");
6950
7634
  const fm = extractFrontmatter(content);
6951
7635
  if (!fm.ok) continue;
6952
7636
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
6953
7637
  if (input.since && ingested && ingested < input.since) continue;
6954
- const s = await stat7(filePath);
7638
+ const s = await stat8(filePath);
6955
7639
  transcripts.push({
6956
7640
  file: `raw/transcripts/${entry.name}`,
6957
7641
  ingested,
@@ -6963,12 +7647,12 @@ async function runTranscripts(input) {
6963
7647
  }
6964
7648
 
6965
7649
  // src/commands/project-index.ts
6966
- import { readdir as readdir6, readFile as readFile22, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
6967
- import { join as join33, dirname as dirname12 } from "path";
7650
+ import { readdir as readdir6, readFile as readFile23, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
7651
+ import { join as join35, dirname as dirname12 } from "path";
6968
7652
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
6969
7653
  async function runProjectIndex(input) {
6970
7654
  const slug = input.slug;
6971
- const projectDir = join33(input.vault, "projects", slug);
7655
+ const projectDir = join35(input.vault, "projects", slug);
6972
7656
  try {
6973
7657
  await readdir6(projectDir);
6974
7658
  } catch {
@@ -6979,15 +7663,15 @@ async function runProjectIndex(input) {
6979
7663
  }
6980
7664
  const wikilinkPattern = `[[${slug}]]`;
6981
7665
  const entries = [];
6982
- const compoundDir = join33(input.vault, "projects", slug, "compound");
7666
+ const compoundDir = join35(input.vault, "projects", slug, "compound");
6983
7667
  try {
6984
7668
  const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
6985
7669
  for (const entry of compoundFiles) {
6986
7670
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
6987
- const filePath = join33(compoundDir, entry.name);
7671
+ const filePath = join35(compoundDir, entry.name);
6988
7672
  let text;
6989
7673
  try {
6990
- text = await readFile22(filePath, "utf8");
7674
+ text = await readFile23(filePath, "utf8");
6991
7675
  } catch {
6992
7676
  continue;
6993
7677
  }
@@ -7004,16 +7688,16 @@ async function runProjectIndex(input) {
7004
7688
  for (const dir of LAYER2_DIRS) {
7005
7689
  let files;
7006
7690
  try {
7007
- files = await readdir6(join33(input.vault, dir), { withFileTypes: true });
7691
+ files = await readdir6(join35(input.vault, dir), { withFileTypes: true });
7008
7692
  } catch {
7009
7693
  continue;
7010
7694
  }
7011
7695
  for (const entry of files) {
7012
7696
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
7013
- const filePath = join33(input.vault, dir, entry.name);
7697
+ const filePath = join35(input.vault, dir, entry.name);
7014
7698
  let text;
7015
7699
  try {
7016
- text = await readFile22(filePath, "utf8");
7700
+ text = await readFile23(filePath, "utf8");
7017
7701
  } catch {
7018
7702
  continue;
7019
7703
  }
@@ -7034,11 +7718,11 @@ async function runProjectIndex(input) {
7034
7718
  const tb = typeOrder[b.type] ?? 99;
7035
7719
  return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
7036
7720
  });
7037
- const indexPath = join33(projectDir, "knowledge.md");
7721
+ const indexPath = join35(projectDir, "knowledge.md");
7038
7722
  let existing = false;
7039
7723
  let stale = false;
7040
7724
  try {
7041
- const existingText = await readFile22(indexPath, "utf8");
7725
+ const existingText = await readFile23(indexPath, "utf8");
7042
7726
  existing = true;
7043
7727
  const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
7044
7728
  const existingPages = new Set(existingEntries.map((l) => {
@@ -7108,9 +7792,9 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
7108
7792
 
7109
7793
  // src/commands/compound.ts
7110
7794
  import { writeFile as writeFile12, mkdir as mkdir11, readdir as readdir7, unlink as unlink4 } from "fs/promises";
7111
- import { join as join34 } from "path";
7112
- import { existsSync as existsSync13 } from "fs";
7113
- import { readFile as readFile23 } from "fs/promises";
7795
+ import { join as join36 } from "path";
7796
+ import { existsSync as existsSync14 } from "fs";
7797
+ import { readFile as readFile24 } from "fs/promises";
7114
7798
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
7115
7799
  var FIELD_RE = {
7116
7800
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -7208,17 +7892,17 @@ function extractRetroFields(date, cycleName, block) {
7208
7892
  };
7209
7893
  }
7210
7894
  async function runCompound(input) {
7211
- const logPath = join34(input.vault, "log.md");
7895
+ const logPath = join36(input.vault, "log.md");
7212
7896
  let logText;
7213
7897
  try {
7214
- logText = await readFile23(logPath, "utf8");
7898
+ logText = await readFile24(logPath, "utf8");
7215
7899
  } catch {
7216
7900
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
7217
7901
  }
7218
7902
  const entries = parseRetroEntries(logText);
7219
7903
  const promoted = [];
7220
7904
  const skipped = [];
7221
- const compoundDir = join34(input.vault, "projects", input.project, "compound");
7905
+ const compoundDir = join36(input.vault, "projects", input.project, "compound");
7222
7906
  for (const entry of entries) {
7223
7907
  const generalizeValue = entry.generalize.trim();
7224
7908
  if (!/^yes/i.test(generalizeValue)) {
@@ -7226,8 +7910,8 @@ async function runCompound(input) {
7226
7910
  continue;
7227
7911
  }
7228
7912
  const slug = slugify(entry.cycleName);
7229
- const compoundPath = join34(compoundDir, `${slug}.md`);
7230
- if (existsSync13(compoundPath)) {
7913
+ const compoundPath = join36(compoundDir, `${slug}.md`);
7914
+ if (existsSync14(compoundPath)) {
7231
7915
  skipped.push(entry.date);
7232
7916
  continue;
7233
7917
  }
@@ -7265,7 +7949,7 @@ async function runCompound(input) {
7265
7949
  ].join("\n");
7266
7950
  const content = frontmatter + "\n" + body;
7267
7951
  if (!input.dryRun) {
7268
- if (!existsSync13(compoundDir)) {
7952
+ if (!existsSync14(compoundDir)) {
7269
7953
  await mkdir11(compoundDir, { recursive: true });
7270
7954
  }
7271
7955
  await writeFile12(compoundPath, content, "utf8");
@@ -7287,16 +7971,16 @@ async function runCompound(input) {
7287
7971
  };
7288
7972
  }
7289
7973
  async function runCompoundDelete(input) {
7290
- const projectDir = join34(input.vault, "projects", input.project);
7291
- if (!existsSync13(projectDir)) {
7974
+ const projectDir = join36(input.vault, "projects", input.project);
7975
+ if (!existsSync14(projectDir)) {
7292
7976
  return {
7293
7977
  exitCode: ExitCode.PROJECT_NOT_FOUND,
7294
7978
  result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
7295
7979
  };
7296
7980
  }
7297
7981
  const entryName = input.entry.replace(/\.md$/, "");
7298
- const compoundPath = join34(projectDir, "compound", `${entryName}.md`);
7299
- if (!existsSync13(compoundPath)) {
7982
+ const compoundPath = join36(projectDir, "compound", `${entryName}.md`);
7983
+ if (!existsSync14(compoundPath)) {
7300
7984
  return {
7301
7985
  exitCode: ExitCode.FILE_NOT_FOUND,
7302
7986
  result: err("FILE_NOT_FOUND", { path: compoundPath })
@@ -7329,8 +8013,8 @@ knowledge.md regenerated`
7329
8013
  };
7330
8014
  }
7331
8015
  async function runCompoundList(input) {
7332
- const compoundDir = join34(input.vault, "projects", input.project, "compound");
7333
- if (!existsSync13(compoundDir)) {
8016
+ const compoundDir = join36(input.vault, "projects", input.project, "compound");
8017
+ if (!existsSync14(compoundDir)) {
7334
8018
  return {
7335
8019
  exitCode: ExitCode.OK,
7336
8020
  result: ok({
@@ -7360,10 +8044,10 @@ could not read compound directory`
7360
8044
  const entries = [];
7361
8045
  for (const dirent of dirents) {
7362
8046
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
7363
- const filePath = join34(compoundDir, dirent.name);
8047
+ const filePath = join36(compoundDir, dirent.name);
7364
8048
  let text;
7365
8049
  try {
7366
- text = await readFile23(filePath, "utf8");
8050
+ text = await readFile24(filePath, "utf8");
7367
8051
  } catch {
7368
8052
  continue;
7369
8053
  }
@@ -7393,9 +8077,9 @@ no compound entries found`;
7393
8077
 
7394
8078
  // src/commands/observe.ts
7395
8079
  import { mkdir as mkdir12, writeFile as writeFile13 } from "fs/promises";
7396
- import { existsSync as existsSync14, statSync as statSync4 } from "fs";
7397
- import { join as join35 } from "path";
7398
- import { createHash as createHash4 } from "crypto";
8080
+ import { existsSync as existsSync15, statSync as statSync4 } from "fs";
8081
+ import { join as join37 } from "path";
8082
+ import { createHash as createHash6 } from "crypto";
7399
8083
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
7400
8084
  function slugify2(text) {
7401
8085
  const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -7417,13 +8101,13 @@ async function runObserve(input) {
7417
8101
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
7418
8102
  };
7419
8103
  }
7420
- if (!existsSync14(input.vault) || !statSync4(input.vault).isDirectory()) {
8104
+ if (!existsSync15(input.vault) || !statSync4(input.vault).isDirectory()) {
7421
8105
  return {
7422
8106
  exitCode: ExitCode.VAULT_PATH_INVALID,
7423
8107
  result: err("VAULT_PATH_INVALID", { path: input.vault })
7424
8108
  };
7425
8109
  }
7426
- const transcriptsDir = join35(input.vault, "raw", "transcripts");
8110
+ const transcriptsDir = join37(input.vault, "raw", "transcripts");
7427
8111
  try {
7428
8112
  await mkdir12(transcriptsDir, { recursive: true });
7429
8113
  } catch {
@@ -7435,11 +8119,11 @@ async function runObserve(input) {
7435
8119
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7436
8120
  const slug = slugify2(input.text);
7437
8121
  const fileName = `${today}-observation-${slug}.md`;
7438
- const filePath = join35(transcriptsDir, fileName);
8122
+ const filePath = join37(transcriptsDir, fileName);
7439
8123
  const body = `
7440
8124
  ${input.text.trim()}
7441
8125
  `;
7442
- const sha256 = createHash4("sha256").update(Buffer.from(body, "utf8")).digest("hex");
8126
+ const sha256 = createHash6("sha256").update(Buffer.from(body, "utf8")).digest("hex");
7443
8127
  const frontmatterLines = [
7444
8128
  "---",
7445
8129
  "source_url:",
@@ -7475,8 +8159,8 @@ ${input.text.trim()}
7475
8159
  }
7476
8160
 
7477
8161
  // src/commands/session-brief.ts
7478
- import { mkdir as mkdir13, readFile as readFile24, writeFile as writeFile14 } from "fs/promises";
7479
- import { join as join36, relative as relative3, sep as sep3 } from "path";
8162
+ import { mkdir as mkdir13, readFile as readFile25, writeFile as writeFile14 } from "fs/promises";
8163
+ import { join as join38, relative as relative3, sep as sep3 } from "path";
7480
8164
  var MAX_WORDS = 900;
7481
8165
  async function runSessionBrief(input) {
7482
8166
  const scan = await scanVault(input.vault);
@@ -7554,7 +8238,7 @@ async function resolveProject(input) {
7554
8238
  const envProject = input.env?.SKILLWIKI_PROJECT;
7555
8239
  if (envProject) return envProject;
7556
8240
  const cwd = input.cwd ?? process.cwd();
7557
- const projectDotenv = await readProjectSlug(join36(cwd, ".skillwiki", ".env"));
8241
+ const projectDotenv = await readProjectSlug(join38(cwd, ".skillwiki", ".env"));
7558
8242
  if (projectDotenv) return projectDotenv;
7559
8243
  const inferred = inferProjectFromPath(input.vault, cwd);
7560
8244
  if (inferred) return inferred;
@@ -7563,7 +8247,7 @@ async function resolveProject(input) {
7563
8247
  async function readProjectSlug(file) {
7564
8248
  let text;
7565
8249
  try {
7566
- text = await readFile24(file, "utf8");
8250
+ text = await readFile25(file, "utf8");
7567
8251
  } catch {
7568
8252
  return void 0;
7569
8253
  }
@@ -7688,7 +8372,7 @@ function appendTextSection(lines, title, items, empty) {
7688
8372
  lines.push("");
7689
8373
  }
7690
8374
  async function loadHealthWarnings(vault) {
7691
- const text = await readIfExists2(join36(vault, ".skillwiki", "health.json"));
8375
+ const text = await readIfExists2(join38(vault, ".skillwiki", "health.json"));
7692
8376
  if (!text) return [];
7693
8377
  try {
7694
8378
  const parsed = JSON.parse(text);
@@ -7701,11 +8385,11 @@ async function loadHealthWarnings(vault) {
7701
8385
  }
7702
8386
  }
7703
8387
  async function writeBriefArtifacts(vault, input) {
7704
- const metaPath = join36(vault, "meta", "latest-session-brief.md");
7705
- const cacheMdPath = join36(vault, ".skillwiki", "session-brief.md");
7706
- const cacheJsonPath = join36(vault, ".skillwiki", "session-brief.json");
7707
- await mkdir13(join36(vault, "meta"), { recursive: true });
7708
- await mkdir13(join36(vault, ".skillwiki"), { recursive: true });
8388
+ const metaPath = join38(vault, "meta", "latest-session-brief.md");
8389
+ const cacheMdPath = join38(vault, ".skillwiki", "session-brief.md");
8390
+ const cacheJsonPath = join38(vault, ".skillwiki", "session-brief.json");
8391
+ await mkdir13(join38(vault, "meta"), { recursive: true });
8392
+ await mkdir13(join38(vault, ".skillwiki"), { recursive: true });
7709
8393
  const committed = renderCommittedBrief(input);
7710
8394
  const previousComparable = comparableBrief(await readIfExists2(metaPath));
7711
8395
  const nextComparable = comparableBrief(committed);
@@ -7765,7 +8449,7 @@ function renderCommittedBrief(input) {
7765
8449
  ].filter((line) => line !== "").join("\n");
7766
8450
  }
7767
8451
  async function ensureIndexEntry(vault) {
7768
- const indexPath = join36(vault, "index.md");
8452
+ const indexPath = join38(vault, "index.md");
7769
8453
  let text = await readIfExists2(indexPath);
7770
8454
  if (!text) return false;
7771
8455
  if (text.includes("[[meta/latest-session-brief]]")) return false;
@@ -7784,7 +8468,7 @@ async function ensureIndexEntry(vault) {
7784
8468
  return true;
7785
8469
  }
7786
8470
  async function appendMaterialLog(vault, today) {
7787
- const logPath = join36(vault, "log.md");
8471
+ const logPath = join38(vault, "log.md");
7788
8472
  const text = await readIfExists2(logPath);
7789
8473
  if (!text) return false;
7790
8474
  const entry = `
@@ -7794,7 +8478,7 @@ async function appendMaterialLog(vault, today) {
7794
8478
  }
7795
8479
  async function readIfExists2(path) {
7796
8480
  try {
7797
- return await readFile24(path, "utf8");
8481
+ return await readFile25(path, "utf8");
7798
8482
  } catch {
7799
8483
  return "";
7800
8484
  }
@@ -7836,9 +8520,9 @@ function dateFromPath(path) {
7836
8520
  }
7837
8521
 
7838
8522
  // src/commands/ingest.ts
7839
- import { readFile as readFile25, writeFile as writeFile15, mkdir as mkdir14 } from "fs/promises";
7840
- import { join as join37 } from "path";
7841
- import { createHash as createHash5 } from "crypto";
8523
+ import { readFile as readFile26, writeFile as writeFile15, mkdir as mkdir14 } from "fs/promises";
8524
+ import { join as join39 } from "path";
8525
+ import { createHash as createHash7 } from "crypto";
7842
8526
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
7843
8527
  var TYPE_DIR = {
7844
8528
  entity: "entities",
@@ -7996,7 +8680,7 @@ async function runIngest(input) {
7996
8680
  sourceContent = fetchResult.data.body;
7997
8681
  } else {
7998
8682
  try {
7999
- sourceContent = await readFile25(input.source, "utf8");
8683
+ sourceContent = await readFile26(input.source, "utf8");
8000
8684
  } catch {
8001
8685
  return {
8002
8686
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -8004,15 +8688,25 @@ async function runIngest(input) {
8004
8688
  };
8005
8689
  }
8006
8690
  }
8007
- const sha256 = createHash5("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
8691
+ const sensitiveFindings = scanSensitiveContent(sourceContent, { file: input.source });
8692
+ if (sensitiveFindings.length > 0) {
8693
+ return {
8694
+ exitCode: ExitCode.SENSITIVE_CONTENT_DETECTED,
8695
+ result: err("SENSITIVE_CONTENT_DETECTED", {
8696
+ message: "source content contains sensitive authentication material; provide a redacted source before ingesting",
8697
+ findings: sensitiveFindings
8698
+ })
8699
+ };
8700
+ }
8701
+ const sha256 = createHash7("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
8008
8702
  const today = todayIso();
8009
8703
  const slug = slugify3(input.title);
8010
8704
  const tags = input.tags && input.tags.length > 0 ? input.tags : [];
8011
8705
  const rawRelPath = `raw/articles/${slug}.md`;
8012
8706
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
8013
8707
  const typedRelPath = `${typedDir}/${slug}.md`;
8014
- const rawAbsPath = join37(input.vault, rawRelPath);
8015
- const typedAbsPath = join37(input.vault, typedRelPath);
8708
+ const rawAbsPath = join39(input.vault, rawRelPath);
8709
+ const typedAbsPath = join39(input.vault, typedRelPath);
8016
8710
  const identity = assessSourceIdentity({
8017
8711
  rawPath: rawRelPath,
8018
8712
  sourceUrl: sourceUrl ?? void 0,
@@ -8094,7 +8788,7 @@ async function runIngest(input) {
8094
8788
  };
8095
8789
  }
8096
8790
  try {
8097
- await mkdir14(join37(input.vault, "raw", "articles"), { recursive: true });
8791
+ await mkdir14(join39(input.vault, "raw", "articles"), { recursive: true });
8098
8792
  await writeFile15(rawAbsPath, rawContent, "utf8");
8099
8793
  } catch (e) {
8100
8794
  return {
@@ -8103,7 +8797,7 @@ async function runIngest(input) {
8103
8797
  };
8104
8798
  }
8105
8799
  try {
8106
- await mkdir14(join37(input.vault, typedDir), { recursive: true });
8800
+ await mkdir14(join39(input.vault, typedDir), { recursive: true });
8107
8801
  await writeFile15(typedAbsPath, typedContent, "utf8");
8108
8802
  } catch (e) {
8109
8803
  return {
@@ -8282,24 +8976,24 @@ ${body}`;
8282
8976
  }
8283
8977
 
8284
8978
  // src/commands/sync.ts
8285
- import { existsSync as existsSync16 } from "fs";
8286
- import { join as join39 } from "path";
8979
+ import { existsSync as existsSync17 } from "fs";
8980
+ import { join as join41 } from "path";
8287
8981
 
8288
8982
  // src/utils/sync-lock.ts
8289
- import { existsSync as existsSync15, mkdirSync as mkdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "fs";
8290
- import { join as join38 } from "path";
8291
- import { createHash as createHash6 } from "crypto";
8983
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "fs";
8984
+ import { join as join40 } from "path";
8985
+ import { createHash as createHash8 } from "crypto";
8292
8986
  function getSessionId() {
8293
8987
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
8294
8988
  if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
8295
8989
  return process.pid.toString();
8296
8990
  }
8297
8991
  function lockPath(vault) {
8298
- return join38(vault, ".skillwiki", "sync.lock");
8992
+ return join40(vault, ".skillwiki", "sync.lock");
8299
8993
  }
8300
8994
  function readLock(vault) {
8301
8995
  const path = lockPath(vault);
8302
- if (!existsSync15(path)) return null;
8996
+ if (!existsSync16(path)) return null;
8303
8997
  try {
8304
8998
  const raw = readFileSync11(path, "utf8");
8305
8999
  return JSON.parse(raw);
@@ -8314,8 +9008,8 @@ function isStale(lock, now) {
8314
9008
  }
8315
9009
  function acquireLock(vault, opts = {}) {
8316
9010
  const path = lockPath(vault);
8317
- const dir = join38(vault, ".skillwiki");
8318
- if (!existsSync15(dir)) {
9011
+ const dir = join40(vault, ".skillwiki");
9012
+ if (!existsSync16(dir)) {
8319
9013
  mkdirSync5(dir, { recursive: true });
8320
9014
  }
8321
9015
  const sessionId = opts.sessionId ?? getSessionId();
@@ -8360,7 +9054,7 @@ function writeLockedFile(path, lock) {
8360
9054
  }
8361
9055
  function releaseLock(vault, opts = {}) {
8362
9056
  const path = lockPath(vault);
8363
- if (!existsSync15(path)) {
9057
+ if (!existsSync16(path)) {
8364
9058
  return { released: false };
8365
9059
  }
8366
9060
  const sessionId = opts.sessionId ?? getSessionId();
@@ -8389,7 +9083,7 @@ function releaseLock(vault, opts = {}) {
8389
9083
  function runSyncStatus(input) {
8390
9084
  const vault = input.vault;
8391
9085
  const includeStashes = input.includeStashes ?? false;
8392
- if (!existsSync16(join39(vault, ".git"))) {
9086
+ if (!existsSync17(join41(vault, ".git"))) {
8393
9087
  return {
8394
9088
  exitCode: ExitCode.VAULT_PATH_INVALID,
8395
9089
  result: ok({
@@ -8467,7 +9161,7 @@ function runSyncStatus(input) {
8467
9161
  }
8468
9162
  async function runSyncPush(input) {
8469
9163
  const vault = input.vault;
8470
- if (!existsSync16(join39(vault, ".git"))) {
9164
+ if (!existsSync17(join41(vault, ".git"))) {
8471
9165
  return {
8472
9166
  exitCode: ExitCode.VAULT_PATH_INVALID,
8473
9167
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -8590,7 +9284,7 @@ function enableGitLongPathsOnWindows(vault) {
8590
9284
  }
8591
9285
  async function runSyncPull(input) {
8592
9286
  const vault = input.vault;
8593
- if (!existsSync16(join39(vault, ".git"))) {
9287
+ if (!existsSync17(join41(vault, ".git"))) {
8594
9288
  return {
8595
9289
  exitCode: ExitCode.VAULT_PATH_INVALID,
8596
9290
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -8762,7 +9456,7 @@ function runSyncPeers(input) {
8762
9456
  }
8763
9457
  function runSyncLock(input) {
8764
9458
  const vault = input.vault;
8765
- if (!existsSync16(vault)) {
9459
+ if (!existsSync17(vault)) {
8766
9460
  return {
8767
9461
  exitCode: ExitCode.VAULT_PATH_INVALID,
8768
9462
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -8797,7 +9491,7 @@ function runSyncLock(input) {
8797
9491
  }
8798
9492
  function runSyncUnlock(input) {
8799
9493
  const vault = input.vault;
8800
- if (!existsSync16(vault)) {
9494
+ if (!existsSync17(vault)) {
8801
9495
  return {
8802
9496
  exitCode: ExitCode.VAULT_PATH_INVALID,
8803
9497
  result: err("VAULT_PATH_INVALID", { path: vault })
@@ -8831,7 +9525,7 @@ function runSyncUnlock(input) {
8831
9525
 
8832
9526
  // src/commands/backup.ts
8833
9527
  import { statSync as statSync5, readdirSync as readdirSync3, readFileSync as readFileSync12, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
8834
- import { join as join40, relative as relative4, dirname as dirname13 } from "path";
9528
+ import { join as join42, relative as relative4, dirname as dirname13 } from "path";
8835
9529
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
8836
9530
 
8837
9531
  // src/utils/s3-client.ts
@@ -8855,7 +9549,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node
8855
9549
  function* walkMarkdown(dir, base) {
8856
9550
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
8857
9551
  if (SKIP_DIRS2.has(entry.name)) continue;
8858
- const full = join40(dir, entry.name);
9552
+ const full = join42(dir, entry.name);
8859
9553
  if (entry.isDirectory()) {
8860
9554
  yield* walkMarkdown(full, base);
8861
9555
  } else if (entry.name.endsWith(".md")) {
@@ -8878,7 +9572,7 @@ async function runBackupSync(input) {
8878
9572
  let failed = 0;
8879
9573
  const files = [...walkMarkdown(input.vault, input.vault)];
8880
9574
  for (const relPath of files) {
8881
- const absPath = join40(input.vault, relPath);
9575
+ const absPath = join42(input.vault, relPath);
8882
9576
  const localStat = statSync5(absPath);
8883
9577
  let needsUpload = true;
8884
9578
  try {
@@ -8954,7 +9648,7 @@ async function runBackupRestore(input) {
8954
9648
  const objects = list.Contents ?? [];
8955
9649
  for (const obj of objects) {
8956
9650
  if (!obj.Key) continue;
8957
- const localPath = join40(target, obj.Key);
9651
+ const localPath = join42(target, obj.Key);
8958
9652
  try {
8959
9653
  const localStat = statSync5(localPath);
8960
9654
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -9000,11 +9694,11 @@ async function runBackupRestore(input) {
9000
9694
  }
9001
9695
 
9002
9696
  // src/commands/status.ts
9003
- import { existsSync as existsSync17, statSync as statSync6 } from "fs";
9004
- import { readFile as readFile26 } from "fs/promises";
9005
- import { join as join41 } from "path";
9697
+ import { existsSync as existsSync18, statSync as statSync6 } from "fs";
9698
+ import { readFile as readFile27 } from "fs/promises";
9699
+ import { join as join43 } from "path";
9006
9700
  async function runStatus(input) {
9007
- if (!existsSync17(input.vault)) {
9701
+ if (!existsSync18(input.vault)) {
9008
9702
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
9009
9703
  }
9010
9704
  const scan = await scanVault(input.vault);
@@ -9029,7 +9723,7 @@ async function runStatus(input) {
9029
9723
  const compound = scan.data.compound.length;
9030
9724
  let schemaVersion = "v1";
9031
9725
  try {
9032
- const schemaContent = await readFile26(join41(input.vault, "SCHEMA.md"), "utf8");
9726
+ const schemaContent = await readFile27(join43(input.vault, "SCHEMA.md"), "utf8");
9033
9727
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
9034
9728
  if (versionMatch) schemaVersion = versionMatch[1];
9035
9729
  } catch {
@@ -9089,8 +9783,8 @@ async function runStatus(input) {
9089
9783
  }
9090
9784
 
9091
9785
  // src/commands/seed.ts
9092
- import { mkdir as mkdir15, writeFile as writeFile16, stat as stat8 } from "fs/promises";
9093
- import { join as join42 } from "path";
9786
+ import { mkdir as mkdir15, writeFile as writeFile16, stat as stat9 } from "fs/promises";
9787
+ import { join as join44 } from "path";
9094
9788
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9095
9789
  var EXAMPLE_PAGES = {
9096
9790
  "entities/example-project.md": `---
@@ -9159,29 +9853,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
9159
9853
  `;
9160
9854
  async function runSeed(input) {
9161
9855
  try {
9162
- await stat8(join42(input.vault, "SCHEMA.md"));
9856
+ await stat9(join44(input.vault, "SCHEMA.md"));
9163
9857
  } catch {
9164
9858
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
9165
9859
  }
9166
9860
  const created = [];
9167
9861
  const skipped = [];
9168
9862
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
9169
- const absPath = join42(input.vault, relPath);
9863
+ const absPath = join44(input.vault, relPath);
9170
9864
  try {
9171
- await stat8(absPath);
9865
+ await stat9(absPath);
9172
9866
  skipped.push(relPath);
9173
9867
  } catch {
9174
- await mkdir15(join42(absPath, ".."), { recursive: true });
9868
+ await mkdir15(join44(absPath, ".."), { recursive: true });
9175
9869
  await writeFile16(absPath, content, "utf8");
9176
9870
  created.push(relPath);
9177
9871
  }
9178
9872
  }
9179
- const rawPath = join42(input.vault, "raw", "articles", "example-source.md");
9873
+ const rawPath = join44(input.vault, "raw", "articles", "example-source.md");
9180
9874
  try {
9181
- await stat8(rawPath);
9875
+ await stat9(rawPath);
9182
9876
  skipped.push("raw/articles/example-source.md");
9183
9877
  } catch {
9184
- await mkdir15(join42(rawPath, ".."), { recursive: true });
9878
+ await mkdir15(join44(rawPath, ".."), { recursive: true });
9185
9879
  await writeFile16(rawPath, EXAMPLE_RAW, "utf8");
9186
9880
  created.push("raw/articles/example-source.md");
9187
9881
  }
@@ -9204,9 +9898,9 @@ async function runSeed(input) {
9204
9898
  }
9205
9899
 
9206
9900
  // src/commands/canvas.ts
9207
- import { readFile as readFile27, writeFile as writeFile17 } from "fs/promises";
9208
- import { existsSync as existsSync18 } from "fs";
9209
- import { join as join43 } from "path";
9901
+ import { readFile as readFile28, writeFile as writeFile17 } from "fs/promises";
9902
+ import { existsSync as existsSync19 } from "fs";
9903
+ import { join as join45 } from "path";
9210
9904
  var NODE_WIDTH = 240;
9211
9905
  var NODE_HEIGHT = 60;
9212
9906
  var COLUMN_SPACING = 400;
@@ -9284,8 +9978,8 @@ function buildCanvasEdges(adjacency) {
9284
9978
  return edges;
9285
9979
  }
9286
9980
  async function runCanvasGenerate(input) {
9287
- const graphPath = input.graphPath ?? join43(input.vault, ".skillwiki", "graph.json");
9288
- if (!existsSync18(graphPath)) {
9981
+ const graphPath = input.graphPath ?? join45(input.vault, ".skillwiki", "graph.json");
9982
+ if (!existsSync19(graphPath)) {
9289
9983
  return {
9290
9984
  exitCode: ExitCode.FILE_NOT_FOUND,
9291
9985
  result: err("FILE_NOT_FOUND", {
@@ -9296,7 +9990,7 @@ async function runCanvasGenerate(input) {
9296
9990
  }
9297
9991
  let raw;
9298
9992
  try {
9299
- raw = await readFile27(graphPath, "utf8");
9993
+ raw = await readFile28(graphPath, "utf8");
9300
9994
  } catch (e) {
9301
9995
  return {
9302
9996
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -9322,7 +10016,7 @@ async function runCanvasGenerate(input) {
9322
10016
  const nodes = buildCanvasNodes(paths);
9323
10017
  const edges = buildCanvasEdges(graph.adjacency);
9324
10018
  const canvas = { nodes, edges };
9325
- const outPath = join43(input.vault, "vault-graph.canvas");
10019
+ const outPath = join45(input.vault, "vault-graph.canvas");
9326
10020
  try {
9327
10021
  await writeFile17(outPath, JSON.stringify(canvas, null, 2));
9328
10022
  } catch (e) {
@@ -9344,8 +10038,8 @@ written: ${outPath}`
9344
10038
  }
9345
10039
 
9346
10040
  // src/commands/query.ts
9347
- import { readFile as readFile28, stat as stat9 } from "fs/promises";
9348
- import { join as join44 } from "path";
10041
+ import { readFile as readFile29, stat as stat10 } from "fs/promises";
10042
+ import { join as join46 } from "path";
9349
10043
  var W_KEYWORD = 2;
9350
10044
  var W_SOURCE_OVERLAP = 4;
9351
10045
  var W_WIKILINK = 3;
@@ -9466,10 +10160,10 @@ function computeKeywordScore(terms, title, tags, body) {
9466
10160
  return score;
9467
10161
  }
9468
10162
  async function loadOrBuildGraph(vault) {
9469
- const graphPath = join44(vault, ".skillwiki", "graph.json");
10163
+ const graphPath = join46(vault, ".skillwiki", "graph.json");
9470
10164
  let needsBuild = false;
9471
10165
  try {
9472
- const fileStat = await stat9(graphPath);
10166
+ const fileStat = await stat10(graphPath);
9473
10167
  const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
9474
10168
  if (ageHours > 24) needsBuild = true;
9475
10169
  } catch {
@@ -9480,274 +10174,22 @@ async function loadOrBuildGraph(vault) {
9480
10174
  if (buildResult.exitCode !== 0) return null;
9481
10175
  }
9482
10176
  try {
9483
- const raw = await readFile28(graphPath, "utf8");
10177
+ const raw = await readFile29(graphPath, "utf8");
9484
10178
  return JSON.parse(raw);
9485
10179
  } catch {
9486
10180
  return null;
9487
10181
  }
9488
10182
  }
9489
10183
 
9490
- // src/commands/fleet.ts
9491
- import { readFile as readFile29 } from "fs/promises";
9492
- import { hostname as nodeHostname, userInfo } from "os";
9493
- import { join as join45 } from "path";
9494
- import yaml3 from "js-yaml";
9495
- var FLEET_REL_PATH = join45("projects", "llm-wiki", "architecture", "fleet.yaml");
9496
- async function runFleetValidate(input) {
9497
- const loaded = await loadFleetManifest(input.file);
9498
- if (!loaded.ok) {
9499
- if (loaded.error === "FILE_NOT_FOUND") {
9500
- return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
9501
- }
9502
- const errors = fleetLoadErrors(loaded);
9503
- return invalidFleet(errors);
9504
- }
9505
- const warnings = fleetWarnings(loaded.manifest);
9506
- const snapshotter = findSnapshotter(loaded.manifest);
9507
- return {
9508
- exitCode: ExitCode.OK,
9509
- result: ok({
9510
- valid: true,
9511
- errors: [],
9512
- warnings,
9513
- host_count: Object.keys(loaded.manifest.hosts).length,
9514
- snapshotter,
9515
- humanHint: `VALID fleet manifest (${Object.keys(loaded.manifest.hosts).length} hosts; snapshotter: ${snapshotter ?? "none"})`
9516
- })
9517
- };
9518
- }
9519
- async function runFleetContext(input) {
9520
- const env = input.env ?? process.env;
9521
- const home = input.home ?? env.HOME ?? "";
9522
- const cwd = input.cwd ?? process.cwd();
9523
- const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
9524
- const user = input.user ?? safeEnvValue(env.USER) ?? safeUserName();
9525
- const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
9526
- const file = input.file ?? (vault ? join45(vault, FLEET_REL_PATH) : void 0);
9527
- const loaded = file ? await loadFleetManifest(file) : { ok: false, error: "FILE_NOT_FOUND" };
9528
- if (!loaded.ok) {
9529
- const markdown2 = formatUnknownContext({ osHostname, user, cwd, vault, reason: "fleet manifest unavailable or invalid" });
9530
- return {
9531
- exitCode: ExitCode.OK,
9532
- result: ok({ manifest_loaded: false, markdown: markdown2, humanHint: markdown2 })
9533
- };
9534
- }
9535
- const resolved = await resolveHostId({
9536
- manifest: loaded.manifest,
9537
- hostId: input.hostId,
9538
- env,
9539
- home,
9540
- osHostname
9541
- });
9542
- if (!resolved.hostId || !loaded.manifest.hosts[resolved.hostId]) {
9543
- const markdown2 = formatUnknownContext({ osHostname, user, cwd, vault, reason: "host identity is unresolved" });
9544
- return {
9545
- exitCode: ExitCode.OK,
9546
- result: ok({ manifest_loaded: true, markdown: markdown2, humanHint: markdown2 })
9547
- };
9548
- }
9549
- const markdown = formatKnownContext({
9550
- manifest: loaded.manifest,
9551
- hostId: resolved.hostId,
9552
- source: resolved.source,
9553
- osHostname,
9554
- user,
9555
- cwd,
9556
- vault
9557
- });
9558
- return {
9559
- exitCode: ExitCode.OK,
9560
- result: ok({
9561
- manifest_loaded: true,
9562
- host_id: resolved.hostId,
9563
- source: resolved.source,
9564
- markdown,
9565
- humanHint: markdown
9566
- })
9567
- };
9568
- }
9569
- async function loadFleetManifest(file) {
9570
- let text;
9571
- try {
9572
- text = await readFile29(file, "utf8");
9573
- } catch {
9574
- return { ok: false, error: "FILE_NOT_FOUND" };
9575
- }
9576
- let parsed;
9577
- try {
9578
- parsed = yaml3.load(text, { schema: yaml3.JSON_SCHEMA });
9579
- } catch (error) {
9580
- return { ok: false, error: "INVALID_YAML", detail: error instanceof Error ? error.message : String(error) };
9581
- }
9582
- const result = FleetManifestSchema.safeParse(parsed);
9583
- if (!result.success) {
9584
- return { ok: false, error: "INVALID_FLEET_MANIFEST", detail: result.error.issues };
9585
- }
9586
- return { ok: true, manifest: result.data };
9587
- }
9588
- function invalidFleet(errors) {
9589
- return {
9590
- exitCode: ExitCode.FLEET_MANIFEST_INVALID,
9591
- result: ok({
9592
- valid: false,
9593
- errors,
9594
- warnings: [],
9595
- host_count: 0,
9596
- humanHint: `INVALID fleet manifest
9597
- ${errors.map((e) => ` ${e.path || "(root)"}: ${e.message}`).join("\n")}`
9598
- })
9599
- };
9600
- }
9601
- function fleetLoadErrors(loaded) {
9602
- if (loaded.error === "INVALID_YAML") {
9603
- return [{ path: "", message: `invalid YAML: ${String(loaded.detail ?? "parse failed")}` }];
9604
- }
9605
- if (loaded.error === "INVALID_FLEET_MANIFEST" && Array.isArray(loaded.detail)) {
9606
- return loaded.detail.map((issue) => {
9607
- const zodIssue = issue;
9608
- return {
9609
- path: (zodIssue.path ?? []).join("."),
9610
- message: zodIssue.message ?? "invalid value"
9611
- };
9612
- });
9613
- }
9614
- return [{ path: "", message: loaded.error }];
9615
- }
9616
- function fleetWarnings(manifest) {
9617
- const warnings = [];
9618
- for (const [id, host] of Object.entries(manifest.hosts)) {
9619
- if (host.role === "snapshotter" && host.protected !== true) {
9620
- warnings.push(`snapshotter host '${id}' is not protected=true`);
9621
- }
9622
- }
9623
- return warnings;
9624
- }
9625
- function findSnapshotter(manifest) {
9626
- return Object.entries(manifest.hosts).find(([, host]) => host.role === "snapshotter")?.[0];
9627
- }
9628
- async function resolveHostId(input) {
9629
- if (input.hostId) return { hostId: input.hostId, source: "host-id" };
9630
- if (input.env.SKILLWIKI_HOST_ID) return { hostId: input.env.SKILLWIKI_HOST_ID, source: "SKILLWIKI_HOST_ID" };
9631
- if (input.env.AGENT_HOST_ID) return { hostId: input.env.AGENT_HOST_ID, source: "AGENT_HOST_ID" };
9632
- if (input.home) {
9633
- const dotenv = await parseDotenvFile(join45(input.home, ".skillwiki", ".env"));
9634
- if (dotenv.SKILLWIKI_HOST_ID) {
9635
- return { hostId: dotenv.SKILLWIKI_HOST_ID, source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID" };
9636
- }
9637
- }
9638
- if (input.env.VS_HOSTNAME) return { hostId: input.env.VS_HOSTNAME, source: "VS_HOSTNAME" };
9639
- const hostname = input.osHostname.trim();
9640
- if (hostname) {
9641
- if (input.manifest.hosts[hostname]) return { hostId: hostname, source: "hostname" };
9642
- const byHostname = Object.entries(input.manifest.hosts).find(([, host]) => host.identity.hostnames.includes(hostname));
9643
- if (byHostname) return { hostId: byHostname[0], source: "hostname" };
9644
- }
9645
- return {};
9646
- }
9647
- function formatKnownContext(input) {
9648
- const host = input.manifest.hosts[input.hostId];
9649
- const protectedValue = host.protected === true ? "true" : "false";
9650
- const writesTo = host.writes_to.join(", ");
9651
- const selfAliases = collectSelfAliases(input.manifest, input.hostId);
9652
- const outbound = collectOutboundAccess(input.manifest, input.hostId);
9653
- const maintenanceLines = formatMaintenanceLines(host);
9654
- const guidance = input.hostId === "macos-dev" ? "use declared SSH aliases for remote work when needed; do not assume undeclared hosts have reciprocal SSH access." : `this session is already on \`${input.hostId}\`; do not SSH to self aliases unless the user explicitly asks. Do not assume outbound SSH to other fleet hosts is configured.`;
9655
- return [
9656
- "## Runtime Host Context",
9657
- "",
9658
- `- Current machine: \`${input.hostId}\`${input.source ? ` (source: \`${input.source}\`)` : ""}`,
9659
- `- OS hostname: ${formatMaybe(input.osHostname)}`,
9660
- `- User: ${formatMaybe(input.user)}`,
9661
- `- Workspace: ${formatMaybe(input.cwd)}`,
9662
- `- Vault: ${formatMaybe(input.vault)}`,
9663
- `- Fleet role: \`${host.role}\`; protected: \`${protectedValue}\`; writes_to: \`${writesTo}\``,
9664
- ...maintenanceLines,
9665
- `- Self SSH aliases known in fleet: ${formatList(selfAliases)}`,
9666
- `- Declared outbound SSH from this source: ${formatOutboundAccess(outbound)}`,
9667
- `- Guidance: ${guidance}`
9668
- ].join("\n");
9669
- }
9670
- function formatUnknownContext(input) {
9671
- return [
9672
- "## Runtime Host Context",
9673
- "",
9674
- "- Current machine: unknown",
9675
- `- OS hostname: ${formatMaybe(input.osHostname)}`,
9676
- `- User: ${formatMaybe(input.user)}`,
9677
- `- Workspace: ${formatMaybe(input.cwd)}`,
9678
- `- Vault: ${formatMaybe(input.vault)}`,
9679
- "- Fleet role: unknown",
9680
- "- Self SSH aliases known in fleet: unknown",
9681
- "- Declared outbound SSH from this source: unknown",
9682
- `- Guidance: ${input.reason}; do not assume local vs remote role. Inspect runtime or ask before SSH/deploy/sync work.`
9683
- ].join("\n");
9684
- }
9685
- function collectSelfAliases(manifest, hostId2) {
9686
- const aliases = [];
9687
- const host = manifest.hosts[hostId2];
9688
- const access = host?.access?.from ?? {};
9689
- for (const profile of Object.values(access)) {
9690
- for (const alias of profile.ssh_aliases ?? []) aliases.push(alias);
9691
- }
9692
- return [...new Set(aliases)];
9693
- }
9694
- function collectOutboundAccess(manifest, sourceHostId) {
9695
- const hosts = [];
9696
- for (const [targetId, target] of Object.entries(manifest.hosts)) {
9697
- if (targetId === sourceHostId) continue;
9698
- const profile = target.access?.from?.[sourceHostId];
9699
- if (profile && (profile.status === "configured" || profile.status === "local")) {
9700
- hosts.push({
9701
- hostId: targetId,
9702
- sshAliases: [...new Set(profile.ssh_aliases ?? [])],
9703
- users: [...new Set(profile.users ?? [])]
9704
- });
9705
- }
9706
- }
9707
- return hosts.sort((left, right) => left.hostId.localeCompare(right.hostId));
9708
- }
9709
- function formatMaintenanceLines(host) {
9710
- const satellite = host.maintenance?.skillwiki_satellite;
9711
- if (!satellite?.enabled) return [];
9712
- return [
9713
- `- Maintenance role: \`skillwiki satellite\`; user: \`${satellite.user}\`; ssh: \`${satellite.ssh_alias}\``,
9714
- `- Maintenance paths: maintenance vault: \`${satellite.vault_path}\`; repo: \`${satellite.repo_path}\`; scheduler: \`${satellite.scheduler}\`; jobs: ${formatList(satellite.jobs)}`
9715
- ];
9716
- }
9717
- function formatOutboundAccess(values) {
9718
- if (values.length === 0) return "none";
9719
- return values.map((value) => {
9720
- const aliasPart = value.sshAliases.length > 0 ? ` via ${formatList(value.sshAliases)}` : " (no SSH aliases)";
9721
- const usersPart = value.users.length > 0 ? ` (users: ${formatList(value.users)})` : "";
9722
- return `\`${value.hostId}\`${aliasPart}${usersPart}`;
9723
- }).join("; ");
9724
- }
9725
- function formatList(values) {
9726
- return values.length > 0 ? values.map((v) => `\`${v}\``).join(", ") : "none";
9727
- }
9728
- function formatMaybe(value) {
9729
- return value && value.trim().length > 0 ? `\`${value}\`` : "unknown";
9730
- }
9731
- function safeEnvValue(value) {
9732
- return value && value.trim().length > 0 ? value : void 0;
9733
- }
9734
- function safeUserName() {
9735
- try {
9736
- return userInfo().username;
9737
- } catch {
9738
- return "";
9739
- }
9740
- }
9741
-
9742
10184
  // src/utils/auto-commit.ts
9743
- import { existsSync as existsSync19 } from "fs";
9744
- import { join as join46 } from "path";
10185
+ import { existsSync as existsSync20 } from "fs";
10186
+ import { join as join47 } from "path";
9745
10187
  async function postCommit(vault, exitCode) {
9746
10188
  if (exitCode !== 0) return;
9747
10189
  const home = process.env.HOME ?? "";
9748
10190
  const dotenv = await parseDotenvFile(configPath(home));
9749
10191
  if (dotenv["AUTO_COMMIT"] === "false") return;
9750
- if (!existsSync19(join46(vault, ".git"))) return;
10192
+ if (!existsSync20(join47(vault, ".git"))) return;
9751
10193
  const lastOps = readLastOp(vault);
9752
10194
  if (lastOps.length === 0) return;
9753
10195
  const porcelain = git(vault, ["status", "--porcelain"]);
@@ -9798,7 +10240,7 @@ program.command("validate <file>").description("validate vault page frontmatter
9798
10240
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
9799
10241
  });
9800
10242
  program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
9801
- const out = opts.out ?? join47(vault, ".skillwiki", "graph.json");
10243
+ const out = opts.out ?? join48(vault, ".skillwiki", "graph.json");
9802
10244
  emit(await runGraphBuild({ vault, out }), vault);
9803
10245
  });
9804
10246
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");