hivecode-mcp 0.11.6 → 0.12.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 +127 -127
- package/README.md +119 -69
- package/core.js +14 -146
- package/docs/MCP.md +19 -11
- package/docs/SELF_HOSTING.md +256 -0
- package/hive-activity.js +310 -0
- package/hive-mcp.js +447 -76
- package/hive-mux-proto.js +58 -0
- package/hive-mux.js +350 -0
- package/hive-pr-staleness.js +189 -0
- package/hive-privacy.js +288 -0
- package/hive-relay-health.js +225 -0
- package/hive-setup.js +445 -0
- package/hive-stdio.js +168 -0
- package/hive-update.js +58 -0
- package/hive-version.js +59 -0
- package/lang-js.js +11 -2
- package/limits.js +49 -1
- package/package.json +29 -7
- package/reconcile.cjs +477 -0
- package/sync.js +613 -43
package/hive-update.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// hive-update.js -- tells an agent, and through it its human, that a newer hivecode is out.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS. `npx -y hivecode-mcp@latest setup` points agent apps at a runtime COPY
|
|
4
|
+
// (~/.hivecode/runtime/<version>), which is what made starting fast, offline and free of npx.
|
|
5
|
+
// It also means nothing updates any more: every start used to fetch @latest, and now an agent
|
|
6
|
+
// stays on the version it was set up with until someone runs setup again. A room with mixed
|
|
7
|
+
// builds is the one condition hive_join itself warns can lose data, so the gap must be visible.
|
|
8
|
+
//
|
|
9
|
+
// IT NEVER GETS IN THE WAY. The check starts with the server, carries a short timeout, and
|
|
10
|
+
// hive_join and hive_status use whatever answer has arrived by then. A failed check says
|
|
11
|
+
// nothing -- no network is not news. HIVE_PRIVATE=1 and HIVE_NO_UPDATE_CHECK=1 both switch it
|
|
12
|
+
// off: a user who asked for privacy did not ask for a request to npm on every start.
|
|
13
|
+
import { VERSION } from './hive-version.js'
|
|
14
|
+
|
|
15
|
+
const REGISTRY_LATEST = 'https://registry.npmjs.org/hivecode-mcp/latest'
|
|
16
|
+
|
|
17
|
+
/** True when version `a` is newer than `b` (x.y.z; anything unparseable is never "newer"). */
|
|
18
|
+
export function newer(a, b) {
|
|
19
|
+
const parse = (v) => String(v).split('-')[0].split('.').map((n) => Number(n))
|
|
20
|
+
const pa = parse(a)
|
|
21
|
+
const pb = parse(b)
|
|
22
|
+
for (let i = 0; i < 3; i++) {
|
|
23
|
+
const x = pa[i] ?? 0
|
|
24
|
+
const y = pb[i] ?? 0
|
|
25
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return false
|
|
26
|
+
if (x !== y) return x > y
|
|
27
|
+
}
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let latest = null
|
|
32
|
+
let started = false
|
|
33
|
+
|
|
34
|
+
export function startUpdateCheck({ fetchImpl = globalThis.fetch, timeoutMs = 4000, env = process.env } = {}) {
|
|
35
|
+
if (started) return
|
|
36
|
+
started = true
|
|
37
|
+
if (env.HIVE_NO_UPDATE_CHECK === '1' || env.HIVE_PRIVATE === '1' || typeof fetchImpl !== 'function') return
|
|
38
|
+
Promise.resolve()
|
|
39
|
+
// HIVE_UPDATE_URL exists for tests/update-notice-test.mjs, which serves a fake registry so the
|
|
40
|
+
// wiring is proven end to end through a real server process.
|
|
41
|
+
.then(() => fetchImpl(env.HIVE_UPDATE_URL || REGISTRY_LATEST, { signal: AbortSignal.timeout(timeoutMs), headers: { accept: 'application/json' } }))
|
|
42
|
+
.then((r) => (r && r.ok ? r.json() : null))
|
|
43
|
+
.then((j) => { if (j && typeof j.version === 'string') latest = j.version })
|
|
44
|
+
.catch(() => { /* offline, blocked, or slow: say nothing */ })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The line to show, or '' when this agent is current (or nobody knows yet). */
|
|
48
|
+
export function updateNotice(current = VERSION, known = latest) {
|
|
49
|
+
if (!known || !newer(known, current)) return ''
|
|
50
|
+
return `UPDATE AVAILABLE: hivecode ${known} is out; this agent runs ${current}. Tell your human to run ` +
|
|
51
|
+
'`npx -y hivecode-mcp@latest setup` and reload this agent. Mixed versions in one room can lose data.'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const _test = {
|
|
55
|
+
reset() { latest = null; started = false },
|
|
56
|
+
setLatest(v) { latest = v; started = true },
|
|
57
|
+
latest: () => latest,
|
|
58
|
+
}
|
package/hive-version.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// hive-version.js — one place that knows what build this is.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS FILE EXISTS, and it is not tidiness.
|
|
4
|
+
//
|
|
5
|
+
// VERSION is load-bearing, not cosmetic. hive_join instructs every arriving agent to announce its
|
|
6
|
+
// version in chat, because two clients running different builds in one room corrupt each other's
|
|
7
|
+
// files with no error on either side — both writes succeed, both are green, and the disagreement
|
|
8
|
+
// surfaces only as lost bytes. That announcement is the ONLY channel that makes a mixed build
|
|
9
|
+
// visible from inside the room. So a version string is not a label; it is the input to the one
|
|
10
|
+
// detector we have.
|
|
11
|
+
//
|
|
12
|
+
// On 20 Sep 2026 three entry points were found reporting a version package.json does not contain:
|
|
13
|
+
// remote-mcp.js said '0.11.5' while live on the hosted relay, hive-mcp-http.js and hive-linear.js
|
|
14
|
+
// both said '1.0.0'. hive-mcp.js had already been fixed months earlier, with a comment explaining
|
|
15
|
+
// exactly why hardcoding it is dangerous — and that fix was never carried to the other three.
|
|
16
|
+
// A detector that lies is strictly worse than no detector, because it is trusted.
|
|
17
|
+
//
|
|
18
|
+
// HIV-P7 closed that by giving each of the four files its own runtime read. Which produced the
|
|
19
|
+
// next defect, and it was measured rather than predicted: with the reader duplicated four times,
|
|
20
|
+
// corrupting ONE copy's fallback string to '9.9.9-DRIFTED' left tests/version-truth-test.mjs at
|
|
21
|
+
// 16 passed, 0 failed. The property guards the version that gets REPORTED and says nothing about
|
|
22
|
+
// the mechanism that produces it, so three of four copies could rot in silence.
|
|
23
|
+
//
|
|
24
|
+
// This codebase has watched that happen before. The extension's reconcile() and the engine's were
|
|
25
|
+
// the same function once; they now share 34 lines and behave differently. Eight lines is not too
|
|
26
|
+
// small to drift — it is exactly small enough that nobody feels the need to check.
|
|
27
|
+
//
|
|
28
|
+
// So the fix is DELETION, not another assertion. One definition, imported by all four. The
|
|
29
|
+
// property becomes true by construction instead of by policing, and a fifth entry point written
|
|
30
|
+
// next year gets it right by importing rather than by remembering.
|
|
31
|
+
//
|
|
32
|
+
// TWO DECISIONS WORTH KEEPING:
|
|
33
|
+
//
|
|
34
|
+
// It READS package.json rather than being told. package.json always ships beside these files —
|
|
35
|
+
// in the repo and in the published tarball — so `new URL('./package.json', import.meta.url)`
|
|
36
|
+
// resolves in both. Anything else (a constant, a build step, a generated file) can disagree with
|
|
37
|
+
// the package the moment `npm version` runs, which is precisely the failure being fixed.
|
|
38
|
+
//
|
|
39
|
+
// IT NEVER THROWS. A version read is not worth a server that will not boot, and a server that
|
|
40
|
+
// refuses to start is a worse outcome than one that says it does not know which build it is. So
|
|
41
|
+
// an unreadable or malformed package.json yields '0.0.0-unknown' — a value that is obviously
|
|
42
|
+
// wrong to a human, will never match a real release, and cannot be mistaken for a true answer.
|
|
43
|
+
// Silence or an empty string could be read as "no mixed build here"; this cannot.
|
|
44
|
+
|
|
45
|
+
import fs from 'fs'
|
|
46
|
+
|
|
47
|
+
/** Recomputed never: a running process cannot change which build it is. */
|
|
48
|
+
export const VERSION = (() => {
|
|
49
|
+
try {
|
|
50
|
+
const v = JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version
|
|
51
|
+
if (typeof v === 'string' && v) return v
|
|
52
|
+
} catch { /* fall through — see IT NEVER THROWS above */ }
|
|
53
|
+
return '0.0.0-unknown'
|
|
54
|
+
})()
|
|
55
|
+
|
|
56
|
+
/** The sentinel, exported so callers and tests can name it instead of retyping the literal. */
|
|
57
|
+
export const UNKNOWN_VERSION = '0.0.0-unknown'
|
|
58
|
+
|
|
59
|
+
export default VERSION
|
package/lang-js.js
CHANGED
|
@@ -128,11 +128,20 @@ function bareName(node) {
|
|
|
128
128
|
function declaredNames(src) {
|
|
129
129
|
const s = new Set()
|
|
130
130
|
for (const node of parse(src).body) {
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
let n = node
|
|
132
|
+
if (n.type === 'ExportNamedDeclaration' && n.declaration) n = n.declaration
|
|
133
|
+
if (n.type === 'VariableDeclaration') {
|
|
134
|
+
for (const d of n.declarations) bindingNames(d.id, s)
|
|
135
|
+
} else {
|
|
136
|
+
const nm = bareName(node); if (nm) s.add(nm)
|
|
137
|
+
}
|
|
138
|
+
// Import locals count as declared names too — so if an import is ever dropped and its
|
|
133
139
|
// binding is still used, the dangling-reference check catches it (safety net for the
|
|
134
140
|
// import merge). Keeps the never-emit-broken-code guarantee honest for imports.
|
|
135
141
|
if (node.type === 'ImportDeclaration') for (const sp of node.specifiers) s.add(sp.local.name)
|
|
142
|
+
if (node.type === 'ExportNamedDeclaration' && !node.declaration && node.specifiers) {
|
|
143
|
+
for (const sp of node.specifiers) s.add(sp.exported.name || sp.exported.value || sp.local.name)
|
|
144
|
+
}
|
|
136
145
|
}
|
|
137
146
|
return s
|
|
138
147
|
}
|
package/limits.js
CHANGED
|
@@ -127,7 +127,27 @@ export const LIMITS = {
|
|
|
127
127
|
// Retained CRDT size for one doc. A doc is one file in this design.
|
|
128
128
|
maxDocBytes: num('HIVE_MAX_DOC_BYTES', 64 * 1024 * 1024),
|
|
129
129
|
maxConnsPerRoom: num('HIVE_MAX_CONNS_PER_ROOM', 64),
|
|
130
|
-
|
|
130
|
+
// The only cap that spans a whole project: a client holds one socket per open file (up to
|
|
131
|
+
// HIVE_MAX_OPEN_FILES=128) plus its base room, and each file is its own room as far as the
|
|
132
|
+
// per-room ceiling is concerned -- so a real team is bounded by THIS number and nothing else.
|
|
133
|
+
// It was 512: three participants on a 150-file room measured 409 connections, so a fourth
|
|
134
|
+
// was refused, on a relay already carrying other rooms. Measured cost 25 Sep 2026: ~56 KB of
|
|
135
|
+
// relay memory per connection (400 extra sockets to the same 400 docs: +22 MB), so 2048 is
|
|
136
|
+
// ~115 MB -- comfortable on a 512 MB free instance.
|
|
137
|
+
maxConnsTotal: num('HIVE_MAX_CONNS_TOTAL', 2048),
|
|
138
|
+
// ONE SOCKET PER PARTICIPANT (hive-mux-proto.js). A mux socket counts ONCE against
|
|
139
|
+
// maxConnsTotal above; each file it opens is a CHANNEL, counted against the file's per-room
|
|
140
|
+
// ceiling exactly as a per-file socket is, and against these two caps instead of the socket
|
|
141
|
+
// total -- which is the whole point: a participant stops costing 129 sockets.
|
|
142
|
+
//
|
|
143
|
+
// Sized by MEMORY, measured 25 Sep 2026: ~25.8 KB per channel whose doc no one else holds (the
|
|
144
|
+
// doc is most of it), ~2.4 KB for each further participant on the same doc. The socket cap
|
|
145
|
+
// used to bound open docs as a side effect; this is what bounds them now. 8192 lone small docs
|
|
146
|
+
// is ~211 MB on the 512 MB hosted instance -- raise it with the instance, not before. The per-
|
|
147
|
+
// participant cap matches the client's own bound in one-socket mode, so no single participant
|
|
148
|
+
// can take more than half the relay.
|
|
149
|
+
maxChannelsTotal: num('HIVE_MAX_CHANNELS_TOTAL', 8192),
|
|
150
|
+
maxChannelsPerSocket: num('HIVE_MUX_MAX_DOCS', 4096),
|
|
131
151
|
// How long a client stays refused after tripping a rate limit. Long enough that a tight
|
|
132
152
|
// loop actually yields, short enough that a human does not notice a one-off burst.
|
|
133
153
|
cooloffMs: num('HIVE_RATE_COOLOFF_MS', 5000),
|
|
@@ -183,6 +203,32 @@ export function noteConnectionOpened(room) {
|
|
|
183
203
|
roomConns.set(room, (roomConns.get(room) || 0) + 1)
|
|
184
204
|
}
|
|
185
205
|
|
|
206
|
+
// Channels share roomConns with sockets on purpose: the per-room ceiling means "how many
|
|
207
|
+
// participants hold this file", and it must mean that whichever transport each one uses.
|
|
208
|
+
let totalChannels = 0
|
|
209
|
+
export function admitChannel(room, tier = null) {
|
|
210
|
+
if (totalChannels >= LIMITS.maxChannelsTotal) {
|
|
211
|
+
return { code: 503, reason: `Q438 relay at capacity (${LIMITS.maxChannelsTotal} open files across all participants)` }
|
|
212
|
+
}
|
|
213
|
+
const cap = (tier && tier.maxConnsPerRoom) || LIMITS.maxConnsPerRoom
|
|
214
|
+
const n = roomConns.get(room) || 0
|
|
215
|
+
if (n >= cap) {
|
|
216
|
+
const which = tier ? `${tier.tier} tier` : 'this relay'
|
|
217
|
+
return { code: 429, reason: `Q431 room at capacity (${cap} connections on ${which})` }
|
|
218
|
+
}
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
export function noteChannelOpened(room) {
|
|
222
|
+
totalChannels++
|
|
223
|
+
roomConns.set(room, (roomConns.get(room) || 0) + 1)
|
|
224
|
+
}
|
|
225
|
+
export function noteChannelClosed(room) {
|
|
226
|
+
totalChannels = Math.max(0, totalChannels - 1)
|
|
227
|
+
const n = (roomConns.get(room) || 1) - 1
|
|
228
|
+
if (n <= 0) roomConns.delete(room)
|
|
229
|
+
else roomConns.set(room, n)
|
|
230
|
+
}
|
|
231
|
+
|
|
186
232
|
export function noteConnectionClosed(room) {
|
|
187
233
|
totalConns = Math.max(0, totalConns - 1)
|
|
188
234
|
const n = (roomConns.get(room) || 1) - 1
|
|
@@ -330,6 +376,7 @@ export function usageFor(room = null) {
|
|
|
330
376
|
export function stats() {
|
|
331
377
|
return {
|
|
332
378
|
connections: totalConns,
|
|
379
|
+
channels: totalChannels,
|
|
333
380
|
rooms: roomConns.size,
|
|
334
381
|
largestDocBytes: docBytes.size ? Math.max(...docBytes.values()) : 0,
|
|
335
382
|
docsTracked: docBytes.size,
|
|
@@ -340,6 +387,7 @@ export function stats() {
|
|
|
340
387
|
/** Test-only: reset module state between cases. */
|
|
341
388
|
export function _reset() {
|
|
342
389
|
totalConns = 0
|
|
390
|
+
totalChannels = 0
|
|
343
391
|
roomConns.clear()
|
|
344
392
|
docBytes.clear()
|
|
345
393
|
usage.clear()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hivecode-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "MCP server that lets any AI coding agent join a Hivecode room — humans and multiple AI agents edit one live codebase with folder-scoped permissions and approval gates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,11 +28,23 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"hive-mcp.js",
|
|
31
|
+
"hive-mux.js",
|
|
32
|
+
"hive-mux-proto.js",
|
|
33
|
+
"hive-version.js",
|
|
34
|
+
"hive-pr-staleness.js",
|
|
31
35
|
"hive-mailbox.js",
|
|
32
36
|
"hive-obligations.js",
|
|
33
37
|
"hive-receipts.js",
|
|
34
38
|
"hive-throttle.js",
|
|
35
39
|
"hive-instructions.js",
|
|
40
|
+
"hive-activity.js",
|
|
41
|
+
"hive-relay-health.js",
|
|
42
|
+
"hive-privacy.js",
|
|
43
|
+
"hive-setup.js",
|
|
44
|
+
"hive-stdio.js",
|
|
45
|
+
"hive-update.js",
|
|
46
|
+
"docs/SELF_HOSTING.md",
|
|
47
|
+
"reconcile.cjs",
|
|
36
48
|
"hive-busy.js",
|
|
37
49
|
"hive-stop-hook.js",
|
|
38
50
|
"hive-hook-install.js",
|
|
@@ -72,8 +84,17 @@
|
|
|
72
84
|
"test:features": "node tests/features-test.mjs",
|
|
73
85
|
"test:features:shape": "node tests/features-test.mjs --shape",
|
|
74
86
|
"test:learnings": "node tests/learnings-test.mjs",
|
|
75
|
-
"test:
|
|
87
|
+
"test:docs": "node tests/readme-promises-test.mjs",
|
|
88
|
+
"test:setup": "node tests/setup-test.mjs --package",
|
|
89
|
+
"test:mcp": "node tests/mcp-conformance-test.mjs",
|
|
90
|
+
"test:recreate": "node tests/blind-recreate-test.mjs",
|
|
91
|
+
"test:liveness": "node tests/agent-liveness-test.mjs && node tests/unreachable-recipient-test.mjs",
|
|
92
|
+
"build:ext": "node scripts/build-icr-cjs.cjs && node scripts/build-privacy-cjs.cjs",
|
|
93
|
+
"test:selfhost": "node tests/relay-pin-test.mjs && node tests/self-host-test.mjs && node tests/extension-privacy-parity-test.mjs",
|
|
94
|
+
"test:hardening": "node tests/genesis-claim-test.mjs && node tests/bare-root-write-test.mjs && node tests/version-truth-test.mjs && node tests/room-repo-drift-test.mjs && node tests/first-contact-refusal-test.mjs && node tests/open-file-cap-test.mjs && node tests/learnings-test.mjs && node tests/agent-liveness-test.mjs && node tests/unreachable-recipient-test.mjs && node tests/join-honesty-test.mjs && node tests/relay-diagnosis-test.mjs && node tests/blind-recreate-test.mjs && node tests/genesis-collision-test.mjs && node tests/relay-pin-test.mjs && node tests/self-host-test.mjs && node tests/extension-privacy-parity-test.mjs && node tests/readme-promises-test.mjs && node tests/setup-test.mjs && node tests/mcp-conformance-test.mjs && node tests/update-notice-test.mjs && node tests/mux-test.mjs",
|
|
95
|
+
"test:extension": "node tests/extension-parity-test.mjs && node tests/extension-privacy-parity-test.mjs",
|
|
76
96
|
"test:ceiling": "node tests/refused-visible-test.mjs && node tests/ceiling-no-data-loss-test.mjs",
|
|
97
|
+
"test:mux": "node tests/mux-test.mjs",
|
|
77
98
|
"test:identity": "node tests/identity-audit-test.mjs",
|
|
78
99
|
"test:billing": "node tests/billing-test.mjs",
|
|
79
100
|
"test:license": "node tests/hive-license-test.js",
|
|
@@ -117,17 +138,18 @@
|
|
|
117
138
|
"node": ">=18"
|
|
118
139
|
},
|
|
119
140
|
"dependencies": {
|
|
120
|
-
"@anthropic-ai/sdk": "^0.104.2",
|
|
121
|
-
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
122
141
|
"acorn": "^8.17.0",
|
|
123
142
|
"ignore": "^7.0.5",
|
|
124
143
|
"lib0": "^0.2.117",
|
|
144
|
+
"lodash.debounce": "^4.0.8",
|
|
125
145
|
"ws": "^8.18.0",
|
|
126
|
-
"y-
|
|
127
|
-
"
|
|
128
|
-
"
|
|
146
|
+
"y-protocols": "^1.0.6",
|
|
147
|
+
"y-websocket": "^3.1.0",
|
|
148
|
+
"yjs": "^13.6.18"
|
|
129
149
|
},
|
|
130
150
|
"devDependencies": {
|
|
151
|
+
"@anthropic-ai/sdk": "^0.104.2",
|
|
152
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
131
153
|
"@types/node": "^26.1.2",
|
|
132
154
|
"esbuild": "^0.28.1",
|
|
133
155
|
"typescript": "^7.0.2"
|