skillwiki 0.6.0 → 0.6.1
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
|
@@ -2780,7 +2780,7 @@ function extractSourceEntries(rawFm) {
|
|
|
2780
2780
|
return entries;
|
|
2781
2781
|
}
|
|
2782
2782
|
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy"];
|
|
2783
|
-
var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
|
|
2783
|
+
var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "file_source_url", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
|
|
2784
2784
|
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
|
|
2785
2785
|
async function runLint(input) {
|
|
2786
2786
|
const buckets = {};
|
|
@@ -2865,6 +2865,16 @@ async function runLint(input) {
|
|
|
2865
2865
|
if (subDirDupes.length > 0) {
|
|
2866
2866
|
buckets.raw_subdirectory_duplicate = subDirDupes;
|
|
2867
2867
|
}
|
|
2868
|
+
const fileSourceUrlFlags = [];
|
|
2869
|
+
for (const raw of scan.data.raw) {
|
|
2870
|
+
const text = await readPage(raw);
|
|
2871
|
+
const split = splitFrontmatter(text);
|
|
2872
|
+
if (!split.ok) continue;
|
|
2873
|
+
if (/^source_url:\s*file:\/\//m.test(split.data.rawFrontmatter)) {
|
|
2874
|
+
fileSourceUrlFlags.push(raw.relPath);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
if (fileSourceUrlFlags.length > 0) buckets.file_source_url = fileSourceUrlFlags;
|
|
2868
2878
|
const legacyPages = [];
|
|
2869
2879
|
const orphanedPages = [];
|
|
2870
2880
|
const structFlags = [];
|
|
@@ -3304,6 +3314,45 @@ ${newBody}`;
|
|
|
3304
3314
|
else delete buckets.wikilink_citation;
|
|
3305
3315
|
}
|
|
3306
3316
|
}
|
|
3317
|
+
if (input.fix && fileSourceUrlFlags.length > 0) {
|
|
3318
|
+
const FILE_FIXED = [];
|
|
3319
|
+
for (const relPath of fileSourceUrlFlags) {
|
|
3320
|
+
try {
|
|
3321
|
+
const absPath = `${input.vault}/${relPath}`;
|
|
3322
|
+
const raw = await readFile15(absPath, "utf8");
|
|
3323
|
+
const parts = raw.split("---", 3);
|
|
3324
|
+
if (parts.length < 3) {
|
|
3325
|
+
unresolved.push(relPath);
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
const rawFm = parts[1];
|
|
3329
|
+
const rest = parts[2];
|
|
3330
|
+
const sourceMatch = rest.match(/^source:\s*"?(https?:\/\/[^\s\n"]+)"?\s*$/m);
|
|
3331
|
+
if (!sourceMatch) {
|
|
3332
|
+
unresolved.push(relPath);
|
|
3333
|
+
continue;
|
|
3334
|
+
}
|
|
3335
|
+
const realUrl = sourceMatch[1];
|
|
3336
|
+
const newRawFm = rawFm.replace(/^source_url:\s*file:\/\/[^\n]+/m, `source_url: ${realUrl}`);
|
|
3337
|
+
const newContent = `---${newRawFm}---${rest}`;
|
|
3338
|
+
const w = await safeWritePage(absPath, newContent);
|
|
3339
|
+
if (!w.ok) {
|
|
3340
|
+
unresolved.push(relPath);
|
|
3341
|
+
continue;
|
|
3342
|
+
}
|
|
3343
|
+
FILE_FIXED.push(relPath);
|
|
3344
|
+
} catch {
|
|
3345
|
+
unresolved.push(relPath);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
fixed.push(...FILE_FIXED);
|
|
3349
|
+
if (FILE_FIXED.length > 0) {
|
|
3350
|
+
const fixedSet = new Set(FILE_FIXED);
|
|
3351
|
+
const remaining = fileSourceUrlFlags.filter((p) => !fixedSet.has(p));
|
|
3352
|
+
if (remaining.length > 0) buckets.file_source_url = remaining;
|
|
3353
|
+
else delete buckets.file_source_url;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3307
3356
|
}
|
|
3308
3357
|
const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3309
3358
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
@@ -3681,7 +3730,7 @@ function detectFuseMount(vaultPath) {
|
|
|
3681
3730
|
best = { point, fsType: `fuse.${match[1].split(":")[0] || "unknown"}` };
|
|
3682
3731
|
}
|
|
3683
3732
|
}
|
|
3684
|
-
if (best) return best;
|
|
3733
|
+
if (best) return { mountPoint: best.point, fsType: best.fsType };
|
|
3685
3734
|
}
|
|
3686
3735
|
} catch {
|
|
3687
3736
|
}
|
|
@@ -4363,6 +4412,17 @@ async function runDoctor(input) {
|
|
|
4363
4412
|
// src/commands/archive.ts
|
|
4364
4413
|
import { rename as rename5, mkdir as mkdir8, readFile as readFile18, writeFile as writeFile9 } from "fs/promises";
|
|
4365
4414
|
import { join as join25, dirname as dirname9 } from "path";
|
|
4415
|
+
function countWikilinks(body, slug) {
|
|
4416
|
+
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4417
|
+
const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
|
|
4418
|
+
const m = body.match(re);
|
|
4419
|
+
return m ? m.length : 0;
|
|
4420
|
+
}
|
|
4421
|
+
function arraysEqual(a, b) {
|
|
4422
|
+
if (a.length !== b.length) return false;
|
|
4423
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
4424
|
+
return true;
|
|
4425
|
+
}
|
|
4366
4426
|
async function runArchive(input) {
|
|
4367
4427
|
const scan = await scanVault(input.vault);
|
|
4368
4428
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -4378,14 +4438,81 @@ async function runArchive(input) {
|
|
|
4378
4438
|
}
|
|
4379
4439
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
4380
4440
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
4441
|
+
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
4381
4442
|
const archivePath = join25("_archive", relPath).replace(/\\/g, "/");
|
|
4443
|
+
let cascade;
|
|
4444
|
+
if (input.cascade) {
|
|
4445
|
+
const wikilinkRefs = [];
|
|
4446
|
+
const sourceArrayRefs = [];
|
|
4447
|
+
for (const page of scan.data.typedKnowledge) {
|
|
4448
|
+
if (page.relPath === relPath) continue;
|
|
4449
|
+
const text = await readPage(page);
|
|
4450
|
+
const split = splitFrontmatter(text);
|
|
4451
|
+
if (!split.ok) continue;
|
|
4452
|
+
const wl = countWikilinks(split.data.body, slug);
|
|
4453
|
+
if (wl > 0) wikilinkRefs.push({ page: page.relPath, count: wl });
|
|
4454
|
+
const fm = extractFrontmatter(text);
|
|
4455
|
+
if (!fm.ok) continue;
|
|
4456
|
+
const sources = fm.data.sources;
|
|
4457
|
+
if (Array.isArray(sources) && sources.includes(relPath)) {
|
|
4458
|
+
const before = sources.filter((s) => typeof s === "string");
|
|
4459
|
+
const after = before.filter((s) => s !== relPath);
|
|
4460
|
+
sourceArrayRefs.push({ page: page.relPath, sources_before: before, sources_after: after });
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
const indexRefs = [];
|
|
4464
|
+
if (!isRaw) {
|
|
4465
|
+
try {
|
|
4466
|
+
const idx = await readFile18(join25(input.vault, "index.md"), "utf8");
|
|
4467
|
+
idx.split("\n").forEach((line, i) => {
|
|
4468
|
+
if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
|
|
4469
|
+
});
|
|
4470
|
+
} catch (e) {
|
|
4471
|
+
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
cascade = { wikilink_refs: wikilinkRefs, index_refs: indexRefs, source_array_refs: sourceArrayRefs };
|
|
4475
|
+
}
|
|
4476
|
+
if (input.cascade && !input.apply) {
|
|
4477
|
+
const summary = `DRY-RUN \u2014 would archive ${relPath}; ${cascade.wikilink_refs.length} wikilink ref(s), ${cascade.index_refs.length} index ref(s), ${cascade.source_array_refs.length} source array ref(s).`;
|
|
4478
|
+
return {
|
|
4479
|
+
exitCode: ExitCode.OK,
|
|
4480
|
+
result: ok({
|
|
4481
|
+
archived_from: relPath,
|
|
4482
|
+
archived_to: archivePath,
|
|
4483
|
+
index_updated: false,
|
|
4484
|
+
applied: false,
|
|
4485
|
+
cascade,
|
|
4486
|
+
humanHint: summary
|
|
4487
|
+
})
|
|
4488
|
+
};
|
|
4489
|
+
}
|
|
4490
|
+
if (input.cascade && input.apply && cascade) {
|
|
4491
|
+
for (const ref of cascade.source_array_refs) {
|
|
4492
|
+
const absPath = join25(input.vault, ref.page);
|
|
4493
|
+
const text = await readFile18(absPath, "utf8");
|
|
4494
|
+
const split = splitFrontmatter(text);
|
|
4495
|
+
if (!split.ok) continue;
|
|
4496
|
+
const before = split.data.rawFrontmatter;
|
|
4497
|
+
const newSourcesYaml = ref.sources_after.length === 0 ? "sources: []" : "sources:\n" + ref.sources_after.map((s) => ` - ${s}`).join("\n");
|
|
4498
|
+
const fmRewritten = before.replace(
|
|
4499
|
+
/^sources:\s*(?:\[[^\]]*\]|(?:\r?\n(?:\s*-\s.*))+)/m,
|
|
4500
|
+
newSourcesYaml
|
|
4501
|
+
);
|
|
4502
|
+
if (fmRewritten === before) continue;
|
|
4503
|
+
if (!arraysEqual(ref.sources_after, ref.sources_before)) {
|
|
4504
|
+
await writeFile9(absPath, `---
|
|
4505
|
+
${fmRewritten}
|
|
4506
|
+
---${split.data.body}`, "utf8");
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4382
4510
|
await mkdir8(dirname9(join25(input.vault, archivePath)), { recursive: true });
|
|
4383
4511
|
let indexUpdated = false;
|
|
4384
4512
|
if (!isRaw) {
|
|
4385
4513
|
const indexPath = join25(input.vault, "index.md");
|
|
4386
4514
|
try {
|
|
4387
4515
|
const idx = await readFile18(indexPath, "utf8");
|
|
4388
|
-
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
4389
4516
|
const originalLines = idx.split("\n");
|
|
4390
4517
|
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
4391
4518
|
if (filtered.length !== originalLines.length) {
|
|
@@ -4398,12 +4525,24 @@ async function runArchive(input) {
|
|
|
4398
4525
|
}
|
|
4399
4526
|
await rename5(join25(input.vault, relPath), join25(input.vault, archivePath));
|
|
4400
4527
|
appendLastOp(input.vault, {
|
|
4401
|
-
operation: "archive",
|
|
4402
|
-
summary: `moved ${relPath} to ${archivePath}`,
|
|
4528
|
+
operation: input.cascade ? "archive-cascade" : "archive",
|
|
4529
|
+
summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
|
|
4403
4530
|
files: [relPath],
|
|
4404
4531
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4405
4532
|
});
|
|
4406
|
-
|
|
4533
|
+
const applied = input.cascade ? true : void 0;
|
|
4534
|
+
const cascadeNote = input.cascade ? ` (cascade: ${cascade.source_array_refs.length} src arrays updated, ${cascade.wikilink_refs.length} wikilinks reported)` : "";
|
|
4535
|
+
return {
|
|
4536
|
+
exitCode: ExitCode.OK,
|
|
4537
|
+
result: ok({
|
|
4538
|
+
archived_from: relPath,
|
|
4539
|
+
archived_to: archivePath,
|
|
4540
|
+
index_updated: indexUpdated,
|
|
4541
|
+
...applied !== void 0 ? { applied } : {},
|
|
4542
|
+
...cascade ? { cascade } : {},
|
|
4543
|
+
humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}${cascadeNote}`
|
|
4544
|
+
})
|
|
4545
|
+
};
|
|
4407
4546
|
}
|
|
4408
4547
|
|
|
4409
4548
|
// src/commands/drift.ts
|
|
@@ -7310,10 +7449,10 @@ program.command("status [vault]").description("output vault diagnostics").option
|
|
|
7310
7449
|
langEnvValue: process.env.WIKI_LANG
|
|
7311
7450
|
}), v.vault);
|
|
7312
7451
|
});
|
|
7313
|
-
program.command("archive <page> [vault]").description("archive a typed-knowledge or raw page").option("--wiki <name>", "wiki profile name").action(async (page, vault, opts) => {
|
|
7452
|
+
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) => {
|
|
7314
7453
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
7315
7454
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
7316
|
-
else emit(await runArchive({ vault: v.vault, page }), v.vault);
|
|
7455
|
+
else emit(await runArchive({ vault: v.vault, page, cascade: !!opts.cascade, apply: !!opts.apply }), v.vault);
|
|
7317
7456
|
});
|
|
7318
7457
|
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) => {
|
|
7319
7458
|
const v = await resolveVaultArg(vault, opts.wiki);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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": {
|
package/skills/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: 0.
|
|
2
|
+
version: 0.6.1
|
|
3
3
|
name: wiki-sync
|
|
4
|
-
description: Safely sync the vault git repository — multi-session safe via advisory lockfile. Runs skillwiki sync status, then guides push or pull with lint guards and conflict resolution.
|
|
4
|
+
description: Safely sync the vault git repository — multi-session safe via advisory lockfile. Handles rebase conflict storms from archive-commit × snapshot-stream patterns. Runs skillwiki sync status, then guides push or pull with lint guards and conflict resolution.
|
|
5
5
|
---
|
|
6
6
|
# wiki-sync
|
|
7
7
|
## When This Skill Activates
|
|
@@ -143,6 +143,31 @@ When `git pull --rebase` reports `CONFLICT (modify/delete)`:
|
|
|
143
143
|
- `git -C "$VAULT" add <path>` — keep the local restoration (rebase continues)
|
|
144
144
|
4. `git -C "$VAULT" rebase --continue`.
|
|
145
145
|
|
|
146
|
+
### Rebase conflict storm (archive commits × snapshot stream)
|
|
147
|
+
|
|
148
|
+
When many local archive-only commits (e.g., `archive: moved X to _archive/`) are rebased over an origin/main that receives frequent snapshot commits (e.g., sg01 `Snapshot YYYYMMDD_HHMMSS`), every archive commit re-triggers the same content conflicts on shared files (`log.md`, `knowledge.md`, `spec.md`). This is predictable and can be resolved systematically.
|
|
149
|
+
|
|
150
|
+
**Detection**: 3+ consecutive rebase stops on commits whose message matches `^archive: moved`.
|
|
151
|
+
|
|
152
|
+
**Resolution**: For each archive commit during the storm:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Apply --ours to all conflicting files (keep HEAD = origin/main + snapshots)
|
|
156
|
+
for f in $(git -C "$VAULT" diff --name-only --diff-filter=U); do
|
|
157
|
+
git -C "$VAULT" checkout --ours "$f" && git -C "$VAULT" add "$f"
|
|
158
|
+
done
|
|
159
|
+
git -C "$VAULT" rebase --continue
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**After the storm passes** (non-archive commits or clean rebase), pop the stash and handle any remaining conflicts per the normal Conflict Resolution sections above.
|
|
163
|
+
|
|
164
|
+
**Prevention**:
|
|
165
|
+
- Sync more frequently — don't let local fall >5 commits behind origin/main
|
|
166
|
+
- Bundle archive commits — `skillwiki archive --batch` groups 5-10 transcript archives into one commit, reducing rebase surface
|
|
167
|
+
- For vaults with snapshot cron, prefer smaller, more frequent syncs over large batch rebases
|
|
168
|
+
|
|
169
|
+
See `concepts/wiki-sync-rebase-conflict-storm-pattern.md` for detailed analysis.
|
|
170
|
+
|
|
146
171
|
## Multi-device coordination
|
|
147
172
|
When the user mentions editing from Obsidian desktop and Claude Code on a server (or any two-device setup):
|
|
148
173
|
- Recommend pulling before every editing session on each device.
|