hivecode-mcp 0.12.4 → 0.13.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/HIVE_RULES.md +11 -40
- package/README.md +421 -346
- package/docs/ALWAYS_ON.md +310 -0
- package/docs/GITHUB_WORKFLOW.md +394 -0
- package/docs/MCP.md +157 -118
- package/docs/QUICKSTART.md +266 -0
- package/docs/SELF_HOSTING.md +2 -2
- package/docs/SLACK_SETUP.md +450 -0
- package/docs/TROUBLESHOOTING.md +105 -0
- package/hive-authors.js +44 -0
- package/hive-await.js +35 -10
- package/hive-branch.js +101 -150
- package/hive-github.js +1944 -0
- package/hive-land.js +691 -0
- package/hive-listen.js +81 -81
- package/hive-mailbox.js +5 -2
- package/hive-mcp-http.js +580 -0
- package/hive-mcp.js +173 -29
- package/hive-notify.js +233 -0
- package/hive-pr.js +205 -29
- package/hive-relay-health.js +230 -230
- package/hive-repos.js +430 -0
- package/hive-room-key.js +209 -0
- package/hive-room-mode.js +1668 -0
- package/hive-room-watcher.js +561 -0
- package/hive-session.js +114 -0
- package/hive-setup.js +14 -4
- package/hive-slack-agent.js +396 -0
- package/hive-slack.js +1809 -0
- package/hive-stop-hook.js +8 -5
- package/hive-time.js +71 -0
- package/hive-webhook.js +54 -0
- package/icr.js +851 -851
- package/limits.js +3 -3
- package/package.json +190 -172
- package/slack-app-manifest-agent.json +61 -0
- package/slack-app-manifest-inbound.json +36 -0
- package/slack-app-manifest.json +27 -0
- package/sync.js +4992 -4691
package/hive-listen.js
CHANGED
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
// hive-listen.js -- how an agent stays reachable after its turn ends, in one place.
|
|
2
|
-
//
|
|
3
|
-
// THE PROBLEM (Ganesh, 4 Oct 2026): agents join a room, answer once, and go deaf. The room already
|
|
4
|
-
// had the answer, a background listener (hive-await.js): it blocks on the room's chat at no token
|
|
5
|
-
// cost and EXITS when the agent is addressed, and Claude Code and Antigravity both start a new turn
|
|
6
|
-
// when a background task ends. But it only works while one is running, and nothing made sure one
|
|
7
|
-
// was. The agent was told the command once, on its first quiet hive_wait, and had to remember to
|
|
8
|
-
// start it again after every wake. Agents forget, and a forgotten listener is invisible: the agent
|
|
9
|
-
// still looks present.
|
|
10
|
-
//
|
|
11
|
-
// ARMED is the fact the rest of the system can now check instead of trusting memory: this machine
|
|
12
|
-
// has a hive-await process for <name> in <room>, alive now, in the agent's own session. The listener
|
|
13
|
-
// writes it to a file and refreshes it; it is never a network question, because the Stop hook that
|
|
14
|
-
// reads it has a hard timeout and must work with the relay down.
|
|
15
|
-
//
|
|
16
|
-
// "CAN'T TELL" READS AS NOT ARMED. A false "armed" is a deaf agent nobody knows is deaf. A false
|
|
17
|
-
// "not armed" costs one extra start, and the newest listener replaces the old one (hive-await.js),
|
|
18
|
-
// so it never becomes two.
|
|
19
|
-
import fs from 'fs'
|
|
20
|
-
import os from 'os'
|
|
21
|
-
import path from 'path'
|
|
22
|
-
import { fileURLToPath } from 'url'
|
|
23
|
-
|
|
24
|
-
// Claude Code stops a background task after about 30 minutes (measured 2 Oct: started 03:46,
|
|
25
|
-
// killed 04:16) and then tells the agent not to restart it; a listener that ends itself on idle
|
|
26
|
-
// can be restarted.
|
|
27
|
-
export const LISTEN_IDLE_MIN = 25
|
|
28
|
-
export const AWAIT_JS = path.join(path.dirname(fileURLToPath(import.meta.url)), 'hive-await.js').replace(/\\/g, '/')
|
|
29
|
-
|
|
30
|
-
// TEMP on Windows, TMPDIR on macOS. On Linux usually NEITHER is set, and the listener used to fall
|
|
31
|
-
// back to '.', the folder it was started in: its cursor landed in the agent's project folder, and a
|
|
32
|
-
// hook started from a different folder could never find it. os.tmpdir() is the same for both.
|
|
33
|
-
export function awaitDir(name, env = process.env) {
|
|
34
|
-
return (env.TEMP || env.TMPDIR || os.tmpdir()) + '/hive-await-' + name
|
|
35
|
-
}
|
|
36
|
-
const safe = (s) => String(s).replace(/[^\w.-]/g, '_')
|
|
37
|
-
export function armedFile(name, room, env = process.env) {
|
|
38
|
-
return path.join(awaitDir(name, env), `armed-${safe(room)}.json`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// The listener refreshes the file every BEAT_MS and deletes it when it exits. A process killed
|
|
42
|
-
// outright cannot delete it, so a file older than FRESH_MS is not believed either. A listener that
|
|
43
|
-
// has not reached the relay within SYNC_GRACE_MS is not believed: one refused for want of a token
|
|
44
|
-
// is alive, refreshed, and hears nothing.
|
|
45
|
-
export const BEAT_MS = Number(process.env.HIVE_AWAIT_BEAT_MS) || 10000
|
|
46
|
-
export const FRESH_MS = Math.max(45000, BEAT_MS * 4)
|
|
47
|
-
export const SYNC_GRACE_MS = 30000
|
|
48
|
-
|
|
49
|
-
export function readArmed(name, room, env = process.env) {
|
|
50
|
-
try { return JSON.parse(fs.readFileSync(armedFile(name, room, env), 'utf8')) } catch { return null }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const alive = (pid) => { try { process.kill(pid, 0); return true } catch (e) { return e.code === 'EPERM' } }
|
|
54
|
-
|
|
55
|
-
/** Is a listener for `name` in `room` running for this agent? Pass `session` when the caller knows
|
|
56
|
-
* its app's session id: a listener started in another session wakes that session, not this one. */
|
|
57
|
-
export function isArmed({ name, room, session = '' }, { env = process.env, now = Date.now() } = {}) {
|
|
58
|
-
const a = readArmed(name, room, env)
|
|
59
|
-
if (!a || !Number(a.pid)) return false
|
|
60
|
-
if (now - (Number(a.beat) || 0) > FRESH_MS) return false
|
|
61
|
-
if (!a.synced && now - (Number(a.at) || 0) > SYNC_GRACE_MS) return false
|
|
62
|
-
if (session && a.session && a.session !== session) return false
|
|
63
|
-
return alive(Number(a.pid))
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** The exact command an agent runs to listen. Forward slashes: bash and PowerShell both accept them. */
|
|
67
|
-
export function listenCommand({ relay, room, name, idleMin = LISTEN_IDLE_MIN, node = process.execPath, awaitJs = AWAIT_JS }) {
|
|
68
|
-
return `"${String(node).replace(/\\/g, '/')}" "${awaitJs}" "${relay}|${room}" "${name}" ${idleMin}`
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Each app is told in its own tool's words; an app nobody has measured gets no order, because an
|
|
72
|
-
// order it cannot carry out is a false "listening".
|
|
73
|
-
export function listenHow(app) {
|
|
74
|
-
if (/claude-code/i.test(app)) return 'Call your Bash tool now, with run_in_background: true, on this command.'
|
|
75
|
-
if (/antigravity/i.test(app)) return 'Call your run_command tool now on this command, with WaitMsBeforeAsync: 1000, so it runs in the background.'
|
|
76
|
-
return ''
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function listenOrder(how, cmd, idleMin = LISTEN_IDLE_MIN) {
|
|
80
|
-
return `[BEFORE YOU END THIS TURN] Ending it stops you hearing this room. ${how} It ends, and that wakes you, when someone addresses you or after ${idleMin} quiet minutes; when it ends, run it again:\n${cmd}`
|
|
81
|
-
}
|
|
1
|
+
// hive-listen.js -- how an agent stays reachable after its turn ends, in one place.
|
|
2
|
+
//
|
|
3
|
+
// THE PROBLEM (Ganesh, 4 Oct 2026): agents join a room, answer once, and go deaf. The room already
|
|
4
|
+
// had the answer, a background listener (hive-await.js): it blocks on the room's chat at no token
|
|
5
|
+
// cost and EXITS when the agent is addressed, and Claude Code and Antigravity both start a new turn
|
|
6
|
+
// when a background task ends. But it only works while one is running, and nothing made sure one
|
|
7
|
+
// was. The agent was told the command once, on its first quiet hive_wait, and had to remember to
|
|
8
|
+
// start it again after every wake. Agents forget, and a forgotten listener is invisible: the agent
|
|
9
|
+
// still looks present.
|
|
10
|
+
//
|
|
11
|
+
// ARMED is the fact the rest of the system can now check instead of trusting memory: this machine
|
|
12
|
+
// has a hive-await process for <name> in <room>, alive now, in the agent's own session. The listener
|
|
13
|
+
// writes it to a file and refreshes it; it is never a network question, because the Stop hook that
|
|
14
|
+
// reads it has a hard timeout and must work with the relay down.
|
|
15
|
+
//
|
|
16
|
+
// "CAN'T TELL" READS AS NOT ARMED. A false "armed" is a deaf agent nobody knows is deaf. A false
|
|
17
|
+
// "not armed" costs one extra start, and the newest listener replaces the old one (hive-await.js),
|
|
18
|
+
// so it never becomes two.
|
|
19
|
+
import fs from 'fs'
|
|
20
|
+
import os from 'os'
|
|
21
|
+
import path from 'path'
|
|
22
|
+
import { fileURLToPath } from 'url'
|
|
23
|
+
|
|
24
|
+
// Claude Code stops a background task after about 30 minutes (measured 2 Oct: started 03:46,
|
|
25
|
+
// killed 04:16) and then tells the agent not to restart it; a listener that ends itself on idle
|
|
26
|
+
// can be restarted.
|
|
27
|
+
export const LISTEN_IDLE_MIN = 25
|
|
28
|
+
export const AWAIT_JS = path.join(path.dirname(fileURLToPath(import.meta.url)), 'hive-await.js').replace(/\\/g, '/')
|
|
29
|
+
|
|
30
|
+
// TEMP on Windows, TMPDIR on macOS. On Linux usually NEITHER is set, and the listener used to fall
|
|
31
|
+
// back to '.', the folder it was started in: its cursor landed in the agent's project folder, and a
|
|
32
|
+
// hook started from a different folder could never find it. os.tmpdir() is the same for both.
|
|
33
|
+
export function awaitDir(name, env = process.env) {
|
|
34
|
+
return (env.TEMP || env.TMPDIR || os.tmpdir()) + '/hive-await-' + name
|
|
35
|
+
}
|
|
36
|
+
const safe = (s) => String(s).replace(/[^\w.-]/g, '_')
|
|
37
|
+
export function armedFile(name, room, env = process.env) {
|
|
38
|
+
return path.join(awaitDir(name, env), `armed-${safe(room)}.json`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The listener refreshes the file every BEAT_MS and deletes it when it exits. A process killed
|
|
42
|
+
// outright cannot delete it, so a file older than FRESH_MS is not believed either. A listener that
|
|
43
|
+
// has not reached the relay within SYNC_GRACE_MS is not believed: one refused for want of a token
|
|
44
|
+
// is alive, refreshed, and hears nothing.
|
|
45
|
+
export const BEAT_MS = Number(process.env.HIVE_AWAIT_BEAT_MS) || 10000
|
|
46
|
+
export const FRESH_MS = Math.max(45000, BEAT_MS * 4)
|
|
47
|
+
export const SYNC_GRACE_MS = 30000
|
|
48
|
+
|
|
49
|
+
export function readArmed(name, room, env = process.env) {
|
|
50
|
+
try { return JSON.parse(fs.readFileSync(armedFile(name, room, env), 'utf8')) } catch { return null }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const alive = (pid) => { try { process.kill(pid, 0); return true } catch (e) { return e.code === 'EPERM' } }
|
|
54
|
+
|
|
55
|
+
/** Is a listener for `name` in `room` running for this agent? Pass `session` when the caller knows
|
|
56
|
+
* its app's session id: a listener started in another session wakes that session, not this one. */
|
|
57
|
+
export function isArmed({ name, room, session = '' }, { env = process.env, now = Date.now() } = {}) {
|
|
58
|
+
const a = readArmed(name, room, env)
|
|
59
|
+
if (!a || !Number(a.pid)) return false
|
|
60
|
+
if (now - (Number(a.beat) || 0) > FRESH_MS) return false
|
|
61
|
+
if (!a.synced && now - (Number(a.at) || 0) > SYNC_GRACE_MS) return false
|
|
62
|
+
if (session && a.session && a.session !== session) return false
|
|
63
|
+
return alive(Number(a.pid))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The exact command an agent runs to listen. Forward slashes: bash and PowerShell both accept them. */
|
|
67
|
+
export function listenCommand({ relay, room, name, idleMin = LISTEN_IDLE_MIN, app = '', node = process.execPath, awaitJs = AWAIT_JS }) {
|
|
68
|
+
return `"${String(node).replace(/\\/g, '/')}" "${awaitJs}" "${relay}|${room}" "${name}" ${idleMin}${app ? ` "${app}"` : ''}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Each app is told in its own tool's words; an app nobody has measured gets no order, because an
|
|
72
|
+
// order it cannot carry out is a false "listening".
|
|
73
|
+
export function listenHow(app) {
|
|
74
|
+
if (/claude-code/i.test(app)) return 'Call your Bash tool now, with run_in_background: true, on this command.'
|
|
75
|
+
if (/antigravity/i.test(app)) return 'Call your run_command tool now on this command, with WaitMsBeforeAsync: 1000, so it runs in the background.'
|
|
76
|
+
return ''
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function listenOrder(how, cmd, idleMin = LISTEN_IDLE_MIN) {
|
|
80
|
+
return `[BEFORE YOU END THIS TURN] Ending it stops you hearing this room. ${how} It ends, and that wakes you, when someone addresses you or after ${idleMin} quiet minutes; when it ends, run it again:\n${cmd}`
|
|
81
|
+
}
|
package/hive-mailbox.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// plain objects out, no Yjs and no I/O — so the delivery rules are unit-testable without a
|
|
19
19
|
// relay, a room, or a network.
|
|
20
20
|
|
|
21
|
+
import { formatTime } from './hive-time.js'
|
|
22
|
+
|
|
21
23
|
/** A chat message as stored in the room: { by, kind, at, text }. */
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -92,7 +94,7 @@ const stripCodeSpans = stripSpansAndQuotes
|
|
|
92
94
|
*
|
|
93
95
|
* Measured in room `night` on 20 Sep 2026, against the old bare /@([\w-]+)/g:
|
|
94
96
|
*
|
|
95
|
-
* "email me at
|
|
97
|
+
* "email me at anyone@gmail.com" -> ["gmail"] every email address
|
|
96
98
|
* "blame line: author@example.org" -> ["example"] every commit trailer
|
|
97
99
|
* "npm i @modelcontextprotocol/sdk" -> ["modelcontextprotocol"] every scoped package
|
|
98
100
|
* "see `@all` in the docs" -> broadcast to everyone every doc about this feature
|
|
@@ -483,7 +485,8 @@ export function createMailbox({ me, startAt = 0 }) {
|
|
|
483
485
|
/** One-line rendering used in tool output, with the repeat count when a run was collapsed. */
|
|
484
486
|
export function formatMessage(m) {
|
|
485
487
|
const times = m.repeated > 1 ? ` (×${m.repeated})` : ''
|
|
486
|
-
|
|
488
|
+
const atStr = m.at ? formatTime(m.at) : ''
|
|
489
|
+
return `${atStr} ${m.by}(${m.kind || 'ai'}): ${m.text}${times}`
|
|
487
490
|
}
|
|
488
491
|
|
|
489
492
|
/**
|