@cmgmyr/hive 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.js +154 -9
- package/dist/db.js +11 -0
- package/dist/help.js +3 -1
- package/dist/mcpConfig.js +34 -5
- package/dist/projectYml.js +17 -0
- package/dist/result.js +25 -2
- package/dist/scheduler.js +22 -0
- package/dist/tools/agents.js +43 -5
- package/dist/transcript.js +6 -3
- package/dist/turnCount.js +26 -0
- package/dist/updateCheck.js +111 -0
- package/dist/upgrade.js +49 -0
- package/dist/version.js +38 -1
- package/package.json +2 -1
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).
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.
|
|
1
|
+
{"version":"1.2.0","sha":"6abbb1c","dirty":false,"build_id":"7bfa0131-a4ec-4341-a842-7d0cc0cd803d"}
|
package/dist/cli.js
CHANGED
|
@@ -11,14 +11,17 @@ 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
|
-
import { dataDir, db, migrate } from "./db.js";
|
|
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";
|
|
20
22
|
import { errorMessage, parseTags, registrationNoticeText, withTrailingNewline } from "./result.js";
|
|
21
23
|
import { ACTIVE_TIMER_WHERE, dashboardFileContained, describeStall, HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, isUnclassifiablePaneHold, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isUnsubmittedInputHold, janitor, resolveDashboardFile, transcriptStaleness, wasHeldForPaneReissue, } from "./scheduler.js";
|
|
24
|
+
import { readTurnCount } from "./turnCount.js";
|
|
22
25
|
import { STALL_BOUND_SECONDS } from "./backgroundTasks.js";
|
|
23
26
|
import { probeSessionInterpreter, reexecTarget, sessionStartVerdict, } from "./sessionProbe.js";
|
|
24
27
|
import { backupHealth, backupNow, backupsDir, formatBytes, listSnapshots, previewRestore, restoreSnapshot, totalSizeBytes, } from "./backup.js";
|
|
@@ -56,6 +59,8 @@ Usage:
|
|
|
56
59
|
hive show <process> [path] move a running process's pane beside the lead
|
|
57
60
|
hive hide <process> [path] move it back into the <project>/processes window
|
|
58
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
|
|
59
64
|
hive setup [--dir <dir>] write a \`hive\` that runs the interpreter this build
|
|
60
65
|
was compiled for; re-run after every update
|
|
61
66
|
hive setup --attach <mode> auto|raw|control: whether tmux attaches carry -CC
|
|
@@ -558,13 +563,20 @@ placement: split # placement for workers and visible processes: s
|
|
|
558
563
|
|
|
559
564
|
# lead: claude --model opus # custom command for the lead window (default: claude)
|
|
560
565
|
|
|
566
|
+
# agents: [claude, codex] # crew harness pool; the first entry is the default
|
|
567
|
+
|
|
561
568
|
# lead_branches: [main, master] # branches where a session gets hive's kickoff
|
|
562
569
|
|
|
563
570
|
# context_checkpoint_percent: null # unset means off; integer 1-100 to enable
|
|
564
571
|
|
|
572
|
+
# lead_turn_budget: {warn: 300, stop: 600} # optional lead statusline thresholds
|
|
573
|
+
|
|
574
|
+
# dashboard: true # write .hive/dashboard.html (default: false)
|
|
575
|
+
|
|
565
576
|
# review_tags: [from-review] # todo tags \`hive doctor\` counts as review findings and
|
|
566
577
|
# reports as triaged (has a comment, completed, or archived)
|
|
567
|
-
# or untriaged.
|
|
578
|
+
# or untriaged. A tag also matches its own suffixed rounds.
|
|
579
|
+
# Absent means it tracks none.
|
|
568
580
|
|
|
569
581
|
# vars: # substituted into the profile runbook
|
|
570
582
|
# repo: owner/name # {{repo}}
|
|
@@ -1451,6 +1463,74 @@ const verboseInfo = (_check, label, ...lines) => {
|
|
|
1451
1463
|
report("info", label, lines);
|
|
1452
1464
|
};
|
|
1453
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
|
+
}
|
|
1454
1534
|
function cmdSetup(argv) {
|
|
1455
1535
|
const parsed = parseArgs(argv, { flags: ["--force"], valued: ["--dir", "--attach", "--auto-attach"] });
|
|
1456
1536
|
rejectUnknownFlags("setup", parsed, "--dir, --attach, --auto-attach, --force");
|
|
@@ -1528,8 +1608,8 @@ function cmdSetup(argv) {
|
|
|
1528
1608
|
console.log("");
|
|
1529
1609
|
for (const line of durabilityLines(node))
|
|
1530
1610
|
console.log(line);
|
|
1531
|
-
console.log("\
|
|
1532
|
-
console.log("
|
|
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.");
|
|
1533
1613
|
console.log(`\nPATH: ${pathAdvice(dir, file).join("\n")}`);
|
|
1534
1614
|
reportSetupRegistrations(node);
|
|
1535
1615
|
}
|
|
@@ -1546,7 +1626,14 @@ function reportSetupRegistrations(pinned) {
|
|
|
1546
1626
|
for (const line of lines.slice(1))
|
|
1547
1627
|
console.log(` ${line}`);
|
|
1548
1628
|
}
|
|
1549
|
-
|
|
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}`);
|
|
1550
1637
|
}
|
|
1551
1638
|
const offer = registrationOffer(pinned, found);
|
|
1552
1639
|
if (offer) {
|
|
@@ -1652,6 +1739,9 @@ function reportMcpRegistrations(project) {
|
|
|
1652
1739
|
}
|
|
1653
1740
|
for (const r of codexRegistrations) {
|
|
1654
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);
|
|
1655
1745
|
}
|
|
1656
1746
|
}
|
|
1657
1747
|
const PTY_OVERRIDE_VARS = ["HIVE_PTY_HEADROOM_JSON", "HIVE_PTY_PS_ROWS_JSON", "HIVE_ORPHAN_SCRATCH_JSON"];
|
|
@@ -1960,6 +2050,13 @@ function cmdDoctor(argv) {
|
|
|
1960
2050
|
const { line, drift } = versionInfo();
|
|
1961
2051
|
return drift ? [line, drift].join("\n ") : line;
|
|
1962
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
|
+
}
|
|
1963
2060
|
check("node", () => describeInterpreter());
|
|
1964
2061
|
check("better-sqlite3", () => {
|
|
1965
2062
|
const status = checkAbi();
|
|
@@ -1978,6 +2075,10 @@ function cmdDoctor(argv) {
|
|
|
1978
2075
|
check("claude", () => execFileSync("which", ["claude"], { encoding: "utf8" }).trim());
|
|
1979
2076
|
check("database", () => {
|
|
1980
2077
|
const n = db.prepare("SELECT COUNT(*) AS n FROM migrations").get().n;
|
|
2078
|
+
const ahead = storeSchemaAhead(db);
|
|
2079
|
+
if (ahead) {
|
|
2080
|
+
warn("database", `store schema v${ahead.store} is ahead of this build (v${ahead.build}): a newer hive has migrated your shared store. Upgrade this install before relying on it.`);
|
|
2081
|
+
}
|
|
1981
2082
|
return `${dataDir} (schema v${n})`;
|
|
1982
2083
|
});
|
|
1983
2084
|
check("hooks file", () => ensureHooksFile());
|
|
@@ -2388,6 +2489,7 @@ function cmdStatusline() {
|
|
|
2388
2489
|
AND NOT EXISTS (${OPEN_BLOCKERS_SQL})`);
|
|
2389
2490
|
const pads = count("SELECT COUNT(*) AS n FROM pads WHERE project_id = ? AND archived = 0");
|
|
2390
2491
|
const wakes = count(`SELECT COUNT(*) AS n FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}`);
|
|
2492
|
+
const schemaAhead = storeSchemaAhead(db);
|
|
2391
2493
|
// One statement, not two: the count and the chosen row must come from the same read, or a wake that
|
|
2392
2494
|
// delivers/re-arms between two separate queries leaves the second with no row to read.
|
|
2393
2495
|
const held = db
|
|
@@ -2403,7 +2505,7 @@ function cmdStatusline() {
|
|
|
2403
2505
|
(SELECT held_reason FROM chosen) AS held_reason,
|
|
2404
2506
|
(SELECT first_held_at FROM chosen) AS first_held_at`)
|
|
2405
2507
|
.get(project.id, `${HELD_REASON_UNSUBMITTED_INPUT_PREFIX}%`, `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}%`, project.id);
|
|
2406
|
-
if (agents + commands + todos + pads + wakes === 0)
|
|
2508
|
+
if (agents + commands + todos + pads + wakes === 0 && !schemaAhead)
|
|
2407
2509
|
return;
|
|
2408
2510
|
const s = (n) => (n === 1 ? "" : "s");
|
|
2409
2511
|
const parts = [
|
|
@@ -2419,6 +2521,34 @@ function cmdStatusline() {
|
|
|
2419
2521
|
const age = held.first_held_at ? humanizeAge(ageSecondsSince(held.first_held_at)) : "?";
|
|
2420
2522
|
parts.push(`${held.n} held (${age}, ${heldReasonLabel(held.held_reason)})`);
|
|
2421
2523
|
}
|
|
2524
|
+
if (schemaAhead)
|
|
2525
|
+
parts.push("store ahead (update hive)");
|
|
2526
|
+
let turns = null;
|
|
2527
|
+
let budget = null;
|
|
2528
|
+
try {
|
|
2529
|
+
const actorId = process.env.HIVE_AGENT_ID;
|
|
2530
|
+
if (actorId) {
|
|
2531
|
+
const lead = db.prepare("SELECT 1 AS lead FROM agents WHERE project_id = ? AND actor_id = ? AND kind = 'lead' AND status = 'running'").get(project.id, actorId);
|
|
2532
|
+
if (lead) {
|
|
2533
|
+
let inputPath = "";
|
|
2534
|
+
if (!process.stdin.isTTY) {
|
|
2535
|
+
try {
|
|
2536
|
+
const raw = readFileSync(0, "utf8").trim();
|
|
2537
|
+
const input = raw ? JSON.parse(raw) : null;
|
|
2538
|
+
inputPath = typeof input?.transcript_path === "string" ? input.transcript_path : "";
|
|
2539
|
+
}
|
|
2540
|
+
catch { }
|
|
2541
|
+
}
|
|
2542
|
+
turns = readTurnCount(inputPath);
|
|
2543
|
+
budget = loadProjectYml(project.path).config?.lead_turn_budget ?? null;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
catch { }
|
|
2548
|
+
if (turns !== null) {
|
|
2549
|
+
const color = budget === null ? "" : turns >= budget.stop ? "\x1b[31m" : turns >= budget.warn ? "\x1b[33m" : "";
|
|
2550
|
+
parts.push(`${color}turns ${turns}${color ? "\x1b[0m" : ""}`);
|
|
2551
|
+
}
|
|
2422
2552
|
console.log(`\x1b[33m⬡\x1b[0m \x1b[2mhive:\x1b[0m ${parts.join(" \x1b[2m·\x1b[0m ")}`);
|
|
2423
2553
|
}
|
|
2424
2554
|
// test/docs.test.mjs fails if a label here is missing from docs/install.md. It checks
|
|
@@ -2801,10 +2931,21 @@ if (command === "--version" || command === "-v") {
|
|
|
2801
2931
|
console.log(line);
|
|
2802
2932
|
if (drift)
|
|
2803
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
|
+
}
|
|
2804
2945
|
process.exit(0);
|
|
2805
2946
|
}
|
|
2806
2947
|
const COMMANDS = [
|
|
2807
|
-
"lead", "init", "attach", "start", "stop", "show", "hide", "status", "setup", "doctor",
|
|
2948
|
+
"lead", "init", "attach", "start", "stop", "show", "hide", "status", "setup", "upgrade", "doctor",
|
|
2808
2949
|
LEAD_PANE_EXITED_VERB,
|
|
2809
2950
|
"pads", "pad", "todos", "todo", "backups", "restore", "runbook", "posture", "profile", "kickoff", "statusline",
|
|
2810
2951
|
];
|
|
@@ -2821,7 +2962,8 @@ if (!COMMANDS.includes(command)) {
|
|
|
2821
2962
|
usage();
|
|
2822
2963
|
}
|
|
2823
2964
|
}
|
|
2824
|
-
|
|
2965
|
+
if (command !== "upgrade")
|
|
2966
|
+
migrate();
|
|
2825
2967
|
try {
|
|
2826
2968
|
switch (command) {
|
|
2827
2969
|
case "lead":
|
|
@@ -2854,6 +2996,9 @@ try {
|
|
|
2854
2996
|
case "setup":
|
|
2855
2997
|
cmdSetup(rest);
|
|
2856
2998
|
break;
|
|
2999
|
+
case "upgrade":
|
|
3000
|
+
cmdUpgrade(rest);
|
|
3001
|
+
break;
|
|
2857
3002
|
case "doctor":
|
|
2858
3003
|
cmdDoctor(rest);
|
|
2859
3004
|
break;
|
package/dist/db.js
CHANGED
|
@@ -370,6 +370,17 @@ CREATE INDEX idx_wakes_active ON wakes(project_id, kind)
|
|
|
370
370
|
CREATE INDEX idx_wakes_parent ON wakes(parent_wake_id) WHERE parent_wake_id IS NOT NULL;
|
|
371
371
|
`,
|
|
372
372
|
];
|
|
373
|
+
export function storeSchemaAhead(database) {
|
|
374
|
+
try {
|
|
375
|
+
const { version } = database.prepare("SELECT MAX(version) AS version FROM migrations").get();
|
|
376
|
+
if (version == null || !Number.isFinite(version) || version <= MIGRATIONS.length)
|
|
377
|
+
return null;
|
|
378
|
+
return { store: version, build: MIGRATIONS.length };
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
373
384
|
function readAppliedVersions() {
|
|
374
385
|
return new Set(db.prepare("SELECT version FROM migrations").all().map((r) => r.version));
|
|
375
386
|
}
|
package/dist/help.js
CHANGED
|
@@ -153,7 +153,9 @@ runbook (fork it first), not a pad write.`,
|
|
|
153
153
|
agent_status(name|agent_id, include_brief?) — liveness, current command,
|
|
154
154
|
short tail, and the path to the brief this worker was given
|
|
155
155
|
(include_brief=true returns its text; no transcript records it).
|
|
156
|
-
agent_list(include_closed?) —
|
|
156
|
+
agent_list(include_closed?, limit?, before_id?) — agents with live status.
|
|
157
|
+
include_closed adds closed/parked rows, newest first, limit 20/max 100;
|
|
158
|
+
page with before_id set to the receipt's next_before_id.
|
|
157
159
|
agent_rename(name|agent_id, new_name) — change the display name. actor_id
|
|
158
160
|
stays agent:N, so older pad writes and todo comments still point here.
|
|
159
161
|
A live claude worker is told to retitle its own session, which arrives as
|
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,
|
|
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
|
-
|
|
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/projectYml.js
CHANGED
|
@@ -65,6 +65,22 @@ export function loadProjectYml(projectPath) {
|
|
|
65
65
|
warnings.push("context_checkpoint_percent must be an integer from 1 through 100; ignoring it.");
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
let lead_turn_budget = null;
|
|
69
|
+
if (root.lead_turn_budget != null) {
|
|
70
|
+
const value = root.lead_turn_budget;
|
|
71
|
+
const budget = typeof value === "object" && value !== null && !Array.isArray(value)
|
|
72
|
+
? value
|
|
73
|
+
: null;
|
|
74
|
+
const warn = budget?.warn;
|
|
75
|
+
const stop = budget?.stop;
|
|
76
|
+
if (typeof warn === "number" && Number.isInteger(warn) && warn > 0 &&
|
|
77
|
+
typeof stop === "number" && Number.isInteger(stop) && stop > warn) {
|
|
78
|
+
lead_turn_budget = { warn, stop };
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
warnings.push("lead_turn_budget must contain positive integer warn and stop values, with stop greater than warn; ignoring it.");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
68
84
|
const lead = typeof root.lead === "string" && root.lead.trim() !== "" ? root.lead.trim() : null;
|
|
69
85
|
let placement = null;
|
|
70
86
|
if (root.placement != null) {
|
|
@@ -231,6 +247,7 @@ export function loadProjectYml(projectPath) {
|
|
|
231
247
|
review_tags,
|
|
232
248
|
lead_branches,
|
|
233
249
|
context_checkpoint_percent,
|
|
250
|
+
lead_turn_budget,
|
|
234
251
|
dashboard,
|
|
235
252
|
vars,
|
|
236
253
|
processes,
|
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";
|
|
@@ -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);
|
package/dist/tools/agents.js
CHANGED
|
@@ -837,18 +837,53 @@ export function registerAgents(server) {
|
|
|
837
837
|
};
|
|
838
838
|
}));
|
|
839
839
|
server.registerTool("agent_list", {
|
|
840
|
-
description: "List this project's agents with live status.",
|
|
840
|
+
description: "List this project's agents with live status. Without include_closed, this is every running agent, in full. With include_closed, it is every agent (running and closed/parked), newest first, bounded by limit (default 20, max 100); when the receipt carries next_before_id, page through the rest by passing it back as before_id.",
|
|
841
841
|
inputSchema: {
|
|
842
842
|
include_closed: z.boolean().optional(),
|
|
843
|
+
limit: z
|
|
844
|
+
.number()
|
|
845
|
+
.int()
|
|
846
|
+
.min(1)
|
|
847
|
+
.max(100)
|
|
848
|
+
.optional()
|
|
849
|
+
.describe("Max rows to return when include_closed is true, newest first. Default 20, max 100. Ignored otherwise."),
|
|
850
|
+
before_id: z
|
|
851
|
+
.number()
|
|
852
|
+
.int()
|
|
853
|
+
.positive()
|
|
854
|
+
.optional()
|
|
855
|
+
.describe("Page cursor for include_closed: only rows with id below this value. Pass the previous receipt's next_before_id to get the next page."),
|
|
843
856
|
project_id: projectIdParam,
|
|
844
857
|
},
|
|
845
858
|
}, (args) => run(() => {
|
|
846
859
|
const project = resolveProject(args.project_id);
|
|
847
|
-
let sql = "SELECT * FROM agents WHERE project_id = ?";
|
|
848
|
-
if (!args.include_closed)
|
|
849
|
-
sql += " AND status = 'running'";
|
|
850
|
-
const rows = db.prepare(`${sql} ORDER BY id`).all(project.id);
|
|
851
860
|
const snapshot = liveTargets();
|
|
861
|
+
let rows;
|
|
862
|
+
let total;
|
|
863
|
+
let nextBeforeId;
|
|
864
|
+
if (args.include_closed) {
|
|
865
|
+
const limit = Math.min(args.limit ?? 20, 100);
|
|
866
|
+
total = db.prepare("SELECT COUNT(*) AS n FROM agents WHERE project_id = ?").get(project.id).n;
|
|
867
|
+
let sql = "SELECT * FROM agents WHERE project_id = ?";
|
|
868
|
+
const params = [project.id];
|
|
869
|
+
if (args.before_id != null) {
|
|
870
|
+
sql += " AND id < ?";
|
|
871
|
+
params.push(args.before_id);
|
|
872
|
+
}
|
|
873
|
+
sql += " ORDER BY id DESC LIMIT ?";
|
|
874
|
+
params.push(limit + 1);
|
|
875
|
+
const page = db.prepare(sql).all(...params);
|
|
876
|
+
const truncated = page.length > limit;
|
|
877
|
+
rows = truncated ? page.slice(0, limit) : page;
|
|
878
|
+
if (truncated)
|
|
879
|
+
nextBeforeId = rows[rows.length - 1].id;
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
rows = db
|
|
883
|
+
.prepare("SELECT * FROM agents WHERE project_id = ? AND status = 'running' ORDER BY id")
|
|
884
|
+
.all(project.id);
|
|
885
|
+
total = rows.length;
|
|
886
|
+
}
|
|
852
887
|
return {
|
|
853
888
|
project_id: project.id,
|
|
854
889
|
project_name: project.name,
|
|
@@ -857,6 +892,9 @@ export function registerAgents(server) {
|
|
|
857
892
|
note: `${PROBE_FAILED_NOTE} These rows are what the store holds; do not conclude a worker died.`,
|
|
858
893
|
}
|
|
859
894
|
: {}),
|
|
895
|
+
total,
|
|
896
|
+
returned: rows.length,
|
|
897
|
+
...(nextBeforeId !== undefined ? { next_before_id: nextBeforeId } : {}),
|
|
860
898
|
agents: rows.map((r) => {
|
|
861
899
|
const summary = agentSummary(r, snapshot);
|
|
862
900
|
return {
|
package/dist/transcript.js
CHANGED
|
@@ -8,6 +8,9 @@ export function transcriptDirName(cwd) {
|
|
|
8
8
|
export function transcriptDir(cwd) {
|
|
9
9
|
return join(claudeConfigDir(), "projects", transcriptDirName(cwd));
|
|
10
10
|
}
|
|
11
|
+
export function transcriptPath(cwd, sessionId, explicitPath = "") {
|
|
12
|
+
return explicitPath || (sessionId ? join(transcriptDir(cwd), `${sessionId}.jsonl`) : "");
|
|
13
|
+
}
|
|
11
14
|
export function resolveTranscriptDir(cwd) {
|
|
12
15
|
const dir = transcriptDir(cwd);
|
|
13
16
|
return existsSync(dir) ? dir : null;
|
|
@@ -116,8 +119,8 @@ export function readContextFill(kind, worker) {
|
|
|
116
119
|
used_tokens: used, window_tokens: window, used_percent: Math.round(used * 100 / window),
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
|
-
export function readContextTokens(cwd, sessionId,
|
|
120
|
-
if (!sessionId && !
|
|
122
|
+
export function readContextTokens(cwd, sessionId, explicitPath = "") {
|
|
123
|
+
if (!sessionId && !explicitPath)
|
|
121
124
|
return null;
|
|
122
|
-
return claudeTokens(tailRecords(transcriptPath
|
|
125
|
+
return claudeTokens(tailRecords(transcriptPath(cwd, sessionId, explicitPath)));
|
|
123
126
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
export function readTurnCount(path) {
|
|
3
|
+
if (!path)
|
|
4
|
+
return null;
|
|
5
|
+
try {
|
|
6
|
+
const ids = new Set();
|
|
7
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
8
|
+
if (!line.includes('"type":"assistant"'))
|
|
9
|
+
continue;
|
|
10
|
+
try {
|
|
11
|
+
const record = JSON.parse(line);
|
|
12
|
+
if (record.type !== "assistant")
|
|
13
|
+
continue;
|
|
14
|
+
const message = record.message;
|
|
15
|
+
if (typeof message?.id !== "string" || message.id === "" || message.model === "<synthetic>")
|
|
16
|
+
continue;
|
|
17
|
+
ids.add(message.id);
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
21
|
+
return ids.size;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/upgrade.js
ADDED
|
@@ -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}`;
|
|
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.
|
|
3
|
+
"version": "1.2.0",
|
|
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": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc && node scripts/gen-version.mjs",
|
|
12
|
+
"check:mermaid": "node scripts/check-mermaid.mjs",
|
|
12
13
|
"prepublishOnly": "npm run build",
|
|
13
14
|
"watch": "tsc --watch",
|
|
14
15
|
"start": "node dist/index.js",
|