@korso/shepherd 0.10.0 → 0.11.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.
Files changed (2) hide show
  1. package/dist/index.js +159 -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;
@@ -1646,6 +1753,8 @@ function registerTools(server, deps) {
1646
1753
  heartbeat.start(newSessionId);
1647
1754
  sessionId = newSessionId;
1648
1755
  agentName = parsed.data.agentName;
1756
+ latestClientVersion = parsed.data.latestClientVersion;
1757
+ minimumClientVersion = parsed.data.minimumClientVersion;
1649
1758
  activeWorkspaceSlug = workspaceSlug;
1650
1759
  linked = true;
1651
1760
  hostedWorkspaceRejected = false;
@@ -1787,7 +1896,7 @@ ${section}` : body;
1787
1896
  You hold this claim until you call done (workItemId: ${result.workItemId}) or it expires (~60 min). Calling work or sync renews it.`
1788
1897
  )
1789
1898
  );
1790
- return { content: [{ type: "text", text }] };
1899
+ return { content: [{ type: "text", text: withUpdateNudge(text) }] };
1791
1900
  } catch (err) {
1792
1901
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
1793
1902
  return degradedResult(err);
@@ -1818,9 +1927,14 @@ You hold this claim until you call done (workItemId: ${result.workItemId}) or it
1818
1927
  mergeAnnouncements(result.announcements, drainLocalInbox())
1819
1928
  );
1820
1929
  return {
1821
- content: [{ type: "text", text: msgs ? `${base}
1930
+ content: [
1931
+ {
1932
+ type: "text",
1933
+ text: withUpdateNudge(msgs ? `${base}
1822
1934
 
1823
- ${msgs}` : base }]
1935
+ ${msgs}` : base)
1936
+ }
1937
+ ]
1824
1938
  };
1825
1939
  } catch (err) {
1826
1940
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
@@ -1852,9 +1966,14 @@ ${msgs}` : base }]
1852
1966
  mergeAnnouncements(result.announcements, drainLocalInbox())
1853
1967
  );
1854
1968
  return {
1855
- content: [{ type: "text", text: msgs ? `${base}
1969
+ content: [
1970
+ {
1971
+ type: "text",
1972
+ text: withUpdateNudge(msgs ? `${base}
1856
1973
 
1857
- ${msgs}` : base }]
1974
+ ${msgs}` : base)
1975
+ }
1976
+ ]
1858
1977
  };
1859
1978
  } catch (err) {
1860
1979
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
@@ -1892,7 +2011,7 @@ ${msgs}` : base }]
1892
2011
  formatLandscape(result.landscape)
1893
2012
  )
1894
2013
  );
1895
- return { content: [{ type: "text", text }] };
2014
+ return { content: [{ type: "text", text: withUpdateNudge(text) }] };
1896
2015
  } catch (err) {
1897
2016
  if (err instanceof HubUnreachable || err instanceof HubRequestError) {
1898
2017
  return degradedResult(err);
@@ -2100,13 +2219,13 @@ function postLinkGuidance(workspace) {
2100
2219
  }
2101
2220
 
2102
2221
  // 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";
2222
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
2223
+ import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
2224
+ import { dirname as dirname5, join as join4 } from "node:path";
2106
2225
  function defaultIdentityCachePath() {
2107
2226
  let base = "";
2108
2227
  try {
2109
- base = homedir3();
2228
+ base = homedir4();
2110
2229
  } catch {
2111
2230
  base = "";
2112
2231
  }
@@ -2116,7 +2235,7 @@ function defaultIdentityCachePath() {
2116
2235
  function readCachedHuman(filePath = defaultIdentityCachePath()) {
2117
2236
  let raw;
2118
2237
  try {
2119
- raw = readFileSync4(filePath, "utf8");
2238
+ raw = readFileSync5(filePath, "utf8");
2120
2239
  } catch {
2121
2240
  return null;
2122
2241
  }
@@ -2131,9 +2250,9 @@ function readCachedHuman(filePath = defaultIdentityCachePath()) {
2131
2250
  function writeCachedHuman(human, filePath = defaultIdentityCachePath()) {
2132
2251
  if (typeof human !== "string" || human.trim().length === 0) return;
2133
2252
  try {
2134
- mkdirSync3(dirname4(filePath), { recursive: true });
2253
+ mkdirSync4(dirname5(filePath), { recursive: true });
2135
2254
  const payload = JSON.stringify({ human });
2136
- writeFileSync4(filePath, payload + "\n", "utf8");
2255
+ writeFileSync5(filePath, payload + "\n", "utf8");
2137
2256
  } catch {
2138
2257
  }
2139
2258
  }
@@ -2397,30 +2516,16 @@ async function ancestorChain(maxDepth = 32, snapshot = snapshotParentMap) {
2397
2516
 
2398
2517
  // src/hookInstall.ts
2399
2518
  import {
2400
- readFileSync as readFileSync5,
2401
- writeFileSync as writeFileSync5,
2402
- mkdirSync as mkdirSync4,
2519
+ readFileSync as readFileSync6,
2520
+ writeFileSync as writeFileSync6,
2521
+ mkdirSync as mkdirSync5,
2403
2522
  copyFileSync,
2404
2523
  existsSync as existsSync4,
2405
2524
  renameSync as renameSync2
2406
2525
  } from "node:fs";
2407
- import { homedir as homedir4 } from "node:os";
2408
- import { dirname as dirname5, join as join5 } from "node:path";
2526
+ import { homedir as homedir5 } from "node:os";
2527
+ import { dirname as dirname6, join as join5 } from "node:path";
2409
2528
  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
2529
  function detectClient(clientName) {
2425
2530
  const name = (clientName ?? "").toLowerCase();
2426
2531
  if (!name) return "unknown";
@@ -2433,16 +2538,16 @@ function detectClient(clientName) {
2433
2538
  var HOOK_COMMAND = `npx -y --package=@korso/shepherd@${PACKAGE_VERSION} shepherd-inbox-hook`;
2434
2539
  var HOOK_MARKER = "shepherd-inbox-hook";
2435
2540
  function ensureHookScript(homeDir, hookScriptSource) {
2436
- const source = hookScriptSource ?? join5(dirname5(fileURLToPath(import.meta.url)), "inboxHook.js");
2541
+ const source = hookScriptSource ?? join5(dirname6(fileURLToPath(import.meta.url)), "inboxHook.js");
2437
2542
  try {
2438
2543
  if (!existsSync4(source)) return null;
2439
2544
  const dest = join5(homeDir, ".shepherd", "hooks", "shepherd-inbox-hook.mjs");
2440
- const next = readFileSync5(source);
2441
- const current = existsSync4(dest) ? readFileSync5(dest) : null;
2545
+ const next = readFileSync6(source);
2546
+ const current = existsSync4(dest) ? readFileSync6(dest) : null;
2442
2547
  if (current === null || !current.equals(next)) {
2443
- mkdirSync4(dirname5(dest), { recursive: true });
2548
+ mkdirSync5(dirname6(dest), { recursive: true });
2444
2549
  const tmp = dest + ".tmp";
2445
- writeFileSync5(tmp, next);
2550
+ writeFileSync6(tmp, next);
2446
2551
  renameSync2(tmp, dest);
2447
2552
  }
2448
2553
  return dest;
@@ -2465,7 +2570,7 @@ function codexHookBlock(scriptPath) {
2465
2570
  }
2466
2571
  async function autoInstallHooks({
2467
2572
  clientName,
2468
- homeDir = homedir4(),
2573
+ homeDir = homedir5(),
2469
2574
  disabled = false,
2470
2575
  extensionSource,
2471
2576
  hookScriptSource,
@@ -2490,8 +2595,8 @@ async function autoInstallHooks({
2490
2595
  } else {
2491
2596
  status = installPi(homeDir, extensionSource, log);
2492
2597
  }
2493
- mkdirSync4(dirname5(recordFile), { recursive: true });
2494
- writeFileSync5(
2598
+ mkdirSync5(dirname6(recordFile), { recursive: true });
2599
+ writeFileSync6(
2495
2600
  recordFile,
2496
2601
  JSON.stringify({ status, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
2497
2602
  "utf8"
@@ -2513,7 +2618,7 @@ function installClaude(homeDir, scriptPath, log) {
2513
2618
  const settingsFile = join5(homeDir, ".claude", "settings.json");
2514
2619
  let raw = "";
2515
2620
  if (existsSync4(settingsFile)) {
2516
- raw = readFileSync5(settingsFile, "utf8");
2621
+ raw = readFileSync6(settingsFile, "utf8");
2517
2622
  if (raw.includes(HOOK_MARKER)) return "already-present";
2518
2623
  }
2519
2624
  let settings = {};
@@ -2556,8 +2661,8 @@ function installClaude(homeDir, scriptPath, log) {
2556
2661
  matcher: "*",
2557
2662
  hooks: [{ type: "command", command }]
2558
2663
  });
2559
- mkdirSync4(dirname5(settingsFile), { recursive: true });
2560
- writeFileSync5(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf8");
2664
+ mkdirSync5(dirname6(settingsFile), { recursive: true });
2665
+ writeFileSync6(settingsFile, JSON.stringify(settings, null, 2) + "\n", "utf8");
2561
2666
  return "installed";
2562
2667
  }
2563
2668
  function installCodex(homeDir, scriptPath, log) {
@@ -2565,13 +2670,13 @@ function installCodex(homeDir, scriptPath, log) {
2565
2670
  const manualHint = "Add the hook manually (see the dashboard's Connect screen).";
2566
2671
  const hookBlock = codexHookBlock(scriptPath);
2567
2672
  if (!existsSync4(configFile)) {
2568
- mkdirSync4(dirname5(configFile), { recursive: true });
2569
- writeFileSync5(configFile, `[features]
2673
+ mkdirSync5(dirname6(configFile), { recursive: true });
2674
+ writeFileSync6(configFile, `[features]
2570
2675
  hooks = true
2571
2676
  ${hookBlock}`, "utf8");
2572
2677
  return "installed";
2573
2678
  }
2574
- const toml = readFileSync5(configFile, "utf8");
2679
+ const toml = readFileSync6(configFile, "utf8");
2575
2680
  if (toml.includes(HOOK_MARKER)) return "already-present";
2576
2681
  if (/^\s*\[hooks\.UserPromptSubmit\]\s*$/m.test(toml)) {
2577
2682
  log(
@@ -2592,10 +2697,10 @@ ${hookBlock}`, "utf8");
2592
2697
  updated = toml.replace(/^(\s*\[features\]\s*)$/m, `$1
2593
2698
  hooks = true`);
2594
2699
  }
2595
- writeFileSync5(configFile, updated + hookBlock, "utf8");
2700
+ writeFileSync6(configFile, updated + hookBlock, "utf8");
2596
2701
  return "installed";
2597
2702
  }
2598
- writeFileSync5(
2703
+ writeFileSync6(
2599
2704
  configFile,
2600
2705
  `${toml}
2601
2706
  [features]
@@ -2609,7 +2714,7 @@ function installCursor(homeDir, scriptPath, log) {
2609
2714
  const hooksFile = join5(homeDir, ".cursor", "hooks.json");
2610
2715
  let raw = "";
2611
2716
  if (existsSync4(hooksFile)) {
2612
- raw = readFileSync5(hooksFile, "utf8");
2717
+ raw = readFileSync6(hooksFile, "utf8");
2613
2718
  if (raw.includes(HOOK_MARKER)) return "already-present";
2614
2719
  }
2615
2720
  let config = {};
@@ -2644,12 +2749,12 @@ function installCursor(homeDir, scriptPath, log) {
2644
2749
  return "skipped";
2645
2750
  }
2646
2751
  entries.push({ command: hookCommandFor(scriptPath) });
2647
- mkdirSync4(dirname5(hooksFile), { recursive: true });
2648
- writeFileSync5(hooksFile, JSON.stringify(config, null, 2) + "\n", "utf8");
2752
+ mkdirSync5(dirname6(hooksFile), { recursive: true });
2753
+ writeFileSync6(hooksFile, JSON.stringify(config, null, 2) + "\n", "utf8");
2649
2754
  return "installed";
2650
2755
  }
2651
2756
  function installPi(homeDir, extensionSource, log) {
2652
- const source = extensionSource ?? join5(dirname5(fileURLToPath(import.meta.url)), "inboxExtension.js");
2757
+ const source = extensionSource ?? join5(dirname6(fileURLToPath(import.meta.url)), "inboxExtension.js");
2653
2758
  const dest = join5(homeDir, ".pi", "agent", "extensions", "shepherd-inbox.js");
2654
2759
  if (existsSync4(dest)) return "already-present";
2655
2760
  if (!existsSync4(source)) {
@@ -2658,7 +2763,7 @@ function installPi(homeDir, extensionSource, log) {
2658
2763
  );
2659
2764
  return "skipped";
2660
2765
  }
2661
- mkdirSync4(dirname5(dest), { recursive: true });
2766
+ mkdirSync5(dirname6(dest), { recursive: true });
2662
2767
  copyFileSync(source, dest);
2663
2768
  return "installed";
2664
2769
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korso/shepherd",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
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": {