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.
- package/dist/{chunk-FU462DVS.js → chunk-TUFQZ5K4.js} +1182 -518
- package/dist/cli.js +1133 -412
- package/dist/skillwiki-mcp.js +1 -1
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +2 -2
- package/skills/.codex-plugin/plugin.json +3 -3
- package/skills/README.md +6 -0
- package/skills/agents/wiki-ingest.md +30 -37
- package/skills/agents/wiki-query.md +13 -3
- package/skills/package.json +1 -1
- package/skills/skills/using-skillwiki/SKILL.md +31 -2
- package/skills/skills/wiki-archive/SKILL.md +18 -4
- package/skills/skills/wiki-ingest/SKILL.md +23 -27
- package/skills/skills/wiki-query/SKILL.md +11 -2
- package/skills/skills/wiki-remove/SKILL.md +96 -0
- package/skills/using-skillwiki/SKILL.md +31 -2
- package/skills/wiki-archive/SKILL.md +18 -4
- package/skills/wiki-ingest/SKILL.md +23 -27
- package/skills/wiki-query/SKILL.md +11 -2
- package/skills/wiki-remove/SKILL.md +96 -0
|
@@ -328,34 +328,162 @@ function getErrorMessage(e) {
|
|
|
328
328
|
return e instanceof Error ? e.message : String(e);
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
// src/commands/
|
|
332
|
-
import { readFile,
|
|
333
|
-
import { join
|
|
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/
|
|
336
|
-
import
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
350
|
-
const
|
|
351
|
-
if (!
|
|
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
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
573
|
-
const absVault =
|
|
574
|
-
const relPath =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
659
|
-
import { join as
|
|
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 &&
|
|
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) &&
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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(
|
|
911
|
-
await
|
|
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
|
|
955
|
-
import { dirname as
|
|
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
|
|
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(
|
|
1005
|
-
await
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
1257
|
-
import { dirname as
|
|
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
|
|
1368
|
-
import { stat as
|
|
1369
|
-
import { join as
|
|
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 = [
|
|
1380
|
-
if (!normalized.endsWith(".md")) candidates.push(
|
|
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(
|
|
1383
|
-
if (!normalized.endsWith(".md")) candidates.push(
|
|
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) =>
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
2069
|
+
await stat4(join10(cur, "SCHEMA.md"));
|
|
1459
2070
|
return cur;
|
|
1460
2071
|
} catch {
|
|
1461
2072
|
}
|
|
1462
|
-
const parent =
|
|
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
|
|
1551
|
-
import { join as
|
|
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
|
|
1556
|
-
function
|
|
1557
|
-
const
|
|
1558
|
-
|
|
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(
|
|
1562
|
-
} catch (
|
|
1563
|
-
return err("INVALID_FRONTMATTER", { message: getErrorMessage(
|
|
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
|
|
1569
|
-
if (!Array.isArray(
|
|
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
|
-
|
|
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
|
|
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
|
|
1616
|
-
import { join as
|
|
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
|
|
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
|
|
1662
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
2546
|
+
const specPath = join13(input.vault, relDir, "spec.md");
|
|
1887
2547
|
try {
|
|
1888
|
-
const specContent = await
|
|
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(
|
|
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 =
|
|
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 =
|
|
2663
|
+
const dest = join13(archiveDir, t.path.split("/").pop());
|
|
2004
2664
|
try {
|
|
2005
|
-
await
|
|
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 =
|
|
2675
|
+
const histDir = join13(input.vault, "projects", slug, "history", "archived-work");
|
|
2016
2676
|
await mkdir3(histDir, { recursive: true });
|
|
2017
|
-
const dest =
|
|
2677
|
+
const dest = join13(histDir, itemName);
|
|
2018
2678
|
try {
|
|
2019
|
-
await
|
|
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 =
|
|
2684
|
+
const dest = join13(archiveDir, w.path.replace(/\//g, "_"));
|
|
2025
2685
|
try {
|
|
2026
|
-
await
|
|
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
|
|
2082
|
-
import { join as
|
|
2083
|
-
var
|
|
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
|
|
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 =
|
|
2750
|
+
const logPath = join14(input.vault, "log.md");
|
|
2091
2751
|
let logText;
|
|
2092
2752
|
try {
|
|
2093
|
-
logText = await
|
|
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(
|
|
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 =
|
|
2770
|
+
const rotatedPath = join14(input.vault, rotatedName);
|
|
2111
2771
|
try {
|
|
2112
|
-
await
|
|
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
|
|
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
|
|
2156
|
-
import { join as
|
|
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
|
|
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
|
|
2175
|
-
import { mkdirSync as
|
|
2176
|
-
import { dirname as
|
|
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:
|
|
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
|
-
|
|
2281
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
2978
|
+
const fullPath = join16(input.vault, oldPath);
|
|
2319
2979
|
try {
|
|
2320
|
-
|
|
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 =
|
|
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
|
|
3075
|
+
return createHash4("sha256").update(body).digest("hex");
|
|
2416
3076
|
}
|
|
2417
3077
|
function readManifest(path) {
|
|
2418
3078
|
try {
|
|
2419
|
-
const parsed = JSON.parse(
|
|
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 {
|
|
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
|
|
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
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
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
|
|
2587
|
-
import { readFile as
|
|
2588
|
-
import { createHash as
|
|
2589
|
-
import { join as
|
|
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
|
|
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 =
|
|
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
|
|
2647
|
-
import { mkdir as mkdir4, readFile as
|
|
2648
|
-
import { dirname as
|
|
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(
|
|
3323
|
+
await unlink2(join17(input.vault, violation.relPath));
|
|
2682
3324
|
} else {
|
|
2683
|
-
await mkdir4(
|
|
2684
|
-
await rename4(
|
|
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
|
|
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 =
|
|
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 =
|
|
2762
|
-
if (!
|
|
2763
|
-
if (await hasSameContent(
|
|
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 =
|
|
2773
|
-
const filename =
|
|
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([
|
|
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 =
|
|
2801
|
-
const newStem =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 (!
|
|
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 =
|
|
3257
|
-
if (!
|
|
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
|
|
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 =
|
|
3772
|
-
const pending =
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
4410
|
-
import { existsSync as
|
|
4411
|
-
import { join as
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
5119
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
4471
5120
|
import { hostname as nodeHostname, userInfo } from "os";
|
|
4472
|
-
import { join as
|
|
5121
|
+
import { join as join20 } from "path";
|
|
4473
5122
|
import yaml3 from "js-yaml";
|
|
4474
|
-
var FLEET_REL_PATH =
|
|
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 ?
|
|
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 ?
|
|
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
|
|
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(
|
|
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
|
|
4933
|
-
import { join as
|
|
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
|
|
4939
|
-
import { join as
|
|
4940
|
-
var REGISTRY_PATH =
|
|
4941
|
-
var CODEX_CONFIG_PATH =
|
|
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 =
|
|
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 =
|
|
4982
|
-
if (!
|
|
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:
|
|
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 =
|
|
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
|
|
5057
|
-
import { join as
|
|
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,
|
|
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 (!
|
|
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 =
|
|
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
|
|
5133
|
-
import { join as
|
|
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 =
|
|
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 (!
|
|
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
|
|
5235
|
-
import { join as
|
|
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
|
|
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 (!
|
|
5909
|
+
if (!existsSync12(latestPath)) return null;
|
|
5261
5910
|
try {
|
|
5262
|
-
return parseLatestRunFile(
|
|
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
|
|
5292
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
6110
|
+
const back = readFile18(testFile, "utf8");
|
|
5462
6111
|
const readMs = Date.now() - readStart;
|
|
5463
6112
|
if (back !== payload) {
|
|
5464
6113
|
try {
|
|
5465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 ===
|
|
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 =
|
|
5551
|
-
if (
|
|
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 =
|
|
5556
|
-
if (
|
|
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 =
|
|
6212
|
+
const st = lstatSync2(binPath);
|
|
5564
6213
|
if (st.isSymbolicLink()) {
|
|
5565
|
-
const target =
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
6294
|
+
if (!existsSync14(join26(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
5646
6295
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
5647
|
-
if (!
|
|
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 ?
|
|
5656
|
-
if (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 =
|
|
5670
|
-
if (
|
|
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 =
|
|
6329
|
+
const skillsDir = join26(home, ".claude", "skills");
|
|
5681
6330
|
const agentSkillDirs = [
|
|
5682
|
-
{ label: "~/.codex/skills/", path:
|
|
5683
|
-
{ label: "~/.agents/skills/", path:
|
|
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 =
|
|
5787
|
-
if (
|
|
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 (!
|
|
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 (!
|
|
5820
|
-
if (!
|
|
5821
|
-
if (!
|
|
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 =
|
|
5832
|
-
if (!
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
6217
|
-
|
|
6865
|
+
join26(home, "Library", "Logs", "wiki-pull.log"),
|
|
6866
|
+
join26(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
|
|
6218
6867
|
] : [
|
|
6219
|
-
|
|
6220
|
-
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
6261
|
-
if (!
|
|
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 =
|
|
6444
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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
|
|
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 ?
|
|
6599
|
-
const shareDir = input.shareDir ?? (isMac ?
|
|
6600
|
-
const filterPath = input.filterPath ??
|
|
6601
|
-
const packagedSnapshotPath =
|
|
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 ?? (
|
|
7252
|
+
const snapshotPath = input.snapshotScriptPath ?? (existsSync14(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
|
|
6604
7253
|
function snapshotLastStatusCheck() {
|
|
6605
|
-
const snapshotLog =
|
|
7254
|
+
const snapshotLog = join26(logDir, "wiki-snapshot.log");
|
|
6606
7255
|
try {
|
|
6607
|
-
const logContent =
|
|
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 =
|
|
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 =
|
|
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" &&
|
|
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" &&
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
6727
|
-
const c1 =
|
|
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 =
|
|
7427
|
+
const logFile = join26(logDir, "wiki-push.log");
|
|
6779
7428
|
let c3;
|
|
6780
7429
|
try {
|
|
6781
|
-
const logContent =
|
|
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 =
|
|
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 =
|
|
7498
|
+
const fetchLogFile = join26(logDir, "wiki-fetch.log");
|
|
6850
7499
|
let cFetch;
|
|
6851
7500
|
try {
|
|
6852
|
-
const logContent =
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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(
|
|
7642
|
+
results.push(join26(dir, entry.name));
|
|
6994
7643
|
} else if (entry.isDirectory()) {
|
|
6995
|
-
results.push(...findSkillMd(
|
|
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) =>
|
|
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() &&
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
7187
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
7894
|
+
const filePath = join27(compoundDir, entry.name);
|
|
7246
7895
|
let text;
|
|
7247
7896
|
try {
|
|
7248
|
-
text = await
|
|
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(
|
|
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 =
|
|
7920
|
+
const filePath = join27(input.vault, dir, entry.name);
|
|
7272
7921
|
let text;
|
|
7273
7922
|
try {
|
|
7274
|
-
text = await
|
|
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 =
|
|
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 =
|
|
7943
|
+
const filePath = join27(input.vault, page);
|
|
7295
7944
|
let text;
|
|
7296
7945
|
try {
|
|
7297
|
-
text = await
|
|
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 =
|
|
7965
|
+
const indexPath = join27(projectDir, "knowledge.md");
|
|
7317
7966
|
let existing = false;
|
|
7318
7967
|
let stale = false;
|
|
7319
7968
|
try {
|
|
7320
|
-
const existingText = await
|
|
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(
|
|
7361
|
-
await
|
|
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
|
|
7390
|
-
import { existsSync as
|
|
7391
|
-
import { join as
|
|
7392
|
-
import { createHash as
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
8081
|
+
const filePath = join28(transcriptsDir, fileName);
|
|
7433
8082
|
const body = `
|
|
7434
8083
|
${input.text.trim()}
|
|
7435
8084
|
`;
|
|
7436
|
-
const sha256 =
|
|
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
|
|
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
|
|
7473
|
-
import { mkdir as mkdir7, readFile as
|
|
7474
|
-
import { basename as basename3, extname, join as
|
|
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 =
|
|
7539
|
-
await mkdir7(
|
|
7540
|
-
await
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
8802
|
+
const st = await stat6(file);
|
|
8154
8803
|
const sourceKind = classifyImportSource(file);
|
|
8155
|
-
const hash =
|
|
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
|
|
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 =
|
|
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 =
|
|
8208
|
-
await mkdir7(
|
|
8209
|
-
await
|
|
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(
|
|
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(
|
|
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(
|
|
9045
|
+
const projectCache = await readIfExists2(join29(vault, memoryCacheRelPath(project)));
|
|
8397
9046
|
if (projectCache) return projectCache;
|
|
8398
9047
|
}
|
|
8399
|
-
return readIfExists2(
|
|
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
|
|
8486
|
-
import { join as
|
|
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 =
|
|
9256
|
+
const graphPath = join30(vault, ".skillwiki", "graph.json");
|
|
8608
9257
|
let needsBuild = false;
|
|
8609
9258
|
try {
|
|
8610
|
-
const fileStat = await
|
|
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
|
|
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
|
|
9285
|
+
import { join as join31, resolve as resolve8 } from "path";
|
|
8637
9286
|
|
|
8638
9287
|
// src/mcp/allowlist.ts
|
|
8639
|
-
import { resolve as
|
|
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) =>
|
|
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 =
|
|
9298
|
+
canonical = realpathSync2(vaultPath);
|
|
8650
9299
|
} catch {
|
|
8651
|
-
canonical =
|
|
9300
|
+
canonical = resolve7(vaultPath);
|
|
8652
9301
|
}
|
|
8653
|
-
const resolved =
|
|
9302
|
+
const resolved = resolve7(canonical);
|
|
8654
9303
|
return allowlist.some((root) => {
|
|
8655
|
-
const r =
|
|
8656
|
-
return resolved === r || resolved.startsWith(r +
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
9362
|
+
import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
8714
9363
|
import { homedir } from "os";
|
|
8715
|
-
import { join as
|
|
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
|
|
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
|
-
|
|
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
|
|
8958
|
-
import { join as
|
|
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
|
|
9087
|
-
import { join as
|
|
9088
|
-
import { existsSync as
|
|
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 ??
|
|
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 (!
|
|
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
|
|
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
|
|
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 =
|
|
9965
|
+
const path = join34(v.data.vault, ".skillwiki", "graph.json");
|
|
9317
9966
|
try {
|
|
9318
|
-
const raw = await
|
|
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
|
-
|
|
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
|
-
|
|
9654
|
-
|
|
9655
|
-
clearLastOp,
|
|
10318
|
+
taxonomyCommentForPage,
|
|
10319
|
+
reconcileTaxonomyDocument,
|
|
9656
10320
|
runLinks,
|
|
9657
10321
|
runTagAudit,
|
|
9658
10322
|
runIndexCheck,
|