skillwiki 0.9.28 → 0.9.30

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
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ExitCode,
4
+ FLEET_REL_PATH,
5
+ SATELLITE_STALE_MS,
4
6
  TypedKnowledgeSchema,
5
7
  appendLastOp,
6
8
  assessSourceIdentity,
@@ -8,11 +10,15 @@ import {
8
10
  configPath,
9
11
  detectSchema,
10
12
  err,
13
+ evaluateSatelliteRunHealth,
11
14
  extractCitationMarkers,
12
15
  extractFrontmatter,
13
16
  extractTaxonomy,
17
+ findPlugin,
14
18
  fixPathTooLong,
15
19
  isBlockedHost,
20
+ isFailedRunStatus,
21
+ loadFleetManifest,
16
22
  ok,
17
23
  parseDotenvFile,
18
24
  parseDotenvText,
@@ -20,6 +26,8 @@ import {
20
26
  readCliPackageJson,
21
27
  readLastOp,
22
28
  readPage,
29
+ readSatelliteLatestRunFromText,
30
+ resolveFleetHostId,
23
31
  resolveInitTimePath,
24
32
  resolveRuntimePath,
25
33
  runAudit,
@@ -31,6 +39,7 @@ import {
31
39
  runDoctor,
32
40
  runFleetContext,
33
41
  runFleetValidate,
42
+ runFrontmatterFix,
34
43
  runGraphBuild,
35
44
  runIndexCheck,
36
45
  runIndexLinkFormat,
@@ -53,19 +62,20 @@ import {
53
62
  runTopicMapCheck,
54
63
  runValidate,
55
64
  safeWritePage,
65
+ satelliteLatestRunPath,
56
66
  scanSensitiveContent,
57
67
  scanVault,
58
68
  splitFrontmatter,
59
69
  triggerAutoUpdate,
60
70
  writeCache,
61
71
  writeDotenv
62
- } from "./chunk-IP3RKVMF.js";
72
+ } from "./chunk-JENSKJP6.js";
63
73
  import {
64
74
  normalizeDistTag
65
75
  } from "./chunk-E6UWZ3S3.js";
66
76
 
67
77
  // src/cli.ts
68
- import { join as join24 } from "path";
78
+ import { join as join25 } from "path";
69
79
  import { Command } from "commander";
70
80
 
71
81
  // src/utils/output.ts
@@ -297,6 +307,28 @@ async function runInstall(input) {
297
307
  if (entries.length === 0) {
298
308
  return { exitCode: ExitCode.PREFLIGHT_FAILED, result: err("PREFLIGHT_FAILED", { reason: "no skills found" }) };
299
309
  }
310
+ const defaultTarget = join2(input.home, ".claude", "skills");
311
+ const isDefaultTarget = resolve(input.target) === resolve(defaultTarget);
312
+ const plugin = input.force || !isDefaultTarget ? null : findPlugin(input.home);
313
+ if (plugin) {
314
+ const manifest_path2 = join2(input.target, "wiki-manifest.json");
315
+ const hintLines2 = [
316
+ `deferred to plugin: skillwiki@llm-wiki v${plugin.version}`,
317
+ `plugin provides skills at ${plugin.installPath}`,
318
+ `use --force to install CLI copies into ${input.target} anyway`
319
+ ];
320
+ return {
321
+ exitCode: ExitCode.OK,
322
+ result: ok({
323
+ installed: [],
324
+ backed_up: [],
325
+ manifest_path: manifest_path2,
326
+ version_warnings: [],
327
+ deferred_to_plugin: true,
328
+ humanHint: hintLines2.join("\n")
329
+ })
330
+ };
331
+ }
300
332
  const installed = [];
301
333
  const backed_up = [];
302
334
  const version_warnings = [];
@@ -377,7 +409,7 @@ async function runInstall(input) {
377
409
  for (const w of version_warnings) hintLines.push(` ${w}`);
378
410
  }
379
411
  const exitCode = version_warnings.length > 0 ? ExitCode.SKILL_VERSION_MISMATCH : ExitCode.OK;
380
- return { exitCode, result: ok({ installed, backed_up, manifest_path, version_warnings, humanHint: hintLines.join("\n") }) };
412
+ return { exitCode, result: ok({ installed, backed_up, manifest_path, version_warnings, deferred_to_plugin: false, humanHint: hintLines.join("\n") }) };
381
413
  }
382
414
 
383
415
  // src/commands/path.ts
@@ -1737,82 +1769,6 @@ ${migratedBody}${newFooter}`;
1737
1769
  };
1738
1770
  }
1739
1771
 
1740
- // src/commands/frontmatter-fix.ts
1741
- function isoToday() {
1742
- return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1743
- }
1744
- function fixFrontmatter(rawFm) {
1745
- const additions = [];
1746
- if (!/^created:/m.test(rawFm)) additions.push(`created: ${isoToday()}`);
1747
- if (!/^updated:/m.test(rawFm)) additions.push(`updated: ${isoToday()}`);
1748
- if (!/^tags:/m.test(rawFm)) additions.push("tags: []");
1749
- if (!/^sources:/m.test(rawFm)) additions.push("sources: []");
1750
- if (!/^provenance:/m.test(rawFm)) additions.push("provenance: research");
1751
- if (additions.length === 0) return rawFm;
1752
- return rawFm.trimEnd() + "\n" + additions.join("\n") + "\n";
1753
- }
1754
- function removeOrphanTagsLines(body) {
1755
- return body.split("\n").filter((line) => !/^tags:\s*\[/.test(line.trim())).join("\n");
1756
- }
1757
- async function runFrontmatterFix(input) {
1758
- const scan = await scanVault(input.vault);
1759
- if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1760
- const fixed = [];
1761
- const skipped = [];
1762
- let unchanged = 0;
1763
- for (const page of scan.data.typedKnowledge) {
1764
- const text = await readPage(page);
1765
- const split = splitFrontmatter(text);
1766
- if (!split.ok) {
1767
- skipped.push(page.relPath);
1768
- continue;
1769
- }
1770
- const { rawFrontmatter, body } = split.data;
1771
- const newFm = fixFrontmatter(rawFrontmatter);
1772
- const newBody = removeOrphanTagsLines(body);
1773
- const newText = `---
1774
- ${newFm}
1775
- ---
1776
- ${newBody}`;
1777
- if (newText === text) {
1778
- unchanged++;
1779
- continue;
1780
- }
1781
- if (!input.dryRun) {
1782
- const w = await safeWritePage(page.absPath, newText);
1783
- if (!w.ok) {
1784
- skipped.push(page.relPath);
1785
- continue;
1786
- }
1787
- }
1788
- fixed.push(page.relPath);
1789
- }
1790
- const exitCode = fixed.length > 0 ? ExitCode.MIGRATION_APPLIED : ExitCode.OK;
1791
- const hintLines = [`scanned: ${fixed.length + skipped.length + unchanged}`];
1792
- if (fixed.length > 0) hintLines.push(`fixed: ${fixed.length}`);
1793
- if (skipped.length > 0) hintLines.push(`skipped (parse error): ${skipped.length}`);
1794
- if (unchanged > 0) hintLines.push(`unchanged: ${unchanged}`);
1795
- if (input.dryRun && fixed.length > 0) hintLines.push("(dry run \u2014 no files written)");
1796
- if (!input.dryRun && fixed.length > 0) {
1797
- appendLastOp(input.vault, {
1798
- operation: "frontmatter-fix",
1799
- summary: `normalized frontmatter on ${fixed.length} page(s)`,
1800
- files: fixed,
1801
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1802
- });
1803
- }
1804
- return {
1805
- exitCode,
1806
- result: ok({
1807
- scanned: fixed.length + skipped.length + unchanged,
1808
- fixed,
1809
- skipped,
1810
- unchanged,
1811
- humanHint: hintLines.join("\n")
1812
- })
1813
- };
1814
- }
1815
-
1816
1772
  // src/commands/update.ts
1817
1773
  import { execSync } from "child_process";
1818
1774
  import { join as join11 } from "path";
@@ -1827,19 +1783,23 @@ function resolveGlobalSkillsRoot() {
1827
1783
  return null;
1828
1784
  }
1829
1785
  }
1830
- async function refreshInstalledSkills(target) {
1786
+ async function refreshInstalledSkills(home, target) {
1831
1787
  const skillsRoot = resolveGlobalSkillsRoot();
1832
1788
  if (!skillsRoot) {
1833
- return { warnings: ["could not locate global skillwiki installation for skill refresh"], refreshed: false };
1789
+ return { warnings: ["could not locate global skillwiki installation for skill refresh"], refreshed: false, deferred_to_plugin: false };
1834
1790
  }
1835
1791
  try {
1836
- const result = await runInstall({ skillsRoot, target, dryRun: false, symlink: false });
1792
+ const result = await runInstall({ skillsRoot, target, dryRun: false, symlink: false, home, force: false });
1837
1793
  if (result.result.ok) {
1838
- return { warnings: result.result.data.version_warnings, refreshed: true };
1794
+ return {
1795
+ warnings: result.result.data.version_warnings,
1796
+ refreshed: !result.result.data.deferred_to_plugin,
1797
+ deferred_to_plugin: result.result.data.deferred_to_plugin
1798
+ };
1839
1799
  }
1840
- return { warnings: [`skill refresh failed: ${result.result.error}`], refreshed: false };
1800
+ return { warnings: [`skill refresh failed: ${result.result.error}`], refreshed: false, deferred_to_plugin: false };
1841
1801
  } catch (e) {
1842
- return { warnings: [`skill refresh error: ${String(e)}`], refreshed: false };
1802
+ return { warnings: [`skill refresh error: ${String(e)}`], refreshed: false, deferred_to_plugin: false };
1843
1803
  }
1844
1804
  }
1845
1805
  async function runUpdate(input) {
@@ -1875,6 +1835,7 @@ async function runUpdate(input) {
1875
1835
  wasAlreadyLatest: true,
1876
1836
  version_warnings: [],
1877
1837
  skills_refreshed: false,
1838
+ deferred_to_plugin: false,
1878
1839
  humanHint: `Already on npm@${tag}: v${currentVersion}`
1879
1840
  })
1880
1841
  };
@@ -1891,12 +1852,13 @@ async function runUpdate(input) {
1891
1852
  };
1892
1853
  }
1893
1854
  writeCache(input.home, { ...cache, updateAppliedAt: Date.now() });
1894
- const installResult = await refreshInstalledSkills(target);
1855
+ const installResult = await refreshInstalledSkills(input.home, target);
1895
1856
  const version_warnings = installResult.warnings;
1896
1857
  const skills_refreshed = installResult.refreshed;
1858
+ const deferred_to_plugin = installResult.deferred_to_plugin;
1897
1859
  const hintLines = [
1898
1860
  `Updated skillwiki ${currentVersion} \u2192 ${latest} via npm@${tag}`,
1899
- `skills refreshed: ${skills_refreshed}`
1861
+ deferred_to_plugin ? `skills deferred to plugin channel (skillwiki@llm-wiki)` : `skills refreshed: ${skills_refreshed}`
1900
1862
  ];
1901
1863
  if (version_warnings.length > 0) {
1902
1864
  hintLines.push(`version warnings: ${version_warnings.length}`);
@@ -1910,6 +1872,7 @@ async function runUpdate(input) {
1910
1872
  wasAlreadyLatest: false,
1911
1873
  version_warnings,
1912
1874
  skills_refreshed,
1875
+ deferred_to_plugin,
1913
1876
  humanHint: hintLines.join("\n")
1914
1877
  })
1915
1878
  };
@@ -2388,7 +2351,10 @@ async function runSessionBrief(input) {
2388
2351
  const transcripts = await loadTranscriptInfo(scan.data.raw);
2389
2352
  const workItems = await loadWorkItems(scan.data.workItems);
2390
2353
  const digests = await loadTrendDigests(scan.data.typedKnowledge);
2391
- const healthWarnings = await loadHealthWarnings(input.vault);
2354
+ const healthWarnings = [
2355
+ ...await loadHealthWarnings(input.vault),
2356
+ ...satelliteHealthWarnings(await loadSatelliteHealth(input.vault))
2357
+ ];
2392
2358
  const memoryTopics = project ? await loadMemoryTopics(input.vault, project) : [];
2393
2359
  const latestLogs = newest(transcripts.filter((t) => t.kind === "session-log"), 3);
2394
2360
  const unclaimedCaptures = newest(transcripts.filter((t) => {
@@ -2598,6 +2564,31 @@ function appendTextSection(lines, title, items, empty) {
2598
2564
  }
2599
2565
  lines.push("");
2600
2566
  }
2567
+ async function loadSatelliteHealth(vaultPath) {
2568
+ const health = evaluateSatelliteRunHealth(vaultPath, /* @__PURE__ */ new Date());
2569
+ if (!health.failed && !health.stale) {
2570
+ const runPath2 = satelliteLatestRunPath(vaultPath);
2571
+ const hasRun = await readIfExists(runPath2);
2572
+ if (!hasRun) return null;
2573
+ }
2574
+ if (health.failed) {
2575
+ const fc = health.failureClass && health.failureClass.length > 0 ? health.failureClass : "unknown";
2576
+ return `agent-memory-trends: last run failed (${fc})`;
2577
+ }
2578
+ if (health.stale && health.finishedAt) {
2579
+ const finishedMs = Date.parse(health.finishedAt);
2580
+ if (!Number.isNaN(finishedMs)) {
2581
+ const ageHours = (Date.now() - finishedMs) / (60 * 60 * 1e3);
2582
+ const hoursRounded = Math.floor(ageHours);
2583
+ const datePart = health.finishedAt.slice(0, 10);
2584
+ return `agent-memory-trends: no run in ${hoursRounded}h (last: ${datePart})`;
2585
+ }
2586
+ }
2587
+ return null;
2588
+ }
2589
+ function satelliteHealthWarnings(warning) {
2590
+ return warning ? [warning] : [];
2591
+ }
2601
2592
  async function loadHealthWarnings(vault) {
2602
2593
  const text = await readIfExists(join15(vault, ".skillwiki", "health.json"));
2603
2594
  if (!text) return [];
@@ -4326,16 +4317,333 @@ written: ${outPath}`
4326
4317
  };
4327
4318
  }
4328
4319
 
4329
- // src/utils/auto-commit.ts
4330
- import { existsSync as existsSync10 } from "fs";
4320
+ // src/commands/fleet-health.ts
4321
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
4322
+ import { execSync as nodeExecSync } from "child_process";
4323
+ import { hostname as nodeHostname, platform as nodePlatform } from "os";
4331
4324
  import { join as join23 } from "path";
4325
+ var SSH_TIMEOUT_MS = 15e3;
4326
+ var TIMER_UNIT = "agent-memory-trends.timer";
4327
+ var SERVICE_UNIT = "agent-memory-trends.service";
4328
+ var SYSTEMD_SERVICE_FAILED_CLASS = "SYSTEMD_SERVICE_FAILED";
4329
+ function defaultDeps() {
4330
+ return {
4331
+ platform: () => nodePlatform(),
4332
+ execSync: nodeExecSync
4333
+ };
4334
+ }
4335
+ function formatAgeHours(finishedAt) {
4336
+ if (!finishedAt) return "never";
4337
+ const ts = Date.parse(finishedAt);
4338
+ if (!Number.isFinite(ts)) return "unknown";
4339
+ const hours = Math.floor((Date.now() - ts) / (60 * 60 * 1e3));
4340
+ if (hours < 1) return "<1h";
4341
+ return `${hours}h`;
4342
+ }
4343
+ function deriveRunFields(parsed) {
4344
+ if (!parsed) {
4345
+ return {
4346
+ last_run_status: "unknown",
4347
+ last_run_age: "never",
4348
+ failure_class: "-",
4349
+ runUnhealthy: false
4350
+ };
4351
+ }
4352
+ const finishedAt = parsed.finished_at;
4353
+ const age = formatAgeHours(finishedAt);
4354
+ const fc = parsed.failure_class != null && String(parsed.failure_class).length > 0 ? String(parsed.failure_class) : "-";
4355
+ if (isFailedRunStatus(parsed.status ?? "")) {
4356
+ return {
4357
+ last_run_status: "fail",
4358
+ last_run_age: age,
4359
+ failure_class: fc === "-" ? "fail" : fc,
4360
+ runUnhealthy: true
4361
+ };
4362
+ }
4363
+ if (finishedAt) {
4364
+ const ts = Date.parse(finishedAt);
4365
+ if (Number.isFinite(ts) && Date.now() - ts > SATELLITE_STALE_MS) {
4366
+ return {
4367
+ last_run_status: "success",
4368
+ last_run_age: age,
4369
+ failure_class: "-",
4370
+ runUnhealthy: true
4371
+ };
4372
+ }
4373
+ }
4374
+ if (parsed.status === "success" || parsed.status === "ok") {
4375
+ return {
4376
+ last_run_status: "success",
4377
+ last_run_age: age,
4378
+ failure_class: "-",
4379
+ runUnhealthy: false
4380
+ };
4381
+ }
4382
+ return {
4383
+ last_run_status: parsed.status ? "unknown" : "none",
4384
+ last_run_age: age,
4385
+ failure_class: "-",
4386
+ runUnhealthy: false
4387
+ };
4388
+ }
4389
+ function systemctlPropLocal(deps, unit, prop) {
4390
+ if (deps.platform() !== "linux") return null;
4391
+ try {
4392
+ return deps.execSync(`systemctl ${prop} ${unit}`, {
4393
+ encoding: "utf8",
4394
+ timeout: 2e3,
4395
+ stdio: ["pipe", "pipe", "pipe"]
4396
+ }).trim();
4397
+ } catch {
4398
+ return null;
4399
+ }
4400
+ }
4401
+ function systemctlIsActiveLocal(deps, unit) {
4402
+ const out = systemctlPropLocal(deps, unit, "is-active");
4403
+ if (out === null) return "unknown";
4404
+ return out === "active" ? "active" : "inactive";
4405
+ }
4406
+ function systemctlIsFailedLocal(deps, unit) {
4407
+ return systemctlPropLocal(deps, unit, "is-failed") === "failed";
4408
+ }
4409
+ function applyServiceFailedOverlay(run, serviceFailed) {
4410
+ if (!serviceFailed) return run;
4411
+ return {
4412
+ ...run,
4413
+ last_run_status: "fail",
4414
+ failure_class: run.failure_class === "-" ? SYSTEMD_SERVICE_FAILED_CLASS : run.failure_class,
4415
+ runUnhealthy: true
4416
+ };
4417
+ }
4418
+ function probeLocal(vaultPath, deps) {
4419
+ const latestPath = satelliteLatestRunPath(vaultPath);
4420
+ let parsed = null;
4421
+ if (existsSync10(latestPath)) {
4422
+ try {
4423
+ const wire = readSatelliteLatestRunFromText(readFileSync6(latestPath, "utf8"));
4424
+ if (wire) {
4425
+ parsed = {
4426
+ status: wire.status,
4427
+ finished_at: wire.finishedAt,
4428
+ failure_class: wire.failureClass ?? null
4429
+ };
4430
+ }
4431
+ } catch {
4432
+ parsed = null;
4433
+ }
4434
+ }
4435
+ const timer = systemctlIsActiveLocal(deps, TIMER_UNIT);
4436
+ const serviceFailed = systemctlIsFailedLocal(deps, SERVICE_UNIT);
4437
+ const run = applyServiceFailedOverlay(deriveRunFields(parsed), serviceFailed);
4438
+ const timerBad = deps.platform() === "linux" && timer === "inactive";
4439
+ return {
4440
+ timer,
4441
+ last_run_status: run.last_run_status,
4442
+ last_run_age: run.last_run_age,
4443
+ failure_class: run.failure_class,
4444
+ reachable: "yes",
4445
+ runUnhealthy: run.runUnhealthy,
4446
+ timerBad
4447
+ };
4448
+ }
4449
+ function probeRemote(sshAlias, vaultPath, deps) {
4450
+ const latest = satelliteLatestRunPath(vaultPath);
4451
+ const remoteCmd = [
4452
+ `cat ${shellQuote2(latest)} 2>/dev/null || true`,
4453
+ "echo __SW_TIMER__",
4454
+ `systemctl is-active ${TIMER_UNIT} 2>/dev/null || echo inactive`,
4455
+ "echo __SW_FAILED__",
4456
+ `systemctl is-failed ${SERVICE_UNIT} 2>/dev/null || echo unknown`
4457
+ ].join("\n");
4458
+ const sshCmd = `ssh -o ConnectTimeout=10 ${sshAlias} ${shellQuote2(remoteCmd)}`;
4459
+ try {
4460
+ const out = deps.execSync(sshCmd, {
4461
+ encoding: "utf8",
4462
+ timeout: SSH_TIMEOUT_MS,
4463
+ stdio: ["pipe", "pipe", "pipe"]
4464
+ });
4465
+ const timerMarker = "__SW_TIMER__";
4466
+ const failedMarker = "__SW_FAILED__";
4467
+ const timerIdx = out.indexOf(timerMarker);
4468
+ const jsonPart = timerIdx >= 0 ? out.slice(0, timerIdx) : out;
4469
+ const afterTimer = timerIdx >= 0 ? out.slice(timerIdx + timerMarker.length) : "";
4470
+ const failedIdx = afterTimer.indexOf(failedMarker);
4471
+ const timerRaw = failedIdx >= 0 ? afterTimer.slice(0, failedIdx) : afterTimer;
4472
+ const failedRaw = failedIdx >= 0 ? afterTimer.slice(failedIdx + failedMarker.length) : "";
4473
+ const jsonText = jsonPart.trim();
4474
+ const parsed = jsonText.startsWith("{") ? (() => {
4475
+ const wire = readSatelliteLatestRunFromText(jsonText);
4476
+ if (!wire) return null;
4477
+ return {
4478
+ status: wire.status,
4479
+ finished_at: wire.finishedAt,
4480
+ failure_class: wire.failureClass ?? null
4481
+ };
4482
+ })() : null;
4483
+ const timerLine = timerRaw.trim().split("\n")[0]?.trim() ?? "unknown";
4484
+ let timer = "unknown";
4485
+ if (timerLine === "active") timer = "active";
4486
+ else if (timerLine === "inactive") timer = "inactive";
4487
+ const failedLine = failedRaw.trim().split("\n")[0]?.trim() ?? "unknown";
4488
+ const serviceFailed = failedLine === "failed";
4489
+ const run = applyServiceFailedOverlay(deriveRunFields(parsed), serviceFailed);
4490
+ const timerBad = timer === "inactive";
4491
+ return {
4492
+ timer,
4493
+ last_run_status: run.last_run_status,
4494
+ last_run_age: run.last_run_age,
4495
+ failure_class: run.failure_class,
4496
+ reachable: "yes",
4497
+ runUnhealthy: run.runUnhealthy,
4498
+ timerBad
4499
+ };
4500
+ } catch {
4501
+ return {
4502
+ timer: "unknown",
4503
+ last_run_status: "unknown",
4504
+ last_run_age: "never",
4505
+ failure_class: "-",
4506
+ reachable: "no",
4507
+ runUnhealthy: true,
4508
+ timerBad: true
4509
+ };
4510
+ }
4511
+ }
4512
+ function shellQuote2(s) {
4513
+ return `'${s.replace(/'/g, `'\\''`)}'`;
4514
+ }
4515
+ function satelliteHosts(manifest) {
4516
+ const out = [];
4517
+ for (const [id, host] of Object.entries(manifest.hosts)) {
4518
+ const sat = host.maintenance?.skillwiki_satellite;
4519
+ if (sat?.enabled === true) {
4520
+ out.push({ id, vaultPath: sat.vault_path, sshAlias: sat.ssh_alias, host });
4521
+ }
4522
+ }
4523
+ return out.sort((a, b) => a.id.localeCompare(b.id));
4524
+ }
4525
+ function hasDeclaredSshAccess(host, localHostId, satelliteAlias) {
4526
+ const profile = host.access?.from?.[localHostId];
4527
+ if (!profile) return false;
4528
+ if (profile.status !== "configured" && profile.status !== "local") return false;
4529
+ const aliases = profile.ssh_aliases ?? [];
4530
+ return aliases.includes(satelliteAlias);
4531
+ }
4532
+ function formatTable(rows) {
4533
+ if (rows.length === 0) return "no satellite hosts configured";
4534
+ const header = "host | timer | last_run_status | last_run_age | failure_class | reachable";
4535
+ const lines = rows.map(
4536
+ (r) => `${r.host} | ${r.timer} | ${r.last_run_status} | ${r.last_run_age} | ${r.failure_class} | ${r.reachable}`
4537
+ );
4538
+ return [header, ...lines].join("\n");
4539
+ }
4540
+ function rowHealthy(reachable, timer, runUnhealthy, timerBad, platform2) {
4541
+ if (reachable === "no-access") return true;
4542
+ if (reachable === "no") return false;
4543
+ if (runUnhealthy) return false;
4544
+ if (platform2 === "linux" && timerBad) return false;
4545
+ if (platform2 === "linux" && timer === "inactive") return false;
4546
+ return true;
4547
+ }
4548
+ async function runFleetHealth(input) {
4549
+ const deps = input.deps ?? defaultDeps();
4550
+ const env = input.env ?? process.env;
4551
+ const home = input.home ?? env.HOME ?? "";
4552
+ const osHostname = input.osHostname ?? env.HOSTNAME ?? nodeHostname();
4553
+ const vault = input.vault ?? env.WIKI_PATH;
4554
+ const file = input.file ?? (vault ? join23(vault, FLEET_REL_PATH) : void 0);
4555
+ if (!file) {
4556
+ return {
4557
+ exitCode: ExitCode.NO_VAULT_CONFIGURED,
4558
+ result: err("NO_VAULT_CONFIGURED", { message: "vault path or --file required" })
4559
+ };
4560
+ }
4561
+ const loaded = await loadFleetManifest(file);
4562
+ if (!loaded.ok) {
4563
+ if (loaded.error === "FILE_NOT_FOUND") {
4564
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: file }) };
4565
+ }
4566
+ return {
4567
+ exitCode: ExitCode.FLEET_MANIFEST_INVALID,
4568
+ result: err("INVALID_FLEET_MANIFEST", { path: file })
4569
+ };
4570
+ }
4571
+ const resolved = await resolveFleetHostId({
4572
+ manifest: loaded.manifest,
4573
+ hostId: input.hostId,
4574
+ env,
4575
+ home,
4576
+ osHostname
4577
+ });
4578
+ const localHostId = resolved.hostId;
4579
+ const targets = satelliteHosts(loaded.manifest);
4580
+ if (targets.length === 0) {
4581
+ const humanHint2 = "no satellite hosts configured";
4582
+ return {
4583
+ exitCode: ExitCode.OK,
4584
+ result: ok({ hosts: [], humanHint: humanHint2 })
4585
+ };
4586
+ }
4587
+ const rows = [];
4588
+ for (const t of targets) {
4589
+ const isLocal = localHostId === t.id;
4590
+ if (isLocal) {
4591
+ const p = probeLocal(t.vaultPath, deps);
4592
+ const healthy = rowHealthy(p.reachable, p.timer, p.runUnhealthy, p.timerBad, deps.platform());
4593
+ rows.push({
4594
+ host: t.id,
4595
+ timer: p.timer,
4596
+ last_run_status: p.last_run_status,
4597
+ last_run_age: p.last_run_age,
4598
+ failure_class: p.failure_class,
4599
+ reachable: p.reachable,
4600
+ healthy
4601
+ });
4602
+ } else {
4603
+ if (!hasDeclaredSshAccess(t.host, localHostId, t.sshAlias)) {
4604
+ rows.push({
4605
+ host: t.id,
4606
+ timer: "unknown",
4607
+ last_run_status: "unknown",
4608
+ last_run_age: "never",
4609
+ failure_class: "-",
4610
+ reachable: "no-access",
4611
+ healthy: true
4612
+ });
4613
+ continue;
4614
+ }
4615
+ const p = probeRemote(t.sshAlias, t.vaultPath, deps);
4616
+ const healthy = rowHealthy(p.reachable, p.timer, p.runUnhealthy, p.timerBad, "linux");
4617
+ rows.push({
4618
+ host: t.id,
4619
+ timer: p.timer,
4620
+ last_run_status: p.last_run_status,
4621
+ last_run_age: p.last_run_age,
4622
+ failure_class: p.failure_class,
4623
+ reachable: p.reachable,
4624
+ healthy
4625
+ });
4626
+ }
4627
+ }
4628
+ const allHealthy = rows.every((r) => r.healthy);
4629
+ const humanHint = formatTable(rows);
4630
+ const exitCode = allHealthy ? ExitCode.OK : ExitCode.FLEET_SATELLITE_HEALTH_FAILED;
4631
+ return {
4632
+ exitCode,
4633
+ result: ok({ hosts: rows, humanHint })
4634
+ };
4635
+ }
4636
+
4637
+ // src/utils/auto-commit.ts
4638
+ import { existsSync as existsSync11 } from "fs";
4639
+ import { join as join24 } from "path";
4332
4640
  async function postCommit(vault, exitCode) {
4333
4641
  if (exitCode !== 0) return;
4334
4642
  const home = process.env.HOME ?? "";
4335
4643
  const dotenv = await parseDotenvFile(configPath(home));
4336
4644
  const autoCommit = process.env.AUTO_COMMIT ?? dotenv["AUTO_COMMIT"];
4337
4645
  if (autoCommit === "false") return;
4338
- if (!existsSync10(join23(vault, ".git"))) return;
4646
+ if (!existsSync11(join24(vault, ".git"))) return;
4339
4647
  const lastOps = readLastOp(vault);
4340
4648
  if (lastOps.length === 0) return;
4341
4649
  const porcelain = git(vault, ["status", "--porcelain", "--", ...VAULT_COMMIT_PATHSPEC]);
@@ -4381,7 +4689,7 @@ program.command("validate <file>").description("validate vault page frontmatter
4381
4689
  emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
4382
4690
  });
4383
4691
  program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
4384
- const out = opts.out ?? join24(vault, ".skillwiki", "graph.json");
4692
+ const out = opts.out ?? join25(vault, ".skillwiki", "graph.json");
4385
4693
  emit(await runGraphBuild({ vault, out }), vault);
4386
4694
  });
4387
4695
  var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
@@ -4407,9 +4715,9 @@ program.command("orphans [vault]").description("find pages not referenced by any
4407
4715
  wiki: opts.wiki
4408
4716
  })));
4409
4717
  program.command("audit <file>").description("audit citation markers and source provenance for a vault page").action(async (file) => emit(await runAudit({ file })));
4410
- program.command("install").description("install skillwiki SKILL.md files into ~/.claude/skills/").option("--target <dir>", "target install directory", `${process.env.HOME ?? ""}/.claude/skills/`).option("--dry-run", "preview only", false).option("--skills-root <dir>", "source skills directory (defaults to packaged)").option("--symlink", "create symlinks instead of copies (dev mode \u2014 edits to source are immediately visible)", false).action(async (opts) => {
4718
+ program.command("install").description("install skillwiki SKILL.md files into ~/.claude/skills/").option("--target <dir>", "target install directory", `${process.env.HOME ?? ""}/.claude/skills/`).option("--dry-run", "preview only", false).option("--skills-root <dir>", "source skills directory (defaults to packaged)").option("--symlink", "create symlinks instead of copies (dev mode \u2014 edits to source are immediately visible)", false).option("--force", "install CLI copies even when the skillwiki@llm-wiki plugin channel is active", false).action(async (opts) => {
4411
4719
  const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
4412
- emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun, symlink: !!opts.symlink }));
4720
+ emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun, symlink: !!opts.symlink, home: process.env.HOME ?? "", force: !!opts.force }));
4413
4721
  });
4414
4722
  program.command("path").description("show the resolved vault path").option("--vault <dir>", "explicit vault override (runtime)").option("--target <dir>", "explicit target override (init-time)").option("--wiki <name>", "wiki profile name").option("--init-time", "use init-time chain instead of runtime", false).option("--explain", "include resolution chain in output", false).action(async (opts) => {
4415
4723
  const initTime = !!opts.initTime;
@@ -4830,6 +5138,20 @@ fleetCmd.command("context [vault]").description("render compact Runtime Host Con
4830
5138
  user: process.env.USER
4831
5139
  }));
4832
5140
  });
5141
+ fleetCmd.command("health [vault]").description("read-only health probe for skillwiki satellite hosts").option("--file <path>", "fleet manifest path").option("--host-id <id>", "explicit current fleet host id").option("--json", "emit JSON result").action(async (vault, opts) => {
5142
+ const r = await runFleetHealth({
5143
+ vault,
5144
+ file: opts.file,
5145
+ hostId: opts.hostId,
5146
+ json: !!opts.json,
5147
+ env: process.env,
5148
+ home: process.env.HOME ?? "",
5149
+ cwd: process.cwd(),
5150
+ osHostname: process.env.HOSTNAME,
5151
+ user: process.env.USER
5152
+ });
5153
+ emit(r, vault);
5154
+ });
4833
5155
  program.command("mcp").description("start stdio Model Context Protocol server (read-only vault tools)").action(async () => {
4834
5156
  await runSkillwikiMcpStdio();
4835
5157
  });
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-IP3RKVMF.js";
4
+ } from "./chunk-JENSKJP6.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.28",
3
+ "version": "0.9.30",
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.28",
3
+ "version": "0.9.30",
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": {