hivecode-mcp 0.14.1 → 0.14.3
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 +98 -98
- package/LICENSE +21 -21
- package/README.md +421 -421
- package/core.js +91 -91
- package/docs/ALWAYS_ON.md +310 -310
- package/docs/GITHUB_WORKFLOW.md +394 -394
- package/docs/HIVE_RULES.md +41 -41
- package/docs/MCP.md +157 -157
- package/docs/QUICKSTART.md +266 -266
- package/docs/SELF_HOSTING.md +256 -256
- package/docs/SLACK_SETUP.md +474 -474
- package/docs/TROUBLESHOOTING.md +106 -105
- package/hive-activity.js +336 -336
- package/hive-authors.js +46 -44
- package/hive-await.js +230 -230
- package/hive-branch.js +674 -607
- package/hive-busy.js +88 -88
- package/hive-coord.js +110 -110
- package/hive-gate.js +135 -135
- package/hive-github.js +3 -1
- package/hive-hook-install.js +179 -179
- package/hive-instructions.js +74 -74
- package/hive-land.js +709 -691
- package/hive-listen.js +81 -81
- package/hive-mailbox.js +520 -520
- package/hive-mcp-http.js +591 -591
- package/hive-mcp.js +92 -12
- package/hive-mux-proto.js +58 -58
- package/hive-mux.js +354 -351
- package/hive-notify.js +234 -233
- package/hive-obligations.js +247 -247
- package/hive-poll-proto.js +50 -50
- package/hive-pr-staleness.js +189 -189
- package/hive-pr.js +637 -512
- package/hive-privacy.js +327 -327
- package/hive-receipts.js +233 -233
- package/hive-relay-health.js +234 -234
- package/hive-repos.js +430 -430
- package/hive-room-key.js +210 -210
- package/hive-room-mode.js +1761 -1668
- package/hive-room-watcher.js +623 -561
- package/hive-secrets.js +89 -89
- package/hive-session.js +114 -114
- package/hive-setup.js +560 -560
- package/hive-slack-agent.js +408 -408
- package/hive-slack.js +2417 -2367
- package/hive-stdio.js +172 -172
- package/hive-stop-hook.js +293 -293
- package/hive-throttle.js +118 -118
- package/hive-time.js +71 -71
- package/hive-transport.js +617 -578
- package/hive-update.js +58 -58
- package/hive-version.js +110 -110
- package/hive-webhook.js +55 -54
- package/icr-merge.js +58 -58
- package/icr.js +856 -851
- package/lang-brace.js +305 -305
- package/lang-js.js +695 -651
- package/lang-json.js +211 -211
- package/lang-python.js +588 -588
- package/lang-ruby.js +230 -230
- package/lang-toml.js +136 -136
- package/lang-yaml.js +183 -183
- package/limits.js +413 -413
- package/package.json +11 -3
- package/reconcile.cjs +621 -621
- package/slack-app-manifest-agent.json +61 -61
- package/slack-app-manifest-inbound.json +36 -36
- package/slack-app-manifest.json +27 -27
- package/substrate.js +252 -252
- package/sync.js +434 -60
- package/token.js +201 -201
package/hive-notify.js
CHANGED
|
@@ -1,233 +1,234 @@
|
|
|
1
|
-
// hive-notify -- tell a person, on Slack or Discord, when an agent in a room is waiting on them.
|
|
2
|
-
//
|
|
3
|
-
// npx -y hivecode-mcp@latest notify "<link>" --webhook <url> --for <name>[,<name>] [--every <minutes>] [--title <text>]
|
|
4
|
-
//
|
|
5
|
-
// WHY. On 10 Oct 2026 a two-agent demo stalled for an hour: one agent waited for its person's OK,
|
|
6
|
-
// the other waited for it, and the person was not looking at the chat. The room knew. Nobody was
|
|
7
|
-
// told. An owner who leaves agents working has to hear about the two things only they can unblock:
|
|
8
|
-
//
|
|
9
|
-
// - a task waiting for their approval sent at once: an agent is stopped until they answer
|
|
10
|
-
// - messages from agents naming them batched, at most one post per --every minutes
|
|
11
|
-
//
|
|
12
|
-
// Neither is sent while that person is in the room (the Control Room counts): they can see it.
|
|
13
|
-
//
|
|
14
|
-
// WHAT IS NEVER SENT: the room's link or full id. On an open relay the id is the key to the room's
|
|
15
|
-
// files, and a Slack channel is not the place for a key. Messages name the room by --title, or by
|
|
16
|
-
// the first characters of its id.
|
|
17
|
-
//
|
|
18
|
-
// IT MUST NOT LOOK ALIVE AND DELIVER NOTHING. It posts once when it starts and refuses to run if
|
|
19
|
-
// that post fails; it says once when it has lost the room and once when it is back.
|
|
20
|
-
//
|
|
21
|
-
// Error codes Q640-Q649 are this module's.
|
|
22
|
-
|
|
23
|
-
import fs from 'fs'
|
|
24
|
-
import os from 'os'
|
|
25
|
-
import path from 'path'
|
|
26
|
-
import crypto from 'crypto'
|
|
27
|
-
import { startSync, parseLink } from './sync.js'
|
|
28
|
-
import { makeNotifier, scrub } from './hive-webhook.js'
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
fs.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
err
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
err
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let
|
|
105
|
-
let
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (!
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
--
|
|
174
|
-
--
|
|
175
|
-
--
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
if (!
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
process.
|
|
229
|
-
process.on('
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
1
|
+
// hive-notify -- tell a person, on Slack or Discord, when an agent in a room is waiting on them.
|
|
2
|
+
//
|
|
3
|
+
// npx -y hivecode-mcp@latest notify "<link>" --webhook <url> --for <name>[,<name>] [--every <minutes>] [--title <text>]
|
|
4
|
+
//
|
|
5
|
+
// WHY. On 10 Oct 2026 a two-agent demo stalled for an hour: one agent waited for its person's OK,
|
|
6
|
+
// the other waited for it, and the person was not looking at the chat. The room knew. Nobody was
|
|
7
|
+
// told. An owner who leaves agents working has to hear about the two things only they can unblock:
|
|
8
|
+
//
|
|
9
|
+
// - a task waiting for their approval sent at once: an agent is stopped until they answer
|
|
10
|
+
// - messages from agents naming them batched, at most one post per --every minutes
|
|
11
|
+
//
|
|
12
|
+
// Neither is sent while that person is in the room (the Control Room counts): they can see it.
|
|
13
|
+
//
|
|
14
|
+
// WHAT IS NEVER SENT: the room's link or full id. On an open relay the id is the key to the room's
|
|
15
|
+
// files, and a Slack channel is not the place for a key. Messages name the room by --title, or by
|
|
16
|
+
// the first characters of its id.
|
|
17
|
+
//
|
|
18
|
+
// IT MUST NOT LOOK ALIVE AND DELIVER NOTHING. It posts once when it starts and refuses to run if
|
|
19
|
+
// that post fails; it says once when it has lost the room and once when it is back.
|
|
20
|
+
//
|
|
21
|
+
// Error codes Q640-Q649 are this module's.
|
|
22
|
+
|
|
23
|
+
import fs from 'fs'
|
|
24
|
+
import os from 'os'
|
|
25
|
+
import path from 'path'
|
|
26
|
+
import crypto from 'crypto'
|
|
27
|
+
import { startSync, parseLink } from './sync.js'
|
|
28
|
+
import { makeNotifier, scrub } from './hive-webhook.js'
|
|
29
|
+
import { relayFetch } from './hive-transport.js'
|
|
30
|
+
|
|
31
|
+
const oneLine = (s, n = 200) => { const t = String(s || '').replace(/\s+/g, ' ').trim(); return t.length > n ? `${t.slice(0, n - 1)}…` : t }
|
|
32
|
+
const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
33
|
+
const LOST_AFTER = 5 * 60000
|
|
34
|
+
|
|
35
|
+
/** Does `text` address `name` (@Name, not part of a longer word)? @all counts. */
|
|
36
|
+
export function addresses(text, name) {
|
|
37
|
+
const t = String(text || '')
|
|
38
|
+
if (/(^|[^\w@])@all\b/i.test(t)) return true
|
|
39
|
+
return new RegExp(`(^|[^\\w@])@${esc(name)}(?![\\w-])`, 'i').test(t)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What has been sent, kept on disk so a restart does not send it again. */
|
|
43
|
+
export function stateFileFor(room, home = os.homedir()) {
|
|
44
|
+
const h = crypto.createHash('sha256').update(String(room)).digest('hex').slice(0, 16)
|
|
45
|
+
return path.join(home, '.hivecode', 'notify', `${h}.json`)
|
|
46
|
+
}
|
|
47
|
+
export function lockFileFor(room, home = os.homedir()) {
|
|
48
|
+
const h = crypto.createHash('sha256').update(String(room)).digest('hex').slice(0, 16)
|
|
49
|
+
return path.join(home, '.hivecode', 'notify', `${h}.lock`)
|
|
50
|
+
}
|
|
51
|
+
const alive = (pid) => { try { process.kill(pid, 0); return true } catch (e) { return e.code === 'EPERM' } }
|
|
52
|
+
|
|
53
|
+
/** One notifier per room. Created exclusively so two starting at the same time cannot double-post. */
|
|
54
|
+
export function takeLock(file) {
|
|
55
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
56
|
+
for (let i = 0; i < 2; i++) {
|
|
57
|
+
try {
|
|
58
|
+
const fd = fs.openSync(file, 'wx')
|
|
59
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: new Date().toISOString() }))
|
|
60
|
+
fs.closeSync(fd)
|
|
61
|
+
return () => { try { if (JSON.parse(fs.readFileSync(file, 'utf8')).pid === process.pid) fs.unlinkSync(file) } catch { } }
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (e.code !== 'EEXIST') throw e
|
|
64
|
+
let held = null
|
|
65
|
+
try { held = JSON.parse(fs.readFileSync(file, 'utf8')) } catch { held = null }
|
|
66
|
+
if (held && held.pid && held.pid !== process.pid && alive(held.pid)) {
|
|
67
|
+
const err = new Error(`Q644 a notifier is already running for this room (process ${held.pid}, since ${held.at}). Use that one, or stop it first.`)
|
|
68
|
+
err.code = 'Q644'
|
|
69
|
+
throw err
|
|
70
|
+
}
|
|
71
|
+
try { fs.unlinkSync(file) } catch { }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const err = new Error(`Q644 could not take ${file}`)
|
|
75
|
+
err.code = 'Q644'
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
|
+
function loadSent(file) {
|
|
79
|
+
try { const j = JSON.parse(fs.readFileSync(file, 'utf8')); if (j && j.tasks) return j } catch { }
|
|
80
|
+
return { tasks: {} }
|
|
81
|
+
}
|
|
82
|
+
function saveSent(file, s) {
|
|
83
|
+
try {
|
|
84
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
85
|
+
const ids = Object.keys(s.tasks)
|
|
86
|
+
if (ids.length > 500) for (const id of ids.slice(0, ids.length - 500)) delete s.tasks[id]
|
|
87
|
+
fs.writeFileSync(file, JSON.stringify(s))
|
|
88
|
+
} catch { /* a notifier that cannot remember sends twice; it still sends */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The watching itself, on a live sync handle. `check()` is one pass; the CLI calls it on a timer,
|
|
93
|
+
* and tests call it with a clock of their own.
|
|
94
|
+
*/
|
|
95
|
+
export function makeWatch({ hive, forNames, title, send, now = () => Date.now(), every = 5, stateFile, log = () => {} }) {
|
|
96
|
+
const people = forNames.map((n) => String(n).trim()).filter(Boolean)
|
|
97
|
+
const lc = new Set(people.map((n) => n.toLowerCase()))
|
|
98
|
+
const tasks = hive.doc.getMap('tasks')
|
|
99
|
+
const owners = hive.doc.getMap('owners')
|
|
100
|
+
const chat = hive.doc.getArray('chat')
|
|
101
|
+
const sent = loadSent(stateFile)
|
|
102
|
+
let seenChat = chat.length // what was said before this started is not news
|
|
103
|
+
const buffer = []
|
|
104
|
+
let lastFlush = 0
|
|
105
|
+
let lostSince = 0
|
|
106
|
+
let toldLost = false
|
|
107
|
+
|
|
108
|
+
const here = (name) => (hive.members ? hive.members() : []).some((m) => String(m.name).toLowerCase() === name.toLowerCase() && m.kind === 'human')
|
|
109
|
+
const ownerOf = (t) => String(owners.get(t.to) || '')
|
|
110
|
+
|
|
111
|
+
function waiting() {
|
|
112
|
+
return [...tasks.values()].filter((t) => t && t.status === 'pending' && lc.has(ownerOf(t).toLowerCase()))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function check() {
|
|
116
|
+
const t = now()
|
|
117
|
+
// THE CONNECTION. Lost for five minutes is said once; back is said once.
|
|
118
|
+
const connected = !!(hive.connection ? hive.connection().connected : hive.provider && hive.provider.wsconnected)
|
|
119
|
+
if (!connected) {
|
|
120
|
+
if (!lostSince) lostSince = t
|
|
121
|
+
if (!toldLost && t - lostSince >= LOST_AFTER) {
|
|
122
|
+
toldLost = true
|
|
123
|
+
await send(`Hivecode lost ${title} ${Math.round((t - lostSince) / 60000)} minutes ago, so nothing from it can reach you here. It keeps trying to reconnect.`)
|
|
124
|
+
}
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
if (lostSince) {
|
|
128
|
+
if (toldLost) await send(`Hivecode is back in ${title}.`)
|
|
129
|
+
lostSince = 0
|
|
130
|
+
toldLost = false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A TASK WAITING FOR APPROVAL: at once, once per task.
|
|
134
|
+
for (const task of waiting()) {
|
|
135
|
+
const owner = ownerOf(task)
|
|
136
|
+
if (sent.tasks[task.id] || here(owner)) continue
|
|
137
|
+
const ok = await send(`Approval needed in ${title}: ${task.by} asked ${task.to} to "${oneLine(task.text, 300)}". ${task.to} waits until ${owner} approves or denies it in the Control Room.`)
|
|
138
|
+
if (ok) {
|
|
139
|
+
sent.tasks[task.id] = t
|
|
140
|
+
saveSent(stateFile, sent)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// MESSAGES NAMING THEM, from agents, while they are away: batched.
|
|
145
|
+
const all = chat.toArray()
|
|
146
|
+
if (all.length < seenChat) seenChat = 0 // the relay restarted and the room's history went with it
|
|
147
|
+
for (const m of all.slice(seenChat)) {
|
|
148
|
+
if (!m || m.kind !== 'ai') continue
|
|
149
|
+
for (const p of people) if (addresses(m.text, p) && !here(p)) { buffer.push({ by: m.by, to: p, text: m.text }); break }
|
|
150
|
+
}
|
|
151
|
+
seenChat = all.length
|
|
152
|
+
if (buffer.length && t - lastFlush >= every * 60000) {
|
|
153
|
+
const shown = buffer.slice()
|
|
154
|
+
const to = [...new Set(shown.map((b) => b.to))].join(', ')
|
|
155
|
+
const lines = shown.slice(0, 5).map((b) => `- ${b.by}: ${oneLine(b.text, 220)}`)
|
|
156
|
+
if (shown.length > 5) lines.push(`- and ${shown.length - 5} more`)
|
|
157
|
+
const ok = await send(`${shown.length} message${shown.length === 1 ? '' : 's'} for ${to} in ${title}:\n${lines.join('\n')}`)
|
|
158
|
+
if (ok) {
|
|
159
|
+
buffer.splice(0, shown.length)
|
|
160
|
+
lastFlush = t
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { check, waiting, buffered: () => buffer.length, sent }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- CLI -------------------------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
const USAGE = `usage:
|
|
171
|
+
npx -y hivecode-mcp@latest notify "<link>" --webhook <Slack or Discord webhook URL> --for <name>[,<name>]
|
|
172
|
+
|
|
173
|
+
--for <names> whose approvals and mentions to send (the people, not their agents)
|
|
174
|
+
--every <min> at most one post of messages per this many minutes (default 5)
|
|
175
|
+
--title <text> what to call the room in posts (default: the first characters of its id)
|
|
176
|
+
--name <name> the notifier's own name in the room (default Notifier)`
|
|
177
|
+
|
|
178
|
+
export async function main(argv, { log = console.log, logErr = console.error, fetchImpl = relayFetch, home = os.homedir(), now = () => Date.now(), once = false } = {}) {
|
|
179
|
+
if (argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') { log(USAGE); return 0 }
|
|
180
|
+
const SWITCHES = new Set(['dry-run', 'once'])
|
|
181
|
+
const pos = []
|
|
182
|
+
const f = {}
|
|
183
|
+
for (let i = 0; i < argv.length; i++) {
|
|
184
|
+
const a = argv[i]
|
|
185
|
+
if (a.startsWith('--')) {
|
|
186
|
+
const k = a.slice(2)
|
|
187
|
+
if (SWITCHES.has(k)) f[k] = true
|
|
188
|
+
else f[k] = argv[++i] ?? ''
|
|
189
|
+
} else {
|
|
190
|
+
pos.push(a)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const link = pos[0] || ''
|
|
194
|
+
const { relay, room, token } = parseLink(link)
|
|
195
|
+
if (!relay || !room) { logErr(`Q640 "${link}" is not a room link (it looks like wss://relay|room-...).\n\n${USAGE}`); return 2 }
|
|
196
|
+
if (!f.webhook) { logErr(`Q641 say where to post, with --webhook <Slack or Discord incoming-webhook URL>.\n\n${USAGE}`); return 2 }
|
|
197
|
+
const forNames = String(f.for || '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
198
|
+
if (!forNames.length) { logErr(`Q642 say whose waits to report, with --for <name> (the person, as they appear in the room).\n\n${USAGE}`); return 2 }
|
|
199
|
+
const every = Number(f.every || 5)
|
|
200
|
+
if (!(every > 0)) { logErr('Q642 --every must be a positive number of minutes'); return 2 }
|
|
201
|
+
const title = f.title ? oneLine(f.title, 60) : `room ${room.slice(0, 8)}…`
|
|
202
|
+
const send = makeNotifier(f.webhook, { fetchImpl, log })
|
|
203
|
+
|
|
204
|
+
// FIRST, PROVE THE WEBHOOK WORKS. A notifier whose posts go nowhere is worse than none: the person
|
|
205
|
+
// believes they will be told.
|
|
206
|
+
if (send.kind === 'other' && send.host) log(`posting in Slack format to ${send.host}`)
|
|
207
|
+
const okSent = await send(`Hivecode will post here when an agent in ${title} is waiting on ${forNames.join(', ')}.`)
|
|
208
|
+
if (!okSent) { logErr('Q643 the webhook refused the first post (see above). Check the URL, then start again. Nothing is running.'); return 1 }
|
|
209
|
+
|
|
210
|
+
const lockFile = lockFileFor(room, home)
|
|
211
|
+
const releaseLock = (() => { try { return takeLock(lockFile) } catch (e) { logErr(e.message); return null } })()
|
|
212
|
+
if (!releaseLock) return 2
|
|
213
|
+
|
|
214
|
+
const dir = path.join(home, '.hivecode', 'notify', `room-${crypto.createHash('sha256').update(room).digest('hex').slice(0, 12)}`)
|
|
215
|
+
const hive = startSync({ relay, room, dir, name: f.name || 'Notifier', kind: 'ai', token: token || process.env.HIVE_TOKEN || '', syncFiles: false, passive: true, log: () => { } })
|
|
216
|
+
const w = makeWatch({ hive, forNames, title, send, now, every, stateFile: stateFileFor(room, home), log })
|
|
217
|
+
let synced = false
|
|
218
|
+
await new Promise((res) => { const t = setTimeout(res, 15000); hive.provider.on('sync', (s) => { if (s) { synced = true; clearTimeout(t); res() } }) })
|
|
219
|
+
if (!synced && !(hive.provider && hive.provider.wsconnected)) {
|
|
220
|
+
logErr(`NOTE: could not connect to relay at ${relay}. The room may be unreachable or the network down.`)
|
|
221
|
+
}
|
|
222
|
+
const members = (hive.members() || []).filter((m) => m.name !== (f.name || 'Notifier')).map((m) => `${m.name} (${m.kind})`)
|
|
223
|
+
const pending = w.waiting().length
|
|
224
|
+
log(`watching ${title}: ${members.length ? members.join(', ') : 'nobody here yet'}; ${pending} approval(s) waiting now. Ctrl+C stops it.`)
|
|
225
|
+
if (!members.length && !hive.doc.getArray('chat').length) log('NOTE: this room is empty -- no members and no chat. Is the link right?')
|
|
226
|
+
if (once || f['dry-run']) { await w.check(); hive.stop(); releaseLock(); return 0 }
|
|
227
|
+
const timer = setInterval(() => { w.check().catch((e) => log(`check failed: ${e.message}`)) }, 15000)
|
|
228
|
+
const quit = () => { clearInterval(timer); try { hive.stop() } catch { } try { releaseLock() } catch { } process.exit(0) }
|
|
229
|
+
process.on('SIGINT', quit)
|
|
230
|
+
process.on('SIGTERM', quit)
|
|
231
|
+
await w.check()
|
|
232
|
+
return new Promise(() => { }) // runs until stopped
|
|
233
|
+
}
|
|
234
|
+
|