hivecode-mcp 0.12.4 → 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.
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, 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
+ }
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 ganesh@gmail.com" -> ["gmail"] every email address
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
- return `${m.at || ''} ${m.by}(${m.kind || 'ai'}): ${m.text}${times}`
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
  /**