skillwiki 0.9.5 → 0.9.7

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
@@ -2070,11 +2070,11 @@ async function runInit(input) {
2070
2070
  ].join("\n");
2071
2071
  });
2072
2072
  if (errTemplate) return errTemplate;
2073
- const err22 = await writeOrPreserve("log.md", async () => {
2073
+ const err2 = await writeOrPreserve("log.md", async () => {
2074
2074
  const tpl = await readFile7(join11(input.templates, "log.md"), "utf8");
2075
2075
  return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
2076
2076
  });
2077
- if (err22) return err22;
2077
+ if (err2) return err2;
2078
2078
  const skipEnv = !!input.noEnv || explicitTarget && !input.profile && swDotenvHadPath && !input.force;
2079
2079
  let envWritten = "";
2080
2080
  if (!skipEnv) {
@@ -2788,8 +2788,8 @@ async function acquireLogLock(vault, opts = {}) {
2788
2788
  writeFileSync2(path, content, { flag: "wx" });
2789
2789
  return { ok: true };
2790
2790
  } catch (e) {
2791
- const err3 = e;
2792
- if (err3.code !== "EEXIST") throw err3;
2791
+ const err2 = e;
2792
+ if (err2.code !== "EEXIST") throw err2;
2793
2793
  }
2794
2794
  try {
2795
2795
  const age = Date.now() - statSync2(path).mtimeMs;
@@ -2867,7 +2867,7 @@ ${content}
2867
2867
  // src/commands/lint.ts
2868
2868
  import { existsSync as existsSync6 } from "fs";
2869
2869
  import { readFile as readFile17 } from "fs/promises";
2870
- import { createHash as createHash4 } from "crypto";
2870
+ import { createHash as createHash5 } from "crypto";
2871
2871
  import { join as join23 } from "path";
2872
2872
 
2873
2873
  // src/commands/sparse-community.ts
@@ -2922,11 +2922,28 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
2922
2922
  }
2923
2923
 
2924
2924
  // src/commands/dedup.ts
2925
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
2926
- import { join as join20 } from "path";
2925
+ import { createHash as createHash3 } from "crypto";
2926
+ import { execFile } from "child_process";
2927
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
2928
+ import { dirname as dirname7, join as join20, resolve as resolve4 } from "path";
2929
+ import { promisify } from "util";
2930
+ var execFileAsync = promisify(execFile);
2927
2931
  async function runDedup(input) {
2932
+ if (input.canonicalPolicy && input.canonicalPolicy !== "scan-order" && input.canonicalPolicy !== "stable-path") {
2933
+ return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--canonical-policy must be stable-path or scan-order" }) };
2934
+ }
2935
+ if (input.remoteDelete && !input.remote) {
2936
+ return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--remote-delete requires --remote" }) };
2937
+ }
2938
+ if (input.remoteDelete && !input.apply && !input.manifestIn) {
2939
+ return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--remote-delete requires --apply or --manifest-in" }) };
2940
+ }
2928
2941
  const scan = await scanVault(input.vault);
2929
2942
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2943
+ const manifestFromFile = input.manifestIn ? readManifest(input.manifestIn) : null;
2944
+ if (manifestFromFile && !manifestFromFile.ok) {
2945
+ return { exitCode: ExitCode.INVALID_FRONTMATTER, result: manifestFromFile };
2946
+ }
2930
2947
  const hashMap = /* @__PURE__ */ new Map();
2931
2948
  let totalFiles = 0;
2932
2949
  for (const raw of scan.data.raw) {
@@ -2939,18 +2956,42 @@ async function runDedup(input) {
2939
2956
  if (existing) existing.push(raw.relPath);
2940
2957
  else hashMap.set(sha, [raw.relPath]);
2941
2958
  }
2942
- const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files }));
2959
+ const canonicalPolicy = input.canonicalPolicy ?? "stable-path";
2960
+ const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files: orderFiles(files, canonicalPolicy) }));
2943
2961
  const rewired = [];
2944
2962
  const removed = [];
2945
- if (input.apply && duplicates.length > 0) {
2963
+ const unsafe = [];
2964
+ const safeEntries = manifestFromFile?.ok ? manifestFromFile.data.entries : buildSafeEntries(input.vault, duplicates, unsafe);
2965
+ const manifest = safeEntries.length > 0 ? {
2966
+ version: 1,
2967
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
2968
+ vault: resolve4(input.vault),
2969
+ entries: safeEntries
2970
+ } : void 0;
2971
+ const remote = await planAndMaybePruneRemote(input, safeEntries);
2972
+ if (!remote.ok) {
2973
+ return {
2974
+ exitCode: remote.error === "SYNC_PUSH_FAILED" ? ExitCode.SYNC_PUSH_FAILED : ExitCode.USAGE,
2975
+ result: remote
2976
+ };
2977
+ }
2978
+ if (input.manifestOut && manifest) {
2979
+ try {
2980
+ mkdirSync3(dirname7(input.manifestOut), { recursive: true });
2981
+ writeFileSync3(input.manifestOut, `${JSON.stringify(manifest, null, 2)}
2982
+ `, "utf-8");
2983
+ } catch (e) {
2984
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { path: input.manifestOut, message: String(e) }) };
2985
+ }
2986
+ }
2987
+ if (input.apply && safeEntries.length > 0) {
2946
2988
  const replacements = /* @__PURE__ */ new Map();
2947
- for (const group of duplicates) {
2948
- const canonical = group.files[0];
2949
- for (let i = 1; i < group.files.length; i++) {
2950
- replacements.set(group.files[i], canonical);
2989
+ for (const entry of safeEntries) {
2990
+ for (const duplicate of entry.duplicates) {
2991
+ replacements.set(duplicate, entry.canonical);
2951
2992
  }
2952
2993
  }
2953
- for (const page of scan.data.typedKnowledge) {
2994
+ for (const page of scan.data.allMarkdown.filter((p) => !p.relPath.startsWith("raw/"))) {
2954
2995
  const text = readFileSync3(join20(input.vault, page.relPath), "utf-8");
2955
2996
  let updated = text;
2956
2997
  let changed = false;
@@ -2973,7 +3014,7 @@ async function runDedup(input) {
2973
3014
  rewired.push(page.relPath);
2974
3015
  }
2975
3016
  }
2976
- for (const [oldPath] of replacements) {
3017
+ for (const oldPath of replacements.keys()) {
2977
3018
  const fullPath = join20(input.vault, oldPath);
2978
3019
  try {
2979
3020
  unlinkSync3(fullPath);
@@ -2999,19 +3040,131 @@ async function runDedup(input) {
2999
3040
  hintLines.push(`rewired: ${rewired.length} pages`);
3000
3041
  hintLines.push(`removed: ${removed.length} raw files`);
3001
3042
  }
3043
+ if (unsafe.length > 0) hintLines.push(`unsafe: ${unsafe.length} groups skipped`);
3044
+ if (remote.data.plannedDeletes.length > 0) {
3045
+ hintLines.push(`remote planned deletes: ${remote.data.plannedDeletes.length}`);
3046
+ if (input.remoteDelete) hintLines.push(`remote deleted: ${remote.data.deleted.length}`);
3047
+ }
3002
3048
  } else {
3003
3049
  hintLines.push("0 duplicates");
3004
3050
  }
3005
3051
  return {
3006
3052
  exitCode,
3007
- result: ok({ scanned: totalFiles, duplicates, rewired, removed, humanHint: hintLines.join("\n") })
3053
+ result: ok({
3054
+ scanned: totalFiles,
3055
+ duplicates,
3056
+ manifest,
3057
+ unsafe,
3058
+ remote: remote.data,
3059
+ rewired,
3060
+ removed,
3061
+ humanHint: hintLines.join("\n")
3062
+ })
3008
3063
  };
3009
3064
  }
3065
+ function orderFiles(files, policy) {
3066
+ if (policy === "scan-order") return [...files];
3067
+ return [...files].sort(compareStableRawPath);
3068
+ }
3069
+ function compareStableRawPath(a, b) {
3070
+ return rawPathScore(a) - rawPathScore(b) || a.localeCompare(b);
3071
+ }
3072
+ function rawPathScore(relPath) {
3073
+ const base = relPath.split("/").pop() ?? relPath;
3074
+ const stem = base.replace(/\.md$/i, "");
3075
+ let score = 0;
3076
+ if (relPath.startsWith("raw/articles/obsidian-import/")) score += 1e5;
3077
+ if (/\bdup(licate)?\b/i.test(stem)) score += 1e4;
3078
+ if (/(?:-[0-9]+|-[0-9a-f]{6,}|[0-9a-f]{8})$/i.test(stem)) score += 1e3;
3079
+ score += relPath.length;
3080
+ return score;
3081
+ }
3082
+ function buildSafeEntries(vault, duplicates, unsafe) {
3083
+ const entries = [];
3084
+ for (const group of duplicates) {
3085
+ const canonical = group.files[0];
3086
+ if (!canonical) {
3087
+ unsafe.push({ sha256: group.sha256, files: group.files, reason: "canonical_missing" });
3088
+ continue;
3089
+ }
3090
+ const bodyHashes = /* @__PURE__ */ new Map();
3091
+ for (const file of group.files) {
3092
+ const bodyHash2 = hashRawBody(vault, file);
3093
+ const existing = bodyHashes.get(bodyHash2);
3094
+ if (existing) existing.push(file);
3095
+ else bodyHashes.set(bodyHash2, [file]);
3096
+ }
3097
+ if (bodyHashes.size !== 1) {
3098
+ unsafe.push({ sha256: group.sha256, files: group.files, reason: "body_hash_mismatch" });
3099
+ continue;
3100
+ }
3101
+ const [bodyHash] = bodyHashes.keys();
3102
+ entries.push({
3103
+ sha256: group.sha256,
3104
+ canonical,
3105
+ duplicates: group.files.slice(1),
3106
+ bodyHash
3107
+ });
3108
+ }
3109
+ return entries;
3110
+ }
3111
+ function hashRawBody(vault, relPath) {
3112
+ const text = readFileSync3(join20(vault, relPath), "utf-8");
3113
+ const split = splitFrontmatter(text);
3114
+ const body = split.ok ? split.data.body : text;
3115
+ return createHash3("sha256").update(body).digest("hex");
3116
+ }
3117
+ function readManifest(path) {
3118
+ try {
3119
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
3120
+ if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
3121
+ return err("INVALID_FRONTMATTER", { message: "dedup manifest must have version 1 and entries[]" });
3122
+ }
3123
+ return ok(parsed);
3124
+ } catch (e) {
3125
+ return err("INVALID_FRONTMATTER", { path, message: String(e) });
3126
+ }
3127
+ }
3128
+ async function planAndMaybePruneRemote(input, entries) {
3129
+ const remoteRoot = input.remote?.replace(/\/+$/, "");
3130
+ const plannedDeletes = remoteRoot ? entries.flatMap((entry) => entry.duplicates.map((path) => `${remoteRoot}/${path}`)) : [];
3131
+ const output = { plannedDeletes, deleted: [] };
3132
+ if (!input.remoteDelete) return ok(output);
3133
+ const maxDeletes = input.maxRemoteDeletes ?? 50;
3134
+ if (!Number.isInteger(maxDeletes) || maxDeletes < 1) {
3135
+ return err("USAGE", { message: "--max-remote-deletes must be a positive integer" });
3136
+ }
3137
+ if (plannedDeletes.length > maxDeletes) {
3138
+ return err("USAGE", { message: `remote delete cap exceeded: ${plannedDeletes.length} > ${maxDeletes}` });
3139
+ }
3140
+ const runner = input.rcloneRunner ?? defaultRcloneRunner;
3141
+ for (const path of plannedDeletes) {
3142
+ const result = await runner(["deletefile", path]);
3143
+ if (result.exitCode !== 0) {
3144
+ output.failed = { path, stderr: result.stderr };
3145
+ return err("SYNC_PUSH_FAILED", { path, stderr: result.stderr });
3146
+ }
3147
+ output.deleted.push(path);
3148
+ }
3149
+ return ok(output);
3150
+ }
3151
+ async function defaultRcloneRunner(args) {
3152
+ try {
3153
+ const result = await execFileAsync("rclone", args, { encoding: "utf-8" });
3154
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
3155
+ } catch (e) {
3156
+ return {
3157
+ exitCode: typeof e?.code === "number" ? e.code : 1,
3158
+ stdout: typeof e?.stdout === "string" ? e.stdout : "",
3159
+ stderr: typeof e?.stderr === "string" ? e.stderr : String(e)
3160
+ };
3161
+ }
3162
+ }
3010
3163
 
3011
3164
  // src/utils/safe-write.ts
3012
3165
  import { open, readFile as readFile15, rename as rename5, unlink as unlink2, writeFile as writeFile9 } from "fs/promises";
3013
3166
  import { randomBytes } from "crypto";
3014
- import { dirname as dirname7, basename, join as join21 } from "path";
3167
+ import { dirname as dirname8, basename, join as join21 } from "path";
3015
3168
  var DEFAULT_MIN_BODY_RATIO = 0.5;
3016
3169
  var DEFAULT_MIN_OLD_BODY_BYTES = 200;
3017
3170
  function bodyBytes(text) {
@@ -3058,7 +3211,7 @@ async function safeWritePage(absPath, newContent, opts = {}) {
3058
3211
  if (!isNew && oldContent === newContent) {
3059
3212
  return ok({ isNew: false, oldBodyBytes, newBodyBytes, bodyRatio, guardSkippedSmall });
3060
3213
  }
3061
- const dir = dirname7(absPath);
3214
+ const dir = dirname8(absPath);
3062
3215
  const tmpName = `.${basename(absPath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
3063
3216
  const tmpPath = join21(dir, tmpName);
3064
3217
  try {
@@ -3084,7 +3237,7 @@ async function safeWritePage(absPath, newContent, opts = {}) {
3084
3237
  }
3085
3238
 
3086
3239
  // src/commands/raw-body-dedup.ts
3087
- import { createHash as createHash3 } from "crypto";
3240
+ import { createHash as createHash4 } from "crypto";
3088
3241
  async function runRawBodyDedup(vault) {
3089
3242
  const scan = await scanVault(vault);
3090
3243
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -3095,7 +3248,7 @@ async function runRawBodyDedup(vault) {
3095
3248
  const split = splitFrontmatter(text);
3096
3249
  if (!split.ok) continue;
3097
3250
  totalFiles++;
3098
- const bodyHash = createHash3("sha256").update(split.data.body).digest("hex");
3251
+ const bodyHash = createHash4("sha256").update(split.data.body).digest("hex");
3099
3252
  const fm = extractFrontmatter(text);
3100
3253
  let fmSha256 = null;
3101
3254
  if (fm.ok && typeof fm.data.sha256 === "string" && fm.data.sha256.length === 64) {
@@ -3126,7 +3279,7 @@ async function runRawBodyDedup(vault) {
3126
3279
  // src/commands/path-too-long.ts
3127
3280
  import { existsSync as existsSync5 } from "fs";
3128
3281
  import { mkdir as mkdir8, readFile as readFile16, rename as rename6, unlink as unlink3 } from "fs/promises";
3129
- import { dirname as dirname8, join as join22, posix, resolve as resolve4 } from "path";
3282
+ import { dirname as dirname9, join as join22, posix, resolve as resolve5 } from "path";
3130
3283
  var MAX_PATH_LENGTH = 240;
3131
3284
  var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
3132
3285
  async function runPathTooLong(input) {
@@ -3161,7 +3314,7 @@ async function fixPathTooLong(input) {
3161
3314
  if (target.mode === "dedupe") {
3162
3315
  await unlink3(join22(input.vault, violation.relPath));
3163
3316
  } else {
3164
- await mkdir8(dirname8(join22(input.vault, target.relPath)), { recursive: true });
3317
+ await mkdir8(dirname9(join22(input.vault, target.relPath)), { recursive: true });
3165
3318
  await rename6(join22(input.vault, violation.relPath), join22(input.vault, target.relPath));
3166
3319
  }
3167
3320
  fixed.push({ from: violation.relPath, to: target.relPath });
@@ -3213,7 +3366,7 @@ function findPathTooLongViolations(pages, maxLength) {
3213
3366
  }
3214
3367
  function maxFixPathLength(vault) {
3215
3368
  if (process.platform !== "win32") return MAX_PATH_LENGTH;
3216
- const root = resolve4(vault);
3369
+ const root = resolve5(vault);
3217
3370
  const separatorBudget = root.endsWith("\\") || root.endsWith("/") ? 0 : 1;
3218
3371
  const absoluteSafeRelLength = WINDOWS_ABSOLUTE_PATH_LIMIT - root.length - separatorBudget;
3219
3372
  return Math.max(1, Math.min(MAX_PATH_LENGTH, absoluteSafeRelLength));
@@ -3332,7 +3485,7 @@ function buildCliSurface() {
3332
3485
  program2.command("status").option("--wiki <name>");
3333
3486
  program2.command("archive").option("--wiki <name>").option("--cascade").option("--apply");
3334
3487
  program2.command("drift").option("--apply").option("--new <date>").option("--wiki <name>");
3335
- program2.command("dedup").option("--apply").option("--wiki <name>");
3488
+ program2.command("dedup").option("--apply").option("--canonical-policy <policy>").option("--manifest-out <path>").option("--manifest-in <path>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--wiki <name>");
3336
3489
  program2.command("migrate-citations").option("--dry-run").option("--wiki <name>");
3337
3490
  program2.command("frontmatter-fix").option("--dry-run").option("--wiki <name>");
3338
3491
  program2.command("update").option("--tag <tag>");
@@ -3584,7 +3737,7 @@ function recomputeRawSha256IfPresent(content) {
3584
3737
  const split = splitFrontmatter(content);
3585
3738
  if (!split.ok) return content;
3586
3739
  if (!/^sha256:\s*[0-9a-f]{64}$/m.test(split.data.rawFrontmatter)) return content;
3587
- const sha256 = createHash4("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
3740
+ const sha256 = createHash5("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
3588
3741
  const rawFrontmatter = split.data.rawFrontmatter.replace(/^sha256:\s*[0-9a-f]{64}$/m, `sha256: ${sha256}`);
3589
3742
  return `---
3590
3743
  ${rawFrontmatter}
@@ -4351,13 +4504,13 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
4351
4504
  }
4352
4505
 
4353
4506
  // src/commands/health.ts
4354
- import { existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync, writeFileSync as writeFileSync6 } from "fs";
4355
- import { dirname as dirname10, join as join30, resolve as resolve6 } from "path";
4507
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync, writeFileSync as writeFileSync6 } from "fs";
4508
+ import { dirname as dirname11, join as join30, resolve as resolve7 } from "path";
4356
4509
  import { platform as platform3 } from "os";
4357
4510
 
4358
4511
  // src/commands/doctor.ts
4359
4512
  import { existsSync as existsSync11, lstatSync, readlinkSync, readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
4360
- import { join as join29, resolve as resolve5 } from "path";
4513
+ import { join as join29, resolve as resolve6 } from "path";
4361
4514
  import { execSync as execSync2 } from "child_process";
4362
4515
  import { platform as platform2 } from "os";
4363
4516
 
@@ -4423,8 +4576,8 @@ async function runConfigPath(input) {
4423
4576
  }
4424
4577
 
4425
4578
  // src/utils/auto-update.ts
4426
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync3 } from "fs";
4427
- import { join as join25, dirname as dirname9 } from "path";
4579
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
4580
+ import { join as join25, dirname as dirname10 } from "path";
4428
4581
  import { spawn } from "child_process";
4429
4582
  function cachePath(home) {
4430
4583
  return join25(home, ".skillwiki", CACHE_FILENAME);
@@ -4446,7 +4599,7 @@ function readCache(home) {
4446
4599
  }
4447
4600
  function writeCache(home, cache) {
4448
4601
  const p = cachePath(home);
4449
- mkdirSync3(dirname9(p), { recursive: true });
4602
+ mkdirSync4(dirname10(p), { recursive: true });
4450
4603
  writeFileSync4(p, JSON.stringify(cache, null, 2));
4451
4604
  }
4452
4605
  function latestFromCache(home, currentVersion) {
@@ -5246,14 +5399,14 @@ function checkNodeVersion() {
5246
5399
  function detectCliChannels(argv, home) {
5247
5400
  const channels = [];
5248
5401
  if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
5249
- const devPath = resolve5(argv[1]);
5402
+ const devPath = resolve6(argv[1]);
5250
5403
  channels.push({ name: "dev", path: devPath, isDevLink: true });
5251
5404
  }
5252
5405
  try {
5253
5406
  const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
5254
5407
  if (whichOut) {
5255
5408
  const isDev = isDevSymlink(whichOut);
5256
- if (!channels.some((c) => c.path === resolve5(whichOut))) {
5409
+ if (!channels.some((c) => c.path === resolve6(whichOut))) {
5257
5410
  channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
5258
5411
  }
5259
5412
  }
@@ -5276,7 +5429,7 @@ function isDevSymlink(binPath) {
5276
5429
  try {
5277
5430
  const st = lstatSync(binPath);
5278
5431
  if (st.isSymbolicLink()) {
5279
- const target = resolve5(binPath, "..", readlinkSync(binPath));
5432
+ const target = resolve6(binPath, "..", readlinkSync(binPath));
5280
5433
  return target.includes("packages/cli") || target.includes("packages\\cli");
5281
5434
  }
5282
5435
  } catch {
@@ -6695,7 +6848,7 @@ function buildHumanHint(report) {
6695
6848
  return lines.join("\n");
6696
6849
  }
6697
6850
  function writeReport(out, report) {
6698
- mkdirSync4(dirname10(out), { recursive: true });
6851
+ mkdirSync5(dirname11(out), { recursive: true });
6699
6852
  const tmp = `${out}.tmp-${process.pid}-${Date.now()}`;
6700
6853
  writeFileSync6(tmp, JSON.stringify(ok(report), null, 2) + "\n", "utf8");
6701
6854
  renameSync(tmp, out);
@@ -6797,7 +6950,7 @@ async function runHealth(input) {
6797
6950
  if (input.out) {
6798
6951
  baseReport.report_path = input.out;
6799
6952
  }
6800
- if (input.out && resolve6(input.out).startsWith(resolve6(input.vault) + "/")) {
6953
+ if (input.out && resolve7(input.out).startsWith(resolve7(input.vault) + "/")) {
6801
6954
  baseReport.warnings.push({
6802
6955
  id: "report_inside_vault",
6803
6956
  message: "health report was written inside the vault; this may create sync churn"
@@ -6831,7 +6984,7 @@ async function runHealth(input) {
6831
6984
 
6832
6985
  // src/commands/archive.ts
6833
6986
  import { rename as rename7, mkdir as mkdir9, readFile as readFile21, writeFile as writeFile10 } from "fs/promises";
6834
- import { join as join31, dirname as dirname11 } from "path";
6987
+ import { join as join31, dirname as dirname12 } from "path";
6835
6988
  function countWikilinks(body, slug) {
6836
6989
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6837
6990
  const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
@@ -6927,7 +7080,7 @@ ${fmRewritten}
6927
7080
  }
6928
7081
  }
6929
7082
  }
6930
- await mkdir9(dirname11(join31(input.vault, archivePath)), { recursive: true });
7083
+ await mkdir9(dirname12(join31(input.vault, archivePath)), { recursive: true });
6931
7084
  let indexUpdated = false;
6932
7085
  if (!isRaw) {
6933
7086
  const indexPath = join31(input.vault, "index.md");
@@ -6966,7 +7119,7 @@ ${fmRewritten}
6966
7119
  }
6967
7120
 
6968
7121
  // src/commands/drift.ts
6969
- import { createHash as createHash5 } from "crypto";
7122
+ import { createHash as createHash6 } from "crypto";
6970
7123
 
6971
7124
  // src/utils/fetch.ts
6972
7125
  async function controlledFetch(url, opts) {
@@ -7045,7 +7198,7 @@ async function runDrift(input) {
7045
7198
  });
7046
7199
  continue;
7047
7200
  }
7048
- const currentHash = createHash5("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
7201
+ const currentHash = createHash6("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
7049
7202
  const identity = assessSourceIdentity({
7050
7203
  rawPath: raw.relPath,
7051
7204
  sourceUrl,
@@ -7648,7 +7801,7 @@ async function runTranscripts(input) {
7648
7801
 
7649
7802
  // src/commands/project-index.ts
7650
7803
  import { readdir as readdir6, readFile as readFile23, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
7651
- import { join as join35, dirname as dirname12 } from "path";
7804
+ import { join as join35, dirname as dirname13 } from "path";
7652
7805
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
7653
7806
  async function runProjectIndex(input) {
7654
7807
  const slug = input.slug;
@@ -7762,7 +7915,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
7762
7915
  }
7763
7916
  if (input.apply) {
7764
7917
  try {
7765
- await mkdir10(dirname12(indexPath), { recursive: true });
7918
+ await mkdir10(dirname13(indexPath), { recursive: true });
7766
7919
  await writeFile11(indexPath, body, "utf8");
7767
7920
  } catch (e) {
7768
7921
  return {
@@ -8079,7 +8232,7 @@ no compound entries found`;
8079
8232
  import { mkdir as mkdir12, writeFile as writeFile13 } from "fs/promises";
8080
8233
  import { existsSync as existsSync15, statSync as statSync4 } from "fs";
8081
8234
  import { join as join37 } from "path";
8082
- import { createHash as createHash6 } from "crypto";
8235
+ import { createHash as createHash7 } from "crypto";
8083
8236
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
8084
8237
  function slugify2(text) {
8085
8238
  const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -8123,7 +8276,7 @@ async function runObserve(input) {
8123
8276
  const body = `
8124
8277
  ${input.text.trim()}
8125
8278
  `;
8126
- const sha256 = createHash6("sha256").update(Buffer.from(body, "utf8")).digest("hex");
8279
+ const sha256 = createHash7("sha256").update(Buffer.from(body, "utf8")).digest("hex");
8127
8280
  const frontmatterLines = [
8128
8281
  "---",
8129
8282
  "source_url:",
@@ -8522,7 +8675,7 @@ function dateFromPath(path) {
8522
8675
  // src/commands/ingest.ts
8523
8676
  import { readFile as readFile26, writeFile as writeFile15, mkdir as mkdir14 } from "fs/promises";
8524
8677
  import { join as join39 } from "path";
8525
- import { createHash as createHash7 } from "crypto";
8678
+ import { createHash as createHash8 } from "crypto";
8526
8679
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
8527
8680
  var TYPE_DIR = {
8528
8681
  entity: "entities",
@@ -8698,7 +8851,7 @@ async function runIngest(input) {
8698
8851
  })
8699
8852
  };
8700
8853
  }
8701
- const sha256 = createHash7("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
8854
+ const sha256 = createHash8("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
8702
8855
  const today = todayIso();
8703
8856
  const slug = slugify3(input.title);
8704
8857
  const tags = input.tags && input.tags.length > 0 ? input.tags : [];
@@ -8980,9 +9133,9 @@ import { existsSync as existsSync17 } from "fs";
8980
9133
  import { join as join41 } from "path";
8981
9134
 
8982
9135
  // src/utils/sync-lock.ts
8983
- import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "fs";
9136
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync11, renameSync as renameSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "fs";
8984
9137
  import { join as join40 } from "path";
8985
- import { createHash as createHash8 } from "crypto";
9138
+ import { createHash as createHash9 } from "crypto";
8986
9139
  function getSessionId() {
8987
9140
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
8988
9141
  if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
@@ -9010,7 +9163,7 @@ function acquireLock(vault, opts = {}) {
9010
9163
  const path = lockPath(vault);
9011
9164
  const dir = join40(vault, ".skillwiki");
9012
9165
  if (!existsSync16(dir)) {
9013
- mkdirSync5(dir, { recursive: true });
9166
+ mkdirSync6(dir, { recursive: true });
9014
9167
  }
9015
9168
  const sessionId = opts.sessionId ?? getSessionId();
9016
9169
  const summary = opts.summary ?? "skillwiki sync";
@@ -9032,8 +9185,8 @@ function acquireLock(vault, opts = {}) {
9032
9185
  writeFileSync7(path, content, { flag: "wx" });
9033
9186
  return { ok: true, lock };
9034
9187
  } catch (e) {
9035
- const err3 = e;
9036
- if (err3.code !== "EEXIST") throw err3;
9188
+ const err2 = e;
9189
+ if (err2.code !== "EEXIST") throw err2;
9037
9190
  }
9038
9191
  const existing = readLock(vault);
9039
9192
  if (!existing) {
@@ -9524,8 +9677,8 @@ function runSyncUnlock(input) {
9524
9677
  }
9525
9678
 
9526
9679
  // src/commands/backup.ts
9527
- import { statSync as statSync5, readdirSync as readdirSync3, readFileSync as readFileSync12, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
9528
- import { join as join42, relative as relative4, dirname as dirname13 } from "path";
9680
+ import { statSync as statSync5, readdirSync as readdirSync3, readFileSync as readFileSync12, mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
9681
+ import { join as join42, relative as relative4, dirname as dirname14 } from "path";
9529
9682
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
9530
9683
 
9531
9684
  // src/utils/s3-client.ts
@@ -9661,7 +9814,7 @@ async function runBackupRestore(input) {
9661
9814
  const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
9662
9815
  const body = await resp.Body?.transformToByteArray();
9663
9816
  if (body) {
9664
- mkdirSync6(dirname13(localPath), { recursive: true });
9817
+ mkdirSync7(dirname14(localPath), { recursive: true });
9665
9818
  writeFileSync8(localPath, Buffer.from(body));
9666
9819
  downloaded++;
9667
9820
  }
@@ -10448,10 +10601,19 @@ program.command("drift [vault]").description("detect content drift in raw source
10448
10601
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
10449
10602
  else emit(await runDrift({ vault: v.vault, apply: opts.apply, newSince: opts.new }), v.vault);
10450
10603
  });
10451
- program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
10604
+ program.command("dedup [vault]").description("detect duplicate raw sources by sha256").option("--apply", "rewire citations and remove duplicate raw files", false).option("--canonical-policy <policy>", "canonical policy: stable-path or scan-order", "stable-path").option("--manifest-out <path>", "write raw dedup delete manifest before applying").option("--manifest-in <path>", "read existing raw dedup delete manifest for remote pruning").option("--remote <remote>", "rclone remote root, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete manifest duplicate paths from the remote", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "50").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
10452
10605
  const v = await resolveVaultArg(vault, opts.wiki);
10453
10606
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
10454
- else emit(await runDedup({ vault: v.vault, apply: opts.apply }), v.vault);
10607
+ else emit(await runDedup({
10608
+ vault: v.vault,
10609
+ apply: opts.apply,
10610
+ canonicalPolicy: opts.canonicalPolicy,
10611
+ manifestOut: opts.manifestOut,
10612
+ manifestIn: opts.manifestIn,
10613
+ remote: opts.remote,
10614
+ remoteDelete: !!opts.remoteDelete,
10615
+ maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
10616
+ }), v.vault);
10455
10617
  });
10456
10618
  program.command("migrate-citations [vault]").description("migrate ^[raw/...] markers to paragraph-end citations").option("--dry-run", "preview changes without writing", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
10457
10619
  const v = await resolveVaultArg(vault, opts.wiki);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
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.9.5",
3
+ "version": "0.9.7",
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.9.5",
3
+ "version": "0.9.7",
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.9.5",
3
+ "version": "0.9.7",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",