skillwiki 0.9.61 → 0.9.63

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.
@@ -328,34 +328,162 @@ function getErrorMessage(e) {
328
328
  return e instanceof Error ? e.message : String(e);
329
329
  }
330
330
 
331
- // src/commands/validate.ts
332
- import { readFile, writeFile } from "fs/promises";
333
- import { join, resolve, relative, sep } from "path";
331
+ // src/commands/log-append.ts
332
+ import { readFile as readFile2, stat } from "fs/promises";
333
+ import { join as join4 } from "path";
334
334
 
335
- // src/parsers/frontmatter.ts
336
- import yaml from "js-yaml";
337
- var FM_OPEN = /^---\r?\n/;
338
- function splitFrontmatter(text) {
339
- if (!FM_OPEN.test(text)) return ok({ rawFrontmatter: "", body: text, bodyStart: 0 });
340
- const afterOpen = text.replace(FM_OPEN, "");
341
- const closeIdx = afterOpen.search(/\r?\n---\r?\n/);
342
- if (closeIdx === -1) return err("MISSING_CLOSING_DELIMITER");
343
- const rawFrontmatter = afterOpen.slice(0, closeIdx);
344
- const closeMatch = afterOpen.slice(closeIdx).match(/\r?\n---\r?\n/);
345
- const bodyStart = text.length - (afterOpen.length - closeIdx - closeMatch[0].length);
346
- const body = text.slice(bodyStart);
347
- return ok({ rawFrontmatter, body, bodyStart });
335
+ // src/utils/last-op.ts
336
+ import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
337
+ import { join } from "path";
338
+ var LAST_OP_DIR = ".skillwiki";
339
+ var LAST_OP_FILE = "last-op.json";
340
+ function lastOpPath(vault) {
341
+ return join(vault, LAST_OP_DIR, LAST_OP_FILE);
348
342
  }
349
- function extractFrontmatter(text) {
350
- const split = splitFrontmatter(text);
351
- if (!split.ok) return split;
352
- if (!split.data.rawFrontmatter) return ok({});
343
+ function readLastOp(vault) {
344
+ const p = lastOpPath(vault);
345
+ if (!existsSync(p)) return [];
353
346
  try {
354
- const parsed = yaml.load(split.data.rawFrontmatter, { schema: yaml.JSON_SCHEMA });
355
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return ok({});
356
- return ok(parsed);
357
- } catch (e) {
358
- return err("INVALID_FRONTMATTER", { message: getErrorMessage(e) });
347
+ const raw = readFileSync(p, "utf8");
348
+ const parsed = JSON.parse(raw);
349
+ if (!Array.isArray(parsed)) {
350
+ unlinkSync(p);
351
+ return [];
352
+ }
353
+ return parsed;
354
+ } catch {
355
+ try {
356
+ unlinkSync(p);
357
+ } catch (_e) {
358
+ }
359
+ return [];
360
+ }
361
+ }
362
+ function appendLastOp(vault, entry) {
363
+ const existing = readLastOp(vault);
364
+ existing.push(entry);
365
+ const dir = join(vault, LAST_OP_DIR);
366
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
367
+ writeFileSync(lastOpPath(vault), JSON.stringify(existing, null, 2), "utf8");
368
+ }
369
+ function clearLastOp(vault) {
370
+ const p = lastOpPath(vault);
371
+ try {
372
+ unlinkSync(p);
373
+ } catch (_e) {
374
+ }
375
+ }
376
+
377
+ // src/utils/atomic-write.ts
378
+ import { randomBytes } from "crypto";
379
+ import { open, readFile, rename, unlink } from "fs/promises";
380
+ import { basename, dirname, join as join2 } from "path";
381
+ async function readExisting(path) {
382
+ try {
383
+ return await readFile(path, "utf8");
384
+ } catch (error) {
385
+ if (error.code === "ENOENT") return null;
386
+ throw error;
387
+ }
388
+ }
389
+ async function atomicWriteText(path, text) {
390
+ let existing;
391
+ try {
392
+ existing = await readExisting(path);
393
+ } catch (error) {
394
+ return err("WRITE_FAILED", { path, phase: "read-existing", message: String(error) });
395
+ }
396
+ if (existing === text) return ok({ changed: false, existed: true });
397
+ const tmp = join2(
398
+ dirname(path),
399
+ `.${basename(path)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`
400
+ );
401
+ try {
402
+ const handle = await open(tmp, "wx");
403
+ try {
404
+ await handle.writeFile(text, "utf8");
405
+ try {
406
+ await handle.sync();
407
+ } catch {
408
+ }
409
+ } finally {
410
+ await handle.close();
411
+ }
412
+ await rename(tmp, path);
413
+ return ok({ changed: true, existed: existing !== null });
414
+ } catch (error) {
415
+ try {
416
+ await unlink(tmp);
417
+ } catch {
418
+ }
419
+ return err("WRITE_FAILED", { path, phase: "atomic-write", message: String(error) });
420
+ }
421
+ }
422
+
423
+ // src/utils/log-lock.ts
424
+ import { randomBytes as randomBytes2 } from "crypto";
425
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
426
+ import { join as join3 } from "path";
427
+ function logLockPath(vault) {
428
+ return join3(vault, ".skillwiki", "log-append.lock");
429
+ }
430
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
431
+ function readLogLock(path) {
432
+ try {
433
+ return JSON.parse(readFileSync2(path, "utf8"));
434
+ } catch {
435
+ return null;
436
+ }
437
+ }
438
+ async function acquireLogLock(vault, opts = {}) {
439
+ const retryMs = opts.retryMs ?? 2e3;
440
+ const pollMs = opts.pollMs ?? 50;
441
+ const staleMs = opts.staleMs ?? 1e4;
442
+ const reclaimStale = opts.reclaimStale ?? true;
443
+ const path = logLockPath(vault);
444
+ const dir = join3(vault, ".skillwiki");
445
+ if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
446
+ const deadline = Date.now() + retryMs;
447
+ const ownerToken = randomBytes2(16).toString("hex");
448
+ const acquired = (/* @__PURE__ */ new Date()).toISOString();
449
+ const content = JSON.stringify({ pid: process.pid, owner_token: ownerToken, acquired }) + "\n";
450
+ for (; ; ) {
451
+ try {
452
+ writeFileSync2(path, content, { flag: "wx" });
453
+ return ok({ vault, path, ownerToken, acquired });
454
+ } catch (error) {
455
+ const fsError = error;
456
+ if (fsError.code !== "EEXIST") {
457
+ return err("WRITE_FAILED", { path, message: String(error) });
458
+ }
459
+ }
460
+ if (reclaimStale) {
461
+ try {
462
+ const age = Date.now() - statSync(path).mtimeMs;
463
+ if (age > staleMs) {
464
+ unlinkSync2(path);
465
+ continue;
466
+ }
467
+ } catch {
468
+ continue;
469
+ }
470
+ }
471
+ if (Date.now() >= deadline) return err("LOG_APPEND_LOCK_HELD", { vault });
472
+ await sleep(pollMs);
473
+ }
474
+ }
475
+ function releaseLogLock(handle) {
476
+ const existing = readLogLock(handle.path);
477
+ if (!existing || existing.owner_token !== handle.ownerToken || existing.acquired !== handle.acquired) {
478
+ return err("LOG_APPEND_LOCK_HELD", {
479
+ message: "log append lock ownership changed; refusing release"
480
+ });
481
+ }
482
+ try {
483
+ unlinkSync2(handle.path);
484
+ return ok({ released: true });
485
+ } catch (error) {
486
+ return err("WRITE_FAILED", { path: handle.path, message: String(error) });
359
487
  }
360
488
  }
361
489
 
@@ -499,15 +627,490 @@ function redactSensitiveContent(text, opts = {}) {
499
627
  };
500
628
  }
501
629
 
630
+ // src/commands/log-append.ts
631
+ var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
632
+ function operationMarker(operationId) {
633
+ if (!/^[0-9a-f]{64}$/.test(operationId)) {
634
+ return err("USAGE", { message: "operationId must be a SHA-256 hex string" });
635
+ }
636
+ return ok(`<!-- skillwiki-page-publish:${operationId} -->`);
637
+ }
638
+ async function appendWhileLocked(logPath, content, marker) {
639
+ let logText;
640
+ try {
641
+ logText = await readFile2(logPath, "utf8");
642
+ } catch {
643
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
644
+ }
645
+ const entriesBefore = [...logText.matchAll(ENTRY_RE)].length;
646
+ if (marker && logText.includes(marker)) {
647
+ return {
648
+ exitCode: ExitCode.OK,
649
+ result: ok({
650
+ entries_before: entriesBefore,
651
+ entries_after: entriesBefore,
652
+ appended: false,
653
+ humanHint: `publication operation already appended (${entriesBefore} entries)`
654
+ })
655
+ };
656
+ }
657
+ const body = logText.replace(/\s+$/, "");
658
+ const appendedContent = marker ? `${content}
659
+ ${marker}` : content;
660
+ const written = await atomicWriteText(logPath, `${body}
661
+
662
+ ${appendedContent}
663
+ `);
664
+ if (!written.ok) {
665
+ return { exitCode: ExitCode.WRITE_FAILED, result: written };
666
+ }
667
+ const entriesAfter = entriesBefore + 1;
668
+ return {
669
+ exitCode: ExitCode.OK,
670
+ result: ok({
671
+ entries_before: entriesBefore,
672
+ entries_after: entriesAfter,
673
+ appended: true,
674
+ humanHint: `appended log entry (${entriesBefore}->${entriesAfter})`
675
+ })
676
+ };
677
+ }
678
+ async function runLogAppend(input) {
679
+ try {
680
+ await stat(join4(input.vault, "SCHEMA.md"));
681
+ } catch {
682
+ return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
683
+ }
684
+ const content = (input.content ?? "").trim();
685
+ if (content.length === 0) {
686
+ return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--content must be a non-empty log entry" }) };
687
+ }
688
+ const sensitive = scanSensitiveContent(content, { file: "log.md" });
689
+ if (sensitive.length > 0) {
690
+ return {
691
+ exitCode: ExitCode.SENSITIVE_CONTENT_DETECTED,
692
+ result: err("SENSITIVE_CONTENT_DETECTED", { file: "log.md", findings: sensitive })
693
+ };
694
+ }
695
+ let marker;
696
+ if (input.operationId !== void 0) {
697
+ const operation = operationMarker(input.operationId);
698
+ if (!operation.ok) return { exitCode: ExitCode.USAGE, result: operation };
699
+ marker = operation.data;
700
+ }
701
+ const acquired = await acquireLogLock(input.vault, input.strictLock ? { reclaimStale: false } : {});
702
+ if (!acquired.ok) {
703
+ if (acquired.error === "WRITE_FAILED") {
704
+ return { exitCode: ExitCode.WRITE_FAILED, result: acquired };
705
+ }
706
+ return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
707
+ }
708
+ const lockHandle = acquired.data;
709
+ const logPath = join4(input.vault, "log.md");
710
+ let outcome;
711
+ let released;
712
+ try {
713
+ outcome = await appendWhileLocked(logPath, content, marker);
714
+ } catch (error) {
715
+ outcome = {
716
+ exitCode: ExitCode.WRITE_FAILED,
717
+ result: err("WRITE_FAILED", { stage: "log-append", message: String(error) })
718
+ };
719
+ } finally {
720
+ released = releaseLogLock(lockHandle);
721
+ }
722
+ if (released === void 0 || !released.ok) {
723
+ return {
724
+ exitCode: ExitCode.WRITE_FAILED,
725
+ result: err("WRITE_FAILED", { stage: "log-unlock" })
726
+ };
727
+ }
728
+ if (outcome === void 0) {
729
+ return {
730
+ exitCode: ExitCode.WRITE_FAILED,
731
+ result: err("WRITE_FAILED", { stage: "log-append" })
732
+ };
733
+ }
734
+ if (outcome.result.ok && outcome.result.data.appended && input.recordLastOp !== false) {
735
+ try {
736
+ appendLastOp(input.vault, {
737
+ operation: "log-append",
738
+ summary: `appended log entry (${outcome.result.data.entries_before}->${outcome.result.data.entries_after})`,
739
+ files: ["log.md"],
740
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
741
+ });
742
+ } catch (error) {
743
+ return {
744
+ exitCode: ExitCode.WRITE_FAILED,
745
+ result: err("WRITE_FAILED", { stage: "last-op", message: String(error) })
746
+ };
747
+ }
748
+ }
749
+ return outcome;
750
+ }
751
+
752
+ // src/utils/sync-lock.ts
753
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
754
+ import { join as join5 } from "path";
755
+ import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
756
+ function getEnvSessionId() {
757
+ if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
758
+ if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
759
+ return void 0;
760
+ }
761
+ function getSessionId() {
762
+ const envSessionId = getEnvSessionId();
763
+ if (envSessionId) return envSessionId;
764
+ return process.pid.toString();
765
+ }
766
+ function getCwdHash(cwd) {
767
+ const path = cwd || process.cwd();
768
+ const hash = createHash2("sha256").update(path).digest("hex");
769
+ return hash.slice(0, 8);
770
+ }
771
+ function getCliSessionId(cwd) {
772
+ const envSessionId = getEnvSessionId();
773
+ if (envSessionId) return envSessionId;
774
+ return `cli-${getCwdHash(cwd)}`;
775
+ }
776
+ function lockPath(vault) {
777
+ return join5(vault, ".skillwiki", "sync.lock");
778
+ }
779
+ function readLock(vault) {
780
+ const path = lockPath(vault);
781
+ if (!existsSync3(path)) return null;
782
+ try {
783
+ const raw = readFileSync3(path, "utf8");
784
+ return JSON.parse(raw);
785
+ } catch {
786
+ return null;
787
+ }
788
+ }
789
+ function isStale(lock, now) {
790
+ const nowTime = (now ?? /* @__PURE__ */ new Date()).getTime();
791
+ const expiresTime = new Date(lock.expires).getTime();
792
+ return expiresTime < nowTime;
793
+ }
794
+ function acquireLock(vault, opts = {}) {
795
+ const path = lockPath(vault);
796
+ const dir = join5(vault, ".skillwiki");
797
+ if (!existsSync3(dir)) {
798
+ mkdirSync3(dir, { recursive: true });
799
+ }
800
+ const sessionId = opts.sessionId ?? getSessionId();
801
+ const summary = opts.summary ?? "skillwiki sync";
802
+ const ttlMinutes = opts.ttlMinutes ?? 30;
803
+ const force = opts.force ?? false;
804
+ const now = /* @__PURE__ */ new Date();
805
+ const acquired = now.toISOString();
806
+ const expires = new Date(now.getTime() + ttlMinutes * 60 * 1e3).toISOString();
807
+ const lock = {
808
+ session_id: sessionId,
809
+ pid: process.pid,
810
+ cwd: process.cwd(),
811
+ summary,
812
+ acquired,
813
+ expires
814
+ };
815
+ try {
816
+ const content = JSON.stringify(lock, null, 2) + "\n";
817
+ writeFileSync3(path, content, { flag: "wx" });
818
+ return { ok: true, lock };
819
+ } catch (e) {
820
+ const err2 = e;
821
+ if (err2.code !== "EEXIST") throw err2;
822
+ }
823
+ const existing = readLock(vault);
824
+ if (!existing) {
825
+ writeLockedFile(path, lock);
826
+ return { ok: true, lock };
827
+ }
828
+ if (force || isStale(existing)) {
829
+ writeLockedFile(path, lock);
830
+ return { ok: true, lock };
831
+ }
832
+ return { ok: false, held: existing };
833
+ }
834
+ function writeLockedFile(path, lock) {
835
+ const tmp = path + ".tmp";
836
+ const content = JSON.stringify(lock, null, 2) + "\n";
837
+ writeFileSync3(tmp, content);
838
+ renameSync(tmp, path);
839
+ }
840
+ function releaseLock(vault, opts = {}) {
841
+ const path = lockPath(vault);
842
+ if (!existsSync3(path)) {
843
+ return { released: false };
844
+ }
845
+ const sessionId = opts.sessionId ?? getSessionId();
846
+ const existing = readLock(vault);
847
+ if (opts.force) {
848
+ try {
849
+ unlinkSync3(path);
850
+ const prior = existing && existing.session_id !== sessionId ? existing : void 0;
851
+ return { released: true, prior };
852
+ } catch {
853
+ return { released: false };
854
+ }
855
+ }
856
+ if (!existing || existing.session_id !== sessionId) {
857
+ return { released: false };
858
+ }
859
+ try {
860
+ unlinkSync3(path);
861
+ return { released: true };
862
+ } catch {
863
+ return { released: false };
864
+ }
865
+ }
866
+ function acquireOwnedSyncLock(vault, opts) {
867
+ const ownerToken = randomBytes3(16).toString("hex");
868
+ const sessionId = `publish-${process.pid}-${ownerToken.slice(0, 12)}`;
869
+ const now = /* @__PURE__ */ new Date();
870
+ const lock = {
871
+ session_id: sessionId,
872
+ owner_token: ownerToken,
873
+ pid: process.pid,
874
+ cwd: process.cwd(),
875
+ summary: opts.summary,
876
+ acquired: now.toISOString(),
877
+ expires: new Date(now.getTime() + opts.ttlMinutes * 6e4).toISOString()
878
+ };
879
+ const path = lockPath(vault);
880
+ try {
881
+ mkdirSync3(join5(vault, ".skillwiki"), { recursive: true });
882
+ } catch (error) {
883
+ return err("WRITE_FAILED", { path, message: String(error) });
884
+ }
885
+ try {
886
+ writeFileSync3(path, JSON.stringify(lock, null, 2) + "\n", { flag: "wx" });
887
+ return ok({ vault, path, sessionId, ownerToken, acquired: lock.acquired });
888
+ } catch (error) {
889
+ if (error.code === "EEXIST") {
890
+ const held = readLock(vault);
891
+ return err("SYNC_LOCK_HELD", { vault, held, malformed: held === null });
892
+ }
893
+ return err("WRITE_FAILED", { path, message: String(error) });
894
+ }
895
+ }
896
+ function releaseOwnedSyncLock(handle) {
897
+ const existing = readLock(handle.vault);
898
+ if (!existing || existing.session_id !== handle.sessionId || existing.owner_token !== handle.ownerToken || existing.acquired !== handle.acquired) {
899
+ return err("SYNC_LOCK_HELD", {
900
+ message: "publication lock ownership changed; refusing release"
901
+ });
902
+ }
903
+ try {
904
+ unlinkSync3(handle.path);
905
+ return ok({ released: true });
906
+ } catch (error) {
907
+ return err("WRITE_FAILED", { path: handle.path, message: String(error) });
908
+ }
909
+ }
910
+
502
911
  // src/commands/validate.ts
503
- var TYPE_TO_SECTION = {
912
+ import { createHash as createHash3 } from "crypto";
913
+ import { readFile as readFile4 } from "fs/promises";
914
+ import { resolve as resolve2, relative as relative2, sep as sep2 } from "path";
915
+
916
+ // src/parsers/frontmatter.ts
917
+ import yaml from "js-yaml";
918
+ var FM_OPEN = /^---\r?\n/;
919
+ function splitFrontmatter(text) {
920
+ if (!FM_OPEN.test(text)) return ok({ rawFrontmatter: "", body: text, bodyStart: 0 });
921
+ const afterOpen = text.replace(FM_OPEN, "");
922
+ const closeIdx = afterOpen.search(/\r?\n---\r?\n/);
923
+ if (closeIdx === -1) return err("MISSING_CLOSING_DELIMITER");
924
+ const rawFrontmatter = afterOpen.slice(0, closeIdx);
925
+ const closeMatch = afterOpen.slice(closeIdx).match(/\r?\n---\r?\n/);
926
+ const bodyStart = text.length - (afterOpen.length - closeIdx - closeMatch[0].length);
927
+ const body = text.slice(bodyStart);
928
+ return ok({ rawFrontmatter, body, bodyStart });
929
+ }
930
+ function extractFrontmatter(text) {
931
+ const split = splitFrontmatter(text);
932
+ if (!split.ok) return split;
933
+ if (!split.data.rawFrontmatter) return ok({});
934
+ try {
935
+ const parsed = yaml.load(split.data.rawFrontmatter, { schema: yaml.JSON_SCHEMA });
936
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return ok({});
937
+ return ok(parsed);
938
+ } catch (e) {
939
+ return err("INVALID_FRONTMATTER", { message: getErrorMessage(e) });
940
+ }
941
+ }
942
+
943
+ // src/utils/index-entry.ts
944
+ import { readFile as readFile3 } from "fs/promises";
945
+ import { join as join6 } from "path";
946
+ var TYPE_SECTION = {
504
947
  entity: "Entities",
505
948
  concept: "Concepts",
506
949
  comparison: "Comparisons",
507
950
  query: "Queries",
508
- summary: "Summaries",
509
951
  meta: "Meta"
510
952
  };
953
+ function renderIndexUpsert(text, input) {
954
+ const section = TYPE_SECTION[input.type];
955
+ if (!section) return err("SCHEME_REJECTED", { type: input.type });
956
+ const ref = input.target.replace(/\.md$/, "");
957
+ if (text.includes(`[[${ref}]]`)) return ok({ text, changed: false });
958
+ if (/[\r\n]/.test(input.title)) {
959
+ return err("SCHEME_REJECTED", { message: "index title must be one line" });
960
+ }
961
+ const newline = text.includes("\r\n") ? "\r\n" : "\n";
962
+ const header = `## ${section}`;
963
+ const entry = `- [[${ref}]] \u2014 ${input.title}`;
964
+ const heading = new RegExp(`^${header}[ \\t]*(?=\\r?$)`, "m").exec(text);
965
+ if (heading?.index !== void 0) {
966
+ const afterHeading = heading.index + heading[0].length;
967
+ const nextHeading = /^##[ \t]+/m.exec(text.slice(afterHeading));
968
+ const sectionEnd = nextHeading?.index === void 0 ? text.length : afterHeading + nextHeading.index;
969
+ const sectionText = text.slice(afterHeading, sectionEnd);
970
+ const trailingWhitespace2 = /(?:\r?\n[ \t]*)*$/.exec(sectionText)?.[0] ?? "";
971
+ const insertAt2 = sectionEnd - trailingWhitespace2.length;
972
+ const before2 = text.slice(0, insertAt2);
973
+ const leadingNewline = before2.endsWith("\n") ? "" : newline;
974
+ return ok({
975
+ text: before2 + leadingNewline + entry + text.slice(insertAt2),
976
+ changed: true
977
+ });
978
+ }
979
+ const trailingWhitespace = /(?:\r?\n[ \t]*)*$/.exec(text)?.[0] ?? "";
980
+ const insertAt = text.length - trailingWhitespace.length;
981
+ const before = text.slice(0, insertAt);
982
+ const separator = before.length === 0 ? "" : newline + newline;
983
+ return ok({
984
+ text: before + separator + header + newline + entry + text.slice(insertAt),
985
+ changed: true
986
+ });
987
+ }
988
+ async function upsertIndexEntry(input) {
989
+ const path = join6(input.vault, "index.md");
990
+ let current;
991
+ try {
992
+ current = await readFile3(path, "utf8");
993
+ } catch (error) {
994
+ return err("FILE_NOT_FOUND", { path, message: String(error) });
995
+ }
996
+ const rendered = renderIndexUpsert(current, input);
997
+ if (!rendered.ok) return rendered;
998
+ if (!rendered.data.changed) return ok({ changed: false });
999
+ const written = await atomicWriteText(path, rendered.data.text);
1000
+ return written.ok ? ok({ changed: written.data.changed }) : written;
1001
+ }
1002
+
1003
+ // src/utils/typed-page.ts
1004
+ import { lstatSync, realpathSync } from "fs";
1005
+ import { dirname as dirname2, posix, relative, resolve, sep } from "path";
1006
+ var TYPE_DIRECTORY = {
1007
+ entity: "entities",
1008
+ concept: "concepts",
1009
+ comparison: "comparisons",
1010
+ query: "queries",
1011
+ meta: "meta"
1012
+ };
1013
+ function validateTypedTarget(target) {
1014
+ const segments = target.split("/");
1015
+ if (target.length === 0 || posix.isAbsolute(target) || target.includes("\\") || posix.normalize(target) !== target || segments.some((segment) => segment === "" || segment === "." || segment === "..") || !/^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9._/-]*\.md$/.test(target)) {
1016
+ return err("VAULT_PATH_INVALID", { target, message: "unsafe typed-page target" });
1017
+ }
1018
+ return ok(target);
1019
+ }
1020
+ function assertTargetInsideVault(vault, target) {
1021
+ const validated = validateTypedTarget(target);
1022
+ if (!validated.ok) return validated;
1023
+ let vaultReal;
1024
+ try {
1025
+ vaultReal = realpathSync(vault);
1026
+ } catch {
1027
+ return err("VAULT_PATH_INVALID", { target, message: "vault realpath failed" });
1028
+ }
1029
+ const absolutePath2 = resolve(vaultReal, target);
1030
+ const parent = dirname2(absolutePath2);
1031
+ let parentReal;
1032
+ try {
1033
+ parentReal = realpathSync(parent);
1034
+ } catch {
1035
+ return err("VAULT_PATH_INVALID", { target, message: "target parent realpath failed" });
1036
+ }
1037
+ const parentRelative = relative(vaultReal, parentReal).split(sep).join("/");
1038
+ if (parentRelative === ".." || parentRelative.startsWith("../")) {
1039
+ return err("VAULT_PATH_INVALID", { target, message: "target parent escapes vault" });
1040
+ }
1041
+ if (parentReal !== parent) {
1042
+ return err("VAULT_PATH_INVALID", { target, message: "target parent may not be a symlink alias" });
1043
+ }
1044
+ let existingRealPath;
1045
+ try {
1046
+ const targetStat = lstatSync(absolutePath2);
1047
+ if (targetStat.isSymbolicLink()) {
1048
+ return err("VAULT_PATH_INVALID", { target, message: "target may not be a symlink" });
1049
+ }
1050
+ if (!targetStat.isFile()) {
1051
+ return err("VAULT_PATH_INVALID", { target, message: "existing target must be a regular file" });
1052
+ }
1053
+ try {
1054
+ existingRealPath = realpathSync(absolutePath2);
1055
+ } catch {
1056
+ return err("VAULT_PATH_INVALID", { target, message: "target realpath failed" });
1057
+ }
1058
+ } catch (error) {
1059
+ if (error.code !== "ENOENT") {
1060
+ return err("VAULT_PATH_INVALID", { target, message: "target lstat failed" });
1061
+ }
1062
+ }
1063
+ return ok({ absolutePath: absolutePath2, existingRealPath });
1064
+ }
1065
+ function invalidFrontmatter(target, issues) {
1066
+ return err("INVALID_FRONTMATTER", {
1067
+ target,
1068
+ errors: issues.map((issue) => ({ path: issue.path.join("."), message: issue.message }))
1069
+ });
1070
+ }
1071
+ function prepareTypedPage(content, target) {
1072
+ const safeTarget = validateTypedTarget(target);
1073
+ if (!safeTarget.ok) return safeTarget;
1074
+ const sensitive = scanSensitiveContent(content, { file: target });
1075
+ if (sensitive.length > 0) {
1076
+ return err("SENSITIVE_CONTENT_DETECTED", { file: target, findings: sensitive });
1077
+ }
1078
+ const frontmatter = extractFrontmatter(content);
1079
+ if (!frontmatter.ok) return frontmatter;
1080
+ const detected = detectSchema(frontmatter.data);
1081
+ if (detected.schema === "typed-knowledge") {
1082
+ const parsed = TypedKnowledgeSchema.safeParse(frontmatter.data);
1083
+ if (!parsed.success) return invalidFrontmatter(target, parsed.error.issues);
1084
+ const expectedDirectory = TYPE_DIRECTORY[parsed.data.type];
1085
+ if (!expectedDirectory || !target.startsWith(`${expectedDirectory}/`)) {
1086
+ return err("SCHEME_REJECTED", { target, type: parsed.data.type, message: "frontmatter type does not match target directory" });
1087
+ }
1088
+ return ok({
1089
+ target,
1090
+ title: parsed.data.title,
1091
+ type: parsed.data.type,
1092
+ tags: [...parsed.data.tags],
1093
+ content
1094
+ });
1095
+ }
1096
+ if (detected.schema === "meta") {
1097
+ const parsed = MetaSchema.safeParse(frontmatter.data);
1098
+ if (!parsed.success) return invalidFrontmatter(target, parsed.error.issues);
1099
+ if (!target.startsWith("meta/")) {
1100
+ return err("SCHEME_REJECTED", { target, type: "meta", message: "frontmatter type does not match target directory" });
1101
+ }
1102
+ return ok({
1103
+ target,
1104
+ title: parsed.data.title,
1105
+ type: "meta",
1106
+ tags: [...parsed.data.tags],
1107
+ content
1108
+ });
1109
+ }
1110
+ return invalidFrontmatter(target, []);
1111
+ }
1112
+
1113
+ // src/commands/validate.ts
511
1114
  var SCHEMAS = {
512
1115
  "typed-knowledge": TypedKnowledgeSchema,
513
1116
  "raw": RawSourceSchema,
@@ -518,7 +1121,7 @@ var SCHEMAS = {
518
1121
  async function runValidate(input) {
519
1122
  let text;
520
1123
  try {
521
- text = await readFile(input.file, "utf8");
1124
+ text = await readFile4(input.file, "utf8");
522
1125
  } catch {
523
1126
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
524
1127
  }
@@ -569,18 +1172,80 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
569
1172
  let logUpdated = false;
570
1173
  let applyHint = "";
571
1174
  if (input.apply && input.vault) {
572
- const absFile = resolve(input.file);
573
- const absVault = resolve(input.vault);
574
- const relPath = relative(absVault, absFile).split(sep).join("/");
1175
+ const absFile = resolve2(input.file);
1176
+ const absVault = resolve2(input.vault);
1177
+ const relPath = relative2(absVault, absFile).split(sep2).join("/");
575
1178
  if (relPath.startsWith("..")) {
576
1179
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { reason: `file ${input.file} is not inside vault ${input.vault}` }) };
577
1180
  }
578
- const pageType = "type" in parsed.data && typeof parsed.data.type === "string" ? parsed.data.type : "";
579
- const title = typeof parsed.data.title === "string" ? parsed.data.title : relPath.replace(/\.md$/, "");
1181
+ const operationId = createHash3("sha256").update("skillwiki-validate-apply-v1\0").update(relPath).update("\0").update(text).digest("hex");
580
1182
  if (det.schema === "typed-knowledge" || det.schema === "meta") {
581
- indexUpdated = await addToIndex(input.vault, relPath, title, pageType);
1183
+ const prepared = prepareTypedPage(text, relPath);
1184
+ if (!prepared.ok) {
1185
+ return { exitCode: ExitCode.INVALID_FRONTMATTER, result: prepared };
1186
+ }
1187
+ let lock;
1188
+ try {
1189
+ lock = acquireOwnedSyncLock(input.vault, {
1190
+ summary: `validate --apply ${relPath}`,
1191
+ ttlMinutes: 1
1192
+ });
1193
+ } catch (error) {
1194
+ return {
1195
+ exitCode: ExitCode.WRITE_FAILED,
1196
+ result: err("WRITE_FAILED", { stage: "lock", message: String(error) })
1197
+ };
1198
+ }
1199
+ if (!lock.ok) {
1200
+ return {
1201
+ exitCode: lock.error === "SYNC_LOCK_HELD" ? ExitCode.SYNC_LOCK_HELD : ExitCode.WRITE_FAILED,
1202
+ result: lock
1203
+ };
1204
+ }
1205
+ let index;
1206
+ let released;
1207
+ try {
1208
+ index = await upsertIndexEntry({
1209
+ vault: input.vault,
1210
+ target: relPath,
1211
+ title: prepared.data.title,
1212
+ type: prepared.data.type
1213
+ });
1214
+ } catch (error) {
1215
+ index = err("WRITE_FAILED", { stage: "index", message: String(error) });
1216
+ } finally {
1217
+ released = releaseOwnedSyncLock(lock.data);
1218
+ }
1219
+ if (released === void 0 || !released.ok) {
1220
+ return {
1221
+ exitCode: ExitCode.WRITE_FAILED,
1222
+ result: err("WRITE_FAILED", { stage: "unlock", detail: released?.detail })
1223
+ };
1224
+ }
1225
+ if (index === void 0 || !index.ok) {
1226
+ return {
1227
+ exitCode: ExitCode.WRITE_FAILED,
1228
+ result: index ?? err("WRITE_FAILED", { stage: "index" })
1229
+ };
1230
+ }
1231
+ indexUpdated = index.data.changed;
1232
+ }
1233
+ const logged = await runLogAppend({
1234
+ vault: input.vault,
1235
+ content: `## [${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}] validate | added: ${relPath}`,
1236
+ operationId,
1237
+ strictLock: true
1238
+ });
1239
+ if (!logged.result.ok) {
1240
+ return { exitCode: logged.exitCode, result: logged.result };
1241
+ }
1242
+ if (logged.exitCode !== ExitCode.OK) {
1243
+ return {
1244
+ exitCode: logged.exitCode,
1245
+ result: err("WRITE_FAILED", { message: "log append returned inconsistent success state" })
1246
+ };
582
1247
  }
583
- logUpdated = await appendToLog(input.vault, relPath);
1248
+ logUpdated = logged.result.data.appended;
584
1249
  if (indexUpdated) applyHint += `
585
1250
  index: added [[${relPath.replace(/\.md$/, "")}]]`;
586
1251
  if (logUpdated) applyHint += "\n log: appended entry";
@@ -594,69 +1259,15 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
594
1259
  humanHint: `VALID (${det.schema})${applyHint}`
595
1260
  }) };
596
1261
  }
597
- async function addToIndex(vault, relPath, title, pageType) {
598
- const section = TYPE_TO_SECTION[pageType];
599
- if (!section) return false;
600
- const indexPath = join(vault, "index.md");
601
- let text;
602
- try {
603
- text = await readFile(indexPath, "utf8");
604
- } catch {
605
- return false;
606
- }
607
- const ref = relPath.replace(/\.md$/, "");
608
- if (text.includes(`[[${ref}]]`)) return false;
609
- const entry = `- [[${ref}]] \u2014 ${title}`;
610
- const lines = text.split("\n");
611
- const sectionLine = `## ${section}`;
612
- const sectionIdx = lines.findIndex((l) => l.trim() === sectionLine);
613
- if (sectionIdx === -1) {
614
- while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
615
- lines.push("", sectionLine, entry);
616
- } else {
617
- let endIdx = sectionIdx + 1;
618
- while (endIdx < lines.length) {
619
- if (lines[endIdx].startsWith("## ")) break;
620
- endIdx++;
621
- }
622
- let insertAt = endIdx;
623
- while (insertAt > sectionIdx + 1 && lines[insertAt - 1].trim() === "") insertAt--;
624
- lines.splice(insertAt, 0, entry);
625
- }
626
- try {
627
- await writeFile(indexPath, lines.join("\n"), "utf8");
628
- } catch {
629
- return false;
630
- }
631
- return true;
632
- }
633
- async function appendToLog(vault, relPath) {
634
- const logPath = join(vault, "log.md");
635
- let text;
636
- try {
637
- text = await readFile(logPath, "utf8");
638
- } catch {
639
- return false;
640
- }
641
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
642
- const entry = `
643
- ## [${today}] validate | added: ${relPath}`;
644
- try {
645
- await writeFile(logPath, text.trimEnd() + entry, "utf8");
646
- } catch {
647
- return false;
648
- }
649
- return true;
650
- }
651
1262
 
652
1263
  // src/commands/graph.ts
653
- import { writeFile as writeFile2, mkdir } from "fs/promises";
654
- import { dirname } from "path";
1264
+ import { writeFile, mkdir } from "fs/promises";
1265
+ import { dirname as dirname3 } from "path";
655
1266
 
656
1267
  // src/utils/vault.ts
657
- import { existsSync, readFileSync } from "fs";
658
- import { readFile as readFile2, readdir, stat } from "fs/promises";
659
- import { join as join2, relative as relative2, sep as sep2 } from "path";
1268
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1269
+ import { readFile as readFile5, readdir, stat as stat2 } from "fs/promises";
1270
+ import { join as join7, relative as relative3, sep as sep3 } from "path";
660
1271
  var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
661
1272
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
662
1273
  var DEFAULT_IO_CONCURRENCY = 1;
@@ -681,11 +1292,11 @@ function resolveReadOnlyVaultRootWithMounts(root, mounts) {
681
1292
  return { root, mirrored: false };
682
1293
  }
683
1294
  const explicitMirror = process.env.SKILLWIKI_VAULT_READ_MIRROR;
684
- if (explicitMirror && existsSync(join2(explicitMirror, "SCHEMA.md"))) {
1295
+ if (explicitMirror && existsSync4(join7(explicitMirror, "SCHEMA.md"))) {
685
1296
  return { root: explicitMirror, mirrored: explicitMirror !== root };
686
1297
  }
687
1298
  const siblingMirror = `${root}-git`;
688
- if (isRcloneFuseVaultFromMounts(root, mounts) && existsSync(join2(siblingMirror, "SCHEMA.md"))) {
1299
+ if (isRcloneFuseVaultFromMounts(root, mounts) && existsSync4(join7(siblingMirror, "SCHEMA.md"))) {
689
1300
  return { root: siblingMirror, mirrored: true };
690
1301
  }
691
1302
  return { root, mirrored: false };
@@ -693,7 +1304,7 @@ function resolveReadOnlyVaultRootWithMounts(root, mounts) {
693
1304
  function resolveReadOnlyVaultRoot(root) {
694
1305
  let mounts = "";
695
1306
  try {
696
- mounts = readFileSync("/proc/mounts", "utf8");
1307
+ mounts = readFileSync4("/proc/mounts", "utf8");
697
1308
  } catch {
698
1309
  }
699
1310
  return resolveReadOnlyVaultRootWithMounts(root, mounts);
@@ -713,12 +1324,12 @@ async function mapWithConcurrency(items, limit, mapper) {
713
1324
  }
714
1325
  async function scanVault(root) {
715
1326
  try {
716
- await stat(join2(root, "SCHEMA.md"));
1327
+ await stat2(join7(root, "SCHEMA.md"));
717
1328
  } catch {
718
1329
  return err("VAULT_PATH_INVALID", { root, reason: "SCHEMA.md missing" });
719
1330
  }
720
1331
  const all = await walk(root);
721
- const rels = all.map((p) => ({ absPath: p, relPath: relative2(root, p).split(sep2).join("/") }));
1332
+ const rels = all.map((p) => ({ absPath: p, relPath: relative3(root, p).split(sep3).join("/") }));
722
1333
  return ok({
723
1334
  root,
724
1335
  allMarkdown: rels,
@@ -733,7 +1344,7 @@ async function walk(dir) {
733
1344
  const out = [];
734
1345
  const subdirs = [];
735
1346
  for (const e of entries) {
736
- const p = join2(dir, e.name);
1347
+ const p = join7(dir, e.name);
737
1348
  if (e.isDirectory()) {
738
1349
  if (SKIP_DIRS.has(e.name)) continue;
739
1350
  subdirs.push(p);
@@ -744,7 +1355,7 @@ async function walk(dir) {
744
1355
  return out;
745
1356
  }
746
1357
  async function readPage(p) {
747
- return readFile2(p.absPath, "utf8");
1358
+ return readFile5(p.absPath, "utf8");
748
1359
  }
749
1360
  async function readPageCached(p, cache) {
750
1361
  if (!cache) return readPage(p);
@@ -907,8 +1518,8 @@ async function runGraphBuild(input) {
907
1518
  const adamicAdar = computeAdamicAdar(adjacency);
908
1519
  const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
909
1520
  try {
910
- await mkdir(dirname(input.out), { recursive: true });
911
- await writeFile2(input.out, JSON.stringify({ adjacency, adamicAdar }, null, 2));
1521
+ await mkdir(dirname3(input.out), { recursive: true });
1522
+ await writeFile(input.out, JSON.stringify({ adjacency, adamicAdar }, null, 2));
912
1523
  } catch (e) {
913
1524
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
914
1525
  }
@@ -951,8 +1562,8 @@ function computeAdamicAdar(adj) {
951
1562
  }
952
1563
 
953
1564
  // src/utils/dotenv.ts
954
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
955
- import { dirname as dirname2 } from "path";
1565
+ import { readFile as readFile6, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1566
+ import { dirname as dirname4 } from "path";
956
1567
  var CONFIG_KEYS = [
957
1568
  "WIKI_PATH",
958
1569
  "WIKI_LANG",
@@ -993,7 +1604,7 @@ function parseDotenvText(text) {
993
1604
  async function parseDotenvFile(path) {
994
1605
  let text;
995
1606
  try {
996
- text = await readFile3(path, "utf8");
1607
+ text = await readFile6(path, "utf8");
997
1608
  } catch {
998
1609
  return {};
999
1610
  }
@@ -1001,8 +1612,8 @@ async function parseDotenvFile(path) {
1001
1612
  }
1002
1613
  async function writeDotenv(filePath, entries, originalContent) {
1003
1614
  const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
1004
- await mkdir2(dirname2(filePath), { recursive: true });
1005
- await writeFile3(filePath, lines.join("\n") + "\n", "utf8");
1615
+ await mkdir2(dirname4(filePath), { recursive: true });
1616
+ await writeFile2(filePath, lines.join("\n") + "\n", "utf8");
1006
1617
  }
1007
1618
  function freshLines(entries) {
1008
1619
  const out = [];
@@ -1045,7 +1656,7 @@ function updateLines(originalContent, entries) {
1045
1656
  }
1046
1657
 
1047
1658
  // src/utils/wiki-path.ts
1048
- import { join as join3 } from "path";
1659
+ import { join as join8 } from "path";
1049
1660
  async function resolveInitTimePath(input) {
1050
1661
  const chain = [];
1051
1662
  if (input.flag !== void 0 && input.flag.length > 0) {
@@ -1058,27 +1669,27 @@ async function resolveInitTimePath(input) {
1058
1669
  return { path: input.envValue, source: "env", ...input.explain ? { chain } : {} };
1059
1670
  }
1060
1671
  if (input.explain) chain.push({ source: "env", matched: false });
1061
- const sw = await parseDotenvFile(join3(input.home, ".skillwiki", ".env"));
1672
+ const sw = await parseDotenvFile(join8(input.home, ".skillwiki", ".env"));
1062
1673
  if (sw.WIKI_PATH !== void 0) {
1063
1674
  if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
1064
1675
  return { path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} };
1065
1676
  }
1066
1677
  if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
1067
- const hermes = await parseDotenvFile(join3(input.home, ".hermes", ".env"));
1678
+ const hermes = await parseDotenvFile(join8(input.home, ".hermes", ".env"));
1068
1679
  if (hermes.WIKI_PATH !== void 0) {
1069
1680
  if (input.explain) chain.push({ source: "hermes-dotenv", matched: true, value: hermes.WIKI_PATH });
1070
1681
  return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
1071
1682
  }
1072
1683
  if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
1073
1684
  if (input.cwd) {
1074
- const projCfg = await parseDotenvFile(join3(input.cwd, ".skillwiki", ".env"));
1685
+ const projCfg = await parseDotenvFile(join8(input.cwd, ".skillwiki", ".env"));
1075
1686
  if (projCfg.WIKI_PATH !== void 0) {
1076
1687
  if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
1077
1688
  return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
1078
1689
  }
1079
1690
  }
1080
1691
  if (input.explain) chain.push({ source: "project-dotenv", matched: false });
1081
- const fallback = join3(input.home, "wiki");
1692
+ const fallback = join8(input.home, "wiki");
1082
1693
  if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
1083
1694
  return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
1084
1695
  }
@@ -1089,7 +1700,7 @@ async function resolveRuntimePath(input) {
1089
1700
  return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
1090
1701
  }
1091
1702
  if (input.explain) chain.push({ source: "flag", matched: false });
1092
- const swGlobal = await parseDotenvFile(join3(input.home, ".skillwiki", ".env"));
1703
+ const swGlobal = await parseDotenvFile(join8(input.home, ".skillwiki", ".env"));
1093
1704
  const wikiName = input.wiki;
1094
1705
  if (wikiName !== void 0 && wikiName.length > 0) {
1095
1706
  if (wikiName.toLowerCase() === "default") {
@@ -1133,7 +1744,7 @@ async function resolveRuntimePath(input) {
1133
1744
  }
1134
1745
  if (input.explain) chain.push({ source: "env", matched: false });
1135
1746
  if (input.cwd) {
1136
- const projCfg = await parseDotenvFile(join3(input.cwd, ".skillwiki", ".env"));
1747
+ const projCfg = await parseDotenvFile(join8(input.cwd, ".skillwiki", ".env"));
1137
1748
  if (projCfg.WIKI_PATH !== void 0) {
1138
1749
  if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
1139
1750
  return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
@@ -1253,8 +1864,8 @@ function simulateRemoval(adj, removed) {
1253
1864
  }
1254
1865
 
1255
1866
  // src/commands/audit.ts
1256
- import { readFile as readFile4, stat as stat3 } from "fs/promises";
1257
- import { dirname as dirname3, resolve as resolve2, join as join5 } from "path";
1867
+ import { readFile as readFile7, stat as stat4 } from "fs/promises";
1868
+ import { dirname as dirname5, resolve as resolve3, join as join10 } from "path";
1258
1869
 
1259
1870
  // src/parsers/citations.ts
1260
1871
  var FENCE2 = /```[\s\S]*?```/g;
@@ -1364,9 +1975,9 @@ function hasWikilinkCitations(body) {
1364
1975
  }
1365
1976
 
1366
1977
  // src/utils/raw-source.ts
1367
- import { existsSync as existsSync2 } from "fs";
1368
- import { stat as stat2 } from "fs/promises";
1369
- import { join as join4 } from "path";
1978
+ import { existsSync as existsSync5 } from "fs";
1979
+ import { stat as stat3 } from "fs/promises";
1980
+ import { join as join9 } from "path";
1370
1981
  function normalizeRawSourceTarget(entry) {
1371
1982
  let target = entry.trim().replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
1372
1983
  target = target.replace(/^\^\[/, "").replace(/\]$/, "");
@@ -1376,21 +1987,21 @@ function normalizeRawSourceTarget(entry) {
1376
1987
  function rawSourceTargetCandidates(vault, target) {
1377
1988
  const normalized = normalizeRawSourceTarget(target);
1378
1989
  if (!normalized) return [];
1379
- const candidates = [join4(vault, normalized)];
1380
- if (!normalized.endsWith(".md")) candidates.push(join4(vault, `${normalized}.md`));
1990
+ const candidates = [join9(vault, normalized)];
1991
+ if (!normalized.endsWith(".md")) candidates.push(join9(vault, `${normalized}.md`));
1381
1992
  if (normalized.startsWith("raw/")) {
1382
- candidates.push(join4(vault, "_archive", normalized));
1383
- if (!normalized.endsWith(".md")) candidates.push(join4(vault, "_archive", `${normalized}.md`));
1993
+ candidates.push(join9(vault, "_archive", normalized));
1994
+ if (!normalized.endsWith(".md")) candidates.push(join9(vault, "_archive", `${normalized}.md`));
1384
1995
  }
1385
1996
  return [...new Set(candidates)];
1386
1997
  }
1387
1998
  function rawSourceTargetExistsSync(vault, target) {
1388
- return rawSourceTargetCandidates(vault, target).some((candidate) => existsSync2(candidate));
1999
+ return rawSourceTargetCandidates(vault, target).some((candidate) => existsSync5(candidate));
1389
2000
  }
1390
2001
  async function rawSourceTargetExists(vault, target) {
1391
2002
  for (const candidate of rawSourceTargetCandidates(vault, target)) {
1392
2003
  try {
1393
- await stat2(candidate);
2004
+ await stat3(candidate);
1394
2005
  return true;
1395
2006
  } catch {
1396
2007
  }
@@ -1402,7 +2013,7 @@ async function rawSourceTargetExists(vault, target) {
1402
2013
  async function runAudit(input) {
1403
2014
  let text;
1404
2015
  try {
1405
- text = await readFile4(input.file, "utf8");
2016
+ text = await readFile7(input.file, "utf8");
1406
2017
  } catch {
1407
2018
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
1408
2019
  }
@@ -1410,7 +2021,7 @@ async function runAudit(input) {
1410
2021
  if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
1411
2022
  const split = splitFrontmatter(text);
1412
2023
  const body = split.ok ? split.data.body : text;
1413
- const vault = await findVaultRoot(dirname3(resolve2(input.file)));
2024
+ const vault = await findVaultRoot(dirname5(resolve3(input.file)));
1414
2025
  if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
1415
2026
  const markers = extractCitationMarkers(body);
1416
2027
  const resolved = await Promise.all(markers.map(async (m) => {
@@ -1455,11 +2066,11 @@ async function findVaultRoot(start) {
1455
2066
  let cur = start;
1456
2067
  for (let i = 0; i < 20; i++) {
1457
2068
  try {
1458
- await stat3(join5(cur, "SCHEMA.md"));
2069
+ await stat4(join10(cur, "SCHEMA.md"));
1459
2070
  return cur;
1460
2071
  } catch {
1461
2072
  }
1462
- const parent = dirname3(cur);
2073
+ const parent = dirname5(cur);
1463
2074
  if (parent === cur) return null;
1464
2075
  cur = parent;
1465
2076
  }
@@ -1547,32 +2158,123 @@ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }
1547
2158
  }
1548
2159
 
1549
2160
  // src/commands/tag-audit.ts
1550
- import { readFile as readFile5 } from "fs/promises";
1551
- import { join as join6 } from "path";
2161
+ import { readFile as readFile8 } from "fs/promises";
2162
+ import { join as join11 } from "path";
1552
2163
 
1553
2164
  // src/parsers/taxonomy.ts
1554
2165
  import yaml2 from "js-yaml";
1555
- var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
1556
- function extractTaxonomy(schemaText) {
1557
- const m = schemaText.match(FENCE_RE);
1558
- if (!m) return err("NO_TAXONOMY_BLOCK", { message: "No fenced YAML taxonomy block found in SCHEMA.md" });
2166
+ var TAG_SLUG_RE = /^[a-z0-9][a-z0-9_./-]*$/;
2167
+ function taxonomyItemIndent(yamlText) {
2168
+ const lines = yamlText.split(/\r?\n/);
2169
+ const taxonomyLine = lines.findIndex((line) => /^taxonomy:[ \t]*(?:#.*)?$/.test(line));
2170
+ if (taxonomyLine === -1) return void 0;
2171
+ for (const line of lines.slice(taxonomyLine + 1)) {
2172
+ if (/^[ \t]*(?:#.*)?$/.test(line)) continue;
2173
+ return /^([ \t]+)-[ \t]+/.exec(line)?.[1];
2174
+ }
2175
+ return void 0;
2176
+ }
2177
+ function parseTaxonomyDocument(schemaText) {
2178
+ const heading = /^##[ \t]+Tag Taxonomy[ \t]*\r?$/m.exec(schemaText);
2179
+ if (!heading || heading.index === void 0) {
2180
+ return err("NO_TAXONOMY_BLOCK", { message: "Tag Taxonomy heading not found" });
2181
+ }
2182
+ const afterHeading = heading.index + heading[0].length;
2183
+ const unboundedTail = schemaText.slice(afterHeading);
2184
+ const nextHeading = /^#{1,2}[ \t]+/m.exec(unboundedTail);
2185
+ const sectionEnd = nextHeading?.index === void 0 ? schemaText.length : afterHeading + nextHeading.index;
2186
+ const sectionText = schemaText.slice(afterHeading, sectionEnd);
2187
+ const open2 = /^```yaml[ \t]*\r?$/m.exec(sectionText);
2188
+ if (!open2 || open2.index === void 0) {
2189
+ return err("NO_TAXONOMY_BLOCK", { message: "Fenced YAML taxonomy block not found" });
2190
+ }
2191
+ const openStart = afterHeading + open2.index;
2192
+ const yamlStart = openStart + open2[0].length + 1;
2193
+ const afterOpen = schemaText.slice(yamlStart, sectionEnd);
2194
+ const close = /^```[ \t]*\r?$/m.exec(afterOpen);
2195
+ if (!close || close.index === void 0) {
2196
+ return err("NO_TAXONOMY_BLOCK", { message: "Taxonomy closing fence not found" });
2197
+ }
2198
+ const closingFenceStart = yamlStart + close.index;
2199
+ const newline = schemaText.slice(closingFenceStart - 2, closingFenceStart) === "\r\n" ? "\r\n" : "\n";
2200
+ const yamlEnd = closingFenceStart - newline.length;
2201
+ const yamlText = schemaText.slice(yamlStart, yamlEnd);
1559
2202
  let parsed;
1560
2203
  try {
1561
- parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
1562
- } catch (e) {
1563
- return err("INVALID_FRONTMATTER", { message: getErrorMessage(e) });
2204
+ parsed = yaml2.load(yamlText, { schema: yaml2.JSON_SCHEMA });
2205
+ } catch (error) {
2206
+ return err("INVALID_FRONTMATTER", { message: getErrorMessage(error) });
1564
2207
  }
1565
- if (parsed === null || typeof parsed !== "object") {
2208
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1566
2209
  return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
1567
2210
  }
1568
- const tax = parsed.taxonomy;
1569
- if (!Array.isArray(tax)) {
1570
- return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
1571
- }
1572
- if (!tax.every((x) => typeof x === "string")) {
2211
+ const tags = parsed.taxonomy;
2212
+ if (!Array.isArray(tags) || !tags.every((tag) => typeof tag === "string")) {
1573
2213
  return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
1574
2214
  }
1575
- return ok(tax);
2215
+ const itemIndent = taxonomyItemIndent(yamlText) ?? " ";
2216
+ return ok({ tags, yamlStart, yamlEnd, closingFenceStart, newline, itemIndent });
2217
+ }
2218
+ function extractTaxonomy(schemaText) {
2219
+ const parsed = parseTaxonomyDocument(schemaText);
2220
+ if ("error" in parsed) return err(parsed.error, parsed.detail);
2221
+ return ok(parsed.data.tags);
2222
+ }
2223
+ function renderTag(tag) {
2224
+ const roundTrip = yaml2.load(`value: ${tag}
2225
+ `, { schema: yaml2.JSON_SCHEMA });
2226
+ return typeof roundTrip.value === "string" && roundTrip.value === tag ? tag : JSON.stringify(tag);
2227
+ }
2228
+ function taxonomyCommentForPage(page, date, reason) {
2229
+ const cycle = /^queries\/\d{4}-\d{2}-\d{2}-research-cycle-(\d+)-report\.md$/.exec(page);
2230
+ const chosen = reason?.trim() || (cycle ? `research-cycle ${cycle[1]} taxonomy reconciliation` : `taxonomy reconciliation for ${page}`);
2231
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._/-]{0,159}$/.test(chosen)) {
2232
+ return err("SCHEME_REJECTED", {
2233
+ message: "reconciliation reason contains unsupported characters or is too long"
2234
+ });
2235
+ }
2236
+ return ok(`# -- added ${date}: ${chosen} --`);
2237
+ }
2238
+ function reconcileTaxonomyDocument(schemaText, input) {
2239
+ const document = parseTaxonomyDocument(schemaText);
2240
+ if ("error" in document) return err(document.error, document.detail);
2241
+ const requested = [...new Set(input.tags)].sort();
2242
+ const existingSet = new Set(document.data.tags);
2243
+ const missing = requested.filter((tag) => !existingSet.has(tag));
2244
+ const invalid = missing.filter((tag) => !TAG_SLUG_RE.test(tag));
2245
+ if (invalid.length > 0) {
2246
+ return err("SCHEME_REJECTED", { message: "invalid taxonomy tag", tags: invalid });
2247
+ }
2248
+ if (missing.length === 0) {
2249
+ return ok({
2250
+ text: schemaText,
2251
+ requested,
2252
+ existing: document.data.tags,
2253
+ missing: [],
2254
+ added: [],
2255
+ changed: false
2256
+ });
2257
+ }
2258
+ const yamlText = schemaText.slice(document.data.yamlStart, document.data.yamlEnd);
2259
+ const itemIndent = taxonomyItemIndent(yamlText);
2260
+ if (!itemIndent) {
2261
+ return err("SCHEME_REJECTED", {
2262
+ message: "taxonomy reconciliation requires a block-style taxonomy list"
2263
+ });
2264
+ }
2265
+ const { newline, closingFenceStart } = document.data;
2266
+ const comment = `${itemIndent}${input.comment}`;
2267
+ const items = missing.map((tag) => `${itemIndent}- ${renderTag(tag)}`);
2268
+ const block = `${comment}${newline}${items.join(newline)}${newline}`;
2269
+ const text = schemaText.slice(0, closingFenceStart) + block + schemaText.slice(closingFenceStart);
2270
+ return ok({
2271
+ text,
2272
+ requested,
2273
+ existing: document.data.tags,
2274
+ missing,
2275
+ added: missing,
2276
+ changed: true
2277
+ });
1576
2278
  }
1577
2279
 
1578
2280
  // src/commands/tag-audit.ts
@@ -1580,7 +2282,7 @@ async function runTagAudit(input) {
1580
2282
  const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
1581
2283
  if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
1582
2284
  const scan = scanResult.data;
1583
- const schemaText = await readFile5(join6(input.vault, "SCHEMA.md"), "utf8");
2285
+ const schemaText = await readFile8(join11(input.vault, "SCHEMA.md"), "utf8");
1584
2286
  const tax = extractTaxonomy(schemaText);
1585
2287
  if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
1586
2288
  const allowed = new Set(tax.data);
@@ -1612,14 +2314,14 @@ async function runTagAudit(input) {
1612
2314
  }
1613
2315
 
1614
2316
  // src/commands/index-check.ts
1615
- import { readFile as readFile6 } from "fs/promises";
1616
- import { join as join7 } from "path";
2317
+ import { readFile as readFile9 } from "fs/promises";
2318
+ import { join as join12 } from "path";
1617
2319
  async function runIndexCheck(input) {
1618
2320
  const scan = input.scan ? ok(input.scan) : await scanVault(input.vault);
1619
2321
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1620
2322
  let indexText = "";
1621
2323
  try {
1622
- indexText = await readFile6(join7(input.vault, "index.md"), "utf8");
2324
+ indexText = await readFile9(join12(input.vault, "index.md"), "utf8");
1623
2325
  } catch {
1624
2326
  }
1625
2327
  const indexSlugsLower = /* @__PURE__ */ new Map();
@@ -1658,8 +2360,8 @@ async function runIndexCheck(input) {
1658
2360
  }
1659
2361
 
1660
2362
  // src/commands/stale.ts
1661
- import { readdir as readdir2, rename, mkdir as mkdir3, readFile as readFile7 } from "fs/promises";
1662
- import { join as join9 } from "path";
2363
+ import { readdir as readdir2, rename as rename2, mkdir as mkdir3, readFile as readFile10 } from "fs/promises";
2364
+ import { join as join13 } from "path";
1663
2365
 
1664
2366
  // src/parsers/expiry-annotations.ts
1665
2367
  var HEADING_RE = /^#{1,6}\s+(.+)$/;
@@ -1695,48 +2397,6 @@ function parseExpiryAnnotations(content, pagePath) {
1695
2397
  return annotations;
1696
2398
  }
1697
2399
 
1698
- // src/utils/last-op.ts
1699
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync, unlinkSync, existsSync as existsSync3 } from "fs";
1700
- import { join as join8 } from "path";
1701
- var LAST_OP_DIR = ".skillwiki";
1702
- var LAST_OP_FILE = "last-op.json";
1703
- function lastOpPath(vault) {
1704
- return join8(vault, LAST_OP_DIR, LAST_OP_FILE);
1705
- }
1706
- function readLastOp(vault) {
1707
- const p = lastOpPath(vault);
1708
- if (!existsSync3(p)) return [];
1709
- try {
1710
- const raw = readFileSync2(p, "utf8");
1711
- const parsed = JSON.parse(raw);
1712
- if (!Array.isArray(parsed)) {
1713
- unlinkSync(p);
1714
- return [];
1715
- }
1716
- return parsed;
1717
- } catch {
1718
- try {
1719
- unlinkSync(p);
1720
- } catch (_e) {
1721
- }
1722
- return [];
1723
- }
1724
- }
1725
- function appendLastOp(vault, entry) {
1726
- const existing = readLastOp(vault);
1727
- existing.push(entry);
1728
- const dir = join8(vault, LAST_OP_DIR);
1729
- if (!existsSync3(dir)) mkdirSync(dir, { recursive: true });
1730
- writeFileSync(lastOpPath(vault), JSON.stringify(existing, null, 2), "utf8");
1731
- }
1732
- function clearLastOp(vault) {
1733
- const p = lastOpPath(vault);
1734
- try {
1735
- unlinkSync(p);
1736
- } catch (_e) {
1737
- }
1738
- }
1739
-
1740
2400
  // src/commands/stale.ts
1741
2401
  function daysSince(isoDate2) {
1742
2402
  return Math.floor((Date.now() - Date.parse(isoDate2)) / 864e5);
@@ -1750,7 +2410,7 @@ async function runStale(input) {
1750
2410
  const archived = [];
1751
2411
  const workDirs = /* @__PURE__ */ new Map();
1752
2412
  const workDirsBySlug = /* @__PURE__ */ new Map();
1753
- const projectsDir = join9(input.vault, "projects");
2413
+ const projectsDir = join13(input.vault, "projects");
1754
2414
  let projectSlugs = [];
1755
2415
  try {
1756
2416
  projectSlugs = (await readdir2(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
@@ -1763,7 +2423,7 @@ async function runStale(input) {
1763
2423
  projectSlugs = [input.project];
1764
2424
  }
1765
2425
  for (const slug of projectSlugs) {
1766
- const workPath = join9(projectsDir, slug, "work");
2426
+ const workPath = join13(projectsDir, slug, "work");
1767
2427
  let entries;
1768
2428
  try {
1769
2429
  entries = await readdir2(workPath, { withFileTypes: true });
@@ -1774,7 +2434,7 @@ async function runStale(input) {
1774
2434
  for (const e of entries) {
1775
2435
  if (!e.isDirectory()) continue;
1776
2436
  const relDir = `projects/${slug}/work/${e.name}`;
1777
- const absDir = join9(workPath, e.name);
2437
+ const absDir = join13(workPath, e.name);
1778
2438
  let status = "";
1779
2439
  let files;
1780
2440
  try {
@@ -1787,7 +2447,7 @@ async function runStale(input) {
1787
2447
  for (const f of files) {
1788
2448
  if (!f.endsWith(".md")) continue;
1789
2449
  try {
1790
- const fm = extractFrontmatter(await readFile7(join9(absDir, f), "utf8"));
2450
+ const fm = extractFrontmatter(await readFile10(join13(absDir, f), "utf8"));
1791
2451
  if (fm.ok && typeof fm.data.status === "string") {
1792
2452
  status = fm.data.status;
1793
2453
  break;
@@ -1883,9 +2543,9 @@ async function runStale(input) {
1883
2543
  }
1884
2544
  }
1885
2545
  await mapWithConcurrency([...workDirs.keys()], vaultIoConcurrency(), async (relDir) => {
1886
- const specPath = join9(input.vault, relDir, "spec.md");
2546
+ const specPath = join13(input.vault, relDir, "spec.md");
1887
2547
  try {
1888
- const specContent = await readFile7(specPath, "utf8");
2548
+ const specContent = await readFile10(specPath, "utf8");
1889
2549
  const specFm = extractFrontmatter(specContent);
1890
2550
  if (specFm.ok && typeof specFm.data.source === "string") {
1891
2551
  const sourcePath = specFm.data.source;
@@ -1914,7 +2574,7 @@ async function runStale(input) {
1914
2574
  if (daysSince(dateStr) < input.days) continue;
1915
2575
  let files;
1916
2576
  try {
1917
- files = await readdir2(join9(input.vault, relDir));
2577
+ files = await readdir2(join13(input.vault, relDir));
1918
2578
  } catch {
1919
2579
  continue;
1920
2580
  }
@@ -1984,7 +2644,7 @@ async function runStale(input) {
1984
2644
  staleSections.push(...staleSectionResults.flat());
1985
2645
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1986
2646
  if (input.archive) {
1987
- const archiveDir = join9(input.vault, "_archive", today);
2647
+ const archiveDir = join13(input.vault, "_archive", today);
1988
2648
  await mkdir3(archiveDir, { recursive: true });
1989
2649
  const citedRawPaths = /* @__PURE__ */ new Set();
1990
2650
  for (const page of scan.typedKnowledge) {
@@ -2000,9 +2660,9 @@ async function runStale(input) {
2000
2660
  }
2001
2661
  for (const t of staleTranscripts) {
2002
2662
  if (citedRawPaths.has(t.path) || citedRawPaths.has(t.path.replace(/\.md$/, ""))) continue;
2003
- const dest = join9(archiveDir, t.path.split("/").pop());
2663
+ const dest = join13(archiveDir, t.path.split("/").pop());
2004
2664
  try {
2005
- await rename(join9(input.vault, t.path), dest);
2665
+ await rename2(join13(input.vault, t.path), dest);
2006
2666
  archived.push(t.path);
2007
2667
  } catch {
2008
2668
  }
@@ -2012,18 +2672,18 @@ async function runStale(input) {
2012
2672
  if (parts.length >= 4 && parts[0] === "projects") {
2013
2673
  const slug = parts[1];
2014
2674
  const itemName = parts[3];
2015
- const histDir = join9(input.vault, "projects", slug, "history", "archived-work");
2675
+ const histDir = join13(input.vault, "projects", slug, "history", "archived-work");
2016
2676
  await mkdir3(histDir, { recursive: true });
2017
- const dest = join9(histDir, itemName);
2677
+ const dest = join13(histDir, itemName);
2018
2678
  try {
2019
- await rename(join9(input.vault, w.path), dest);
2679
+ await rename2(join13(input.vault, w.path), dest);
2020
2680
  archived.push(w.path);
2021
2681
  } catch {
2022
2682
  }
2023
2683
  } else {
2024
- const dest = join9(archiveDir, w.path.replace(/\//g, "_"));
2684
+ const dest = join13(archiveDir, w.path.replace(/\//g, "_"));
2025
2685
  try {
2026
- await rename(join9(input.vault, w.path), dest);
2686
+ await rename2(join13(input.vault, w.path), dest);
2027
2687
  archived.push(w.path);
2028
2688
  } catch {
2029
2689
  }
@@ -2078,23 +2738,23 @@ async function runPagesize(input) {
2078
2738
  }
2079
2739
 
2080
2740
  // src/commands/log-rotate.ts
2081
- import { readFile as readFile8, rename as rename2, writeFile as writeFile4, stat as stat4 } from "fs/promises";
2082
- import { join as join10 } from "path";
2083
- var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
2741
+ import { readFile as readFile11, rename as rename3, writeFile as writeFile3, stat as stat5 } from "fs/promises";
2742
+ import { join as join14 } from "path";
2743
+ var ENTRY_RE2 = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
2084
2744
  async function runLogRotate(input) {
2085
2745
  try {
2086
- await stat4(join10(input.vault, "SCHEMA.md"));
2746
+ await stat5(join14(input.vault, "SCHEMA.md"));
2087
2747
  } catch {
2088
2748
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
2089
2749
  }
2090
- const logPath = join10(input.vault, "log.md");
2750
+ const logPath = join14(input.vault, "log.md");
2091
2751
  let logText;
2092
2752
  try {
2093
- logText = await readFile8(logPath, "utf8");
2753
+ logText = await readFile11(logPath, "utf8");
2094
2754
  } catch {
2095
2755
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
2096
2756
  }
2097
- const matches = [...logText.matchAll(ENTRY_RE)];
2757
+ const matches = [...logText.matchAll(ENTRY_RE2)];
2098
2758
  const entries = matches.length;
2099
2759
  if (entries < input.threshold) {
2100
2760
  return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
@@ -2107,9 +2767,9 @@ async function runLogRotate(input) {
2107
2767
  }
2108
2768
  const newestYear = matches[matches.length - 1][1];
2109
2769
  const rotatedName = `log-${newestYear}.md`;
2110
- const rotatedPath = join10(input.vault, rotatedName);
2770
+ const rotatedPath = join14(input.vault, rotatedName);
2111
2771
  try {
2112
- await rename2(logPath, rotatedPath);
2772
+ await rename3(logPath, rotatedPath);
2113
2773
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2114
2774
  const fresh = `# Vault Log
2115
2775
 
@@ -2119,7 +2779,7 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
2119
2779
 
2120
2780
  - Previous log moved to ${rotatedName}
2121
2781
  `;
2122
- await writeFile4(logPath, fresh, "utf8");
2782
+ await writeFile3(logPath, fresh, "utf8");
2123
2783
  } catch (e) {
2124
2784
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
2125
2785
  }
@@ -2152,13 +2812,13 @@ async function runTopicMapCheck(input) {
2152
2812
  }
2153
2813
 
2154
2814
  // src/commands/index-link-format.ts
2155
- import { readFile as readFile9 } from "fs/promises";
2156
- import { join as join11 } from "path";
2815
+ import { readFile as readFile12 } from "fs/promises";
2816
+ import { join as join15 } from "path";
2157
2817
  var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
2158
2818
  async function runIndexLinkFormat(input) {
2159
2819
  let text = "";
2160
2820
  try {
2161
- text = await readFile9(join11(input.vault, "index.md"), "utf8");
2821
+ text = await readFile12(join15(input.vault, "index.md"), "utf8");
2162
2822
  } catch {
2163
2823
  }
2164
2824
  const markdown_links = [];
@@ -2171,9 +2831,9 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
2171
2831
  }
2172
2832
 
2173
2833
  // src/commands/dedup.ts
2174
- import { createHash as createHash2 } from "crypto";
2175
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
2176
- import { dirname as dirname4, join as join12, resolve as resolve3 } from "path";
2834
+ import { createHash as createHash4 } from "crypto";
2835
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
2836
+ import { dirname as dirname6, join as join16, resolve as resolve4 } from "path";
2177
2837
 
2178
2838
  // src/utils/rclone.ts
2179
2839
  import { execFile } from "child_process";
@@ -2265,7 +2925,7 @@ async function runDedup(input) {
2265
2925
  const manifest = safeEntries.length > 0 ? {
2266
2926
  version: 1,
2267
2927
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
2268
- vault: resolve3(input.vault),
2928
+ vault: resolve4(input.vault),
2269
2929
  entries: safeEntries
2270
2930
  } : void 0;
2271
2931
  const remote = await planAndMaybePruneRemote(input, safeEntries);
@@ -2277,8 +2937,8 @@ async function runDedup(input) {
2277
2937
  }
2278
2938
  if (input.manifestOut && manifest) {
2279
2939
  try {
2280
- mkdirSync2(dirname4(input.manifestOut), { recursive: true });
2281
- writeFileSync2(input.manifestOut, `${JSON.stringify(manifest, null, 2)}
2940
+ mkdirSync4(dirname6(input.manifestOut), { recursive: true });
2941
+ writeFileSync4(input.manifestOut, `${JSON.stringify(manifest, null, 2)}
2282
2942
  `, "utf-8");
2283
2943
  } catch (e) {
2284
2944
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: input.manifestOut, message: String(e) }) };
@@ -2292,7 +2952,7 @@ async function runDedup(input) {
2292
2952
  }
2293
2953
  }
2294
2954
  for (const page of scan.allMarkdown.filter((p) => !p.relPath.startsWith("raw/"))) {
2295
- const text = readFileSync3(join12(input.vault, page.relPath), "utf-8");
2955
+ const text = readFileSync5(join16(input.vault, page.relPath), "utf-8");
2296
2956
  let updated = text;
2297
2957
  let changed = false;
2298
2958
  for (const [oldPath, newPath] of replacements) {
@@ -2310,14 +2970,14 @@ async function runDedup(input) {
2310
2970
  }
2311
2971
  }
2312
2972
  if (changed) {
2313
- writeFileSync2(join12(input.vault, page.relPath), updated);
2973
+ writeFileSync4(join16(input.vault, page.relPath), updated);
2314
2974
  rewired.push(page.relPath);
2315
2975
  }
2316
2976
  }
2317
2977
  for (const oldPath of replacements.keys()) {
2318
- const fullPath = join12(input.vault, oldPath);
2978
+ const fullPath = join16(input.vault, oldPath);
2319
2979
  try {
2320
- unlinkSync2(fullPath);
2980
+ unlinkSync4(fullPath);
2321
2981
  removed.push(oldPath);
2322
2982
  } catch {
2323
2983
  }
@@ -2409,14 +3069,14 @@ function buildSafeEntries(vault, duplicates, unsafe) {
2409
3069
  return entries;
2410
3070
  }
2411
3071
  function hashRawBody(vault, relPath) {
2412
- const text = readFileSync3(join12(vault, relPath), "utf-8");
3072
+ const text = readFileSync5(join16(vault, relPath), "utf-8");
2413
3073
  const split = splitFrontmatter(text);
2414
3074
  const body = split.ok ? split.data.body : text;
2415
- return createHash2("sha256").update(body).digest("hex");
3075
+ return createHash4("sha256").update(body).digest("hex");
2416
3076
  }
2417
3077
  function readManifest(path) {
2418
3078
  try {
2419
- const parsed = JSON.parse(readFileSync3(path, "utf-8"));
3079
+ const parsed = JSON.parse(readFileSync5(path, "utf-8"));
2420
3080
  if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
2421
3081
  return err("INVALID_FRONTMATTER", { message: "dedup manifest must have version 1 and entries[]" });
2422
3082
  }
@@ -2432,9 +3092,7 @@ async function planAndMaybePruneRemote(input, entries) {
2432
3092
  }
2433
3093
 
2434
3094
  // src/utils/safe-write.ts
2435
- import { open, readFile as readFile10, rename as rename3, unlink, writeFile as writeFile5 } from "fs/promises";
2436
- import { randomBytes } from "crypto";
2437
- import { dirname as dirname5, basename, join as join13 } from "path";
3095
+ import { readFile as readFile13, writeFile as writeFile4 } from "fs/promises";
2438
3096
  var DEFAULT_MIN_BODY_RATIO = 0.5;
2439
3097
  var DEFAULT_MIN_OLD_BODY_BYTES = 200;
2440
3098
  function bodyBytes(text) {
@@ -2444,7 +3102,7 @@ function bodyBytes(text) {
2444
3102
  }
2445
3103
  async function readIfExists(absPath) {
2446
3104
  try {
2447
- return await readFile10(absPath, "utf8");
3105
+ return await readFile13(absPath, "utf8");
2448
3106
  } catch (e) {
2449
3107
  if (e.code === "ENOENT") return null;
2450
3108
  throw e;
@@ -2478,32 +3136,16 @@ async function safeWritePage(absPath, newContent, opts = {}) {
2478
3136
  });
2479
3137
  }
2480
3138
  }
2481
- if (!isNew && oldContent === newContent) {
2482
- return ok({ isNew: false, oldBodyBytes, newBodyBytes, bodyRatio, guardSkippedSmall });
2483
- }
2484
- const dir = dirname5(absPath);
2485
- const tmpName = `.${basename(absPath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
2486
- const tmpPath = join13(dir, tmpName);
2487
- try {
2488
- const handle = await open(tmpPath, "w");
2489
- try {
2490
- await handle.writeFile(newContent, "utf8");
2491
- try {
2492
- await handle.sync();
2493
- } catch {
2494
- }
2495
- } finally {
2496
- await handle.close();
2497
- }
2498
- await rename3(tmpPath, absPath);
2499
- return ok({ isNew, oldBodyBytes, newBodyBytes, bodyRatio, guardSkippedSmall });
2500
- } catch (e) {
2501
- try {
2502
- await unlink(tmpPath);
2503
- } catch {
2504
- }
2505
- return err("WRITE_FAILED", { path: absPath, phase: "atomic-write", message: String(e) });
2506
- }
3139
+ const written = await atomicWriteText(absPath, newContent);
3140
+ if (!written.ok) return written;
3141
+ return ok({
3142
+ changed: written.data.changed,
3143
+ isNew,
3144
+ oldBodyBytes,
3145
+ newBodyBytes,
3146
+ bodyRatio,
3147
+ guardSkippedSmall
3148
+ });
2507
3149
  }
2508
3150
 
2509
3151
  // src/commands/frontmatter-fix.ts
@@ -2583,10 +3225,10 @@ ${newBody}`;
2583
3225
  }
2584
3226
 
2585
3227
  // src/commands/lint.ts
2586
- import { existsSync as existsSync5 } from "fs";
2587
- import { readFile as readFile12, readdir as readdir3 } from "fs/promises";
2588
- import { createHash as createHash4 } from "crypto";
2589
- import { join as join15, relative as relative3, sep as sep3 } from "path";
3228
+ import { existsSync as existsSync7 } from "fs";
3229
+ import { readFile as readFile15, readdir as readdir3 } from "fs/promises";
3230
+ import { createHash as createHash6 } from "crypto";
3231
+ import { join as join18, relative as relative4, sep as sep4 } from "path";
2590
3232
 
2591
3233
  // src/commands/sparse-community.ts
2592
3234
  async function runSparseCommunity(input) {
@@ -2602,7 +3244,7 @@ async function runSparseCommunity(input) {
2602
3244
  }
2603
3245
 
2604
3246
  // src/commands/raw-body-dedup.ts
2605
- import { createHash as createHash3 } from "crypto";
3247
+ import { createHash as createHash5 } from "crypto";
2606
3248
  async function runRawBodyDedup(vault, scan, pageTextCache) {
2607
3249
  const scanResult = scan ? ok(scan) : await scanVault(vault);
2608
3250
  if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
@@ -2611,7 +3253,7 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
2611
3253
  const text = await readPageCached(raw, pageTextCache);
2612
3254
  const split = splitFrontmatter(text);
2613
3255
  if (!split.ok) return null;
2614
- const bodyHash = createHash3("sha256").update(split.data.body).digest("hex");
3256
+ const bodyHash = createHash5("sha256").update(split.data.body).digest("hex");
2615
3257
  const fm = extractFrontmatter(text);
2616
3258
  let fmSha256 = null;
2617
3259
  if (fm.ok && typeof fm.data.sha256 === "string" && fm.data.sha256.length === 64) {
@@ -2643,9 +3285,9 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
2643
3285
  }
2644
3286
 
2645
3287
  // src/commands/path-too-long.ts
2646
- import { existsSync as existsSync4 } from "fs";
2647
- import { mkdir as mkdir4, readFile as readFile11, rename as rename4, unlink as unlink2 } from "fs/promises";
2648
- import { dirname as dirname6, join as join14, posix, resolve as resolve4 } from "path";
3288
+ import { existsSync as existsSync6 } from "fs";
3289
+ import { mkdir as mkdir4, readFile as readFile14, rename as rename4, unlink as unlink2 } from "fs/promises";
3290
+ import { dirname as dirname7, join as join17, posix as posix2, resolve as resolve5 } from "path";
2649
3291
  var MAX_PATH_LENGTH = 240;
2650
3292
  var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
2651
3293
  async function runPathTooLong(input) {
@@ -2678,10 +3320,10 @@ async function fixPathTooLong(input) {
2678
3320
  }
2679
3321
  try {
2680
3322
  if (target.mode === "dedupe") {
2681
- await unlink2(join14(input.vault, violation.relPath));
3323
+ await unlink2(join17(input.vault, violation.relPath));
2682
3324
  } else {
2683
- await mkdir4(dirname6(join14(input.vault, target.relPath)), { recursive: true });
2684
- await rename4(join14(input.vault, violation.relPath), join14(input.vault, target.relPath));
3325
+ await mkdir4(dirname7(join17(input.vault, target.relPath)), { recursive: true });
3326
+ await rename4(join17(input.vault, violation.relPath), join17(input.vault, target.relPath));
2685
3327
  }
2686
3328
  fixed.push({ from: violation.relPath, to: target.relPath });
2687
3329
  } catch {
@@ -2695,7 +3337,7 @@ async function fixPathTooLong(input) {
2695
3337
  for (const page of afterScan.data.allMarkdown) {
2696
3338
  if (!shouldRewriteReferences(page.relPath)) continue;
2697
3339
  try {
2698
- const original = await readFile11(page.absPath, "utf8");
3340
+ const original = await readFile14(page.absPath, "utf8");
2699
3341
  let updated = original;
2700
3342
  for (const fix of fixed) {
2701
3343
  updated = replacePathReferences(updated, fix.from, fix.to);
@@ -2732,7 +3374,7 @@ function findPathTooLongViolations(pages, maxLength) {
2732
3374
  }
2733
3375
  function maxFixPathLength(vault) {
2734
3376
  if (process.platform !== "win32") return MAX_PATH_LENGTH;
2735
- const root = resolve4(vault);
3377
+ const root = resolve5(vault);
2736
3378
  const separatorBudget = root.endsWith("\\") || root.endsWith("/") ? 0 : 1;
2737
3379
  const absoluteSafeRelLength = WINDOWS_ABSOLUTE_PATH_LIMIT - root.length - separatorBudget;
2738
3380
  return Math.max(1, Math.min(MAX_PATH_LENGTH, absoluteSafeRelLength));
@@ -2758,9 +3400,9 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
2758
3400
  async function resolveFixTarget(vault, original, preferred, maxLength) {
2759
3401
  for (const candidate of candidateRelPaths(preferred, maxLength)) {
2760
3402
  if (candidate === original || candidate.length > maxLength) continue;
2761
- const candidatePath = join14(vault, candidate);
2762
- if (!existsSync4(candidatePath)) return { relPath: candidate, mode: "rename" };
2763
- if (await hasSameContent(join14(vault, original), candidatePath)) {
3403
+ const candidatePath = join17(vault, candidate);
3404
+ if (!existsSync6(candidatePath)) return { relPath: candidate, mode: "rename" };
3405
+ if (await hasSameContent(join17(vault, original), candidatePath)) {
2764
3406
  return { relPath: candidate, mode: "dedupe" };
2765
3407
  }
2766
3408
  }
@@ -2769,8 +3411,8 @@ async function resolveFixTarget(vault, original, preferred, maxLength) {
2769
3411
  function candidateRelPaths(preferred, maxLength) {
2770
3412
  const candidates = [preferred];
2771
3413
  if (preferred.length > maxLength) return candidates;
2772
- const dir = posix.dirname(preferred) === "." ? "" : posix.dirname(preferred);
2773
- const filename = posix.basename(preferred);
3414
+ const dir = posix2.dirname(preferred) === "." ? "" : posix2.dirname(preferred);
3415
+ const filename = posix2.basename(preferred);
2774
3416
  const ext = filename.endsWith(".md") ? ".md" : "";
2775
3417
  const base = ext ? filename.slice(0, -3) : filename;
2776
3418
  const dirPrefix = dir ? `${dir}/` : "";
@@ -2784,7 +3426,7 @@ function candidateRelPaths(preferred, maxLength) {
2784
3426
  }
2785
3427
  async function hasSameContent(a, b) {
2786
3428
  try {
2787
- const [left, right] = await Promise.all([readFile11(a), readFile11(b)]);
3429
+ const [left, right] = await Promise.all([readFile14(a), readFile14(b)]);
2788
3430
  return left.equals(right);
2789
3431
  } catch {
2790
3432
  return false;
@@ -2797,8 +3439,8 @@ function shouldRewriteReferences(relPath) {
2797
3439
  }
2798
3440
  function replacePathReferences(content, oldRelPath, newRelPath) {
2799
3441
  let updated = content.replaceAll(oldRelPath, newRelPath);
2800
- const oldStem = posix.basename(oldRelPath).replace(/\.md$/, "");
2801
- const newStem = posix.basename(newRelPath).replace(/\.md$/, "");
3442
+ const oldStem = posix2.basename(oldRelPath).replace(/\.md$/, "");
3443
+ const newStem = posix2.basename(newRelPath).replace(/\.md$/, "");
2802
3444
  if (oldStem !== newStem) {
2803
3445
  const oldStemEscaped = oldStem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2804
3446
  const stemWikilinkRe = new RegExp(`\\[\\[${oldStemEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
@@ -2850,6 +3492,7 @@ function buildCliSurface() {
2850
3492
  program.command("doctor").option("--check-snapshotter");
2851
3493
  program.command("status").option("--wiki <name>");
2852
3494
  program.command("archive").option("--wiki <name>").option("--cascade").option("--apply").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>");
3495
+ program.command("remove").option("--wiki <name>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--reason <text>");
2853
3496
  program.command("drift").option("--apply").option("--new <date>").option("--wiki <name>");
2854
3497
  program.command("dedup").option("--apply").option("--canonical-policy <policy>").option("--manifest-out <path>").option("--manifest-in <path>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--wiki <name>");
2855
3498
  program.command("migrate-citations").option("--dry-run").option("--wiki <name>");
@@ -2859,6 +3502,7 @@ function buildCliSurface() {
2859
3502
  program.command("transcripts").option("--since <date>").option("--wiki <name>");
2860
3503
  program.command("project-index").option("--apply").option("--wiki <name>");
2861
3504
  program.command("compound");
3505
+ program.command("tag");
2862
3506
  program.command("tag-sync").option("--dry-run").option("--wiki <name>");
2863
3507
  program.command("sync");
2864
3508
  program.command("backup");
@@ -2868,6 +3512,7 @@ function buildCliSurface() {
2868
3512
  program.command("memory");
2869
3513
  program.command("ingest").requiredOption("--vault <path>").requiredOption("--type <type>").requiredOption("--title <title>").option("--tags <csv>").option("--provenance <provenance>").option("--dry-run");
2870
3514
  program.command("fleet");
3515
+ program.command("page");
2871
3516
  const graphCmd = program.commands.find((c) => c.name() === "graph");
2872
3517
  graphCmd.command("build").option("--out <path>").option("--wiki <name>");
2873
3518
  const canvasCmd = program.commands.find((c) => c.name() === "canvas");
@@ -2881,6 +3526,10 @@ function buildCliSurface() {
2881
3526
  compoundCmd.command("promote").requiredOption("--project <slug>").option("--dry-run").option("--wiki <name>");
2882
3527
  compoundCmd.command("list").requiredOption("--project <slug>").option("--wiki <name>");
2883
3528
  compoundCmd.command("delete").requiredOption("--project <slug>").option("--wiki <name>");
3529
+ const tagCmd = program.commands.find((c) => c.name() === "tag");
3530
+ tagCmd.command("reconcile").requiredOption("--page <path>").option("--from <path>").option("--tags <csv>").option("--reason <text>").option("--write").option("--wiki <name>");
3531
+ const pageCmd = program.commands.find((c) => c.name() === "page");
3532
+ pageCmd.command("publish").requiredOption("--target <path>").option("--log-note <text>").option("--write").option("--wiki <name>");
2884
3533
  const syncCmd = program.commands.find((c) => c.name() === "sync");
2885
3534
  syncCmd.command("status").option("--wiki <name>").option("--include-stashes").option("--include-remote-health").option("--check-snapshotter");
2886
3535
  syncCmd.command("push").option("--wiki <name>");
@@ -3189,7 +3838,7 @@ function recomputeRawSha256IfPresent(content) {
3189
3838
  const split = splitFrontmatter(content);
3190
3839
  if (!split.ok) return content;
3191
3840
  if (!/^sha256:\s*[0-9a-f]{64}$/m.test(split.data.rawFrontmatter)) return content;
3192
- const sha256 = createHash4("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
3841
+ const sha256 = createHash6("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
3193
3842
  const rawFrontmatter = split.data.rawFrontmatter.replace(/^sha256:\s*[0-9a-f]{64}$/m, `sha256: ${sha256}`);
3194
3843
  return `---
3195
3844
  ${rawFrontmatter}
@@ -3237,24 +3886,24 @@ async function walkMarkdownFiles(absDir, vaultRoot) {
3237
3886
  const entries = await readdir3(absDir, { withFileTypes: true });
3238
3887
  const pages = [];
3239
3888
  for (const entry of entries) {
3240
- const absPath = join15(absDir, entry.name);
3889
+ const absPath = join18(absDir, entry.name);
3241
3890
  if (entry.isDirectory()) {
3242
3891
  if (entry.name === ".git" || entry.name === "node_modules") continue;
3243
3892
  pages.push(...await walkMarkdownFiles(absPath, vaultRoot));
3244
3893
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
3245
- pages.push({ absPath, relPath: relative3(vaultRoot, absPath).split(sep3).join("/") });
3894
+ pages.push({ absPath, relPath: relative4(vaultRoot, absPath).split(sep4).join("/") });
3246
3895
  }
3247
3896
  }
3248
3897
  return pages;
3249
3898
  }
3250
3899
  async function collectCliRefsPages(vault) {
3251
- if (!existsSync5(join15(vault, "SCHEMA.md"))) {
3900
+ if (!existsSync7(join18(vault, "SCHEMA.md"))) {
3252
3901
  return err("VAULT_PATH_INVALID", { root: vault, reason: "SCHEMA.md missing" });
3253
3902
  }
3254
3903
  const pages = [];
3255
3904
  for (const dir of CLI_REFS_TYPED_DIRS) {
3256
- const absDir = join15(vault, dir);
3257
- if (!existsSync5(absDir)) continue;
3905
+ const absDir = join18(vault, dir);
3906
+ if (!existsSync7(absDir)) continue;
3258
3907
  pages.push(...await walkMarkdownFiles(absDir, vault));
3259
3908
  }
3260
3909
  return ok(pages);
@@ -3375,7 +4024,7 @@ async function applyFileSourceUrlFix(input, scan, fileSourceUrlFlags, fileSource
3375
4024
  for (const relPath of fileSourceUrlFrontmatterFlags) {
3376
4025
  try {
3377
4026
  const absPath = `${input.vault}/${relPath}`;
3378
- const raw = await readFile12(absPath, "utf8");
4027
+ const raw = await readFile15(absPath, "utf8");
3379
4028
  const parts = raw.split("---", 3);
3380
4029
  if (parts.length < 3) {
3381
4030
  unresolved.push(relPath);
@@ -3768,8 +4417,8 @@ async function runLint(input) {
3768
4417
  const readKnowledgeContent = (slug) => {
3769
4418
  const existing = knowledgeContentCache.get(slug);
3770
4419
  if (existing) return existing;
3771
- const knowledgePath = join15(lintVault, "projects", slug, "knowledge.md");
3772
- const pending = existsSync5(knowledgePath) ? readFile12(knowledgePath, "utf8").catch(() => null) : Promise.resolve(null);
4420
+ const knowledgePath = join18(lintVault, "projects", slug, "knowledge.md");
4421
+ const pending = existsSync7(knowledgePath) ? readFile15(knowledgePath, "utf8").catch(() => null) : Promise.resolve(null);
3773
4422
  knowledgeContentCache.set(slug, pending);
3774
4423
  return pending;
3775
4424
  };
@@ -3870,12 +4519,12 @@ async function runLint(input) {
3870
4519
  else delete buckets.sensitive_content;
3871
4520
  }
3872
4521
  if (shouldFix("legacy_citation_style") && legacyPages.length > 0) {
3873
- const FENCE_RE2 = /```[\s\S]*?```/g;
4522
+ const FENCE_RE = /```[\s\S]*?```/g;
3874
4523
  const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
3875
4524
  for (const relPath of legacyPages) {
3876
4525
  try {
3877
4526
  const absPath = `${input.vault}/${relPath}`;
3878
- const raw = await readFile12(absPath, "utf8");
4527
+ const raw = await readFile15(absPath, "utf8");
3879
4528
  const split = splitFrontmatter(raw);
3880
4529
  if (!split.ok) {
3881
4530
  unresolved.push(relPath);
@@ -3883,7 +4532,7 @@ async function runLint(input) {
3883
4532
  }
3884
4533
  const body = split.data.body;
3885
4534
  const rawFm = split.data.rawFrontmatter;
3886
- const stripped = body.replace(FENCE_RE2, "");
4535
+ const stripped = body.replace(FENCE_RE, "");
3887
4536
  const lines = stripped.split("\n");
3888
4537
  const inlineMarkers = [];
3889
4538
  let inSources = false;
@@ -3974,7 +4623,7 @@ ${newBody}`;
3974
4623
  for (const relPath of noOverview) {
3975
4624
  try {
3976
4625
  const absPath = `${input.vault}/${relPath}`;
3977
- const raw = await readFile12(absPath, "utf8");
4626
+ const raw = await readFile15(absPath, "utf8");
3978
4627
  const split = splitFrontmatter(raw);
3979
4628
  if (!split.ok) {
3980
4629
  unresolved.push(relPath);
@@ -4015,7 +4664,7 @@ ${trimmedBody}`;
4015
4664
  for (const relPath of missingTldrFlags) {
4016
4665
  try {
4017
4666
  const absPath = `${input.vault}/${relPath}`;
4018
- const raw = await readFile12(absPath, "utf8");
4667
+ const raw = await readFile15(absPath, "utf8");
4019
4668
  const split = splitFrontmatter(raw);
4020
4669
  if (!split.ok) {
4021
4670
  unresolved.push(relPath);
@@ -4060,12 +4709,12 @@ ${lines.join("\n")}`;
4060
4709
  }
4061
4710
  if (shouldFix("wikilink_citation") && wikilinkCitationFlags.length > 0) {
4062
4711
  const WIKILINK_RE = /\[\[raw\/([^\]|]+)(?:\|[^\]]*)?\]\]/g;
4063
- const FENCE_RE2 = /```[\s\S]*?```/g;
4712
+ const FENCE_RE = /```[\s\S]*?```/g;
4064
4713
  const wikilinkFixed = [];
4065
4714
  for (const relPath of wikilinkCitationFlags) {
4066
4715
  try {
4067
4716
  const absPath = `${input.vault}/${relPath}`;
4068
- const raw = await readFile12(absPath, "utf8");
4717
+ const raw = await readFile15(absPath, "utf8");
4069
4718
  const split = splitFrontmatter(raw);
4070
4719
  if (!split.ok) {
4071
4720
  unresolved.push(relPath);
@@ -4073,7 +4722,7 @@ ${lines.join("\n")}`;
4073
4722
  }
4074
4723
  const body = split.data.body;
4075
4724
  const rawFm = split.data.rawFrontmatter;
4076
- const stripped = body.replace(FENCE_RE2, "");
4725
+ const stripped = body.replace(FENCE_RE, "");
4077
4726
  const wikilinkMatches = [...stripped.matchAll(WIKILINK_RE)];
4078
4727
  if (wikilinkMatches.length === 0) {
4079
4728
  unresolved.push(relPath);
@@ -4406,14 +5055,14 @@ async function runSyncLintDelta(input) {
4406
5055
  }
4407
5056
 
4408
5057
  // src/commands/config.ts
4409
- import { readFile as readFile13 } from "fs/promises";
4410
- import { existsSync as existsSync6 } from "fs";
4411
- import { join as join16 } from "path";
5058
+ import { readFile as readFile16 } from "fs/promises";
5059
+ import { existsSync as existsSync8 } from "fs";
5060
+ import { join as join19 } from "path";
4412
5061
  function validateKey(key) {
4413
5062
  return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
4414
5063
  }
4415
5064
  function configPath(home) {
4416
- return join16(home, ".skillwiki", ".env");
5065
+ return join19(home, ".skillwiki", ".env");
4417
5066
  }
4418
5067
  async function runConfigGet(input) {
4419
5068
  if (!validateKey(input.key)) {
@@ -4431,7 +5080,7 @@ async function runConfigSet(input) {
4431
5080
  try {
4432
5081
  let originalContent;
4433
5082
  try {
4434
- originalContent = await readFile13(filePath, "utf8");
5083
+ originalContent = await readFile16(filePath, "utf8");
4435
5084
  } catch {
4436
5085
  }
4437
5086
  const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
@@ -4463,15 +5112,15 @@ async function runConfigList(input) {
4463
5112
  }
4464
5113
  async function runConfigPath(input) {
4465
5114
  const filePath = configPath(input.home);
4466
- return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync6(filePath), humanHint: filePath }) };
5115
+ return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync8(filePath), humanHint: filePath }) };
4467
5116
  }
4468
5117
 
4469
5118
  // src/commands/fleet.ts
4470
- import { readFile as readFile14 } from "fs/promises";
5119
+ import { readFile as readFile17 } from "fs/promises";
4471
5120
  import { hostname as nodeHostname, userInfo } from "os";
4472
- import { join as join17 } from "path";
5121
+ import { join as join20 } from "path";
4473
5122
  import yaml3 from "js-yaml";
4474
- var FLEET_REL_PATH = join17("projects", "llm-wiki", "architecture", "fleet.yaml");
5123
+ var FLEET_REL_PATH = join20("projects", "llm-wiki", "architecture", "fleet.yaml");
4475
5124
  async function runFleetValidate(input) {
4476
5125
  const loaded = await loadFleetManifest(input.file);
4477
5126
  if (!loaded.ok) {
@@ -4502,7 +5151,7 @@ async function runFleetContext(input) {
4502
5151
  const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
4503
5152
  const user = input.user ?? safeEnvValue(env.USER) ?? safeUserName();
4504
5153
  const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
4505
- const file = input.file ?? (vault ? join17(vault, FLEET_REL_PATH) : void 0);
5154
+ const file = input.file ?? (vault ? join20(vault, FLEET_REL_PATH) : void 0);
4506
5155
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
4507
5156
  const loaded = file ? await loadFleetManifest(file) : { ok: false, error: "FILE_NOT_FOUND" };
4508
5157
  if (!loaded.ok) {
@@ -4622,7 +5271,7 @@ function fleetContextEnv(input) {
4622
5271
  const home = input.home ?? env.HOME ?? "";
4623
5272
  const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
4624
5273
  const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
4625
- const file = input.file ?? (vault ? join17(vault, FLEET_REL_PATH) : void 0);
5274
+ const file = input.file ?? (vault ? join20(vault, FLEET_REL_PATH) : void 0);
4626
5275
  return { env, home, osHostname, vault, file };
4627
5276
  }
4628
5277
  async function loadFleetManifestAndHost(input) {
@@ -4682,7 +5331,7 @@ function satelliteGateFromFleetLoad(load) {
4682
5331
  async function loadFleetManifest(file) {
4683
5332
  let text;
4684
5333
  try {
4685
- text = await readFile14(file, "utf8");
5334
+ text = await readFile17(file, "utf8");
4686
5335
  } catch {
4687
5336
  return { ok: false, error: "FILE_NOT_FOUND" };
4688
5337
  }
@@ -4756,7 +5405,7 @@ async function resolveFleetHostId(input) {
4756
5405
  }
4757
5406
  trace.push({ source: "AGENT_HOST_ID", status: "unset" });
4758
5407
  if (input.home) {
4759
- const dotenv = await parseDotenvFile(join17(input.home, ".skillwiki", ".env"));
5408
+ const dotenv = await parseDotenvFile(join20(input.home, ".skillwiki", ".env"));
4760
5409
  if (dotenv.SKILLWIKI_HOST_ID) {
4761
5410
  trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "matched", value: dotenv.SKILLWIKI_HOST_ID });
4762
5411
  return { hostId: dotenv.SKILLWIKI_HOST_ID, source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", trace };
@@ -4929,20 +5578,20 @@ function safeUserName() {
4929
5578
  }
4930
5579
 
4931
5580
  // src/commands/doctor.ts
4932
- import { existsSync as existsSync12, lstatSync, readlinkSync, readdirSync as readdirSync3, statSync, readFileSync as readFileSync9 } from "fs";
4933
- import { join as join23, resolve as resolve5 } from "path";
5581
+ import { existsSync as existsSync14, lstatSync as lstatSync2, readlinkSync, readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync11 } from "fs";
5582
+ import { join as join26, resolve as resolve6 } from "path";
4934
5583
  import { execSync as execSync2 } from "child_process";
4935
5584
  import { platform as platform2 } from "os";
4936
5585
 
4937
5586
  // src/utils/plugin-registry.ts
4938
- import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4 } from "fs";
4939
- import { join as join18 } from "path";
4940
- var REGISTRY_PATH = join18(".claude", "plugins", "installed_plugins.json");
4941
- var CODEX_CONFIG_PATH = join18(".codex", "config.toml");
5587
+ import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync6 } from "fs";
5588
+ import { join as join21 } from "path";
5589
+ var REGISTRY_PATH = join21(".claude", "plugins", "installed_plugins.json");
5590
+ var CODEX_CONFIG_PATH = join21(".codex", "config.toml");
4942
5591
  var PLUGIN_KEY = "skillwiki@llm-wiki";
4943
5592
  function readInstalledPlugins(home) {
4944
5593
  try {
4945
- const raw = readFileSync4(join18(home, REGISTRY_PATH), "utf8");
5594
+ const raw = readFileSync6(join21(home, REGISTRY_PATH), "utf8");
4946
5595
  return JSON.parse(raw);
4947
5596
  } catch {
4948
5597
  return null;
@@ -4978,8 +5627,8 @@ function findPluginInstallations(home, key = PLUGIN_KEY) {
4978
5627
  function findCodexPlugin(home, key, pluginName, marketplace) {
4979
5628
  const config = readCodexPluginConfig(home, key, marketplace);
4980
5629
  if (!config?.enabled) return null;
4981
- const cacheRoot = join18(home, ".codex", "plugins", "cache", marketplace, pluginName);
4982
- if (!existsSync7(cacheRoot)) return null;
5630
+ const cacheRoot = join21(home, ".codex", "plugins", "cache", marketplace, pluginName);
5631
+ if (!existsSync9(cacheRoot)) return null;
4983
5632
  let versions;
4984
5633
  try {
4985
5634
  versions = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -4994,7 +5643,7 @@ function findCodexPlugin(home, key, pluginName, marketplace) {
4994
5643
  key,
4995
5644
  pluginName,
4996
5645
  marketplace,
4997
- installPath: join18(cacheRoot, version),
5646
+ installPath: join21(cacheRoot, version),
4998
5647
  version,
4999
5648
  sourceType: config.sourceType,
5000
5649
  source: config.source
@@ -5011,7 +5660,7 @@ function parsePluginKey(key) {
5011
5660
  function readCodexPluginConfig(home, key, marketplace) {
5012
5661
  let raw;
5013
5662
  try {
5014
- raw = readFileSync4(join18(home, CODEX_CONFIG_PATH), "utf8");
5663
+ raw = readFileSync6(join21(home, CODEX_CONFIG_PATH), "utf8");
5015
5664
  } catch {
5016
5665
  return null;
5017
5666
  }
@@ -5053,8 +5702,8 @@ function parseTomlScalar(rawValue) {
5053
5702
  }
5054
5703
 
5055
5704
  // src/utils/conflict-markers.ts
5056
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
5057
- import { join as join19 } from "path";
5705
+ import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
5706
+ import { join as join22 } from "path";
5058
5707
  function scanConflictMarkerBlocksInText(relPath, text) {
5059
5708
  const findings = [];
5060
5709
  const lines = text.split(/\r?\n/);
@@ -5105,21 +5754,21 @@ function walkMarkdownFiles2(root, dir, rel, out) {
5105
5754
  for (const entry of entries) {
5106
5755
  if (entry.isDirectory()) {
5107
5756
  if (PRUNE_DIRS.has(entry.name)) continue;
5108
- walkMarkdownFiles2(root, join19(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
5757
+ walkMarkdownFiles2(root, join22(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
5109
5758
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
5110
5759
  out.push(rel ? `${rel}/${entry.name}` : entry.name);
5111
5760
  }
5112
5761
  }
5113
5762
  }
5114
5763
  function scanVaultConflictMarkers(vaultRoot) {
5115
- if (!existsSync8(vaultRoot)) return [];
5764
+ if (!existsSync10(vaultRoot)) return [];
5116
5765
  const relPaths = [];
5117
5766
  walkMarkdownFiles2(vaultRoot, vaultRoot, "", relPaths);
5118
5767
  const all = [];
5119
5768
  for (const rel of relPaths) {
5120
5769
  let text;
5121
5770
  try {
5122
- text = readFileSync5(join19(vaultRoot, rel), "utf8");
5771
+ text = readFileSync7(join22(vaultRoot, rel), "utf8");
5123
5772
  } catch {
5124
5773
  continue;
5125
5774
  }
@@ -5129,8 +5778,8 @@ function scanVaultConflictMarkers(vaultRoot) {
5129
5778
  }
5130
5779
 
5131
5780
  // src/utils/remote-health.ts
5132
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
5133
- import { join as join20 } from "path";
5781
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
5782
+ import { join as join23 } from "path";
5134
5783
  import { execFileSync } from "child_process";
5135
5784
  var REMOTE_PROBE_TIMEOUT_MS = 3e3;
5136
5785
  var defaultExec = (file, args, cwd) => execFileSync(file, args, {
@@ -5141,7 +5790,7 @@ var defaultExec = (file, args, cwd) => execFileSync(file, args, {
5141
5790
  }).trim();
5142
5791
  function readWikiS3RemoteConfigured(home) {
5143
5792
  try {
5144
- const content = readFileSync6(join20(home, ".skillwiki", ".env"), "utf8");
5793
+ const content = readFileSync8(join23(home, ".skillwiki", ".env"), "utf8");
5145
5794
  for (const line of content.split(/\r?\n/)) {
5146
5795
  const trimmed = line.trim();
5147
5796
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -5166,7 +5815,7 @@ function resolveWikiS3Remote(input) {
5166
5815
  return readWikiS3RemoteConfigured(input.home);
5167
5816
  }
5168
5817
  function probeGithubReachability(vaultPath, exec = defaultExec) {
5169
- if (!existsSync9(join20(vaultPath, ".git"))) return "unknown";
5818
+ if (!existsSync11(join23(vaultPath, ".git"))) return "unknown";
5170
5819
  try {
5171
5820
  exec("git", ["remote", "get-url", "origin"], vaultPath);
5172
5821
  } catch {
@@ -5231,11 +5880,11 @@ function probeRemoteHealth(input) {
5231
5880
  }
5232
5881
 
5233
5882
  // src/utils/satellite-run-health.ts
5234
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
5235
- import { join as join21 } from "path";
5883
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
5884
+ import { join as join24 } from "path";
5236
5885
  var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
5237
5886
  function satelliteLatestRunPath(vault) {
5238
- return join21(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
5887
+ return join24(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
5239
5888
  }
5240
5889
  function isFailedRunStatus(status) {
5241
5890
  return status === "fail" || status === "failure";
@@ -5257,9 +5906,9 @@ function readSatelliteLatestRunFromText(text) {
5257
5906
  }
5258
5907
  function readSatelliteLatestRun(vault) {
5259
5908
  const latestPath = satelliteLatestRunPath(vault);
5260
- if (!existsSync10(latestPath)) return null;
5909
+ if (!existsSync12(latestPath)) return null;
5261
5910
  try {
5262
- return parseLatestRunFile(readFileSync7(latestPath, "utf8"));
5911
+ return parseLatestRunFile(readFileSync9(latestPath, "utf8"));
5263
5912
  } catch {
5264
5913
  return null;
5265
5914
  }
@@ -5288,8 +5937,8 @@ function evaluateSatelliteRunHealth(vault, now) {
5288
5937
  // src/utils/s3-mount-health.ts
5289
5938
  import { execSync } from "child_process";
5290
5939
  import { platform } from "os";
5291
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, readFileSync as readFile15 } from "fs";
5292
- import { join as join22 } from "path";
5940
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5, readFileSync as readFile18 } from "fs";
5941
+ import { join as join25 } from "path";
5293
5942
  var OS = platform();
5294
5943
  function findRcloneMountPid() {
5295
5944
  try {
@@ -5373,7 +6022,7 @@ function extractRcloneFs(args) {
5373
6022
  function getRcloneArgs(pid) {
5374
6023
  try {
5375
6024
  if (OS === "linux") {
5376
- const raw = readFileSync8(`/proc/${pid}/cmdline`);
6025
+ const raw = readFileSync10(`/proc/${pid}/cmdline`);
5377
6026
  return new TextDecoder().decode(raw).split("\0").filter(Boolean);
5378
6027
  } else {
5379
6028
  const out = execSync(`ps -o args= -p ${pid}`, {
@@ -5416,7 +6065,7 @@ function queryRcloneRC(rcAddr, fs) {
5416
6065
  function detectFuseMount(vaultPath) {
5417
6066
  try {
5418
6067
  if (OS === "linux") {
5419
- const mounts = readFileSync8("/proc/mounts", "utf8");
6068
+ const mounts = readFileSync10("/proc/mounts", "utf8");
5420
6069
  let best = null;
5421
6070
  for (const line of mounts.split("\n")) {
5422
6071
  const parts = line.split(" ");
@@ -5447,35 +6096,35 @@ function detectFuseMount(vaultPath) {
5447
6096
  return null;
5448
6097
  }
5449
6098
  function writeTest(dir) {
5450
- const testFile = join22(dir, `.doctor-write-test-${process.pid}.tmp`);
6099
+ const testFile = join25(dir, `.doctor-write-test-${process.pid}.tmp`);
5451
6100
  const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
5452
6101
  const start = Date.now();
5453
6102
  try {
5454
- writeFileSync3(testFile, payload, "utf8");
6103
+ writeFileSync5(testFile, payload, "utf8");
5455
6104
  } catch (e) {
5456
6105
  return { success: false, writeMs: Date.now() - start, readMs: 0, size: 0, error: `write failed: ${e.message}` };
5457
6106
  }
5458
6107
  const writeMs = Date.now() - start;
5459
6108
  const readStart = Date.now();
5460
6109
  try {
5461
- const back = readFile15(testFile, "utf8");
6110
+ const back = readFile18(testFile, "utf8");
5462
6111
  const readMs = Date.now() - readStart;
5463
6112
  if (back !== payload) {
5464
6113
  try {
5465
- unlinkSync3(testFile);
6114
+ unlinkSync5(testFile);
5466
6115
  } catch {
5467
6116
  }
5468
6117
  return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
5469
6118
  }
5470
6119
  } catch (e) {
5471
6120
  try {
5472
- unlinkSync3(testFile);
6121
+ unlinkSync5(testFile);
5473
6122
  } catch {
5474
6123
  }
5475
6124
  return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
5476
6125
  }
5477
6126
  try {
5478
- unlinkSync3(testFile);
6127
+ unlinkSync5(testFile);
5479
6128
  } catch {
5480
6129
  }
5481
6130
  return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
@@ -5532,14 +6181,14 @@ function checkNodeVersion() {
5532
6181
  function detectCliChannels(argv, home) {
5533
6182
  const channels = [];
5534
6183
  if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
5535
- const devPath = resolve5(argv[1]);
6184
+ const devPath = resolve6(argv[1]);
5536
6185
  channels.push({ name: "dev", path: devPath, isDevLink: true });
5537
6186
  }
5538
6187
  try {
5539
6188
  const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
5540
6189
  if (whichOut) {
5541
6190
  const isDev = isDevSymlink(whichOut);
5542
- if (!channels.some((c) => c.path === resolve5(whichOut))) {
6191
+ if (!channels.some((c) => c.path === resolve6(whichOut))) {
5543
6192
  channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
5544
6193
  }
5545
6194
  }
@@ -5547,22 +6196,22 @@ function detectCliChannels(argv, home) {
5547
6196
  }
5548
6197
  const plugin = findPlugin(home);
5549
6198
  if (plugin) {
5550
- const pluginBin = join23(plugin.installPath, "bin", "skillwiki");
5551
- if (existsSync12(pluginBin)) {
6199
+ const pluginBin = join26(plugin.installPath, "bin", "skillwiki");
6200
+ if (existsSync14(pluginBin)) {
5552
6201
  channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
5553
6202
  }
5554
6203
  }
5555
- const installBin = join23(home, ".claude", "skills", "bin", "skillwiki");
5556
- if (existsSync12(installBin)) {
6204
+ const installBin = join26(home, ".claude", "skills", "bin", "skillwiki");
6205
+ if (existsSync14(installBin)) {
5557
6206
  channels.push({ name: "install", path: installBin, isDevLink: false });
5558
6207
  }
5559
6208
  return channels;
5560
6209
  }
5561
6210
  function isDevSymlink(binPath) {
5562
6211
  try {
5563
- const st = lstatSync(binPath);
6212
+ const st = lstatSync2(binPath);
5564
6213
  if (st.isSymbolicLink()) {
5565
- const target = resolve5(binPath, "..", readlinkSync(binPath));
6214
+ const target = resolve6(binPath, "..", readlinkSync(binPath));
5566
6215
  return target.includes("packages/cli") || target.includes("packages\\cli");
5567
6216
  }
5568
6217
  } catch {
@@ -5614,7 +6263,7 @@ function isDevSourceRun(argv) {
5614
6263
  }
5615
6264
  async function checkConfigFile(home) {
5616
6265
  const cfgPath = configPath(home);
5617
- if (!existsSync12(cfgPath)) {
6266
+ if (!existsSync14(cfgPath)) {
5618
6267
  return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
5619
6268
  }
5620
6269
  try {
@@ -5629,7 +6278,7 @@ function checkWikiPathExists(resolvedPath) {
5629
6278
  if (resolvedPath === void 0) {
5630
6279
  return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
5631
6280
  }
5632
- if (existsSync12(resolvedPath) && statSync(resolvedPath).isDirectory()) {
6281
+ if (existsSync14(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
5633
6282
  return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
5634
6283
  }
5635
6284
  return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
@@ -5638,13 +6287,13 @@ function checkVaultStructure(resolvedPath) {
5638
6287
  if (resolvedPath === void 0) {
5639
6288
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
5640
6289
  }
5641
- if (!existsSync12(resolvedPath)) {
6290
+ if (!existsSync14(resolvedPath)) {
5642
6291
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
5643
6292
  }
5644
6293
  const missing = [];
5645
- if (!existsSync12(join23(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
6294
+ if (!existsSync14(join26(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
5646
6295
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
5647
- if (!existsSync12(join23(resolvedPath, dir))) missing.push(dir + "/");
6296
+ if (!existsSync14(join26(resolvedPath, dir))) missing.push(dir + "/");
5648
6297
  }
5649
6298
  if (missing.length === 0) {
5650
6299
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -5652,8 +6301,8 @@ function checkVaultStructure(resolvedPath) {
5652
6301
  return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
5653
6302
  }
5654
6303
  function checkSkillsInstalled(home, cwd) {
5655
- const srcDir = cwd ? join23(cwd, "packages", "skills") : void 0;
5656
- if (srcDir && existsSync12(srcDir)) {
6304
+ const srcDir = cwd ? join26(cwd, "packages", "skills") : void 0;
6305
+ if (srcDir && existsSync14(srcDir)) {
5657
6306
  const found = findInstalledSkillMd(srcDir);
5658
6307
  if (found.length > 0) {
5659
6308
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
@@ -5666,8 +6315,8 @@ function checkSkillsInstalled(home, cwd) {
5666
6315
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
5667
6316
  }
5668
6317
  }
5669
- const skillsDir = join23(home, ".claude", "skills");
5670
- if (existsSync12(skillsDir)) {
6318
+ const skillsDir = join26(home, ".claude", "skills");
6319
+ if (existsSync14(skillsDir)) {
5671
6320
  const found = findInstalledSkillMd(skillsDir);
5672
6321
  if (found.length > 0) {
5673
6322
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
@@ -5677,10 +6326,10 @@ function checkSkillsInstalled(home, cwd) {
5677
6326
  }
5678
6327
  function checkDuplicateSkills(home) {
5679
6328
  const plugin = findPlugin(home);
5680
- const skillsDir = join23(home, ".claude", "skills");
6329
+ const skillsDir = join26(home, ".claude", "skills");
5681
6330
  const agentSkillDirs = [
5682
- { label: "~/.codex/skills/", path: join23(home, ".codex", "skills") },
5683
- { label: "~/.agents/skills/", path: join23(home, ".agents", "skills") }
6331
+ { label: "~/.codex/skills/", path: join26(home, ".codex", "skills") },
6332
+ { label: "~/.agents/skills/", path: join26(home, ".agents", "skills") }
5684
6333
  ];
5685
6334
  if (!plugin) {
5686
6335
  return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
@@ -5783,8 +6432,8 @@ async function checkProfiles(home) {
5783
6432
  }
5784
6433
  async function checkProjectLocalOverride(cwd) {
5785
6434
  const dir = cwd ?? process.cwd();
5786
- const envPath = join23(dir, ".skillwiki", ".env");
5787
- if (existsSync12(envPath)) {
6435
+ const envPath = join26(dir, ".skillwiki", ".env");
6436
+ if (existsSync14(envPath)) {
5788
6437
  return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
5789
6438
  }
5790
6439
  return check("pass", "project_local", "Project-local config", "None");
@@ -5793,7 +6442,7 @@ function checkVaultGitRemote(resolvedPath) {
5793
6442
  if (resolvedPath === void 0) {
5794
6443
  return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
5795
6444
  }
5796
- if (!existsSync12(join23(resolvedPath, ".git"))) {
6445
+ if (!existsSync14(join26(resolvedPath, ".git"))) {
5797
6446
  return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
5798
6447
  }
5799
6448
  try {
@@ -5816,9 +6465,9 @@ function checkObsidianTemplates(resolvedPath) {
5816
6465
  return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
5817
6466
  }
5818
6467
  const missing = [];
5819
- if (!existsSync12(join23(resolvedPath, "_Templates"))) missing.push("_Templates/");
5820
- if (!existsSync12(join23(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
5821
- if (!existsSync12(join23(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
6468
+ if (!existsSync14(join26(resolvedPath, "_Templates"))) missing.push("_Templates/");
6469
+ if (!existsSync14(join26(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
6470
+ if (!existsSync14(join26(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
5822
6471
  if (missing.length === 0) {
5823
6472
  return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
5824
6473
  }
@@ -5828,8 +6477,8 @@ function checkDotStoreClean(resolvedPath) {
5828
6477
  if (resolvedPath === void 0) {
5829
6478
  return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
5830
6479
  }
5831
- const rawDir = join23(resolvedPath, "raw");
5832
- if (!existsSync12(rawDir)) {
6480
+ const rawDir = join26(resolvedPath, "raw");
6481
+ if (!existsSync14(rawDir)) {
5833
6482
  return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
5834
6483
  }
5835
6484
  const found = [];
@@ -5844,7 +6493,7 @@ function checkDotStoreClean(resolvedPath) {
5844
6493
  if (entry.name === ".DS_Store") {
5845
6494
  found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
5846
6495
  } else if (entry.isDirectory()) {
5847
- walk2(join23(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
6496
+ walk2(join26(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
5848
6497
  }
5849
6498
  }
5850
6499
  })(rawDir, "");
@@ -5875,7 +6524,7 @@ function checkSyncLastPush(resolvedPath) {
5875
6524
  if (resolvedPath === void 0) {
5876
6525
  return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
5877
6526
  }
5878
- if (!existsSync12(join23(resolvedPath, ".git"))) {
6527
+ if (!existsSync14(join26(resolvedPath, ".git"))) {
5879
6528
  return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
5880
6529
  }
5881
6530
  let timestamp;
@@ -5923,7 +6572,7 @@ function checkVaultGitDirty(resolvedPath) {
5923
6572
  if (resolvedPath === void 0) {
5924
6573
  return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
5925
6574
  }
5926
- if (!existsSync12(join23(resolvedPath, ".git"))) {
6575
+ if (!existsSync14(join26(resolvedPath, ".git"))) {
5927
6576
  return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
5928
6577
  }
5929
6578
  try {
@@ -5991,7 +6640,7 @@ function remoteMainHash(resolvedPath) {
5991
6640
  }
5992
6641
  function checkStaleRemoteMain(resolvedPath) {
5993
6642
  if (resolvedPath === void 0) return void 0;
5994
- if (!existsSync12(join23(resolvedPath, ".git"))) return void 0;
6643
+ if (!existsSync14(join26(resolvedPath, ".git"))) return void 0;
5995
6644
  const localOrigin = gitRefHash(resolvedPath, "origin/main");
5996
6645
  if (!localOrigin) return void 0;
5997
6646
  const remoteMain = remoteMainHash(resolvedPath);
@@ -6007,7 +6656,7 @@ function checkVaultLocalGit(resolvedPath) {
6007
6656
  if (resolvedPath === void 0) {
6008
6657
  return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
6009
6658
  }
6010
- if (!existsSync12(join23(resolvedPath, ".git"))) {
6659
+ if (!existsSync14(join26(resolvedPath, ".git"))) {
6011
6660
  return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
6012
6661
  }
6013
6662
  try {
@@ -6026,7 +6675,7 @@ function checkVaultGithubRemote(resolvedPath, exec) {
6026
6675
  if (resolvedPath === void 0) {
6027
6676
  return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
6028
6677
  }
6029
- if (!existsSync12(join23(resolvedPath, ".git"))) {
6678
+ if (!existsSync14(join26(resolvedPath, ".git"))) {
6030
6679
  return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
6031
6680
  }
6032
6681
  const state = probeGithubReachability(resolvedPath, exec);
@@ -6070,7 +6719,7 @@ function checkVaultPromotionLag(resolvedPath) {
6070
6719
  if (resolvedPath === void 0) {
6071
6720
  return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
6072
6721
  }
6073
- if (!existsSync12(join23(resolvedPath, ".git"))) {
6722
+ if (!existsSync14(join26(resolvedPath, ".git"))) {
6074
6723
  return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
6075
6724
  }
6076
6725
  try {
@@ -6097,7 +6746,7 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
6097
6746
  if (resolvedPath === void 0) {
6098
6747
  return check("pass", id, label, "No vault path \u2014 check skipped");
6099
6748
  }
6100
- if (!existsSync12(join23(resolvedPath, ".git"))) {
6749
+ if (!existsSync14(join26(resolvedPath, ".git"))) {
6101
6750
  return check("pass", id, label, "No git repo \u2014 check skipped");
6102
6751
  }
6103
6752
  if (!hasOriginMain(resolvedPath)) {
@@ -6125,7 +6774,7 @@ function checkSatelliteLastRun(vaultPath, satelliteExpected) {
6125
6774
  return check("pass", "satellite_job_last_run", "Satellite job last run", "No vault path \u2014 check skipped");
6126
6775
  }
6127
6776
  const latestPath = satelliteLatestRunPath(vaultPath);
6128
- if (!existsSync12(latestPath)) {
6777
+ if (!existsSync14(latestPath)) {
6129
6778
  return check("pass", "satellite_job_last_run", "Satellite job last run", "No latest-run.json \u2014 satellite has not run yet");
6130
6779
  }
6131
6780
  try {
@@ -6213,11 +6862,11 @@ async function checkFleetIdentity(input) {
6213
6862
  }
6214
6863
  function pullLogPaths(home) {
6215
6864
  const paths = platform2() === "darwin" ? [
6216
- join23(home, "Library", "Logs", "wiki-pull.log"),
6217
- join23(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
6865
+ join26(home, "Library", "Logs", "wiki-pull.log"),
6866
+ join26(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
6218
6867
  ] : [
6219
- join23(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
6220
- join23(home, "Library", "Logs", "wiki-pull.log")
6868
+ join26(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
6869
+ join26(home, "Library", "Logs", "wiki-pull.log")
6221
6870
  ];
6222
6871
  return [...new Set(paths)];
6223
6872
  }
@@ -6229,12 +6878,12 @@ function isRecentLogLine(line, nowMs) {
6229
6878
  return nowMs - ts <= 24 * 60 * 60 * 1e3;
6230
6879
  }
6231
6880
  function checkVaultGitPullFailures(home) {
6232
- const path = pullLogPaths(home).find((p) => existsSync12(p));
6881
+ const path = pullLogPaths(home).find((p) => existsSync14(p));
6233
6882
  if (!path) {
6234
6883
  return check("pass", "vault_git_pull_failures", "Vault pull failures", "No wiki-pull.log found \u2014 check skipped");
6235
6884
  }
6236
6885
  try {
6237
- const lines = readFileSync9(path, "utf8").split(/\r?\n/).filter(Boolean);
6886
+ const lines = readFileSync11(path, "utf8").split(/\r?\n/).filter(Boolean);
6238
6887
  const now = Date.now();
6239
6888
  const failures = lines.filter(
6240
6889
  (line) => isRecentLogLine(line, now) && /(pre-push pull failed|FAIL .*pull|FAIL .*rebase|cannot pull with rebase|unstaged changes)/i.test(line)
@@ -6257,8 +6906,8 @@ function checkS3MountPerf(resolvedPath) {
6257
6906
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
6258
6907
  }
6259
6908
  const mountPoint = fuse.mountPoint;
6260
- const conceptsDir = join23(resolvedPath, "concepts");
6261
- if (!existsSync12(conceptsDir)) {
6909
+ const conceptsDir = join26(resolvedPath, "concepts");
6910
+ if (!existsSync14(conceptsDir)) {
6262
6911
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
6263
6912
  }
6264
6913
  const start = Date.now();
@@ -6440,8 +7089,8 @@ function checkWriteTest(resolvedPath) {
6440
7089
  if (!fuse) {
6441
7090
  return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
6442
7091
  }
6443
- const conceptsDir = join23(resolvedPath, "concepts");
6444
- if (!existsSync12(conceptsDir)) {
7092
+ const conceptsDir = join26(resolvedPath, "concepts");
7093
+ if (!existsSync14(conceptsDir)) {
6445
7094
  return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
6446
7095
  }
6447
7096
  const result = writeTest(conceptsDir);
@@ -6527,7 +7176,7 @@ function checkVfsCacheHealth(resolvedPath) {
6527
7176
  }
6528
7177
  function readVaultSyncConfig(home) {
6529
7178
  try {
6530
- const content = readFileSync9(join23(home, ".skillwiki", ".env"), "utf8");
7179
+ const content = readFileSync11(join26(home, ".skillwiki", ".env"), "utf8");
6531
7180
  let installed = false;
6532
7181
  let role;
6533
7182
  let serviceScope;
@@ -6556,7 +7205,7 @@ function readVaultSyncConfig(home) {
6556
7205
  }
6557
7206
  function readKeyFromEnvFile(path, keys) {
6558
7207
  try {
6559
- const content = readFileSync9(path, "utf8");
7208
+ const content = readFileSync11(path, "utf8");
6560
7209
  for (const line of content.split(/\r?\n/)) {
6561
7210
  const trimmed = line.trim();
6562
7211
  if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
@@ -6578,7 +7227,7 @@ function resolveSnapshotGitWorktree(config) {
6578
7227
  if (fromProfile) return fromProfile;
6579
7228
  }
6580
7229
  const defaultPath = "/root/wiki-git";
6581
- return existsSync12(defaultPath) ? defaultPath : void 0;
7230
+ return existsSync14(defaultPath) ? defaultPath : void 0;
6582
7231
  }
6583
7232
  function vaultSyncChecks(input) {
6584
7233
  const os = input.os ?? platform2();
@@ -6595,16 +7244,16 @@ function vaultSyncChecks(input) {
6595
7244
  ];
6596
7245
  }
6597
7246
  const isMac = os === "darwin";
6598
- const logDir = input.logDir ?? (isMac ? join23(home, "Library", "Logs") : join23(home, ".local", "state", "vault-sync", "log"));
6599
- const shareDir = input.shareDir ?? (isMac ? join23(home, "Library", "Application Support", "vault-sync", "bin") : join23(home, ".local", "share", "vault-sync", "bin"));
6600
- const filterPath = input.filterPath ?? join23(home, ".config", "rclone", "wiki-push-filters.txt");
6601
- const packagedSnapshotPath = join23(shareDir, "wiki-snapshot.sh");
7247
+ const logDir = input.logDir ?? (isMac ? join26(home, "Library", "Logs") : join26(home, ".local", "state", "vault-sync", "log"));
7248
+ const shareDir = input.shareDir ?? (isMac ? join26(home, "Library", "Application Support", "vault-sync", "bin") : join26(home, ".local", "share", "vault-sync", "bin"));
7249
+ const filterPath = input.filterPath ?? join26(home, ".config", "rclone", "wiki-push-filters.txt");
7250
+ const packagedSnapshotPath = join26(shareDir, "wiki-snapshot.sh");
6602
7251
  const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
6603
- const snapshotPath = input.snapshotScriptPath ?? (existsSync12(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
7252
+ const snapshotPath = input.snapshotScriptPath ?? (existsSync14(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
6604
7253
  function snapshotLastStatusCheck() {
6605
- const snapshotLog = join23(logDir, "wiki-snapshot.log");
7254
+ const snapshotLog = join26(logDir, "wiki-snapshot.log");
6606
7255
  try {
6607
- const logContent = readFileSync9(snapshotLog, "utf8");
7256
+ const logContent = readFileSync11(snapshotLog, "utf8");
6608
7257
  const lines = logContent.trim().split("\n").filter(Boolean);
6609
7258
  if (lines.length === 0) {
6610
7259
  return check(
@@ -6649,14 +7298,14 @@ function vaultSyncChecks(input) {
6649
7298
  }
6650
7299
  }
6651
7300
  if (input.vaultSyncRole === "snapshotter") {
6652
- const c12 = existsSync12(snapshotPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found snapshot script: ${snapshotPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Snapshot script not found at ${snapshotPath}`);
7301
+ const c12 = existsSync14(snapshotPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found snapshot script: ${snapshotPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Snapshot script not found at ${snapshotPath}`);
6653
7302
  const serviceScope = input.vaultSyncServiceScope ?? "user";
6654
- const userTimerPath = join23(home, ".config", "systemd", "user", "wiki-snapshot.timer");
7303
+ const userTimerPath = join26(home, ".config", "systemd", "user", "wiki-snapshot.timer");
6655
7304
  const systemTimerPath = "/etc/systemd/system/wiki-snapshot.timer";
6656
7305
  let c22;
6657
- if (serviceScope === "user" && existsSync12(userTimerPath)) {
7306
+ if (serviceScope === "user" && existsSync14(userTimerPath)) {
6658
7307
  c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${userTimerPath}`);
6659
- } else if (serviceScope === "system" && existsSync12(systemTimerPath)) {
7308
+ } else if (serviceScope === "system" && existsSync14(systemTimerPath)) {
6660
7309
  c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${systemTimerPath}`);
6661
7310
  } else if (os !== "linux") {
6662
7311
  c22 = check("warn", "vault_sync_jobs_enabled", "Vault sync jobs enabled", "Snapshotter scheduler is Linux-only and no wiki-snapshot.timer file was found");
@@ -6688,7 +7337,7 @@ function vaultSyncChecks(input) {
6688
7337
  );
6689
7338
  let c52;
6690
7339
  try {
6691
- if (!existsSync12(snapshotPath)) {
7340
+ if (!existsSync14(snapshotPath)) {
6692
7341
  c52 = check(
6693
7342
  "error",
6694
7343
  "vault_sync_snapshot_guard",
@@ -6696,7 +7345,7 @@ function vaultSyncChecks(input) {
6696
7345
  `Snapshot script not found at ${snapshotPath}`
6697
7346
  );
6698
7347
  } else {
6699
- const content = readFileSync9(snapshotPath, "utf8");
7348
+ const content = readFileSync11(snapshotPath, "utf8");
6700
7349
  if (!content.includes("--max-delete")) {
6701
7350
  c52 = check(
6702
7351
  "error",
@@ -6723,8 +7372,8 @@ function vaultSyncChecks(input) {
6723
7372
  }
6724
7373
  return [c12, c22, c32, cFetch2, c42, c52];
6725
7374
  }
6726
- const pushScriptPath = join23(shareDir, "wiki-push.sh");
6727
- const c1 = existsSync12(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`);
7375
+ const pushScriptPath = join26(shareDir, "wiki-push.sh");
7376
+ const c1 = existsSync14(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`);
6728
7377
  let c2;
6729
7378
  try {
6730
7379
  if (isMac) {
@@ -6775,10 +7424,10 @@ function vaultSyncChecks(input) {
6775
7424
  "Scheduler check failed \u2014 run vault-sync-install"
6776
7425
  );
6777
7426
  }
6778
- const logFile = join23(logDir, "wiki-push.log");
7427
+ const logFile = join26(logDir, "wiki-push.log");
6779
7428
  let c3;
6780
7429
  try {
6781
- const logContent = readFileSync9(logFile, "utf8");
7430
+ const logContent = readFileSync11(logFile, "utf8");
6782
7431
  const lines = logContent.trim().split("\n").filter(Boolean);
6783
7432
  if (lines.length === 0) {
6784
7433
  c3 = check(
@@ -6834,7 +7483,7 @@ function vaultSyncChecks(input) {
6834
7483
  }
6835
7484
  }
6836
7485
  } catch {
6837
- c3 = existsSync12(logDir) ? check(
7486
+ c3 = existsSync14(logDir) ? check(
6838
7487
  "warn",
6839
7488
  "vault_sync_last_push_age",
6840
7489
  "Vault sync last push recency",
@@ -6846,10 +7495,10 @@ function vaultSyncChecks(input) {
6846
7495
  `Log directory not found at ${logDir}`
6847
7496
  );
6848
7497
  }
6849
- const fetchLogFile = join23(logDir, "wiki-fetch.log");
7498
+ const fetchLogFile = join26(logDir, "wiki-fetch.log");
6850
7499
  let cFetch;
6851
7500
  try {
6852
- const logContent = readFileSync9(fetchLogFile, "utf8");
7501
+ const logContent = readFileSync11(fetchLogFile, "utf8");
6853
7502
  const lines = logContent.trim().split("\n").filter(Boolean);
6854
7503
  if (lines.length === 0) {
6855
7504
  cFetch = check(
@@ -6893,7 +7542,7 @@ function vaultSyncChecks(input) {
6893
7542
  }
6894
7543
  let c4;
6895
7544
  try {
6896
- if (!existsSync12(filterPath)) {
7545
+ if (!existsSync14(filterPath)) {
6897
7546
  c4 = check(
6898
7547
  "error",
6899
7548
  "vault_sync_filter_present",
@@ -6901,7 +7550,7 @@ function vaultSyncChecks(input) {
6901
7550
  `Filter file not found at ${filterPath}`
6902
7551
  );
6903
7552
  } else {
6904
- const content = readFileSync9(filterPath, "utf8");
7553
+ const content = readFileSync11(filterPath, "utf8");
6905
7554
  const requiredExcludes = [
6906
7555
  "remotely-save/data.json",
6907
7556
  ".skillwiki/sync.lock",
@@ -6944,7 +7593,7 @@ function vaultSyncChecks(input) {
6944
7593
  );
6945
7594
  } else {
6946
7595
  try {
6947
- if (!existsSync12(snapshotPath)) {
7596
+ if (!existsSync14(snapshotPath)) {
6948
7597
  c5 = check(
6949
7598
  "error",
6950
7599
  "vault_sync_snapshot_guard",
@@ -6952,7 +7601,7 @@ function vaultSyncChecks(input) {
6952
7601
  `Snapshot script not found at ${snapshotPath}`
6953
7602
  );
6954
7603
  } else {
6955
- const content = readFileSync9(snapshotPath, "utf8");
7604
+ const content = readFileSync11(snapshotPath, "utf8");
6956
7605
  if (!content.includes("--max-delete")) {
6957
7606
  c5 = check(
6958
7607
  "error",
@@ -6990,15 +7639,15 @@ function findSkillMd(dir) {
6990
7639
  }
6991
7640
  for (const entry of entries) {
6992
7641
  if (entry.isFile() && entry.name === "SKILL.md") {
6993
- results.push(join23(dir, entry.name));
7642
+ results.push(join26(dir, entry.name));
6994
7643
  } else if (entry.isDirectory()) {
6995
- results.push(...findSkillMd(join23(dir, entry.name)));
7644
+ results.push(...findSkillMd(join26(dir, entry.name)));
6996
7645
  }
6997
7646
  }
6998
7647
  return results;
6999
7648
  }
7000
7649
  function findInstalledSkillMd(dir) {
7001
- const directSkills = findSkillNames(dir).map((name) => join23(dir, name, "SKILL.md"));
7650
+ const directSkills = findSkillNames(dir).map((name) => join26(dir, name, "SKILL.md"));
7002
7651
  return directSkills.length > 0 ? directSkills : findSkillMd(dir);
7003
7652
  }
7004
7653
  function findSkillNames(dir) {
@@ -7010,7 +7659,7 @@ function findSkillNames(dir) {
7010
7659
  return results;
7011
7660
  }
7012
7661
  for (const entry of entries) {
7013
- if (entry.isDirectory() && existsSync12(join23(dir, entry.name, "SKILL.md"))) {
7662
+ if (entry.isDirectory() && existsSync14(join26(dir, entry.name, "SKILL.md"))) {
7014
7663
  results.push(entry.name);
7015
7664
  }
7016
7665
  }
@@ -7054,7 +7703,7 @@ async function vaultMetrics(resolvedPath) {
7054
7703
  }
7055
7704
  let logLines = 0;
7056
7705
  try {
7057
- logLines = readFileSync9(join23(resolvedPath, "log.md"), "utf8").split("\n").length;
7706
+ logLines = readFileSync11(join26(resolvedPath, "log.md"), "utf8").split("\n").length;
7058
7707
  } catch {
7059
7708
  }
7060
7709
  return [
@@ -7162,7 +7811,7 @@ async function runDoctor(input) {
7162
7811
  }
7163
7812
 
7164
7813
  // src/utils/package-info.ts
7165
- import { readFileSync as readFileSync10 } from "fs";
7814
+ import { readFileSync as readFileSync12 } from "fs";
7166
7815
  function packageJsonCandidateUrls(baseUrl = import.meta.url) {
7167
7816
  return [
7168
7817
  new URL("../package.json", baseUrl),
@@ -7172,7 +7821,7 @@ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
7172
7821
  function readCliPackageJson(baseUrl = import.meta.url) {
7173
7822
  for (const url of packageJsonCandidateUrls(baseUrl)) {
7174
7823
  try {
7175
- const pkg = JSON.parse(readFileSync10(url, "utf8"));
7824
+ const pkg = JSON.parse(readFileSync12(url, "utf8"));
7176
7825
  if (typeof pkg.version === "string") {
7177
7826
  return { ...pkg, version: pkg.version };
7178
7827
  }
@@ -7183,8 +7832,8 @@ function readCliPackageJson(baseUrl = import.meta.url) {
7183
7832
  }
7184
7833
 
7185
7834
  // src/commands/project-index.ts
7186
- import { readdir as readdir4, readFile as readFile16, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
7187
- import { join as join24, dirname as dirname7, basename as basename2 } from "path";
7835
+ import { readdir as readdir4, readFile as readFile19, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
7836
+ import { join as join27, dirname as dirname8, basename as basename2 } from "path";
7188
7837
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
7189
7838
  var PROJECT_LOCAL_DIRS = ["requirements", "work", "architecture", "history"];
7190
7839
  async function scanMarkdownTree(rootAbs, rootRel) {
@@ -7196,7 +7845,7 @@ async function scanMarkdownTree(rootAbs, rootRel) {
7196
7845
  return found;
7197
7846
  }
7198
7847
  for (const entry of entries) {
7199
- const abs = join24(rootAbs, entry.name);
7848
+ const abs = join27(rootAbs, entry.name);
7200
7849
  const rel = `${rootRel}/${entry.name}`;
7201
7850
  if (entry.isDirectory()) {
7202
7851
  found.push(...await scanMarkdownTree(abs, rel));
@@ -7226,7 +7875,7 @@ function projectLocalType(slug, page, data) {
7226
7875
  }
7227
7876
  async function runProjectIndex(input) {
7228
7877
  const slug = input.slug;
7229
- const projectDir = join24(input.vault, "projects", slug);
7878
+ const projectDir = join27(input.vault, "projects", slug);
7230
7879
  try {
7231
7880
  await readdir4(projectDir);
7232
7881
  } catch {
@@ -7237,15 +7886,15 @@ async function runProjectIndex(input) {
7237
7886
  }
7238
7887
  const wikilinkPattern = `[[${slug}]]`;
7239
7888
  const entries = [];
7240
- const compoundDir = join24(input.vault, "projects", slug, "compound");
7889
+ const compoundDir = join27(input.vault, "projects", slug, "compound");
7241
7890
  try {
7242
7891
  const compoundFiles = await readdir4(compoundDir, { withFileTypes: true });
7243
7892
  for (const entry of compoundFiles) {
7244
7893
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
7245
- const filePath = join24(compoundDir, entry.name);
7894
+ const filePath = join27(compoundDir, entry.name);
7246
7895
  let text;
7247
7896
  try {
7248
- text = await readFile16(filePath, "utf8");
7897
+ text = await readFile19(filePath, "utf8");
7249
7898
  } catch {
7250
7899
  continue;
7251
7900
  }
@@ -7262,16 +7911,16 @@ async function runProjectIndex(input) {
7262
7911
  for (const dir of LAYER2_DIRS) {
7263
7912
  let files;
7264
7913
  try {
7265
- files = await readdir4(join24(input.vault, dir), { withFileTypes: true });
7914
+ files = await readdir4(join27(input.vault, dir), { withFileTypes: true });
7266
7915
  } catch {
7267
7916
  continue;
7268
7917
  }
7269
7918
  for (const entry of files) {
7270
7919
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
7271
- const filePath = join24(input.vault, dir, entry.name);
7920
+ const filePath = join27(input.vault, dir, entry.name);
7272
7921
  let text;
7273
7922
  try {
7274
- text = await readFile16(filePath, "utf8");
7923
+ text = await readFile19(filePath, "utf8");
7275
7924
  } catch {
7276
7925
  continue;
7277
7926
  }
@@ -7287,14 +7936,14 @@ async function runProjectIndex(input) {
7287
7936
  }
7288
7937
  }
7289
7938
  for (const dir of PROJECT_LOCAL_DIRS) {
7290
- const rootAbs = join24(projectDir, dir);
7939
+ const rootAbs = join27(projectDir, dir);
7291
7940
  const rootRel = `projects/${slug}/${dir}`;
7292
7941
  const pages = await scanMarkdownTree(rootAbs, rootRel);
7293
7942
  for (const page of pages) {
7294
- const filePath = join24(input.vault, page);
7943
+ const filePath = join27(input.vault, page);
7295
7944
  let text;
7296
7945
  try {
7297
- text = await readFile16(filePath, "utf8");
7946
+ text = await readFile19(filePath, "utf8");
7298
7947
  } catch {
7299
7948
  continue;
7300
7949
  }
@@ -7313,11 +7962,11 @@ async function runProjectIndex(input) {
7313
7962
  const tb = typeOrder[b.type] ?? 99;
7314
7963
  return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
7315
7964
  });
7316
- const indexPath = join24(projectDir, "knowledge.md");
7965
+ const indexPath = join27(projectDir, "knowledge.md");
7317
7966
  let existing = false;
7318
7967
  let stale = false;
7319
7968
  try {
7320
- const existingText = await readFile16(indexPath, "utf8");
7969
+ const existingText = await readFile19(indexPath, "utf8");
7321
7970
  existing = true;
7322
7971
  const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
7323
7972
  const existingPages = new Set(existingEntries.map((l) => {
@@ -7357,8 +8006,8 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
7357
8006
  }
7358
8007
  if (input.apply) {
7359
8008
  try {
7360
- await mkdir5(dirname7(indexPath), { recursive: true });
7361
- await writeFile6(indexPath, body, "utf8");
8009
+ await mkdir5(dirname8(indexPath), { recursive: true });
8010
+ await writeFile5(indexPath, body, "utf8");
7362
8011
  } catch (e) {
7363
8012
  return {
7364
8013
  exitCode: ExitCode.WRITE_FAILED,
@@ -7386,10 +8035,10 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
7386
8035
  }
7387
8036
 
7388
8037
  // src/commands/observe.ts
7389
- import { mkdir as mkdir6, writeFile as writeFile7 } from "fs/promises";
7390
- import { existsSync as existsSync13, statSync as statSync2 } from "fs";
7391
- import { join as join25 } from "path";
7392
- import { createHash as createHash5 } from "crypto";
8038
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
8039
+ import { existsSync as existsSync15, statSync as statSync3 } from "fs";
8040
+ import { join as join28 } from "path";
8041
+ import { createHash as createHash7 } from "crypto";
7393
8042
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
7394
8043
  function slugify(text) {
7395
8044
  const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -7411,13 +8060,13 @@ async function runObserve(input) {
7411
8060
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
7412
8061
  };
7413
8062
  }
7414
- if (!existsSync13(input.vault) || !statSync2(input.vault).isDirectory()) {
8063
+ if (!existsSync15(input.vault) || !statSync3(input.vault).isDirectory()) {
7415
8064
  return {
7416
8065
  exitCode: ExitCode.VAULT_PATH_INVALID,
7417
8066
  result: err("VAULT_PATH_INVALID", { path: input.vault })
7418
8067
  };
7419
8068
  }
7420
- const transcriptsDir = join25(input.vault, "raw", "transcripts");
8069
+ const transcriptsDir = join28(input.vault, "raw", "transcripts");
7421
8070
  try {
7422
8071
  await mkdir6(transcriptsDir, { recursive: true });
7423
8072
  } catch {
@@ -7429,11 +8078,11 @@ async function runObserve(input) {
7429
8078
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7430
8079
  const slug = slugify(input.text);
7431
8080
  const fileName = `${today}-observation-${slug}.md`;
7432
- const filePath = join25(transcriptsDir, fileName);
8081
+ const filePath = join28(transcriptsDir, fileName);
7433
8082
  const body = `
7434
8083
  ${input.text.trim()}
7435
8084
  `;
7436
- const sha256 = createHash5("sha256").update(Buffer.from(body, "utf8")).digest("hex");
8085
+ const sha256 = createHash7("sha256").update(Buffer.from(body, "utf8")).digest("hex");
7437
8086
  const frontmatterLines = [
7438
8087
  "---",
7439
8088
  "source_url:",
@@ -7447,7 +8096,7 @@ ${input.text.trim()}
7447
8096
  frontmatterLines.push("---");
7448
8097
  const content = frontmatterLines.join("\n") + body;
7449
8098
  try {
7450
- await writeFile7(filePath, content, "utf8");
8099
+ await writeFile6(filePath, content, "utf8");
7451
8100
  } catch (e) {
7452
8101
  return {
7453
8102
  exitCode: ExitCode.WRITE_FAILED,
@@ -7469,9 +8118,9 @@ ${input.text.trim()}
7469
8118
  }
7470
8119
 
7471
8120
  // src/commands/memory.ts
7472
- import { createHash as createHash6 } from "crypto";
7473
- import { mkdir as mkdir7, readFile as readFile17, readdir as readdir5, stat as stat5, writeFile as writeFile8 } from "fs/promises";
7474
- import { basename as basename3, extname, join as join26, relative as relative4, sep as sep4 } from "path";
8121
+ import { createHash as createHash8 } from "crypto";
8122
+ import { mkdir as mkdir7, readFile as readFile20, readdir as readdir5, stat as stat6, writeFile as writeFile7 } from "fs/promises";
8123
+ import { basename as basename3, extname, join as join29, relative as relative5, sep as sep5 } from "path";
7475
8124
  async function runMemoryTopics(input) {
7476
8125
  const scan = await scanVault(input.vault);
7477
8126
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -7535,9 +8184,9 @@ async function runMemoryIndex(input) {
7535
8184
  }
7536
8185
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
7537
8186
  const relCachePath = memoryCacheRelPath(input.project);
7538
- const absCachePath = join26(input.vault, relCachePath);
7539
- await mkdir7(join26(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
7540
- await writeFile8(absCachePath, `${JSON.stringify({
8187
+ const absCachePath = join29(input.vault, relCachePath);
8188
+ await mkdir7(join29(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
8189
+ await writeFile7(absCachePath, `${JSON.stringify({
7541
8190
  generated_at: generatedAt,
7542
8191
  project: input.project,
7543
8192
  topics: state.topics,
@@ -7728,7 +8377,7 @@ async function buildMemoryIndexState(pages, project) {
7728
8377
  }
7729
8378
  async function checkMemoryIndex(vault, project, current) {
7730
8379
  const relCachePath = memoryCacheRelPath(project);
7731
- const cacheText = await readIfExists2(join26(vault, relCachePath));
8380
+ const cacheText = await readIfExists2(join29(vault, relCachePath));
7732
8381
  if (!cacheText) {
7733
8382
  return {
7734
8383
  ok: true,
@@ -7845,7 +8494,7 @@ async function readMemoryPage(page, project, warnings) {
7845
8494
  title,
7846
8495
  summary: summarize(body),
7847
8496
  updated,
7848
- hash: createHash6("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
8497
+ hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
7849
8498
  topics,
7850
8499
  project,
7851
8500
  ...stringField(fm.data.memory_kind) ? { memory_kind: stringField(fm.data.memory_kind) } : {},
@@ -8121,13 +8770,13 @@ function renderMemoryIndexStatusHint(status) {
8121
8770
  }
8122
8771
  async function readIfExists2(path) {
8123
8772
  try {
8124
- return await readFile17(path, "utf8");
8773
+ return await readFile20(path, "utf8");
8125
8774
  } catch {
8126
8775
  return "";
8127
8776
  }
8128
8777
  }
8129
8778
  async function collectImportFiles(source) {
8130
- const st = await stat5(source);
8779
+ const st = await stat6(source);
8131
8780
  if (st.isFile()) return isImportCandidate(source) ? [source] : [];
8132
8781
  const files = [];
8133
8782
  await walkImportFiles(source, files);
@@ -8137,7 +8786,7 @@ async function walkImportFiles(dir, out) {
8137
8786
  const entries = await readdir5(dir, { withFileTypes: true });
8138
8787
  for (const entry of entries) {
8139
8788
  if (entry.name === ".git" || entry.name === "node_modules") continue;
8140
- const path = join26(dir, entry.name);
8789
+ const path = join29(dir, entry.name);
8141
8790
  if (entry.isDirectory()) {
8142
8791
  await walkImportFiles(path, out);
8143
8792
  } else if (entry.isFile() && isImportCandidate(path)) {
@@ -8150,9 +8799,9 @@ function isImportCandidate(path) {
8150
8799
  return ext === ".md" || ext === ".txt";
8151
8800
  }
8152
8801
  async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
8153
- const st = await stat5(file);
8802
+ const st = await stat6(file);
8154
8803
  const sourceKind = classifyImportSource(file);
8155
- const hash = createHash6("sha256").update(await readFile17(file)).digest("hex");
8804
+ const hash = createHash8("sha256").update(await readFile20(file)).digest("hex");
8156
8805
  const baseEntry = {
8157
8806
  source_path: file,
8158
8807
  source_kind: sourceKind,
@@ -8175,7 +8824,7 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
8175
8824
  reason: "policy_source_not_imported"
8176
8825
  };
8177
8826
  }
8178
- const text = await readFile17(file, "utf8");
8827
+ const text = await readFile20(file, "utf8");
8179
8828
  const extracted = extractImportText(text, sourceKind);
8180
8829
  if (!extracted) {
8181
8830
  return {
@@ -8187,7 +8836,7 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
8187
8836
  const redacted = redactSensitiveContent(extracted, { file });
8188
8837
  const privacy = redacted.findings.length > 0 ? "sensitive" : "local";
8189
8838
  const sourceSlug = slugify2(basename3(file, extname(file)));
8190
- const relSource = relative4(sourceRoot, file).split(sep4).join("/");
8839
+ const relSource = relative5(sourceRoot, file).split(sep5).join("/");
8191
8840
  const entry = {
8192
8841
  ...baseEntry,
8193
8842
  status: "ready",
@@ -8204,9 +8853,9 @@ async function writeImportCapture(vault, entry, today) {
8204
8853
  const content = hiddenString(entry, "__content");
8205
8854
  const project = hiddenString(entry, "__project");
8206
8855
  const relPath = await availableImportPath(vault, entry.proposed_path);
8207
- const absPath = join26(vault, relPath);
8208
- await mkdir7(join26(vault, "raw", "transcripts"), { recursive: true });
8209
- await writeFile8(absPath, renderImportCapture(entry, content, project, today), "utf8");
8856
+ const absPath = join29(vault, relPath);
8857
+ await mkdir7(join29(vault, "raw", "transcripts"), { recursive: true });
8858
+ await writeFile7(absPath, renderImportCapture(entry, content, project, today), "utf8");
8210
8859
  const validation = await runValidate({ file: absPath });
8211
8860
  return {
8212
8861
  relPath,
@@ -8221,7 +8870,7 @@ async function availableImportPath(vault, proposed) {
8221
8870
  const stem = proposed.slice(0, -ext.length);
8222
8871
  let candidate = proposed;
8223
8872
  let i = 2;
8224
- while (await readIfExists2(join26(vault, candidate))) {
8873
+ while (await readIfExists2(join29(vault, candidate))) {
8225
8874
  candidate = `${stem}-${i}${ext}`;
8226
8875
  i++;
8227
8876
  }
@@ -8259,7 +8908,7 @@ function hiddenString(entry, key) {
8259
8908
  return entry[key] ?? "";
8260
8909
  }
8261
8910
  function classifyImportSource(file) {
8262
- const rel = file.split(sep4).join("/");
8911
+ const rel = file.split(sep5).join("/");
8263
8912
  const name = basename3(file);
8264
8913
  if (rel.includes("/.codex/memories/")) return "codex-memory";
8265
8914
  if (rel.includes("/.codex/rules/")) return "codex-rule";
@@ -8393,10 +9042,10 @@ function memoryCacheRelPath(project) {
8393
9042
  }
8394
9043
  async function readMemoryCache(vault, project) {
8395
9044
  if (project) {
8396
- const projectCache = await readIfExists2(join26(vault, memoryCacheRelPath(project)));
9045
+ const projectCache = await readIfExists2(join29(vault, memoryCacheRelPath(project)));
8397
9046
  if (projectCache) return projectCache;
8398
9047
  }
8399
- return readIfExists2(join26(vault, ".skillwiki", "memory-topics.json"));
9048
+ return readIfExists2(join29(vault, ".skillwiki", "memory-topics.json"));
8400
9049
  }
8401
9050
  function dedupePages(pages) {
8402
9051
  const seen = /* @__PURE__ */ new Set();
@@ -8482,8 +9131,8 @@ function slugify2(value) {
8482
9131
  }
8483
9132
 
8484
9133
  // src/commands/query.ts
8485
- import { readFile as readFile18, stat as stat6 } from "fs/promises";
8486
- import { join as join27 } from "path";
9134
+ import { readFile as readFile21, stat as stat7 } from "fs/promises";
9135
+ import { join as join30 } from "path";
8487
9136
  var W_KEYWORD = 2;
8488
9137
  var W_SOURCE_OVERLAP = 4;
8489
9138
  var W_WIKILINK = 3;
@@ -8604,10 +9253,10 @@ function computeKeywordScore(terms, title, tags, body) {
8604
9253
  return score;
8605
9254
  }
8606
9255
  async function loadOrBuildGraph(vault) {
8607
- const graphPath = join27(vault, ".skillwiki", "graph.json");
9256
+ const graphPath = join30(vault, ".skillwiki", "graph.json");
8608
9257
  let needsBuild = false;
8609
9258
  try {
8610
- const fileStat = await stat6(graphPath);
9259
+ const fileStat = await stat7(graphPath);
8611
9260
  const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
8612
9261
  if (ageHours > 24) needsBuild = true;
8613
9262
  } catch {
@@ -8618,7 +9267,7 @@ async function loadOrBuildGraph(vault) {
8618
9267
  if (buildResult.exitCode !== 0) return null;
8619
9268
  }
8620
9269
  try {
8621
- const raw = await readFile18(graphPath, "utf8");
9270
+ const raw = await readFile21(graphPath, "utf8");
8622
9271
  return JSON.parse(raw);
8623
9272
  } catch {
8624
9273
  return null;
@@ -8633,27 +9282,27 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
8633
9282
  import { z as z2 } from "zod";
8634
9283
 
8635
9284
  // src/mcp/vault-resolve.ts
8636
- import { join as join28, resolve as resolve7 } from "path";
9285
+ import { join as join31, resolve as resolve8 } from "path";
8637
9286
 
8638
9287
  // src/mcp/allowlist.ts
8639
- import { resolve as resolve6, sep as sep5 } from "path";
8640
- import { realpathSync } from "fs";
9288
+ import { resolve as resolve7, sep as sep6 } from "path";
9289
+ import { realpathSync as realpathSync2 } from "fs";
8641
9290
  function parseVaultAllowlist(envValue) {
8642
9291
  if (envValue === void 0 || envValue.trim() === "") return null;
8643
- return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((p) => resolve6(p));
9292
+ return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((p) => resolve7(p));
8644
9293
  }
8645
9294
  function vaultAllowedByList(vaultPath, allowlist) {
8646
9295
  if (!allowlist || allowlist.length === 0) return true;
8647
9296
  let canonical = vaultPath;
8648
9297
  try {
8649
- canonical = realpathSync(vaultPath);
9298
+ canonical = realpathSync2(vaultPath);
8650
9299
  } catch {
8651
- canonical = resolve6(vaultPath);
9300
+ canonical = resolve7(vaultPath);
8652
9301
  }
8653
- const resolved = resolve6(canonical);
9302
+ const resolved = resolve7(canonical);
8654
9303
  return allowlist.some((root) => {
8655
- const r = resolve6(root);
8656
- return resolved === r || resolved.startsWith(r + sep5);
9304
+ const r = resolve7(root);
9305
+ return resolved === r || resolved.startsWith(r + sep6);
8657
9306
  });
8658
9307
  }
8659
9308
  function getVaultAllowlistFromEnv() {
@@ -8666,7 +9315,7 @@ async function resolveMcpVault(input) {
8666
9315
  let vaultPath;
8667
9316
  let source = "resolved";
8668
9317
  if (input.vault !== void 0 && input.vault.length > 0) {
8669
- vaultPath = resolve7(input.vault);
9318
+ vaultPath = resolve8(input.vault);
8670
9319
  source = "flag";
8671
9320
  } else {
8672
9321
  const r = await resolveRuntimePath({
@@ -8678,7 +9327,7 @@ async function resolveMcpVault(input) {
8678
9327
  cwd: input.cwd ?? process.cwd()
8679
9328
  });
8680
9329
  if (!r.ok) return r;
8681
- vaultPath = resolve7(r.data.path);
9330
+ vaultPath = resolve8(r.data.path);
8682
9331
  source = r.data.source;
8683
9332
  }
8684
9333
  const scan = await scanVault(vaultPath);
@@ -8695,7 +9344,7 @@ async function resolveMcpVault(input) {
8695
9344
  return ok({ vault: vaultPath, source });
8696
9345
  }
8697
9346
  function defaultGraphOut(vault) {
8698
- return join28(vault, ".skillwiki", "graph.json");
9347
+ return join31(vault, ".skillwiki", "graph.json");
8699
9348
  }
8700
9349
 
8701
9350
  // src/mcp/result-format.ts
@@ -8710,9 +9359,9 @@ function formatToolResult(payload) {
8710
9359
  }
8711
9360
 
8712
9361
  // src/mcp/audit-log.ts
8713
- import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
9362
+ import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
8714
9363
  import { homedir } from "os";
8715
- import { join as join29 } from "path";
9364
+ import { join as join32 } from "path";
8716
9365
  function auditEnabled() {
8717
9366
  const v = process.env.SKILLWIKI_MCP_AUDIT;
8718
9367
  if (v === "0" || v === "false") return false;
@@ -8724,7 +9373,7 @@ function auditSink() {
8724
9373
  function auditFilePath() {
8725
9374
  const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
8726
9375
  if (custom && custom.length > 0) return custom;
8727
- return join29(homedir(), ".skillwiki", "mcp-audit.jsonl");
9376
+ return join32(homedir(), ".skillwiki", "mcp-audit.jsonl");
8728
9377
  }
8729
9378
  function auditMcpToolCall(entry) {
8730
9379
  if (!auditEnabled()) return;
@@ -8734,7 +9383,7 @@ function auditMcpToolCall(entry) {
8734
9383
  return;
8735
9384
  }
8736
9385
  const path = auditFilePath();
8737
- mkdirSync3(join29(path, ".."), { recursive: true });
9386
+ mkdirSync5(join32(path, ".."), { recursive: true });
8738
9387
  appendFileSync(path, line, "utf8");
8739
9388
  }
8740
9389
  async function runMcpToolHandler(tool, input, fn) {
@@ -8954,8 +9603,8 @@ function registerMcpMutatingTools(server) {
8954
9603
  }
8955
9604
 
8956
9605
  // src/mcp/resources.ts
8957
- import { readFile as readFile20 } from "fs/promises";
8958
- import { join as join31 } from "path";
9606
+ import { readFile as readFile23 } from "fs/promises";
9607
+ import { join as join34 } from "path";
8959
9608
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
8960
9609
 
8961
9610
  // src/mcp/lint-bucket.ts
@@ -9083,9 +9732,9 @@ async function fetchQueryPreview(input) {
9083
9732
  }
9084
9733
 
9085
9734
  // src/mcp/graph-html.ts
9086
- import { readFile as readFile19 } from "fs/promises";
9087
- import { join as join30 } from "path";
9088
- import { existsSync as existsSync14 } from "fs";
9735
+ import { readFile as readFile22 } from "fs/promises";
9736
+ import { join as join33 } from "path";
9737
+ import { existsSync as existsSync16 } from "fs";
9089
9738
  var TYPE_COLORS = {
9090
9739
  entities: "#e74c3c",
9091
9740
  concepts: "#27ae60",
@@ -9153,9 +9802,9 @@ ${nodeSvg}
9153
9802
  return { html, node_count: nodes.length, edge_count: edges.length, truncated };
9154
9803
  }
9155
9804
  async function fetchGraphHtmlReport(input) {
9156
- const graphPath = input.graphPath ?? join30(input.vault, ".skillwiki", "graph.json");
9805
+ const graphPath = input.graphPath ?? join33(input.vault, ".skillwiki", "graph.json");
9157
9806
  const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
9158
- if (!existsSync14(graphPath)) {
9807
+ if (!existsSync16(graphPath)) {
9159
9808
  return {
9160
9809
  exitCode: ExitCode.FILE_NOT_FOUND,
9161
9810
  result: err("GRAPH_MISSING", { path: graphPath, hint: "Run skillwiki.graph_build first." })
@@ -9163,7 +9812,7 @@ async function fetchGraphHtmlReport(input) {
9163
9812
  }
9164
9813
  let raw;
9165
9814
  try {
9166
- raw = await readFile19(graphPath, "utf8");
9815
+ raw = await readFile22(graphPath, "utf8");
9167
9816
  } catch (e) {
9168
9817
  return {
9169
9818
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -9227,7 +9876,7 @@ async function fetchStaleSummary(input) {
9227
9876
 
9228
9877
  // src/mcp/resources.ts
9229
9878
  async function readVaultFile(vault, rel) {
9230
- return readFile20(join31(vault, rel), "utf8");
9879
+ return readFile23(join34(vault, rel), "utf8");
9231
9880
  }
9232
9881
  async function tailLines(text, lines) {
9233
9882
  const parts = text.split(/\r?\n/);
@@ -9313,9 +9962,9 @@ function registerMcpResources(server) {
9313
9962
  if (!v.ok) {
9314
9963
  return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
9315
9964
  }
9316
- const path = join31(v.data.vault, ".skillwiki", "graph.json");
9965
+ const path = join34(v.data.vault, ".skillwiki", "graph.json");
9317
9966
  try {
9318
- const raw = await readFile20(path, "utf8");
9967
+ const raw = await readFile23(path, "utf8");
9319
9968
  const graph = JSON.parse(raw);
9320
9969
  const adjacency = graph.adjacency ?? {};
9321
9970
  const nodes = Object.keys(adjacency);
@@ -9628,13 +10277,29 @@ export {
9628
10277
  ExitCode,
9629
10278
  ok,
9630
10279
  err,
9631
- TypedKnowledgeSchema,
10280
+ RawSourceSchema,
9632
10281
  MetaSchema,
9633
- detectSchema,
9634
10282
  isBlockedHost,
9635
10283
  splitFrontmatter,
9636
10284
  extractFrontmatter,
9637
10285
  scanSensitiveContent,
10286
+ redactSensitiveContent,
10287
+ readLastOp,
10288
+ appendLastOp,
10289
+ clearLastOp,
10290
+ atomicWriteText,
10291
+ runLogAppend,
10292
+ renderIndexUpsert,
10293
+ upsertIndexEntry,
10294
+ getSessionId,
10295
+ getCliSessionId,
10296
+ readLock,
10297
+ acquireLock,
10298
+ releaseLock,
10299
+ acquireOwnedSyncLock,
10300
+ releaseOwnedSyncLock,
10301
+ assertTargetInsideVault,
10302
+ prepareTypedPage,
9638
10303
  runValidate,
9639
10304
  scanVault,
9640
10305
  readPage,
@@ -9650,9 +10315,8 @@ export {
9650
10315
  runAudit,
9651
10316
  findPlugin,
9652
10317
  extractTaxonomy,
9653
- readLastOp,
9654
- appendLastOp,
9655
- clearLastOp,
10318
+ taxonomyCommentForPage,
10319
+ reconcileTaxonomyDocument,
9656
10320
  runLinks,
9657
10321
  runTagAudit,
9658
10322
  runIndexCheck,