hivecode-mcp 0.14.1 → 0.14.2

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.
Files changed (72) hide show
  1. package/HIVE_RULES.md +98 -98
  2. package/LICENSE +21 -21
  3. package/README.md +421 -421
  4. package/core.js +91 -91
  5. package/docs/ALWAYS_ON.md +310 -310
  6. package/docs/GITHUB_WORKFLOW.md +394 -394
  7. package/docs/HIVE_RULES.md +41 -41
  8. package/docs/MCP.md +157 -157
  9. package/docs/QUICKSTART.md +266 -266
  10. package/docs/SELF_HOSTING.md +256 -256
  11. package/docs/SLACK_SETUP.md +474 -474
  12. package/docs/TROUBLESHOOTING.md +105 -105
  13. package/hive-activity.js +336 -336
  14. package/hive-authors.js +46 -44
  15. package/hive-await.js +230 -230
  16. package/hive-branch.js +608 -607
  17. package/hive-busy.js +88 -88
  18. package/hive-coord.js +110 -110
  19. package/hive-gate.js +135 -135
  20. package/hive-github.js +3 -1
  21. package/hive-hook-install.js +179 -179
  22. package/hive-instructions.js +74 -74
  23. package/hive-land.js +695 -691
  24. package/hive-listen.js +81 -81
  25. package/hive-mailbox.js +520 -520
  26. package/hive-mcp-http.js +591 -591
  27. package/hive-mcp.js +33 -6
  28. package/hive-mux-proto.js +58 -58
  29. package/hive-mux.js +351 -351
  30. package/hive-notify.js +234 -233
  31. package/hive-obligations.js +247 -247
  32. package/hive-poll-proto.js +50 -50
  33. package/hive-pr-staleness.js +189 -189
  34. package/hive-pr.js +600 -512
  35. package/hive-privacy.js +327 -327
  36. package/hive-receipts.js +233 -233
  37. package/hive-relay-health.js +234 -234
  38. package/hive-repos.js +430 -430
  39. package/hive-room-key.js +210 -210
  40. package/hive-room-mode.js +1668 -1668
  41. package/hive-room-watcher.js +562 -561
  42. package/hive-secrets.js +89 -89
  43. package/hive-session.js +114 -114
  44. package/hive-setup.js +560 -560
  45. package/hive-slack-agent.js +408 -408
  46. package/hive-slack.js +2384 -2367
  47. package/hive-stdio.js +172 -172
  48. package/hive-stop-hook.js +293 -293
  49. package/hive-throttle.js +118 -118
  50. package/hive-time.js +71 -71
  51. package/hive-transport.js +578 -578
  52. package/hive-update.js +58 -58
  53. package/hive-version.js +110 -110
  54. package/hive-webhook.js +55 -54
  55. package/icr-merge.js +58 -58
  56. package/icr.js +851 -851
  57. package/lang-brace.js +305 -305
  58. package/lang-js.js +651 -651
  59. package/lang-json.js +211 -211
  60. package/lang-python.js +588 -588
  61. package/lang-ruby.js +230 -230
  62. package/lang-toml.js +136 -136
  63. package/lang-yaml.js +183 -183
  64. package/limits.js +413 -413
  65. package/package.json +1 -1
  66. package/reconcile.cjs +621 -621
  67. package/slack-app-manifest-agent.json +61 -61
  68. package/slack-app-manifest-inbound.json +36 -36
  69. package/slack-app-manifest.json +27 -27
  70. package/substrate.js +252 -252
  71. package/sync.js +169 -11
  72. package/token.js +201 -201
package/hive-mcp.js CHANGED
@@ -609,9 +609,10 @@ const connectionOf = (hive) => (typeof hive.connection === 'function' ? hive.con
609
609
  // copy changed and whether this folder's copy arrived. Said, not refused: claiming and then deciding
610
610
  // not to edit is legitimate.
611
611
  const claimSnaps = new Map() // `${room}|${region}` -> sha256 of the room's text at claim time
612
+ const toLF = (s) => (typeof s === 'string' && s.indexOf('\r\n') !== -1 ? s.replace(/\r\n/g, '\n') : s)
612
613
  const textHash = (s) => crypto.createHash('sha256').update(String(s)).digest('hex')
613
614
  const roomTextOf = (s, region) => { try { const t = s.hive._fileText(region); return typeof t === 'string' ? t : null } catch { return null } }
614
- const diskTextOf = (s, region) => { try { return fs.readFileSync(path.join(s.dir, ...region.split('/')), 'utf8') } catch { return null } }
615
+ const diskTextOf = (s, region) => { try { return toLF(fs.readFileSync(path.join(s.dir, ...region.split('/')), 'utf8')) } catch { return null } }
615
616
  function noteClaim(s, region) {
616
617
  const t = roomTextOf(s, region)
617
618
  claimSnaps.set(`${s.room}|${region}`, t == null ? null : textHash(t))
@@ -880,8 +881,8 @@ const TOOLS = [
880
881
  inputSchema: { type: 'object', properties: { region: { type: 'string', description: 'file path or region id' }, force: { type: 'boolean', description: 'release despite a failing gate; recorded against you and announced; for abandoned work only' }, why: { type: 'string', description: 'why the work is abandoned (give it whenever force is true)' } }, required: ['region'] } },
881
882
  { name: 'hive_claims', description: 'The live claim board: who holds which file. Authoritative; read it instead of asking the room.',
882
883
  inputSchema: { type: 'object', properties: {} } },
883
- { name: 'hive_edit', description: 'Write a file through the medium (preferred over editing on disk): your intent and the exact change are signed into the provenance ledger as it lands. Claim the file first.',
884
- inputSchema: { type: 'object', properties: { file: { type: 'string', description: 'relative path in the room' }, content: { type: 'string', description: 'the complete new file content' }, intent: { type: 'string', description: 'why you are making the change (signed into provenance)' } }, required: ['file', 'content'] } },
884
+ { name: 'hive_edit', description: 'Write a file through the medium (preferred over editing on disk): your intent and the exact change are signed into the provenance ledger as it lands. Claim the file first. Changing existing lines: pass old (whole lines, exactly as you read them) and new; if those lines changed since you read them it is refused and nothing is written. A whole new file: pass content.',
885
+ inputSchema: { type: 'object', properties: { file: { type: 'string', description: 'relative path in the room' }, old: { type: 'string', description: 'whole lines you are replacing, exactly as you read them; must appear once' }, new: { type: 'string', description: 'what replaces old' }, content: { type: 'string', description: 'the complete new file content (instead of old/new)' }, intent: { type: 'string', description: 'why you are making the change (signed into provenance)' } }, required: ['file'] } },
885
886
  { name: 'hive_fork', description: 'Read a silent fork on a file: the common base plus every divergent version with its author and signed intent. Reconcile them, then submit with hive_resolve.',
886
887
  inputSchema: { type: 'object', properties: { file: { type: 'string', description: 'the forked file path, as announced in chat' } }, required: ['file'] } },
887
888
  { name: 'hive_resolve', description: 'Resolve a fork with the full reconciled file, serving every side\'s intent. It must parse with no dangling references, or it is refused and nothing changes.',
@@ -976,11 +977,14 @@ function enforceDcoLock(opts = {}) {
976
977
  // violated within the hour by everyone including its author. A COUNTED lease makes the second
977
978
  // write impossible rather than discouraged: the server spends it, and the window shuts itself.
978
979
  const lease = /^PUBLISH(?::(\d+))?$/.exec(String(status || ''))
979
- if (lease && opts.consumes) {
980
+ // `peek` checks the lease without spending it: hive_edit old/new peeks, writes, and spends only when
981
+ // something landed, so a refused or no-op edit keeps the window (antigravity-3, 1 Nov).
982
+ if (lease && (opts.consumes || opts.peek)) {
980
983
  const left = lease[1] === undefined ? 1 : Number(lease[1])
981
984
  if (left <= 0) {
982
985
  throw new Error(`[Lease Spent] Your PUBLISH window is used up. Ask for a new one — and if this is a revision of what you just wrote, publish to a NEW path rather than rewriting it.`)
983
986
  }
987
+ if (!opts.consumes) return
984
988
  const next = left - 1
985
989
  try {
986
990
  s.hive.setState(`${name}_status`, next > 0 ? `PUBLISH:${next}` : 'LOCKED')
@@ -1739,9 +1743,15 @@ Then call hive_release.`)
1739
1743
  case 'hive_claims': { const b = requireSession().hive.claimsBoard(); return text(b.length ? b.map((c) => `${c.region} — ${c.by}${c.intent ? ` (${c.intent})` : ''}`).join('\n') : '(nothing claimed right now — all open)') }
1740
1744
  case 'hive_edit': {
1741
1745
  const editPath = String(args.file || '')
1746
+ // Two modes, never mixed: content is the whole file, old/new is the lines the agent edited.
1747
+ // Only `file` is required by the schema now, so these checks are what refuse a call missing both.
1748
+ const byLines = args.old !== undefined || args.new !== undefined
1749
+ if (byLines && args.content !== undefined) return err('pass either content (the whole file) or old and new (the lines you changed), not both. Nothing was written.')
1750
+ if (byLines && (typeof args.old !== 'string' || typeof args.new !== 'string')) return err('old/new needs both old and new. Nothing was written.')
1751
+ if (!byLines && typeof args.content !== 'string') return err('pass content (the whole file), or old and new (the lines you changed). Nothing was written.')
1742
1752
  // Path gate + lease consumed BEFORE the write, not after: a spent lease that still wrote is
1743
1753
  // indistinguishable from no lease at all.
1744
- enforceDcoLock({ consumes: true, path: editPath })
1754
+ enforceDcoLock(byLines ? { peek: true, path: editPath } : { consumes: true, path: editPath })
1745
1755
  // HIV-27, same reasoning: this tool's entire promise is that the change reaches every
1746
1756
  // peer. On a path this client never opens a connection for, it would not -- and it
1747
1757
  // would still report success.
@@ -1751,8 +1761,20 @@ Then call hive_release.`)
1751
1761
 
1752
1762
  Nothing was written. This tool exists to guarantee the change syncs to every peer; on this path it cannot.`)
1753
1763
  }
1754
- const r = requireSession().hive.edit(editPath, String(args.content ?? ''), String(args.intent || ''))
1764
+ let r
1765
+ if (byLines) {
1766
+ // A file the room has but this folder has not loaded yet comes back as `loading`: wait for it
1767
+ // (up to 8 s) rather than hand the agent a retry it may answer with a blind content write.
1768
+ for (let t = 0; ; t++) {
1769
+ if (t) enforceDcoLock({ peek: true, path: editPath })
1770
+ r = requireSession().hive.editLines(editPath, args.old, args.new, String(args.intent || ''))
1771
+ if (!r.loading || t >= 32) break
1772
+ await new Promise((res) => setTimeout(res, 250))
1773
+ }
1774
+ if (r.ok && !r.noop) enforceDcoLock({ consumes: true, path: editPath }) // peeked just above, nothing awaited since
1775
+ } else r = requireSession().hive.edit(editPath, args.content, String(args.intent || ''))
1755
1776
  if (r.error) return err(r.error)
1777
+ if (r.noop) return text(`nothing to change: old and new are the same in ${r.file}. Nothing was written.`)
1756
1778
  if (r.withheld) {
1757
1779
  firstTime('withheld:' + r.file) // said here, so the next hive_wait does not say it again
1758
1780
  return text(`⛔ wrote ${r.file} in YOUR folder only. It was NOT shared, because it contains ${r.withheld}, and Hivecode does not send keys to the room or the relay. Nobody else has this change. Read the key from the environment instead (put it in .env, which is never shared); the file is shared on its next save without the key.`)
@@ -1836,6 +1858,11 @@ Nothing was written. This tool exists to guarantee the change syncs to every pee
1836
1858
  unread: mailbox().peek(chatArray()).unreadCount,
1837
1859
  connected: conn ? conn.connected : null,
1838
1860
  filesSynced: conn ? `${conn.filesSynced}/${conn.files}` : null,
1861
+ // How full this client's bounded working set is: files open / the bound (HIVE_MAX_OPEN_FILES, or
1862
+ // 4096 on one socket, 128 with a socket per file). At the bound, files that were in the room
1863
+ // at join arrive when touched; new ones still open (sync.js newRemote), and `waiting` counts
1864
+ // the new ones queued for room.
1865
+ ...(conn && conn.openCap ? { workingSet: `${conn.files}/${conn.openCap} open (${conn.transport === 'mux' ? 'one socket' : conn.transport === 'socket' ? 'a socket per file' : conn.transport})${conn.newWaiting ? `, ${conn.newWaiting} new waiting` : ''}` } : {}),
1839
1866
  // 'ws', or 'poll' where this network refuses WebSockets (HTTPS long-poll: same rooms, same rules).
1840
1867
  transport: conn ? transportFor(s.relay) : null,
1841
1868
  ...(conn && conn.folderLost ? { folderLost: conn.folderLost } : {}),
package/hive-mux-proto.js CHANGED
@@ -1,58 +1,58 @@
1
- // hive-mux-proto.js -- the wire format for ONE socket per participant, shared by relay and client.
2
- //
3
- // WHY: Hivecode opened one WebSocket per FILE. A participant held 1 + min(files, 128) sockets,
4
- // so an agent in a 1000-file repository saw only 128 files on disk, and a relay's connection cap
5
- // divided by 129 per person. The mux carries every file doc of one room over one socket.
6
- //
7
- // The design rule that makes it safe: the relay does not grow a second enforcement path. Each
8
- // OPEN runs the SAME per-doc admission a per-file upgrade runs (token scope for that path, the
9
- // per-room ceiling, the committer limit), and the accepted channel is wrapped in a VirtualConn
10
- // that looks exactly like a ws connection to installUsageLimits, makeReadOnly, installWriteGate
11
- // and setupWSConnection. The bytes inside a MSG frame are the same y-protocols message a
12
- // per-file socket would carry.
13
- //
14
- // Frames are binary, lib0-encoded:
15
- // client -> relay OPEN [0, id, docName] open this file doc as channel `id`
16
- // CLOSE [1, id] the client is done with it
17
- // MSG [2, id, ...payload] a y-protocols message for that doc
18
- // relay -> client MSG [2, id, ...payload]
19
- // OPENED [3, id] admitted; the relay's sync step 1 follows
20
- // REFUSED [4, id, code, reason] not admitted -- the HTTP status a per-file
21
- // upgrade would have answered, and why
22
- // CLOSED [5, id, reason] the relay closed that channel
23
- // BEAT [6, 0] liveness, so a dead socket is noticed
24
- // Channel ids are chosen by the client and are only meaningful on the socket that opened them.
25
- import * as encoding from 'lib0/encoding'
26
- import * as decoding from 'lib0/decoding'
27
-
28
- export const MUX_PREFIX = '_hive_mux/'
29
- export const MUX_VERSION = 1
30
- export const F = Object.freeze({ OPEN: 0, CLOSE: 1, MSG: 2, OPENED: 3, REFUSED: 4, CLOSED: 5, BEAT: 6 })
31
-
32
- export function encodeFrame(kind, id, a, b) {
33
- const e = encoding.createEncoder()
34
- encoding.writeVarUint(e, kind)
35
- encoding.writeVarUint(e, id >>> 0)
36
- if (kind === F.OPEN) encoding.writeVarString(e, String(a))
37
- else if (kind === F.MSG) encoding.writeUint8Array(e, a instanceof Uint8Array ? a : new Uint8Array(a))
38
- else if (kind === F.REFUSED) { encoding.writeVarUint(e, a >>> 0); encoding.writeVarString(e, String(b || '')) }
39
- else if (kind === F.CLOSED) encoding.writeVarString(e, String(a || ''))
40
- return encoding.toUint8Array(e)
41
- }
42
-
43
- /** Decode one frame. Throws on garbage, which the caller treats as a protocol violation. */
44
- export function decodeFrame(buf) {
45
- const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
46
- const d = decoding.createDecoder(u8)
47
- const kind = decoding.readVarUint(d)
48
- const id = decoding.readVarUint(d)
49
- if (kind === F.OPEN) return { kind, id, docName: decoding.readVarString(d) }
50
- if (kind === F.MSG) return { kind, id, payload: u8.subarray(d.pos) }
51
- if (kind === F.REFUSED) return { kind, id, code: decoding.readVarUint(d), reason: decoding.readVarString(d) }
52
- if (kind === F.CLOSED) return { kind, id, reason: decoding.readVarString(d) }
53
- if (kind === F.CLOSE || kind === F.OPENED || kind === F.BEAT) return { kind, id }
54
- throw new Error(`unknown mux frame kind ${kind}`)
55
- }
56
-
57
- /** The mux upgrade path for a base room, as the relay parses it (pathname minus the slash). */
58
- export function muxPath(room) { return MUX_PREFIX + encodeURIComponent(room) }
1
+ // hive-mux-proto.js -- the wire format for ONE socket per participant, shared by relay and client.
2
+ //
3
+ // WHY: Hivecode opened one WebSocket per FILE. A participant held 1 + min(files, 128) sockets,
4
+ // so an agent in a 1000-file repository saw only 128 files on disk, and a relay's connection cap
5
+ // divided by 129 per person. The mux carries every file doc of one room over one socket.
6
+ //
7
+ // The design rule that makes it safe: the relay does not grow a second enforcement path. Each
8
+ // OPEN runs the SAME per-doc admission a per-file upgrade runs (token scope for that path, the
9
+ // per-room ceiling, the committer limit), and the accepted channel is wrapped in a VirtualConn
10
+ // that looks exactly like a ws connection to installUsageLimits, makeReadOnly, installWriteGate
11
+ // and setupWSConnection. The bytes inside a MSG frame are the same y-protocols message a
12
+ // per-file socket would carry.
13
+ //
14
+ // Frames are binary, lib0-encoded:
15
+ // client -> relay OPEN [0, id, docName] open this file doc as channel `id`
16
+ // CLOSE [1, id] the client is done with it
17
+ // MSG [2, id, ...payload] a y-protocols message for that doc
18
+ // relay -> client MSG [2, id, ...payload]
19
+ // OPENED [3, id] admitted; the relay's sync step 1 follows
20
+ // REFUSED [4, id, code, reason] not admitted -- the HTTP status a per-file
21
+ // upgrade would have answered, and why
22
+ // CLOSED [5, id, reason] the relay closed that channel
23
+ // BEAT [6, 0] liveness, so a dead socket is noticed
24
+ // Channel ids are chosen by the client and are only meaningful on the socket that opened them.
25
+ import * as encoding from 'lib0/encoding'
26
+ import * as decoding from 'lib0/decoding'
27
+
28
+ export const MUX_PREFIX = '_hive_mux/'
29
+ export const MUX_VERSION = 1
30
+ export const F = Object.freeze({ OPEN: 0, CLOSE: 1, MSG: 2, OPENED: 3, REFUSED: 4, CLOSED: 5, BEAT: 6 })
31
+
32
+ export function encodeFrame(kind, id, a, b) {
33
+ const e = encoding.createEncoder()
34
+ encoding.writeVarUint(e, kind)
35
+ encoding.writeVarUint(e, id >>> 0)
36
+ if (kind === F.OPEN) encoding.writeVarString(e, String(a))
37
+ else if (kind === F.MSG) encoding.writeUint8Array(e, a instanceof Uint8Array ? a : new Uint8Array(a))
38
+ else if (kind === F.REFUSED) { encoding.writeVarUint(e, a >>> 0); encoding.writeVarString(e, String(b || '')) }
39
+ else if (kind === F.CLOSED) encoding.writeVarString(e, String(a || ''))
40
+ return encoding.toUint8Array(e)
41
+ }
42
+
43
+ /** Decode one frame. Throws on garbage, which the caller treats as a protocol violation. */
44
+ export function decodeFrame(buf) {
45
+ const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
46
+ const d = decoding.createDecoder(u8)
47
+ const kind = decoding.readVarUint(d)
48
+ const id = decoding.readVarUint(d)
49
+ if (kind === F.OPEN) return { kind, id, docName: decoding.readVarString(d) }
50
+ if (kind === F.MSG) return { kind, id, payload: u8.subarray(d.pos) }
51
+ if (kind === F.REFUSED) return { kind, id, code: decoding.readVarUint(d), reason: decoding.readVarString(d) }
52
+ if (kind === F.CLOSED) return { kind, id, reason: decoding.readVarString(d) }
53
+ if (kind === F.CLOSE || kind === F.OPENED || kind === F.BEAT) return { kind, id }
54
+ throw new Error(`unknown mux frame kind ${kind}`)
55
+ }
56
+
57
+ /** The mux upgrade path for a base room, as the relay parses it (pathname minus the slash). */
58
+ export function muxPath(room) { return MUX_PREFIX + encodeURIComponent(room) }