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-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
+ }
@@ -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
- const nm = bareName(node); if (nm) s.add(nm)
132
- // Import locals count as declared names too — so if an import is ever dropped and its
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
- maxConnsTotal: num('HIVE_MAX_CONNS_TOTAL', 512),
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.11.6",
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:extension": "node tests/extension-parity-test.mjs",
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-websocket": "^2.0.4",
127
- "yjs": "^13.6.18",
128
- "zod": "^4.4.3"
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"