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-stop-hook.js
CHANGED
|
@@ -57,6 +57,7 @@ import fs from 'fs'
|
|
|
57
57
|
import path from 'path'
|
|
58
58
|
import { mentionsName, unansweredFor } from './hive-mailbox.js'
|
|
59
59
|
import { isArmed, listenCommand, listenHow, listenOrder } from './hive-listen.js'
|
|
60
|
+
import { readAgents, agentForHook } from './hive-session.js'
|
|
60
61
|
|
|
61
62
|
// Consecutive turns we will block purely because a task is still open, before standing down.
|
|
62
63
|
// Messages are not subject to this: the watermark already makes each one block at most once.
|
|
@@ -205,12 +206,14 @@ function obligations({ chat, tasks, me, seen, taskBlocks, nudged = [] }) {
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
/** Walk up from `start` looking for the session file; the hook's cwd is usually but not always
|
|
208
|
-
* the folder the MCP server was launched in.
|
|
209
|
-
|
|
209
|
+
* the folder the MCP server was launched in. That folder can have started several agents (two
|
|
210
|
+
* Claude Code sessions in one repo), so the record is the one for THIS hook's conversation, and
|
|
211
|
+
* none at all when that cannot be told (hive-session.js). */
|
|
212
|
+
function findSession(start, hook = {}) {
|
|
210
213
|
let dir = path.resolve(start)
|
|
211
214
|
for (let i = 0; i < 4; i++) {
|
|
212
215
|
const p = path.join(dir, SESSION_FILE)
|
|
213
|
-
if (fs.existsSync(p)) return { root: dir, session:
|
|
216
|
+
if (fs.existsSync(p)) return { root: dir, session: agentForHook(readAgents(p), hook) }
|
|
214
217
|
const up = path.dirname(dir)
|
|
215
218
|
if (up === dir) break
|
|
216
219
|
dir = up
|
|
@@ -230,7 +233,7 @@ async function main() {
|
|
|
230
233
|
// GUARD 1: this stop is already the product of a block. Never stack another on it.
|
|
231
234
|
if (hook.stop_hook_active || Number(hook.executionNum) > 0) return allow()
|
|
232
235
|
|
|
233
|
-
const found = findSession(hook.cwd || (Array.isArray(hook.workspacePaths) && hook.workspacePaths[0]) || process.cwd())
|
|
236
|
+
const found = findSession(hook.cwd || (Array.isArray(hook.workspacePaths) && hook.workspacePaths[0]) || process.cwd(), hook)
|
|
234
237
|
if (!found || !found.session || !found.session.dir || !found.session.name) return allow() // not in a room
|
|
235
238
|
|
|
236
239
|
const { root, session } = found
|
|
@@ -258,7 +261,7 @@ async function main() {
|
|
|
258
261
|
let listening = null, listenLine = ''
|
|
259
262
|
if (how && cfg.relay && cfg.room && !session.authed && !cfg.token) {
|
|
260
263
|
listening = isArmed({ name: session.name, room: cfg.room, session: String(hook.session_id || hook.conversationId || process.env.ANTIGRAVITY_CONVERSATION_ID || '') })
|
|
261
|
-
listenLine = listenOrder(how, listenCommand({ relay: cfg.relay, room: cfg.room, name: session.name }))
|
|
264
|
+
listenLine = listenOrder(how, listenCommand({ relay: cfg.relay, room: cfg.room, name: session.name, app: APP }))
|
|
262
265
|
}
|
|
263
266
|
|
|
264
267
|
const d = decide({
|
package/hive-time.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified timestamp parsing and formatting for Hivecode chat and receipts.
|
|
3
|
+
* Single source of truth used by:
|
|
4
|
+
* 1. public/control.html (browser Control Room ago() and chat timestamps)
|
|
5
|
+
* 2. hive-mailbox.js (hive_read_chat formatMessage)
|
|
6
|
+
* 3. sync.js (HIVE_CHAT.md renderChat)
|
|
7
|
+
*
|
|
8
|
+
* Handles:
|
|
9
|
+
* - Unix microseconds (> 1e14), as emitted by hive-slack.js, hive-mcp-http.js, hive-gate.js
|
|
10
|
+
* - Unix milliseconds (> 1e11)
|
|
11
|
+
* - Existing "HH:MM:SS" strings (passed through as-is per Claude's directive)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function toMillis(at) {
|
|
15
|
+
if (at == null || at === '') return null
|
|
16
|
+
const num = typeof at === 'number' ? at : Number(at)
|
|
17
|
+
if (!isNaN(num) && num > 0) {
|
|
18
|
+
if (num > 1e14) {
|
|
19
|
+
// Unix microseconds (> 100,000,000,000,000)
|
|
20
|
+
return Math.round(num / 1000)
|
|
21
|
+
}
|
|
22
|
+
if (num > 1e11) {
|
|
23
|
+
// Unix milliseconds (> 100,000,000,000)
|
|
24
|
+
return Math.round(num)
|
|
25
|
+
}
|
|
26
|
+
if (num > 1e9) {
|
|
27
|
+
// Unix seconds (> 1,000,000,000)
|
|
28
|
+
return Math.round(num * 1000)
|
|
29
|
+
}
|
|
30
|
+
return Math.round(num)
|
|
31
|
+
}
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ago(at, now = Date.now()) {
|
|
36
|
+
if (!at) return ''
|
|
37
|
+
if (typeof at === 'string') {
|
|
38
|
+
const num = Number(at)
|
|
39
|
+
if (!isNaN(num) && num > 1e11) {
|
|
40
|
+
return ago(num, now)
|
|
41
|
+
}
|
|
42
|
+
// String with clock time or text: print as is (matches control.html behavior)
|
|
43
|
+
return at
|
|
44
|
+
}
|
|
45
|
+
const ms = toMillis(at)
|
|
46
|
+
if (ms == null) return String(at)
|
|
47
|
+
const d = Math.max(0, now - ms)
|
|
48
|
+
const m = Math.floor(d / 60000)
|
|
49
|
+
if (m < 1) return 'now'
|
|
50
|
+
if (m < 60) return `${m}m`
|
|
51
|
+
const h = Math.floor(m / 60)
|
|
52
|
+
return h < 24 ? `${h}h` : `${Math.floor(h / 24)}d`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatTime(at, now = Date.now()) {
|
|
56
|
+
if (!at) return ''
|
|
57
|
+
if (typeof at === 'string') {
|
|
58
|
+
const num = Number(at)
|
|
59
|
+
if (!isNaN(num) && num > 1e11) {
|
|
60
|
+
return formatTime(num, now)
|
|
61
|
+
}
|
|
62
|
+
return at
|
|
63
|
+
}
|
|
64
|
+
const ms = toMillis(at)
|
|
65
|
+
if (ms == null) return String(at)
|
|
66
|
+
return new Date(ms).toTimeString().slice(0, 8)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatAt(at, { ago: isAgo = false, now = Date.now() } = {}) {
|
|
70
|
+
return isAgo ? ago(at, now) : formatTime(at, now)
|
|
71
|
+
}
|
package/hive-webhook.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// hive-webhook -- one Slack or Discord incoming-webhook URL, and the rule for what may be sent to it.
|
|
2
|
+
//
|
|
3
|
+
// Shared by the GitHub watcher (hive-github.js) and the room notifier (hive-notify.js): the same
|
|
4
|
+
// URL works for both, and the same two promises hold for both.
|
|
5
|
+
// 1. Nothing with a key or token in it leaves: every message is scrubbed first.
|
|
6
|
+
// 2. A notification that fails is logged and dropped, never retried in a loop and never a reason
|
|
7
|
+
// to stop: it is a courtesy, the work is the point.
|
|
8
|
+
import { findSecrets } from './hive-secrets.js'
|
|
9
|
+
|
|
10
|
+
/** Every key or token in `text` replaced. Run on everything that goes to GitHub or a webhook. */
|
|
11
|
+
export function scrub(text) {
|
|
12
|
+
let s = String(text || '')
|
|
13
|
+
for (const f of findSecrets(s, { tokens: true })) s = s.split(f.value).join(`[${f.kind} removed]`)
|
|
14
|
+
return s
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function cleanError(e, url) {
|
|
18
|
+
let s = String(e && (e.message || e) || 'unknown error')
|
|
19
|
+
if (url) s = s.split(url).join('[webhook url removed]')
|
|
20
|
+
try {
|
|
21
|
+
const u = new URL(url)
|
|
22
|
+
if (u.pathname && u.pathname !== '/') s = s.split(u.pathname).join('[path removed]')
|
|
23
|
+
} catch { }
|
|
24
|
+
s = scrub(s)
|
|
25
|
+
s = s.replace(/Failed to parse URL from .+/i, 'Failed to parse URL')
|
|
26
|
+
return s
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `send(text)` resolves true when the webhook took it. Slack wants {text}, Discord {content}; any
|
|
30
|
+
* other host gets Slack format ({text}). */
|
|
31
|
+
export function makeNotifier(url, { fetchImpl = globalThis.fetch, log = () => {} } = {}) {
|
|
32
|
+
if (!url) return async () => false
|
|
33
|
+
let host = ''
|
|
34
|
+
try { host = new URL(url).hostname.toLowerCase() } catch { host = '' }
|
|
35
|
+
const slack = /(^|\.)slack\.com$/.test(host)
|
|
36
|
+
const discord = /(^|\.)discord(app)?\.com$/.test(host)
|
|
37
|
+
const kind = discord ? 'discord' : slack ? 'slack' : 'other'
|
|
38
|
+
const send = async (text) => {
|
|
39
|
+
const msg = scrub(text)
|
|
40
|
+
// allowed_mentions: an issue titled "@everyone" must not page a whole Discord server.
|
|
41
|
+
const body = discord ? { content: msg.slice(0, 1900), allowed_mentions: { parse: [] } } : { text: msg }
|
|
42
|
+
try {
|
|
43
|
+
const r = await fetchImpl(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(10000) })
|
|
44
|
+
if (!r.ok) { log(`notification not delivered: HTTP ${r.status}`); return false }
|
|
45
|
+
return true
|
|
46
|
+
} catch (e) {
|
|
47
|
+
log(`notification not delivered: ${cleanError(e, url)}`)
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
send.host = host
|
|
52
|
+
send.kind = kind
|
|
53
|
+
return send
|
|
54
|
+
}
|