skillwiki 0.6.2-beta.1 → 0.8.0

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
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-TPS5XD2J.js";
9
9
 
10
10
  // src/cli.ts
11
- import { readFileSync as readFileSync11 } from "fs";
11
+ import { readFileSync as readFileSync12 } from "fs";
12
12
  import { join as join41 } from "path";
13
13
  import { Command as Command2 } from "commander";
14
14
 
@@ -2364,7 +2364,7 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
2364
2364
 
2365
2365
  // src/commands/lint.ts
2366
2366
  import { existsSync as existsSync3 } from "fs";
2367
- import { readFile as readFile15 } from "fs/promises";
2367
+ import { readFile as readFile15, rename as rename5 } from "fs/promises";
2368
2368
  import { join as join19 } from "path";
2369
2369
 
2370
2370
  // src/commands/topic-map-check.ts
@@ -2607,6 +2607,60 @@ async function runRawBodyDedup(vault) {
2607
2607
  };
2608
2608
  }
2609
2609
 
2610
+ // src/commands/path-too-long.ts
2611
+ var MAX_PATH_LENGTH = 240;
2612
+ async function runPathTooLong(input) {
2613
+ const scan = await scanVault(input.vault);
2614
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2615
+ const allPages = [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound];
2616
+ const violations = [];
2617
+ for (const page of allPages) {
2618
+ if (page.relPath.length > MAX_PATH_LENGTH) {
2619
+ violations.push({ relPath: page.relPath, length: page.relPath.length });
2620
+ }
2621
+ }
2622
+ if (violations.length > 0) {
2623
+ return {
2624
+ exitCode: ExitCode.LINT_HAS_ERRORS,
2625
+ result: ok({
2626
+ violations,
2627
+ humanHint: violations.map((v) => `${v.relPath}: ${v.length} chars (max ${MAX_PATH_LENGTH})`).join("\n")
2628
+ })
2629
+ };
2630
+ }
2631
+ return { exitCode: ExitCode.OK, result: ok({ violations, humanHint: "all paths within length limit" }) };
2632
+ }
2633
+ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
2634
+ if (relPath.length <= maxLength) return relPath;
2635
+ const lastSlash = relPath.lastIndexOf("/");
2636
+ const dir = lastSlash >= 0 ? relPath.slice(0, lastSlash) : "";
2637
+ const filename = lastSlash >= 0 ? relPath.slice(lastSlash + 1) : relPath;
2638
+ const hash = computeShortHash(relPath);
2639
+ const ext = filename.endsWith(".md") ? ".md" : "";
2640
+ const base = filename.endsWith(".md") ? filename.slice(0, -3) : filename;
2641
+ const suffix = `-${hash}${ext}`;
2642
+ const dirPrefix = dir ? dir + "/" : "";
2643
+ const maxPrefixLen = maxLength - dirPrefix.length - suffix.length;
2644
+ if (maxPrefixLen <= 0) {
2645
+ const fallback = dirPrefix + hash + ext;
2646
+ if (fallback.length > maxLength) {
2647
+ const dirBudget = maxLength - suffix.length;
2648
+ return dirPrefix.slice(0, Math.max(0, dirBudget)) + suffix;
2649
+ }
2650
+ return fallback;
2651
+ }
2652
+ const prefix = base.slice(0, maxPrefixLen).replace(/[-_\s]+$/, "");
2653
+ return dirPrefix + prefix + suffix;
2654
+ }
2655
+ function computeShortHash(input) {
2656
+ let hash = 2166136261;
2657
+ for (let i = 0; i < input.length; i++) {
2658
+ hash ^= input.charCodeAt(i);
2659
+ hash = Math.imul(hash, 16777619);
2660
+ }
2661
+ return (hash >>> 0).toString(16).padStart(8, "0").slice(0, 8);
2662
+ }
2663
+
2610
2664
  // src/utils/cli-surface.ts
2611
2665
  import { Command } from "commander";
2612
2666
  function buildCliSurface() {
@@ -2779,7 +2833,7 @@ function extractSourceEntries(rawFm) {
2779
2833
  }
2780
2834
  return entries;
2781
2835
  }
2782
- var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy"];
2836
+ var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
2783
2837
  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
2838
  var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
2785
2839
  async function runLint(input) {
@@ -2839,6 +2893,8 @@ async function runLint(input) {
2839
2893
  }
2840
2894
  const compoundRefs = await validateCompoundReferences(input.vault);
2841
2895
  if (compoundRefs.ok && compoundRefs.data.length > 0) buckets.compound_refs = compoundRefs.data;
2896
+ const pathCheck = await runPathTooLong({ vault: input.vault });
2897
+ if (pathCheck.result.ok && pathCheck.result.data.violations.length > 0) buckets.path_too_long = pathCheck.result.data.violations;
2842
2898
  const scan = await scanVault(input.vault);
2843
2899
  const allPages = scan.ok ? [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound] : [];
2844
2900
  const slugs = scan.ok ? buildSlugMap(allPages) : /* @__PURE__ */ new Map();
@@ -3353,6 +3409,45 @@ ${newBody}`;
3353
3409
  else delete buckets.file_source_url;
3354
3410
  }
3355
3411
  }
3412
+ const pathViolations = buckets.path_too_long;
3413
+ if (input.fix && pathViolations && pathViolations.length > 0) {
3414
+ const pathFixed = [];
3415
+ for (const v of pathViolations) {
3416
+ try {
3417
+ const absPath = `${input.vault}/${v.relPath}`;
3418
+ const newRelPath = truncateFilename(v.relPath);
3419
+ const newAbsPath = `${input.vault}/${newRelPath}`;
3420
+ await rename5(absPath, newAbsPath);
3421
+ const oldPathEscaped = v.relPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3422
+ for (const page of allPages) {
3423
+ if (page.relPath === v.relPath) continue;
3424
+ const content = await readFile15(page.absPath, "utf8");
3425
+ if (!content.includes(v.relPath)) continue;
3426
+ let updated = content;
3427
+ const citationRe = new RegExp(`\\^\\[${oldPathEscaped}\\]`, "g");
3428
+ updated = updated.replace(citationRe, `^[${newRelPath}]`);
3429
+ const wikilinkRe = new RegExp(`\\[\\[${oldPathEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
3430
+ updated = updated.replace(wikilinkRe, (_m, alias) => `[[${newRelPath}${alias ?? ""}]]`);
3431
+ if (updated !== content) {
3432
+ const w = await safeWritePage(page.absPath, updated);
3433
+ if (!w.ok) {
3434
+ unresolved.push(`${page.relPath} (rewire)`);
3435
+ }
3436
+ }
3437
+ }
3438
+ pathFixed.push(v.relPath);
3439
+ } catch {
3440
+ unresolved.push(v.relPath);
3441
+ }
3442
+ }
3443
+ fixed.push(...pathFixed);
3444
+ if (pathFixed.length > 0) {
3445
+ const fixedSet = new Set(pathFixed);
3446
+ const remaining = pathViolations.filter((v) => !fixedSet.has(v.relPath));
3447
+ if (remaining.length > 0) buckets.path_too_long = remaining;
3448
+ else delete buckets.path_too_long;
3449
+ }
3450
+ }
3356
3451
  }
3357
3452
  const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
3358
3453
  const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
@@ -3489,9 +3584,10 @@ async function runConfigPath(input) {
3489
3584
  }
3490
3585
 
3491
3586
  // src/commands/doctor.ts
3492
- import { existsSync as existsSync7, lstatSync, readlinkSync, readdirSync, statSync as statSync2 } from "fs";
3587
+ import { existsSync as existsSync7, lstatSync, readlinkSync, readdirSync, statSync as statSync2, readFileSync as readFileSync7 } from "fs";
3493
3588
  import { join as join24, resolve as resolve4 } from "path";
3494
3589
  import { execSync as execSync2 } from "child_process";
3590
+ import { platform as platform2 } from "os";
3495
3591
 
3496
3592
  // src/utils/auto-update.ts
3497
3593
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
@@ -4209,12 +4305,12 @@ function checkRcloneFlagAudit(resolvedPath) {
4209
4305
  }
4210
4306
  return check("pass", "rclone_flags", "rclone VFS flags", `PID ${pid}: all critical flags at safe values`);
4211
4307
  }
4212
- function checkRcloneVersion(resolvedPath) {
4213
- if (!resolvedPath) {
4308
+ function checkRcloneVersion(resolvedPath, vaultSyncInstalled) {
4309
+ if (!resolvedPath && !vaultSyncInstalled) {
4214
4310
  return check("pass", "rclone_version", "rclone version", "No vault path \u2014 check skipped");
4215
4311
  }
4216
- const fuse = detectFuseMount(resolvedPath);
4217
- if (!fuse) {
4312
+ const fuse = resolvedPath ? detectFuseMount(resolvedPath) : null;
4313
+ if (!fuse && !vaultSyncInstalled) {
4218
4314
  return check("pass", "rclone_version", "rclone version", "local disk \u2014 check skipped");
4219
4315
  }
4220
4316
  const ver = getRcloneVersion();
@@ -4326,6 +4422,301 @@ function checkVfsCacheHealth(resolvedPath) {
4326
4422
  `${stats.files} files, ${(stats.bytesUsed / 1024 / 1024).toFixed(1)}MB \u2014 clean (0 errored, 0 pending)`
4327
4423
  );
4328
4424
  }
4425
+ function readVaultSyncConfig(home) {
4426
+ try {
4427
+ const content = readFileSync7(join24(home, ".skillwiki", ".env"), "utf8");
4428
+ let installed = false;
4429
+ let role;
4430
+ for (const line of content.split(/\r?\n/)) {
4431
+ const trimmed = line.trim();
4432
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
4433
+ const eq = trimmed.indexOf("=");
4434
+ if (eq <= 0) continue;
4435
+ const k = trimmed.slice(0, eq).trim();
4436
+ const v = trimmed.slice(eq + 1).trim();
4437
+ if (v.length === 0) continue;
4438
+ if (k === "vault_sync.installed" && v === "true") installed = true;
4439
+ if (k === "vault_sync.role") role = v;
4440
+ }
4441
+ return { installed, role };
4442
+ } catch {
4443
+ return { installed: false };
4444
+ }
4445
+ }
4446
+ function vaultSyncChecks(input) {
4447
+ const os = input.os ?? platform2();
4448
+ const home = input.home;
4449
+ if (!input.vaultSyncInstalled) {
4450
+ const skip = (id, label) => check("pass", id, label, "vault-sync not installed \u2014 check skipped");
4451
+ return [
4452
+ skip("vault_sync_installed", "Vault sync installed"),
4453
+ skip("vault_sync_jobs_enabled", "Vault sync jobs enabled"),
4454
+ skip("vault_sync_last_push_age", "Vault sync last push recency"),
4455
+ skip("vault_sync_last_fetch_status", "Vault sync last fetch status"),
4456
+ skip("vault_sync_filter_present", "Vault sync filter file present"),
4457
+ skip("vault_sync_snapshot_guard", "Snapshot script guard")
4458
+ ];
4459
+ }
4460
+ const isMac = os === "darwin";
4461
+ const logDir = input.logDir ?? (isMac ? join24(home, "Library", "Logs") : join24(home, ".local", "state", "vault-sync", "log"));
4462
+ const shareDir = input.shareDir ?? (isMac ? join24(home, "Library", "Application Support", "vault-sync", "bin") : join24(home, ".local", "share", "vault-sync", "bin"));
4463
+ const filterPath = input.filterPath ?? join24(home, ".config", "rclone", "wiki-push-filters.txt");
4464
+ const snapshotPath = input.snapshotScriptPath ?? "/root/.hermes/scripts/wiki-snapshot-v3.sh";
4465
+ const pushScriptPath = join24(shareDir, "wiki-push.sh");
4466
+ const c1 = existsSync7(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
4467
+ let c2;
4468
+ try {
4469
+ if (isMac) {
4470
+ const uidStr = execSync2("id -u", {
4471
+ encoding: "utf8",
4472
+ timeout: 2e3,
4473
+ stdio: ["pipe", "pipe", "pipe"]
4474
+ }).trim();
4475
+ const uid = parseInt(uidStr, 10);
4476
+ execSync2(`launchctl print gui/${uid}/com.karlchow.wiki-push`, {
4477
+ encoding: "utf8",
4478
+ timeout: 2e3,
4479
+ stdio: ["pipe", "pipe", "pipe"]
4480
+ });
4481
+ c2 = check(
4482
+ "pass",
4483
+ "vault_sync_jobs_enabled",
4484
+ "Vault sync jobs enabled",
4485
+ "launchd: com.karlchow.wiki-push loaded"
4486
+ );
4487
+ } else {
4488
+ const out = execSync2("systemctl --user is-enabled wiki-push.timer", {
4489
+ encoding: "utf8",
4490
+ timeout: 2e3,
4491
+ stdio: ["pipe", "pipe", "pipe"]
4492
+ }).trim();
4493
+ if (out === "enabled") {
4494
+ c2 = check(
4495
+ "pass",
4496
+ "vault_sync_jobs_enabled",
4497
+ "Vault sync jobs enabled",
4498
+ "systemd: wiki-push.timer enabled"
4499
+ );
4500
+ } else {
4501
+ c2 = check(
4502
+ "error",
4503
+ "vault_sync_jobs_enabled",
4504
+ "Vault sync jobs enabled",
4505
+ `systemd: wiki-push.timer is ${out} \u2014 run vault-sync-install`
4506
+ );
4507
+ }
4508
+ }
4509
+ } catch {
4510
+ c2 = check(
4511
+ "error",
4512
+ "vault_sync_jobs_enabled",
4513
+ "Vault sync jobs enabled",
4514
+ "Scheduler check failed \u2014 run vault-sync-install"
4515
+ );
4516
+ }
4517
+ const logFile = join24(logDir, "wiki-push.log");
4518
+ let c3;
4519
+ try {
4520
+ const logContent = readFileSync7(logFile, "utf8");
4521
+ const lines = logContent.trim().split("\n").filter(Boolean);
4522
+ if (lines.length === 0) {
4523
+ c3 = check(
4524
+ "warn",
4525
+ "vault_sync_last_push_age",
4526
+ "Vault sync last push recency",
4527
+ "Log file is empty"
4528
+ );
4529
+ } else {
4530
+ const lastLine = lines[lines.length - 1];
4531
+ if (/FAIL/.test(lastLine)) {
4532
+ c3 = check(
4533
+ "error",
4534
+ "vault_sync_last_push_age",
4535
+ "Vault sync last push recency",
4536
+ `Last push failed: ${lastLine}`
4537
+ );
4538
+ } else if (/OK push/.test(lastLine)) {
4539
+ const tsMatch = lastLine.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/);
4540
+ if (tsMatch) {
4541
+ const lastPush = new Date(tsMatch[1]).getTime();
4542
+ const ageSec = (Date.now() - lastPush) / 1e3;
4543
+ if (ageSec <= 180) {
4544
+ c3 = check(
4545
+ "pass",
4546
+ "vault_sync_last_push_age",
4547
+ "Vault sync last push recency",
4548
+ `Last push ${ageSec.toFixed(0)}s ago`
4549
+ );
4550
+ } else {
4551
+ c3 = check(
4552
+ "warn",
4553
+ "vault_sync_last_push_age",
4554
+ "Vault sync last push recency",
4555
+ `Last push ${Math.round(ageSec)}s ago (>3 min)`
4556
+ );
4557
+ }
4558
+ } else {
4559
+ c3 = check(
4560
+ "warn",
4561
+ "vault_sync_last_push_age",
4562
+ "Vault sync last push recency",
4563
+ `Unparseable push line: ${lastLine.slice(0, 80)}`
4564
+ );
4565
+ }
4566
+ } else {
4567
+ c3 = check(
4568
+ "warn",
4569
+ "vault_sync_last_push_age",
4570
+ "Vault sync last push recency",
4571
+ `Last log entry: ${lastLine.slice(0, 80)}`
4572
+ );
4573
+ }
4574
+ }
4575
+ } catch {
4576
+ c3 = existsSync7(logDir) ? check(
4577
+ "warn",
4578
+ "vault_sync_last_push_age",
4579
+ "Vault sync last push recency",
4580
+ `Log file not found at ${logFile}`
4581
+ ) : check(
4582
+ "error",
4583
+ "vault_sync_last_push_age",
4584
+ "Vault sync last push recency",
4585
+ `Log directory not found at ${logDir}`
4586
+ );
4587
+ }
4588
+ const fetchLogFile = join24(logDir, "wiki-fetch.log");
4589
+ let cFetch;
4590
+ try {
4591
+ const logContent = readFileSync7(fetchLogFile, "utf8");
4592
+ const lines = logContent.trim().split("\n").filter(Boolean);
4593
+ if (lines.length === 0) {
4594
+ cFetch = check(
4595
+ "warn",
4596
+ "vault_sync_last_fetch_status",
4597
+ "Vault sync last fetch status",
4598
+ "Fetch log file is empty"
4599
+ );
4600
+ } else {
4601
+ const lastLine = lines[lines.length - 1];
4602
+ if (/fetch failed/i.test(lastLine)) {
4603
+ cFetch = check(
4604
+ "error",
4605
+ "vault_sync_last_fetch_status",
4606
+ "Vault sync last fetch status",
4607
+ `Last fetch failed: ${lastLine.slice(0, 100)}`
4608
+ );
4609
+ } else if (/OK/.test(lastLine)) {
4610
+ cFetch = check(
4611
+ "pass",
4612
+ "vault_sync_last_fetch_status",
4613
+ "Vault sync last fetch status",
4614
+ lastLine.slice(0, 100)
4615
+ );
4616
+ } else {
4617
+ cFetch = check(
4618
+ "warn",
4619
+ "vault_sync_last_fetch_status",
4620
+ "Vault sync last fetch status",
4621
+ `Last fetch log entry: ${lastLine.slice(0, 80)}`
4622
+ );
4623
+ }
4624
+ }
4625
+ } catch {
4626
+ cFetch = check(
4627
+ "warn",
4628
+ "vault_sync_last_fetch_status",
4629
+ "Vault sync last fetch status",
4630
+ `Fetch log not found at ${fetchLogFile}`
4631
+ );
4632
+ }
4633
+ let c4;
4634
+ try {
4635
+ if (!existsSync7(filterPath)) {
4636
+ c4 = check(
4637
+ "error",
4638
+ "vault_sync_filter_present",
4639
+ "Vault sync filter file present",
4640
+ `Filter file not found at ${filterPath}`
4641
+ );
4642
+ } else {
4643
+ const content = readFileSync7(filterPath, "utf8");
4644
+ const requiredExcludes = [
4645
+ "remotely-save/data.json",
4646
+ ".skillwiki/sync.lock",
4647
+ ".claude/settings.local.json"
4648
+ ];
4649
+ const missing = requiredExcludes.filter((ex) => !content.includes(ex));
4650
+ if (missing.length > 0) {
4651
+ c4 = check(
4652
+ "warn",
4653
+ "vault_sync_filter_present",
4654
+ "Vault sync filter file present",
4655
+ `Missing required excludes: ${missing.join(", ")}`
4656
+ );
4657
+ } else {
4658
+ c4 = check(
4659
+ "pass",
4660
+ "vault_sync_filter_present",
4661
+ "Vault sync filter file present",
4662
+ `Found with required excludes at ${filterPath}`
4663
+ );
4664
+ }
4665
+ }
4666
+ } catch {
4667
+ c4 = check(
4668
+ "error",
4669
+ "vault_sync_filter_present",
4670
+ "Vault sync filter file present",
4671
+ `Cannot read filter file at ${filterPath}`
4672
+ );
4673
+ }
4674
+ let c5;
4675
+ if (input.vaultSyncRole !== "snapshotter") {
4676
+ c5 = check(
4677
+ "pass",
4678
+ "vault_sync_snapshot_guard",
4679
+ "Snapshot script guard",
4680
+ "Not a snapshotter host \u2014 check skipped"
4681
+ );
4682
+ } else {
4683
+ try {
4684
+ if (!existsSync7(snapshotPath)) {
4685
+ c5 = check(
4686
+ "error",
4687
+ "vault_sync_snapshot_guard",
4688
+ "Snapshot script guard",
4689
+ `Snapshot script not found at ${snapshotPath}`
4690
+ );
4691
+ } else {
4692
+ const content = readFileSync7(snapshotPath, "utf8");
4693
+ if (!content.includes("--max-delete")) {
4694
+ c5 = check(
4695
+ "error",
4696
+ "vault_sync_snapshot_guard",
4697
+ "Snapshot script guard",
4698
+ `${snapshotPath} is missing --max-delete guard \u2014 dangerous without it`
4699
+ );
4700
+ } else {
4701
+ c5 = check(
4702
+ "pass",
4703
+ "vault_sync_snapshot_guard",
4704
+ "Snapshot script guard",
4705
+ `--max-delete present in ${snapshotPath}`
4706
+ );
4707
+ }
4708
+ }
4709
+ } catch {
4710
+ c5 = check(
4711
+ "error",
4712
+ "vault_sync_snapshot_guard",
4713
+ "Snapshot script guard",
4714
+ `Cannot read ${snapshotPath}`
4715
+ );
4716
+ }
4717
+ }
4718
+ return [c1, c2, c3, cFetch, c4, c5];
4719
+ }
4329
4720
  function findSkillMd(dir) {
4330
4721
  const results = [];
4331
4722
  let entries;
@@ -4360,6 +4751,7 @@ function findSkillNames(dir) {
4360
4751
  }
4361
4752
  async function runDoctor(input) {
4362
4753
  const checks = [];
4754
+ const vsConfig = readVaultSyncConfig(input.home);
4363
4755
  checks.push(checkNodeVersion());
4364
4756
  checks.push(checkCliChannels(input.argv, input.home));
4365
4757
  checks.push(await checkConfigFile(input.home));
@@ -4380,13 +4772,18 @@ async function runDoctor(input) {
4380
4772
  checks.push(checkDotStoreClean(resolvedPath));
4381
4773
  checks.push(checkS3MountPerf(resolvedPath));
4382
4774
  checks.push(checkRcloneFlagAudit(resolvedPath));
4383
- checks.push(checkRcloneVersion(resolvedPath));
4775
+ checks.push(checkRcloneVersion(resolvedPath, vsConfig.installed));
4384
4776
  checks.push(checkWriteTest(resolvedPath));
4385
4777
  checks.push(checkVfsCacheHealth(resolvedPath));
4386
4778
  checks.push(checkSkillsInstalled(input.home, input.cwd));
4387
4779
  checks.push(checkDuplicateSkills(input.home));
4388
4780
  checks.push(checkNpmUpdate(input.home, input.currentVersion));
4389
4781
  checks.push(checkPluginVersionDrift(input.home, input.currentVersion));
4782
+ checks.push(...vaultSyncChecks({
4783
+ home: input.home,
4784
+ vaultSyncInstalled: vsConfig.installed,
4785
+ vaultSyncRole: vsConfig.role
4786
+ }));
4390
4787
  const summary = {
4391
4788
  pass: checks.filter((c) => c.status === "pass").length,
4392
4789
  info: checks.filter((c) => c.status === "info").length,
@@ -4410,7 +4807,7 @@ async function runDoctor(input) {
4410
4807
  }
4411
4808
 
4412
4809
  // src/commands/archive.ts
4413
- import { rename as rename5, mkdir as mkdir8, readFile as readFile18, writeFile as writeFile9 } from "fs/promises";
4810
+ import { rename as rename6, mkdir as mkdir8, readFile as readFile18, writeFile as writeFile9 } from "fs/promises";
4414
4811
  import { join as join25, dirname as dirname9 } from "path";
4415
4812
  function countWikilinks(body, slug) {
4416
4813
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -4523,7 +4920,7 @@ ${fmRewritten}
4523
4920
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
4524
4921
  }
4525
4922
  }
4526
- await rename5(join25(input.vault, relPath), join25(input.vault, archivePath));
4923
+ await rename6(join25(input.vault, relPath), join25(input.vault, archivePath));
4527
4924
  appendLastOp(input.vault, {
4528
4925
  operation: input.cascade ? "archive-cascade" : "archive",
4529
4926
  summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
@@ -4909,7 +5306,7 @@ ${newBody}`;
4909
5306
 
4910
5307
  // src/commands/update.ts
4911
5308
  import { execSync as execSync3 } from "child_process";
4912
- import { readFileSync as readFileSync7 } from "fs";
5309
+ import { readFileSync as readFileSync8 } from "fs";
4913
5310
  import { join as join26 } from "path";
4914
5311
  function resolveGlobalSkillsRoot() {
4915
5312
  try {
@@ -4939,7 +5336,7 @@ async function refreshInstalledSkills(target) {
4939
5336
  }
4940
5337
  async function runUpdate(input) {
4941
5338
  const pkg2 = JSON.parse(
4942
- readFileSync7(new URL("../../package.json", import.meta.url), "utf8")
5339
+ readFileSync8(new URL("../../package.json", import.meta.url), "utf8")
4943
5340
  );
4944
5341
  const currentVersion = pkg2.version;
4945
5342
  const tag = input.distTag ?? "latest";
@@ -5013,12 +5410,12 @@ async function runUpdate(input) {
5013
5410
 
5014
5411
  // src/commands/self-update.ts
5015
5412
  import { execSync as execSync4 } from "child_process";
5016
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
5413
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
5017
5414
  import { join as join27 } from "path";
5018
5415
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
5019
5416
  async function runSelfUpdate(input) {
5020
5417
  const currentVersion = JSON.parse(
5021
- readFileSync8(new URL("../../package.json", import.meta.url), "utf8")
5418
+ readFileSync9(new URL("../../package.json", import.meta.url), "utf8")
5022
5419
  ).version;
5023
5420
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
5024
5421
  const localPkgPath = join27(sourceRoot, "packages", "cli", "package.json");
@@ -5029,7 +5426,7 @@ async function runSelfUpdate(input) {
5029
5426
  if (hasLocalSource) {
5030
5427
  source = "local";
5031
5428
  try {
5032
- availableVersion = JSON.parse(readFileSync8(localPkgPath, "utf8")).version ?? null;
5429
+ availableVersion = JSON.parse(readFileSync9(localPkgPath, "utf8")).version ?? null;
5033
5430
  } catch {
5034
5431
  availableVersion = null;
5035
5432
  }
@@ -5087,7 +5484,7 @@ async function runSelfUpdate(input) {
5087
5484
  }
5088
5485
  const newVersion = (() => {
5089
5486
  try {
5090
- return JSON.parse(readFileSync8(localPkgPath, "utf8")).version ?? "unknown";
5487
+ return JSON.parse(readFileSync9(localPkgPath, "utf8")).version ?? "unknown";
5091
5488
  } catch {
5092
5489
  return "unknown";
5093
5490
  }
@@ -6127,7 +6524,7 @@ import { existsSync as existsSync12 } from "fs";
6127
6524
  import { join as join34 } from "path";
6128
6525
 
6129
6526
  // src/utils/sync-lock.ts
6130
- import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
6527
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync10, renameSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
6131
6528
  import { join as join33 } from "path";
6132
6529
  import { createHash as createHash6 } from "crypto";
6133
6530
  function getSessionId() {
@@ -6142,7 +6539,7 @@ function readLock(vault) {
6142
6539
  const path = lockPath(vault);
6143
6540
  if (!existsSync11(path)) return null;
6144
6541
  try {
6145
- const raw = readFileSync9(path, "utf8");
6542
+ const raw = readFileSync10(path, "utf8");
6146
6543
  return JSON.parse(raw);
6147
6544
  } catch {
6148
6545
  return null;
@@ -6430,6 +6827,7 @@ async function runSyncPull(input) {
6430
6827
  let pulled = false;
6431
6828
  let conflicts = 0;
6432
6829
  let filesUpdated = 0;
6830
+ let autoResolved = 0;
6433
6831
  try {
6434
6832
  const pullOutput = gitStrict(vault, ["pull", "--rebase", "origin", "HEAD"]);
6435
6833
  pulled = true;
@@ -6438,25 +6836,71 @@ async function runSyncPull(input) {
6438
6836
  } catch (e) {
6439
6837
  const errString = String(e);
6440
6838
  if (errString.includes("conflict")) {
6441
- const porcelain = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
6442
- conflicts = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
6839
+ let inConflict = true;
6840
+ while (inConflict) {
6841
+ const stoppedSha = git(vault, ["rev-parse", "--verify", "REBASE_HEAD"]);
6842
+ let commitMsg = "";
6843
+ if (stoppedSha) {
6844
+ commitMsg = git(vault, ["log", "--format=%s", "-1", stoppedSha]);
6845
+ }
6846
+ const isArchiveOrSnapshot = commitMsg.startsWith("archive: moved") || commitMsg.startsWith("Snapshot ");
6847
+ const conflictedFiles = git(vault, ["diff", "--name-only", "--diff-filter=U"]);
6848
+ const conflictedList = conflictedFiles ? conflictedFiles.split("\n").filter((l) => l.trim().length > 0) : [];
6849
+ if (conflictedList.length === 0) {
6850
+ try {
6851
+ gitStrict(vault, ["rebase", "--continue"]);
6852
+ inConflict = true;
6853
+ } catch {
6854
+ inConflict = false;
6855
+ }
6856
+ continue;
6857
+ }
6858
+ if (isArchiveOrSnapshot) {
6859
+ for (const f of conflictedList) {
6860
+ try {
6861
+ gitStrict(vault, ["checkout", "--ours", f]);
6862
+ gitStrict(vault, ["add", f]);
6863
+ } catch {
6864
+ }
6865
+ }
6866
+ autoResolved += conflictedList.length;
6867
+ try {
6868
+ gitStrict(vault, ["rebase", "--continue"]);
6869
+ } catch (continueErr) {
6870
+ continue;
6871
+ }
6872
+ } else {
6873
+ conflicts = conflictedList.length;
6874
+ return {
6875
+ exitCode: ExitCode.SYNC_PULL_FAILED,
6876
+ result: ok({
6877
+ fetched,
6878
+ pulled: false,
6879
+ files_updated: 0,
6880
+ conflicts,
6881
+ auto_resolved: 0,
6882
+ lint_errors: 0,
6883
+ lint_warnings: 0,
6884
+ humanHint: `pull failed with ${conflicts} conflict(s) on non-archive commit "${commitMsg}" \u2014 resolve manually`
6885
+ })
6886
+ };
6887
+ }
6888
+ }
6889
+ if (autoResolved > 0) {
6890
+ const diffOutput = git(vault, ["diff", "--stat", "HEAD@{1}..HEAD"]);
6891
+ if (diffOutput) {
6892
+ const fileMatch = diffOutput.match(/(\d+) file[s]? changed/);
6893
+ if (fileMatch) filesUpdated = parseInt(fileMatch[1], 10);
6894
+ }
6895
+ pulled = true;
6896
+ conflicts = 0;
6897
+ }
6898
+ } else {
6443
6899
  return {
6444
6900
  exitCode: ExitCode.SYNC_PULL_FAILED,
6445
- result: ok({
6446
- fetched,
6447
- pulled: false,
6448
- files_updated: 0,
6449
- conflicts,
6450
- lint_errors: 0,
6451
- lint_warnings: 0,
6452
- humanHint: `pull failed with ${conflicts} conflict(s) \u2014 resolve manually`
6453
- })
6901
+ result: err("GIT_PULL_FAILED", { message: errString })
6454
6902
  };
6455
6903
  }
6456
- return {
6457
- exitCode: ExitCode.SYNC_PULL_FAILED,
6458
- result: err("GIT_PULL_FAILED", { message: errString })
6459
- };
6460
6904
  }
6461
6905
  let lintErrors = 0;
6462
6906
  let lintWarnings = 0;
@@ -6468,6 +6912,7 @@ async function runSyncPull(input) {
6468
6912
  const hintParts = [];
6469
6913
  if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
6470
6914
  else hintParts.push("already up to date");
6915
+ if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
6471
6916
  if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
6472
6917
  if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
6473
6918
  const exitCode = lintErrors > 0 ? ExitCode.LINT_HAS_ERRORS : lintWarnings > 0 ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
@@ -6478,6 +6923,7 @@ async function runSyncPull(input) {
6478
6923
  pulled,
6479
6924
  files_updated: filesUpdated,
6480
6925
  conflicts,
6926
+ auto_resolved: autoResolved,
6481
6927
  lint_errors: lintErrors,
6482
6928
  lint_warnings: lintWarnings,
6483
6929
  humanHint: hintParts.join(", ")
@@ -6598,7 +7044,7 @@ function runSyncUnlock(input) {
6598
7044
  }
6599
7045
 
6600
7046
  // src/commands/backup.ts
6601
- import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync10, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
7047
+ import { statSync as statSync4, readdirSync as readdirSync2, readFileSync as readFileSync11, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
6602
7048
  import { join as join35, relative as relative3, dirname as dirname11 } from "path";
6603
7049
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
6604
7050
 
@@ -6665,7 +7111,7 @@ async function runBackupSync(input) {
6665
7111
  continue;
6666
7112
  }
6667
7113
  try {
6668
- const body = readFileSync10(absPath);
7114
+ const body = readFileSync11(absPath);
6669
7115
  await client.send(new PutObjectCommand({ Bucket: input.bucket, Key: relPath, Body: body }));
6670
7116
  uploaded++;
6671
7117
  } catch {
@@ -7292,7 +7738,7 @@ async function postCommit(vault, exitCode) {
7292
7738
  }
7293
7739
 
7294
7740
  // src/cli.ts
7295
- var pkg = JSON.parse(readFileSync11(new URL("../package.json", import.meta.url), "utf8"));
7741
+ var pkg = JSON.parse(readFileSync12(new URL("../package.json", import.meta.url), "utf8"));
7296
7742
  var program = new Command2();
7297
7743
  program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
7298
7744
  program.option("--human", "render terminal-readable output instead of JSON");
@@ -7440,7 +7886,7 @@ program.command("log-rotate [vault]").description("rotate or trim the vault log
7440
7886
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7441
7887
  else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }), v.vault);
7442
7888
  });
7443
- program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix legacy_citation_style violations").option("--only <bucket>", "run only the specified lint bucket").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
7889
+ program.command("lint [vault]").description("run all vault health checks").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).option("--fix", "auto-fix supported lint violations").option("--only <bucket>", "run only the specified lint bucket").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
7444
7890
  const v = await resolveVaultArg(vault, opts.wiki);
7445
7891
  if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
7446
7892
  else emit(await runLint({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.6.2-beta.1",
3
+ "version": "0.8.0",
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.6.2-beta.1",
3
+ "version": "0.8.0",
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.6.2-beta.1",
3
+ "version": "0.8.0",
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.6.2-beta.1",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -8,7 +8,7 @@ Capture ad-hoc ideas, bugs, tasks, and notes into the vault. Three entry points
8
8
  | Entry | When | What happens |
9
9
  |-------|------|-------------|
10
10
  | `/wiki-add-task <text>` | You're in a Claude Code session (NOT Hermes compact) | Creates `raw/transcripts/YYYY-MM-DD-{type}-{slug}.md` with ad-hoc capture frontmatter |
11
- | `skillwiki add-task <text>` | Hermes Agent compact mode | Same as above — compact-compatible CLI trigger |
11
+ | Filesystem drop | Hermes Agent compact mode (no slash commands available) | Same as above — create `.md` in `raw/transcripts/`, dev-loop discovers it |
12
12
  | Filesystem drop | You're NOT in a Claude session (Obsidian, editor, sync) | Create any `.md` file in `raw/transcripts/` using the vault template — dev-loop discovers it on next cycle |
13
13
  | Dev-loop discovery | Automatic, next cycle | Scans `raw/transcripts/` for new files since last cycle, surfaces as claimable work |
14
14
  **Path Rule:** Captures ALWAYS go to `$(skillwiki path)/raw/transcripts/` (Layer 1). Never under `projects/{slug}/raw/` — that violates SCHEMA.md Layer 1 immutability.