skillwiki 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  // src/cli.ts
11
11
  import { readFileSync as readFileSync10 } from "fs";
12
- import { join as join38 } from "path";
12
+ import { join as join40 } from "path";
13
13
  import { Command as Command2 } from "commander";
14
14
 
15
15
  // ../shared/src/exit-codes.ts
@@ -60,7 +60,8 @@ var ExitCode = {
60
60
  SYNC_PULL_FAILED: 43,
61
61
  BACKUP_SYNC_FAILED: 44,
62
62
  BACKUP_RESTORE_CONFLICTS: 45,
63
- USAGE: 46
63
+ USAGE: 46,
64
+ BODY_TRUNCATION_GUARD: 47
64
65
  };
65
66
 
66
67
  // ../shared/src/json-output.ts
@@ -307,11 +308,11 @@ async function runHash(input) {
307
308
  }
308
309
  const split = splitFrontmatter(text);
309
310
  if (!split.ok) return { exitCode: ExitCode.MISSING_CLOSING_DELIMITER, result: split };
310
- const bodyBytes = Buffer.from(split.data.body, "utf8");
311
- const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
311
+ const bodyBytes2 = Buffer.from(split.data.body, "utf8");
312
+ const sha256 = createHash("sha256").update(bodyBytes2).digest("hex");
312
313
  return {
313
314
  exitCode: ExitCode.OK,
314
- result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
315
+ result: ok({ path: input.file, sha256, byte_count: bodyBytes2.byteLength, humanHint: sha256 })
315
316
  };
316
317
  }
317
318
 
@@ -1957,9 +1958,9 @@ async function runStale(input) {
1957
1958
  if (input.project && !project.includes(input.project)) continue;
1958
1959
  let inferred = false;
1959
1960
  if (input.forceScan && !kind) {
1960
- const basename = t.relPath.split("/").pop();
1961
- if (!LOOP_CYCLE_PATTERN.test(basename)) {
1962
- const m = basename.match(KIND_FROM_FILENAME);
1961
+ const basename2 = t.relPath.split("/").pop();
1962
+ if (!LOOP_CYCLE_PATTERN.test(basename2)) {
1963
+ const m = basename2.match(KIND_FROM_FILENAME);
1963
1964
  if (m) {
1964
1965
  kind = m[1];
1965
1966
  inferred = true;
@@ -2362,8 +2363,8 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
2362
2363
 
2363
2364
  // src/commands/lint.ts
2364
2365
  import { existsSync as existsSync3 } from "fs";
2365
- import { readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
2366
- import { join as join18 } from "path";
2366
+ import { readFile as readFile15 } from "fs/promises";
2367
+ import { join as join19 } from "path";
2367
2368
 
2368
2369
  // src/commands/topic-map-check.ts
2369
2370
  var DEFAULT_THRESHOLD = 200;
@@ -2490,6 +2491,81 @@ async function runDedup(input) {
2490
2491
  };
2491
2492
  }
2492
2493
 
2494
+ // src/utils/safe-write.ts
2495
+ import { open, readFile as readFile14, rename as rename4, unlink as unlink2, writeFile as writeFile8 } from "fs/promises";
2496
+ import { randomBytes } from "crypto";
2497
+ import { dirname as dirname7, basename, join as join18 } from "path";
2498
+ var DEFAULT_MIN_BODY_RATIO = 0.5;
2499
+ var DEFAULT_MIN_OLD_BODY_BYTES = 200;
2500
+ function bodyBytes(text) {
2501
+ const split = splitFrontmatter(text);
2502
+ if (!split.ok) return Buffer.byteLength(text, "utf8");
2503
+ return Buffer.byteLength(split.data.body, "utf8");
2504
+ }
2505
+ async function readIfExists(absPath) {
2506
+ try {
2507
+ return await readFile14(absPath, "utf8");
2508
+ } catch (e) {
2509
+ if (e.code === "ENOENT") return null;
2510
+ throw e;
2511
+ }
2512
+ }
2513
+ async function safeWritePage(absPath, newContent, opts = {}) {
2514
+ const minRatio = opts.minBodyRatio === void 0 ? DEFAULT_MIN_BODY_RATIO : opts.minBodyRatio;
2515
+ const minOldBytes = opts.minOldBodyBytes ?? DEFAULT_MIN_OLD_BODY_BYTES;
2516
+ let oldContent;
2517
+ try {
2518
+ oldContent = await readIfExists(absPath);
2519
+ } catch (e) {
2520
+ return err("WRITE_FAILED", { path: absPath, phase: "read-existing", message: String(e) });
2521
+ }
2522
+ const isNew = oldContent === null;
2523
+ const oldBodyBytes = isNew ? 0 : bodyBytes(oldContent);
2524
+ const newBodyBytes = bodyBytes(newContent);
2525
+ const bodyRatio = oldBodyBytes > 0 ? newBodyBytes / oldBodyBytes : null;
2526
+ let guardSkippedSmall = false;
2527
+ if (!isNew && minRatio !== null && bodyRatio !== null && bodyRatio < minRatio) {
2528
+ if (oldBodyBytes < minOldBytes) {
2529
+ guardSkippedSmall = true;
2530
+ } else {
2531
+ return err("BODY_TRUNCATION_GUARD", {
2532
+ path: absPath,
2533
+ oldBodyBytes,
2534
+ newBodyBytes,
2535
+ bodyRatio,
2536
+ minBodyRatio: minRatio,
2537
+ hint: "Refusing to write \u2014 new body lost too much content. Likely a parse-modify-serialize bug or a write race. Verify the page source before retrying."
2538
+ });
2539
+ }
2540
+ }
2541
+ if (!isNew && oldContent === newContent) {
2542
+ return ok({ isNew: false, oldBodyBytes, newBodyBytes, bodyRatio, guardSkippedSmall });
2543
+ }
2544
+ const dir = dirname7(absPath);
2545
+ const tmpName = `.${basename(absPath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
2546
+ const tmpPath = join18(dir, tmpName);
2547
+ try {
2548
+ const handle = await open(tmpPath, "w");
2549
+ try {
2550
+ await handle.writeFile(newContent, "utf8");
2551
+ try {
2552
+ await handle.sync();
2553
+ } catch {
2554
+ }
2555
+ } finally {
2556
+ await handle.close();
2557
+ }
2558
+ await rename4(tmpPath, absPath);
2559
+ return ok({ isNew, oldBodyBytes, newBodyBytes, bodyRatio, guardSkippedSmall });
2560
+ } catch (e) {
2561
+ try {
2562
+ await unlink2(tmpPath);
2563
+ } catch {
2564
+ }
2565
+ return err("WRITE_FAILED", { path: absPath, phase: "atomic-write", message: String(e) });
2566
+ }
2567
+ }
2568
+
2493
2569
  // src/commands/raw-body-dedup.ts
2494
2570
  import { createHash as createHash2 } from "crypto";
2495
2571
  async function runRawBodyDedup(vault) {
@@ -2813,7 +2889,7 @@ async function runLint(input) {
2813
2889
  let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
2814
2890
  rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
2815
2891
  if (!rawPath.startsWith("raw/") && !rawPath.startsWith("_archive/raw/")) continue;
2816
- if (!existsSync3(join18(input.vault, rawPath)) && !existsSync3(join18(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync3(join18(input.vault, "_archive", rawPath)) && !existsSync3(join18(input.vault, "_archive", rawPath + ".md"))) {
2892
+ if (!existsSync3(join19(input.vault, rawPath)) && !existsSync3(join19(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync3(join19(input.vault, "_archive", rawPath)) && !existsSync3(join19(input.vault, "_archive", rawPath + ".md"))) {
2817
2893
  brokenSourceFlags.push(`${page.relPath}: ${rawPath}`);
2818
2894
  }
2819
2895
  }
@@ -2904,11 +2980,11 @@ async function runLint(input) {
2904
2980
  const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
2905
2981
  if (!slugMatch) continue;
2906
2982
  const slug = slugMatch[1];
2907
- const knowledgePath = join18(input.vault, "projects", slug, "knowledge.md");
2983
+ const knowledgePath = join19(input.vault, "projects", slug, "knowledge.md");
2908
2984
  if (!existsSync3(knowledgePath)) continue;
2909
2985
  const pageRef = page.relPath.replace(/\.md$/, "");
2910
2986
  try {
2911
- const knowledgeContent = await readFile14(knowledgePath, "utf8");
2987
+ const knowledgeContent = await readFile15(knowledgePath, "utf8");
2912
2988
  if (!knowledgeContent.includes(`[[${pageRef}]]`)) {
2913
2989
  orphanedProjectPages.push(`${page.relPath}: not in projects/${slug}/knowledge.md`);
2914
2990
  }
@@ -2955,7 +3031,7 @@ async function runLint(input) {
2955
3031
  for (const relPath of legacyPages) {
2956
3032
  try {
2957
3033
  const absPath = `${input.vault}/${relPath}`;
2958
- const raw = await readFile14(absPath, "utf8");
3034
+ const raw = await readFile15(absPath, "utf8");
2959
3035
  const split = splitFrontmatter(raw);
2960
3036
  if (!split.ok) {
2961
3037
  unresolved.push(relPath);
@@ -3033,7 +3109,11 @@ async function runLint(input) {
3033
3109
  ${rawFm}
3034
3110
  ---
3035
3111
  ${newBody}`;
3036
- await writeFile8(absPath, newContent, "utf8");
3112
+ const w = await safeWritePage(absPath, newContent);
3113
+ if (!w.ok) {
3114
+ unresolved.push(relPath);
3115
+ continue;
3116
+ }
3037
3117
  fixed.push(relPath);
3038
3118
  } catch {
3039
3119
  unresolved.push(relPath);
@@ -3050,7 +3130,7 @@ ${newBody}`;
3050
3130
  for (const relPath of noOverview) {
3051
3131
  try {
3052
3132
  const absPath = `${input.vault}/${relPath}`;
3053
- const raw = await readFile14(absPath, "utf8");
3133
+ const raw = await readFile15(absPath, "utf8");
3054
3134
  const split = splitFrontmatter(raw);
3055
3135
  if (!split.ok) {
3056
3136
  unresolved.push(relPath);
@@ -3071,7 +3151,11 @@ ${rawFm}
3071
3151
  ${overviewSection}
3072
3152
 
3073
3153
  ${trimmedBody}`;
3074
- await writeFile8(absPath, newContent, "utf8");
3154
+ const w = await safeWritePage(absPath, newContent);
3155
+ if (!w.ok) {
3156
+ unresolved.push(relPath);
3157
+ continue;
3158
+ }
3075
3159
  fixed.push(relPath);
3076
3160
  } catch {
3077
3161
  unresolved.push(relPath);
@@ -3087,7 +3171,7 @@ ${trimmedBody}`;
3087
3171
  for (const relPath of missingTldrFlags) {
3088
3172
  try {
3089
3173
  const absPath = `${input.vault}/${relPath}`;
3090
- const raw = await readFile14(absPath, "utf8");
3174
+ const raw = await readFile15(absPath, "utf8");
3091
3175
  const split = splitFrontmatter(raw);
3092
3176
  if (!split.ok) {
3093
3177
  unresolved.push(relPath);
@@ -3115,7 +3199,11 @@ ${trimmedBody}`;
3115
3199
  const newContent = `---
3116
3200
  ${trimmedFm}---
3117
3201
  ${lines.join("\n")}`;
3118
- await writeFile8(absPath, newContent, "utf8");
3202
+ const w = await safeWritePage(absPath, newContent);
3203
+ if (!w.ok) {
3204
+ unresolved.push(relPath);
3205
+ continue;
3206
+ }
3119
3207
  fixed.push(relPath);
3120
3208
  } catch {
3121
3209
  unresolved.push(relPath);
@@ -3133,7 +3221,7 @@ ${lines.join("\n")}`;
3133
3221
  for (const relPath of wikilinkCitationFlags) {
3134
3222
  try {
3135
3223
  const absPath = `${input.vault}/${relPath}`;
3136
- const raw = await readFile14(absPath, "utf8");
3224
+ const raw = await readFile15(absPath, "utf8");
3137
3225
  const split = splitFrontmatter(raw);
3138
3226
  if (!split.ok) {
3139
3227
  unresolved.push(relPath);
@@ -3197,7 +3285,11 @@ ${lines.join("\n")}`;
3197
3285
  ${rawFm}
3198
3286
  ---
3199
3287
  ${newBody}`;
3200
- await writeFile8(absPath, newContent, "utf8");
3288
+ const w = await safeWritePage(absPath, newContent);
3289
+ if (!w.ok) {
3290
+ unresolved.push(relPath);
3291
+ continue;
3292
+ }
3201
3293
  wikilinkFixed.push(relPath);
3202
3294
  } catch {
3203
3295
  unresolved.push(relPath);
@@ -3286,14 +3378,14 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
3286
3378
  }
3287
3379
 
3288
3380
  // src/commands/config.ts
3289
- import { readFile as readFile15 } from "fs/promises";
3381
+ import { readFile as readFile16 } from "fs/promises";
3290
3382
  import { existsSync as existsSync4 } from "fs";
3291
- import { join as join19 } from "path";
3383
+ import { join as join20 } from "path";
3292
3384
  function validateKey(key) {
3293
3385
  return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
3294
3386
  }
3295
3387
  function configPath(home) {
3296
- return join19(home, ".skillwiki", ".env");
3388
+ return join20(home, ".skillwiki", ".env");
3297
3389
  }
3298
3390
  async function runConfigGet(input) {
3299
3391
  if (!validateKey(input.key)) {
@@ -3311,7 +3403,7 @@ async function runConfigSet(input) {
3311
3403
  try {
3312
3404
  let originalContent;
3313
3405
  try {
3314
- originalContent = await readFile15(filePath, "utf8");
3406
+ originalContent = await readFile16(filePath, "utf8");
3315
3407
  } catch {
3316
3408
  }
3317
3409
  const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
@@ -3347,14 +3439,13 @@ async function runConfigPath(input) {
3347
3439
  }
3348
3440
 
3349
3441
  // src/commands/doctor.ts
3350
- import { existsSync as existsSync6, lstatSync, readlinkSync, readdirSync, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
3351
- import { join as join22, resolve as resolve4 } from "path";
3352
- import { execSync } from "child_process";
3353
- import { platform } from "os";
3442
+ import { existsSync as existsSync7, lstatSync, readlinkSync, readdirSync, statSync as statSync2 } from "fs";
3443
+ import { join as join24, resolve as resolve4 } from "path";
3444
+ import { execSync as execSync2 } from "child_process";
3354
3445
 
3355
3446
  // src/utils/auto-update.ts
3356
3447
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
3357
- import { join as join20, dirname as dirname7 } from "path";
3448
+ import { join as join21, dirname as dirname8 } from "path";
3358
3449
  import { spawn } from "child_process";
3359
3450
 
3360
3451
  // src/utils/update-consts.ts
@@ -3365,7 +3456,7 @@ var CLI_DISABLE_FLAG = "--no-update-notifier";
3365
3456
 
3366
3457
  // src/utils/auto-update.ts
3367
3458
  function cachePath(home) {
3368
- return join20(home, ".skillwiki", CACHE_FILENAME);
3459
+ return join21(home, ".skillwiki", CACHE_FILENAME);
3369
3460
  }
3370
3461
  function readCacheRaw(home) {
3371
3462
  try {
@@ -3384,7 +3475,7 @@ function readCache(home) {
3384
3475
  }
3385
3476
  function writeCache(home, cache) {
3386
3477
  const p = cachePath(home);
3387
- mkdirSync2(dirname7(p), { recursive: true });
3478
+ mkdirSync2(dirname8(p), { recursive: true });
3388
3479
  writeFileSync3(p, JSON.stringify(cache, null, 2));
3389
3480
  }
3390
3481
  function latestFromCache(home, currentVersion) {
@@ -3415,12 +3506,12 @@ function triggerAutoUpdate(home, currentVersion) {
3415
3506
 
3416
3507
  // src/utils/plugin-registry.ts
3417
3508
  import { readFileSync as readFileSync5 } from "fs";
3418
- import { join as join21 } from "path";
3419
- var REGISTRY_PATH = join21(".claude", "plugins", "installed_plugins.json");
3509
+ import { join as join22 } from "path";
3510
+ var REGISTRY_PATH = join22(".claude", "plugins", "installed_plugins.json");
3420
3511
  var PLUGIN_KEY = "skillwiki@llm-wiki";
3421
3512
  function readInstalledPlugins(home) {
3422
3513
  try {
3423
- const raw = readFileSync5(join21(home, REGISTRY_PATH), "utf8");
3514
+ const raw = readFileSync5(join22(home, REGISTRY_PATH), "utf8");
3424
3515
  return JSON.parse(raw);
3425
3516
  } catch {
3426
3517
  return null;
@@ -3434,6 +3525,208 @@ function findPlugin(home, key = PLUGIN_KEY) {
3434
3525
  return entries[0];
3435
3526
  }
3436
3527
 
3528
+ // src/utils/s3-mount-health.ts
3529
+ import { execSync } from "child_process";
3530
+ import { platform } from "os";
3531
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3, readFileSync as readFile17 } from "fs";
3532
+ import { join as join23 } from "path";
3533
+ var OS = platform();
3534
+ function findRcloneMountPid() {
3535
+ try {
3536
+ const out = execSync("pgrep -f 'rclone.*mount'", {
3537
+ encoding: "utf8",
3538
+ timeout: 2e3,
3539
+ stdio: ["pipe", "pipe", "pipe"]
3540
+ }).trim();
3541
+ const pids = out.split("\n").filter(Boolean);
3542
+ if (pids.length === 0) return null;
3543
+ return parseInt(pids[0], 10);
3544
+ } catch {
3545
+ try {
3546
+ const out = execSync("ps aux", { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] });
3547
+ for (const line of out.split("\n")) {
3548
+ if (line.includes("rclone") && line.includes("mount") && !line.includes("grep")) {
3549
+ const parts = line.trim().split(/\s+/);
3550
+ if (parts.length >= 2) return parseInt(parts[1], 10);
3551
+ }
3552
+ }
3553
+ } catch {
3554
+ }
3555
+ return null;
3556
+ }
3557
+ }
3558
+ function parseRcloneFlags(pid) {
3559
+ const flags = /* @__PURE__ */ new Map();
3560
+ try {
3561
+ const args = getRcloneArgs(pid);
3562
+ for (let i = 0; i < args.length; i++) {
3563
+ const arg = args[i];
3564
+ if (arg.startsWith("--") && arg.includes("=")) {
3565
+ const eq = arg.indexOf("=");
3566
+ flags.set(arg.slice(0, eq), arg.slice(eq + 1));
3567
+ } else if (arg.startsWith("--")) {
3568
+ const next = args[i + 1];
3569
+ if (next && !next.startsWith("-")) {
3570
+ flags.set(arg, next);
3571
+ i++;
3572
+ } else {
3573
+ flags.set(arg, "");
3574
+ }
3575
+ }
3576
+ }
3577
+ } catch {
3578
+ }
3579
+ return flags;
3580
+ }
3581
+ function getRcloneVersion() {
3582
+ try {
3583
+ const out = execSync("rclone version", {
3584
+ encoding: "utf8",
3585
+ timeout: 3e3,
3586
+ stdio: ["pipe", "pipe", "pipe"]
3587
+ });
3588
+ const match = out.match(/rclone\s+v(\d+)\.(\d+)\.(\d+)/i);
3589
+ if (!match) return null;
3590
+ return {
3591
+ major: parseInt(match[1], 10),
3592
+ minor: parseInt(match[2], 10),
3593
+ patch: parseInt(match[3], 10),
3594
+ raw: out.split("\n")[0].trim()
3595
+ };
3596
+ } catch {
3597
+ return null;
3598
+ }
3599
+ }
3600
+ function extractRcloneFs(args) {
3601
+ let foundMount = false;
3602
+ for (const arg of args) {
3603
+ if (arg === "mount") {
3604
+ foundMount = true;
3605
+ continue;
3606
+ }
3607
+ if (foundMount && arg.includes(":") && !arg.startsWith("-") && !arg.startsWith("/")) {
3608
+ return arg;
3609
+ }
3610
+ }
3611
+ return null;
3612
+ }
3613
+ function getRcloneArgs(pid) {
3614
+ try {
3615
+ if (OS === "linux") {
3616
+ const raw = readFileSync6(`/proc/${pid}/cmdline`);
3617
+ return new TextDecoder().decode(raw).split("\0").filter(Boolean);
3618
+ } else {
3619
+ const out = execSync(`ps -o args= -p ${pid}`, {
3620
+ encoding: "utf8",
3621
+ timeout: 2e3,
3622
+ stdio: ["pipe", "pipe", "pipe"]
3623
+ }).trim();
3624
+ return out.split(/\s+/);
3625
+ }
3626
+ } catch {
3627
+ return [];
3628
+ }
3629
+ }
3630
+ function queryRcloneRC(rcAddr, fs) {
3631
+ try {
3632
+ const payload = JSON.stringify({ fs });
3633
+ const out = execSync(
3634
+ `curl -s --max-time 3 -X POST "http://${rcAddr}/vfs/stats" -H "Content-Type: application/json" -d '${payload}' 2>/dev/null`,
3635
+ { encoding: "utf8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
3636
+ );
3637
+ if (!out.trim()) return null;
3638
+ const data = JSON.parse(out);
3639
+ if (data.status && data.status >= 400) {
3640
+ return { error: data.error || `RC error (status ${data.status})`, erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
3641
+ }
3642
+ const dc = data.diskCache || {};
3643
+ return {
3644
+ erroredFiles: dc.erroredFiles ?? 0,
3645
+ uploadsInProgress: dc.uploadsInProgress ?? 0,
3646
+ uploadsQueued: dc.uploadsQueued ?? 0,
3647
+ outOfSpace: dc.outOfSpace ?? false,
3648
+ bytesUsed: dc.bytesUsed ?? 0,
3649
+ files: dc.files ?? 0,
3650
+ totalSize: data.totalSize || "unknown"
3651
+ };
3652
+ } catch {
3653
+ return { error: "RC endpoint unreachable", erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
3654
+ }
3655
+ }
3656
+ function detectFuseMount(vaultPath) {
3657
+ try {
3658
+ if (OS === "linux") {
3659
+ const mounts = readFileSync6("/proc/mounts", "utf8");
3660
+ let best = null;
3661
+ for (const line of mounts.split("\n")) {
3662
+ const parts = line.split(" ");
3663
+ if (parts.length < 3) continue;
3664
+ const point = parts[1];
3665
+ const fs = parts[2];
3666
+ if (vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
3667
+ best = { point, fs };
3668
+ }
3669
+ }
3670
+ if (best && best.fs.includes("fuse")) return { mountPoint: best.point, fsType: best.fs };
3671
+ } else if (OS === "darwin") {
3672
+ const out = execSync("mount", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
3673
+ let best = null;
3674
+ for (const line of out.split("\n")) {
3675
+ const match = line.match(/^(\S+) on (\S+) \((.*?)\)/);
3676
+ if (!match) continue;
3677
+ const point = match[2];
3678
+ const opts = match[3];
3679
+ if (opts.includes("fuse") && vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
3680
+ best = { point, fsType: `fuse.${match[1].split(":")[0] || "unknown"}` };
3681
+ }
3682
+ }
3683
+ if (best) return best;
3684
+ }
3685
+ } catch {
3686
+ }
3687
+ return null;
3688
+ }
3689
+ function writeTest(dir) {
3690
+ const testFile = join23(dir, `.doctor-write-test-${process.pid}.tmp`);
3691
+ const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
3692
+ const start = Date.now();
3693
+ try {
3694
+ writeFileSync4(testFile, payload, "utf8");
3695
+ } catch (e) {
3696
+ return { success: false, writeMs: Date.now() - start, readMs: 0, size: 0, error: `write failed: ${e.message}` };
3697
+ }
3698
+ const writeMs = Date.now() - start;
3699
+ const readStart = Date.now();
3700
+ try {
3701
+ const back = readFile17(testFile, "utf8");
3702
+ const readMs = Date.now() - readStart;
3703
+ if (back !== payload) {
3704
+ try {
3705
+ unlinkSync3(testFile);
3706
+ } catch {
3707
+ }
3708
+ return { success: false, writeMs, readMs, size: Buffer.byteLength(payload, "utf8"), error: "content mismatch \u2014 wrote and read-back differ" };
3709
+ }
3710
+ } catch (e) {
3711
+ try {
3712
+ unlinkSync3(testFile);
3713
+ } catch {
3714
+ }
3715
+ return { success: false, writeMs, readMs: Date.now() - readStart, size: 0, error: `read failed: ${e.message}` };
3716
+ }
3717
+ try {
3718
+ unlinkSync3(testFile);
3719
+ } catch {
3720
+ }
3721
+ return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
3722
+ }
3723
+ var FLAG_THRESHOLDS = {
3724
+ "--vfs-write-back": { min: 15, unit: "s", label: "VFS write-back window" },
3725
+ "--vfs-write-wait": { min: 10, unit: "s", label: "VFS write-wait" },
3726
+ "--vfs-cache-max-age": { min: 24, unit: "h", label: "VFS cache max age" }
3727
+ };
3728
+ var MIN_RCLONE_VERSION = { major: 1, minor: 65, patch: 0 };
3729
+
3437
3730
  // src/commands/doctor.ts
3438
3731
  function check(status, id, label, detail) {
3439
3732
  return { id, label, status, detail };
@@ -3452,7 +3745,7 @@ function detectCliChannels(argv, home) {
3452
3745
  channels.push({ name: "dev", path: devPath, isDevLink: true });
3453
3746
  }
3454
3747
  try {
3455
- const whichOut = execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
3748
+ const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
3456
3749
  if (whichOut) {
3457
3750
  const isDev = isDevSymlink(whichOut);
3458
3751
  if (!channels.some((c) => c.path === resolve4(whichOut))) {
@@ -3463,13 +3756,13 @@ function detectCliChannels(argv, home) {
3463
3756
  }
3464
3757
  const plugin = findPlugin(home);
3465
3758
  if (plugin) {
3466
- const pluginBin = join22(plugin.installPath, "bin", "skillwiki");
3467
- if (existsSync6(pluginBin)) {
3759
+ const pluginBin = join24(plugin.installPath, "bin", "skillwiki");
3760
+ if (existsSync7(pluginBin)) {
3468
3761
  channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
3469
3762
  }
3470
3763
  }
3471
- const installBin = join22(home, ".claude", "skills", "bin", "skillwiki");
3472
- if (existsSync6(installBin)) {
3764
+ const installBin = join24(home, ".claude", "skills", "bin", "skillwiki");
3765
+ if (existsSync7(installBin)) {
3473
3766
  channels.push({ name: "install", path: installBin, isDevLink: false });
3474
3767
  }
3475
3768
  return channels;
@@ -3521,7 +3814,7 @@ function checkCliChannels(argv, home) {
3521
3814
  }
3522
3815
  async function checkConfigFile(home) {
3523
3816
  const cfgPath = configPath(home);
3524
- if (!existsSync6(cfgPath)) {
3817
+ if (!existsSync7(cfgPath)) {
3525
3818
  return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
3526
3819
  }
3527
3820
  try {
@@ -3536,7 +3829,7 @@ function checkWikiPathExists(resolvedPath) {
3536
3829
  if (resolvedPath === void 0) {
3537
3830
  return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
3538
3831
  }
3539
- if (existsSync6(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
3832
+ if (existsSync7(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
3540
3833
  return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
3541
3834
  }
3542
3835
  return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
@@ -3545,13 +3838,13 @@ function checkVaultStructure(resolvedPath) {
3545
3838
  if (resolvedPath === void 0) {
3546
3839
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
3547
3840
  }
3548
- if (!existsSync6(resolvedPath)) {
3841
+ if (!existsSync7(resolvedPath)) {
3549
3842
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
3550
3843
  }
3551
3844
  const missing = [];
3552
- if (!existsSync6(join22(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
3845
+ if (!existsSync7(join24(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
3553
3846
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
3554
- if (!existsSync6(join22(resolvedPath, dir))) missing.push(dir + "/");
3847
+ if (!existsSync7(join24(resolvedPath, dir))) missing.push(dir + "/");
3555
3848
  }
3556
3849
  if (missing.length === 0) {
3557
3850
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -3559,8 +3852,8 @@ function checkVaultStructure(resolvedPath) {
3559
3852
  return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
3560
3853
  }
3561
3854
  function checkSkillsInstalled(home, cwd) {
3562
- const srcDir = cwd ? join22(cwd, "packages", "skills") : void 0;
3563
- if (srcDir && existsSync6(srcDir)) {
3855
+ const srcDir = cwd ? join24(cwd, "packages", "skills") : void 0;
3856
+ if (srcDir && existsSync7(srcDir)) {
3564
3857
  const found = findSkillMd(srcDir);
3565
3858
  if (found.length > 0) {
3566
3859
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
@@ -3573,8 +3866,8 @@ function checkSkillsInstalled(home, cwd) {
3573
3866
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
3574
3867
  }
3575
3868
  }
3576
- const skillsDir = join22(home, ".claude", "skills");
3577
- if (existsSync6(skillsDir)) {
3869
+ const skillsDir = join24(home, ".claude", "skills");
3870
+ if (existsSync7(skillsDir)) {
3578
3871
  const found = findSkillMd(skillsDir);
3579
3872
  if (found.length > 0) {
3580
3873
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
@@ -3584,10 +3877,10 @@ function checkSkillsInstalled(home, cwd) {
3584
3877
  }
3585
3878
  function checkDuplicateSkills(home) {
3586
3879
  const plugin = findPlugin(home);
3587
- const skillsDir = join22(home, ".claude", "skills");
3880
+ const skillsDir = join24(home, ".claude", "skills");
3588
3881
  const agentSkillDirs = [
3589
- { label: "~/.codex/skills/", path: join22(home, ".codex", "skills") },
3590
- { label: "~/.agents/skills/", path: join22(home, ".agents", "skills") }
3882
+ { label: "~/.codex/skills/", path: join24(home, ".codex", "skills") },
3883
+ { label: "~/.agents/skills/", path: join24(home, ".agents", "skills") }
3591
3884
  ];
3592
3885
  if (!plugin) {
3593
3886
  return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
@@ -3664,8 +3957,8 @@ async function checkProfiles(home) {
3664
3957
  }
3665
3958
  async function checkProjectLocalOverride(cwd) {
3666
3959
  const dir = cwd ?? process.cwd();
3667
- const envPath = join22(dir, ".skillwiki", ".env");
3668
- if (existsSync6(envPath)) {
3960
+ const envPath = join24(dir, ".skillwiki", ".env");
3961
+ if (existsSync7(envPath)) {
3669
3962
  return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
3670
3963
  }
3671
3964
  return check("pass", "project_local", "Project-local config", "None");
@@ -3674,17 +3967,17 @@ function checkVaultGitRemote(resolvedPath) {
3674
3967
  if (resolvedPath === void 0) {
3675
3968
  return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
3676
3969
  }
3677
- if (!existsSync6(join22(resolvedPath, ".git"))) {
3970
+ if (!existsSync7(join24(resolvedPath, ".git"))) {
3678
3971
  return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
3679
3972
  }
3680
3973
  try {
3681
- const remote = execSync("git remote", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3974
+ const remote = execSync2("git remote", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3682
3975
  if (!remote) {
3683
3976
  return check("warn", "vault_git_remote", "Vault git remote", "No remote configured \u2014 push/pull unavailable");
3684
3977
  }
3685
3978
  let branch = "(no commits yet)";
3686
3979
  try {
3687
- branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3980
+ branch = execSync2("git rev-parse --abbrev-ref HEAD", { cwd: resolvedPath, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3688
3981
  } catch {
3689
3982
  }
3690
3983
  return check("pass", "vault_git_remote", "Vault git remote", `Remote: ${remote.split("\n")[0]}, branch: ${branch}`);
@@ -3697,9 +3990,9 @@ function checkObsidianTemplates(resolvedPath) {
3697
3990
  return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
3698
3991
  }
3699
3992
  const missing = [];
3700
- if (!existsSync6(join22(resolvedPath, "_Templates"))) missing.push("_Templates/");
3701
- if (!existsSync6(join22(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
3702
- if (!existsSync6(join22(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
3993
+ if (!existsSync7(join24(resolvedPath, "_Templates"))) missing.push("_Templates/");
3994
+ if (!existsSync7(join24(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
3995
+ if (!existsSync7(join24(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
3703
3996
  if (missing.length === 0) {
3704
3997
  return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
3705
3998
  }
@@ -3709,8 +4002,8 @@ function checkDotStoreClean(resolvedPath) {
3709
4002
  if (resolvedPath === void 0) {
3710
4003
  return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
3711
4004
  }
3712
- const rawDir = join22(resolvedPath, "raw");
3713
- if (!existsSync6(rawDir)) {
4005
+ const rawDir = join24(resolvedPath, "raw");
4006
+ if (!existsSync7(rawDir)) {
3714
4007
  return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
3715
4008
  }
3716
4009
  const found = [];
@@ -3725,7 +4018,7 @@ function checkDotStoreClean(resolvedPath) {
3725
4018
  if (entry.name === ".DS_Store") {
3726
4019
  found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
3727
4020
  } else if (entry.isDirectory()) {
3728
- walk2(join22(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
4021
+ walk2(join24(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
3729
4022
  }
3730
4023
  }
3731
4024
  })(rawDir, "");
@@ -3738,12 +4031,12 @@ function checkSyncLastPush(resolvedPath) {
3738
4031
  if (resolvedPath === void 0) {
3739
4032
  return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
3740
4033
  }
3741
- if (!existsSync6(join22(resolvedPath, ".git"))) {
4034
+ if (!existsSync7(join24(resolvedPath, ".git"))) {
3742
4035
  return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
3743
4036
  }
3744
4037
  let timestamp;
3745
4038
  try {
3746
- const out = execSync("git log -1 --format=%ct origin/HEAD", {
4039
+ const out = execSync2("git log -1 --format=%ct origin/HEAD", {
3747
4040
  cwd: resolvedPath,
3748
4041
  encoding: "utf8",
3749
4042
  stdio: ["pipe", "pipe", "pipe"]
@@ -3751,7 +4044,7 @@ function checkSyncLastPush(resolvedPath) {
3751
4044
  timestamp = parseInt(out, 10);
3752
4045
  } catch {
3753
4046
  try {
3754
- const out = execSync("git log -1 --format=%ct HEAD", {
4047
+ const out = execSync2("git log -1 --format=%ct HEAD", {
3755
4048
  cwd: resolvedPath,
3756
4049
  encoding: "utf8",
3757
4050
  stdio: ["pipe", "pipe", "pipe"]
@@ -3770,56 +4063,23 @@ function checkSyncLastPush(resolvedPath) {
3770
4063
  }
3771
4064
  return check("pass", "sync_last_push", "Vault sync recency", `Last push: ${dateStr} (${daysSince2} day(s) ago)`);
3772
4065
  }
3773
- function detectFuseMount(vaultPath) {
3774
- const os = platform();
3775
- try {
3776
- if (os === "linux") {
3777
- const mounts = readFileSync6("/proc/mounts", "utf8");
3778
- let best = null;
3779
- for (const line of mounts.split("\n")) {
3780
- const parts = line.split(" ");
3781
- if (parts.length < 3) continue;
3782
- const point = parts[1];
3783
- const fs = parts[2];
3784
- if (vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
3785
- best = { point, fs };
3786
- }
3787
- }
3788
- if (best && best.fs.includes("fuse")) return best.point;
3789
- } else if (os === "darwin") {
3790
- const out = execSync("mount", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
3791
- let best = null;
3792
- for (const line of out.split("\n")) {
3793
- const match = line.match(/^(\S+) on (\S+) \((.*?)\)/);
3794
- if (!match) continue;
3795
- const point = match[2];
3796
- const opts = match[3];
3797
- if (opts.includes("fuse") && vaultPath.startsWith(point) && (!best || point.length > best.point.length)) {
3798
- best = { point };
3799
- }
3800
- }
3801
- if (best) return best.point;
3802
- }
3803
- } catch {
3804
- }
3805
- return null;
3806
- }
3807
4066
  function checkS3MountPerf(resolvedPath) {
3808
4067
  if (resolvedPath === void 0) {
3809
4068
  return check("pass", "s3_mount_perf", "S3 mount performance", "No vault path \u2014 check skipped");
3810
4069
  }
3811
- const mountPoint = detectFuseMount(resolvedPath);
3812
- if (!mountPoint) {
4070
+ const fuse = detectFuseMount(resolvedPath);
4071
+ if (!fuse) {
3813
4072
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
3814
4073
  }
3815
- const conceptsDir = join22(resolvedPath, "concepts");
3816
- if (!existsSync6(conceptsDir)) {
4074
+ const mountPoint = fuse.mountPoint;
4075
+ const conceptsDir = join24(resolvedPath, "concepts");
4076
+ if (!existsSync7(conceptsDir)) {
3817
4077
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
3818
4078
  }
3819
4079
  const start = Date.now();
3820
4080
  let timedOut = false;
3821
4081
  try {
3822
- execSync(`rg -l "." "${conceptsDir}"`, {
4082
+ execSync2(`rg -l "." "${conceptsDir}"`, {
3823
4083
  timeout: 5e3,
3824
4084
  encoding: "utf8",
3825
4085
  stdio: ["pipe", "pipe", "pipe"]
@@ -3852,6 +4112,170 @@ function checkS3MountPerf(resolvedPath) {
3852
4112
  `S3 FUSE mount, cache warm (rg scan: ${elapsed.toFixed(3)}s)`
3853
4113
  );
3854
4114
  }
4115
+ function checkRcloneFlagAudit(resolvedPath) {
4116
+ if (!resolvedPath) {
4117
+ return check("pass", "rclone_flags", "rclone VFS flags", "No vault path \u2014 check skipped");
4118
+ }
4119
+ const fuse = detectFuseMount(resolvedPath);
4120
+ if (!fuse) {
4121
+ return check("pass", "rclone_flags", "rclone VFS flags", "local disk \u2014 check skipped");
4122
+ }
4123
+ const pid = findRcloneMountPid();
4124
+ if (pid === null) {
4125
+ return check("warn", "rclone_flags", "rclone VFS flags", `S3 FUSE mount (${fuse.mountPoint}) but no rclone process found \u2014 cannot audit flags`);
4126
+ }
4127
+ const flags = parseRcloneFlags(pid);
4128
+ if (flags.size === 0) {
4129
+ return check("warn", "rclone_flags", "rclone VFS flags", `rclone PID ${pid} found but could not parse flags`);
4130
+ }
4131
+ const warnings = [];
4132
+ for (const [flag, threshold] of Object.entries(FLAG_THRESHOLDS)) {
4133
+ const raw = flags.get(flag);
4134
+ if (raw === void 0) {
4135
+ warnings.push(`${flag} not set (default may be unsafe)`);
4136
+ continue;
4137
+ }
4138
+ const value = parseFloat(raw);
4139
+ if (isNaN(value)) continue;
4140
+ let inSeconds = value;
4141
+ if (raw.endsWith("h")) inSeconds = value * 3600;
4142
+ else if (raw.endsWith("m")) inSeconds = value * 60;
4143
+ const thresholdSec = threshold.unit === "h" ? threshold.min * 3600 : threshold.unit === "m" ? threshold.min * 60 : threshold.min;
4144
+ if (inSeconds < thresholdSec) {
4145
+ warnings.push(`${flag}=${raw} (recommended \u2265${threshold.min}${threshold.unit})`);
4146
+ }
4147
+ }
4148
+ const cacheMode = flags.get("--vfs-cache-mode");
4149
+ if (!cacheMode) {
4150
+ warnings.push("--vfs-cache-mode not set (recommended: full)");
4151
+ } else if (cacheMode !== "full") {
4152
+ warnings.push(`--vfs-cache-mode=${cacheMode} (recommended: full)`);
4153
+ }
4154
+ if (!flags.has("--log-file")) {
4155
+ warnings.push("--log-file not set \u2014 no rclone error log configured");
4156
+ }
4157
+ if (warnings.length > 0) {
4158
+ return check("warn", "rclone_flags", "rclone VFS flags", warnings.join("; "));
4159
+ }
4160
+ return check("pass", "rclone_flags", "rclone VFS flags", `PID ${pid}: all critical flags at safe values`);
4161
+ }
4162
+ function checkRcloneVersion(resolvedPath) {
4163
+ if (!resolvedPath) {
4164
+ return check("pass", "rclone_version", "rclone version", "No vault path \u2014 check skipped");
4165
+ }
4166
+ const fuse = detectFuseMount(resolvedPath);
4167
+ if (!fuse) {
4168
+ return check("pass", "rclone_version", "rclone version", "local disk \u2014 check skipped");
4169
+ }
4170
+ const ver = getRcloneVersion();
4171
+ if (!ver) {
4172
+ return check("warn", "rclone_version", "rclone version", "rclone not found on PATH \u2014 cannot verify version");
4173
+ }
4174
+ const min = MIN_RCLONE_VERSION;
4175
+ const tooOld = ver.major < min.major || ver.major === min.major && ver.minor < min.minor || ver.major === min.major && ver.minor === min.minor && ver.patch < min.patch;
4176
+ if (tooOld) {
4177
+ return check(
4178
+ "warn",
4179
+ "rclone_version",
4180
+ "rclone version",
4181
+ `${ver.raw} \u2014 upgrade to \u2265v${min.major}.${min.minor}.${min.patch} for --vfs-write-wait support (current version may silently ignore this flag)`
4182
+ );
4183
+ }
4184
+ return check("pass", "rclone_version", "rclone version", ver.raw);
4185
+ }
4186
+ function checkWriteTest(resolvedPath) {
4187
+ if (!resolvedPath) {
4188
+ return check("pass", "s3_write_test", "S3 write test", "No vault path \u2014 check skipped");
4189
+ }
4190
+ const fuse = detectFuseMount(resolvedPath);
4191
+ if (!fuse) {
4192
+ return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
4193
+ }
4194
+ const conceptsDir = join24(resolvedPath, "concepts");
4195
+ if (!existsSync7(conceptsDir)) {
4196
+ return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
4197
+ }
4198
+ const result = writeTest(conceptsDir);
4199
+ if (result.success) {
4200
+ const totalMs = result.writeMs + result.readMs;
4201
+ if (totalMs > 3e3) {
4202
+ return check(
4203
+ "warn",
4204
+ "s3_write_test",
4205
+ "S3 write test",
4206
+ `write+read ${totalMs}ms (write ${result.writeMs}ms, read ${result.readMs}ms, ${result.size}B) \u2014 S3 mount is slow`
4207
+ );
4208
+ }
4209
+ return check(
4210
+ "pass",
4211
+ "s3_write_test",
4212
+ "S3 write test",
4213
+ `write+read ${totalMs}ms (write ${result.writeMs}ms, read ${result.readMs}ms)`
4214
+ );
4215
+ }
4216
+ return check(
4217
+ "warn",
4218
+ "s3_write_test",
4219
+ "S3 write test",
4220
+ `${result.error} \u2014 S3 mount may have a stale FUSE handle or write-back failure`
4221
+ );
4222
+ }
4223
+ function checkVfsCacheHealth(resolvedPath) {
4224
+ if (!resolvedPath) {
4225
+ return check("pass", "vfs_cache_health", "VFS cache health", "No vault path \u2014 check skipped");
4226
+ }
4227
+ const fuse = detectFuseMount(resolvedPath);
4228
+ if (!fuse) {
4229
+ return check("pass", "vfs_cache_health", "VFS cache health", "local disk \u2014 check skipped");
4230
+ }
4231
+ const pid = findRcloneMountPid();
4232
+ if (pid === null) {
4233
+ return check("warn", "vfs_cache_health", "VFS cache health", "no rclone process found \u2014 cannot query VFS stats");
4234
+ }
4235
+ const flags = parseRcloneFlags(pid);
4236
+ const rcAddr = flags.get("--rc-addr") || "127.0.0.1:5572";
4237
+ if (!flags.has("--rc")) {
4238
+ return check(
4239
+ "info",
4240
+ "vfs_cache_health",
4241
+ "VFS cache health",
4242
+ `rclone RC not enabled \u2014 add --rc --rc-addr ${rcAddr} to enable cache health monitoring`
4243
+ );
4244
+ }
4245
+ const args = getRcloneArgs(pid);
4246
+ const fs = extractRcloneFs(args) || "unknown:";
4247
+ const stats = queryRcloneRC(rcAddr, fs || "unknown:");
4248
+ if (!stats) {
4249
+ return check(
4250
+ "warn",
4251
+ "vfs_cache_health",
4252
+ "VFS cache health",
4253
+ `RC endpoint ${rcAddr} unreachable \u2014 is rclone --rc enabled?`
4254
+ );
4255
+ }
4256
+ if (stats.error) {
4257
+ return check("warn", "vfs_cache_health", "VFS cache health", stats.error);
4258
+ }
4259
+ const issues = [];
4260
+ if (stats.uploadsInProgress > 0) issues.push(`${stats.uploadsInProgress} upload(s) in progress`);
4261
+ if (stats.uploadsQueued > 10) issues.push(`${stats.uploadsQueued} upload(s) queued (backlog)`);
4262
+ if (stats.erroredFiles > 0) issues.push(`${stats.erroredFiles} errored file(s)`);
4263
+ if (stats.outOfSpace) issues.push("cache disk full");
4264
+ if (issues.length > 0) {
4265
+ return check(
4266
+ "warn",
4267
+ "vfs_cache_health",
4268
+ "VFS cache health",
4269
+ `${stats.files} files, ${stats.bytesUsed} bytes \u2014 ${issues.join("; ")}`
4270
+ );
4271
+ }
4272
+ return check(
4273
+ "pass",
4274
+ "vfs_cache_health",
4275
+ "VFS cache health",
4276
+ `${stats.files} files, ${(stats.bytesUsed / 1024 / 1024).toFixed(1)}MB \u2014 clean (0 errored, 0 pending)`
4277
+ );
4278
+ }
3855
4279
  function findSkillMd(dir) {
3856
4280
  const results = [];
3857
4281
  let entries;
@@ -3862,9 +4286,9 @@ function findSkillMd(dir) {
3862
4286
  }
3863
4287
  for (const entry of entries) {
3864
4288
  if (entry.isFile() && entry.name === "SKILL.md") {
3865
- results.push(join22(dir, entry.name));
4289
+ results.push(join24(dir, entry.name));
3866
4290
  } else if (entry.isDirectory()) {
3867
- results.push(...findSkillMd(join22(dir, entry.name)));
4291
+ results.push(...findSkillMd(join24(dir, entry.name)));
3868
4292
  }
3869
4293
  }
3870
4294
  return results;
@@ -3878,7 +4302,7 @@ function findSkillNames(dir) {
3878
4302
  return results;
3879
4303
  }
3880
4304
  for (const entry of entries) {
3881
- if (entry.isDirectory() && existsSync6(join22(dir, entry.name, "SKILL.md"))) {
4305
+ if (entry.isDirectory() && existsSync7(join24(dir, entry.name, "SKILL.md"))) {
3882
4306
  results.push(entry.name);
3883
4307
  }
3884
4308
  }
@@ -3905,6 +4329,10 @@ async function runDoctor(input) {
3905
4329
  checks.push(checkSyncLastPush(resolvedPath));
3906
4330
  checks.push(checkDotStoreClean(resolvedPath));
3907
4331
  checks.push(checkS3MountPerf(resolvedPath));
4332
+ checks.push(checkRcloneFlagAudit(resolvedPath));
4333
+ checks.push(checkRcloneVersion(resolvedPath));
4334
+ checks.push(checkWriteTest(resolvedPath));
4335
+ checks.push(checkVfsCacheHealth(resolvedPath));
3908
4336
  checks.push(checkSkillsInstalled(input.home, input.cwd));
3909
4337
  checks.push(checkDuplicateSkills(input.home));
3910
4338
  checks.push(checkNpmUpdate(input.home, input.currentVersion));
@@ -3932,8 +4360,8 @@ async function runDoctor(input) {
3932
4360
  }
3933
4361
 
3934
4362
  // src/commands/archive.ts
3935
- import { rename as rename4, mkdir as mkdir8, readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
3936
- import { join as join23, dirname as dirname8 } from "path";
4363
+ import { rename as rename5, mkdir as mkdir8, readFile as readFile18, writeFile as writeFile9 } from "fs/promises";
4364
+ import { join as join25, dirname as dirname9 } from "path";
3937
4365
  async function runArchive(input) {
3938
4366
  const scan = await scanVault(input.vault);
3939
4367
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -3949,13 +4377,13 @@ async function runArchive(input) {
3949
4377
  }
3950
4378
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
3951
4379
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
3952
- const archivePath = join23("_archive", relPath).replace(/\\/g, "/");
3953
- await mkdir8(dirname8(join23(input.vault, archivePath)), { recursive: true });
4380
+ const archivePath = join25("_archive", relPath).replace(/\\/g, "/");
4381
+ await mkdir8(dirname9(join25(input.vault, archivePath)), { recursive: true });
3954
4382
  let indexUpdated = false;
3955
4383
  if (!isRaw) {
3956
- const indexPath = join23(input.vault, "index.md");
4384
+ const indexPath = join25(input.vault, "index.md");
3957
4385
  try {
3958
- const idx = await readFile16(indexPath, "utf8");
4386
+ const idx = await readFile18(indexPath, "utf8");
3959
4387
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
3960
4388
  const originalLines = idx.split("\n");
3961
4389
  const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
@@ -3967,7 +4395,7 @@ async function runArchive(input) {
3967
4395
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
3968
4396
  }
3969
4397
  }
3970
- await rename4(join23(input.vault, relPath), join23(input.vault, archivePath));
4398
+ await rename5(join25(input.vault, relPath), join25(input.vault, archivePath));
3971
4399
  appendLastOp(input.vault, {
3972
4400
  operation: "archive",
3973
4401
  summary: `moved ${relPath} to ${archivePath}`,
@@ -3979,7 +4407,6 @@ async function runArchive(input) {
3979
4407
 
3980
4408
  // src/commands/drift.ts
3981
4409
  import { createHash as createHash3 } from "crypto";
3982
- import { writeFile as writeFile10 } from "fs/promises";
3983
4410
 
3984
4411
  // src/utils/fetch.ts
3985
4412
  async function controlledFetch(url, opts) {
@@ -4066,7 +4493,7 @@ async function runDrift(input) {
4066
4493
  ${newFm}
4067
4494
  ---
4068
4495
  ${body}`;
4069
- await writeFile10(raw.absPath, newText, "utf8");
4496
+ await safeWritePage(raw.absPath, newText);
4070
4497
  results.push({
4071
4498
  raw_path: raw.relPath,
4072
4499
  source_url: sourceUrl,
@@ -4109,7 +4536,6 @@ ${body}`;
4109
4536
  }
4110
4537
 
4111
4538
  // src/commands/migrate-citations.ts
4112
- import { writeFile as writeFile11 } from "fs/promises";
4113
4539
  var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
4114
4540
  function moveMarkersToParagraphEnd(body) {
4115
4541
  const lines = body.split("\n");
@@ -4232,7 +4658,11 @@ ${migratedBody}${newFooter}`;
4232
4658
  continue;
4233
4659
  }
4234
4660
  if (!input.dryRun) {
4235
- await writeFile11(page.absPath, newText, "utf8");
4661
+ const w = await safeWritePage(page.absPath, newText);
4662
+ if (!w.ok) {
4663
+ skipped.push(page.relPath);
4664
+ continue;
4665
+ }
4236
4666
  }
4237
4667
  migrated.push(page.relPath);
4238
4668
  }
@@ -4262,7 +4692,6 @@ ${migratedBody}${newFooter}`;
4262
4692
  }
4263
4693
 
4264
4694
  // src/commands/frontmatter-fix.ts
4265
- import { writeFile as writeFile12 } from "fs/promises";
4266
4695
  function isoToday() {
4267
4696
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4268
4697
  }
@@ -4304,7 +4733,11 @@ ${newBody}`;
4304
4733
  continue;
4305
4734
  }
4306
4735
  if (!input.dryRun) {
4307
- await writeFile12(page.absPath, newText, "utf8");
4736
+ const w = await safeWritePage(page.absPath, newText);
4737
+ if (!w.ok) {
4738
+ skipped.push(page.relPath);
4739
+ continue;
4740
+ }
4308
4741
  }
4309
4742
  fixed.push(page.relPath);
4310
4743
  }
@@ -4335,16 +4768,16 @@ ${newBody}`;
4335
4768
  }
4336
4769
 
4337
4770
  // src/commands/update.ts
4338
- import { execSync as execSync2 } from "child_process";
4771
+ import { execSync as execSync3 } from "child_process";
4339
4772
  import { readFileSync as readFileSync7 } from "fs";
4340
- import { join as join24 } from "path";
4773
+ import { join as join26 } from "path";
4341
4774
  function resolveGlobalSkillsRoot() {
4342
4775
  try {
4343
- const globalRoot = execSync2("npm root -g", {
4776
+ const globalRoot = execSync3("npm root -g", {
4344
4777
  encoding: "utf8",
4345
4778
  timeout: 5e3
4346
4779
  }).trim();
4347
- return join24(globalRoot, "skillwiki", "skills");
4780
+ return join26(globalRoot, "skillwiki", "skills");
4348
4781
  } catch {
4349
4782
  return null;
4350
4783
  }
@@ -4370,10 +4803,10 @@ async function runUpdate(input) {
4370
4803
  );
4371
4804
  const currentVersion = pkg2.version;
4372
4805
  const tag = input.distTag ?? "latest";
4373
- const target = join24(input.home, ".claude", "skills");
4806
+ const target = join26(input.home, ".claude", "skills");
4374
4807
  let latest;
4375
4808
  try {
4376
- latest = execSync2(`npm view skillwiki@${tag} version`, {
4809
+ latest = execSync3(`npm view skillwiki@${tag} version`, {
4377
4810
  encoding: "utf8",
4378
4811
  timeout: 15e3
4379
4812
  }).trim();
@@ -4403,7 +4836,7 @@ async function runUpdate(input) {
4403
4836
  };
4404
4837
  }
4405
4838
  try {
4406
- execSync2(`npm install -g skillwiki@${tag}`, {
4839
+ execSync3(`npm install -g skillwiki@${tag}`, {
4407
4840
  stdio: "pipe",
4408
4841
  timeout: 6e4
4409
4842
  });
@@ -4439,17 +4872,17 @@ async function runUpdate(input) {
4439
4872
  }
4440
4873
 
4441
4874
  // src/commands/self-update.ts
4442
- import { execSync as execSync3 } from "child_process";
4443
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
4444
- import { join as join25 } from "path";
4875
+ import { execSync as execSync4 } from "child_process";
4876
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
4877
+ import { join as join27 } from "path";
4445
4878
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
4446
4879
  async function runSelfUpdate(input) {
4447
4880
  const currentVersion = JSON.parse(
4448
4881
  readFileSync8(new URL("../../package.json", import.meta.url), "utf8")
4449
4882
  ).version;
4450
4883
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
4451
- const localPkgPath = join25(sourceRoot, "packages", "cli", "package.json");
4452
- const hasLocalSource = existsSync7(localPkgPath);
4884
+ const localPkgPath = join27(sourceRoot, "packages", "cli", "package.json");
4885
+ const hasLocalSource = existsSync8(localPkgPath);
4453
4886
  if (input.check) {
4454
4887
  let availableVersion = null;
4455
4888
  let source;
@@ -4463,7 +4896,7 @@ async function runSelfUpdate(input) {
4463
4896
  } else {
4464
4897
  source = "npm";
4465
4898
  try {
4466
- availableVersion = execSync3("npm view skillwiki@latest version", {
4899
+ availableVersion = execSync4("npm view skillwiki@latest version", {
4467
4900
  encoding: "utf8",
4468
4901
  timeout: 15e3
4469
4902
  }).trim();
@@ -4489,7 +4922,7 @@ async function runSelfUpdate(input) {
4489
4922
  }
4490
4923
  if (hasLocalSource) {
4491
4924
  try {
4492
- execSync3("npm run build -w packages/cli", {
4925
+ execSync4("npm run build -w packages/cli", {
4493
4926
  cwd: sourceRoot,
4494
4927
  stdio: "pipe",
4495
4928
  timeout: 6e4
@@ -4501,7 +4934,7 @@ async function runSelfUpdate(input) {
4501
4934
  };
4502
4935
  }
4503
4936
  try {
4504
- execSync3("npm link ./packages/cli", {
4937
+ execSync4("npm link ./packages/cli", {
4505
4938
  cwd: sourceRoot,
4506
4939
  stdio: "pipe",
4507
4940
  timeout: 3e4
@@ -4533,7 +4966,7 @@ async function runSelfUpdate(input) {
4533
4966
  }
4534
4967
  let latestVersion;
4535
4968
  try {
4536
- latestVersion = execSync3("npm view skillwiki@latest version", {
4969
+ latestVersion = execSync4("npm view skillwiki@latest version", {
4537
4970
  encoding: "utf8",
4538
4971
  timeout: 15e3
4539
4972
  }).trim();
@@ -4556,7 +4989,7 @@ async function runSelfUpdate(input) {
4556
4989
  };
4557
4990
  }
4558
4991
  try {
4559
- execSync3("npm install -g skillwiki@latest", {
4992
+ execSync4("npm install -g skillwiki@latest", {
4560
4993
  stdio: "pipe",
4561
4994
  timeout: 6e4
4562
4995
  });
@@ -4580,10 +5013,10 @@ async function runSelfUpdate(input) {
4580
5013
  }
4581
5014
 
4582
5015
  // src/commands/transcripts.ts
4583
- import { readdir as readdir5, stat as stat6, readFile as readFile17 } from "fs/promises";
4584
- import { join as join26 } from "path";
5016
+ import { readdir as readdir5, stat as stat6, readFile as readFile19 } from "fs/promises";
5017
+ import { join as join28 } from "path";
4585
5018
  async function runTranscripts(input) {
4586
- const dir = join26(input.vault, "raw", "transcripts");
5019
+ const dir = join28(input.vault, "raw", "transcripts");
4587
5020
  let entries;
4588
5021
  try {
4589
5022
  entries = await readdir5(dir, { withFileTypes: true });
@@ -4593,8 +5026,8 @@ async function runTranscripts(input) {
4593
5026
  const transcripts = [];
4594
5027
  for (const entry of entries) {
4595
5028
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4596
- const filePath = join26(dir, entry.name);
4597
- const content = await readFile17(filePath, "utf8");
5029
+ const filePath = join28(dir, entry.name);
5030
+ const content = await readFile19(filePath, "utf8");
4598
5031
  const fm = extractFrontmatter(content);
4599
5032
  if (!fm.ok) continue;
4600
5033
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
@@ -4611,12 +5044,12 @@ async function runTranscripts(input) {
4611
5044
  }
4612
5045
 
4613
5046
  // src/commands/project-index.ts
4614
- import { readdir as readdir6, readFile as readFile18, writeFile as writeFile13, mkdir as mkdir9 } from "fs/promises";
4615
- import { join as join27, dirname as dirname9 } from "path";
5047
+ import { readdir as readdir6, readFile as readFile20, writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
5048
+ import { join as join29, dirname as dirname10 } from "path";
4616
5049
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
4617
5050
  async function runProjectIndex(input) {
4618
5051
  const slug = input.slug;
4619
- const projectDir = join27(input.vault, "projects", slug);
5052
+ const projectDir = join29(input.vault, "projects", slug);
4620
5053
  try {
4621
5054
  await readdir6(projectDir);
4622
5055
  } catch {
@@ -4627,15 +5060,15 @@ async function runProjectIndex(input) {
4627
5060
  }
4628
5061
  const wikilinkPattern = `[[${slug}]]`;
4629
5062
  const entries = [];
4630
- const compoundDir = join27(input.vault, "projects", slug, "compound");
5063
+ const compoundDir = join29(input.vault, "projects", slug, "compound");
4631
5064
  try {
4632
5065
  const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
4633
5066
  for (const entry of compoundFiles) {
4634
5067
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4635
- const filePath = join27(compoundDir, entry.name);
5068
+ const filePath = join29(compoundDir, entry.name);
4636
5069
  let text;
4637
5070
  try {
4638
- text = await readFile18(filePath, "utf8");
5071
+ text = await readFile20(filePath, "utf8");
4639
5072
  } catch {
4640
5073
  continue;
4641
5074
  }
@@ -4652,16 +5085,16 @@ async function runProjectIndex(input) {
4652
5085
  for (const dir of LAYER2_DIRS) {
4653
5086
  let files;
4654
5087
  try {
4655
- files = await readdir6(join27(input.vault, dir), { withFileTypes: true });
5088
+ files = await readdir6(join29(input.vault, dir), { withFileTypes: true });
4656
5089
  } catch {
4657
5090
  continue;
4658
5091
  }
4659
5092
  for (const entry of files) {
4660
5093
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4661
- const filePath = join27(input.vault, dir, entry.name);
5094
+ const filePath = join29(input.vault, dir, entry.name);
4662
5095
  let text;
4663
5096
  try {
4664
- text = await readFile18(filePath, "utf8");
5097
+ text = await readFile20(filePath, "utf8");
4665
5098
  } catch {
4666
5099
  continue;
4667
5100
  }
@@ -4682,11 +5115,11 @@ async function runProjectIndex(input) {
4682
5115
  const tb = typeOrder[b.type] ?? 99;
4683
5116
  return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
4684
5117
  });
4685
- const indexPath = join27(projectDir, "knowledge.md");
5118
+ const indexPath = join29(projectDir, "knowledge.md");
4686
5119
  let existing = false;
4687
5120
  let stale = false;
4688
5121
  try {
4689
- const existingText = await readFile18(indexPath, "utf8");
5122
+ const existingText = await readFile20(indexPath, "utf8");
4690
5123
  existing = true;
4691
5124
  const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
4692
5125
  const existingPages = new Set(existingEntries.map((l) => {
@@ -4726,8 +5159,8 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
4726
5159
  }
4727
5160
  if (input.apply) {
4728
5161
  try {
4729
- await mkdir9(dirname9(indexPath), { recursive: true });
4730
- await writeFile13(indexPath, body, "utf8");
5162
+ await mkdir9(dirname10(indexPath), { recursive: true });
5163
+ await writeFile10(indexPath, body, "utf8");
4731
5164
  } catch (e) {
4732
5165
  return {
4733
5166
  exitCode: ExitCode.WRITE_FAILED,
@@ -4755,10 +5188,10 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
4755
5188
  }
4756
5189
 
4757
5190
  // src/commands/compound.ts
4758
- import { writeFile as writeFile14, mkdir as mkdir10, readdir as readdir7, unlink as unlink2 } from "fs/promises";
4759
- import { join as join28 } from "path";
4760
- import { existsSync as existsSync8 } from "fs";
4761
- import { readFile as readFile19 } from "fs/promises";
5191
+ import { writeFile as writeFile11, mkdir as mkdir10, readdir as readdir7, unlink as unlink3 } from "fs/promises";
5192
+ import { join as join30 } from "path";
5193
+ import { existsSync as existsSync9 } from "fs";
5194
+ import { readFile as readFile21 } from "fs/promises";
4762
5195
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
4763
5196
  var FIELD_RE = {
4764
5197
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -4856,17 +5289,17 @@ function extractRetroFields(date, cycleName, block) {
4856
5289
  };
4857
5290
  }
4858
5291
  async function runCompound(input) {
4859
- const logPath = join28(input.vault, "log.md");
5292
+ const logPath = join30(input.vault, "log.md");
4860
5293
  let logText;
4861
5294
  try {
4862
- logText = await readFile19(logPath, "utf8");
5295
+ logText = await readFile21(logPath, "utf8");
4863
5296
  } catch {
4864
5297
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
4865
5298
  }
4866
5299
  const entries = parseRetroEntries(logText);
4867
5300
  const promoted = [];
4868
5301
  const skipped = [];
4869
- const compoundDir = join28(input.vault, "projects", input.project, "compound");
5302
+ const compoundDir = join30(input.vault, "projects", input.project, "compound");
4870
5303
  for (const entry of entries) {
4871
5304
  const generalizeValue = entry.generalize.trim();
4872
5305
  if (!/^yes/i.test(generalizeValue)) {
@@ -4874,8 +5307,8 @@ async function runCompound(input) {
4874
5307
  continue;
4875
5308
  }
4876
5309
  const slug = slugify(entry.cycleName);
4877
- const compoundPath = join28(compoundDir, `${slug}.md`);
4878
- if (existsSync8(compoundPath)) {
5310
+ const compoundPath = join30(compoundDir, `${slug}.md`);
5311
+ if (existsSync9(compoundPath)) {
4879
5312
  skipped.push(entry.date);
4880
5313
  continue;
4881
5314
  }
@@ -4913,10 +5346,10 @@ async function runCompound(input) {
4913
5346
  ].join("\n");
4914
5347
  const content = frontmatter + "\n" + body;
4915
5348
  if (!input.dryRun) {
4916
- if (!existsSync8(compoundDir)) {
5349
+ if (!existsSync9(compoundDir)) {
4917
5350
  await mkdir10(compoundDir, { recursive: true });
4918
5351
  }
4919
- await writeFile14(compoundPath, content, "utf8");
5352
+ await writeFile11(compoundPath, content, "utf8");
4920
5353
  }
4921
5354
  promoted.push(`${slug}.md`);
4922
5355
  }
@@ -4935,23 +5368,23 @@ async function runCompound(input) {
4935
5368
  };
4936
5369
  }
4937
5370
  async function runCompoundDelete(input) {
4938
- const projectDir = join28(input.vault, "projects", input.project);
4939
- if (!existsSync8(projectDir)) {
5371
+ const projectDir = join30(input.vault, "projects", input.project);
5372
+ if (!existsSync9(projectDir)) {
4940
5373
  return {
4941
5374
  exitCode: ExitCode.PROJECT_NOT_FOUND,
4942
5375
  result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
4943
5376
  };
4944
5377
  }
4945
5378
  const entryName = input.entry.replace(/\.md$/, "");
4946
- const compoundPath = join28(projectDir, "compound", `${entryName}.md`);
4947
- if (!existsSync8(compoundPath)) {
5379
+ const compoundPath = join30(projectDir, "compound", `${entryName}.md`);
5380
+ if (!existsSync9(compoundPath)) {
4948
5381
  return {
4949
5382
  exitCode: ExitCode.FILE_NOT_FOUND,
4950
5383
  result: err("FILE_NOT_FOUND", { path: compoundPath })
4951
5384
  };
4952
5385
  }
4953
5386
  try {
4954
- await unlink2(compoundPath);
5387
+ await unlink3(compoundPath);
4955
5388
  } catch (e) {
4956
5389
  return {
4957
5390
  exitCode: ExitCode.WRITE_FAILED,
@@ -4977,8 +5410,8 @@ knowledge.md regenerated`
4977
5410
  };
4978
5411
  }
4979
5412
  async function runCompoundList(input) {
4980
- const compoundDir = join28(input.vault, "projects", input.project, "compound");
4981
- if (!existsSync8(compoundDir)) {
5413
+ const compoundDir = join30(input.vault, "projects", input.project, "compound");
5414
+ if (!existsSync9(compoundDir)) {
4982
5415
  return {
4983
5416
  exitCode: ExitCode.OK,
4984
5417
  result: ok({
@@ -5008,10 +5441,10 @@ could not read compound directory`
5008
5441
  const entries = [];
5009
5442
  for (const dirent of dirents) {
5010
5443
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
5011
- const filePath = join28(compoundDir, dirent.name);
5444
+ const filePath = join30(compoundDir, dirent.name);
5012
5445
  let text;
5013
5446
  try {
5014
- text = await readFile19(filePath, "utf8");
5447
+ text = await readFile21(filePath, "utf8");
5015
5448
  } catch {
5016
5449
  continue;
5017
5450
  }
@@ -5040,9 +5473,9 @@ no compound entries found`;
5040
5473
  }
5041
5474
 
5042
5475
  // src/commands/observe.ts
5043
- import { mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
5044
- import { existsSync as existsSync9, statSync as statSync3 } from "fs";
5045
- import { join as join29 } from "path";
5476
+ import { mkdir as mkdir11, writeFile as writeFile12 } from "fs/promises";
5477
+ import { existsSync as existsSync10, statSync as statSync3 } from "fs";
5478
+ import { join as join31 } from "path";
5046
5479
  import { createHash as createHash4 } from "crypto";
5047
5480
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
5048
5481
  function slugify2(text) {
@@ -5065,13 +5498,13 @@ async function runObserve(input) {
5065
5498
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
5066
5499
  };
5067
5500
  }
5068
- if (!existsSync9(input.vault) || !statSync3(input.vault).isDirectory()) {
5501
+ if (!existsSync10(input.vault) || !statSync3(input.vault).isDirectory()) {
5069
5502
  return {
5070
5503
  exitCode: ExitCode.VAULT_PATH_INVALID,
5071
5504
  result: err("VAULT_PATH_INVALID", { path: input.vault })
5072
5505
  };
5073
5506
  }
5074
- const transcriptsDir = join29(input.vault, "raw", "transcripts");
5507
+ const transcriptsDir = join31(input.vault, "raw", "transcripts");
5075
5508
  try {
5076
5509
  await mkdir11(transcriptsDir, { recursive: true });
5077
5510
  } catch {
@@ -5083,7 +5516,7 @@ async function runObserve(input) {
5083
5516
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5084
5517
  const slug = slugify2(input.text);
5085
5518
  const fileName = `${today}-observation-${slug}.md`;
5086
- const filePath = join29(transcriptsDir, fileName);
5519
+ const filePath = join31(transcriptsDir, fileName);
5087
5520
  const body = `
5088
5521
  ${input.text.trim()}
5089
5522
  `;
@@ -5101,7 +5534,7 @@ ${input.text.trim()}
5101
5534
  frontmatterLines.push("---");
5102
5535
  const content = frontmatterLines.join("\n") + body;
5103
5536
  try {
5104
- await writeFile15(filePath, content, "utf8");
5537
+ await writeFile12(filePath, content, "utf8");
5105
5538
  } catch (e) {
5106
5539
  return {
5107
5540
  exitCode: ExitCode.WRITE_FAILED,
@@ -5123,8 +5556,8 @@ ${input.text.trim()}
5123
5556
  }
5124
5557
 
5125
5558
  // src/commands/ingest.ts
5126
- import { readFile as readFile20, writeFile as writeFile16, mkdir as mkdir12 } from "fs/promises";
5127
- import { join as join30 } from "path";
5559
+ import { readFile as readFile22, writeFile as writeFile13, mkdir as mkdir12 } from "fs/promises";
5560
+ import { join as join32 } from "path";
5128
5561
  import { createHash as createHash5 } from "crypto";
5129
5562
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
5130
5563
  var TYPE_DIR = {
@@ -5283,7 +5716,7 @@ async function runIngest(input) {
5283
5716
  sourceContent = fetchResult.data.body;
5284
5717
  } else {
5285
5718
  try {
5286
- sourceContent = await readFile20(input.source, "utf8");
5719
+ sourceContent = await readFile22(input.source, "utf8");
5287
5720
  } catch {
5288
5721
  return {
5289
5722
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -5298,8 +5731,8 @@ async function runIngest(input) {
5298
5731
  const rawRelPath = `raw/articles/${slug}.md`;
5299
5732
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
5300
5733
  const typedRelPath = `${typedDir}/${slug}.md`;
5301
- const rawAbsPath = join30(input.vault, rawRelPath);
5302
- const typedAbsPath = join30(input.vault, typedRelPath);
5734
+ const rawAbsPath = join32(input.vault, rawRelPath);
5735
+ const typedAbsPath = join32(input.vault, typedRelPath);
5303
5736
  const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
5304
5737
  const typedContent = buildTypedContent(
5305
5738
  input.title,
@@ -5362,8 +5795,8 @@ async function runIngest(input) {
5362
5795
  };
5363
5796
  }
5364
5797
  try {
5365
- await mkdir12(join30(input.vault, "raw", "articles"), { recursive: true });
5366
- await writeFile16(rawAbsPath, rawContent, "utf8");
5798
+ await mkdir12(join32(input.vault, "raw", "articles"), { recursive: true });
5799
+ await writeFile13(rawAbsPath, rawContent, "utf8");
5367
5800
  } catch (e) {
5368
5801
  return {
5369
5802
  exitCode: ExitCode.WRITE_FAILED,
@@ -5371,8 +5804,8 @@ async function runIngest(input) {
5371
5804
  };
5372
5805
  }
5373
5806
  try {
5374
- await mkdir12(join30(input.vault, typedDir), { recursive: true });
5375
- await writeFile16(typedAbsPath, typedContent, "utf8");
5807
+ await mkdir12(join32(input.vault, typedDir), { recursive: true });
5808
+ await writeFile13(typedAbsPath, typedContent, "utf8");
5376
5809
  } catch (e) {
5377
5810
  return {
5378
5811
  exitCode: ExitCode.WRITE_FAILED,
@@ -5403,7 +5836,6 @@ async function runIngest(input) {
5403
5836
  }
5404
5837
 
5405
5838
  // src/commands/tag-sync.ts
5406
- import { writeFile as writeFile17 } from "fs/promises";
5407
5839
  var ENUM_MIRRORS = {
5408
5840
  provenance: ["research", "project", "mixed"],
5409
5841
  confidence: ["high", "medium", "low"]
@@ -5518,7 +5950,11 @@ ${newFm}
5518
5950
  ---
5519
5951
  ${body}`;
5520
5952
  if (!input.dryRun) {
5521
- await writeFile17(page.absPath, newText, "utf8");
5953
+ const w = await safeWritePage(page.absPath, newText);
5954
+ if (!w.ok) {
5955
+ unchanged++;
5956
+ continue;
5957
+ }
5522
5958
  }
5523
5959
  synced.push(page.relPath);
5524
5960
  }
@@ -5547,11 +5983,11 @@ ${body}`;
5547
5983
  }
5548
5984
 
5549
5985
  // src/commands/sync.ts
5550
- import { existsSync as existsSync10 } from "fs";
5551
- import { join as join31 } from "path";
5986
+ import { existsSync as existsSync11 } from "fs";
5987
+ import { join as join33 } from "path";
5552
5988
  function runSyncStatus(input) {
5553
5989
  const vault = input.vault;
5554
- if (!existsSync10(join31(vault, ".git"))) {
5990
+ if (!existsSync11(join33(vault, ".git"))) {
5555
5991
  return {
5556
5992
  exitCode: ExitCode.VAULT_PATH_INVALID,
5557
5993
  result: ok({
@@ -5620,7 +6056,7 @@ function runSyncStatus(input) {
5620
6056
  }
5621
6057
  async function runSyncPush(input) {
5622
6058
  const vault = input.vault;
5623
- if (!existsSync10(join31(vault, ".git"))) {
6059
+ if (!existsSync11(join33(vault, ".git"))) {
5624
6060
  return {
5625
6061
  exitCode: ExitCode.VAULT_PATH_INVALID,
5626
6062
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -5705,7 +6141,7 @@ async function runSyncPush(input) {
5705
6141
  }
5706
6142
  async function runSyncPull(input) {
5707
6143
  const vault = input.vault;
5708
- if (!existsSync10(join31(vault, ".git"))) {
6144
+ if (!existsSync11(join33(vault, ".git"))) {
5709
6145
  return {
5710
6146
  exitCode: ExitCode.VAULT_PATH_INVALID,
5711
6147
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -5780,8 +6216,8 @@ async function runSyncPull(input) {
5780
6216
  }
5781
6217
 
5782
6218
  // src/commands/backup.ts
5783
- import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
5784
- import { join as join32, relative as relative3, dirname as dirname10 } from "path";
6219
+ import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync9, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
6220
+ import { join as join34, relative as relative3, dirname as dirname11 } from "path";
5785
6221
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
5786
6222
 
5787
6223
  // src/utils/s3-client.ts
@@ -5805,7 +6241,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
5805
6241
  function* walkMarkdown(dir, base) {
5806
6242
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
5807
6243
  if (SKIP_DIRS.has(entry.name)) continue;
5808
- const full = join32(dir, entry.name);
6244
+ const full = join34(dir, entry.name);
5809
6245
  if (entry.isDirectory()) {
5810
6246
  yield* walkMarkdown(full, base);
5811
6247
  } else if (entry.name.endsWith(".md")) {
@@ -5828,7 +6264,7 @@ async function runBackupSync(input) {
5828
6264
  let failed = 0;
5829
6265
  const files = [...walkMarkdown(input.vault, input.vault)];
5830
6266
  for (const relPath of files) {
5831
- const absPath = join32(input.vault, relPath);
6267
+ const absPath = join34(input.vault, relPath);
5832
6268
  const localStat = statSync4(absPath);
5833
6269
  let needsUpload = true;
5834
6270
  try {
@@ -5904,7 +6340,7 @@ async function runBackupRestore(input) {
5904
6340
  const objects = list.Contents ?? [];
5905
6341
  for (const obj of objects) {
5906
6342
  if (!obj.Key) continue;
5907
- const localPath = join32(target, obj.Key);
6343
+ const localPath = join34(target, obj.Key);
5908
6344
  try {
5909
6345
  const localStat = statSync4(localPath);
5910
6346
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -5917,8 +6353,8 @@ async function runBackupRestore(input) {
5917
6353
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
5918
6354
  const body = await resp.Body?.transformToByteArray();
5919
6355
  if (body) {
5920
- mkdirSync3(dirname10(localPath), { recursive: true });
5921
- writeFileSync4(localPath, Buffer.from(body));
6356
+ mkdirSync3(dirname11(localPath), { recursive: true });
6357
+ writeFileSync5(localPath, Buffer.from(body));
5922
6358
  downloaded++;
5923
6359
  }
5924
6360
  } catch {
@@ -5950,11 +6386,11 @@ async function runBackupRestore(input) {
5950
6386
  }
5951
6387
 
5952
6388
  // src/commands/status.ts
5953
- import { existsSync as existsSync11, statSync as statSync5 } from "fs";
5954
- import { readFile as readFile21 } from "fs/promises";
5955
- import { join as join33 } from "path";
6389
+ import { existsSync as existsSync12, statSync as statSync5 } from "fs";
6390
+ import { readFile as readFile23 } from "fs/promises";
6391
+ import { join as join35 } from "path";
5956
6392
  async function runStatus(input) {
5957
- if (!existsSync11(input.vault)) {
6393
+ if (!existsSync12(input.vault)) {
5958
6394
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
5959
6395
  }
5960
6396
  const scan = await scanVault(input.vault);
@@ -5979,7 +6415,7 @@ async function runStatus(input) {
5979
6415
  const compound = scan.data.compound.length;
5980
6416
  let schemaVersion = "v1";
5981
6417
  try {
5982
- const schemaContent = await readFile21(join33(input.vault, "SCHEMA.md"), "utf8");
6418
+ const schemaContent = await readFile23(join35(input.vault, "SCHEMA.md"), "utf8");
5983
6419
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
5984
6420
  if (versionMatch) schemaVersion = versionMatch[1];
5985
6421
  } catch {
@@ -6039,8 +6475,8 @@ async function runStatus(input) {
6039
6475
  }
6040
6476
 
6041
6477
  // src/commands/seed.ts
6042
- import { mkdir as mkdir13, writeFile as writeFile18, stat as stat7 } from "fs/promises";
6043
- import { join as join34 } from "path";
6478
+ import { mkdir as mkdir13, writeFile as writeFile14, stat as stat7 } from "fs/promises";
6479
+ import { join as join36 } from "path";
6044
6480
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6045
6481
  var EXAMPLE_PAGES = {
6046
6482
  "entities/example-project.md": `---
@@ -6109,30 +6545,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
6109
6545
  `;
6110
6546
  async function runSeed(input) {
6111
6547
  try {
6112
- await stat7(join34(input.vault, "SCHEMA.md"));
6548
+ await stat7(join36(input.vault, "SCHEMA.md"));
6113
6549
  } catch {
6114
6550
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
6115
6551
  }
6116
6552
  const created = [];
6117
6553
  const skipped = [];
6118
6554
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
6119
- const absPath = join34(input.vault, relPath);
6555
+ const absPath = join36(input.vault, relPath);
6120
6556
  try {
6121
6557
  await stat7(absPath);
6122
6558
  skipped.push(relPath);
6123
6559
  } catch {
6124
- await mkdir13(join34(absPath, ".."), { recursive: true });
6125
- await writeFile18(absPath, content, "utf8");
6560
+ await mkdir13(join36(absPath, ".."), { recursive: true });
6561
+ await writeFile14(absPath, content, "utf8");
6126
6562
  created.push(relPath);
6127
6563
  }
6128
6564
  }
6129
- const rawPath = join34(input.vault, "raw", "articles", "example-source.md");
6565
+ const rawPath = join36(input.vault, "raw", "articles", "example-source.md");
6130
6566
  try {
6131
6567
  await stat7(rawPath);
6132
6568
  skipped.push("raw/articles/example-source.md");
6133
6569
  } catch {
6134
- await mkdir13(join34(rawPath, ".."), { recursive: true });
6135
- await writeFile18(rawPath, EXAMPLE_RAW, "utf8");
6570
+ await mkdir13(join36(rawPath, ".."), { recursive: true });
6571
+ await writeFile14(rawPath, EXAMPLE_RAW, "utf8");
6136
6572
  created.push("raw/articles/example-source.md");
6137
6573
  }
6138
6574
  if (created.length > 0) {
@@ -6154,9 +6590,9 @@ async function runSeed(input) {
6154
6590
  }
6155
6591
 
6156
6592
  // src/commands/canvas.ts
6157
- import { readFile as readFile22, writeFile as writeFile19 } from "fs/promises";
6158
- import { existsSync as existsSync12 } from "fs";
6159
- import { join as join35 } from "path";
6593
+ import { readFile as readFile24, writeFile as writeFile15 } from "fs/promises";
6594
+ import { existsSync as existsSync13 } from "fs";
6595
+ import { join as join37 } from "path";
6160
6596
  var NODE_WIDTH = 240;
6161
6597
  var NODE_HEIGHT = 60;
6162
6598
  var COLUMN_SPACING = 400;
@@ -6234,8 +6670,8 @@ function buildCanvasEdges(adjacency) {
6234
6670
  return edges;
6235
6671
  }
6236
6672
  async function runCanvasGenerate(input) {
6237
- const graphPath = input.graphPath ?? join35(input.vault, ".skillwiki", "graph.json");
6238
- if (!existsSync12(graphPath)) {
6673
+ const graphPath = input.graphPath ?? join37(input.vault, ".skillwiki", "graph.json");
6674
+ if (!existsSync13(graphPath)) {
6239
6675
  return {
6240
6676
  exitCode: ExitCode.FILE_NOT_FOUND,
6241
6677
  result: err("FILE_NOT_FOUND", {
@@ -6246,7 +6682,7 @@ async function runCanvasGenerate(input) {
6246
6682
  }
6247
6683
  let raw;
6248
6684
  try {
6249
- raw = await readFile22(graphPath, "utf8");
6685
+ raw = await readFile24(graphPath, "utf8");
6250
6686
  } catch (e) {
6251
6687
  return {
6252
6688
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -6272,9 +6708,9 @@ async function runCanvasGenerate(input) {
6272
6708
  const nodes = buildCanvasNodes(paths);
6273
6709
  const edges = buildCanvasEdges(graph.adjacency);
6274
6710
  const canvas = { nodes, edges };
6275
- const outPath = join35(input.vault, "vault-graph.canvas");
6711
+ const outPath = join37(input.vault, "vault-graph.canvas");
6276
6712
  try {
6277
- await writeFile19(outPath, JSON.stringify(canvas, null, 2));
6713
+ await writeFile15(outPath, JSON.stringify(canvas, null, 2));
6278
6714
  } catch (e) {
6279
6715
  return {
6280
6716
  exitCode: ExitCode.WRITE_FAILED,
@@ -6294,8 +6730,8 @@ written: ${outPath}`
6294
6730
  }
6295
6731
 
6296
6732
  // src/commands/query.ts
6297
- import { readFile as readFile23, stat as stat8 } from "fs/promises";
6298
- import { join as join36 } from "path";
6733
+ import { readFile as readFile25, stat as stat8 } from "fs/promises";
6734
+ import { join as join38 } from "path";
6299
6735
  var W_KEYWORD = 2;
6300
6736
  var W_SOURCE_OVERLAP = 4;
6301
6737
  var W_WIKILINK = 3;
@@ -6416,7 +6852,7 @@ function computeKeywordScore(terms, title, tags, body) {
6416
6852
  return score;
6417
6853
  }
6418
6854
  async function loadOrBuildGraph(vault) {
6419
- const graphPath = join36(vault, ".skillwiki", "graph.json");
6855
+ const graphPath = join38(vault, ".skillwiki", "graph.json");
6420
6856
  let needsBuild = false;
6421
6857
  try {
6422
6858
  const fileStat = await stat8(graphPath);
@@ -6430,7 +6866,7 @@ async function loadOrBuildGraph(vault) {
6430
6866
  if (buildResult.exitCode !== 0) return null;
6431
6867
  }
6432
6868
  try {
6433
- const raw = await readFile23(graphPath, "utf8");
6869
+ const raw = await readFile25(graphPath, "utf8");
6434
6870
  return JSON.parse(raw);
6435
6871
  } catch {
6436
6872
  return null;
@@ -6438,14 +6874,14 @@ async function loadOrBuildGraph(vault) {
6438
6874
  }
6439
6875
 
6440
6876
  // src/utils/auto-commit.ts
6441
- import { existsSync as existsSync13 } from "fs";
6442
- import { join as join37 } from "path";
6877
+ import { existsSync as existsSync14 } from "fs";
6878
+ import { join as join39 } from "path";
6443
6879
  async function postCommit(vault, exitCode) {
6444
6880
  if (exitCode !== 0) return;
6445
6881
  const home = process.env.HOME ?? "";
6446
6882
  const dotenv = await parseDotenvFile(configPath(home));
6447
6883
  if (dotenv["AUTO_COMMIT"] === "false") return;
6448
- if (!existsSync13(join37(vault, ".git"))) return;
6884
+ if (!existsSync14(join39(vault, ".git"))) return;
6449
6885
  const lastOps = readLastOp(vault);
6450
6886
  if (lastOps.length === 0) return;
6451
6887
  const porcelain = git(vault, ["status", "--porcelain"]);
@@ -6496,7 +6932,7 @@ program.command("validate <file>").description("validate vault page frontmatter
6496
6932
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
6497
6933
  });
6498
6934
  program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
6499
- const out = opts.out ?? join38(vault, ".skillwiki", "graph.json");
6935
+ const out = opts.out ?? join40(vault, ".skillwiki", "graph.json");
6500
6936
  emit(await runGraphBuild({ vault, out }), vault);
6501
6937
  });
6502
6938
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");