skillwiki 0.9.61 → 0.9.62
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-2PENIQ3A.js} +1181 -518
- package/dist/cli.js +994 -438
- package/dist/skillwiki-mcp.js +1 -1
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- 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 +16 -1
- package/skills/skills/wiki-ingest/SKILL.md +23 -27
- package/skills/skills/wiki-query/SKILL.md +11 -2
- package/skills/using-skillwiki/SKILL.md +16 -1
- package/skills/wiki-ingest/SKILL.md +23 -27
- package/skills/wiki-query/SKILL.md +11 -2
|
@@ -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");
|
|
@@ -2859,6 +3501,7 @@ function buildCliSurface() {
|
|
|
2859
3501
|
program.command("transcripts").option("--since <date>").option("--wiki <name>");
|
|
2860
3502
|
program.command("project-index").option("--apply").option("--wiki <name>");
|
|
2861
3503
|
program.command("compound");
|
|
3504
|
+
program.command("tag");
|
|
2862
3505
|
program.command("tag-sync").option("--dry-run").option("--wiki <name>");
|
|
2863
3506
|
program.command("sync");
|
|
2864
3507
|
program.command("backup");
|
|
@@ -2868,6 +3511,7 @@ function buildCliSurface() {
|
|
|
2868
3511
|
program.command("memory");
|
|
2869
3512
|
program.command("ingest").requiredOption("--vault <path>").requiredOption("--type <type>").requiredOption("--title <title>").option("--tags <csv>").option("--provenance <provenance>").option("--dry-run");
|
|
2870
3513
|
program.command("fleet");
|
|
3514
|
+
program.command("page");
|
|
2871
3515
|
const graphCmd = program.commands.find((c) => c.name() === "graph");
|
|
2872
3516
|
graphCmd.command("build").option("--out <path>").option("--wiki <name>");
|
|
2873
3517
|
const canvasCmd = program.commands.find((c) => c.name() === "canvas");
|
|
@@ -2881,6 +3525,10 @@ function buildCliSurface() {
|
|
|
2881
3525
|
compoundCmd.command("promote").requiredOption("--project <slug>").option("--dry-run").option("--wiki <name>");
|
|
2882
3526
|
compoundCmd.command("list").requiredOption("--project <slug>").option("--wiki <name>");
|
|
2883
3527
|
compoundCmd.command("delete").requiredOption("--project <slug>").option("--wiki <name>");
|
|
3528
|
+
const tagCmd = program.commands.find((c) => c.name() === "tag");
|
|
3529
|
+
tagCmd.command("reconcile").requiredOption("--page <path>").option("--from <path>").option("--tags <csv>").option("--reason <text>").option("--write").option("--wiki <name>");
|
|
3530
|
+
const pageCmd = program.commands.find((c) => c.name() === "page");
|
|
3531
|
+
pageCmd.command("publish").requiredOption("--target <path>").option("--log-note <text>").option("--write").option("--wiki <name>");
|
|
2884
3532
|
const syncCmd = program.commands.find((c) => c.name() === "sync");
|
|
2885
3533
|
syncCmd.command("status").option("--wiki <name>").option("--include-stashes").option("--include-remote-health").option("--check-snapshotter");
|
|
2886
3534
|
syncCmd.command("push").option("--wiki <name>");
|
|
@@ -3189,7 +3837,7 @@ function recomputeRawSha256IfPresent(content) {
|
|
|
3189
3837
|
const split = splitFrontmatter(content);
|
|
3190
3838
|
if (!split.ok) return content;
|
|
3191
3839
|
if (!/^sha256:\s*[0-9a-f]{64}$/m.test(split.data.rawFrontmatter)) return content;
|
|
3192
|
-
const sha256 =
|
|
3840
|
+
const sha256 = createHash6("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
|
|
3193
3841
|
const rawFrontmatter = split.data.rawFrontmatter.replace(/^sha256:\s*[0-9a-f]{64}$/m, `sha256: ${sha256}`);
|
|
3194
3842
|
return `---
|
|
3195
3843
|
${rawFrontmatter}
|
|
@@ -3237,24 +3885,24 @@ async function walkMarkdownFiles(absDir, vaultRoot) {
|
|
|
3237
3885
|
const entries = await readdir3(absDir, { withFileTypes: true });
|
|
3238
3886
|
const pages = [];
|
|
3239
3887
|
for (const entry of entries) {
|
|
3240
|
-
const absPath =
|
|
3888
|
+
const absPath = join18(absDir, entry.name);
|
|
3241
3889
|
if (entry.isDirectory()) {
|
|
3242
3890
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
3243
3891
|
pages.push(...await walkMarkdownFiles(absPath, vaultRoot));
|
|
3244
3892
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3245
|
-
pages.push({ absPath, relPath:
|
|
3893
|
+
pages.push({ absPath, relPath: relative4(vaultRoot, absPath).split(sep4).join("/") });
|
|
3246
3894
|
}
|
|
3247
3895
|
}
|
|
3248
3896
|
return pages;
|
|
3249
3897
|
}
|
|
3250
3898
|
async function collectCliRefsPages(vault) {
|
|
3251
|
-
if (!
|
|
3899
|
+
if (!existsSync7(join18(vault, "SCHEMA.md"))) {
|
|
3252
3900
|
return err("VAULT_PATH_INVALID", { root: vault, reason: "SCHEMA.md missing" });
|
|
3253
3901
|
}
|
|
3254
3902
|
const pages = [];
|
|
3255
3903
|
for (const dir of CLI_REFS_TYPED_DIRS) {
|
|
3256
|
-
const absDir =
|
|
3257
|
-
if (!
|
|
3904
|
+
const absDir = join18(vault, dir);
|
|
3905
|
+
if (!existsSync7(absDir)) continue;
|
|
3258
3906
|
pages.push(...await walkMarkdownFiles(absDir, vault));
|
|
3259
3907
|
}
|
|
3260
3908
|
return ok(pages);
|
|
@@ -3375,7 +4023,7 @@ async function applyFileSourceUrlFix(input, scan, fileSourceUrlFlags, fileSource
|
|
|
3375
4023
|
for (const relPath of fileSourceUrlFrontmatterFlags) {
|
|
3376
4024
|
try {
|
|
3377
4025
|
const absPath = `${input.vault}/${relPath}`;
|
|
3378
|
-
const raw = await
|
|
4026
|
+
const raw = await readFile15(absPath, "utf8");
|
|
3379
4027
|
const parts = raw.split("---", 3);
|
|
3380
4028
|
if (parts.length < 3) {
|
|
3381
4029
|
unresolved.push(relPath);
|
|
@@ -3768,8 +4416,8 @@ async function runLint(input) {
|
|
|
3768
4416
|
const readKnowledgeContent = (slug) => {
|
|
3769
4417
|
const existing = knowledgeContentCache.get(slug);
|
|
3770
4418
|
if (existing) return existing;
|
|
3771
|
-
const knowledgePath =
|
|
3772
|
-
const pending =
|
|
4419
|
+
const knowledgePath = join18(lintVault, "projects", slug, "knowledge.md");
|
|
4420
|
+
const pending = existsSync7(knowledgePath) ? readFile15(knowledgePath, "utf8").catch(() => null) : Promise.resolve(null);
|
|
3773
4421
|
knowledgeContentCache.set(slug, pending);
|
|
3774
4422
|
return pending;
|
|
3775
4423
|
};
|
|
@@ -3870,12 +4518,12 @@ async function runLint(input) {
|
|
|
3870
4518
|
else delete buckets.sensitive_content;
|
|
3871
4519
|
}
|
|
3872
4520
|
if (shouldFix("legacy_citation_style") && legacyPages.length > 0) {
|
|
3873
|
-
const
|
|
4521
|
+
const FENCE_RE = /```[\s\S]*?```/g;
|
|
3874
4522
|
const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
|
|
3875
4523
|
for (const relPath of legacyPages) {
|
|
3876
4524
|
try {
|
|
3877
4525
|
const absPath = `${input.vault}/${relPath}`;
|
|
3878
|
-
const raw = await
|
|
4526
|
+
const raw = await readFile15(absPath, "utf8");
|
|
3879
4527
|
const split = splitFrontmatter(raw);
|
|
3880
4528
|
if (!split.ok) {
|
|
3881
4529
|
unresolved.push(relPath);
|
|
@@ -3883,7 +4531,7 @@ async function runLint(input) {
|
|
|
3883
4531
|
}
|
|
3884
4532
|
const body = split.data.body;
|
|
3885
4533
|
const rawFm = split.data.rawFrontmatter;
|
|
3886
|
-
const stripped = body.replace(
|
|
4534
|
+
const stripped = body.replace(FENCE_RE, "");
|
|
3887
4535
|
const lines = stripped.split("\n");
|
|
3888
4536
|
const inlineMarkers = [];
|
|
3889
4537
|
let inSources = false;
|
|
@@ -3974,7 +4622,7 @@ ${newBody}`;
|
|
|
3974
4622
|
for (const relPath of noOverview) {
|
|
3975
4623
|
try {
|
|
3976
4624
|
const absPath = `${input.vault}/${relPath}`;
|
|
3977
|
-
const raw = await
|
|
4625
|
+
const raw = await readFile15(absPath, "utf8");
|
|
3978
4626
|
const split = splitFrontmatter(raw);
|
|
3979
4627
|
if (!split.ok) {
|
|
3980
4628
|
unresolved.push(relPath);
|
|
@@ -4015,7 +4663,7 @@ ${trimmedBody}`;
|
|
|
4015
4663
|
for (const relPath of missingTldrFlags) {
|
|
4016
4664
|
try {
|
|
4017
4665
|
const absPath = `${input.vault}/${relPath}`;
|
|
4018
|
-
const raw = await
|
|
4666
|
+
const raw = await readFile15(absPath, "utf8");
|
|
4019
4667
|
const split = splitFrontmatter(raw);
|
|
4020
4668
|
if (!split.ok) {
|
|
4021
4669
|
unresolved.push(relPath);
|
|
@@ -4060,12 +4708,12 @@ ${lines.join("\n")}`;
|
|
|
4060
4708
|
}
|
|
4061
4709
|
if (shouldFix("wikilink_citation") && wikilinkCitationFlags.length > 0) {
|
|
4062
4710
|
const WIKILINK_RE = /\[\[raw\/([^\]|]+)(?:\|[^\]]*)?\]\]/g;
|
|
4063
|
-
const
|
|
4711
|
+
const FENCE_RE = /```[\s\S]*?```/g;
|
|
4064
4712
|
const wikilinkFixed = [];
|
|
4065
4713
|
for (const relPath of wikilinkCitationFlags) {
|
|
4066
4714
|
try {
|
|
4067
4715
|
const absPath = `${input.vault}/${relPath}`;
|
|
4068
|
-
const raw = await
|
|
4716
|
+
const raw = await readFile15(absPath, "utf8");
|
|
4069
4717
|
const split = splitFrontmatter(raw);
|
|
4070
4718
|
if (!split.ok) {
|
|
4071
4719
|
unresolved.push(relPath);
|
|
@@ -4073,7 +4721,7 @@ ${lines.join("\n")}`;
|
|
|
4073
4721
|
}
|
|
4074
4722
|
const body = split.data.body;
|
|
4075
4723
|
const rawFm = split.data.rawFrontmatter;
|
|
4076
|
-
const stripped = body.replace(
|
|
4724
|
+
const stripped = body.replace(FENCE_RE, "");
|
|
4077
4725
|
const wikilinkMatches = [...stripped.matchAll(WIKILINK_RE)];
|
|
4078
4726
|
if (wikilinkMatches.length === 0) {
|
|
4079
4727
|
unresolved.push(relPath);
|
|
@@ -4406,14 +5054,14 @@ async function runSyncLintDelta(input) {
|
|
|
4406
5054
|
}
|
|
4407
5055
|
|
|
4408
5056
|
// src/commands/config.ts
|
|
4409
|
-
import { readFile as
|
|
4410
|
-
import { existsSync as
|
|
4411
|
-
import { join as
|
|
5057
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
5058
|
+
import { existsSync as existsSync8 } from "fs";
|
|
5059
|
+
import { join as join19 } from "path";
|
|
4412
5060
|
function validateKey(key) {
|
|
4413
5061
|
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
4414
5062
|
}
|
|
4415
5063
|
function configPath(home) {
|
|
4416
|
-
return
|
|
5064
|
+
return join19(home, ".skillwiki", ".env");
|
|
4417
5065
|
}
|
|
4418
5066
|
async function runConfigGet(input) {
|
|
4419
5067
|
if (!validateKey(input.key)) {
|
|
@@ -4431,7 +5079,7 @@ async function runConfigSet(input) {
|
|
|
4431
5079
|
try {
|
|
4432
5080
|
let originalContent;
|
|
4433
5081
|
try {
|
|
4434
|
-
originalContent = await
|
|
5082
|
+
originalContent = await readFile16(filePath, "utf8");
|
|
4435
5083
|
} catch {
|
|
4436
5084
|
}
|
|
4437
5085
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -4463,15 +5111,15 @@ async function runConfigList(input) {
|
|
|
4463
5111
|
}
|
|
4464
5112
|
async function runConfigPath(input) {
|
|
4465
5113
|
const filePath = configPath(input.home);
|
|
4466
|
-
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists:
|
|
5114
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync8(filePath), humanHint: filePath }) };
|
|
4467
5115
|
}
|
|
4468
5116
|
|
|
4469
5117
|
// src/commands/fleet.ts
|
|
4470
|
-
import { readFile as
|
|
5118
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
4471
5119
|
import { hostname as nodeHostname, userInfo } from "os";
|
|
4472
|
-
import { join as
|
|
5120
|
+
import { join as join20 } from "path";
|
|
4473
5121
|
import yaml3 from "js-yaml";
|
|
4474
|
-
var FLEET_REL_PATH =
|
|
5122
|
+
var FLEET_REL_PATH = join20("projects", "llm-wiki", "architecture", "fleet.yaml");
|
|
4475
5123
|
async function runFleetValidate(input) {
|
|
4476
5124
|
const loaded = await loadFleetManifest(input.file);
|
|
4477
5125
|
if (!loaded.ok) {
|
|
@@ -4502,7 +5150,7 @@ async function runFleetContext(input) {
|
|
|
4502
5150
|
const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
|
|
4503
5151
|
const user = input.user ?? safeEnvValue(env.USER) ?? safeUserName();
|
|
4504
5152
|
const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
|
|
4505
|
-
const file = input.file ?? (vault ?
|
|
5153
|
+
const file = input.file ?? (vault ? join20(vault, FLEET_REL_PATH) : void 0);
|
|
4506
5154
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4507
5155
|
const loaded = file ? await loadFleetManifest(file) : { ok: false, error: "FILE_NOT_FOUND" };
|
|
4508
5156
|
if (!loaded.ok) {
|
|
@@ -4622,7 +5270,7 @@ function fleetContextEnv(input) {
|
|
|
4622
5270
|
const home = input.home ?? env.HOME ?? "";
|
|
4623
5271
|
const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
|
|
4624
5272
|
const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
|
|
4625
|
-
const file = input.file ?? (vault ?
|
|
5273
|
+
const file = input.file ?? (vault ? join20(vault, FLEET_REL_PATH) : void 0);
|
|
4626
5274
|
return { env, home, osHostname, vault, file };
|
|
4627
5275
|
}
|
|
4628
5276
|
async function loadFleetManifestAndHost(input) {
|
|
@@ -4682,7 +5330,7 @@ function satelliteGateFromFleetLoad(load) {
|
|
|
4682
5330
|
async function loadFleetManifest(file) {
|
|
4683
5331
|
let text;
|
|
4684
5332
|
try {
|
|
4685
|
-
text = await
|
|
5333
|
+
text = await readFile17(file, "utf8");
|
|
4686
5334
|
} catch {
|
|
4687
5335
|
return { ok: false, error: "FILE_NOT_FOUND" };
|
|
4688
5336
|
}
|
|
@@ -4756,7 +5404,7 @@ async function resolveFleetHostId(input) {
|
|
|
4756
5404
|
}
|
|
4757
5405
|
trace.push({ source: "AGENT_HOST_ID", status: "unset" });
|
|
4758
5406
|
if (input.home) {
|
|
4759
|
-
const dotenv = await parseDotenvFile(
|
|
5407
|
+
const dotenv = await parseDotenvFile(join20(input.home, ".skillwiki", ".env"));
|
|
4760
5408
|
if (dotenv.SKILLWIKI_HOST_ID) {
|
|
4761
5409
|
trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "matched", value: dotenv.SKILLWIKI_HOST_ID });
|
|
4762
5410
|
return { hostId: dotenv.SKILLWIKI_HOST_ID, source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", trace };
|
|
@@ -4929,20 +5577,20 @@ function safeUserName() {
|
|
|
4929
5577
|
}
|
|
4930
5578
|
|
|
4931
5579
|
// src/commands/doctor.ts
|
|
4932
|
-
import { existsSync as
|
|
4933
|
-
import { join as
|
|
5580
|
+
import { existsSync as existsSync14, lstatSync as lstatSync2, readlinkSync, readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync11 } from "fs";
|
|
5581
|
+
import { join as join26, resolve as resolve6 } from "path";
|
|
4934
5582
|
import { execSync as execSync2 } from "child_process";
|
|
4935
5583
|
import { platform as platform2 } from "os";
|
|
4936
5584
|
|
|
4937
5585
|
// src/utils/plugin-registry.ts
|
|
4938
|
-
import { existsSync as
|
|
4939
|
-
import { join as
|
|
4940
|
-
var REGISTRY_PATH =
|
|
4941
|
-
var CODEX_CONFIG_PATH =
|
|
5586
|
+
import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync6 } from "fs";
|
|
5587
|
+
import { join as join21 } from "path";
|
|
5588
|
+
var REGISTRY_PATH = join21(".claude", "plugins", "installed_plugins.json");
|
|
5589
|
+
var CODEX_CONFIG_PATH = join21(".codex", "config.toml");
|
|
4942
5590
|
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
4943
5591
|
function readInstalledPlugins(home) {
|
|
4944
5592
|
try {
|
|
4945
|
-
const raw =
|
|
5593
|
+
const raw = readFileSync6(join21(home, REGISTRY_PATH), "utf8");
|
|
4946
5594
|
return JSON.parse(raw);
|
|
4947
5595
|
} catch {
|
|
4948
5596
|
return null;
|
|
@@ -4978,8 +5626,8 @@ function findPluginInstallations(home, key = PLUGIN_KEY) {
|
|
|
4978
5626
|
function findCodexPlugin(home, key, pluginName, marketplace) {
|
|
4979
5627
|
const config = readCodexPluginConfig(home, key, marketplace);
|
|
4980
5628
|
if (!config?.enabled) return null;
|
|
4981
|
-
const cacheRoot =
|
|
4982
|
-
if (!
|
|
5629
|
+
const cacheRoot = join21(home, ".codex", "plugins", "cache", marketplace, pluginName);
|
|
5630
|
+
if (!existsSync9(cacheRoot)) return null;
|
|
4983
5631
|
let versions;
|
|
4984
5632
|
try {
|
|
4985
5633
|
versions = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
@@ -4994,7 +5642,7 @@ function findCodexPlugin(home, key, pluginName, marketplace) {
|
|
|
4994
5642
|
key,
|
|
4995
5643
|
pluginName,
|
|
4996
5644
|
marketplace,
|
|
4997
|
-
installPath:
|
|
5645
|
+
installPath: join21(cacheRoot, version),
|
|
4998
5646
|
version,
|
|
4999
5647
|
sourceType: config.sourceType,
|
|
5000
5648
|
source: config.source
|
|
@@ -5011,7 +5659,7 @@ function parsePluginKey(key) {
|
|
|
5011
5659
|
function readCodexPluginConfig(home, key, marketplace) {
|
|
5012
5660
|
let raw;
|
|
5013
5661
|
try {
|
|
5014
|
-
raw =
|
|
5662
|
+
raw = readFileSync6(join21(home, CODEX_CONFIG_PATH), "utf8");
|
|
5015
5663
|
} catch {
|
|
5016
5664
|
return null;
|
|
5017
5665
|
}
|
|
@@ -5053,8 +5701,8 @@ function parseTomlScalar(rawValue) {
|
|
|
5053
5701
|
}
|
|
5054
5702
|
|
|
5055
5703
|
// src/utils/conflict-markers.ts
|
|
5056
|
-
import { existsSync as
|
|
5057
|
-
import { join as
|
|
5704
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
|
|
5705
|
+
import { join as join22 } from "path";
|
|
5058
5706
|
function scanConflictMarkerBlocksInText(relPath, text) {
|
|
5059
5707
|
const findings = [];
|
|
5060
5708
|
const lines = text.split(/\r?\n/);
|
|
@@ -5105,21 +5753,21 @@ function walkMarkdownFiles2(root, dir, rel, out) {
|
|
|
5105
5753
|
for (const entry of entries) {
|
|
5106
5754
|
if (entry.isDirectory()) {
|
|
5107
5755
|
if (PRUNE_DIRS.has(entry.name)) continue;
|
|
5108
|
-
walkMarkdownFiles2(root,
|
|
5756
|
+
walkMarkdownFiles2(root, join22(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
|
|
5109
5757
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
5110
5758
|
out.push(rel ? `${rel}/${entry.name}` : entry.name);
|
|
5111
5759
|
}
|
|
5112
5760
|
}
|
|
5113
5761
|
}
|
|
5114
5762
|
function scanVaultConflictMarkers(vaultRoot) {
|
|
5115
|
-
if (!
|
|
5763
|
+
if (!existsSync10(vaultRoot)) return [];
|
|
5116
5764
|
const relPaths = [];
|
|
5117
5765
|
walkMarkdownFiles2(vaultRoot, vaultRoot, "", relPaths);
|
|
5118
5766
|
const all = [];
|
|
5119
5767
|
for (const rel of relPaths) {
|
|
5120
5768
|
let text;
|
|
5121
5769
|
try {
|
|
5122
|
-
text =
|
|
5770
|
+
text = readFileSync7(join22(vaultRoot, rel), "utf8");
|
|
5123
5771
|
} catch {
|
|
5124
5772
|
continue;
|
|
5125
5773
|
}
|
|
@@ -5129,8 +5777,8 @@ function scanVaultConflictMarkers(vaultRoot) {
|
|
|
5129
5777
|
}
|
|
5130
5778
|
|
|
5131
5779
|
// src/utils/remote-health.ts
|
|
5132
|
-
import { existsSync as
|
|
5133
|
-
import { join as
|
|
5780
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
5781
|
+
import { join as join23 } from "path";
|
|
5134
5782
|
import { execFileSync } from "child_process";
|
|
5135
5783
|
var REMOTE_PROBE_TIMEOUT_MS = 3e3;
|
|
5136
5784
|
var defaultExec = (file, args, cwd) => execFileSync(file, args, {
|
|
@@ -5141,7 +5789,7 @@ var defaultExec = (file, args, cwd) => execFileSync(file, args, {
|
|
|
5141
5789
|
}).trim();
|
|
5142
5790
|
function readWikiS3RemoteConfigured(home) {
|
|
5143
5791
|
try {
|
|
5144
|
-
const content =
|
|
5792
|
+
const content = readFileSync8(join23(home, ".skillwiki", ".env"), "utf8");
|
|
5145
5793
|
for (const line of content.split(/\r?\n/)) {
|
|
5146
5794
|
const trimmed = line.trim();
|
|
5147
5795
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -5166,7 +5814,7 @@ function resolveWikiS3Remote(input) {
|
|
|
5166
5814
|
return readWikiS3RemoteConfigured(input.home);
|
|
5167
5815
|
}
|
|
5168
5816
|
function probeGithubReachability(vaultPath, exec = defaultExec) {
|
|
5169
|
-
if (!
|
|
5817
|
+
if (!existsSync11(join23(vaultPath, ".git"))) return "unknown";
|
|
5170
5818
|
try {
|
|
5171
5819
|
exec("git", ["remote", "get-url", "origin"], vaultPath);
|
|
5172
5820
|
} catch {
|
|
@@ -5231,11 +5879,11 @@ function probeRemoteHealth(input) {
|
|
|
5231
5879
|
}
|
|
5232
5880
|
|
|
5233
5881
|
// src/utils/satellite-run-health.ts
|
|
5234
|
-
import { existsSync as
|
|
5235
|
-
import { join as
|
|
5882
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
5883
|
+
import { join as join24 } from "path";
|
|
5236
5884
|
var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
|
|
5237
5885
|
function satelliteLatestRunPath(vault) {
|
|
5238
|
-
return
|
|
5886
|
+
return join24(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
|
|
5239
5887
|
}
|
|
5240
5888
|
function isFailedRunStatus(status) {
|
|
5241
5889
|
return status === "fail" || status === "failure";
|
|
@@ -5257,9 +5905,9 @@ function readSatelliteLatestRunFromText(text) {
|
|
|
5257
5905
|
}
|
|
5258
5906
|
function readSatelliteLatestRun(vault) {
|
|
5259
5907
|
const latestPath = satelliteLatestRunPath(vault);
|
|
5260
|
-
if (!
|
|
5908
|
+
if (!existsSync12(latestPath)) return null;
|
|
5261
5909
|
try {
|
|
5262
|
-
return parseLatestRunFile(
|
|
5910
|
+
return parseLatestRunFile(readFileSync9(latestPath, "utf8"));
|
|
5263
5911
|
} catch {
|
|
5264
5912
|
return null;
|
|
5265
5913
|
}
|
|
@@ -5288,8 +5936,8 @@ function evaluateSatelliteRunHealth(vault, now) {
|
|
|
5288
5936
|
// src/utils/s3-mount-health.ts
|
|
5289
5937
|
import { execSync } from "child_process";
|
|
5290
5938
|
import { platform } from "os";
|
|
5291
|
-
import { readFileSync as
|
|
5292
|
-
import { join as
|
|
5939
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5, readFileSync as readFile18 } from "fs";
|
|
5940
|
+
import { join as join25 } from "path";
|
|
5293
5941
|
var OS = platform();
|
|
5294
5942
|
function findRcloneMountPid() {
|
|
5295
5943
|
try {
|
|
@@ -5373,7 +6021,7 @@ function extractRcloneFs(args) {
|
|
|
5373
6021
|
function getRcloneArgs(pid) {
|
|
5374
6022
|
try {
|
|
5375
6023
|
if (OS === "linux") {
|
|
5376
|
-
const raw =
|
|
6024
|
+
const raw = readFileSync10(`/proc/${pid}/cmdline`);
|
|
5377
6025
|
return new TextDecoder().decode(raw).split("\0").filter(Boolean);
|
|
5378
6026
|
} else {
|
|
5379
6027
|
const out = execSync(`ps -o args= -p ${pid}`, {
|
|
@@ -5416,7 +6064,7 @@ function queryRcloneRC(rcAddr, fs) {
|
|
|
5416
6064
|
function detectFuseMount(vaultPath) {
|
|
5417
6065
|
try {
|
|
5418
6066
|
if (OS === "linux") {
|
|
5419
|
-
const mounts =
|
|
6067
|
+
const mounts = readFileSync10("/proc/mounts", "utf8");
|
|
5420
6068
|
let best = null;
|
|
5421
6069
|
for (const line of mounts.split("\n")) {
|
|
5422
6070
|
const parts = line.split(" ");
|
|
@@ -5447,35 +6095,35 @@ function detectFuseMount(vaultPath) {
|
|
|
5447
6095
|
return null;
|
|
5448
6096
|
}
|
|
5449
6097
|
function writeTest(dir) {
|
|
5450
|
-
const testFile =
|
|
6098
|
+
const testFile = join25(dir, `.doctor-write-test-${process.pid}.tmp`);
|
|
5451
6099
|
const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
|
|
5452
6100
|
const start = Date.now();
|
|
5453
6101
|
try {
|
|
5454
|
-
|
|
6102
|
+
writeFileSync5(testFile, payload, "utf8");
|
|
5455
6103
|
} catch (e) {
|
|
5456
6104
|
return { success: false, writeMs: Date.now() - start, readMs: 0, size: 0, error: `write failed: ${e.message}` };
|
|
5457
6105
|
}
|
|
5458
6106
|
const writeMs = Date.now() - start;
|
|
5459
6107
|
const readStart = Date.now();
|
|
5460
6108
|
try {
|
|
5461
|
-
const back =
|
|
6109
|
+
const back = readFile18(testFile, "utf8");
|
|
5462
6110
|
const readMs = Date.now() - readStart;
|
|
5463
6111
|
if (back !== payload) {
|
|
5464
6112
|
try {
|
|
5465
|
-
|
|
6113
|
+
unlinkSync5(testFile);
|
|
5466
6114
|
} catch {
|
|
5467
6115
|
}
|
|
5468
6116
|
return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
|
|
5469
6117
|
}
|
|
5470
6118
|
} catch (e) {
|
|
5471
6119
|
try {
|
|
5472
|
-
|
|
6120
|
+
unlinkSync5(testFile);
|
|
5473
6121
|
} catch {
|
|
5474
6122
|
}
|
|
5475
6123
|
return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
|
|
5476
6124
|
}
|
|
5477
6125
|
try {
|
|
5478
|
-
|
|
6126
|
+
unlinkSync5(testFile);
|
|
5479
6127
|
} catch {
|
|
5480
6128
|
}
|
|
5481
6129
|
return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
|
|
@@ -5532,14 +6180,14 @@ function checkNodeVersion() {
|
|
|
5532
6180
|
function detectCliChannels(argv, home) {
|
|
5533
6181
|
const channels = [];
|
|
5534
6182
|
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
5535
|
-
const devPath =
|
|
6183
|
+
const devPath = resolve6(argv[1]);
|
|
5536
6184
|
channels.push({ name: "dev", path: devPath, isDevLink: true });
|
|
5537
6185
|
}
|
|
5538
6186
|
try {
|
|
5539
6187
|
const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
5540
6188
|
if (whichOut) {
|
|
5541
6189
|
const isDev = isDevSymlink(whichOut);
|
|
5542
|
-
if (!channels.some((c) => c.path ===
|
|
6190
|
+
if (!channels.some((c) => c.path === resolve6(whichOut))) {
|
|
5543
6191
|
channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
|
|
5544
6192
|
}
|
|
5545
6193
|
}
|
|
@@ -5547,22 +6195,22 @@ function detectCliChannels(argv, home) {
|
|
|
5547
6195
|
}
|
|
5548
6196
|
const plugin = findPlugin(home);
|
|
5549
6197
|
if (plugin) {
|
|
5550
|
-
const pluginBin =
|
|
5551
|
-
if (
|
|
6198
|
+
const pluginBin = join26(plugin.installPath, "bin", "skillwiki");
|
|
6199
|
+
if (existsSync14(pluginBin)) {
|
|
5552
6200
|
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
5553
6201
|
}
|
|
5554
6202
|
}
|
|
5555
|
-
const installBin =
|
|
5556
|
-
if (
|
|
6203
|
+
const installBin = join26(home, ".claude", "skills", "bin", "skillwiki");
|
|
6204
|
+
if (existsSync14(installBin)) {
|
|
5557
6205
|
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
5558
6206
|
}
|
|
5559
6207
|
return channels;
|
|
5560
6208
|
}
|
|
5561
6209
|
function isDevSymlink(binPath) {
|
|
5562
6210
|
try {
|
|
5563
|
-
const st =
|
|
6211
|
+
const st = lstatSync2(binPath);
|
|
5564
6212
|
if (st.isSymbolicLink()) {
|
|
5565
|
-
const target =
|
|
6213
|
+
const target = resolve6(binPath, "..", readlinkSync(binPath));
|
|
5566
6214
|
return target.includes("packages/cli") || target.includes("packages\\cli");
|
|
5567
6215
|
}
|
|
5568
6216
|
} catch {
|
|
@@ -5614,7 +6262,7 @@ function isDevSourceRun(argv) {
|
|
|
5614
6262
|
}
|
|
5615
6263
|
async function checkConfigFile(home) {
|
|
5616
6264
|
const cfgPath = configPath(home);
|
|
5617
|
-
if (!
|
|
6265
|
+
if (!existsSync14(cfgPath)) {
|
|
5618
6266
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
5619
6267
|
}
|
|
5620
6268
|
try {
|
|
@@ -5629,7 +6277,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
5629
6277
|
if (resolvedPath === void 0) {
|
|
5630
6278
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5631
6279
|
}
|
|
5632
|
-
if (
|
|
6280
|
+
if (existsSync14(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
|
|
5633
6281
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
5634
6282
|
}
|
|
5635
6283
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -5638,13 +6286,13 @@ function checkVaultStructure(resolvedPath) {
|
|
|
5638
6286
|
if (resolvedPath === void 0) {
|
|
5639
6287
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5640
6288
|
}
|
|
5641
|
-
if (!
|
|
6289
|
+
if (!existsSync14(resolvedPath)) {
|
|
5642
6290
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
5643
6291
|
}
|
|
5644
6292
|
const missing = [];
|
|
5645
|
-
if (!
|
|
6293
|
+
if (!existsSync14(join26(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
5646
6294
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
5647
|
-
if (!
|
|
6295
|
+
if (!existsSync14(join26(resolvedPath, dir))) missing.push(dir + "/");
|
|
5648
6296
|
}
|
|
5649
6297
|
if (missing.length === 0) {
|
|
5650
6298
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
@@ -5652,8 +6300,8 @@ function checkVaultStructure(resolvedPath) {
|
|
|
5652
6300
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
5653
6301
|
}
|
|
5654
6302
|
function checkSkillsInstalled(home, cwd) {
|
|
5655
|
-
const srcDir = cwd ?
|
|
5656
|
-
if (srcDir &&
|
|
6303
|
+
const srcDir = cwd ? join26(cwd, "packages", "skills") : void 0;
|
|
6304
|
+
if (srcDir && existsSync14(srcDir)) {
|
|
5657
6305
|
const found = findInstalledSkillMd(srcDir);
|
|
5658
6306
|
if (found.length > 0) {
|
|
5659
6307
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
|
|
@@ -5666,8 +6314,8 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
5666
6314
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
5667
6315
|
}
|
|
5668
6316
|
}
|
|
5669
|
-
const skillsDir =
|
|
5670
|
-
if (
|
|
6317
|
+
const skillsDir = join26(home, ".claude", "skills");
|
|
6318
|
+
if (existsSync14(skillsDir)) {
|
|
5671
6319
|
const found = findInstalledSkillMd(skillsDir);
|
|
5672
6320
|
if (found.length > 0) {
|
|
5673
6321
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
@@ -5677,10 +6325,10 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
5677
6325
|
}
|
|
5678
6326
|
function checkDuplicateSkills(home) {
|
|
5679
6327
|
const plugin = findPlugin(home);
|
|
5680
|
-
const skillsDir =
|
|
6328
|
+
const skillsDir = join26(home, ".claude", "skills");
|
|
5681
6329
|
const agentSkillDirs = [
|
|
5682
|
-
{ label: "~/.codex/skills/", path:
|
|
5683
|
-
{ label: "~/.agents/skills/", path:
|
|
6330
|
+
{ label: "~/.codex/skills/", path: join26(home, ".codex", "skills") },
|
|
6331
|
+
{ label: "~/.agents/skills/", path: join26(home, ".agents", "skills") }
|
|
5684
6332
|
];
|
|
5685
6333
|
if (!plugin) {
|
|
5686
6334
|
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
@@ -5783,8 +6431,8 @@ async function checkProfiles(home) {
|
|
|
5783
6431
|
}
|
|
5784
6432
|
async function checkProjectLocalOverride(cwd) {
|
|
5785
6433
|
const dir = cwd ?? process.cwd();
|
|
5786
|
-
const envPath =
|
|
5787
|
-
if (
|
|
6434
|
+
const envPath = join26(dir, ".skillwiki", ".env");
|
|
6435
|
+
if (existsSync14(envPath)) {
|
|
5788
6436
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
5789
6437
|
}
|
|
5790
6438
|
return check("pass", "project_local", "Project-local config", "None");
|
|
@@ -5793,7 +6441,7 @@ function checkVaultGitRemote(resolvedPath) {
|
|
|
5793
6441
|
if (resolvedPath === void 0) {
|
|
5794
6442
|
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5795
6443
|
}
|
|
5796
|
-
if (!
|
|
6444
|
+
if (!existsSync14(join26(resolvedPath, ".git"))) {
|
|
5797
6445
|
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
5798
6446
|
}
|
|
5799
6447
|
try {
|
|
@@ -5816,9 +6464,9 @@ function checkObsidianTemplates(resolvedPath) {
|
|
|
5816
6464
|
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5817
6465
|
}
|
|
5818
6466
|
const missing = [];
|
|
5819
|
-
if (!
|
|
5820
|
-
if (!
|
|
5821
|
-
if (!
|
|
6467
|
+
if (!existsSync14(join26(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
6468
|
+
if (!existsSync14(join26(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
6469
|
+
if (!existsSync14(join26(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
5822
6470
|
if (missing.length === 0) {
|
|
5823
6471
|
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
5824
6472
|
}
|
|
@@ -5828,8 +6476,8 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
5828
6476
|
if (resolvedPath === void 0) {
|
|
5829
6477
|
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5830
6478
|
}
|
|
5831
|
-
const rawDir =
|
|
5832
|
-
if (!
|
|
6479
|
+
const rawDir = join26(resolvedPath, "raw");
|
|
6480
|
+
if (!existsSync14(rawDir)) {
|
|
5833
6481
|
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
5834
6482
|
}
|
|
5835
6483
|
const found = [];
|
|
@@ -5844,7 +6492,7 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
5844
6492
|
if (entry.name === ".DS_Store") {
|
|
5845
6493
|
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
5846
6494
|
} else if (entry.isDirectory()) {
|
|
5847
|
-
walk2(
|
|
6495
|
+
walk2(join26(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
5848
6496
|
}
|
|
5849
6497
|
}
|
|
5850
6498
|
})(rawDir, "");
|
|
@@ -5875,7 +6523,7 @@ function checkSyncLastPush(resolvedPath) {
|
|
|
5875
6523
|
if (resolvedPath === void 0) {
|
|
5876
6524
|
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
5877
6525
|
}
|
|
5878
|
-
if (!
|
|
6526
|
+
if (!existsSync14(join26(resolvedPath, ".git"))) {
|
|
5879
6527
|
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
5880
6528
|
}
|
|
5881
6529
|
let timestamp;
|
|
@@ -5923,7 +6571,7 @@ function checkVaultGitDirty(resolvedPath) {
|
|
|
5923
6571
|
if (resolvedPath === void 0) {
|
|
5924
6572
|
return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
|
|
5925
6573
|
}
|
|
5926
|
-
if (!
|
|
6574
|
+
if (!existsSync14(join26(resolvedPath, ".git"))) {
|
|
5927
6575
|
return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
|
|
5928
6576
|
}
|
|
5929
6577
|
try {
|
|
@@ -5991,7 +6639,7 @@ function remoteMainHash(resolvedPath) {
|
|
|
5991
6639
|
}
|
|
5992
6640
|
function checkStaleRemoteMain(resolvedPath) {
|
|
5993
6641
|
if (resolvedPath === void 0) return void 0;
|
|
5994
|
-
if (!
|
|
6642
|
+
if (!existsSync14(join26(resolvedPath, ".git"))) return void 0;
|
|
5995
6643
|
const localOrigin = gitRefHash(resolvedPath, "origin/main");
|
|
5996
6644
|
if (!localOrigin) return void 0;
|
|
5997
6645
|
const remoteMain = remoteMainHash(resolvedPath);
|
|
@@ -6007,7 +6655,7 @@ function checkVaultLocalGit(resolvedPath) {
|
|
|
6007
6655
|
if (resolvedPath === void 0) {
|
|
6008
6656
|
return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
6009
6657
|
}
|
|
6010
|
-
if (!
|
|
6658
|
+
if (!existsSync14(join26(resolvedPath, ".git"))) {
|
|
6011
6659
|
return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
|
|
6012
6660
|
}
|
|
6013
6661
|
try {
|
|
@@ -6026,7 +6674,7 @@ function checkVaultGithubRemote(resolvedPath, exec) {
|
|
|
6026
6674
|
if (resolvedPath === void 0) {
|
|
6027
6675
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
|
|
6028
6676
|
}
|
|
6029
|
-
if (!
|
|
6677
|
+
if (!existsSync14(join26(resolvedPath, ".git"))) {
|
|
6030
6678
|
return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
|
|
6031
6679
|
}
|
|
6032
6680
|
const state = probeGithubReachability(resolvedPath, exec);
|
|
@@ -6070,7 +6718,7 @@ function checkVaultPromotionLag(resolvedPath) {
|
|
|
6070
6718
|
if (resolvedPath === void 0) {
|
|
6071
6719
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
|
|
6072
6720
|
}
|
|
6073
|
-
if (!
|
|
6721
|
+
if (!existsSync14(join26(resolvedPath, ".git"))) {
|
|
6074
6722
|
return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
|
|
6075
6723
|
}
|
|
6076
6724
|
try {
|
|
@@ -6097,7 +6745,7 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
|
|
|
6097
6745
|
if (resolvedPath === void 0) {
|
|
6098
6746
|
return check("pass", id, label, "No vault path \u2014 check skipped");
|
|
6099
6747
|
}
|
|
6100
|
-
if (!
|
|
6748
|
+
if (!existsSync14(join26(resolvedPath, ".git"))) {
|
|
6101
6749
|
return check("pass", id, label, "No git repo \u2014 check skipped");
|
|
6102
6750
|
}
|
|
6103
6751
|
if (!hasOriginMain(resolvedPath)) {
|
|
@@ -6125,7 +6773,7 @@ function checkSatelliteLastRun(vaultPath, satelliteExpected) {
|
|
|
6125
6773
|
return check("pass", "satellite_job_last_run", "Satellite job last run", "No vault path \u2014 check skipped");
|
|
6126
6774
|
}
|
|
6127
6775
|
const latestPath = satelliteLatestRunPath(vaultPath);
|
|
6128
|
-
if (!
|
|
6776
|
+
if (!existsSync14(latestPath)) {
|
|
6129
6777
|
return check("pass", "satellite_job_last_run", "Satellite job last run", "No latest-run.json \u2014 satellite has not run yet");
|
|
6130
6778
|
}
|
|
6131
6779
|
try {
|
|
@@ -6213,11 +6861,11 @@ async function checkFleetIdentity(input) {
|
|
|
6213
6861
|
}
|
|
6214
6862
|
function pullLogPaths(home) {
|
|
6215
6863
|
const paths = platform2() === "darwin" ? [
|
|
6216
|
-
|
|
6217
|
-
|
|
6864
|
+
join26(home, "Library", "Logs", "wiki-pull.log"),
|
|
6865
|
+
join26(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
|
|
6218
6866
|
] : [
|
|
6219
|
-
|
|
6220
|
-
|
|
6867
|
+
join26(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
|
|
6868
|
+
join26(home, "Library", "Logs", "wiki-pull.log")
|
|
6221
6869
|
];
|
|
6222
6870
|
return [...new Set(paths)];
|
|
6223
6871
|
}
|
|
@@ -6229,12 +6877,12 @@ function isRecentLogLine(line, nowMs) {
|
|
|
6229
6877
|
return nowMs - ts <= 24 * 60 * 60 * 1e3;
|
|
6230
6878
|
}
|
|
6231
6879
|
function checkVaultGitPullFailures(home) {
|
|
6232
|
-
const path = pullLogPaths(home).find((p) =>
|
|
6880
|
+
const path = pullLogPaths(home).find((p) => existsSync14(p));
|
|
6233
6881
|
if (!path) {
|
|
6234
6882
|
return check("pass", "vault_git_pull_failures", "Vault pull failures", "No wiki-pull.log found \u2014 check skipped");
|
|
6235
6883
|
}
|
|
6236
6884
|
try {
|
|
6237
|
-
const lines =
|
|
6885
|
+
const lines = readFileSync11(path, "utf8").split(/\r?\n/).filter(Boolean);
|
|
6238
6886
|
const now = Date.now();
|
|
6239
6887
|
const failures = lines.filter(
|
|
6240
6888
|
(line) => isRecentLogLine(line, now) && /(pre-push pull failed|FAIL .*pull|FAIL .*rebase|cannot pull with rebase|unstaged changes)/i.test(line)
|
|
@@ -6257,8 +6905,8 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
6257
6905
|
return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
|
|
6258
6906
|
}
|
|
6259
6907
|
const mountPoint = fuse.mountPoint;
|
|
6260
|
-
const conceptsDir =
|
|
6261
|
-
if (!
|
|
6908
|
+
const conceptsDir = join26(resolvedPath, "concepts");
|
|
6909
|
+
if (!existsSync14(conceptsDir)) {
|
|
6262
6910
|
return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
|
|
6263
6911
|
}
|
|
6264
6912
|
const start = Date.now();
|
|
@@ -6440,8 +7088,8 @@ function checkWriteTest(resolvedPath) {
|
|
|
6440
7088
|
if (!fuse) {
|
|
6441
7089
|
return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
|
|
6442
7090
|
}
|
|
6443
|
-
const conceptsDir =
|
|
6444
|
-
if (!
|
|
7091
|
+
const conceptsDir = join26(resolvedPath, "concepts");
|
|
7092
|
+
if (!existsSync14(conceptsDir)) {
|
|
6445
7093
|
return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
|
|
6446
7094
|
}
|
|
6447
7095
|
const result = writeTest(conceptsDir);
|
|
@@ -6527,7 +7175,7 @@ function checkVfsCacheHealth(resolvedPath) {
|
|
|
6527
7175
|
}
|
|
6528
7176
|
function readVaultSyncConfig(home) {
|
|
6529
7177
|
try {
|
|
6530
|
-
const content =
|
|
7178
|
+
const content = readFileSync11(join26(home, ".skillwiki", ".env"), "utf8");
|
|
6531
7179
|
let installed = false;
|
|
6532
7180
|
let role;
|
|
6533
7181
|
let serviceScope;
|
|
@@ -6556,7 +7204,7 @@ function readVaultSyncConfig(home) {
|
|
|
6556
7204
|
}
|
|
6557
7205
|
function readKeyFromEnvFile(path, keys) {
|
|
6558
7206
|
try {
|
|
6559
|
-
const content =
|
|
7207
|
+
const content = readFileSync11(path, "utf8");
|
|
6560
7208
|
for (const line of content.split(/\r?\n/)) {
|
|
6561
7209
|
const trimmed = line.trim();
|
|
6562
7210
|
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
|
|
@@ -6578,7 +7226,7 @@ function resolveSnapshotGitWorktree(config) {
|
|
|
6578
7226
|
if (fromProfile) return fromProfile;
|
|
6579
7227
|
}
|
|
6580
7228
|
const defaultPath = "/root/wiki-git";
|
|
6581
|
-
return
|
|
7229
|
+
return existsSync14(defaultPath) ? defaultPath : void 0;
|
|
6582
7230
|
}
|
|
6583
7231
|
function vaultSyncChecks(input) {
|
|
6584
7232
|
const os = input.os ?? platform2();
|
|
@@ -6595,16 +7243,16 @@ function vaultSyncChecks(input) {
|
|
|
6595
7243
|
];
|
|
6596
7244
|
}
|
|
6597
7245
|
const isMac = os === "darwin";
|
|
6598
|
-
const logDir = input.logDir ?? (isMac ?
|
|
6599
|
-
const shareDir = input.shareDir ?? (isMac ?
|
|
6600
|
-
const filterPath = input.filterPath ??
|
|
6601
|
-
const packagedSnapshotPath =
|
|
7246
|
+
const logDir = input.logDir ?? (isMac ? join26(home, "Library", "Logs") : join26(home, ".local", "state", "vault-sync", "log"));
|
|
7247
|
+
const shareDir = input.shareDir ?? (isMac ? join26(home, "Library", "Application Support", "vault-sync", "bin") : join26(home, ".local", "share", "vault-sync", "bin"));
|
|
7248
|
+
const filterPath = input.filterPath ?? join26(home, ".config", "rclone", "wiki-push-filters.txt");
|
|
7249
|
+
const packagedSnapshotPath = join26(shareDir, "wiki-snapshot.sh");
|
|
6602
7250
|
const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
|
|
6603
|
-
const snapshotPath = input.snapshotScriptPath ?? (
|
|
7251
|
+
const snapshotPath = input.snapshotScriptPath ?? (existsSync14(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
|
|
6604
7252
|
function snapshotLastStatusCheck() {
|
|
6605
|
-
const snapshotLog =
|
|
7253
|
+
const snapshotLog = join26(logDir, "wiki-snapshot.log");
|
|
6606
7254
|
try {
|
|
6607
|
-
const logContent =
|
|
7255
|
+
const logContent = readFileSync11(snapshotLog, "utf8");
|
|
6608
7256
|
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
6609
7257
|
if (lines.length === 0) {
|
|
6610
7258
|
return check(
|
|
@@ -6649,14 +7297,14 @@ function vaultSyncChecks(input) {
|
|
|
6649
7297
|
}
|
|
6650
7298
|
}
|
|
6651
7299
|
if (input.vaultSyncRole === "snapshotter") {
|
|
6652
|
-
const c12 =
|
|
7300
|
+
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
7301
|
const serviceScope = input.vaultSyncServiceScope ?? "user";
|
|
6654
|
-
const userTimerPath =
|
|
7302
|
+
const userTimerPath = join26(home, ".config", "systemd", "user", "wiki-snapshot.timer");
|
|
6655
7303
|
const systemTimerPath = "/etc/systemd/system/wiki-snapshot.timer";
|
|
6656
7304
|
let c22;
|
|
6657
|
-
if (serviceScope === "user" &&
|
|
7305
|
+
if (serviceScope === "user" && existsSync14(userTimerPath)) {
|
|
6658
7306
|
c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${userTimerPath}`);
|
|
6659
|
-
} else if (serviceScope === "system" &&
|
|
7307
|
+
} else if (serviceScope === "system" && existsSync14(systemTimerPath)) {
|
|
6660
7308
|
c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${systemTimerPath}`);
|
|
6661
7309
|
} else if (os !== "linux") {
|
|
6662
7310
|
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 +7336,7 @@ function vaultSyncChecks(input) {
|
|
|
6688
7336
|
);
|
|
6689
7337
|
let c52;
|
|
6690
7338
|
try {
|
|
6691
|
-
if (!
|
|
7339
|
+
if (!existsSync14(snapshotPath)) {
|
|
6692
7340
|
c52 = check(
|
|
6693
7341
|
"error",
|
|
6694
7342
|
"vault_sync_snapshot_guard",
|
|
@@ -6696,7 +7344,7 @@ function vaultSyncChecks(input) {
|
|
|
6696
7344
|
`Snapshot script not found at ${snapshotPath}`
|
|
6697
7345
|
);
|
|
6698
7346
|
} else {
|
|
6699
|
-
const content =
|
|
7347
|
+
const content = readFileSync11(snapshotPath, "utf8");
|
|
6700
7348
|
if (!content.includes("--max-delete")) {
|
|
6701
7349
|
c52 = check(
|
|
6702
7350
|
"error",
|
|
@@ -6723,8 +7371,8 @@ function vaultSyncChecks(input) {
|
|
|
6723
7371
|
}
|
|
6724
7372
|
return [c12, c22, c32, cFetch2, c42, c52];
|
|
6725
7373
|
}
|
|
6726
|
-
const pushScriptPath =
|
|
6727
|
-
const c1 =
|
|
7374
|
+
const pushScriptPath = join26(shareDir, "wiki-push.sh");
|
|
7375
|
+
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
7376
|
let c2;
|
|
6729
7377
|
try {
|
|
6730
7378
|
if (isMac) {
|
|
@@ -6775,10 +7423,10 @@ function vaultSyncChecks(input) {
|
|
|
6775
7423
|
"Scheduler check failed \u2014 run vault-sync-install"
|
|
6776
7424
|
);
|
|
6777
7425
|
}
|
|
6778
|
-
const logFile =
|
|
7426
|
+
const logFile = join26(logDir, "wiki-push.log");
|
|
6779
7427
|
let c3;
|
|
6780
7428
|
try {
|
|
6781
|
-
const logContent =
|
|
7429
|
+
const logContent = readFileSync11(logFile, "utf8");
|
|
6782
7430
|
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
6783
7431
|
if (lines.length === 0) {
|
|
6784
7432
|
c3 = check(
|
|
@@ -6834,7 +7482,7 @@ function vaultSyncChecks(input) {
|
|
|
6834
7482
|
}
|
|
6835
7483
|
}
|
|
6836
7484
|
} catch {
|
|
6837
|
-
c3 =
|
|
7485
|
+
c3 = existsSync14(logDir) ? check(
|
|
6838
7486
|
"warn",
|
|
6839
7487
|
"vault_sync_last_push_age",
|
|
6840
7488
|
"Vault sync last push recency",
|
|
@@ -6846,10 +7494,10 @@ function vaultSyncChecks(input) {
|
|
|
6846
7494
|
`Log directory not found at ${logDir}`
|
|
6847
7495
|
);
|
|
6848
7496
|
}
|
|
6849
|
-
const fetchLogFile =
|
|
7497
|
+
const fetchLogFile = join26(logDir, "wiki-fetch.log");
|
|
6850
7498
|
let cFetch;
|
|
6851
7499
|
try {
|
|
6852
|
-
const logContent =
|
|
7500
|
+
const logContent = readFileSync11(fetchLogFile, "utf8");
|
|
6853
7501
|
const lines = logContent.trim().split("\n").filter(Boolean);
|
|
6854
7502
|
if (lines.length === 0) {
|
|
6855
7503
|
cFetch = check(
|
|
@@ -6893,7 +7541,7 @@ function vaultSyncChecks(input) {
|
|
|
6893
7541
|
}
|
|
6894
7542
|
let c4;
|
|
6895
7543
|
try {
|
|
6896
|
-
if (!
|
|
7544
|
+
if (!existsSync14(filterPath)) {
|
|
6897
7545
|
c4 = check(
|
|
6898
7546
|
"error",
|
|
6899
7547
|
"vault_sync_filter_present",
|
|
@@ -6901,7 +7549,7 @@ function vaultSyncChecks(input) {
|
|
|
6901
7549
|
`Filter file not found at ${filterPath}`
|
|
6902
7550
|
);
|
|
6903
7551
|
} else {
|
|
6904
|
-
const content =
|
|
7552
|
+
const content = readFileSync11(filterPath, "utf8");
|
|
6905
7553
|
const requiredExcludes = [
|
|
6906
7554
|
"remotely-save/data.json",
|
|
6907
7555
|
".skillwiki/sync.lock",
|
|
@@ -6944,7 +7592,7 @@ function vaultSyncChecks(input) {
|
|
|
6944
7592
|
);
|
|
6945
7593
|
} else {
|
|
6946
7594
|
try {
|
|
6947
|
-
if (!
|
|
7595
|
+
if (!existsSync14(snapshotPath)) {
|
|
6948
7596
|
c5 = check(
|
|
6949
7597
|
"error",
|
|
6950
7598
|
"vault_sync_snapshot_guard",
|
|
@@ -6952,7 +7600,7 @@ function vaultSyncChecks(input) {
|
|
|
6952
7600
|
`Snapshot script not found at ${snapshotPath}`
|
|
6953
7601
|
);
|
|
6954
7602
|
} else {
|
|
6955
|
-
const content =
|
|
7603
|
+
const content = readFileSync11(snapshotPath, "utf8");
|
|
6956
7604
|
if (!content.includes("--max-delete")) {
|
|
6957
7605
|
c5 = check(
|
|
6958
7606
|
"error",
|
|
@@ -6990,15 +7638,15 @@ function findSkillMd(dir) {
|
|
|
6990
7638
|
}
|
|
6991
7639
|
for (const entry of entries) {
|
|
6992
7640
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
6993
|
-
results.push(
|
|
7641
|
+
results.push(join26(dir, entry.name));
|
|
6994
7642
|
} else if (entry.isDirectory()) {
|
|
6995
|
-
results.push(...findSkillMd(
|
|
7643
|
+
results.push(...findSkillMd(join26(dir, entry.name)));
|
|
6996
7644
|
}
|
|
6997
7645
|
}
|
|
6998
7646
|
return results;
|
|
6999
7647
|
}
|
|
7000
7648
|
function findInstalledSkillMd(dir) {
|
|
7001
|
-
const directSkills = findSkillNames(dir).map((name) =>
|
|
7649
|
+
const directSkills = findSkillNames(dir).map((name) => join26(dir, name, "SKILL.md"));
|
|
7002
7650
|
return directSkills.length > 0 ? directSkills : findSkillMd(dir);
|
|
7003
7651
|
}
|
|
7004
7652
|
function findSkillNames(dir) {
|
|
@@ -7010,7 +7658,7 @@ function findSkillNames(dir) {
|
|
|
7010
7658
|
return results;
|
|
7011
7659
|
}
|
|
7012
7660
|
for (const entry of entries) {
|
|
7013
|
-
if (entry.isDirectory() &&
|
|
7661
|
+
if (entry.isDirectory() && existsSync14(join26(dir, entry.name, "SKILL.md"))) {
|
|
7014
7662
|
results.push(entry.name);
|
|
7015
7663
|
}
|
|
7016
7664
|
}
|
|
@@ -7054,7 +7702,7 @@ async function vaultMetrics(resolvedPath) {
|
|
|
7054
7702
|
}
|
|
7055
7703
|
let logLines = 0;
|
|
7056
7704
|
try {
|
|
7057
|
-
logLines =
|
|
7705
|
+
logLines = readFileSync11(join26(resolvedPath, "log.md"), "utf8").split("\n").length;
|
|
7058
7706
|
} catch {
|
|
7059
7707
|
}
|
|
7060
7708
|
return [
|
|
@@ -7162,7 +7810,7 @@ async function runDoctor(input) {
|
|
|
7162
7810
|
}
|
|
7163
7811
|
|
|
7164
7812
|
// src/utils/package-info.ts
|
|
7165
|
-
import { readFileSync as
|
|
7813
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
7166
7814
|
function packageJsonCandidateUrls(baseUrl = import.meta.url) {
|
|
7167
7815
|
return [
|
|
7168
7816
|
new URL("../package.json", baseUrl),
|
|
@@ -7172,7 +7820,7 @@ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
|
|
|
7172
7820
|
function readCliPackageJson(baseUrl = import.meta.url) {
|
|
7173
7821
|
for (const url of packageJsonCandidateUrls(baseUrl)) {
|
|
7174
7822
|
try {
|
|
7175
|
-
const pkg = JSON.parse(
|
|
7823
|
+
const pkg = JSON.parse(readFileSync12(url, "utf8"));
|
|
7176
7824
|
if (typeof pkg.version === "string") {
|
|
7177
7825
|
return { ...pkg, version: pkg.version };
|
|
7178
7826
|
}
|
|
@@ -7183,8 +7831,8 @@ function readCliPackageJson(baseUrl = import.meta.url) {
|
|
|
7183
7831
|
}
|
|
7184
7832
|
|
|
7185
7833
|
// src/commands/project-index.ts
|
|
7186
|
-
import { readdir as readdir4, readFile as
|
|
7187
|
-
import { join as
|
|
7834
|
+
import { readdir as readdir4, readFile as readFile19, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
|
|
7835
|
+
import { join as join27, dirname as dirname8, basename as basename2 } from "path";
|
|
7188
7836
|
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
7189
7837
|
var PROJECT_LOCAL_DIRS = ["requirements", "work", "architecture", "history"];
|
|
7190
7838
|
async function scanMarkdownTree(rootAbs, rootRel) {
|
|
@@ -7196,7 +7844,7 @@ async function scanMarkdownTree(rootAbs, rootRel) {
|
|
|
7196
7844
|
return found;
|
|
7197
7845
|
}
|
|
7198
7846
|
for (const entry of entries) {
|
|
7199
|
-
const abs =
|
|
7847
|
+
const abs = join27(rootAbs, entry.name);
|
|
7200
7848
|
const rel = `${rootRel}/${entry.name}`;
|
|
7201
7849
|
if (entry.isDirectory()) {
|
|
7202
7850
|
found.push(...await scanMarkdownTree(abs, rel));
|
|
@@ -7226,7 +7874,7 @@ function projectLocalType(slug, page, data) {
|
|
|
7226
7874
|
}
|
|
7227
7875
|
async function runProjectIndex(input) {
|
|
7228
7876
|
const slug = input.slug;
|
|
7229
|
-
const projectDir =
|
|
7877
|
+
const projectDir = join27(input.vault, "projects", slug);
|
|
7230
7878
|
try {
|
|
7231
7879
|
await readdir4(projectDir);
|
|
7232
7880
|
} catch {
|
|
@@ -7237,15 +7885,15 @@ async function runProjectIndex(input) {
|
|
|
7237
7885
|
}
|
|
7238
7886
|
const wikilinkPattern = `[[${slug}]]`;
|
|
7239
7887
|
const entries = [];
|
|
7240
|
-
const compoundDir =
|
|
7888
|
+
const compoundDir = join27(input.vault, "projects", slug, "compound");
|
|
7241
7889
|
try {
|
|
7242
7890
|
const compoundFiles = await readdir4(compoundDir, { withFileTypes: true });
|
|
7243
7891
|
for (const entry of compoundFiles) {
|
|
7244
7892
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
7245
|
-
const filePath =
|
|
7893
|
+
const filePath = join27(compoundDir, entry.name);
|
|
7246
7894
|
let text;
|
|
7247
7895
|
try {
|
|
7248
|
-
text = await
|
|
7896
|
+
text = await readFile19(filePath, "utf8");
|
|
7249
7897
|
} catch {
|
|
7250
7898
|
continue;
|
|
7251
7899
|
}
|
|
@@ -7262,16 +7910,16 @@ async function runProjectIndex(input) {
|
|
|
7262
7910
|
for (const dir of LAYER2_DIRS) {
|
|
7263
7911
|
let files;
|
|
7264
7912
|
try {
|
|
7265
|
-
files = await readdir4(
|
|
7913
|
+
files = await readdir4(join27(input.vault, dir), { withFileTypes: true });
|
|
7266
7914
|
} catch {
|
|
7267
7915
|
continue;
|
|
7268
7916
|
}
|
|
7269
7917
|
for (const entry of files) {
|
|
7270
7918
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
7271
|
-
const filePath =
|
|
7919
|
+
const filePath = join27(input.vault, dir, entry.name);
|
|
7272
7920
|
let text;
|
|
7273
7921
|
try {
|
|
7274
|
-
text = await
|
|
7922
|
+
text = await readFile19(filePath, "utf8");
|
|
7275
7923
|
} catch {
|
|
7276
7924
|
continue;
|
|
7277
7925
|
}
|
|
@@ -7287,14 +7935,14 @@ async function runProjectIndex(input) {
|
|
|
7287
7935
|
}
|
|
7288
7936
|
}
|
|
7289
7937
|
for (const dir of PROJECT_LOCAL_DIRS) {
|
|
7290
|
-
const rootAbs =
|
|
7938
|
+
const rootAbs = join27(projectDir, dir);
|
|
7291
7939
|
const rootRel = `projects/${slug}/${dir}`;
|
|
7292
7940
|
const pages = await scanMarkdownTree(rootAbs, rootRel);
|
|
7293
7941
|
for (const page of pages) {
|
|
7294
|
-
const filePath =
|
|
7942
|
+
const filePath = join27(input.vault, page);
|
|
7295
7943
|
let text;
|
|
7296
7944
|
try {
|
|
7297
|
-
text = await
|
|
7945
|
+
text = await readFile19(filePath, "utf8");
|
|
7298
7946
|
} catch {
|
|
7299
7947
|
continue;
|
|
7300
7948
|
}
|
|
@@ -7313,11 +7961,11 @@ async function runProjectIndex(input) {
|
|
|
7313
7961
|
const tb = typeOrder[b.type] ?? 99;
|
|
7314
7962
|
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
7315
7963
|
});
|
|
7316
|
-
const indexPath =
|
|
7964
|
+
const indexPath = join27(projectDir, "knowledge.md");
|
|
7317
7965
|
let existing = false;
|
|
7318
7966
|
let stale = false;
|
|
7319
7967
|
try {
|
|
7320
|
-
const existingText = await
|
|
7968
|
+
const existingText = await readFile19(indexPath, "utf8");
|
|
7321
7969
|
existing = true;
|
|
7322
7970
|
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
7323
7971
|
const existingPages = new Set(existingEntries.map((l) => {
|
|
@@ -7357,8 +8005,8 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
|
7357
8005
|
}
|
|
7358
8006
|
if (input.apply) {
|
|
7359
8007
|
try {
|
|
7360
|
-
await mkdir5(
|
|
7361
|
-
await
|
|
8008
|
+
await mkdir5(dirname8(indexPath), { recursive: true });
|
|
8009
|
+
await writeFile5(indexPath, body, "utf8");
|
|
7362
8010
|
} catch (e) {
|
|
7363
8011
|
return {
|
|
7364
8012
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -7386,10 +8034,10 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
|
|
|
7386
8034
|
}
|
|
7387
8035
|
|
|
7388
8036
|
// src/commands/observe.ts
|
|
7389
|
-
import { mkdir as mkdir6, writeFile as
|
|
7390
|
-
import { existsSync as
|
|
7391
|
-
import { join as
|
|
7392
|
-
import { createHash as
|
|
8037
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
|
|
8038
|
+
import { existsSync as existsSync15, statSync as statSync3 } from "fs";
|
|
8039
|
+
import { join as join28 } from "path";
|
|
8040
|
+
import { createHash as createHash7 } from "crypto";
|
|
7393
8041
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
7394
8042
|
function slugify(text) {
|
|
7395
8043
|
const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -7411,13 +8059,13 @@ async function runObserve(input) {
|
|
|
7411
8059
|
result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
|
|
7412
8060
|
};
|
|
7413
8061
|
}
|
|
7414
|
-
if (!
|
|
8062
|
+
if (!existsSync15(input.vault) || !statSync3(input.vault).isDirectory()) {
|
|
7415
8063
|
return {
|
|
7416
8064
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
7417
8065
|
result: err("VAULT_PATH_INVALID", { path: input.vault })
|
|
7418
8066
|
};
|
|
7419
8067
|
}
|
|
7420
|
-
const transcriptsDir =
|
|
8068
|
+
const transcriptsDir = join28(input.vault, "raw", "transcripts");
|
|
7421
8069
|
try {
|
|
7422
8070
|
await mkdir6(transcriptsDir, { recursive: true });
|
|
7423
8071
|
} catch {
|
|
@@ -7429,11 +8077,11 @@ async function runObserve(input) {
|
|
|
7429
8077
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7430
8078
|
const slug = slugify(input.text);
|
|
7431
8079
|
const fileName = `${today}-observation-${slug}.md`;
|
|
7432
|
-
const filePath =
|
|
8080
|
+
const filePath = join28(transcriptsDir, fileName);
|
|
7433
8081
|
const body = `
|
|
7434
8082
|
${input.text.trim()}
|
|
7435
8083
|
`;
|
|
7436
|
-
const sha256 =
|
|
8084
|
+
const sha256 = createHash7("sha256").update(Buffer.from(body, "utf8")).digest("hex");
|
|
7437
8085
|
const frontmatterLines = [
|
|
7438
8086
|
"---",
|
|
7439
8087
|
"source_url:",
|
|
@@ -7447,7 +8095,7 @@ ${input.text.trim()}
|
|
|
7447
8095
|
frontmatterLines.push("---");
|
|
7448
8096
|
const content = frontmatterLines.join("\n") + body;
|
|
7449
8097
|
try {
|
|
7450
|
-
await
|
|
8098
|
+
await writeFile6(filePath, content, "utf8");
|
|
7451
8099
|
} catch (e) {
|
|
7452
8100
|
return {
|
|
7453
8101
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -7469,9 +8117,9 @@ ${input.text.trim()}
|
|
|
7469
8117
|
}
|
|
7470
8118
|
|
|
7471
8119
|
// src/commands/memory.ts
|
|
7472
|
-
import { createHash as
|
|
7473
|
-
import { mkdir as mkdir7, readFile as
|
|
7474
|
-
import { basename as basename3, extname, join as
|
|
8120
|
+
import { createHash as createHash8 } from "crypto";
|
|
8121
|
+
import { mkdir as mkdir7, readFile as readFile20, readdir as readdir5, stat as stat6, writeFile as writeFile7 } from "fs/promises";
|
|
8122
|
+
import { basename as basename3, extname, join as join29, relative as relative5, sep as sep5 } from "path";
|
|
7475
8123
|
async function runMemoryTopics(input) {
|
|
7476
8124
|
const scan = await scanVault(input.vault);
|
|
7477
8125
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -7535,9 +8183,9 @@ async function runMemoryIndex(input) {
|
|
|
7535
8183
|
}
|
|
7536
8184
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
7537
8185
|
const relCachePath = memoryCacheRelPath(input.project);
|
|
7538
|
-
const absCachePath =
|
|
7539
|
-
await mkdir7(
|
|
7540
|
-
await
|
|
8186
|
+
const absCachePath = join29(input.vault, relCachePath);
|
|
8187
|
+
await mkdir7(join29(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
|
|
8188
|
+
await writeFile7(absCachePath, `${JSON.stringify({
|
|
7541
8189
|
generated_at: generatedAt,
|
|
7542
8190
|
project: input.project,
|
|
7543
8191
|
topics: state.topics,
|
|
@@ -7728,7 +8376,7 @@ async function buildMemoryIndexState(pages, project) {
|
|
|
7728
8376
|
}
|
|
7729
8377
|
async function checkMemoryIndex(vault, project, current) {
|
|
7730
8378
|
const relCachePath = memoryCacheRelPath(project);
|
|
7731
|
-
const cacheText = await readIfExists2(
|
|
8379
|
+
const cacheText = await readIfExists2(join29(vault, relCachePath));
|
|
7732
8380
|
if (!cacheText) {
|
|
7733
8381
|
return {
|
|
7734
8382
|
ok: true,
|
|
@@ -7845,7 +8493,7 @@ async function readMemoryPage(page, project, warnings) {
|
|
|
7845
8493
|
title,
|
|
7846
8494
|
summary: summarize(body),
|
|
7847
8495
|
updated,
|
|
7848
|
-
hash:
|
|
8496
|
+
hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
|
|
7849
8497
|
topics,
|
|
7850
8498
|
project,
|
|
7851
8499
|
...stringField(fm.data.memory_kind) ? { memory_kind: stringField(fm.data.memory_kind) } : {},
|
|
@@ -8121,13 +8769,13 @@ function renderMemoryIndexStatusHint(status) {
|
|
|
8121
8769
|
}
|
|
8122
8770
|
async function readIfExists2(path) {
|
|
8123
8771
|
try {
|
|
8124
|
-
return await
|
|
8772
|
+
return await readFile20(path, "utf8");
|
|
8125
8773
|
} catch {
|
|
8126
8774
|
return "";
|
|
8127
8775
|
}
|
|
8128
8776
|
}
|
|
8129
8777
|
async function collectImportFiles(source) {
|
|
8130
|
-
const st = await
|
|
8778
|
+
const st = await stat6(source);
|
|
8131
8779
|
if (st.isFile()) return isImportCandidate(source) ? [source] : [];
|
|
8132
8780
|
const files = [];
|
|
8133
8781
|
await walkImportFiles(source, files);
|
|
@@ -8137,7 +8785,7 @@ async function walkImportFiles(dir, out) {
|
|
|
8137
8785
|
const entries = await readdir5(dir, { withFileTypes: true });
|
|
8138
8786
|
for (const entry of entries) {
|
|
8139
8787
|
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
8140
|
-
const path =
|
|
8788
|
+
const path = join29(dir, entry.name);
|
|
8141
8789
|
if (entry.isDirectory()) {
|
|
8142
8790
|
await walkImportFiles(path, out);
|
|
8143
8791
|
} else if (entry.isFile() && isImportCandidate(path)) {
|
|
@@ -8150,9 +8798,9 @@ function isImportCandidate(path) {
|
|
|
8150
8798
|
return ext === ".md" || ext === ".txt";
|
|
8151
8799
|
}
|
|
8152
8800
|
async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
8153
|
-
const st = await
|
|
8801
|
+
const st = await stat6(file);
|
|
8154
8802
|
const sourceKind = classifyImportSource(file);
|
|
8155
|
-
const hash =
|
|
8803
|
+
const hash = createHash8("sha256").update(await readFile20(file)).digest("hex");
|
|
8156
8804
|
const baseEntry = {
|
|
8157
8805
|
source_path: file,
|
|
8158
8806
|
source_kind: sourceKind,
|
|
@@ -8175,7 +8823,7 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
|
8175
8823
|
reason: "policy_source_not_imported"
|
|
8176
8824
|
};
|
|
8177
8825
|
}
|
|
8178
|
-
const text = await
|
|
8826
|
+
const text = await readFile20(file, "utf8");
|
|
8179
8827
|
const extracted = extractImportText(text, sourceKind);
|
|
8180
8828
|
if (!extracted) {
|
|
8181
8829
|
return {
|
|
@@ -8187,7 +8835,7 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
|
|
|
8187
8835
|
const redacted = redactSensitiveContent(extracted, { file });
|
|
8188
8836
|
const privacy = redacted.findings.length > 0 ? "sensitive" : "local";
|
|
8189
8837
|
const sourceSlug = slugify2(basename3(file, extname(file)));
|
|
8190
|
-
const relSource =
|
|
8838
|
+
const relSource = relative5(sourceRoot, file).split(sep5).join("/");
|
|
8191
8839
|
const entry = {
|
|
8192
8840
|
...baseEntry,
|
|
8193
8841
|
status: "ready",
|
|
@@ -8204,9 +8852,9 @@ async function writeImportCapture(vault, entry, today) {
|
|
|
8204
8852
|
const content = hiddenString(entry, "__content");
|
|
8205
8853
|
const project = hiddenString(entry, "__project");
|
|
8206
8854
|
const relPath = await availableImportPath(vault, entry.proposed_path);
|
|
8207
|
-
const absPath =
|
|
8208
|
-
await mkdir7(
|
|
8209
|
-
await
|
|
8855
|
+
const absPath = join29(vault, relPath);
|
|
8856
|
+
await mkdir7(join29(vault, "raw", "transcripts"), { recursive: true });
|
|
8857
|
+
await writeFile7(absPath, renderImportCapture(entry, content, project, today), "utf8");
|
|
8210
8858
|
const validation = await runValidate({ file: absPath });
|
|
8211
8859
|
return {
|
|
8212
8860
|
relPath,
|
|
@@ -8221,7 +8869,7 @@ async function availableImportPath(vault, proposed) {
|
|
|
8221
8869
|
const stem = proposed.slice(0, -ext.length);
|
|
8222
8870
|
let candidate = proposed;
|
|
8223
8871
|
let i = 2;
|
|
8224
|
-
while (await readIfExists2(
|
|
8872
|
+
while (await readIfExists2(join29(vault, candidate))) {
|
|
8225
8873
|
candidate = `${stem}-${i}${ext}`;
|
|
8226
8874
|
i++;
|
|
8227
8875
|
}
|
|
@@ -8259,7 +8907,7 @@ function hiddenString(entry, key) {
|
|
|
8259
8907
|
return entry[key] ?? "";
|
|
8260
8908
|
}
|
|
8261
8909
|
function classifyImportSource(file) {
|
|
8262
|
-
const rel = file.split(
|
|
8910
|
+
const rel = file.split(sep5).join("/");
|
|
8263
8911
|
const name = basename3(file);
|
|
8264
8912
|
if (rel.includes("/.codex/memories/")) return "codex-memory";
|
|
8265
8913
|
if (rel.includes("/.codex/rules/")) return "codex-rule";
|
|
@@ -8393,10 +9041,10 @@ function memoryCacheRelPath(project) {
|
|
|
8393
9041
|
}
|
|
8394
9042
|
async function readMemoryCache(vault, project) {
|
|
8395
9043
|
if (project) {
|
|
8396
|
-
const projectCache = await readIfExists2(
|
|
9044
|
+
const projectCache = await readIfExists2(join29(vault, memoryCacheRelPath(project)));
|
|
8397
9045
|
if (projectCache) return projectCache;
|
|
8398
9046
|
}
|
|
8399
|
-
return readIfExists2(
|
|
9047
|
+
return readIfExists2(join29(vault, ".skillwiki", "memory-topics.json"));
|
|
8400
9048
|
}
|
|
8401
9049
|
function dedupePages(pages) {
|
|
8402
9050
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -8482,8 +9130,8 @@ function slugify2(value) {
|
|
|
8482
9130
|
}
|
|
8483
9131
|
|
|
8484
9132
|
// src/commands/query.ts
|
|
8485
|
-
import { readFile as
|
|
8486
|
-
import { join as
|
|
9133
|
+
import { readFile as readFile21, stat as stat7 } from "fs/promises";
|
|
9134
|
+
import { join as join30 } from "path";
|
|
8487
9135
|
var W_KEYWORD = 2;
|
|
8488
9136
|
var W_SOURCE_OVERLAP = 4;
|
|
8489
9137
|
var W_WIKILINK = 3;
|
|
@@ -8604,10 +9252,10 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
8604
9252
|
return score;
|
|
8605
9253
|
}
|
|
8606
9254
|
async function loadOrBuildGraph(vault) {
|
|
8607
|
-
const graphPath =
|
|
9255
|
+
const graphPath = join30(vault, ".skillwiki", "graph.json");
|
|
8608
9256
|
let needsBuild = false;
|
|
8609
9257
|
try {
|
|
8610
|
-
const fileStat = await
|
|
9258
|
+
const fileStat = await stat7(graphPath);
|
|
8611
9259
|
const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
|
|
8612
9260
|
if (ageHours > 24) needsBuild = true;
|
|
8613
9261
|
} catch {
|
|
@@ -8618,7 +9266,7 @@ async function loadOrBuildGraph(vault) {
|
|
|
8618
9266
|
if (buildResult.exitCode !== 0) return null;
|
|
8619
9267
|
}
|
|
8620
9268
|
try {
|
|
8621
|
-
const raw = await
|
|
9269
|
+
const raw = await readFile21(graphPath, "utf8");
|
|
8622
9270
|
return JSON.parse(raw);
|
|
8623
9271
|
} catch {
|
|
8624
9272
|
return null;
|
|
@@ -8633,27 +9281,27 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
8633
9281
|
import { z as z2 } from "zod";
|
|
8634
9282
|
|
|
8635
9283
|
// src/mcp/vault-resolve.ts
|
|
8636
|
-
import { join as
|
|
9284
|
+
import { join as join31, resolve as resolve8 } from "path";
|
|
8637
9285
|
|
|
8638
9286
|
// src/mcp/allowlist.ts
|
|
8639
|
-
import { resolve as
|
|
8640
|
-
import { realpathSync } from "fs";
|
|
9287
|
+
import { resolve as resolve7, sep as sep6 } from "path";
|
|
9288
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
8641
9289
|
function parseVaultAllowlist(envValue) {
|
|
8642
9290
|
if (envValue === void 0 || envValue.trim() === "") return null;
|
|
8643
|
-
return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((p) =>
|
|
9291
|
+
return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((p) => resolve7(p));
|
|
8644
9292
|
}
|
|
8645
9293
|
function vaultAllowedByList(vaultPath, allowlist) {
|
|
8646
9294
|
if (!allowlist || allowlist.length === 0) return true;
|
|
8647
9295
|
let canonical = vaultPath;
|
|
8648
9296
|
try {
|
|
8649
|
-
canonical =
|
|
9297
|
+
canonical = realpathSync2(vaultPath);
|
|
8650
9298
|
} catch {
|
|
8651
|
-
canonical =
|
|
9299
|
+
canonical = resolve7(vaultPath);
|
|
8652
9300
|
}
|
|
8653
|
-
const resolved =
|
|
9301
|
+
const resolved = resolve7(canonical);
|
|
8654
9302
|
return allowlist.some((root) => {
|
|
8655
|
-
const r =
|
|
8656
|
-
return resolved === r || resolved.startsWith(r +
|
|
9303
|
+
const r = resolve7(root);
|
|
9304
|
+
return resolved === r || resolved.startsWith(r + sep6);
|
|
8657
9305
|
});
|
|
8658
9306
|
}
|
|
8659
9307
|
function getVaultAllowlistFromEnv() {
|
|
@@ -8666,7 +9314,7 @@ async function resolveMcpVault(input) {
|
|
|
8666
9314
|
let vaultPath;
|
|
8667
9315
|
let source = "resolved";
|
|
8668
9316
|
if (input.vault !== void 0 && input.vault.length > 0) {
|
|
8669
|
-
vaultPath =
|
|
9317
|
+
vaultPath = resolve8(input.vault);
|
|
8670
9318
|
source = "flag";
|
|
8671
9319
|
} else {
|
|
8672
9320
|
const r = await resolveRuntimePath({
|
|
@@ -8678,7 +9326,7 @@ async function resolveMcpVault(input) {
|
|
|
8678
9326
|
cwd: input.cwd ?? process.cwd()
|
|
8679
9327
|
});
|
|
8680
9328
|
if (!r.ok) return r;
|
|
8681
|
-
vaultPath =
|
|
9329
|
+
vaultPath = resolve8(r.data.path);
|
|
8682
9330
|
source = r.data.source;
|
|
8683
9331
|
}
|
|
8684
9332
|
const scan = await scanVault(vaultPath);
|
|
@@ -8695,7 +9343,7 @@ async function resolveMcpVault(input) {
|
|
|
8695
9343
|
return ok({ vault: vaultPath, source });
|
|
8696
9344
|
}
|
|
8697
9345
|
function defaultGraphOut(vault) {
|
|
8698
|
-
return
|
|
9346
|
+
return join31(vault, ".skillwiki", "graph.json");
|
|
8699
9347
|
}
|
|
8700
9348
|
|
|
8701
9349
|
// src/mcp/result-format.ts
|
|
@@ -8710,9 +9358,9 @@ function formatToolResult(payload) {
|
|
|
8710
9358
|
}
|
|
8711
9359
|
|
|
8712
9360
|
// src/mcp/audit-log.ts
|
|
8713
|
-
import { appendFileSync, mkdirSync as
|
|
9361
|
+
import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
8714
9362
|
import { homedir } from "os";
|
|
8715
|
-
import { join as
|
|
9363
|
+
import { join as join32 } from "path";
|
|
8716
9364
|
function auditEnabled() {
|
|
8717
9365
|
const v = process.env.SKILLWIKI_MCP_AUDIT;
|
|
8718
9366
|
if (v === "0" || v === "false") return false;
|
|
@@ -8724,7 +9372,7 @@ function auditSink() {
|
|
|
8724
9372
|
function auditFilePath() {
|
|
8725
9373
|
const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
|
|
8726
9374
|
if (custom && custom.length > 0) return custom;
|
|
8727
|
-
return
|
|
9375
|
+
return join32(homedir(), ".skillwiki", "mcp-audit.jsonl");
|
|
8728
9376
|
}
|
|
8729
9377
|
function auditMcpToolCall(entry) {
|
|
8730
9378
|
if (!auditEnabled()) return;
|
|
@@ -8734,7 +9382,7 @@ function auditMcpToolCall(entry) {
|
|
|
8734
9382
|
return;
|
|
8735
9383
|
}
|
|
8736
9384
|
const path = auditFilePath();
|
|
8737
|
-
|
|
9385
|
+
mkdirSync5(join32(path, ".."), { recursive: true });
|
|
8738
9386
|
appendFileSync(path, line, "utf8");
|
|
8739
9387
|
}
|
|
8740
9388
|
async function runMcpToolHandler(tool, input, fn) {
|
|
@@ -8954,8 +9602,8 @@ function registerMcpMutatingTools(server) {
|
|
|
8954
9602
|
}
|
|
8955
9603
|
|
|
8956
9604
|
// src/mcp/resources.ts
|
|
8957
|
-
import { readFile as
|
|
8958
|
-
import { join as
|
|
9605
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
9606
|
+
import { join as join34 } from "path";
|
|
8959
9607
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8960
9608
|
|
|
8961
9609
|
// src/mcp/lint-bucket.ts
|
|
@@ -9083,9 +9731,9 @@ async function fetchQueryPreview(input) {
|
|
|
9083
9731
|
}
|
|
9084
9732
|
|
|
9085
9733
|
// src/mcp/graph-html.ts
|
|
9086
|
-
import { readFile as
|
|
9087
|
-
import { join as
|
|
9088
|
-
import { existsSync as
|
|
9734
|
+
import { readFile as readFile22 } from "fs/promises";
|
|
9735
|
+
import { join as join33 } from "path";
|
|
9736
|
+
import { existsSync as existsSync16 } from "fs";
|
|
9089
9737
|
var TYPE_COLORS = {
|
|
9090
9738
|
entities: "#e74c3c",
|
|
9091
9739
|
concepts: "#27ae60",
|
|
@@ -9153,9 +9801,9 @@ ${nodeSvg}
|
|
|
9153
9801
|
return { html, node_count: nodes.length, edge_count: edges.length, truncated };
|
|
9154
9802
|
}
|
|
9155
9803
|
async function fetchGraphHtmlReport(input) {
|
|
9156
|
-
const graphPath = input.graphPath ??
|
|
9804
|
+
const graphPath = input.graphPath ?? join33(input.vault, ".skillwiki", "graph.json");
|
|
9157
9805
|
const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
|
|
9158
|
-
if (!
|
|
9806
|
+
if (!existsSync16(graphPath)) {
|
|
9159
9807
|
return {
|
|
9160
9808
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
9161
9809
|
result: err("GRAPH_MISSING", { path: graphPath, hint: "Run skillwiki.graph_build first." })
|
|
@@ -9163,7 +9811,7 @@ async function fetchGraphHtmlReport(input) {
|
|
|
9163
9811
|
}
|
|
9164
9812
|
let raw;
|
|
9165
9813
|
try {
|
|
9166
|
-
raw = await
|
|
9814
|
+
raw = await readFile22(graphPath, "utf8");
|
|
9167
9815
|
} catch (e) {
|
|
9168
9816
|
return {
|
|
9169
9817
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -9227,7 +9875,7 @@ async function fetchStaleSummary(input) {
|
|
|
9227
9875
|
|
|
9228
9876
|
// src/mcp/resources.ts
|
|
9229
9877
|
async function readVaultFile(vault, rel) {
|
|
9230
|
-
return
|
|
9878
|
+
return readFile23(join34(vault, rel), "utf8");
|
|
9231
9879
|
}
|
|
9232
9880
|
async function tailLines(text, lines) {
|
|
9233
9881
|
const parts = text.split(/\r?\n/);
|
|
@@ -9313,9 +9961,9 @@ function registerMcpResources(server) {
|
|
|
9313
9961
|
if (!v.ok) {
|
|
9314
9962
|
return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
|
|
9315
9963
|
}
|
|
9316
|
-
const path =
|
|
9964
|
+
const path = join34(v.data.vault, ".skillwiki", "graph.json");
|
|
9317
9965
|
try {
|
|
9318
|
-
const raw = await
|
|
9966
|
+
const raw = await readFile23(path, "utf8");
|
|
9319
9967
|
const graph = JSON.parse(raw);
|
|
9320
9968
|
const adjacency = graph.adjacency ?? {};
|
|
9321
9969
|
const nodes = Object.keys(adjacency);
|
|
@@ -9628,13 +10276,29 @@ export {
|
|
|
9628
10276
|
ExitCode,
|
|
9629
10277
|
ok,
|
|
9630
10278
|
err,
|
|
9631
|
-
|
|
10279
|
+
RawSourceSchema,
|
|
9632
10280
|
MetaSchema,
|
|
9633
|
-
detectSchema,
|
|
9634
10281
|
isBlockedHost,
|
|
9635
10282
|
splitFrontmatter,
|
|
9636
10283
|
extractFrontmatter,
|
|
9637
10284
|
scanSensitiveContent,
|
|
10285
|
+
redactSensitiveContent,
|
|
10286
|
+
readLastOp,
|
|
10287
|
+
appendLastOp,
|
|
10288
|
+
clearLastOp,
|
|
10289
|
+
atomicWriteText,
|
|
10290
|
+
runLogAppend,
|
|
10291
|
+
renderIndexUpsert,
|
|
10292
|
+
upsertIndexEntry,
|
|
10293
|
+
getSessionId,
|
|
10294
|
+
getCliSessionId,
|
|
10295
|
+
readLock,
|
|
10296
|
+
acquireLock,
|
|
10297
|
+
releaseLock,
|
|
10298
|
+
acquireOwnedSyncLock,
|
|
10299
|
+
releaseOwnedSyncLock,
|
|
10300
|
+
assertTargetInsideVault,
|
|
10301
|
+
prepareTypedPage,
|
|
9638
10302
|
runValidate,
|
|
9639
10303
|
scanVault,
|
|
9640
10304
|
readPage,
|
|
@@ -9650,9 +10314,8 @@ export {
|
|
|
9650
10314
|
runAudit,
|
|
9651
10315
|
findPlugin,
|
|
9652
10316
|
extractTaxonomy,
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
clearLastOp,
|
|
10317
|
+
taxonomyCommentForPage,
|
|
10318
|
+
reconcileTaxonomyDocument,
|
|
9656
10319
|
runLinks,
|
|
9657
10320
|
runTagAudit,
|
|
9658
10321
|
runIndexCheck,
|