@korso/shepherd 0.10.0 → 0.11.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.
Files changed (2) hide show
  1. package/dist/index.js +173 -54
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -481,7 +481,12 @@ var JoinRequest = z2.object({
481
481
  });
482
482
  var JoinResponse = z2.object({
483
483
  agentName: z2.string(),
484
- sessionId: z2.string().uuid()
484
+ sessionId: z2.string().uuid(),
485
+ // Advertised so clients can nudge their humans to update. Optional: older
486
+ // hubs omit them, and a hub that cannot determine its bundled client
487
+ // version fails open by leaving them out.
488
+ latestClientVersion: z2.string().optional(),
489
+ minimumClientVersion: z2.string().optional()
485
490
  });
486
491
  var WorkRequest = z2.object({
487
492
  sessionId: z2.string().uuid(),
@@ -1368,6 +1373,81 @@ function defaultRunGitStatus(cwd) {
1368
1373
  });
1369
1374
  }
1370
1375
 
1376
+ // src/updateNudge.ts
1377
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1378
+ import { dirname as dirname4 } from "node:path";
1379
+ var NUDGE_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
1380
+ function parseVersion(v) {
1381
+ const m = /^v?(\d+(?:\.\d+)*)/.exec(v.trim());
1382
+ if (!m) return null;
1383
+ return m[1].split(".").map(Number);
1384
+ }
1385
+ function compareVersions(a, b) {
1386
+ const pa = parseVersion(a) ?? [];
1387
+ const pb = parseVersion(b) ?? [];
1388
+ const len = Math.max(pa.length, pb.length);
1389
+ for (let i = 0; i < len; i++) {
1390
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1391
+ if (d !== 0) return d < 0 ? -1 : 1;
1392
+ }
1393
+ return 0;
1394
+ }
1395
+ function readStamp(stampFile) {
1396
+ try {
1397
+ const parsed = JSON.parse(readFileSync4(stampFile, "utf8"));
1398
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.latest === "string" && typeof parsed.at === "number") {
1399
+ return parsed;
1400
+ }
1401
+ } catch {
1402
+ }
1403
+ return null;
1404
+ }
1405
+ function writeStamp(stampFile, stamp) {
1406
+ try {
1407
+ mkdirSync3(dirname4(stampFile), { recursive: true });
1408
+ writeFileSync4(stampFile, JSON.stringify(stamp), "utf8");
1409
+ } catch {
1410
+ }
1411
+ }
1412
+ var SUGGEST = "Let your human know, and suggest the update command that matches how Shepherd is installed on this machine (global npm, an npx cache, a version manager, \u2026).";
1413
+ function maybeUpdateNudge(opts) {
1414
+ const now = opts.nowMs ?? Date.now();
1415
+ if (!parseVersion(opts.current)) return "";
1416
+ const latest = opts.latest !== void 0 && parseVersion(opts.latest) ? opts.latest : void 0;
1417
+ const minimum = opts.minimum !== void 0 && parseVersion(opts.minimum) ? opts.minimum : void 0;
1418
+ const belowMinimum = minimum !== void 0 && compareVersions(opts.current, minimum) < 0;
1419
+ const behind = latest !== void 0 && compareVersions(opts.current, latest) < 0;
1420
+ if (!belowMinimum && !behind) return "";
1421
+ if (!belowMinimum) {
1422
+ const stamp = readStamp(opts.stampFile);
1423
+ if (stamp !== null && compareVersions(latest, stamp.latest) <= 0 && now - stamp.at < NUDGE_COOLDOWN_MS) {
1424
+ return "";
1425
+ }
1426
+ }
1427
+ writeStamp(opts.stampFile, { latest: latest ?? opts.current, at: now });
1428
+ if (belowMinimum) {
1429
+ const latestPart = latest !== void 0 ? ` (latest: ${latest})` : "";
1430
+ return `[shepherd] This client (${opts.current}) is below the minimum supported version ${minimum}${latestPart} \u2014 coordination may misbehave until it is updated. ${SUGGEST}`;
1431
+ }
1432
+ return `[shepherd] Update available: @korso/shepherd ${latest} (this machine runs ${opts.current}). ${SUGGEST}`;
1433
+ }
1434
+
1435
+ // src/version.ts
1436
+ import { createRequire } from "node:module";
1437
+ var PACKAGE_VERSION = (() => {
1438
+ try {
1439
+ const req = createRequire(import.meta.url);
1440
+ const pkg = req("../package.json");
1441
+ return pkg.version ?? "0.0.0";
1442
+ } catch {
1443
+ return "0.0.0";
1444
+ }
1445
+ })();
1446
+
1447
+ // src/tools.ts
1448
+ import { homedir as homedir3 } from "node:os";
1449
+ import nodePath from "node:path";
1450
+
1371
1451
  // src/linkPopup.ts
1372
1452
  var NEVER_ASK_CHOICE = "No \u2014 don't ask again";
1373
1453
  async function offerLinkPopup({
@@ -1582,6 +1662,33 @@ function registerTools(server, deps) {
1582
1662
  const { hubClient, config, context, heartbeat, inboxFile } = deps;
1583
1663
  const markerCwd = deps.cwd ?? process.cwd();
1584
1664
  const declinedDir = deps.declinedDir;
1665
+ const updateNudge = deps.updateNudge ?? ((versions) => maybeUpdateNudge({
1666
+ current: PACKAGE_VERSION,
1667
+ latest: versions.latest,
1668
+ minimum: versions.minimum,
1669
+ stampFile: nodePath.join(homedir3(), ".shepherd", "update-nudge.json")
1670
+ }));
1671
+ let latestClientVersion;
1672
+ let minimumClientVersion;
1673
+ let nudgeDelivered = false;
1674
+ function withUpdateNudge(text) {
1675
+ if (nudgeDelivered) return text;
1676
+ if (latestClientVersion === void 0 && minimumClientVersion === void 0) {
1677
+ return text;
1678
+ }
1679
+ nudgeDelivered = true;
1680
+ let nudge = "";
1681
+ try {
1682
+ nudge = updateNudge({
1683
+ latest: latestClientVersion,
1684
+ minimum: minimumClientVersion
1685
+ });
1686
+ } catch {
1687
+ }
1688
+ return nudge ? `${text}
1689
+
1690
+ ${nudge}` : text;
1691
+ }
1585
1692
  const repoRoot = findRepoRoot(markerCwd);
1586
1693
  let sessionId = null;
1587
1694
  let agentName = null;
@@ -1616,8 +1723,12 @@ function registerTools(server, deps) {
1616
1723
  syncToolSurface();
1617
1724
  }
1618
1725
  let joinFailure = null;
1726
+ let joinFailureTransient = false;
1727
+ let targetWorkspaceSlug = null;
1728
+ let gateRetry = null;
1619
1729
  let joinInFlight = Promise.resolve();
1620
1730
  async function activate(workspaceSlug) {
1731
+ targetWorkspaceSlug = workspaceSlug;
1621
1732
  if (sessionId !== null && activeWorkspaceSlug === workspaceSlug) {
1622
1733
  return { ok: true };
1623
1734
  }
@@ -1637,6 +1748,7 @@ function registerTools(server, deps) {
1637
1748
  const parsed = JoinResponse.safeParse(raw);
1638
1749
  if (!parsed.success || !parsed.data.sessionId) {
1639
1750
  joinFailure = "validation";
1751
+ joinFailureTransient = false;
1640
1752
  console.error(
1641
1753
  "[shepherd] join failed (validation): hub returned a malformed join response (no usable sessionId)"
1642
1754
  );
@@ -1646,14 +1758,18 @@ function registerTools(server, deps) {
1646
1758
  heartbeat.start(newSessionId);
1647
1759
  sessionId = newSessionId;
1648
1760
  agentName = parsed.data.agentName;
1761
+ latestClientVersion = parsed.data.latestClientVersion;
1762
+ minimumClientVersion = parsed.data.minimumClientVersion;
1649
1763
  activeWorkspaceSlug = workspaceSlug;
1650
1764
  linked = true;
1651
1765
  hostedWorkspaceRejected = false;
1652
1766
  joinFailure = null;
1767
+ joinFailureTransient = false;
1653
1768
  return { ok: true };
1654
1769
  } catch (err) {
1655
1770
  const reason = classifyActivateFailure(err);
1656
1771
  joinFailure = classifyJoinFailure(err);
1772
+ joinFailureTransient = err instanceof HubUnreachable || err instanceof HubRequestError && reason === "unknown";
1657
1773
  if (reason === "workspaceRejected") {
1658
1774
  hostedWorkspaceRejected = true;
1659
1775
  console.error(
@@ -1714,6 +1830,13 @@ function registerTools(server, deps) {
1714
1830
  await awaitJoin();
1715
1831
  if (!linked) return notLinked();
1716
1832
  if (selfHostMismatch || hostedWorkspaceRejected) return workspaceMismatch();
1833
+ if (sessionId === null && targetWorkspaceSlug !== null && joinFailureTransient) {
1834
+ gateRetry ??= activate(targetWorkspaceSlug).finally(() => {
1835
+ gateRetry = null;
1836
+ });
1837
+ await gateRetry;
1838
+ if (hostedWorkspaceRejected) return workspaceMismatch();
1839
+ }
1717
1840
  if (sessionId === null) return sessionNotReady();
1718
1841
  return null;
1719
1842
  }
@@ -1787,7 +1910,7 @@ ${section}` : body;
1787
1910
  You hold this claim until you call done (workItemId: ${result.workItemId}) or it expires (~60 min). Calling work or sync renews it.`
1788
1911
  )
1789
1912
  );
1790
- return { content: [{ type: "text", text }] };
1913
+ return { content: [{ type: "text", text: withUpdateNudge(text) }] };
1791
1914
  } catch (err) {
1792
1915
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
1793
1916
  return degradedResult(err);
@@ -1818,9 +1941,14 @@ You hold this claim until you call done (workItemId: ${result.workItemId}) or it
1818
1941
  mergeAnnouncements(result.announcements, drainLocalInbox())
1819
1942
  );
1820
1943
  return {
1821
- content: [{ type: "text", text: msgs ? `${base}
1944
+ content: [
1945
+ {
1946
+ type: "text",
1947
+ text: withUpdateNudge(msgs ? `${base}
1822
1948
 
1823
- ${msgs}` : base }]
1949
+ ${msgs}` : base)
1950
+ }
1951
+ ]
1824
1952
  };
1825
1953
  } catch (err) {
1826
1954
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
@@ -1852,9 +1980,14 @@ ${msgs}` : base }]
1852
1980
  mergeAnnouncements(result.announcements, drainLocalInbox())
1853
1981
  );
1854
1982
  return {
1855
- content: [{ type: "text", text: msgs ? `${base}
1983
+ content: [
1984
+ {
1985
+ type: "text",
1986
+ text: withUpdateNudge(msgs ? `${base}
1856
1987
 
1857
- ${msgs}` : base }]
1988
+ ${msgs}` : base)
1989
+ }
1990
+ ]
1858
1991
  };
1859
1992
  } catch (err) {
1860
1993
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
@@ -1892,7 +2025,7 @@ ${msgs}` : base }]
1892
2025
  formatLandscape(result.landscape)
1893
2026
  )
1894
2027
  );
1895
- return { content: [{ type: "text", text }] };
2028
+ return { content: [{ type: "text", text: withUpdateNudge(text) }] };
1896
2029
  } catch (err) {
1897
2030
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
1898
2031
  return degradedResult(err);
@@ -2100,13 +2233,13 @@ function postLinkGuidance(workspace) {
2100
2233
  }
2101
2234
 
2102
2235
  // src/identityCache.ts
2103
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
2104
- import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
2105
- import { dirname as dirname4, join as join4 } from "node:path";
2236
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
2237
+ import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
2238
+ import { dirname as dirname5, join as join4 } from "node:path";
2106
2239
  function defaultIdentityCachePath() {
2107
2240
  let base = "";
2108
2241
  try {
2109
- base = homedir3();
2242
+ base = homedir4();
2110
2243
  } catch {
2111
2244
  base = "";
2112
2245
  }
@@ -2116,7 +2249,7 @@ function defaultIdentityCachePath() {
2116
2249
  function readCachedHuman(filePath = defaultIdentityCachePath()) {
2117
2250
  let raw;
2118
2251
  try {
2119
- raw = readFileSync4(filePath, "utf8");
2252
+ raw = readFileSync5(filePath, "utf8");
2120
2253
  } catch {
2121
2254
  return null;
2122
2255
  }
@@ -2131,9 +2264,9 @@ function readCachedHuman(filePath = defaultIdentityCachePath()) {
2131
2264
  function writeCachedHuman(human, filePath = defaultIdentityCachePath()) {
2132
2265
  if (typeof human !== "string" || human.trim().length === 0) return;
2133
2266
  try {
2134
- mkdirSync3(dirname4(filePath), { recursive: true });
2267
+ mkdirSync4(dirname5(filePath), { recursive: true });
2135
2268
  const payload = JSON.stringify({ human });
2136
- writeFileSync4(filePath, payload + "\n", "utf8");
2269
+ writeFileSync5(filePath, payload + "\n", "utf8");
2137
2270
  } catch {
2138
2271
  }
2139
2272
  }
@@ -2397,30 +2530,16 @@ async function ancestorChain(maxDepth = 32, snapshot = snapshotParentMap) {
2397
2530
 
2398
2531
  // src/hookInstall.ts
2399
2532
  import {
2400
- readFileSync as readFileSync5,
2401
- writeFileSync as writeFileSync5,
2402
- mkdirSync as mkdirSync4,
2533
+ readFileSync as readFileSync6,
2534
+ writeFileSync as writeFileSync6,
2535
+ mkdirSync as mkdirSync5,
2403
2536
  copyFileSync,
2404
2537
  existsSync as existsSync4,
2405
2538
  renameSync as renameSync2
2406
2539
  } from "node:fs";
2407
- import { homedir as homedir4 } from "node:os";
2408
- import { dirname as dirname5, join as join5 } from "node:path";
2540
+ import { homedir as homedir5 } from "node:os";
2541
+ import { dirname as dirname6, join as join5 } from "node:path";
2409
2542
  import { fileURLToPath } from "node:url";
2410
-
2411
- // src/version.ts
2412
- import { createRequire } from "node:module";
2413
- var PACKAGE_VERSION = (() => {
2414
- try {
2415
- const req = createRequire(import.meta.url);
2416
- const pkg = req("../package.json");
2417
- return pkg.version ?? "0.0.0";
2418
- } catch {
2419
- return "0.0.0";
2420
- }
2421
- })();
2422
-
2423
- // src/hookInstall.ts
2424
2543
  function detectClient(clientName) {
2425
2544
  const name = (clientName ?? "").toLowerCase();
2426
2545
  if (!name) return "unknown";
@@ -2433,16 +2552,16 @@ function detectClient(clientName) {
2433
2552
  var HOOK_COMMAND = `npx -y --package=@korso/shepherd@${PACKAGE_VERSION} shepherd-inbox-hook`;
2434
2553
  var HOOK_MARKER = "shepherd-inbox-hook";
2435
2554
  function ensureHookScript(homeDir, hookScriptSource) {
2436
- const source = hookScriptSource ?? join5(dirname5(fileURLToPath(import.meta.url)), "inboxHook.js");
2555
+ const source = hookScriptSource ?? join5(dirname6(fileURLToPath(import.meta.url)), "inboxHook.js");
2437
2556
  try {
2438
2557
  if (!existsSync4(source)) return null;
2439
2558
  const dest = join5(homeDir, ".shepherd", "hooks", "shepherd-inbox-hook.mjs");
2440
- const next = readFileSync5(source);
2441
- const current = existsSync4(dest) ? readFileSync5(dest) : null;
2559
+ const next = readFileSync6(source);
2560
+ const current = existsSync4(dest) ? readFileSync6(dest) : null;
2442
2561
  if (current === null || !current.equals(next)) {
2443
- mkdirSync4(dirname5(dest), { recursive: true });
2562
+ mkdirSync5(dirname6(dest), { recursive: true });
2444
2563
  const tmp = dest + ".tmp";
2445
- writeFileSync5(tmp, next);
2564
+ writeFileSync6(tmp, next);
2446
2565
  renameSync2(tmp, dest);
2447
2566
  }
2448
2567
  return dest;
@@ -2465,7 +2584,7 @@ function codexHookBlock(scriptPath) {
2465
2584
  }
2466
2585
  async function autoInstallHooks({
2467
2586
  clientName,
2468
- homeDir = homedir4(),
2587
+ homeDir = homedir5(),
2469
2588
  disabled = false,
2470
2589
  extensionSource,
2471
2590
  hookScriptSource,
@@ -2490,8 +2609,8 @@ async function autoInstallHooks({
2490
2609
  } else {
2491
2610
  status = installPi(homeDir, extensionSource, log);
2492
2611
  }
2493
- mkdirSync4(dirname5(recordFile), { recursive: true });
2494
- writeFileSync5(
2612
+ mkdirSync5(dirname6(recordFile), { recursive: true });
2613
+ writeFileSync6(
2495
2614
  recordFile,
2496
2615
  JSON.stringify({ status, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
2497
2616
  "utf8"
@@ -2513,7 +2632,7 @@ function installClaude(homeDir, scriptPath, log) {
2513
2632
  const settingsFile = join5(homeDir, ".claude", "settings.json");
2514
2633
  let raw = "";
2515
2634
  if (existsSync4(settingsFile)) {
2516
- raw = readFileSync5(settingsFile, "utf8");
2635
+ raw = readFileSync6(settingsFile, "utf8");
2517
2636
  if (raw.includes(HOOK_MARKER)) return "already-present";
2518
2637
  }
2519
2638
  let settings = {};
@@ -2556,8 +2675,8 @@ function installClaude(homeDir, scriptPath, log) {
2556
2675
  matcher: "*",
2557
2676
  hooks: [{ type: "command", command }]
2558
2677
  });
2559
- mkdirSync4(dirname5(settingsFile), { recursive: true });
2560
- writeFileSync5(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf8");
2678
+ mkdirSync5(dirname6(settingsFile), { recursive: true });
2679
+ writeFileSync6(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf8");
2561
2680
  return "installed";
2562
2681
  }
2563
2682
  function installCodex(homeDir, scriptPath, log) {
@@ -2565,13 +2684,13 @@ function installCodex(homeDir, scriptPath, log) {
2565
2684
  const manualHint = "Add the hook manually (see the dashboard's Connect screen).";
2566
2685
  const hookBlock = codexHookBlock(scriptPath);
2567
2686
  if (!existsSync4(configFile)) {
2568
- mkdirSync4(dirname5(configFile), { recursive: true });
2569
- writeFileSync5(configFile, `[features]
2687
+ mkdirSync5(dirname6(configFile), { recursive: true });
2688
+ writeFileSync6(configFile, `[features]
2570
2689
  hooks = true
2571
2690
  ${hookBlock}`, "utf8");
2572
2691
  return "installed";
2573
2692
  }
2574
- const toml = readFileSync5(configFile, "utf8");
2693
+ const toml = readFileSync6(configFile, "utf8");
2575
2694
  if (toml.includes(HOOK_MARKER)) return "already-present";
2576
2695
  if (/^\s*\[hooks\.UserPromptSubmit\]\s*$/m.test(toml)) {
2577
2696
  log(
@@ -2592,10 +2711,10 @@ ${hookBlock}`, "utf8");
2592
2711
  updated = toml.replace(/^(\s*\[features\]\s*)$/m, `$1
2593
2712
  hooks = true`);
2594
2713
  }
2595
- writeFileSync5(configFile, updated + hookBlock, "utf8");
2714
+ writeFileSync6(configFile, updated + hookBlock, "utf8");
2596
2715
  return "installed";
2597
2716
  }
2598
- writeFileSync5(
2717
+ writeFileSync6(
2599
2718
  configFile,
2600
2719
  `${toml}
2601
2720
  [features]
@@ -2609,7 +2728,7 @@ function installCursor(homeDir, scriptPath, log) {
2609
2728
  const hooksFile = join5(homeDir, ".cursor", "hooks.json");
2610
2729
  let raw = "";
2611
2730
  if (existsSync4(hooksFile)) {
2612
- raw = readFileSync5(hooksFile, "utf8");
2731
+ raw = readFileSync6(hooksFile, "utf8");
2613
2732
  if (raw.includes(HOOK_MARKER)) return "already-present";
2614
2733
  }
2615
2734
  let config = {};
@@ -2644,12 +2763,12 @@ function installCursor(homeDir, scriptPath, log) {
2644
2763
  return "skipped";
2645
2764
  }
2646
2765
  entries.push({ command: hookCommandFor(scriptPath) });
2647
- mkdirSync4(dirname5(hooksFile), { recursive: true });
2648
- writeFileSync5(hooksFile, JSON.stringify(config, null, 2) + "\n", "utf8");
2766
+ mkdirSync5(dirname6(hooksFile), { recursive: true });
2767
+ writeFileSync6(hooksFile, JSON.stringify(config, null, 2) + "\n", "utf8");
2649
2768
  return "installed";
2650
2769
  }
2651
2770
  function installPi(homeDir, extensionSource, log) {
2652
- const source = extensionSource ?? join5(dirname5(fileURLToPath(import.meta.url)), "inboxExtension.js");
2771
+ const source = extensionSource ?? join5(dirname6(fileURLToPath(import.meta.url)), "inboxExtension.js");
2653
2772
  const dest = join5(homeDir, ".pi", "agent", "extensions", "shepherd-inbox.js");
2654
2773
  if (existsSync4(dest)) return "already-present";
2655
2774
  if (!existsSync4(source)) {
@@ -2658,7 +2777,7 @@ function installPi(homeDir, extensionSource, log) {
2658
2777
  );
2659
2778
  return "skipped";
2660
2779
  }
2661
- mkdirSync4(dirname5(dest), { recursive: true });
2780
+ mkdirSync5(dirname6(dest), { recursive: true });
2662
2781
  copyFileSync(source, dest);
2663
2782
  return "installed";
2664
2783
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korso/shepherd",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Shepherd MCP server — gives any MCP-capable agent (Claude Code, Codex, etc.) advisory cross-session coordination tools (work/done/announce/sync, plus link/unlink/decline) backed by the shared Shepherd hub. Joins the workspace automatically and ships standing instructions so the agent self-coordinates.",
5
5
  "homepage": "https://github.com/Korso-AI/shepherd#readme",
6
6
  "bugs": {