@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 +4 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.js +116 -7
- package/dist/index.js +2 -2
- package/dist/mcpConfig.js +34 -5
- package/dist/result.js +25 -2
- package/dist/scheduler.js +33 -1
- package/dist/updateCheck.js +111 -0
- package/dist/upgrade.js +49 -0
- package/dist/version.js +38 -1
- package/package.json +1 -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
|
|
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.
|
|
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("\
|
|
1535
|
-
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.");
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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/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 =
|
|
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
|
+
}
|
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}:${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"));
|