hivecode-mcp 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/HIVE_RULES.md +11 -40
  2. package/LICENSE +21 -21
  3. package/README.md +421 -167
  4. package/core.js +91 -91
  5. package/docs/ALWAYS_ON.md +310 -0
  6. package/docs/GITHUB_WORKFLOW.md +334 -0
  7. package/docs/HIVE_RULES.md +41 -41
  8. package/docs/MCP.md +157 -118
  9. package/docs/QUICKSTART.md +266 -0
  10. package/docs/SELF_HOSTING.md +256 -256
  11. package/docs/SLACK_SETUP.md +450 -0
  12. package/docs/TROUBLESHOOTING.md +87 -0
  13. package/hive-activity.js +336 -310
  14. package/hive-await.js +207 -0
  15. package/hive-branch.js +686 -0
  16. package/hive-busy.js +88 -88
  17. package/hive-coord.js +110 -110
  18. package/hive-gate.js +135 -135
  19. package/hive-github.js +1904 -0
  20. package/hive-hook-install.js +179 -103
  21. package/hive-instructions.js +74 -66
  22. package/hive-listen.js +81 -0
  23. package/hive-mailbox.js +520 -386
  24. package/hive-mcp-http.js +580 -0
  25. package/hive-mcp.js +579 -137
  26. package/hive-mux-proto.js +58 -58
  27. package/hive-mux.js +350 -350
  28. package/hive-notify.js +233 -0
  29. package/hive-obligations.js +247 -247
  30. package/hive-pr-staleness.js +189 -189
  31. package/hive-pr.js +350 -0
  32. package/hive-privacy.js +327 -288
  33. package/hive-receipts.js +233 -233
  34. package/hive-relay-health.js +230 -225
  35. package/hive-room-key.js +209 -0
  36. package/hive-secrets.js +89 -0
  37. package/hive-session.js +114 -0
  38. package/hive-setup.js +558 -445
  39. package/hive-slack-agent.js +396 -0
  40. package/hive-slack.js +1809 -0
  41. package/hive-stdio.js +172 -168
  42. package/hive-stop-hook.js +293 -241
  43. package/hive-throttle.js +118 -118
  44. package/hive-time.js +71 -0
  45. package/hive-update.js +58 -58
  46. package/hive-version.js +110 -59
  47. package/hive-webhook.js +54 -0
  48. package/icr-merge.js +58 -58
  49. package/icr.js +851 -745
  50. package/lang-json.js +211 -211
  51. package/lang-ruby.js +230 -230
  52. package/lang-toml.js +136 -136
  53. package/lang-yaml.js +183 -183
  54. package/limits.js +413 -394
  55. package/package.json +189 -164
  56. package/reconcile.cjs +614 -477
  57. package/slack-app-manifest-agent.json +61 -0
  58. package/slack-app-manifest-inbound.json +36 -0
  59. package/slack-app-manifest.json +27 -0
  60. package/substrate.js +252 -252
  61. package/sync.js +1956 -177
  62. package/grm-link.cjs +0 -141
package/hive-await.js ADDED
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+ // hive-await — block until someone speaks to you, then exit. The CLI's `hive_wait`.
3
+ //
4
+ // npx -y -p hivecode-mcp@latest hivecode-await "<relay|room>" <YourName> [idleMinutes]
5
+ //
6
+ // Run it in the BACKGROUND and let its exit wake you. Exits 0 in both cases; read `reason` on stdout:
7
+ // {"reason":"mention","messages":[...]} someone said @YourName or @all -> read, reply, run it again
8
+ // {"reason":"idle","note":"..."} nothing in N minutes -> run it again
9
+ //
10
+ // WHY THIS EXISTS. An agent driving Hivecode from the shell has no way to be interrupted. The
11
+ // only pattern available was a timer:
12
+ //
13
+ // for i in 1 2 3 4 5; do hive-readchat.js ...; sleep 30; done
14
+ //
15
+ // which is deaf by construction. A sleeping process cannot be woken by a message, so the agent
16
+ // is blind for the whole window — and the window is exactly when people are talking to it.
17
+ // Measured on 11 Sep 2026: Ganesh messaged the Director and got no answer for minutes, not
18
+ // because the Director was ignoring him but because it was mid-`sleep`. The agents were alive
19
+ // and deaf, which from the outside is indistinguishable from broken.
20
+ //
21
+ // A timer also makes you choose between two bad things: sleep long and be unresponsive, or
22
+ // sleep short and burn a tool call every few seconds for nothing. This waits on the EVENT.
23
+ // Blocked costs nothing — it is a CRDT observer, not a poll — and it returns the instant your
24
+ // name is said.
25
+ //
26
+ // SHIPPED, 29 Sep 2026. Until then this file lived untracked in one developer's folder, and every
27
+ // agent in a four-machine room got it by path or by paste — the listener the whole product's
28
+ // "stay reachable" rule depends on was not in the package.
29
+ //
30
+ // It joins under YOUR name, beside your MCP connection, tagged `listener`, so the room can say
31
+ // "listening" instead of "parked". It is NOT `passive`: passive means "never answers", and presence
32
+ // is merged by name, so a passive listener would mark the agent itself as one nobody may wait on.
33
+
34
+ import fs from 'fs'
35
+ import path from 'path'
36
+ import { startSync, parseLink } from './sync.js'
37
+ import * as RC from './hive-receipts.js'
38
+ import { awaitDir, armedFile, BEAT_MS } from './hive-listen.js'
39
+
40
+ const [, , LINK = '', NAME = '', IDLE_MIN = '10'] = process.argv
41
+ if (!LINK || !NAME) {
42
+ console.error('usage: npx -y -p hivecode-mcp@latest hivecode-await "<relay|room>" <YourName> [idleMinutes]')
43
+ process.exit(2)
44
+ }
45
+ const { relay, room } = parseLink(LINK)
46
+ const dir = awaitDir(NAME)
47
+
48
+ const hive = startSync({ relay, room, dir, name: NAME, kind: 'ai', token: process.env.HIVE_TOKEN || '', syncFiles: false, listener: true, log: () => {} })
49
+
50
+ // ARMED, AND THE NEWEST LISTENER WINS (4 Oct 2026; see hive-listen.js). This file is how the Stop
51
+ // hook and the MCP server know, with no network call, that the agent will hear the room after its
52
+ // turn ends. It is written at start, not on the first sync: a listener still connecting delivers
53
+ // every mention once it does (check() runs on every resync), and calling it unarmed would only make
54
+ // the agent start a second one. Two listeners for one agent wake it twice for one message, so a
55
+ // listener that finds the file naming another process leaves: that one was started after it.
56
+ const ARMED = armedFile(NAME, room)
57
+ const SESSION = process.env.CLAUDE_CODE_SESSION_ID || process.env.ANTIGRAVITY_CONVERSATION_ID || ''
58
+ const AGAIN = `"${process.execPath.replace(/\\/g, '/')}" "${path.resolve(process.argv[1]).replace(/\\/g, '/')}" "${LINK}" "${NAME}" ${IDLE_MIN}`
59
+ const startedAt = Date.now()
60
+ let synced = false
61
+ const readArmed = () => { try { return JSON.parse(fs.readFileSync(ARMED, 'utf8')) } catch { return null } }
62
+ const ownerPid = () => Number((readArmed() || {}).pid) || 0
63
+ const alive = (pid) => { try { process.kill(pid, 0); return true } catch (e) { return e.code === 'EPERM' } }
64
+ // THE NEWEST BY START TIME WINS, NOT THE LAST ONE TO WRITE (6 Oct 2026, the first macOS run). Every
65
+ // listener reads the file and then rewrites it on each beat. An older listener that read it just
66
+ // before a newer one wrote it put its own claim back on top, and the NEWER one then left as
67
+ // "superseded" while the older stayed: N1 and N3 failed on macOS, never on Windows. So a claim is
68
+ // judged by when its listener started. A listener yields only to one that started after it and is
69
+ // still running; a newer one that finds an older claim on top takes the file back on its next beat.
70
+ // The file is also written whole (a temp file renamed over it), so a reader never sees half of one.
71
+ const newerThanMe = (rec) => {
72
+ const pid = Number(rec && rec.pid) || 0
73
+ if (!pid || pid === process.pid || !alive(pid)) return false
74
+ const at = Number(rec.at) || 0
75
+ return at > startedAt || (at === startedAt && pid > process.pid)
76
+ }
77
+ function beat() {
78
+ try {
79
+ fs.mkdirSync(path.dirname(ARMED), { recursive: true })
80
+ const body = JSON.stringify({ pid: process.pid, name: NAME, room, link: LINK, session: SESSION, at: startedAt, beat: Date.now(), synced })
81
+ const part = `${ARMED}.${process.pid}.tmp`
82
+ try { fs.writeFileSync(part, body); fs.renameSync(part, ARMED) } catch {
83
+ try { fs.rmSync(part, { force: true }) } catch {}
84
+ fs.writeFileSync(ARMED, body)
85
+ }
86
+ } catch { /* unarmed is the safe reading: the hook asks for a fresh listener, and it replaces this one */ }
87
+ }
88
+ const superseded = () => {
89
+ const rec = readArmed()
90
+ if (!newerThanMe(rec)) return false
91
+ finish('superseded', { note: `a newer listener for ${NAME} in ${room} took over (pid ${rec.pid}). Do not start this one again.` })
92
+ return true
93
+ }
94
+ beat()
95
+ setInterval(() => { if (!superseded()) beat() }, BEAT_MS).unref()
96
+ process.on('exit', () => { try { if (ownerPid() === process.pid) fs.unlinkSync(ARMED) } catch { } })
97
+ const chat = hive.doc.getArray('chat')
98
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
99
+ // `@all` wakes everyone — it is how the room is addressed deliberately. `\b` is NOT enough: "-" is a
100
+ // word boundary, so `@Claude\b` fired on `@Claude-2`, a different participant. A name ends where no
101
+ // name character follows. `@all` must not match `@allen` either.
102
+ const rx = new RegExp(`@(${esc(NAME)}|all)(?![\\w-])`, 'i')
103
+ let armed = false
104
+ let finishing = false
105
+
106
+ // `flushMs`: when this exit just moved the room's read cursor, give that update time to leave the
107
+ // socket. Exiting at once can drop it, and the MCP side would announce the same mail again.
108
+ async function finish(reason, payload, flushMs = 0) {
109
+ if (finishing) return
110
+ finishing = true
111
+ if (flushMs) {
112
+ const ws = hive.provider && hive.provider.ws
113
+ const end = Date.now() + flushMs
114
+ while (Date.now() < end && ws && ws.readyState === 1 && ws.bufferedAmount > 0) await new Promise((r) => setTimeout(r, 25))
115
+ await new Promise((r) => setTimeout(r, 200))
116
+ }
117
+ console.log(JSON.stringify({ reason, ...payload }))
118
+ try { hive.stop() } catch {}
119
+ process.exit(0)
120
+ }
121
+
122
+ // THE ROOM'S CURSOR IS THE SHARED ONE (3 Oct 2026). This process and the agent's MCP server each
123
+ // kept their own: a message delivered here was still unread there, so the next tool call announced
124
+ // it again as "UNREAD MAIL ... Handle before continuing" (19 old messages in one session), and mail
125
+ // the agent read through hive_check could wake a fresh listener a second time. Both now meet at the
126
+ // room's per-member cursor (hive-receipts.js), which only moves forward. Publishing it here is also
127
+ // what lets a sender's hive_delivered say READ for mail this listener handed over.
128
+ const cursors = hive.doc.getMap('cursors')
129
+ const roomCursor = (len) => {
130
+ try { const c = RC.cursorOf(cursors, NAME); const at = (c && Number(c.at)) || 0; return at <= len ? at : 0 } catch { return 0 }
131
+ }
132
+ const publish = (n) => { try { RC.publishCursor(cursors, NAME, n) } catch { /* a room that predates receipts */ } }
133
+
134
+ // A PERSISTED CURSOR, not just "my last message".
135
+ //
136
+ // The first version anchored only on the last thing I said. That is fine for an agent that
137
+ // replies to everything, and a livelock for one that listens without speaking: `lastMine` stays
138
+ // -1, so every call returns the SAME old messages, the caller loops, and it never advances.
139
+ // Measured 11 Sep 2026 — Builder's watch loop ran 420 iterations over ~42 minutes re-reading the
140
+ // same two messages, and the respawning that caused left orphaned processes holding room
141
+ // presence after the agent itself had died.
142
+ //
143
+ // So the cursor is written to disk and advanced on every exit. `lastMine` is still used, as a
144
+ // FLOOR: anything I have already replied to is handled by definition, even if a stale cursor
145
+ // says otherwise. Whichever is further along wins.
146
+ // AN ORPHAN MUST NOT KEEP SAYING "LISTENING" (2 Oct 2026). When the app that started this listener
147
+ // closes, the listener can outlive it: on macOS and Linux it is reparented, and on any OS a detached
148
+ // start escapes the parent's job. It then keeps its agent marked LISTENING, reachable, in every
149
+ // teammate's hive_members, and wakes nobody when they call. So it watches the process that started
150
+ // it and leaves the room once that process is gone. HIVE_AWAIT_KEEP_ALIVE=1 opts out, for a listener
151
+ // that is detached on purpose. (tests/hive-await-test.mjs, "an orphaned listener")
152
+ const PARENT = process.ppid
153
+ const parentGone = () => {
154
+ if (process.ppid !== PARENT) return true // reparented: the parent exited
155
+ try { process.kill(PARENT, 0); return false } catch (e) { return e.code !== 'EPERM' }
156
+ }
157
+ if (process.env.HIVE_AWAIT_KEEP_ALIVE !== '1' && PARENT > 1 && !parentGone()) {
158
+ setInterval(() => {
159
+ if (parentGone()) finish('parent-gone', { note: `the process that started this listener exited, so ${NAME} left the room` })
160
+ }, Number(process.env.HIVE_AWAIT_PARENT_CHECK_MS) || 15000).unref()
161
+ }
162
+
163
+ const cursorFile = path.join(dir, `.await-cursor-${room}.json`)
164
+ const readCursor = () => { try { return Number(JSON.parse(fs.readFileSync(cursorFile, 'utf8')).at) || 0 } catch { return 0 } }
165
+ const writeCursor = (n) => { try { fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(cursorFile, JSON.stringify({ at: n }), 'utf8') } catch { /* a cursor we cannot persist must not break the wait */ } }
166
+
167
+ function check() {
168
+ if (superseded()) return // the newer listener delivers it; consuming it here would hide it from that one
169
+ const msgs = chat.toArray()
170
+ let lastMine = -1
171
+ for (let i = msgs.length - 1; i >= 0; i--) { if (msgs[i].by === NAME) { lastMine = i; break } }
172
+ const from = Math.max(readCursor(), lastMine + 1, roomCursor(msgs.length))
173
+ const hits = []
174
+ for (let i = from; i < msgs.length; i++) {
175
+ const m = msgs[i]
176
+ if (rx.test(String(m.text || '')) && m.by !== NAME) hits.push(m)
177
+ }
178
+ // Advance past everything READ, not just everything matched — an unaddressed message I have
179
+ // already seen must not be re-scanned on the next call either.
180
+ if (hits.length) {
181
+ writeCursor(msgs.length)
182
+ publish(msgs.length)
183
+ // THE ORDER TO START AGAIN RIDES ON EVERY WAKE. It used to be said once, on the agent's first
184
+ // quiet hive_wait, and an agent had to remember it after every wake; agents forget (4 Oct 2026).
185
+ finish('mention', { messages: hits.map((m) => `${m.by}(${m.kind}): ${m.text}`), next: `When you have answered, start this listener again in the background, the same way you started it, or you stop hearing the room when your turn ends: ${AGAIN}` }, 1500)
186
+ }
187
+ }
188
+
189
+ hive.provider.on('sync', (s) => {
190
+ if (!s) return
191
+ if (!synced) { synced = true; beat() }
192
+ if (!armed) {
193
+ armed = true
194
+ console.error(`[await] ${NAME} listening in ${room}. In room: ${hive.members().map((m) => m.name).join(', ')}`)
195
+ chat.observe(check)
196
+ }
197
+ check() // also on every resync — mentions may have landed while the socket was down
198
+ })
199
+
200
+ // An idle timeout also advances the cursor. Without this, a quiet window followed by a loud one
201
+ // would re-deliver everything that arrived during the quiet window too.
202
+ setTimeout(() => {
203
+ try { writeCursor(chat.length) } catch {}
204
+ publish(chat.length) // nothing in the window named us, so nothing in it is mail the MCP should raise
205
+ finish('idle', { note: `nothing addressed to ${NAME} in ${IDLE_MIN} min. Run this same command again in the background to keep listening: ${AGAIN}` }, 1500)
206
+ }, Number(IDLE_MIN) * 60 * 1000)
207
+ process.on('SIGINT', () => { try { hive.stop() } catch {}; process.exit(0) })