skillwiki 0.9.31 → 0.9.32

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.
@@ -2056,11 +2056,57 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
2056
2056
 
2057
2057
  // src/commands/dedup.ts
2058
2058
  import { createHash as createHash2 } from "crypto";
2059
- import { execFile } from "child_process";
2060
2059
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
2061
2060
  import { dirname as dirname4, join as join12, resolve as resolve3 } from "path";
2061
+
2062
+ // src/utils/rclone.ts
2063
+ import { execFile } from "child_process";
2062
2064
  import { promisify } from "util";
2063
2065
  var execFileAsync = promisify(execFile);
2066
+ function normalizeRemoteRoot(remote) {
2067
+ return remote?.replace(/\/+$/, "");
2068
+ }
2069
+ function buildRemoteObjectPath(remoteRoot, relPath) {
2070
+ return remoteRoot ? `${remoteRoot}/${relPath}` : void 0;
2071
+ }
2072
+ function isValidRemoteDeleteCap(maxRemoteDeletes) {
2073
+ if (maxRemoteDeletes === void 0) return true;
2074
+ return Number.isInteger(maxRemoteDeletes) && maxRemoteDeletes > 0;
2075
+ }
2076
+ async function planAndMaybePruneRemoteObjects(plannedDeletes, input) {
2077
+ const output = { plannedDeletes, deleted: [] };
2078
+ if (!input.remoteDelete) return ok(output);
2079
+ const maxDeletes = input.maxRemoteDeletes ?? input.defaultMaxDeletes ?? 1;
2080
+ if (!Number.isInteger(maxDeletes) || maxDeletes < 1) {
2081
+ return err("USAGE", { message: "--max-remote-deletes must be a positive integer" });
2082
+ }
2083
+ if (plannedDeletes.length > maxDeletes) {
2084
+ return err("USAGE", { message: `remote delete cap exceeded: ${plannedDeletes.length} > ${maxDeletes}` });
2085
+ }
2086
+ const runner = input.rcloneRunner ?? defaultRcloneRunner;
2087
+ for (const path of plannedDeletes) {
2088
+ const result = await runner(["deletefile", path]);
2089
+ if (result.exitCode !== 0) {
2090
+ return err("SYNC_PUSH_FAILED", { path, stderr: result.stderr, deleted: output.deleted });
2091
+ }
2092
+ output.deleted.push(path);
2093
+ }
2094
+ return ok(output);
2095
+ }
2096
+ async function defaultRcloneRunner(args) {
2097
+ try {
2098
+ const result = await execFileAsync("rclone", args, { encoding: "utf-8" });
2099
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
2100
+ } catch (e) {
2101
+ return {
2102
+ exitCode: typeof e?.code === "number" ? e.code : 1,
2103
+ stdout: typeof e?.stdout === "string" ? e.stdout : "",
2104
+ stderr: typeof e?.stderr === "string" ? e.stderr : String(e)
2105
+ };
2106
+ }
2107
+ }
2108
+
2109
+ // src/commands/dedup.ts
2064
2110
  async function runDedup(input) {
2065
2111
  if (input.canonicalPolicy && input.canonicalPolicy !== "scan-order" && input.canonicalPolicy !== "stable-path") {
2066
2112
  return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--canonical-policy must be stable-path or scan-order" }) };
@@ -2259,39 +2305,9 @@ function readManifest(path) {
2259
2305
  }
2260
2306
  }
2261
2307
  async function planAndMaybePruneRemote(input, entries) {
2262
- const remoteRoot = input.remote?.replace(/\/+$/, "");
2308
+ const remoteRoot = normalizeRemoteRoot(input.remote);
2263
2309
  const plannedDeletes = remoteRoot ? entries.flatMap((entry) => entry.duplicates.map((path) => `${remoteRoot}/${path}`)) : [];
2264
- const output = { plannedDeletes, deleted: [] };
2265
- if (!input.remoteDelete) return ok(output);
2266
- const maxDeletes = input.maxRemoteDeletes ?? 50;
2267
- if (!Number.isInteger(maxDeletes) || maxDeletes < 1) {
2268
- return err("USAGE", { message: "--max-remote-deletes must be a positive integer" });
2269
- }
2270
- if (plannedDeletes.length > maxDeletes) {
2271
- return err("USAGE", { message: `remote delete cap exceeded: ${plannedDeletes.length} > ${maxDeletes}` });
2272
- }
2273
- const runner = input.rcloneRunner ?? defaultRcloneRunner;
2274
- for (const path of plannedDeletes) {
2275
- const result = await runner(["deletefile", path]);
2276
- if (result.exitCode !== 0) {
2277
- output.failed = { path, stderr: result.stderr };
2278
- return err("SYNC_PUSH_FAILED", { path, stderr: result.stderr });
2279
- }
2280
- output.deleted.push(path);
2281
- }
2282
- return ok(output);
2283
- }
2284
- async function defaultRcloneRunner(args) {
2285
- try {
2286
- const result = await execFileAsync("rclone", args, { encoding: "utf-8" });
2287
- return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
2288
- } catch (e) {
2289
- return {
2290
- exitCode: typeof e?.code === "number" ? e.code : 1,
2291
- stdout: typeof e?.stdout === "string" ? e.stdout : "",
2292
- stderr: typeof e?.stderr === "string" ? e.stderr : String(e)
2293
- };
2294
- }
2310
+ return planAndMaybePruneRemoteObjects(plannedDeletes, { ...input, defaultMaxDeletes: 50 });
2295
2311
  }
2296
2312
 
2297
2313
  // src/utils/safe-write.ts
@@ -2711,7 +2727,7 @@ function buildCliSurface() {
2711
2727
  program.command("health").option("--wiki <name>").option("--sync <mode>").option("--no-fail").option("--out <path>").option("--examples <n>");
2712
2728
  program.command("doctor");
2713
2729
  program.command("status").option("--wiki <name>");
2714
- program.command("archive").option("--wiki <name>").option("--cascade").option("--apply");
2730
+ program.command("archive").option("--wiki <name>").option("--cascade").option("--apply").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>");
2715
2731
  program.command("drift").option("--apply").option("--new <date>").option("--wiki <name>");
2716
2732
  program.command("dedup").option("--apply").option("--canonical-policy <policy>").option("--manifest-out <path>").option("--manifest-in <path>").option("--remote <remote>").option("--remote-delete").option("--max-remote-deletes <n>").option("--wiki <name>");
2717
2733
  program.command("migrate-citations").option("--dry-run").option("--wiki <name>");
@@ -8863,6 +8879,10 @@ export {
8863
8879
  runLogRotate,
8864
8880
  runTopicMapCheck,
8865
8881
  runIndexLinkFormat,
8882
+ normalizeRemoteRoot,
8883
+ buildRemoteObjectPath,
8884
+ isValidRemoteDeleteCap,
8885
+ planAndMaybePruneRemoteObjects,
8866
8886
  runDedup,
8867
8887
  safeWritePage,
8868
8888
  runFrontmatterFix,
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  TypedKnowledgeSchema,
7
7
  appendLastOp,
8
8
  assessSourceIdentity,
9
+ buildRemoteObjectPath,
9
10
  clearLastOp,
10
11
  configPath,
11
12
  detectSchema,
@@ -18,10 +19,13 @@ import {
18
19
  fixPathTooLong,
19
20
  isBlockedHost,
20
21
  isFailedRunStatus,
22
+ isValidRemoteDeleteCap,
21
23
  loadFleetManifest,
24
+ normalizeRemoteRoot,
22
25
  ok,
23
26
  parseDotenvFile,
24
27
  parseDotenvText,
28
+ planAndMaybePruneRemoteObjects,
25
29
  profileKey,
26
30
  readCliPackageJson,
27
31
  readLastOp,
@@ -69,7 +73,7 @@ import {
69
73
  triggerAutoUpdate,
70
74
  writeCache,
71
75
  writeDotenv
72
- } from "./chunk-6G7RAAOS.js";
76
+ } from "./chunk-OPXMAS4O.js";
73
77
  import {
74
78
  normalizeDistTag
75
79
  } from "./chunk-E6UWZ3S3.js";
@@ -1339,6 +1343,12 @@ function arraysEqual(a, b) {
1339
1343
  return true;
1340
1344
  }
1341
1345
  async function runArchive(input) {
1346
+ if (input.remoteDelete && !input.remote) {
1347
+ return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--remote-delete requires --remote" }) };
1348
+ }
1349
+ if (input.remoteDelete && !isValidRemoteDeleteCap(input.maxRemoteDeletes)) {
1350
+ return { exitCode: ExitCode.USAGE, result: err("USAGE", { message: "--max-remote-deletes must be a positive integer" }) };
1351
+ }
1342
1352
  const scan = await scanVault(input.vault);
1343
1353
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1344
1354
  const lookup = (pages) => {
@@ -1355,6 +1365,8 @@ async function runArchive(input) {
1355
1365
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
1356
1366
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
1357
1367
  const archivePath = join10("_archive", relPath).replace(/\\/g, "/");
1368
+ const remoteRoot = normalizeRemoteRoot(input.remote);
1369
+ const remoteObjectPath = buildRemoteObjectPath(remoteRoot, relPath);
1358
1370
  let cascade;
1359
1371
  if (input.cascade) {
1360
1372
  const wikilinkRefs = [];
@@ -1398,7 +1410,8 @@ async function runArchive(input) {
1398
1410
  index_updated: false,
1399
1411
  applied: false,
1400
1412
  cascade,
1401
- humanHint: summary
1413
+ ...remoteObjectPath ? { remote: { plannedDeletes: [remoteObjectPath], deleted: [] } } : {},
1414
+ humanHint: summary + (remoteObjectPath ? ` (remote planned ${input.remoteDelete ? "delete" : "preview"}: ${remoteObjectPath})` : "")
1402
1415
  })
1403
1416
  };
1404
1417
  }
@@ -1445,8 +1458,18 @@ ${fmRewritten}
1445
1458
  files: [relPath],
1446
1459
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1447
1460
  });
1461
+ let remote;
1462
+ if (remoteObjectPath) {
1463
+ const plannedDeletes = [remoteObjectPath];
1464
+ const pruned = await planAndMaybePruneRemoteObjects(plannedDeletes, input);
1465
+ if (!pruned.ok) {
1466
+ return { exitCode: ExitCode.SYNC_PUSH_FAILED, result: pruned };
1467
+ }
1468
+ remote = pruned.data;
1469
+ }
1448
1470
  const applied = input.cascade ? true : void 0;
1449
1471
  const cascadeNote = input.cascade ? ` (cascade: ${cascade.source_array_refs.length} src arrays updated, ${cascade.wikilink_refs.length} wikilinks reported)` : "";
1472
+ const remoteNote = remote ? ` (remote ${input.remoteDelete ? `deleted ${remote.deleted.length}` : `planned ${remote.plannedDeletes.length}`})` : "";
1450
1473
  return {
1451
1474
  exitCode: ExitCode.OK,
1452
1475
  result: ok({
@@ -1455,7 +1478,8 @@ ${fmRewritten}
1455
1478
  index_updated: indexUpdated,
1456
1479
  ...applied !== void 0 ? { applied } : {},
1457
1480
  ...cascade ? { cascade } : {},
1458
- humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}${cascadeNote}`
1481
+ ...remote ? { remote } : {},
1482
+ humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}${cascadeNote}${remoteNote}`
1459
1483
  })
1460
1484
  };
1461
1485
  }
@@ -4886,10 +4910,18 @@ program.command("status [vault]").description("output vault diagnostics").option
4886
4910
  langEnvValue: process.env.WIKI_LANG
4887
4911
  }), v.vault);
4888
4912
  });
4889
- program.command("archive <page> [vault]").description("archive a typed-knowledge or raw page").option("--wiki <name>", "wiki profile name").option("--cascade", "scan vault for references (wikilinks + sources arrays); preview by default", false).option("--apply", "with --cascade: mutate sources arrays and archive (without --apply, --cascade is preview-only)", false).action(async (page, vault, opts) => {
4913
+ program.command("archive <page> [vault]").description("archive a typed-knowledge or raw page").option("--wiki <name>", "wiki profile name").option("--cascade", "scan vault for references (wikilinks + sources arrays); preview by default", false).option("--apply", "with --cascade: mutate sources arrays and archive (without --apply, --cascade is preview-only)", false).option("--remote <remote>", "rclone remote root to prune the archived source path, for example seaweed-wiki:cloud/wiki").option("--remote-delete", "delete the archived source path from the remote after local archive", false).option("--max-remote-deletes <n>", "maximum remote object deletes allowed", "1").action(async (page, vault, opts) => {
4890
4914
  const v = await resolveVaultArg(vault, opts.wiki);
4891
4915
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
4892
- else emit(await runArchive({ vault: v.vault, page, cascade: !!opts.cascade, apply: !!opts.apply }), v.vault);
4916
+ else emit(await runArchive({
4917
+ vault: v.vault,
4918
+ page,
4919
+ cascade: !!opts.cascade,
4920
+ apply: !!opts.apply,
4921
+ remote: opts.remote,
4922
+ remoteDelete: !!opts.remoteDelete,
4923
+ maxRemoteDeletes: Number.parseInt(opts.maxRemoteDeletes, 10)
4924
+ }), v.vault);
4893
4925
  });
4894
4926
  program.command("drift [vault]").description("detect content drift in raw sources").option("--apply", "update sha256 in drifted sources").option("--new <date>", "list raw files ingested on/after this date (YYYY-MM-DD)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4895
4927
  const v = await resolveVaultArg(vault, opts.wiki);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-6G7RAAOS.js";
4
+ } from "./chunk-OPXMAS4O.js";
5
5
  import "./chunk-E6UWZ3S3.js";
6
6
 
7
7
  // src/mcp-entry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.9.31",
3
+ "version": "0.9.32",
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.31",
3
+ "version": "0.9.32",
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.31",
3
+ "version": "0.9.32",
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.31",
3
+ "version": "0.9.32",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -22,7 +22,7 @@ Standard four reads (SCHEMA, index, log, project context if applicable).
22
22
 
23
23
  0. Resolve vault: `skillwiki path` and `skillwiki lang`.
24
24
  1. Identify the target page. Confirm with the user which page to archive (show full relPath).
25
- 2. Run `skillwiki archive <page> [vault]`. Read the JSON output.
25
+ 2. Run `skillwiki archive <page> [vault]`. On a vault-sync leaf host where S3 stale originals must be pruned, use `skillwiki archive <page> [vault] --remote seaweed-wiki:cloud/wiki --remote-delete --max-remote-deletes 1` only when that remote path deletion is explicitly intended. Read the JSON output.
26
26
  3. Verify with `skillwiki index-check [vault]` — confirm no ghost entries remain.
27
27
  4. Run `skillwiki lint [vault]` — check for broken wikilinks from other pages that still reference the archived page. If found, update those pages to point to the replacement or remove the stale link.
28
28
  5. **Raw file archiving (N9 Reingest Protocol only):** When archiving a `raw/` file due to content drift, update ALL `^[raw/...]` citation markers and `sources:` frontmatter entries that reference the old path. Change `raw/articles/foo.md` to `_archive/raw/articles/foo.md` in every referencing page. Verify with `skillwiki audit` that no broken markers remain.
@@ -30,7 +30,7 @@ Standard four reads (SCHEMA, index, log, project context if applicable).
30
30
 
31
31
  ## Reversibility
32
32
 
33
- Archiving is reversible: move the file back from `_archive/` to its original directory and re-add the wikilink entry to `index.md`. No data is deleted.
33
+ Archiving is locally reversible: move the file back from `_archive/` to its original directory and re-add the wikilink entry to `index.md`. If `--remote-delete` was used, the stale active-path object is pruned from the remote after the archive move, but the archived copy remains and a restore will republish the active path on the next push.
34
34
 
35
35
  ## Stop conditions
36
36
 
@@ -42,4 +42,4 @@ Archiving is reversible: move the file back from `_archive/` to its original dir
42
42
  - Archiving `raw/` files outside the N9 Reingest Protocol (raw is immutable except during content-drift reingestion).
43
43
  - Archiving raw files without updating all `^[raw/...]` citation markers that reference them.
44
44
  - Archiving without user confirmation.
45
- - Deleting files (archive moves, never deletes).
45
+ - Deleting local vault files directly (archive moves locally; remote stale-path pruning is only allowed through the explicit `skillwiki archive --remote ... --remote-delete --max-remote-deletes 1` path).
@@ -22,7 +22,7 @@ Standard four reads (SCHEMA, index, log, project context if applicable).
22
22
 
23
23
  0. Resolve vault: `skillwiki path` and `skillwiki lang`.
24
24
  1. Identify the target page. Confirm with the user which page to archive (show full relPath).
25
- 2. Run `skillwiki archive <page> [vault]`. Read the JSON output.
25
+ 2. Run `skillwiki archive <page> [vault]`. On a vault-sync leaf host where S3 stale originals must be pruned, use `skillwiki archive <page> [vault] --remote seaweed-wiki:cloud/wiki --remote-delete --max-remote-deletes 1` only when that remote path deletion is explicitly intended. Read the JSON output.
26
26
  3. Verify with `skillwiki index-check [vault]` — confirm no ghost entries remain.
27
27
  4. Run `skillwiki lint [vault]` — check for broken wikilinks from other pages that still reference the archived page. If found, update those pages to point to the replacement or remove the stale link.
28
28
  5. **Raw file archiving (N9 Reingest Protocol only):** When archiving a `raw/` file due to content drift, update ALL `^[raw/...]` citation markers and `sources:` frontmatter entries that reference the old path. Change `raw/articles/foo.md` to `_archive/raw/articles/foo.md` in every referencing page. Verify with `skillwiki audit` that no broken markers remain.
@@ -30,7 +30,7 @@ Standard four reads (SCHEMA, index, log, project context if applicable).
30
30
 
31
31
  ## Reversibility
32
32
 
33
- Archiving is reversible: move the file back from `_archive/` to its original directory and re-add the wikilink entry to `index.md`. No data is deleted.
33
+ Archiving is locally reversible: move the file back from `_archive/` to its original directory and re-add the wikilink entry to `index.md`. If `--remote-delete` was used, the stale active-path object is pruned from the remote after the archive move, but the archived copy remains and a restore will republish the active path on the next push.
34
34
 
35
35
  ## Stop conditions
36
36
 
@@ -42,4 +42,4 @@ Archiving is reversible: move the file back from `_archive/` to its original dir
42
42
  - Archiving `raw/` files outside the N9 Reingest Protocol (raw is immutable except during content-drift reingestion).
43
43
  - Archiving raw files without updating all `^[raw/...]` citation markers that reference them.
44
44
  - Archiving without user confirmation.
45
- - Deleting files (archive moves, never deletes).
45
+ - Deleting local vault files directly (archive moves locally; remote stale-path pruning is only allowed through the explicit `skillwiki archive --remote ... --remote-delete --max-remote-deletes 1` path).