hivecode-mcp 0.14.2 → 0.14.4

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-transport.js CHANGED
@@ -24,7 +24,8 @@
24
24
  // set when node started, so the WebSocket died with ENOTFOUND, the long-poll probe died the same
25
25
  // way, and hive_join said "the relay refused the connection" about a relay it never reached. Now:
26
26
  // - proxyFor(url) picks the proxy: HIVE_PROXY (a URL, or off), else HTTPS_PROXY for wss:/https:
27
- // and HTTP_PROXY for ws:/http:. NO_PROXY is honoured; loopback never goes through a proxy.
27
+ // and HTTP_PROXY for ws:/http:. NO_PROXY is honoured; loopback never goes through a proxy,
28
+ // including where node's env-proxy mode would send it there (directFetch).
28
29
  // - Both pipes ride an HTTP CONNECT tunnel through it: the WebSocket (an agent for `ws`) and the
29
30
  // long-poll requests (fetchFor). Through a tunnel to a wss:// relay the proxy sees only
30
31
  // "CONNECT host:443"; the path and query -- where the room token rides -- stay inside TLS.
@@ -184,13 +185,46 @@ function tunnelAgent(proxy, secure, kind = 'fetch') {
184
185
 
185
186
  /** A fetch-shaped call carried over the tunnel agent: just what offersPoll and PollSocket use. */
186
187
  function proxyFetch(proxy, url, init = {}) {
188
+ return agentFetch(url, init, (secure) => tunnelAgent(proxy, secure))
189
+ }
190
+
191
+ // DIRECT MEANS DIRECT, EVEN WHERE NODE APPLIES THE ENV PROXY ITSELF. With env-proxy mode on
192
+ // (NODE_USE_ENV_PROXY=1 or --use-env-proxy, what Antigravity's VMs run), the built-in fetch AND http's
193
+ // global agent send every request through http(s)_proxy unless node's own NO_PROXY exempts it. So a
194
+ // call proxyFor() had decided was direct -- loopback, a NO_PROXY match, HIVE_PROXY=off -- went to the
195
+ // proxy anyway: a self-hosted relay on localhost was "unreachable" for an invite revoke or the /health
196
+ // probe while its WebSockets worked (antigravity-VM, 9 Nov 2026; tests/loopback-direct-fetch-test.mjs).
197
+ // Measured on node 22.23 and 24.22: an agent of our own is never proxied. Used only in env-proxy mode,
198
+ // so everywhere else a direct call is still the built-in fetch, exactly as before.
199
+ // Node fixes the mode ONCE, AT STARTUP: clearing NODE_USE_ENV_PROXY later does not turn it off (a
200
+ // first version read it per call, and proxy-fetch-test, which clears it mid-run, still went to the
201
+ // proxy). So the mode is read when this module loads -- the nearest point to node's own decision --
202
+ // and a per-call env can only add it, never take it away.
203
+ const envProxyIn = (env) => {
204
+ if (/^(1|true|yes|on)$/i.test(String((env && env.NODE_USE_ENV_PROXY) || '').trim())) return true
205
+ return /(^|\s)--use-env-proxy(\s|=|$)/.test(String((env && env.NODE_OPTIONS) || ''))
206
+ }
207
+ const ENV_PROXY_AT_START = envProxyIn(process.env) || process.execArgv.includes('--use-env-proxy')
208
+ const envProxyMode = (env = process.env) => ENV_PROXY_AT_START || envProxyIn(env)
209
+ const directAgents = new Map()
210
+ function directAgent(secure) {
211
+ let a = directAgents.get(secure)
212
+ if (!a) { a = new (secure ? https.Agent : http.Agent)({ keepAlive: true, maxFreeSockets: 4 }); directAgents.set(secure, a) }
213
+ return a
214
+ }
215
+ function directFetch(url, init = {}) {
216
+ return agentFetch(url, init, directAgent)
217
+ }
218
+
219
+ /** The request behind proxyFetch and directFetch: `agentFor(secure)` picks the agent that carries it. */
220
+ function agentFetch(url, init, agentFor) {
187
221
  return new Promise((resolve, reject) => {
188
222
  const u = new URL(url)
189
223
  const secure = u.protocol === 'https:'
190
224
  const body = init.body == null ? null : Buffer.from(init.body)
191
225
  const headers = { ...(init.headers || {}) }
192
226
  if (body) headers['content-length'] = String(body.length)
193
- const req = (secure ? https : http).request(u, { method: init.method || 'GET', headers, agent: tunnelAgent(proxy, secure) }, (res) => {
227
+ const req = (secure ? https : http).request(u, { method: init.method || 'GET', headers, agent: agentFor(secure) }, (res) => {
194
228
  const chunks = []
195
229
  res.on('data', (c) => chunks.push(c))
196
230
  res.on('error', reject)
@@ -218,10 +252,11 @@ function proxyFetch(proxy, url, init = {}) {
218
252
  })
219
253
  }
220
254
 
221
- /** The fetch to reach `url` with: through the proxy tunnel when one applies, else the built-in one. */
255
+ /** The fetch to reach `url` with: through the proxy tunnel when one applies, else direct (see directFetch). */
222
256
  export function fetchFor(url, env = process.env) {
223
257
  const p = proxyFor(url, env)
224
- return p ? (u, init) => proxyFetch(p, u, init) : globalThis.fetch
258
+ if (p) return (u, init) => proxyFetch(p, u, init)
259
+ return envProxyMode(env) ? directFetch : globalThis.fetch
225
260
  }
226
261
 
227
262
  /**
@@ -373,6 +408,7 @@ export class PollSocket extends SocketShape {
373
408
  _lost(reason, code = 1006) {
374
409
  if (this.readyState === CLOSED) return
375
410
  this.readyState = CLOSED
411
+ clearTimeout(this._closeTimer)
376
412
  if (this._sid) this._req('close', undefined, {}, 5000).catch(() => { })
377
413
  fire(this, 'close', { type: 'close', code, reason, wasClean: false, target: this })
378
414
  }
@@ -392,7 +428,7 @@ export class PollSocket extends SocketShape {
392
428
  // A proxy that cuts long requests shows up as a failure after a long wait: hold shorter.
393
429
  if (startedAt && now - startedAt > 5000 && this._hold > 4000) this._hold = Math.max(4000, Math.floor(this._hold / 2))
394
430
  await sleep(Math.min(2000, 250 * 2 ** Math.min(3, Math.floor((now - this._troubleSince) / 5000))))
395
- return this.readyState === OPEN
431
+ return this.readyState === OPEN || this.readyState === CLOSING
396
432
  }
397
433
  _ok() { this._troubleSince = 0 }
398
434
  async _recvLoop() {
@@ -429,17 +465,18 @@ export class PollSocket extends SocketShape {
429
465
  if (!this._flushQueued) { this._flushQueued = true; setImmediate(() => { this._flushQueued = false; this._flush() }) }
430
466
  }
431
467
  async _flush() {
432
- if (this._sending || !this._outbox.length || this.readyState !== OPEN) return
468
+ const live = () => this.readyState === OPEN || this.readyState === CLOSING
469
+ if (this._sending || !this._outbox.length || !live()) { this._closeIfDrained(); return }
433
470
  this._sending = true
434
471
  try {
435
- while (this._outbox.length && this.readyState === OPEN) {
472
+ while (this._outbox.length && live()) {
436
473
  let n = 0, bytes = 0
437
474
  while (n < this._outbox.length && n < 512 && (n === 0 || bytes + this._outbox[n].length <= 4 * 1048576)) { bytes += this._outbox[n].length; n++ }
438
475
  const batch = this._outbox.slice(0, n)
439
476
  const first = this._outSeq
440
477
  const body = packFrames(batch)
441
478
  for (;;) {
442
- if (this.readyState !== OPEN) return
479
+ if (!live()) return
443
480
  const started = Date.now()
444
481
  let r
445
482
  try { r = await this._req('send', body, { 'content-type': 'application/octet-stream', 'x-hive-seq': String(first) }, 60000) }
@@ -455,17 +492,54 @@ export class PollSocket extends SocketShape {
455
492
  this._outbox.splice(0, n)
456
493
  this._outSeq = first + n
457
494
  }
458
- } finally { this._sending = false }
495
+ } finally { this._sending = false; this._closeIfDrained() }
459
496
  }
497
+ // CLOSE DELIVERS WHAT WAS SENT FIRST, as a WebSocket's does (0.14.3 P0-1). A WebSocket hands its
498
+ // bytes to the OS on send(), so ws.close() still delivers them; this socket keeps them in _outbox
499
+ // until the relay accepts them, and close() used to drop that array: an edit made just before the
500
+ // agent left never reached the room while hive_edit had already said it was syncing (8-9 Nov mock
501
+ // run, both Antigravity VM agents). Now close() keeps sending for up to CLOSE_DRAIN_MS, then ends
502
+ // the session. The pending request keeps the process alive meanwhile, so an exit waits for it.
460
503
  close(code = 1000, reason = '') {
461
504
  if (this.readyState === CLOSING || this.readyState === CLOSED) return
505
+ if (this.readyState === OPEN && (this._outbox.length || this._sending)) {
506
+ this.readyState = CLOSING
507
+ this._closeWith = { code, reason }
508
+ this._closeTimer = setTimeout(() => this._endClose(false), PollSocket.CLOSE_DRAIN_MS)
509
+ this._flush()
510
+ return
511
+ }
512
+ this._closeWith = { code, reason }
513
+ this._endClose(true)
514
+ }
515
+ _closeIfDrained() {
516
+ if (this.readyState === CLOSING && !this._sending && !this._outbox.length) this._endClose(true)
517
+ }
518
+ _endClose(drained) {
519
+ if (this.readyState === CLOSED) return
520
+ clearTimeout(this._closeTimer)
462
521
  const had = !!this._sid
522
+ const { code, reason } = this._closeWith || { code: 1000, reason: '' }
463
523
  this.readyState = CLOSED
464
524
  if (had) this._req('close', undefined, {}, 5000).catch(() => { })
465
- setImmediate(() => fire(this, 'close', { type: 'close', code, reason, wasClean: true, target: this }))
525
+ setImmediate(() => fire(this, 'close', { type: 'close', code, reason, wasClean: drained, target: this }))
526
+ }
527
+ /** Resolves once nothing is waiting to reach the relay (or the socket is gone): the drain, observable. */
528
+ drained(timeoutMs = PollSocket.CLOSE_DRAIN_MS) {
529
+ const end = Date.now() + timeoutMs
530
+ return new Promise((resolve) => {
531
+ const tick = () => {
532
+ if (this.readyState === CLOSED || (!this._outbox.length && !this._sending)) return resolve(!this._outbox.length)
533
+ if (Date.now() >= end) return resolve(false)
534
+ setTimeout(tick, 25)
535
+ }
536
+ tick()
537
+ })
466
538
  }
467
- terminate() { this.close(1006, 'terminated') }
539
+ // terminate is for a dead link (the liveness check): nothing can be delivered on it, so no drain.
540
+ terminate() { this._closeWith = { code: 1006, reason: 'terminated' }; this._endClose(false) }
468
541
  }
542
+ PollSocket.CLOSE_DRAIN_MS = 10000
469
543
 
470
544
  /**
471
545
  * The socket every provider gets: a WebSocket, or long-poll where WebSockets are refused (above).
package/icr.js CHANGED
@@ -802,12 +802,17 @@ export function structuralMerge(base, a, b, opts = {}) {
802
802
  // ASYNC/AWAIT SYNCHRONIZATION (intent):
803
803
  // When a declaration was synchronous in base and became ASYNC in the merged result,
804
804
  // upgrade call sites inside async callers with `await` so the contract holds.
805
+ // ONLY a function that EXISTED in the base as sync (0.14.3 P0-5). A function new in this merge was
806
+ // written async by its author, together with its callers: nothing was "made async" under anyone.
807
+ // Counting it rewrote authored lines (`return f()` -> `return await f()`, credited to the author,
808
+ // silently), and two peers making the same insert at once stacked in the shared text: the room's
809
+ // own server.js read `return await await await admitMux(req, q)` (9 Nov).
805
810
  const asyncUpgrades = []
806
811
  if (lang.isAsyncDecl && lang.upgradeCallSitesToAwait) {
807
812
  for (const name of mergedNames) {
808
- const wasAsyncInBase = baseNames.has(name) ? lang.isAsyncDecl(base, name) : false
813
+ const wasSyncInBase = baseNames.has(name) && !lang.isAsyncDecl(base, name)
809
814
  const isAsyncInMerged = lang.isAsyncDecl(merged, name)
810
- if (!wasAsyncInBase && isAsyncInMerged) {
815
+ if (wasSyncInBase && isAsyncInMerged) {
811
816
  const upgraded = lang.upgradeCallSitesToAwait(merged, name)
812
817
  if (upgraded !== merged && lang.parses(upgraded)) {
813
818
  merged = upgraded
package/lang-js.js CHANGED
@@ -230,45 +230,63 @@ function isAsyncDecl(src, name) {
230
230
 
231
231
  // Upgrade free call sites of `fnName` to `await fnName(...)` if they are inside an async parent
232
232
  // and not already awaited.
233
+ //
234
+ // Left alone (0.14.3 P0-5): a call that is already awaited (`await f()`, `await (f())`); a call that
235
+ // IS the value an async function returns (`return f()`, `async () => f()`) -- OUTSIDE a try, where
236
+ // returning the promise resolves to the same value. Inside a try (its block or its catch) `return f()`
237
+ // is NOT the same once f is async: a rejection escapes the catch and a finally runs before f settles,
238
+ // so there it is upgraded, which is what the feature is for (Claude-cloud's review). A call that is an
239
+ // argument of an awaited call (`await g(f())`) is NOT awaited by that await and is upgraded too.
240
+ // Each position is inserted at most once.
233
241
  function upgradeCallSitesToAwait(src, fnName) {
234
242
  try {
235
243
  const ast = parse(src)
236
- const spots = []
244
+ const spots = new Set()
237
245
 
238
- function walkAST(node, parent, isAsyncScope = false) {
246
+ // awaited: this node IS the operand of an await (through parentheses or an optional chain only).
247
+ // inTry: this node sits in a try block or catch clause of the current function.
248
+ function walkAST(node, parent, isAsyncScope = false, awaited = false, inTry = false) {
239
249
  if (!node || typeof node.type !== 'string') return
240
250
 
241
251
  let childAsyncScope = isAsyncScope
252
+ let childInTry = inTry
242
253
  if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
243
254
  childAsyncScope = !!node.async
255
+ childInTry = false // a nested function has its own try scopes
244
256
  }
257
+ const passAwait = node.type === 'AwaitExpression' ? true
258
+ : (node.type === 'ParenthesizedExpression' || node.type === 'ChainExpression') ? awaited : false
245
259
 
246
260
  if (node.type === 'CallExpression') {
247
261
  let isTarget = false
248
262
  if (node.callee.type === 'Identifier' && node.callee.name === fnName) isTarget = true
249
263
  if (isTarget && childAsyncScope) {
250
- const alreadyAwaited = parent && parent.type === 'AwaitExpression'
251
- if (!alreadyAwaited) {
252
- spots.push(node.start)
253
- }
264
+ const alreadyAwaited = awaited || (parent && parent.type === 'AwaitExpression')
265
+ const returnedAsIs = !inTry && parent && (parent.type === 'ReturnStatement' ||
266
+ (parent.type === 'ArrowFunctionExpression' && parent.body === node))
267
+ if (!alreadyAwaited && !returnedAsIs) spots.add(node.start)
254
268
  }
255
269
  }
256
270
 
257
271
  for (const k of Object.keys(node)) {
258
272
  if (k === 'type' || k === 'start' || k === 'end' || k === 'loc' || k === 'range') continue
259
273
  const v = node[k]
274
+ // A try's block and its catch clause are "in try"; its finally is not (a return there is final).
275
+ const kInTry = node.type === 'TryStatement' ? (k === 'block' || k === 'handler') || childInTry : childInTry
260
276
  if (Array.isArray(v)) {
261
- for (const c of v) walkAST(c, node, childAsyncScope)
277
+ for (const c of v) walkAST(c, node, childAsyncScope, passAwait, kInTry)
262
278
  } else if (v && typeof v.type === 'string') {
263
- walkAST(v, node, childAsyncScope)
279
+ walkAST(v, node, childAsyncScope, passAwait, kInTry)
264
280
  }
265
281
  }
266
282
  }
267
283
 
268
284
  walkAST(ast, null, false)
269
- if (!spots.length) return src
285
+ if (!spots.size) return src
270
286
 
271
- spots.sort((a, b) => b - a)
287
+ const order = [...spots].sort((a, b) => b - a)
288
+ spots.clear()
289
+ for (const p of order) spots.add(p)
272
290
  let out = src
273
291
  for (const pos of spots) {
274
292
  out = out.slice(0, pos) + 'await ' + out.slice(pos)
@@ -279,6 +297,31 @@ function upgradeCallSitesToAwait(src, fnName) {
279
297
  }
280
298
  }
281
299
 
300
+ // STACKED AWAITS LEFT BY A CRDT UNION (0.14.3 P0-5-3). Two peers making the same sync -> async
301
+ // change at once each insert `await ` before the same call; the shared text keeps BOTH inserts, so
302
+ // the room reads `await await f()`. No merge can prevent that (each peer's merge is right on its own),
303
+ // so the engine removes it afterwards. Returns the [start, end) spans to delete: for every
304
+ // `await await x`, the OUTER `await` and the whitespace after it, and only when nothing but whitespace
305
+ // separates the two (so `await (await x)`, a comment, or a string is never touched). On the same
306
+ // converged text every peer computes the same spans, so their deletes are of the same characters.
307
+ function stackedAwaits(src) {
308
+ let ast
309
+ try { ast = parse(src) } catch { return [] }
310
+ const spans = []
311
+ const walk = (node) => {
312
+ if (!node || typeof node.type !== 'string') return
313
+ if (node.type === 'AwaitExpression' && node.argument && node.argument.type === 'AwaitExpression' &&
314
+ /^await\s+$/.test(src.slice(node.start, node.argument.start))) spans.push([node.start, node.argument.start])
315
+ for (const k of Object.keys(node)) {
316
+ if (k === 'type' || k === 'start' || k === 'end' || k === 'loc' || k === 'range') continue
317
+ const v = node[k]
318
+ if (Array.isArray(v)) { for (const c of v) walk(c) } else if (v && typeof v.type === 'string') walk(v)
319
+ }
320
+ }
321
+ walk(ast)
322
+ return spans.sort((a, b) => a[0] - b[0])
323
+ }
324
+
282
325
  // Find callers of `fnName` that are synchronous (cannot be auto-awaited without changing the caller's signature)
283
326
  function syncCallSitesOf(src, fnName) {
284
327
  const syncCallers = []
@@ -641,6 +684,7 @@ export const javascript = {
641
684
  declBody,
642
685
  isAsyncDecl,
643
686
  upgradeCallSitesToAwait,
687
+ stackedAwaits, // spans of `await await` a CRDT union left (P0-5-3)
644
688
  syncCallSitesOf,
645
689
  renameRefs, // rewrites every matching identifier (kept for reference)
646
690
  renameFreeRefs, // scope-aware — only rewrites references that resolve to the binding
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hivecode-mcp",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
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",
@@ -36,8 +36,13 @@
36
36
  "hive-version.js",
37
37
  "hive-pr-staleness.js",
38
38
  "hive-pr.js",
39
- "hive-branch.js", "hive-land.js", "hive-repos.js", "hive-room-mode.js", "hive-room-watcher.js",
39
+ "hive-branch.js",
40
+ "hive-land.js",
41
+ "hive-repos.js",
42
+ "hive-room-mode.js",
43
+ "hive-room-watcher.js",
40
44
  "hive-mailbox.js",
45
+ "hive-seen.js",
41
46
  "hive-obligations.js",
42
47
  "hive-receipts.js",
43
48
  "hive-throttle.js",
@@ -134,7 +139,7 @@
134
139
  "test:substrate:authority": "node substrate-authority-test.js",
135
140
  "test:substrate:all": "node substrate-test.js && node substrate-live-test.js && node substrate-enforce-test.js && node substrate-authority-test.js",
136
141
  "test:recover": "node tests/recover-test.mjs",
137
- "test:mailbox": "node tests/mailbox-test.mjs && node tests/mcp-wait-test.mjs && node tests/mailbox-noise-test.mjs",
142
+ "test:mailbox": "node tests/mailbox-test.mjs && node tests/mcp-wait-test.mjs && node tests/mailbox-noise-test.mjs && node tests/mail-identity-test.mjs",
138
143
  "test:stophook": "node tests/stop-hook-test.mjs",
139
144
  "test:limits": "node tests/limits-test.mjs && node tests/rate-hold-test.mjs && node tests/persist-batch-test.mjs",
140
145
  "test:merge": "node tests/diff3-test.mjs && node tests/merge-balloon-test.mjs && node tests/icr-merge-test.js && node tests/icr-gap-comments-test.mjs && node tests/icr-comment-fuzz-test.mjs",
@@ -188,5 +193,9 @@
188
193
  "doc": "docs",
189
194
  "test": "tests"
190
195
  },
191
- "author": "Ganesh Shivlal Kute <support@getgrm.tech>"
196
+ "author": "Ganesh Shivlal Kute <support@getgrm.tech>",
197
+ "repository": {
198
+ "type": "git",
199
+ "url": "git+https://github.com/Ganesh-Kute/HiveCode.git"
200
+ }
192
201
  }