skillwiki 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 join39 } 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) : {};
@@ -3348,13 +3440,13 @@ async function runConfigPath(input) {
3348
3440
 
3349
3441
  // src/commands/doctor.ts
3350
3442
  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";
3443
+ import { join as join23, resolve as resolve4 } from "path";
3352
3444
  import { execSync } from "child_process";
3353
3445
  import { platform } from "os";
3354
3446
 
3355
3447
  // src/utils/auto-update.ts
3356
3448
  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";
3449
+ import { join as join21, dirname as dirname8 } from "path";
3358
3450
  import { spawn } from "child_process";
3359
3451
 
3360
3452
  // src/utils/update-consts.ts
@@ -3365,7 +3457,7 @@ var CLI_DISABLE_FLAG = "--no-update-notifier";
3365
3457
 
3366
3458
  // src/utils/auto-update.ts
3367
3459
  function cachePath(home) {
3368
- return join20(home, ".skillwiki", CACHE_FILENAME);
3460
+ return join21(home, ".skillwiki", CACHE_FILENAME);
3369
3461
  }
3370
3462
  function readCacheRaw(home) {
3371
3463
  try {
@@ -3384,7 +3476,7 @@ function readCache(home) {
3384
3476
  }
3385
3477
  function writeCache(home, cache) {
3386
3478
  const p = cachePath(home);
3387
- mkdirSync2(dirname7(p), { recursive: true });
3479
+ mkdirSync2(dirname8(p), { recursive: true });
3388
3480
  writeFileSync3(p, JSON.stringify(cache, null, 2));
3389
3481
  }
3390
3482
  function latestFromCache(home, currentVersion) {
@@ -3415,12 +3507,12 @@ function triggerAutoUpdate(home, currentVersion) {
3415
3507
 
3416
3508
  // src/utils/plugin-registry.ts
3417
3509
  import { readFileSync as readFileSync5 } from "fs";
3418
- import { join as join21 } from "path";
3419
- var REGISTRY_PATH = join21(".claude", "plugins", "installed_plugins.json");
3510
+ import { join as join22 } from "path";
3511
+ var REGISTRY_PATH = join22(".claude", "plugins", "installed_plugins.json");
3420
3512
  var PLUGIN_KEY = "skillwiki@llm-wiki";
3421
3513
  function readInstalledPlugins(home) {
3422
3514
  try {
3423
- const raw = readFileSync5(join21(home, REGISTRY_PATH), "utf8");
3515
+ const raw = readFileSync5(join22(home, REGISTRY_PATH), "utf8");
3424
3516
  return JSON.parse(raw);
3425
3517
  } catch {
3426
3518
  return null;
@@ -3463,12 +3555,12 @@ function detectCliChannels(argv, home) {
3463
3555
  }
3464
3556
  const plugin = findPlugin(home);
3465
3557
  if (plugin) {
3466
- const pluginBin = join22(plugin.installPath, "bin", "skillwiki");
3558
+ const pluginBin = join23(plugin.installPath, "bin", "skillwiki");
3467
3559
  if (existsSync6(pluginBin)) {
3468
3560
  channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
3469
3561
  }
3470
3562
  }
3471
- const installBin = join22(home, ".claude", "skills", "bin", "skillwiki");
3563
+ const installBin = join23(home, ".claude", "skills", "bin", "skillwiki");
3472
3564
  if (existsSync6(installBin)) {
3473
3565
  channels.push({ name: "install", path: installBin, isDevLink: false });
3474
3566
  }
@@ -3549,9 +3641,9 @@ function checkVaultStructure(resolvedPath) {
3549
3641
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
3550
3642
  }
3551
3643
  const missing = [];
3552
- if (!existsSync6(join22(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
3644
+ if (!existsSync6(join23(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
3553
3645
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
3554
- if (!existsSync6(join22(resolvedPath, dir))) missing.push(dir + "/");
3646
+ if (!existsSync6(join23(resolvedPath, dir))) missing.push(dir + "/");
3555
3647
  }
3556
3648
  if (missing.length === 0) {
3557
3649
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -3559,7 +3651,7 @@ function checkVaultStructure(resolvedPath) {
3559
3651
  return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
3560
3652
  }
3561
3653
  function checkSkillsInstalled(home, cwd) {
3562
- const srcDir = cwd ? join22(cwd, "packages", "skills") : void 0;
3654
+ const srcDir = cwd ? join23(cwd, "packages", "skills") : void 0;
3563
3655
  if (srcDir && existsSync6(srcDir)) {
3564
3656
  const found = findSkillMd(srcDir);
3565
3657
  if (found.length > 0) {
@@ -3573,7 +3665,7 @@ function checkSkillsInstalled(home, cwd) {
3573
3665
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
3574
3666
  }
3575
3667
  }
3576
- const skillsDir = join22(home, ".claude", "skills");
3668
+ const skillsDir = join23(home, ".claude", "skills");
3577
3669
  if (existsSync6(skillsDir)) {
3578
3670
  const found = findSkillMd(skillsDir);
3579
3671
  if (found.length > 0) {
@@ -3584,10 +3676,10 @@ function checkSkillsInstalled(home, cwd) {
3584
3676
  }
3585
3677
  function checkDuplicateSkills(home) {
3586
3678
  const plugin = findPlugin(home);
3587
- const skillsDir = join22(home, ".claude", "skills");
3679
+ const skillsDir = join23(home, ".claude", "skills");
3588
3680
  const agentSkillDirs = [
3589
- { label: "~/.codex/skills/", path: join22(home, ".codex", "skills") },
3590
- { label: "~/.agents/skills/", path: join22(home, ".agents", "skills") }
3681
+ { label: "~/.codex/skills/", path: join23(home, ".codex", "skills") },
3682
+ { label: "~/.agents/skills/", path: join23(home, ".agents", "skills") }
3591
3683
  ];
3592
3684
  if (!plugin) {
3593
3685
  return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
@@ -3664,7 +3756,7 @@ async function checkProfiles(home) {
3664
3756
  }
3665
3757
  async function checkProjectLocalOverride(cwd) {
3666
3758
  const dir = cwd ?? process.cwd();
3667
- const envPath = join22(dir, ".skillwiki", ".env");
3759
+ const envPath = join23(dir, ".skillwiki", ".env");
3668
3760
  if (existsSync6(envPath)) {
3669
3761
  return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
3670
3762
  }
@@ -3674,7 +3766,7 @@ function checkVaultGitRemote(resolvedPath) {
3674
3766
  if (resolvedPath === void 0) {
3675
3767
  return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
3676
3768
  }
3677
- if (!existsSync6(join22(resolvedPath, ".git"))) {
3769
+ if (!existsSync6(join23(resolvedPath, ".git"))) {
3678
3770
  return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
3679
3771
  }
3680
3772
  try {
@@ -3697,9 +3789,9 @@ function checkObsidianTemplates(resolvedPath) {
3697
3789
  return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
3698
3790
  }
3699
3791
  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");
3792
+ if (!existsSync6(join23(resolvedPath, "_Templates"))) missing.push("_Templates/");
3793
+ if (!existsSync6(join23(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
3794
+ if (!existsSync6(join23(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
3703
3795
  if (missing.length === 0) {
3704
3796
  return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
3705
3797
  }
@@ -3709,7 +3801,7 @@ function checkDotStoreClean(resolvedPath) {
3709
3801
  if (resolvedPath === void 0) {
3710
3802
  return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
3711
3803
  }
3712
- const rawDir = join22(resolvedPath, "raw");
3804
+ const rawDir = join23(resolvedPath, "raw");
3713
3805
  if (!existsSync6(rawDir)) {
3714
3806
  return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
3715
3807
  }
@@ -3725,7 +3817,7 @@ function checkDotStoreClean(resolvedPath) {
3725
3817
  if (entry.name === ".DS_Store") {
3726
3818
  found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
3727
3819
  } else if (entry.isDirectory()) {
3728
- walk2(join22(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
3820
+ walk2(join23(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
3729
3821
  }
3730
3822
  }
3731
3823
  })(rawDir, "");
@@ -3738,7 +3830,7 @@ function checkSyncLastPush(resolvedPath) {
3738
3830
  if (resolvedPath === void 0) {
3739
3831
  return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
3740
3832
  }
3741
- if (!existsSync6(join22(resolvedPath, ".git"))) {
3833
+ if (!existsSync6(join23(resolvedPath, ".git"))) {
3742
3834
  return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
3743
3835
  }
3744
3836
  let timestamp;
@@ -3812,7 +3904,7 @@ function checkS3MountPerf(resolvedPath) {
3812
3904
  if (!mountPoint) {
3813
3905
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
3814
3906
  }
3815
- const conceptsDir = join22(resolvedPath, "concepts");
3907
+ const conceptsDir = join23(resolvedPath, "concepts");
3816
3908
  if (!existsSync6(conceptsDir)) {
3817
3909
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
3818
3910
  }
@@ -3862,9 +3954,9 @@ function findSkillMd(dir) {
3862
3954
  }
3863
3955
  for (const entry of entries) {
3864
3956
  if (entry.isFile() && entry.name === "SKILL.md") {
3865
- results.push(join22(dir, entry.name));
3957
+ results.push(join23(dir, entry.name));
3866
3958
  } else if (entry.isDirectory()) {
3867
- results.push(...findSkillMd(join22(dir, entry.name)));
3959
+ results.push(...findSkillMd(join23(dir, entry.name)));
3868
3960
  }
3869
3961
  }
3870
3962
  return results;
@@ -3878,7 +3970,7 @@ function findSkillNames(dir) {
3878
3970
  return results;
3879
3971
  }
3880
3972
  for (const entry of entries) {
3881
- if (entry.isDirectory() && existsSync6(join22(dir, entry.name, "SKILL.md"))) {
3973
+ if (entry.isDirectory() && existsSync6(join23(dir, entry.name, "SKILL.md"))) {
3882
3974
  results.push(entry.name);
3883
3975
  }
3884
3976
  }
@@ -3932,8 +4024,8 @@ async function runDoctor(input) {
3932
4024
  }
3933
4025
 
3934
4026
  // 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";
4027
+ import { rename as rename5, mkdir as mkdir8, readFile as readFile17, writeFile as writeFile9 } from "fs/promises";
4028
+ import { join as join24, dirname as dirname9 } from "path";
3937
4029
  async function runArchive(input) {
3938
4030
  const scan = await scanVault(input.vault);
3939
4031
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -3949,13 +4041,13 @@ async function runArchive(input) {
3949
4041
  }
3950
4042
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
3951
4043
  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 });
4044
+ const archivePath = join24("_archive", relPath).replace(/\\/g, "/");
4045
+ await mkdir8(dirname9(join24(input.vault, archivePath)), { recursive: true });
3954
4046
  let indexUpdated = false;
3955
4047
  if (!isRaw) {
3956
- const indexPath = join23(input.vault, "index.md");
4048
+ const indexPath = join24(input.vault, "index.md");
3957
4049
  try {
3958
- const idx = await readFile16(indexPath, "utf8");
4050
+ const idx = await readFile17(indexPath, "utf8");
3959
4051
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
3960
4052
  const originalLines = idx.split("\n");
3961
4053
  const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
@@ -3967,7 +4059,7 @@ async function runArchive(input) {
3967
4059
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
3968
4060
  }
3969
4061
  }
3970
- await rename4(join23(input.vault, relPath), join23(input.vault, archivePath));
4062
+ await rename5(join24(input.vault, relPath), join24(input.vault, archivePath));
3971
4063
  appendLastOp(input.vault, {
3972
4064
  operation: "archive",
3973
4065
  summary: `moved ${relPath} to ${archivePath}`,
@@ -3979,7 +4071,6 @@ async function runArchive(input) {
3979
4071
 
3980
4072
  // src/commands/drift.ts
3981
4073
  import { createHash as createHash3 } from "crypto";
3982
- import { writeFile as writeFile10 } from "fs/promises";
3983
4074
 
3984
4075
  // src/utils/fetch.ts
3985
4076
  async function controlledFetch(url, opts) {
@@ -4066,7 +4157,7 @@ async function runDrift(input) {
4066
4157
  ${newFm}
4067
4158
  ---
4068
4159
  ${body}`;
4069
- await writeFile10(raw.absPath, newText, "utf8");
4160
+ await safeWritePage(raw.absPath, newText);
4070
4161
  results.push({
4071
4162
  raw_path: raw.relPath,
4072
4163
  source_url: sourceUrl,
@@ -4109,7 +4200,6 @@ ${body}`;
4109
4200
  }
4110
4201
 
4111
4202
  // src/commands/migrate-citations.ts
4112
- import { writeFile as writeFile11 } from "fs/promises";
4113
4203
  var MARKER_RE2 = /\^\[(raw\/[^\]]+)\]/g;
4114
4204
  function moveMarkersToParagraphEnd(body) {
4115
4205
  const lines = body.split("\n");
@@ -4232,7 +4322,11 @@ ${migratedBody}${newFooter}`;
4232
4322
  continue;
4233
4323
  }
4234
4324
  if (!input.dryRun) {
4235
- await writeFile11(page.absPath, newText, "utf8");
4325
+ const w = await safeWritePage(page.absPath, newText);
4326
+ if (!w.ok) {
4327
+ skipped.push(page.relPath);
4328
+ continue;
4329
+ }
4236
4330
  }
4237
4331
  migrated.push(page.relPath);
4238
4332
  }
@@ -4262,7 +4356,6 @@ ${migratedBody}${newFooter}`;
4262
4356
  }
4263
4357
 
4264
4358
  // src/commands/frontmatter-fix.ts
4265
- import { writeFile as writeFile12 } from "fs/promises";
4266
4359
  function isoToday() {
4267
4360
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4268
4361
  }
@@ -4304,7 +4397,11 @@ ${newBody}`;
4304
4397
  continue;
4305
4398
  }
4306
4399
  if (!input.dryRun) {
4307
- await writeFile12(page.absPath, newText, "utf8");
4400
+ const w = await safeWritePage(page.absPath, newText);
4401
+ if (!w.ok) {
4402
+ skipped.push(page.relPath);
4403
+ continue;
4404
+ }
4308
4405
  }
4309
4406
  fixed.push(page.relPath);
4310
4407
  }
@@ -4337,14 +4434,14 @@ ${newBody}`;
4337
4434
  // src/commands/update.ts
4338
4435
  import { execSync as execSync2 } from "child_process";
4339
4436
  import { readFileSync as readFileSync7 } from "fs";
4340
- import { join as join24 } from "path";
4437
+ import { join as join25 } from "path";
4341
4438
  function resolveGlobalSkillsRoot() {
4342
4439
  try {
4343
4440
  const globalRoot = execSync2("npm root -g", {
4344
4441
  encoding: "utf8",
4345
4442
  timeout: 5e3
4346
4443
  }).trim();
4347
- return join24(globalRoot, "skillwiki", "skills");
4444
+ return join25(globalRoot, "skillwiki", "skills");
4348
4445
  } catch {
4349
4446
  return null;
4350
4447
  }
@@ -4370,7 +4467,7 @@ async function runUpdate(input) {
4370
4467
  );
4371
4468
  const currentVersion = pkg2.version;
4372
4469
  const tag = input.distTag ?? "latest";
4373
- const target = join24(input.home, ".claude", "skills");
4470
+ const target = join25(input.home, ".claude", "skills");
4374
4471
  let latest;
4375
4472
  try {
4376
4473
  latest = execSync2(`npm view skillwiki@${tag} version`, {
@@ -4441,14 +4538,14 @@ async function runUpdate(input) {
4441
4538
  // src/commands/self-update.ts
4442
4539
  import { execSync as execSync3 } from "child_process";
4443
4540
  import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
4444
- import { join as join25 } from "path";
4541
+ import { join as join26 } from "path";
4445
4542
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
4446
4543
  async function runSelfUpdate(input) {
4447
4544
  const currentVersion = JSON.parse(
4448
4545
  readFileSync8(new URL("../../package.json", import.meta.url), "utf8")
4449
4546
  ).version;
4450
4547
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
4451
- const localPkgPath = join25(sourceRoot, "packages", "cli", "package.json");
4548
+ const localPkgPath = join26(sourceRoot, "packages", "cli", "package.json");
4452
4549
  const hasLocalSource = existsSync7(localPkgPath);
4453
4550
  if (input.check) {
4454
4551
  let availableVersion = null;
@@ -4580,10 +4677,10 @@ async function runSelfUpdate(input) {
4580
4677
  }
4581
4678
 
4582
4679
  // 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";
4680
+ import { readdir as readdir5, stat as stat6, readFile as readFile18 } from "fs/promises";
4681
+ import { join as join27 } from "path";
4585
4682
  async function runTranscripts(input) {
4586
- const dir = join26(input.vault, "raw", "transcripts");
4683
+ const dir = join27(input.vault, "raw", "transcripts");
4587
4684
  let entries;
4588
4685
  try {
4589
4686
  entries = await readdir5(dir, { withFileTypes: true });
@@ -4593,8 +4690,8 @@ async function runTranscripts(input) {
4593
4690
  const transcripts = [];
4594
4691
  for (const entry of entries) {
4595
4692
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4596
- const filePath = join26(dir, entry.name);
4597
- const content = await readFile17(filePath, "utf8");
4693
+ const filePath = join27(dir, entry.name);
4694
+ const content = await readFile18(filePath, "utf8");
4598
4695
  const fm = extractFrontmatter(content);
4599
4696
  if (!fm.ok) continue;
4600
4697
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
@@ -4611,12 +4708,12 @@ async function runTranscripts(input) {
4611
4708
  }
4612
4709
 
4613
4710
  // 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";
4711
+ import { readdir as readdir6, readFile as readFile19, writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
4712
+ import { join as join28, dirname as dirname10 } from "path";
4616
4713
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
4617
4714
  async function runProjectIndex(input) {
4618
4715
  const slug = input.slug;
4619
- const projectDir = join27(input.vault, "projects", slug);
4716
+ const projectDir = join28(input.vault, "projects", slug);
4620
4717
  try {
4621
4718
  await readdir6(projectDir);
4622
4719
  } catch {
@@ -4627,15 +4724,15 @@ async function runProjectIndex(input) {
4627
4724
  }
4628
4725
  const wikilinkPattern = `[[${slug}]]`;
4629
4726
  const entries = [];
4630
- const compoundDir = join27(input.vault, "projects", slug, "compound");
4727
+ const compoundDir = join28(input.vault, "projects", slug, "compound");
4631
4728
  try {
4632
4729
  const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
4633
4730
  for (const entry of compoundFiles) {
4634
4731
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4635
- const filePath = join27(compoundDir, entry.name);
4732
+ const filePath = join28(compoundDir, entry.name);
4636
4733
  let text;
4637
4734
  try {
4638
- text = await readFile18(filePath, "utf8");
4735
+ text = await readFile19(filePath, "utf8");
4639
4736
  } catch {
4640
4737
  continue;
4641
4738
  }
@@ -4652,16 +4749,16 @@ async function runProjectIndex(input) {
4652
4749
  for (const dir of LAYER2_DIRS) {
4653
4750
  let files;
4654
4751
  try {
4655
- files = await readdir6(join27(input.vault, dir), { withFileTypes: true });
4752
+ files = await readdir6(join28(input.vault, dir), { withFileTypes: true });
4656
4753
  } catch {
4657
4754
  continue;
4658
4755
  }
4659
4756
  for (const entry of files) {
4660
4757
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
4661
- const filePath = join27(input.vault, dir, entry.name);
4758
+ const filePath = join28(input.vault, dir, entry.name);
4662
4759
  let text;
4663
4760
  try {
4664
- text = await readFile18(filePath, "utf8");
4761
+ text = await readFile19(filePath, "utf8");
4665
4762
  } catch {
4666
4763
  continue;
4667
4764
  }
@@ -4682,11 +4779,11 @@ async function runProjectIndex(input) {
4682
4779
  const tb = typeOrder[b.type] ?? 99;
4683
4780
  return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
4684
4781
  });
4685
- const indexPath = join27(projectDir, "knowledge.md");
4782
+ const indexPath = join28(projectDir, "knowledge.md");
4686
4783
  let existing = false;
4687
4784
  let stale = false;
4688
4785
  try {
4689
- const existingText = await readFile18(indexPath, "utf8");
4786
+ const existingText = await readFile19(indexPath, "utf8");
4690
4787
  existing = true;
4691
4788
  const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
4692
4789
  const existingPages = new Set(existingEntries.map((l) => {
@@ -4726,8 +4823,8 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
4726
4823
  }
4727
4824
  if (input.apply) {
4728
4825
  try {
4729
- await mkdir9(dirname9(indexPath), { recursive: true });
4730
- await writeFile13(indexPath, body, "utf8");
4826
+ await mkdir9(dirname10(indexPath), { recursive: true });
4827
+ await writeFile10(indexPath, body, "utf8");
4731
4828
  } catch (e) {
4732
4829
  return {
4733
4830
  exitCode: ExitCode.WRITE_FAILED,
@@ -4755,10 +4852,10 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
4755
4852
  }
4756
4853
 
4757
4854
  // 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";
4855
+ import { writeFile as writeFile11, mkdir as mkdir10, readdir as readdir7, unlink as unlink3 } from "fs/promises";
4856
+ import { join as join29 } from "path";
4760
4857
  import { existsSync as existsSync8 } from "fs";
4761
- import { readFile as readFile19 } from "fs/promises";
4858
+ import { readFile as readFile20 } from "fs/promises";
4762
4859
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
4763
4860
  var FIELD_RE = {
4764
4861
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -4856,17 +4953,17 @@ function extractRetroFields(date, cycleName, block) {
4856
4953
  };
4857
4954
  }
4858
4955
  async function runCompound(input) {
4859
- const logPath = join28(input.vault, "log.md");
4956
+ const logPath = join29(input.vault, "log.md");
4860
4957
  let logText;
4861
4958
  try {
4862
- logText = await readFile19(logPath, "utf8");
4959
+ logText = await readFile20(logPath, "utf8");
4863
4960
  } catch {
4864
4961
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
4865
4962
  }
4866
4963
  const entries = parseRetroEntries(logText);
4867
4964
  const promoted = [];
4868
4965
  const skipped = [];
4869
- const compoundDir = join28(input.vault, "projects", input.project, "compound");
4966
+ const compoundDir = join29(input.vault, "projects", input.project, "compound");
4870
4967
  for (const entry of entries) {
4871
4968
  const generalizeValue = entry.generalize.trim();
4872
4969
  if (!/^yes/i.test(generalizeValue)) {
@@ -4874,7 +4971,7 @@ async function runCompound(input) {
4874
4971
  continue;
4875
4972
  }
4876
4973
  const slug = slugify(entry.cycleName);
4877
- const compoundPath = join28(compoundDir, `${slug}.md`);
4974
+ const compoundPath = join29(compoundDir, `${slug}.md`);
4878
4975
  if (existsSync8(compoundPath)) {
4879
4976
  skipped.push(entry.date);
4880
4977
  continue;
@@ -4916,7 +5013,7 @@ async function runCompound(input) {
4916
5013
  if (!existsSync8(compoundDir)) {
4917
5014
  await mkdir10(compoundDir, { recursive: true });
4918
5015
  }
4919
- await writeFile14(compoundPath, content, "utf8");
5016
+ await writeFile11(compoundPath, content, "utf8");
4920
5017
  }
4921
5018
  promoted.push(`${slug}.md`);
4922
5019
  }
@@ -4935,7 +5032,7 @@ async function runCompound(input) {
4935
5032
  };
4936
5033
  }
4937
5034
  async function runCompoundDelete(input) {
4938
- const projectDir = join28(input.vault, "projects", input.project);
5035
+ const projectDir = join29(input.vault, "projects", input.project);
4939
5036
  if (!existsSync8(projectDir)) {
4940
5037
  return {
4941
5038
  exitCode: ExitCode.PROJECT_NOT_FOUND,
@@ -4943,7 +5040,7 @@ async function runCompoundDelete(input) {
4943
5040
  };
4944
5041
  }
4945
5042
  const entryName = input.entry.replace(/\.md$/, "");
4946
- const compoundPath = join28(projectDir, "compound", `${entryName}.md`);
5043
+ const compoundPath = join29(projectDir, "compound", `${entryName}.md`);
4947
5044
  if (!existsSync8(compoundPath)) {
4948
5045
  return {
4949
5046
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -4951,7 +5048,7 @@ async function runCompoundDelete(input) {
4951
5048
  };
4952
5049
  }
4953
5050
  try {
4954
- await unlink2(compoundPath);
5051
+ await unlink3(compoundPath);
4955
5052
  } catch (e) {
4956
5053
  return {
4957
5054
  exitCode: ExitCode.WRITE_FAILED,
@@ -4977,7 +5074,7 @@ knowledge.md regenerated`
4977
5074
  };
4978
5075
  }
4979
5076
  async function runCompoundList(input) {
4980
- const compoundDir = join28(input.vault, "projects", input.project, "compound");
5077
+ const compoundDir = join29(input.vault, "projects", input.project, "compound");
4981
5078
  if (!existsSync8(compoundDir)) {
4982
5079
  return {
4983
5080
  exitCode: ExitCode.OK,
@@ -5008,10 +5105,10 @@ could not read compound directory`
5008
5105
  const entries = [];
5009
5106
  for (const dirent of dirents) {
5010
5107
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
5011
- const filePath = join28(compoundDir, dirent.name);
5108
+ const filePath = join29(compoundDir, dirent.name);
5012
5109
  let text;
5013
5110
  try {
5014
- text = await readFile19(filePath, "utf8");
5111
+ text = await readFile20(filePath, "utf8");
5015
5112
  } catch {
5016
5113
  continue;
5017
5114
  }
@@ -5040,9 +5137,9 @@ no compound entries found`;
5040
5137
  }
5041
5138
 
5042
5139
  // src/commands/observe.ts
5043
- import { mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
5140
+ import { mkdir as mkdir11, writeFile as writeFile12 } from "fs/promises";
5044
5141
  import { existsSync as existsSync9, statSync as statSync3 } from "fs";
5045
- import { join as join29 } from "path";
5142
+ import { join as join30 } from "path";
5046
5143
  import { createHash as createHash4 } from "crypto";
5047
5144
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
5048
5145
  function slugify2(text) {
@@ -5071,7 +5168,7 @@ async function runObserve(input) {
5071
5168
  result: err("VAULT_PATH_INVALID", { path: input.vault })
5072
5169
  };
5073
5170
  }
5074
- const transcriptsDir = join29(input.vault, "raw", "transcripts");
5171
+ const transcriptsDir = join30(input.vault, "raw", "transcripts");
5075
5172
  try {
5076
5173
  await mkdir11(transcriptsDir, { recursive: true });
5077
5174
  } catch {
@@ -5083,7 +5180,7 @@ async function runObserve(input) {
5083
5180
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5084
5181
  const slug = slugify2(input.text);
5085
5182
  const fileName = `${today}-observation-${slug}.md`;
5086
- const filePath = join29(transcriptsDir, fileName);
5183
+ const filePath = join30(transcriptsDir, fileName);
5087
5184
  const body = `
5088
5185
  ${input.text.trim()}
5089
5186
  `;
@@ -5101,7 +5198,7 @@ ${input.text.trim()}
5101
5198
  frontmatterLines.push("---");
5102
5199
  const content = frontmatterLines.join("\n") + body;
5103
5200
  try {
5104
- await writeFile15(filePath, content, "utf8");
5201
+ await writeFile12(filePath, content, "utf8");
5105
5202
  } catch (e) {
5106
5203
  return {
5107
5204
  exitCode: ExitCode.WRITE_FAILED,
@@ -5123,8 +5220,8 @@ ${input.text.trim()}
5123
5220
  }
5124
5221
 
5125
5222
  // 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";
5223
+ import { readFile as readFile21, writeFile as writeFile13, mkdir as mkdir12 } from "fs/promises";
5224
+ import { join as join31 } from "path";
5128
5225
  import { createHash as createHash5 } from "crypto";
5129
5226
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
5130
5227
  var TYPE_DIR = {
@@ -5283,7 +5380,7 @@ async function runIngest(input) {
5283
5380
  sourceContent = fetchResult.data.body;
5284
5381
  } else {
5285
5382
  try {
5286
- sourceContent = await readFile20(input.source, "utf8");
5383
+ sourceContent = await readFile21(input.source, "utf8");
5287
5384
  } catch {
5288
5385
  return {
5289
5386
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -5298,8 +5395,8 @@ async function runIngest(input) {
5298
5395
  const rawRelPath = `raw/articles/${slug}.md`;
5299
5396
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
5300
5397
  const typedRelPath = `${typedDir}/${slug}.md`;
5301
- const rawAbsPath = join30(input.vault, rawRelPath);
5302
- const typedAbsPath = join30(input.vault, typedRelPath);
5398
+ const rawAbsPath = join31(input.vault, rawRelPath);
5399
+ const typedAbsPath = join31(input.vault, typedRelPath);
5303
5400
  const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
5304
5401
  const typedContent = buildTypedContent(
5305
5402
  input.title,
@@ -5362,8 +5459,8 @@ async function runIngest(input) {
5362
5459
  };
5363
5460
  }
5364
5461
  try {
5365
- await mkdir12(join30(input.vault, "raw", "articles"), { recursive: true });
5366
- await writeFile16(rawAbsPath, rawContent, "utf8");
5462
+ await mkdir12(join31(input.vault, "raw", "articles"), { recursive: true });
5463
+ await writeFile13(rawAbsPath, rawContent, "utf8");
5367
5464
  } catch (e) {
5368
5465
  return {
5369
5466
  exitCode: ExitCode.WRITE_FAILED,
@@ -5371,8 +5468,8 @@ async function runIngest(input) {
5371
5468
  };
5372
5469
  }
5373
5470
  try {
5374
- await mkdir12(join30(input.vault, typedDir), { recursive: true });
5375
- await writeFile16(typedAbsPath, typedContent, "utf8");
5471
+ await mkdir12(join31(input.vault, typedDir), { recursive: true });
5472
+ await writeFile13(typedAbsPath, typedContent, "utf8");
5376
5473
  } catch (e) {
5377
5474
  return {
5378
5475
  exitCode: ExitCode.WRITE_FAILED,
@@ -5403,7 +5500,6 @@ async function runIngest(input) {
5403
5500
  }
5404
5501
 
5405
5502
  // src/commands/tag-sync.ts
5406
- import { writeFile as writeFile17 } from "fs/promises";
5407
5503
  var ENUM_MIRRORS = {
5408
5504
  provenance: ["research", "project", "mixed"],
5409
5505
  confidence: ["high", "medium", "low"]
@@ -5518,7 +5614,11 @@ ${newFm}
5518
5614
  ---
5519
5615
  ${body}`;
5520
5616
  if (!input.dryRun) {
5521
- await writeFile17(page.absPath, newText, "utf8");
5617
+ const w = await safeWritePage(page.absPath, newText);
5618
+ if (!w.ok) {
5619
+ unchanged++;
5620
+ continue;
5621
+ }
5522
5622
  }
5523
5623
  synced.push(page.relPath);
5524
5624
  }
@@ -5548,10 +5648,10 @@ ${body}`;
5548
5648
 
5549
5649
  // src/commands/sync.ts
5550
5650
  import { existsSync as existsSync10 } from "fs";
5551
- import { join as join31 } from "path";
5651
+ import { join as join32 } from "path";
5552
5652
  function runSyncStatus(input) {
5553
5653
  const vault = input.vault;
5554
- if (!existsSync10(join31(vault, ".git"))) {
5654
+ if (!existsSync10(join32(vault, ".git"))) {
5555
5655
  return {
5556
5656
  exitCode: ExitCode.VAULT_PATH_INVALID,
5557
5657
  result: ok({
@@ -5620,7 +5720,7 @@ function runSyncStatus(input) {
5620
5720
  }
5621
5721
  async function runSyncPush(input) {
5622
5722
  const vault = input.vault;
5623
- if (!existsSync10(join31(vault, ".git"))) {
5723
+ if (!existsSync10(join32(vault, ".git"))) {
5624
5724
  return {
5625
5725
  exitCode: ExitCode.VAULT_PATH_INVALID,
5626
5726
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -5705,7 +5805,7 @@ async function runSyncPush(input) {
5705
5805
  }
5706
5806
  async function runSyncPull(input) {
5707
5807
  const vault = input.vault;
5708
- if (!existsSync10(join31(vault, ".git"))) {
5808
+ if (!existsSync10(join32(vault, ".git"))) {
5709
5809
  return {
5710
5810
  exitCode: ExitCode.VAULT_PATH_INVALID,
5711
5811
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -5781,7 +5881,7 @@ async function runSyncPull(input) {
5781
5881
 
5782
5882
  // src/commands/backup.ts
5783
5883
  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";
5884
+ import { join as join33, relative as relative3, dirname as dirname11 } from "path";
5785
5885
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
5786
5886
 
5787
5887
  // src/utils/s3-client.ts
@@ -5805,7 +5905,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_
5805
5905
  function* walkMarkdown(dir, base) {
5806
5906
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
5807
5907
  if (SKIP_DIRS.has(entry.name)) continue;
5808
- const full = join32(dir, entry.name);
5908
+ const full = join33(dir, entry.name);
5809
5909
  if (entry.isDirectory()) {
5810
5910
  yield* walkMarkdown(full, base);
5811
5911
  } else if (entry.name.endsWith(".md")) {
@@ -5828,7 +5928,7 @@ async function runBackupSync(input) {
5828
5928
  let failed = 0;
5829
5929
  const files = [...walkMarkdown(input.vault, input.vault)];
5830
5930
  for (const relPath of files) {
5831
- const absPath = join32(input.vault, relPath);
5931
+ const absPath = join33(input.vault, relPath);
5832
5932
  const localStat = statSync4(absPath);
5833
5933
  let needsUpload = true;
5834
5934
  try {
@@ -5904,7 +6004,7 @@ async function runBackupRestore(input) {
5904
6004
  const objects = list.Contents ?? [];
5905
6005
  for (const obj of objects) {
5906
6006
  if (!obj.Key) continue;
5907
- const localPath = join32(target, obj.Key);
6007
+ const localPath = join33(target, obj.Key);
5908
6008
  try {
5909
6009
  const localStat = statSync4(localPath);
5910
6010
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -5917,7 +6017,7 @@ async function runBackupRestore(input) {
5917
6017
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
5918
6018
  const body = await resp.Body?.transformToByteArray();
5919
6019
  if (body) {
5920
- mkdirSync3(dirname10(localPath), { recursive: true });
6020
+ mkdirSync3(dirname11(localPath), { recursive: true });
5921
6021
  writeFileSync4(localPath, Buffer.from(body));
5922
6022
  downloaded++;
5923
6023
  }
@@ -5951,8 +6051,8 @@ async function runBackupRestore(input) {
5951
6051
 
5952
6052
  // src/commands/status.ts
5953
6053
  import { existsSync as existsSync11, statSync as statSync5 } from "fs";
5954
- import { readFile as readFile21 } from "fs/promises";
5955
- import { join as join33 } from "path";
6054
+ import { readFile as readFile22 } from "fs/promises";
6055
+ import { join as join34 } from "path";
5956
6056
  async function runStatus(input) {
5957
6057
  if (!existsSync11(input.vault)) {
5958
6058
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
@@ -5979,7 +6079,7 @@ async function runStatus(input) {
5979
6079
  const compound = scan.data.compound.length;
5980
6080
  let schemaVersion = "v1";
5981
6081
  try {
5982
- const schemaContent = await readFile21(join33(input.vault, "SCHEMA.md"), "utf8");
6082
+ const schemaContent = await readFile22(join34(input.vault, "SCHEMA.md"), "utf8");
5983
6083
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
5984
6084
  if (versionMatch) schemaVersion = versionMatch[1];
5985
6085
  } catch {
@@ -6039,8 +6139,8 @@ async function runStatus(input) {
6039
6139
  }
6040
6140
 
6041
6141
  // 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";
6142
+ import { mkdir as mkdir13, writeFile as writeFile14, stat as stat7 } from "fs/promises";
6143
+ import { join as join35 } from "path";
6044
6144
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6045
6145
  var EXAMPLE_PAGES = {
6046
6146
  "entities/example-project.md": `---
@@ -6109,30 +6209,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
6109
6209
  `;
6110
6210
  async function runSeed(input) {
6111
6211
  try {
6112
- await stat7(join34(input.vault, "SCHEMA.md"));
6212
+ await stat7(join35(input.vault, "SCHEMA.md"));
6113
6213
  } catch {
6114
6214
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
6115
6215
  }
6116
6216
  const created = [];
6117
6217
  const skipped = [];
6118
6218
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
6119
- const absPath = join34(input.vault, relPath);
6219
+ const absPath = join35(input.vault, relPath);
6120
6220
  try {
6121
6221
  await stat7(absPath);
6122
6222
  skipped.push(relPath);
6123
6223
  } catch {
6124
- await mkdir13(join34(absPath, ".."), { recursive: true });
6125
- await writeFile18(absPath, content, "utf8");
6224
+ await mkdir13(join35(absPath, ".."), { recursive: true });
6225
+ await writeFile14(absPath, content, "utf8");
6126
6226
  created.push(relPath);
6127
6227
  }
6128
6228
  }
6129
- const rawPath = join34(input.vault, "raw", "articles", "example-source.md");
6229
+ const rawPath = join35(input.vault, "raw", "articles", "example-source.md");
6130
6230
  try {
6131
6231
  await stat7(rawPath);
6132
6232
  skipped.push("raw/articles/example-source.md");
6133
6233
  } catch {
6134
- await mkdir13(join34(rawPath, ".."), { recursive: true });
6135
- await writeFile18(rawPath, EXAMPLE_RAW, "utf8");
6234
+ await mkdir13(join35(rawPath, ".."), { recursive: true });
6235
+ await writeFile14(rawPath, EXAMPLE_RAW, "utf8");
6136
6236
  created.push("raw/articles/example-source.md");
6137
6237
  }
6138
6238
  if (created.length > 0) {
@@ -6154,9 +6254,9 @@ async function runSeed(input) {
6154
6254
  }
6155
6255
 
6156
6256
  // src/commands/canvas.ts
6157
- import { readFile as readFile22, writeFile as writeFile19 } from "fs/promises";
6257
+ import { readFile as readFile23, writeFile as writeFile15 } from "fs/promises";
6158
6258
  import { existsSync as existsSync12 } from "fs";
6159
- import { join as join35 } from "path";
6259
+ import { join as join36 } from "path";
6160
6260
  var NODE_WIDTH = 240;
6161
6261
  var NODE_HEIGHT = 60;
6162
6262
  var COLUMN_SPACING = 400;
@@ -6234,7 +6334,7 @@ function buildCanvasEdges(adjacency) {
6234
6334
  return edges;
6235
6335
  }
6236
6336
  async function runCanvasGenerate(input) {
6237
- const graphPath = input.graphPath ?? join35(input.vault, ".skillwiki", "graph.json");
6337
+ const graphPath = input.graphPath ?? join36(input.vault, ".skillwiki", "graph.json");
6238
6338
  if (!existsSync12(graphPath)) {
6239
6339
  return {
6240
6340
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -6246,7 +6346,7 @@ async function runCanvasGenerate(input) {
6246
6346
  }
6247
6347
  let raw;
6248
6348
  try {
6249
- raw = await readFile22(graphPath, "utf8");
6349
+ raw = await readFile23(graphPath, "utf8");
6250
6350
  } catch (e) {
6251
6351
  return {
6252
6352
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -6272,9 +6372,9 @@ async function runCanvasGenerate(input) {
6272
6372
  const nodes = buildCanvasNodes(paths);
6273
6373
  const edges = buildCanvasEdges(graph.adjacency);
6274
6374
  const canvas = { nodes, edges };
6275
- const outPath = join35(input.vault, "vault-graph.canvas");
6375
+ const outPath = join36(input.vault, "vault-graph.canvas");
6276
6376
  try {
6277
- await writeFile19(outPath, JSON.stringify(canvas, null, 2));
6377
+ await writeFile15(outPath, JSON.stringify(canvas, null, 2));
6278
6378
  } catch (e) {
6279
6379
  return {
6280
6380
  exitCode: ExitCode.WRITE_FAILED,
@@ -6294,8 +6394,8 @@ written: ${outPath}`
6294
6394
  }
6295
6395
 
6296
6396
  // src/commands/query.ts
6297
- import { readFile as readFile23, stat as stat8 } from "fs/promises";
6298
- import { join as join36 } from "path";
6397
+ import { readFile as readFile24, stat as stat8 } from "fs/promises";
6398
+ import { join as join37 } from "path";
6299
6399
  var W_KEYWORD = 2;
6300
6400
  var W_SOURCE_OVERLAP = 4;
6301
6401
  var W_WIKILINK = 3;
@@ -6416,7 +6516,7 @@ function computeKeywordScore(terms, title, tags, body) {
6416
6516
  return score;
6417
6517
  }
6418
6518
  async function loadOrBuildGraph(vault) {
6419
- const graphPath = join36(vault, ".skillwiki", "graph.json");
6519
+ const graphPath = join37(vault, ".skillwiki", "graph.json");
6420
6520
  let needsBuild = false;
6421
6521
  try {
6422
6522
  const fileStat = await stat8(graphPath);
@@ -6430,7 +6530,7 @@ async function loadOrBuildGraph(vault) {
6430
6530
  if (buildResult.exitCode !== 0) return null;
6431
6531
  }
6432
6532
  try {
6433
- const raw = await readFile23(graphPath, "utf8");
6533
+ const raw = await readFile24(graphPath, "utf8");
6434
6534
  return JSON.parse(raw);
6435
6535
  } catch {
6436
6536
  return null;
@@ -6439,13 +6539,13 @@ async function loadOrBuildGraph(vault) {
6439
6539
 
6440
6540
  // src/utils/auto-commit.ts
6441
6541
  import { existsSync as existsSync13 } from "fs";
6442
- import { join as join37 } from "path";
6542
+ import { join as join38 } from "path";
6443
6543
  async function postCommit(vault, exitCode) {
6444
6544
  if (exitCode !== 0) return;
6445
6545
  const home = process.env.HOME ?? "";
6446
6546
  const dotenv = await parseDotenvFile(configPath(home));
6447
6547
  if (dotenv["AUTO_COMMIT"] === "false") return;
6448
- if (!existsSync13(join37(vault, ".git"))) return;
6548
+ if (!existsSync13(join38(vault, ".git"))) return;
6449
6549
  const lastOps = readLastOp(vault);
6450
6550
  if (lastOps.length === 0) return;
6451
6551
  const porcelain = git(vault, ["status", "--porcelain"]);
@@ -6496,7 +6596,7 @@ program.command("validate <file>").description("validate vault page frontmatter
6496
6596
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
6497
6597
  });
6498
6598
  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");
6599
+ const out = opts.out ?? join39(vault, ".skillwiki", "graph.json");
6500
6600
  emit(await runGraphBuild({ vault, out }), vault);
6501
6601
  });
6502
6602
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 18 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",