@cmgmyr/hive 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,3 +67,7 @@ hive is a personal daily-driver tool, released low-key. It is single-user by des
67
67
  | [Tools](docs/tools.md) | The 45 MCP tools: what each does and when to use it |
68
68
  | [tmux settings](docs/tmux.md) | Attach modes, pane options, and what to put in `~/.tmux.conf` |
69
69
  | [Development](docs/development.md) | Building and testing hive itself |
70
+
71
+ ## Updating
72
+
73
+ Run `hive upgrade` for a global npm install, or `hive upgrade --check` to preview the commands without changing your install. In a git checkout, the default prints the recipe; `hive upgrade --run` executes it. Upgrade reports Claude Code and Codex registration repairs without editing their configs. Restart every session with hive loaded after the upgrade. See [Updating and recovery](docs/install.md#updating).
@@ -1 +1 @@
1
- {"version":"1.1.0","sha":"38f535a","dirty":false}
1
+ {"version":"1.2.1","sha":"1d14c85","dirty":false,"build_id":"6d50625e-ceaf-412d-8dcf-6b7a922fceaa"}
package/dist/cli.js CHANGED
@@ -11,9 +11,11 @@ import { versionInfo } from "./version.js";
11
11
  import { claudeConfigDir } from "./claudeDir.js";
12
12
  import { ATTACH_MODES, AUTO_ATTACH_MODES, attachMode, isAttachMode, isAutoAttach, resolvedAttachMode, resolvedAutoAttach, setAttachMode, setAutoAttach, } from "./config.js";
13
13
  import { cliPath, dispatcherDir, dispatcherPath, dispatcherScript, durabilityLines, firstHiveOnPath, linkedWorktreePin, pathAdvice, readDispatcher, } from "./dispatcher.js";
14
- import { codexConfigPath, codexHiveRegistrations, hiveRegistrations, registrationOffer, registrationProblem, } from "./mcpConfig.js";
14
+ import { codexConfigPath, codexHiveRegistrations, codexRegistrationProblem, hiveRegistrations, registrationOffer, registrationProblem, } from "./mcpConfig.js";
15
15
  import { DEFAULT_DATA_DIR } from "./dataDir.js";
16
16
  import { dataDir, db, migrate, storeSchemaAhead } from "./db.js";
17
+ import { cacheIsStale, queryUpdate, readCachedUpdate, refreshUpdate, shouldAutoRefresh, updateLine } from "./updateCheck.js";
18
+ import { checkoutUpgradeSteps, detectInstallShape, globalUpgradeSteps } from "./upgrade.js";
17
19
  import { isLowHeadroom, orphanLoginShellDetails, ptyHeadroom } from "./ptys.js";
18
20
  import { addProject, agentProjectPin, currentActor, effectiveProjectId, findProjectForCwd, getProjectByPath, gitPrimaryRoot, getProject, listProjects, registeredAncestor, takeRegistrationNotice, } from "./context.js";
19
21
  import { ensureHooksFile } from "./hooks.js";
@@ -57,6 +59,8 @@ Usage:
57
59
  hive show <process> [path] move a running process's pane beside the lead
58
60
  hive hide <process> [path] move it back into the <project>/processes window
59
61
  hive status overview of agents, todos, and wake-ups everywhere
62
+ hive upgrade [--check] upgrade a global npm install; preview with --check
63
+ hive upgrade --run run the printed pull/install/build/setup checkout recipe
60
64
  hive setup [--dir <dir>] write a \`hive\` that runs the interpreter this build
61
65
  was compiled for; re-run after every update
62
66
  hive setup --attach <mode> auto|raw|control: whether tmux attaches carry -CC
@@ -559,15 +563,20 @@ placement: split # placement for workers and visible processes: s
559
563
 
560
564
  # lead: claude --model opus # custom command for the lead window (default: claude)
561
565
 
566
+ # agents: [claude, codex] # crew harness pool; the first entry is the default
567
+
562
568
  # lead_branches: [main, master] # branches where a session gets hive's kickoff
563
569
 
564
570
  # context_checkpoint_percent: null # unset means off; integer 1-100 to enable
565
571
 
566
572
  # lead_turn_budget: {warn: 300, stop: 600} # optional lead statusline thresholds
567
573
 
574
+ # dashboard: true # write .hive/dashboard.html (default: false)
575
+
568
576
  # review_tags: [from-review] # todo tags \`hive doctor\` counts as review findings and
569
577
  # reports as triaged (has a comment, completed, or archived)
570
- # or untriaged. Absent means doctor tracks none.
578
+ # or untriaged. A tag also matches its own suffixed rounds.
579
+ # Absent means it tracks none.
571
580
 
572
581
  # vars: # substituted into the profile runbook
573
582
  # repo: owner/name # {{repo}}
@@ -1454,6 +1463,74 @@ const verboseInfo = (_check, label, ...lines) => {
1454
1463
  report("info", label, lines);
1455
1464
  };
1456
1465
  const isReviewFindingTag = (bases, tag) => bases.some((base) => tag === base || tag.startsWith(`${base}-`));
1466
+ function cmdUpgrade(argv) {
1467
+ const parsed = parseArgs(argv, { flags: ["--check", "--run"] });
1468
+ rejectUnknownFlags("upgrade", parsed, "--check, --run");
1469
+ if (parsed.positional.length || (parsed.flags.has("--check") && parsed.flags.has("--run"))) {
1470
+ console.error("hive upgrade: use --check or --run, without positional arguments.");
1471
+ process.exitCode = 1;
1472
+ return;
1473
+ }
1474
+ const shape = detectInstallShape();
1475
+ if (shape.kind === "unknown") {
1476
+ console.error(`Cannot upgrade this install: ${shape.reason}`);
1477
+ console.error(`CLI: ${shape.cli}\nPackage root: ${shape.packageRoot}\n.git: ${shape.gitState}\nnpm root -g: ${shape.npmRoot ?? "unavailable"}`);
1478
+ console.error("Put the npm for this package's global root first on PATH, or use a git checkout.");
1479
+ process.exitCode = 1;
1480
+ return;
1481
+ }
1482
+ if (shape.kind === "global" && parsed.flags.has("--run")) {
1483
+ console.error("hive upgrade: --run is only for checkouts; global installs upgrade by default.");
1484
+ process.exitCode = 1;
1485
+ return;
1486
+ }
1487
+ const steps = shape.kind === "checkout" ? checkoutUpgradeSteps(shape.packageRoot) : globalUpgradeSteps(shape);
1488
+ const render = (step) => [step.command, ...step.args]
1489
+ .map((arg) => /^[a-zA-Z0-9_@./=-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", "'\\''")}'`).join(" ");
1490
+ const restart = "Restart every Claude Code or Codex session that has hive loaded; existing sessions keep running old in-memory code.";
1491
+ if (shape.kind === "global") {
1492
+ const update = queryUpdate();
1493
+ console.log(updateLine(update));
1494
+ if (update.status === "unknown") {
1495
+ console.error("not upgrading: could not read the latest version");
1496
+ process.exitCode = 1;
1497
+ return;
1498
+ }
1499
+ if (update.status === "current")
1500
+ return;
1501
+ }
1502
+ if (parsed.flags.has("--check") || (shape.kind === "checkout" && !parsed.flags.has("--run"))) {
1503
+ console.log(shape.kind === "checkout" ? `Checkout: ${shape.packageRoot}\nRun in that directory, or use hive upgrade --run:` : "Would run:");
1504
+ for (const step of steps)
1505
+ console.log(` ${render(step)}`);
1506
+ console.log(restart);
1507
+ return;
1508
+ }
1509
+ for (const [index, step] of steps.entries()) {
1510
+ console.log(`\n${step.label}: ${render(step)}`);
1511
+ const result = spawnSync(step.command, step.args, { cwd: step.cwd, stdio: "inherit" });
1512
+ if (result.status === 0 && !result.error && !result.signal)
1513
+ continue;
1514
+ console.error(`${step.label} failed: ${result.error?.message ?? (result.signal ? `signal ${result.signal}` : `exit ${result.status}`)}`);
1515
+ if (shape.kind === "global") {
1516
+ console.error(index === 0
1517
+ ? "npm may have left the global package partially changed. Retry the install, then repair setup:"
1518
+ : "The package is new, but the dispatcher was not confirmed re-pinned. Repair setup:");
1519
+ for (const remaining of steps.slice(index))
1520
+ console.error(` ${render(remaining)}`);
1521
+ console.error(" hive doctor --strict");
1522
+ }
1523
+ else {
1524
+ console.error(`Stopped at ${render(step)}. Remaining recipe not run:`);
1525
+ for (const remaining of steps.slice(index + 1))
1526
+ console.error(` ${render(remaining)}`);
1527
+ }
1528
+ console.error(`After repair: ${restart}`);
1529
+ process.exitCode = 1;
1530
+ return;
1531
+ }
1532
+ console.log(`\nUpgrade complete. ${restart}`);
1533
+ }
1457
1534
  function cmdSetup(argv) {
1458
1535
  const parsed = parseArgs(argv, { flags: ["--force"], valued: ["--dir", "--attach", "--auto-attach"] });
1459
1536
  rejectUnknownFlags("setup", parsed, "--dir, --attach, --auto-attach, --force");
@@ -1531,8 +1608,8 @@ function cmdSetup(argv) {
1531
1608
  console.log("");
1532
1609
  for (const line of durabilityLines(node))
1533
1610
  console.log(line);
1534
- console.log("\nA rebuild does not re-pin anything on its own. After every update:");
1535
- console.log(" npm install && npm run build && hive setup");
1611
+ console.log("\nFor future updates, run hive upgrade. It re-pins after installing;");
1612
+ console.log("checkouts print the recipe and run it only with --run.");
1536
1613
  console.log(`\nPATH: ${pathAdvice(dir, file).join("\n")}`);
1537
1614
  reportSetupRegistrations(node);
1538
1615
  }
@@ -1549,7 +1626,14 @@ function reportSetupRegistrations(pinned) {
1549
1626
  for (const line of lines.slice(1))
1550
1627
  console.log(` ${line}`);
1551
1628
  }
1552
- return;
1629
+ }
1630
+ for (const r of codexHiveRegistrations()) {
1631
+ const lines = codexRegistrationProblem(r, pinned);
1632
+ if (!lines)
1633
+ continue;
1634
+ console.log(`\n mcp registration (codex): ${lines[0]}`);
1635
+ for (const line of lines.slice(1))
1636
+ console.log(` ${line}`);
1553
1637
  }
1554
1638
  const offer = registrationOffer(pinned, found);
1555
1639
  if (offer) {
@@ -1655,6 +1739,9 @@ function reportMcpRegistrations(project) {
1655
1739
  }
1656
1740
  for (const r of codexRegistrations) {
1657
1741
  info("mcp registration (codex)", [r.command, ...r.args].join(" "));
1742
+ const problem = codexRegistrationProblem(r, process.execPath);
1743
+ if (problem)
1744
+ gatingWarn("mcp registration (codex)", ...problem);
1658
1745
  }
1659
1746
  }
1660
1747
  const PTY_OVERRIDE_VARS = ["HIVE_PTY_HEADROOM_JSON", "HIVE_PTY_PS_ROWS_JSON", "HIVE_ORPHAN_SCRATCH_JSON"];
@@ -1963,6 +2050,13 @@ function cmdDoctor(argv) {
1963
2050
  const { line, drift } = versionInfo();
1964
2051
  return drift ? [line, drift].join("\n ") : line;
1965
2052
  });
2053
+ let cachedUpdate = readCachedUpdate();
2054
+ if (cacheIsStale(cachedUpdate) && shouldAutoRefresh())
2055
+ cachedUpdate = refreshUpdate();
2056
+ if (cachedUpdate?.status === "newer" && cachedUpdate.checkedAt) {
2057
+ const hours = Math.max(0, Math.floor((Date.now() - Date.parse(cachedUpdate.checkedAt)) / (60 * 60 * 1000)));
2058
+ warn("update", `${updateLine(cachedUpdate)} (checked ${hours}h ago)`);
2059
+ }
1966
2060
  check("node", () => describeInterpreter());
1967
2061
  check("better-sqlite3", () => {
1968
2062
  const status = checkAbi();
@@ -2837,10 +2931,21 @@ if (command === "--version" || command === "-v") {
2837
2931
  console.log(line);
2838
2932
  if (drift)
2839
2933
  console.log(`! ${drift}`);
2934
+ if (rest.includes("--check")) {
2935
+ if (process.env.HIVE_NO_UPDATE_CHECK === "1")
2936
+ console.log("update check disabled (HIVE_NO_UPDATE_CHECK)");
2937
+ else
2938
+ console.log(updateLine(refreshUpdate()));
2939
+ }
2940
+ else {
2941
+ const cached = readCachedUpdate();
2942
+ if (cached?.status === "newer" && !cacheIsStale(cached))
2943
+ console.log(`! ${updateLine(cached)}`);
2944
+ }
2840
2945
  process.exit(0);
2841
2946
  }
2842
2947
  const COMMANDS = [
2843
- "lead", "init", "attach", "start", "stop", "show", "hide", "status", "setup", "doctor",
2948
+ "lead", "init", "attach", "start", "stop", "show", "hide", "status", "setup", "upgrade", "doctor",
2844
2949
  LEAD_PANE_EXITED_VERB,
2845
2950
  "pads", "pad", "todos", "todo", "backups", "restore", "runbook", "posture", "profile", "kickoff", "statusline",
2846
2951
  ];
@@ -2857,7 +2962,8 @@ if (!COMMANDS.includes(command)) {
2857
2962
  usage();
2858
2963
  }
2859
2964
  }
2860
- migrate();
2965
+ if (command !== "upgrade")
2966
+ migrate();
2861
2967
  try {
2862
2968
  switch (command) {
2863
2969
  case "lead":
@@ -2890,6 +2996,9 @@ try {
2890
2996
  case "setup":
2891
2997
  cmdSetup(rest);
2892
2998
  break;
2999
+ case "upgrade":
3000
+ cmdUpgrade(rest);
3001
+ break;
2893
3002
  case "doctor":
2894
3003
  cmdDoctor(rest);
2895
3004
  break;
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { registerKv } from "./tools/kv.js";
10
10
  import { registerLeases } from "./tools/leases.js";
11
11
  import { registerWakes } from "./tools/wakes.js";
12
12
  import { registerPrompts } from "./prompts.js";
13
- import { startScheduler } from "./scheduler.js";
13
+ import { schedulerIntervalMs, startScheduler } from "./scheduler.js";
14
14
  import { enforceStrictInput } from "./strictInput.js";
15
15
  const server = enforceStrictInput(new McpServer({ name: "hive", version: "0.1.0" }, {
16
16
  instructions: `Hive gives Claude Code sessions shared, persistent state for coordinating work on a project: pads for plans and findings, todos for tracked work with blockers, kv for small shared values, leases for claiming shared work areas, agents for spawning and directing other sessions, and wake-ups for scheduled or idle-triggered check-ins. Search for these tools when a task needs to persist past this conversation, hand off to or watch another session, or track multi-step work with dependencies.
@@ -30,5 +30,5 @@ registerKv(server);
30
30
  registerLeases(server);
31
31
  registerWakes(server);
32
32
  registerPrompts(server);
33
- startScheduler();
33
+ startScheduler(schedulerIntervalMs());
34
34
  await server.connect(new StdioServerTransport());
package/dist/mcpConfig.js CHANGED
@@ -1,14 +1,27 @@
1
- import { readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { parse as parseToml } from "smol-toml";
6
- const serverPath = () => fileURLToPath(new URL("./index.js", import.meta.url));
6
+ export const serverPath = () => fileURLToPath(new URL("./index.js", import.meta.url));
7
+ function serverProblem(args, expected) {
8
+ const server = args[0];
9
+ if (!server)
10
+ return "has no server path.";
11
+ if (!existsSync(server))
12
+ return `points at a missing server: ${server}`;
13
+ try {
14
+ if (realpathSync(server) === realpathSync(expected))
15
+ return null;
16
+ }
17
+ catch { }
18
+ return `points at a different server: ${server} (expected ${expected})`;
19
+ }
7
20
  function addCommand(scope, name, pinned, args) {
8
21
  return ` claude mcp add --scope ${scope} ${name} -- ${[pinned, ...args].map((a) => `"${a}"`).join(" ")}`;
9
22
  }
10
- export function registrationProblem(r, pinned) {
11
- const reRegister = [addCommand(r.scope, r.name, pinned, r.args)];
23
+ export function registrationProblem(r, pinned, expectedServer = serverPath()) {
24
+ const reRegister = [addCommand(r.scope, r.name, pinned, [expectedServer])];
12
25
  if (!r.command.includes("/")) {
13
26
  return [
14
27
  `runs "${r.command}", which a Node version manager re-resolves per`,
@@ -25,7 +38,23 @@ export function registrationProblem(r, pinned) {
25
38
  ...reRegister,
26
39
  ];
27
40
  }
28
- return null;
41
+ const problem = serverProblem(r.args, expectedServer);
42
+ return problem ? [problem, `Re-register in ${r.source}:`, ...reRegister] : null;
43
+ }
44
+ export function codexRegistrationProblem(r, pinned, expectedServer = serverPath()) {
45
+ const problem = !r.command.includes("/")
46
+ ? `runs "${r.command}", which a Node version manager re-resolves per directory.`
47
+ : r.command !== pinned
48
+ ? `pins a different interpreter than this CLI runs (${pinned}).`
49
+ : serverProblem(r.args, expectedServer);
50
+ if (!problem)
51
+ return null;
52
+ return [
53
+ problem,
54
+ `Re-register in ${r.source}:`,
55
+ ` codex mcp remove ${r.name}`,
56
+ ` codex mcp add ${r.name} -- "${pinned}" "${expectedServer}"`,
57
+ ];
29
58
  }
30
59
  function readJson(file) {
31
60
  try {
package/dist/result.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { runningBuildChange, runningBuildNotice } from "./version.js";
1
2
  export function errorMessage(e) {
2
3
  return e instanceof Error ? e.message : String(e);
3
4
  }
@@ -14,10 +15,32 @@ export function registrationNoticeText(notice) {
14
15
  `it if it owns nothing yet, run the session from the intended directory, or call ` +
15
16
  `project_select.`);
16
17
  }
18
+ const reportedBuilds = new Set();
19
+ export async function appendRunningBuildNotice(result, session) {
20
+ try {
21
+ const change = runningBuildChange();
22
+ if (!change || reportedBuilds.has(change.disk.build_id))
23
+ return result;
24
+ const actor = process.env.HIVE_AGENT_ID;
25
+ if (actor) {
26
+ const { db } = await import("./db.js");
27
+ const row = db.prepare("SELECT kind FROM agents WHERE actor_id = ?").get(actor);
28
+ if (row?.kind === "agent")
29
+ return result;
30
+ }
31
+ if (reportedBuilds.has(change.disk.build_id))
32
+ return result;
33
+ result.content.push({ type: "text", text: runningBuildNotice(change, session) });
34
+ reportedBuilds.add(change.disk.build_id);
35
+ }
36
+ catch {
37
+ }
38
+ return result;
39
+ }
17
40
  export async function run(fn) {
18
41
  const { storeReplaced } = await import("./db.js");
19
42
  if (storeReplaced()) {
20
- return { content: [{ type: "text", text: STORE_REPLACED_MESSAGE }], isError: true };
43
+ return appendRunningBuildNotice({ content: [{ type: "text", text: STORE_REPLACED_MESSAGE }], isError: true });
21
44
  }
22
45
  const { takeRegistrationNotice } = await import("./context.js");
23
46
  let result;
@@ -31,7 +54,7 @@ export async function run(fn) {
31
54
  if (notice) {
32
55
  result.content.push({ type: "text", text: registrationNoticeText(notice) });
33
56
  }
34
- return result;
57
+ return appendRunningBuildNotice(result);
35
58
  }
36
59
  export function parseTags(text) {
37
60
  try {
package/dist/scheduler.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { runningBuildChange, runningBuildNotice } from "./version.js";
1
2
  import { existsSync, mkdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
3
  import { dirname, join, sep } from "node:path";
3
4
  import { dataDir, db, storeReplaced } from "./db.js";
@@ -42,7 +43,7 @@ function bestEffortRun(sql, ...params) {
42
43
  }
43
44
  let ticking = false;
44
45
  let schedulerInterval;
45
- export function startScheduler(intervalMs = 3000) {
46
+ export function startScheduler(intervalMs = DEFAULT_SCHEDULER_INTERVAL_MS) {
46
47
  schedulerInterval = setInterval(() => {
47
48
  void tick();
48
49
  }, intervalMs).unref();
@@ -161,6 +162,25 @@ function reportDeadProcess(agent, snapshot) {
161
162
  catch {
162
163
  }
163
164
  }
165
+ const reportedBuilds = new Set();
166
+ export function reportRunningBuildChange(snapshot) {
167
+ try {
168
+ const change = runningBuildChange();
169
+ const actor = process.env.HIVE_AGENT_ID;
170
+ if (!change || !actor || reportedBuilds.has(change.disk.build_id))
171
+ return;
172
+ const lead = stmt(`SELECT project_id, actor_id, tmux_target, tmux_socket, pane_pid, command FROM agents
173
+ WHERE actor_id = ? AND kind = ? AND status = 'running'`).get(actor, LEAD_KIND);
174
+ if (!lead?.tmux_target || !screenClassifiable(lead.command) ||
175
+ rowAlive(lead.tmux_socket, lead.tmux_target, snapshot) !== true ||
176
+ paneReissued(lead.pane_pid, rowAliveProbe(lead.tmux_socket, lead.tmux_target, snapshot)))
177
+ return;
178
+ insertNotice({ project_id: lead.project_id, owner: lead.actor_id }, lead.actor_id, lead.tmux_target, runningBuildNotice(change), null);
179
+ reportedBuilds.add(change.disk.build_id);
180
+ }
181
+ catch {
182
+ }
183
+ }
164
184
  const deadProcessBody = (name) => `[hive] process "${name}" exited on its own; its pane is gone. Restart it with: hive start "${name}"`;
165
185
  export function janitor(snapshot = liveTargets()) {
166
186
  const reapedCodexHomes = reapClosedCodexHomes();
@@ -359,6 +379,8 @@ export async function tick(snapshot) {
359
379
  if (snapshot === undefined)
360
380
  snapshot = liveTargets();
361
381
  janitor(snapshot);
382
+ if (snapshot)
383
+ reportRunningBuildChange(snapshot);
362
384
  checkConfirmations();
363
385
  pruneStateLog();
364
386
  maybeBackupHourly(db, dataDir);
@@ -1620,3 +1642,13 @@ async function deliver(timer, note, choices, typedSeen, firstHeldAt) {
1620
1642
  if (!strandedTextWouldHold)
1621
1643
  recordTyped();
1622
1644
  }
1645
+ export const DEFAULT_SCHEDULER_INTERVAL_MS = 3000;
1646
+ const MIN_SCHEDULER_INTERVAL_MS = 100;
1647
+ // Node clamps a setInterval delay above this to 1ms.
1648
+ const MAX_SCHEDULER_INTERVAL_MS = 2_147_483_647;
1649
+ export function schedulerIntervalMs(raw = process.env.HIVE_SCHEDULER_INTERVAL_MS) {
1650
+ const parsed = Number(raw);
1651
+ return raw && Number.isInteger(parsed) && parsed >= MIN_SCHEDULER_INTERVAL_MS && parsed <= MAX_SCHEDULER_INTERVAL_MS
1652
+ ? parsed
1653
+ : DEFAULT_SCHEDULER_INTERVAL_MS;
1654
+ }
@@ -0,0 +1,111 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { storeDir } from "./dataDir.js";
5
+ import { versionInfo } from "./version.js";
6
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
7
+ const CACHE_FILE = "update-check.json";
8
+ const VERSION_RE = /^(\d+)\.(\d+)\.(\d+)$/;
9
+ function currentVersion() {
10
+ return versionInfo().line.match(/^hive\s+(\S+)/)?.[1] ?? "0.0.0";
11
+ }
12
+ function compareVersions(current, latest) {
13
+ const currentMatch = VERSION_RE.exec(current);
14
+ const latestMatch = VERSION_RE.exec(latest);
15
+ if (!currentMatch || !latestMatch)
16
+ return "unknown";
17
+ for (let i = 1; i <= 3; i += 1) {
18
+ const difference = Number(latestMatch[i]) - Number(currentMatch[i]);
19
+ if (difference > 0)
20
+ return "newer";
21
+ if (difference < 0)
22
+ return "current";
23
+ }
24
+ return "current";
25
+ }
26
+ function fromCache(record) {
27
+ const current = currentVersion();
28
+ if (record.error !== null) {
29
+ return { current, latest: null, status: "unknown", reason: record.error, checkedAt: record.checked_at };
30
+ }
31
+ const latest = typeof record.latest === "string" ? record.latest : null;
32
+ const status = latest === null ? "unknown" : compareVersions(current, latest);
33
+ return {
34
+ current,
35
+ latest,
36
+ status,
37
+ reason: status === "unknown" ? "unparseable version" : null,
38
+ checkedAt: record.checked_at,
39
+ };
40
+ }
41
+ function cachePath() {
42
+ return join(storeDir(), CACHE_FILE);
43
+ }
44
+ export function readCachedUpdate() {
45
+ try {
46
+ const raw = JSON.parse(readFileSync(cachePath(), "utf8"));
47
+ if (typeof raw.checked_at !== "string" || (typeof raw.latest !== "string" && raw.latest !== null) || (typeof raw.error !== "string" && raw.error !== null)) {
48
+ return null;
49
+ }
50
+ return fromCache({ checked_at: raw.checked_at, latest: raw.latest, error: raw.error });
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ function writeCache(result) {
57
+ const dir = storeDir();
58
+ mkdirSync(dir, { recursive: true });
59
+ const path = cachePath();
60
+ const temporary = `${path}.${process.pid}.tmp`;
61
+ writeFileSync(temporary, JSON.stringify({ checked_at: result.checkedAt, latest: result.latest, error: result.reason }));
62
+ renameSync(temporary, path);
63
+ }
64
+ export function queryUpdate() {
65
+ const current = currentVersion();
66
+ const checkedAt = new Date().toISOString();
67
+ let latest = null;
68
+ let reason = null;
69
+ if (process.env.HIVE_NO_UPDATE_CHECK === "1") {
70
+ reason = "disabled (HIVE_NO_UPDATE_CHECK)";
71
+ }
72
+ else {
73
+ try {
74
+ latest = execFileSync("npm", ["view", "@cmgmyr/hive", "version"], {
75
+ encoding: "utf8",
76
+ stdio: ["ignore", "pipe", "pipe"],
77
+ timeout: 5000,
78
+ }).trim();
79
+ if (!VERSION_RE.test(latest) || !VERSION_RE.test(current))
80
+ reason = "unparseable version";
81
+ }
82
+ catch (error) {
83
+ const code = error.code;
84
+ reason = code === "ENOENT" ? "npm not found" : code === "ETIMEDOUT" ? "timed out" : "offline or npm failed";
85
+ }
86
+ }
87
+ const status = reason === null && latest !== null ? compareVersions(current, latest) : "unknown";
88
+ return { current, latest, status, reason, checkedAt };
89
+ }
90
+ export function refreshUpdate() {
91
+ const result = queryUpdate();
92
+ writeCache(result);
93
+ return result;
94
+ }
95
+ export function cacheIsStale(update, now = Date.now()) {
96
+ if (!update?.checkedAt)
97
+ return true;
98
+ const checked = Date.parse(update.checkedAt);
99
+ return !Number.isFinite(checked) || now - checked >= CACHE_TTL_MS;
100
+ }
101
+ export function updateLine(update) {
102
+ if (update.status === "newer" && update.latest) {
103
+ return `update available: hive ${update.latest} (you have ${update.current}); run: hive upgrade`;
104
+ }
105
+ if (update.status === "current")
106
+ return `up to date: hive ${update.current} is the latest on npm`;
107
+ return `update check: unknown (${update.reason ?? "offline or npm failed"})`;
108
+ }
109
+ export function shouldAutoRefresh() {
110
+ return process.stdout.isTTY === true && !process.env.CI && process.env.HIVE_NO_UPDATE_CHECK !== "1";
111
+ }
@@ -0,0 +1,49 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { realpathSync, statSync } from "node:fs";
3
+ import { dirname, isAbsolute, join } from "node:path";
4
+ import { cliPath } from "./dispatcher.js";
5
+ export function detectInstallShape(cli = cliPath(), npmCommand = "npm") {
6
+ const packageRoot = dirname(dirname(cli));
7
+ let gitState = "absent";
8
+ try {
9
+ const git = statSync(join(packageRoot, ".git"));
10
+ if (git.isDirectory() || git.isFile())
11
+ return { kind: "checkout", packageRoot };
12
+ gitState = "neither file nor directory";
13
+ }
14
+ catch (error) {
15
+ if (error.code !== "ENOENT")
16
+ gitState = String(error);
17
+ }
18
+ let npmRoot = null;
19
+ let reason;
20
+ try {
21
+ npmRoot = execFileSync(npmCommand, ["root", "-g"], {
22
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000,
23
+ }).trim();
24
+ if (!isAbsolute(npmRoot))
25
+ throw new Error("npm root -g did not return an absolute path");
26
+ if (realpathSync(packageRoot) === realpathSync(join(npmRoot, "@cmgmyr/hive"))) {
27
+ return { kind: "global", packageRoot, npmRoot, npmCommand };
28
+ }
29
+ reason = "npm root -g does not own the running package";
30
+ }
31
+ catch (error) {
32
+ reason = `could not confirm npm root ownership: ${String(error)}`;
33
+ }
34
+ return { kind: "unknown", packageRoot, cli, gitState, npmRoot, reason };
35
+ }
36
+ export function checkoutUpgradeSteps(packageRoot, node = process.execPath) {
37
+ return [
38
+ { label: "pull", command: "git", args: ["pull", "--ff-only"], cwd: packageRoot },
39
+ { label: "install", command: "npm", args: ["install"], cwd: packageRoot },
40
+ { label: "build", command: "npm", args: ["run", "build"], cwd: packageRoot },
41
+ { label: "setup", command: node, args: [join(packageRoot, "dist/cli.js"), "setup"], cwd: packageRoot },
42
+ ];
43
+ }
44
+ export function globalUpgradeSteps(shape, node = process.execPath) {
45
+ return [
46
+ { label: "install", command: shape.npmCommand, args: ["install", "-g", "@cmgmyr/hive@latest"] },
47
+ { label: "setup", command: node, args: [join(shape.npmRoot, "@cmgmyr/hive/dist/cli.js"), "setup"] },
48
+ ];
49
+ }
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { readFileSync } from "node:fs";
2
+ import { readFileSync, statSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  const moduleDir = dirname(fileURLToPath(import.meta.url));
@@ -13,6 +13,43 @@ function readBuildInfo() {
13
13
  return null;
14
14
  }
15
15
  }
16
+ function readRunningBuild() {
17
+ const info = readBuildInfo();
18
+ return info && typeof info.build_id === "string" && info.build_id.trim() !== "" &&
19
+ (info.sha === null || typeof info.sha === "string") && typeof info.dirty === "boolean"
20
+ ? info : null;
21
+ }
22
+ const loadedBuild = readRunningBuild();
23
+ let diskSignature;
24
+ let diskBuild = null;
25
+ export function runningBuildChange() {
26
+ try {
27
+ if (!loadedBuild)
28
+ return null;
29
+ const stat = statSync(join(moduleDir, "build-info.json"));
30
+ const signature = `${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
31
+ if (signature !== diskSignature) {
32
+ diskBuild = readRunningBuild();
33
+ diskSignature = signature;
34
+ }
35
+ return diskBuild && diskBuild.build_id !== loadedBuild.build_id
36
+ ? { loaded: loadedBuild, disk: diskBuild } : null;
37
+ }
38
+ catch {
39
+ diskSignature = undefined;
40
+ diskBuild = null;
41
+ return null;
42
+ }
43
+ }
44
+ export function runningBuildNotice(change, session) {
45
+ const subject = session ? `session ${JSON.stringify(session)}'s` : "this session's";
46
+ const detail = (info) => info.sha === null ? "no git sha" : describe(info.sha, info.dirty);
47
+ const sameDescription = change.loaded.version === change.disk.version && detail(change.loaded) === detail(change.disk);
48
+ const label = (info) => `hive ${info.version} (${detail(info)}${sameDescription ? `, build ${info.build_id.slice(0, 8)}` : ""})`;
49
+ return `hive: ${subject} hive server loaded ${label(change.loaded)}; ` +
50
+ `the build on disk changed to ${label(change.disk)}. ` +
51
+ "Restart this session, or reconnect hive in /mcp, to pick it up.";
52
+ }
16
53
  function packageVersion() {
17
54
  try {
18
55
  const raw = JSON.parse(readFileSync(join(checkoutRoot, "package.json"), "utf8"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmgmyr/hive",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Shared memory and coordination for Claude Code sessions: one lead, many tmux workers, one project-scoped store.",
5
5
  "type": "module",
6
6
  "bin": {