hivecode-mcp 0.13.1 → 0.14.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/docs/SLACK_SETUP.md +24 -0
- package/hive-await.js +23 -5
- package/hive-github.js +2002 -1944
- package/hive-mcp-http.js +12 -1
- package/hive-mcp.js +2129 -2090
- package/hive-mux.js +2 -1
- package/hive-poll-proto.js +50 -0
- package/hive-relay-health.js +5 -1
- package/hive-room-key.js +2 -1
- package/hive-setup.js +4 -2
- package/hive-slack-agent.js +14 -2
- package/hive-slack.js +595 -37
- package/hive-transport.js +578 -0
- package/package.json +192 -190
- package/reconcile.cjs +17 -10
- package/sync.js +5031 -4992
package/hive-mux.js
CHANGED
|
@@ -19,6 +19,7 @@ import * as decoding from 'lib0/decoding'
|
|
|
19
19
|
import { ObservableV2 } from 'lib0/observable'
|
|
20
20
|
import { WebsocketProvider } from 'y-websocket'
|
|
21
21
|
import { F, encodeFrame, decodeFrame, muxPath, MUX_VERSION } from './hive-mux-proto.js'
|
|
22
|
+
import { relayFetch } from './hive-transport.js' // /health through the proxy, as the sockets go
|
|
22
23
|
|
|
23
24
|
const messageSync = 0
|
|
24
25
|
|
|
@@ -257,7 +258,7 @@ class MuxConnection {
|
|
|
257
258
|
* decided once -- HIVE_MUX=1 forces mux, HIVE_MUX=0 forces a socket per file, anything else asks
|
|
258
259
|
* the relay's /health -- and file providers created before the answer are bound when it comes.
|
|
259
260
|
*/
|
|
260
|
-
export function createFileTransport({ relay, room, wsOpts = {}, log = () => { }, env = process.env, fetchImpl =
|
|
261
|
+
export function createFileTransport({ relay, room, wsOpts = {}, log = () => { }, env = process.env, fetchImpl = relayFetch, onFallback = () => { }, retryMs = 1000, detectForMs = 90000 }) {
|
|
261
262
|
const forced = env.HIVE_MUX === '0' || env.HIVE_MUX === '1'
|
|
262
263
|
let mode = env.HIVE_MUX === '0' ? 'socket' : env.HIVE_MUX === '1' ? 'mux' : null
|
|
263
264
|
let mux = null
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// hive-poll-proto.js -- the wire format of the HTTPS long-poll transport (relay-poll.js on the
|
|
2
|
+
// relay, hive-transport.js in clients). It exists for networks that refuse WebSocket upgrades but
|
|
3
|
+
// pass ordinary HTTPS: Antigravity's cloud VMs (their egress proxy answers "403 WebSocket upgrades
|
|
4
|
+
// are not allowed", 7 Nov 2026) and many company proxies.
|
|
5
|
+
//
|
|
6
|
+
// It carries EXACTLY the bytes a WebSocket would: each message is one frame, in order, so every
|
|
7
|
+
// gate on the relay (claims, read-only, the governor, rate limits) sees what it always sees.
|
|
8
|
+
//
|
|
9
|
+
// POST /_hive_poll/open JSON {path, v} -> 200 {sid, holdMs, idleMs, v} | the refusal an upgrade would get
|
|
10
|
+
// POST /_hive_poll/<sid>/send frames, header x-hive-seq: the first frame's number (1, 2, 3, ...)
|
|
11
|
+
// -> 204 once delivered. A retry of the same numbers is ignored, so a
|
|
12
|
+
// lost answer never applies a message twice; a gap is a 409.
|
|
13
|
+
// POST /_hive_poll/<sid>/recv header x-hive-ack: the last frame received, x-hive-hold: ms to wait
|
|
14
|
+
// -> 200 frames (x-hive-seq: the first one's number) | 204 nothing yet
|
|
15
|
+
// Frames up to the ack are forgotten; the rest are sent again, so a
|
|
16
|
+
// lost answer loses nothing.
|
|
17
|
+
// POST /_hive_poll/<sid>/close -> 204
|
|
18
|
+
// An unknown or ended session answers 404 {gone:true}: the client reconnects and resyncs.
|
|
19
|
+
//
|
|
20
|
+
// The room, the token and the name travel in the open request's BODY, never in a URL: a proxy logs
|
|
21
|
+
// URLs, and a room id is the key to the room.
|
|
22
|
+
export const POLL_PREFIX = '/_hive_poll/'
|
|
23
|
+
export const POLL_VERSION = 1
|
|
24
|
+
|
|
25
|
+
/** Frames to one buffer: for each, a 4-byte big-endian length, then its bytes. */
|
|
26
|
+
export function packFrames(frames) {
|
|
27
|
+
let total = 0
|
|
28
|
+
for (const f of frames) total += 4 + f.length
|
|
29
|
+
const out = Buffer.allocUnsafe(total)
|
|
30
|
+
let o = 0
|
|
31
|
+
for (const f of frames) {
|
|
32
|
+
out.writeUInt32BE(f.length, o); o += 4
|
|
33
|
+
Buffer.from(f.buffer, f.byteOffset, f.length).copy(out, o); o += f.length
|
|
34
|
+
}
|
|
35
|
+
return out
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One buffer back to its frames. Throws on anything malformed, so nothing half-read is delivered. */
|
|
39
|
+
export function unpackFrames(buf) {
|
|
40
|
+
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf)
|
|
41
|
+
const out = []
|
|
42
|
+
let o = 0
|
|
43
|
+
while (o < b.length) {
|
|
44
|
+
if (o + 4 > b.length) throw new Error('truncated frame length')
|
|
45
|
+
const n = b.readUInt32BE(o); o += 4
|
|
46
|
+
if (o + n > b.length) throw new Error('truncated frame')
|
|
47
|
+
out.push(b.subarray(o, o + n)); o += n
|
|
48
|
+
}
|
|
49
|
+
return out
|
|
50
|
+
}
|
package/hive-relay-health.js
CHANGED
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
// the only place the wording lives, and tests/relay-diagnosis-test.mjs COUNTS the producers so a
|
|
36
36
|
// third copy fails a test rather than shipping.
|
|
37
37
|
|
|
38
|
+
import { relayFetch } from './hive-transport.js'
|
|
39
|
+
|
|
38
40
|
/** A free-tier per-room cap. At or below this, Q431 is the likely refusal and seats are the fix. */
|
|
39
41
|
export const FREE_TIER_ROOM_CAP = 8
|
|
40
42
|
|
|
@@ -67,7 +69,9 @@ export function healthUrlFor(relay) {
|
|
|
67
69
|
export async function probeRelayHealth(relay, { timeoutMs = 2500, fetchImpl } = {}) {
|
|
68
70
|
const url = healthUrlFor(relay)
|
|
69
71
|
if (!url) return { ok: false, url: null, error: 'no usable relay URL' }
|
|
70
|
-
|
|
72
|
+
// relayFetch, not the built-in fetch: behind a proxy the built-in one cannot reach the relay at all,
|
|
73
|
+
// and this diagnosis would then blame the relay for what is this machine's network.
|
|
74
|
+
const f = fetchImpl || relayFetch
|
|
71
75
|
if (!f) return { ok: false, url, error: 'no fetch available in this runtime' }
|
|
72
76
|
try {
|
|
73
77
|
const ac = new AbortController()
|
package/hive-room-key.js
CHANGED
|
@@ -22,6 +22,7 @@ import path from 'path'
|
|
|
22
22
|
import crypto from 'crypto'
|
|
23
23
|
import { sign, decodeUnsafe, keyFingerprint, roomFingerprint, isSecuredRoom, makeSecuredRoomId } from './token.js'
|
|
24
24
|
import { validateMemberName } from './hive-mailbox.js'
|
|
25
|
+
import { relayFetch } from './hive-transport.js' // through the proxy, as every call to the relay
|
|
25
26
|
|
|
26
27
|
export const keysDir = () => process.env.HIVE_KEYS_DIR || path.join(os.homedir(), '.hivecode', 'keys')
|
|
27
28
|
const keyFile = (room) => path.join(keysDir(), `${room}.json`)
|
|
@@ -143,7 +144,7 @@ export async function revokeInvite({ room, relay, name }) {
|
|
|
143
144
|
const failures = []
|
|
144
145
|
for (const i of live) {
|
|
145
146
|
try {
|
|
146
|
-
const r = await
|
|
147
|
+
const r = await relayFetch(`${httpBase(relay || rec.relay)}/__hive/revoke`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ room, token: owner, jti: i.jti }) })
|
|
147
148
|
const body = await r.json().catch(() => ({}))
|
|
148
149
|
if (r.ok && body.ok) { i.revokedAt = Date.now(); done++; dropped += Number(body.dropped) || 0 }
|
|
149
150
|
else failures.push(`HTTP ${r.status}${body.error ? ` ${body.error}` : ''}`)
|
package/hive-setup.js
CHANGED
|
@@ -40,6 +40,7 @@ import { VERSION } from './hive-version.js'
|
|
|
40
40
|
import { currentRelayFor, controlUrl, PUBLIC_DEFAULT_RELAY } from './hive-privacy.js'
|
|
41
41
|
import { validateMemberName } from './hive-mailbox.js'
|
|
42
42
|
import { installStopHook, installAntigravityStopHook } from './hive-hook-install.js'
|
|
43
|
+
import { relayFetch } from './hive-transport.js' // /health through the proxy, as every call to the relay
|
|
43
44
|
|
|
44
45
|
export const SERVER_KEY = 'hivecode'
|
|
45
46
|
const BACKUP_SUFFIX = '.before-hivecode'
|
|
@@ -340,11 +341,12 @@ function defaultName() {
|
|
|
340
341
|
return v.ok ? cand : v.suggested
|
|
341
342
|
}
|
|
342
343
|
|
|
343
|
-
|
|
344
|
+
/** Exported for tests/proxy-fetch-test.mjs. */
|
|
345
|
+
export async function wakeRelay(relay) {
|
|
344
346
|
const url = relay.replace(/^wss:/, 'https:').replace(/^ws:/, 'http:').replace(/\/+$/, '') + '/health'
|
|
345
347
|
const t0 = Date.now()
|
|
346
348
|
try {
|
|
347
|
-
const r = await
|
|
349
|
+
const r = await relayFetch(url, { signal: AbortSignal.timeout(75000) })
|
|
348
350
|
return { ok: r.ok, ms: Date.now() - t0, status: r.status }
|
|
349
351
|
} catch (e) { return { ok: false, ms: Date.now() - t0, error: e.name === 'TimeoutError' ? 'no answer in 75s' : e.message } }
|
|
350
352
|
}
|
package/hive-slack-agent.js
CHANGED
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
|
|
27
27
|
import WebSocket from 'ws'
|
|
28
28
|
import { startSync, parseLink } from './sync.js'
|
|
29
|
+
import { realpathSync as __hiveRealpath } from 'fs'
|
|
30
|
+
import { resolve as __hiveResolve } from 'path'
|
|
31
|
+
import { fileURLToPath as __hiveFileURLToPath } from 'url'
|
|
29
32
|
import {
|
|
30
33
|
makeQueue,
|
|
31
34
|
slackPost,
|
|
@@ -39,6 +42,7 @@ import {
|
|
|
39
42
|
preflightSocketMode,
|
|
40
43
|
connectSocketMode,
|
|
41
44
|
parseSlackAllow,
|
|
45
|
+
notFromAPerson,
|
|
42
46
|
} from './hive-slack.js'
|
|
43
47
|
|
|
44
48
|
export { stopAll, resumeAll, resolveName, route }
|
|
@@ -219,7 +223,7 @@ export function makeAgent({
|
|
|
219
223
|
/** The one entry point for Slack events. Only a message in the configured channel or in a DM, or
|
|
220
224
|
* the native stop button, does anything; everything else is ignored before any actor lookup. */
|
|
221
225
|
async function onEvent(ev) {
|
|
222
|
-
if (
|
|
226
|
+
if (notFromAPerson(ev) || (botUserId && ev.user === botUserId)) return { ignored: 'bot_or_subtype' }
|
|
223
227
|
const targetChannel = ev.channel || channel
|
|
224
228
|
const isDm = ev.channel_type === 'im' || (typeof targetChannel === 'string' && targetChannel.startsWith('D'))
|
|
225
229
|
const here = targetChannel === channel || isDm
|
|
@@ -391,6 +395,14 @@ export async function main(argv = process.argv.slice(2), {
|
|
|
391
395
|
})
|
|
392
396
|
}
|
|
393
397
|
|
|
394
|
-
|
|
398
|
+
// Run directly (node <file>, or through an npx / node_modules/.bin symlink)? Compared by real path:
|
|
399
|
+
// the old `file:///${argv[1]}` test built "file:////..." on macOS and Linux, never matched, and the
|
|
400
|
+
// process exited 0 having done nothing.
|
|
401
|
+
function __hiveIsEntry() {
|
|
402
|
+
if (!process.argv[1]) return false
|
|
403
|
+
const me = __hiveFileURLToPath(import.meta.url)
|
|
404
|
+
try { return __hiveRealpath(process.argv[1]) === __hiveRealpath(me) } catch { return __hiveResolve(process.argv[1]) === __hiveResolve(me) }
|
|
405
|
+
}
|
|
406
|
+
if (__hiveIsEntry()) {
|
|
395
407
|
main().then((code) => { if (code !== 0) process.exit(code) })
|
|
396
408
|
}
|