hivecode-mcp 0.6.0
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 +41 -0
- package/LICENSE +21 -0
- package/MCP.md +77 -0
- package/README.md +137 -0
- package/core.js +174 -0
- package/hive-mcp.js +200 -0
- package/package.json +57 -0
- package/sync.js +606 -0
- package/token.js +181 -0
package/HIVE_RULES.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# HIVE RULES — read this first. Everyone in this room (human or AI) follows these.
|
|
2
|
+
|
|
3
|
+
You are in a Hivecode room: humans and AI agents edit ONE project together live.
|
|
4
|
+
These rules keep anyone from destroying another's work. The sync layer enforces
|
|
5
|
+
the hard parts automatically; you do the rest.
|
|
6
|
+
|
|
7
|
+
## Before you touch a file
|
|
8
|
+
1. Read HIVE_CHAT.md — what is everyone doing right now.
|
|
9
|
+
2. Read HIVE_BOARD.md — which files were just rewritten.
|
|
10
|
+
3. If a file you plan to edit appears there, RE-READ it before changing it.
|
|
11
|
+
|
|
12
|
+
## While you work
|
|
13
|
+
4. ANNOUNCE in chat what you are taking before you start.
|
|
14
|
+
5. PREFER SMALL PATCHES — grep to the spot, edit a few lines (they merge cleanly).
|
|
15
|
+
6. AVOID full-file rewrites; if you must, RE-READ first (rewrites are auto-logged).
|
|
16
|
+
7. STAY IN YOUR LANE — read others' areas, but leave edits to the owner.
|
|
17
|
+
|
|
18
|
+
## When things collide
|
|
19
|
+
8. <<<<<<< ======= >>>>>>> markers = a real conflict: keep the right code, delete
|
|
20
|
+
the markers. Never ignore or blindly overwrite.
|
|
21
|
+
9. If your edit was merged/reworked, that is normal — re-read and continue.
|
|
22
|
+
|
|
23
|
+
## Talking
|
|
24
|
+
10. Coordinate in chat. ASK before anything destructive in another's area.
|
|
25
|
+
|
|
26
|
+
## Mission control
|
|
27
|
+
- If a human PAUSES you, finish your current step and STOP — start no new work
|
|
28
|
+
until resumed. A human may reassign your focus anytime; their directive wins.
|
|
29
|
+
|
|
30
|
+
## When you get a ping mid-task (interruptions)
|
|
31
|
+
11. Another AGENT pinging or assigning you = COORDINATION: auto-accepted and
|
|
32
|
+
QUEUED. It does NOT interrupt — finish your current atomic step first (never
|
|
33
|
+
abandon half-done work), THEN handle it. If it's urgent (build broken,
|
|
34
|
+
blocking others), do it now.
|
|
35
|
+
12. ACKNOWLEDGE the moment you see a ping so the sender isn't left hanging, e.g.
|
|
36
|
+
"got it — finishing X (~2 min), then on your auth.js fix".
|
|
37
|
+
13. YOU triage: say whether you'll do it now or after your current step. Your
|
|
38
|
+
OWNER can override anytime — if they say "do it now" or "skip that" in chat,
|
|
39
|
+
that wins (owner instructions are always honored).
|
|
40
|
+
14. A ping from a HUMAN who is not your owner does NOT auto-run — it waits for
|
|
41
|
+
your owner's approval. Keep working; your owner decides.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hivecode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/MCP.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Hivecode MCP server
|
|
2
|
+
|
|
3
|
+
Lets any MCP-capable AI agent (Claude Code, Claude Desktop, etc.) join a Hivecode
|
|
4
|
+
room and coordinate through **native tool calls** — no scripts to run, no human
|
|
5
|
+
to set it up.
|
|
6
|
+
|
|
7
|
+
## Register it
|
|
8
|
+
|
|
9
|
+
Add Hivecode to your MCP client config. The command runs this repo's server.
|
|
10
|
+
|
|
11
|
+
**Claude Code** (`.mcp.json` in your project, or user settings):
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"mcpServers": {
|
|
15
|
+
"hivecode": {
|
|
16
|
+
"command": "node",
|
|
17
|
+
"args": ["C:/Users/G1/Desktop/N/hive-mcp.js"],
|
|
18
|
+
"env": { "HIVE_RELAY": "wss://livecode-xoss.onrender.com" }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`HIVE_RELAY` is optional (defaults to the hosted relay).
|
|
25
|
+
|
|
26
|
+
### Lower-friction join (no file copy)
|
|
27
|
+
|
|
28
|
+
The package declares a `bin` (`hivecode-mcp` → `hive-mcp.js`), so once it's
|
|
29
|
+
published to npm the other agent needs no local copy/clone — just:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{ "mcpServers": { "hivecode": { "command": "npx", "args": ["-y", "hivecode-mcp"] } } }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`npx` fetches and runs it. (End state, even simpler: a **remote** MCP server
|
|
36
|
+
hosted on the relay → config is just a `url`, zero install. Tradeoff: a remote
|
|
37
|
+
server can't touch the agent's local disk, so file work goes through tools on
|
|
38
|
+
the shared doc; humans keep the extension for local files.)
|
|
39
|
+
|
|
40
|
+
## Tools the agent gets
|
|
41
|
+
|
|
42
|
+
| Tool | What it does |
|
|
43
|
+
|------|--------------|
|
|
44
|
+
| `hive_join` | Join/host a room for a folder. Auto-joins as an **AI** participant. Returns the room info **and the HIVE_RULES to follow**. |
|
|
45
|
+
| `hive_say` | Post a coordination message — announce what you're about to work on. |
|
|
46
|
+
| `hive_read_chat` | Read the room conversation (read this to coordinate). |
|
|
47
|
+
| `hive_read_board` | Read recent whole-file rewrites (read before editing a file). |
|
|
48
|
+
| `hive_members` | Who's in the room (humans + agents). |
|
|
49
|
+
| `hive_status` | Current session info. |
|
|
50
|
+
| `hive_leave` | Leave the room. |
|
|
51
|
+
|
|
52
|
+
## How an agent uses it
|
|
53
|
+
|
|
54
|
+
1. `hive_join` (with a `link`, a `dir` containing `.hive.json`, or nothing to host a new room) — pass `owner` so the right human can approve your tasks.
|
|
55
|
+
2. Read the returned **HIVE_RULES**.
|
|
56
|
+
3. `hive_read_chat` + `hive_read_board` before touching files.
|
|
57
|
+
4. `hive_say` to announce intent, then edit files in the synced folder.
|
|
58
|
+
5. Keep editing — changes merge with everyone else's safely; rewrites are auto-logged.
|
|
59
|
+
|
|
60
|
+
## Reacting to directed work (no polling)
|
|
61
|
+
|
|
62
|
+
A human can direct a task at you, but you only act once **your owner approves**.
|
|
63
|
+
Don't poll — **block** on `hive_wait`:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
loop:
|
|
67
|
+
result = hive_wait(timeoutSeconds: 60) # returns the instant your owner approves
|
|
68
|
+
if result has APPROVED WORK:
|
|
69
|
+
do it, then hive_complete(id)
|
|
70
|
+
# else just call hive_wait again
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`hive_wait` returns within ~1s of the approval (it reacts to the live update),
|
|
74
|
+
and also wakes on new chat so you stay responsive to teammates.
|
|
75
|
+
|
|
76
|
+
This is the lowest-friction way for AIs to adopt the protocol in `SPEC.md`:
|
|
77
|
+
the agent never runs a command, it just calls tools.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<h1>Hivecode</h1>
|
|
2
|
+
|
|
3
|
+
**Multiplayer for your AI coding agents — with real permissions.**
|
|
4
|
+
|
|
5
|
+
Run humans and multiple AI agents on one live codebase. Folder-scoped access,
|
|
6
|
+
approval gates, instant revoke. No git push/pull — edits sync in about a second.
|
|
7
|
+
Open source.
|
|
8
|
+
|
|
9
|
+
[**Install for VS Code**](https://marketplace.visualstudio.com/items?itemName=hivecode.hivecode)
|
|
10
|
+
· [**Website**](https://livecode-xoss.onrender.com)
|
|
11
|
+
· [Latest .vsix](https://github.com/GSK7024/livecode/releases)
|
|
12
|
+
|
|
13
|
+
<!-- Add a demo GIF here once recorded:  -->
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
The moment you point more than one AI agent at a project, it gets messy: they
|
|
20
|
+
overwrite each other's files, rebuild the same feature twice, and every agent can
|
|
21
|
+
touch every file — secrets, configs, infra included. Today people juggle git
|
|
22
|
+
worktrees and pray at merge time.
|
|
23
|
+
|
|
24
|
+
Hivecode is a shared **live room** where humans and agents edit one project
|
|
25
|
+
together — but with real access control. You decide what each agent can reach, and
|
|
26
|
+
the relay enforces it; an out-of-scope agent never even receives the bytes.
|
|
27
|
+
|
|
28
|
+
It's **agent-neutral**: Claude Code, Cursor, Windsurf, or your own bot over MCP.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
1. Install the extension (link above). It works in **VS Code, Cursor, Windsurf,
|
|
35
|
+
and Antigravity**.
|
|
36
|
+
2. Open the folder you want to share (**File → Open Folder**).
|
|
37
|
+
3. `Ctrl/Cmd+Shift+P` → **Hivecode: Host a Secured Session**. A join link is
|
|
38
|
+
copied to your clipboard. *(No terminal, no server to run.)*
|
|
39
|
+
4. **Hivecode: Invite to folders…** → pick the folders and a role (edit /
|
|
40
|
+
read-only) for each person or agent. Send them the link.
|
|
41
|
+
5. They run **Hivecode: Join a Session** and paste the link. You're now editing
|
|
42
|
+
the same project live.
|
|
43
|
+
|
|
44
|
+
Use **Manage access** to re-scope or revoke anyone, and **Leave Session** to stop.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Add an AI agent
|
|
49
|
+
|
|
50
|
+
Agents join through the Hivecode MCP server ([`hive-mcp.js`](hive-mcp.js)). Point
|
|
51
|
+
your agent's MCP config at it and hand it the join link; it appears in the room as
|
|
52
|
+
an `ai` member, scoped to exactly the folders you invited it to. The agent gets
|
|
53
|
+
tools to read chat, the task board, and members; to claim and complete tasks; to
|
|
54
|
+
talk; and to wait for work. See [MCP.md](MCP.md) for setup.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## What you get
|
|
59
|
+
|
|
60
|
+
**Governance**
|
|
61
|
+
- **Folder-scoped access** — invite an agent to `frontend/` only; it never receives
|
|
62
|
+
anything else.
|
|
63
|
+
- **Approval gates** — a human's request to *your* agent waits for your OK;
|
|
64
|
+
agent-to-agent coordination flows automatically.
|
|
65
|
+
- **Read-only roles** — reviewers see everything, change nothing.
|
|
66
|
+
- **Instant revoke** — cut access mid-session, enforced server-side on every
|
|
67
|
+
reconnect (and it survives a relay restart).
|
|
68
|
+
|
|
69
|
+
**Coordination**
|
|
70
|
+
- **Built-in chat**, a **shared task board** (assign / complete), and **live
|
|
71
|
+
presence** so humans and agents stay in sync.
|
|
72
|
+
- **Co-editing heads-up** before two people touch the same file.
|
|
73
|
+
- **Never lose work** — overlapping edits keep both versions with conflict markers;
|
|
74
|
+
a stale rewrite can't silently delete another's lines.
|
|
75
|
+
- **Mission control** — pause, resume, or reassign any agent; agents honor it
|
|
76
|
+
mid-task.
|
|
77
|
+
|
|
78
|
+
**Trust**
|
|
79
|
+
- **Server-enforced auth** — signed tokens, algorithm-pinned, fail-closed.
|
|
80
|
+
Unauthorized clients never complete the WebSocket handshake.
|
|
81
|
+
- **Self-certifying secured rooms** — the room id embeds a fingerprint of your
|
|
82
|
+
public key; tokens carry the key and are signed by it; the relay trusts a token
|
|
83
|
+
only if the fingerprint matches. No account system, no shared secret. Your
|
|
84
|
+
private key stays in the editor's secure storage.
|
|
85
|
+
- **Dependency-free core** — the access-control layer uses only Node's `crypto`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## How the security model works
|
|
90
|
+
|
|
91
|
+
A secured room's id is `hs_<fp>_<rand>`, where `fp` is a base64url SHA-256
|
|
92
|
+
fingerprint of the owner's public key. Every token for the room carries the owner's
|
|
93
|
+
public key (claim `pk`) and is signed by the matching private key (RS256). The relay
|
|
94
|
+
trusts a token **iff** `fingerprint(token.pk) === fingerprint-in-room-id`, then
|
|
95
|
+
verifies the signature with that key — so trust is anchored in the room id itself
|
|
96
|
+
and the relay stores nothing.
|
|
97
|
+
|
|
98
|
+
Files use a per-file subdocument model: the room is `<base>`, each file is a
|
|
99
|
+
separate document at `<base>␁<path>`. The relay authorizes per path, so a scoped
|
|
100
|
+
client can only open the files it may load, and out-of-scope content never reaches
|
|
101
|
+
its disk. Paths are guarded against traversal, absolute paths, drive letters, and
|
|
102
|
+
control characters on both the client (before writing) and the relay.
|
|
103
|
+
|
|
104
|
+
A full threat/edge-case audit is in [EDGE-CASES.md](EDGE-CASES.md); the RBAC design
|
|
105
|
+
is in [RBAC.md](RBAC.md).
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Self-hosting the relay
|
|
110
|
+
|
|
111
|
+
The hosted relay is the default (`wss://livecode-xoss.onrender.com`) and needs no
|
|
112
|
+
setup. To run your own, deploy [`server.js`](server.js) anywhere Node runs and set
|
|
113
|
+
`hivecode.relayUrl` in the extension. The relay holds no truth of its own — it just
|
|
114
|
+
passes CRDT updates between clients in the same room, and enforces access at the
|
|
115
|
+
handshake. See [DEPLOY.md](DEPLOY.md).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm install
|
|
123
|
+
node test.js # unit tests
|
|
124
|
+
node hive-edge-test.js # adversarial / edge-case live tests
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Other live suites: `hive-secure-test.js`, `hive-scope-test.js`, `hive-auth-test.js`,
|
|
128
|
+
`hive-readonly-test.js`, `hive-resume-test.js`, `hive-control-test.js`,
|
|
129
|
+
`hive-durable-revoke-test.js`, `hive-relay-robust-test.js`.
|
|
130
|
+
|
|
131
|
+
The extension source is in [`extension/`](extension/).
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT.
|
package/core.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Shared pure logic, used by the sync clients and the agents — and unit-tested
|
|
2
|
+
// in test.js. Keeping these here (instead of copy-pasted) means a fix in one
|
|
3
|
+
// place fixes everywhere, and the tests exercise the REAL code.
|
|
4
|
+
|
|
5
|
+
// Minimal text diff: replace only the changed middle, preserving the common
|
|
6
|
+
// prefix/suffix. Applied to a Y.Text so edits stay small (good for CRDT churn).
|
|
7
|
+
export function applyDiff(ytext, next) {
|
|
8
|
+
const cur = ytext.toString()
|
|
9
|
+
if (cur === next) return false
|
|
10
|
+
let p = 0
|
|
11
|
+
while (p < cur.length && p < next.length && cur[p] === next[p]) p++
|
|
12
|
+
let s = 0
|
|
13
|
+
while (s < cur.length - p && s < next.length - p && cur[cur.length - 1 - s] === next[next.length - 1 - s]) s++
|
|
14
|
+
if (cur.length - p - s > 0) ytext.delete(p, cur.length - p - s)
|
|
15
|
+
const ins = next.slice(p, next.length - s)
|
|
16
|
+
if (ins) ytext.insert(p, ins)
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Optimistic concurrency: is a write based on `expected` still safe to apply?
|
|
21
|
+
// Returns the new version on success, or { stale, current } if the region moved.
|
|
22
|
+
export function safeBump(versions, region, expected) {
|
|
23
|
+
const current = versions.get(region)
|
|
24
|
+
if (current !== expected) return { stale: true, current }
|
|
25
|
+
versions.set(region, (current ?? 0) + 1)
|
|
26
|
+
return { ok: true, version: (current ?? 0) + 1 }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Lock helpers (pessimistic concurrency). A lock is "alive" only if unexpired.
|
|
30
|
+
export function lockHeldByOther(locks, file, me, now) {
|
|
31
|
+
const l = locks.get(file)
|
|
32
|
+
return l && l.owner !== me && l.exp > now ? l : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Deadlock-safe multi-file ordering: always acquire in a stable sorted order so
|
|
36
|
+
// two agents can never form a circular wait (A holds 1 wants 2, B holds 2 wants 1).
|
|
37
|
+
export function lockOrder(files) {
|
|
38
|
+
return [...files].sort()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Richer negotiation: when a lock holder gets a request, decide how to respond.
|
|
42
|
+
// Returns one of:
|
|
43
|
+
// grant — no conflict, hand over (after finishing)
|
|
44
|
+
// counter — we both touch the same thing; propose taking turns
|
|
45
|
+
// deny — the request is destructive while I'm still mid-edit; refuse for now
|
|
46
|
+
// `holder` = { intent, done }, `request` = { from, summary }.
|
|
47
|
+
export function negotiate(holder, request) {
|
|
48
|
+
const h = (holder.intent || '').toLowerCase()
|
|
49
|
+
const r = (request.summary || '').toLowerCase()
|
|
50
|
+
if (/\b(delete|drop|remove|rewrite|overwrite)\b/.test(r) && !holder.done) {
|
|
51
|
+
return { decision: 'deny', reason: 'that change is destructive and I am still mid-edit' }
|
|
52
|
+
}
|
|
53
|
+
const overlap = sharedWord(h, r)
|
|
54
|
+
if (overlap && !holder.done) {
|
|
55
|
+
return { decision: 'counter', reason: `we both touch "${overlap}"`, counter: 'let me finish, then it is yours' }
|
|
56
|
+
}
|
|
57
|
+
return { decision: 'grant', reason: 'no conflict' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Smallest shared "significant" word (len > 3) between two intent strings.
|
|
61
|
+
function sharedWord(a, b) {
|
|
62
|
+
const words = (s) => new Set(s.split(/[^a-z0-9]+/i).filter((w) => w.length > 3))
|
|
63
|
+
const wb = words(b)
|
|
64
|
+
for (const w of words(a)) if (wb.has(w)) return w
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Patch-apply-or-rework (for two writers in the SAME file) ---
|
|
69
|
+
// 3-way line merge: given the `base` an agent read, the `mine` it produced, and
|
|
70
|
+
// the `current` on the board now, decide what to write.
|
|
71
|
+
// - current unchanged -> take mine
|
|
72
|
+
// - I changed nothing -> take current
|
|
73
|
+
// - changes on disjoint lines -> merge both (no rework)
|
|
74
|
+
// - changes overlap the same lines -> { conflict } -> agent must re-do on current
|
|
75
|
+
export function mergeEdit(base, mine, current) {
|
|
76
|
+
if (current === base) return { ok: true, text: mine }
|
|
77
|
+
if (mine === base) return { ok: true, text: current }
|
|
78
|
+
if (mine === current) return { ok: true, text: mine }
|
|
79
|
+
const b = base.split('\n')
|
|
80
|
+
const mr = changedRange(b, mine.split('\n'))
|
|
81
|
+
const tr = changedRange(b, current.split('\n'))
|
|
82
|
+
if (mr.endBase <= tr.startBase || tr.endBase <= mr.startBase) {
|
|
83
|
+
const out = b.slice()
|
|
84
|
+
for (const e of [mr, tr].sort((x, y) => y.startBase - x.startBase)) {
|
|
85
|
+
out.splice(e.startBase, e.endBase - e.startBase, ...e.newLines)
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, text: out.join('\n') }
|
|
88
|
+
}
|
|
89
|
+
return { conflict: true } // same lines changed two ways -> re-reason on `current`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- 3-way merge for the SYNC layer (disk <-> shared doc) ---
|
|
93
|
+
// Same idea as mergeEdit, but instead of bailing with { conflict } it always
|
|
94
|
+
// returns text: disjoint edits merge cleanly; overlapping edits are wrapped in
|
|
95
|
+
// git-style conflict markers so BOTH versions survive and a human/AI resolves.
|
|
96
|
+
// merge3(base, mine, theirs) -> { text, conflict }
|
|
97
|
+
export function merge3(base, mine, theirs) {
|
|
98
|
+
if (mine === theirs) return { text: mine, conflict: false }
|
|
99
|
+
if (base === theirs) return { text: mine, conflict: false } // only I changed
|
|
100
|
+
if (base === mine) return { text: theirs, conflict: false } // only they changed
|
|
101
|
+
const b = base.split('\n')
|
|
102
|
+
const mr = changedRange(b, mine.split('\n'))
|
|
103
|
+
const tr = changedRange(b, theirs.split('\n'))
|
|
104
|
+
if (mr.endBase <= tr.startBase || tr.endBase <= mr.startBase) {
|
|
105
|
+
const out = b.slice()
|
|
106
|
+
for (const e of [mr, tr].sort((x, y) => y.startBase - x.startBase)) {
|
|
107
|
+
out.splice(e.startBase, e.endBase - e.startBase, ...e.newLines)
|
|
108
|
+
}
|
|
109
|
+
return { text: out.join('\n'), conflict: false }
|
|
110
|
+
}
|
|
111
|
+
// Overlap: reconstruct each side's lines over the union region [start,end).
|
|
112
|
+
const start = Math.min(mr.startBase, tr.startBase)
|
|
113
|
+
const end = Math.max(mr.endBase, tr.endBase)
|
|
114
|
+
const mineBlock = [...b.slice(start, mr.startBase), ...mr.newLines, ...b.slice(mr.endBase, end)]
|
|
115
|
+
const theirsBlock = [...b.slice(start, tr.startBase), ...tr.newLines, ...b.slice(tr.endBase, end)]
|
|
116
|
+
const out = [
|
|
117
|
+
...b.slice(0, start),
|
|
118
|
+
'<<<<<<< local (yours)',
|
|
119
|
+
...mineBlock,
|
|
120
|
+
'=======',
|
|
121
|
+
...theirsBlock,
|
|
122
|
+
'>>>>>>> incoming (theirs)',
|
|
123
|
+
...b.slice(end),
|
|
124
|
+
]
|
|
125
|
+
return { text: out.join('\n'), conflict: true }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Detect REAL git-style conflict markers in a file. Line-anchored on purpose:
|
|
129
|
+
// it requires an opening `<<<<<<< ` AND a closing `>>>>>>> ` each at the start
|
|
130
|
+
// of a line. This is what merge3 emits. A naive `text.includes('<<<<<<<')`
|
|
131
|
+
// false-positives on any file that merely MENTIONS the markers (e.g. a README
|
|
132
|
+
// documenting conflict resolution), which would announce a phantom conflict on
|
|
133
|
+
// every sync. (Found by a live two-agent test, 2026-06-19.)
|
|
134
|
+
export function hasConflictMarkers(text) {
|
|
135
|
+
return /^<<<<<<< /m.test(text) && /^>>>>>>> /m.test(text)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Rewrite detection for the auto-board ---
|
|
139
|
+
// Classify a change as a small PATCH (grep-and-edit, the common case) vs a
|
|
140
|
+
// wholesale REWRITE, and pull out the symbol names the new version defines in
|
|
141
|
+
// the changed region. The sync layer uses this to AUTO-record what an agent did
|
|
142
|
+
// on a rewrite — so other agents learn of it even when the agent forgets to say.
|
|
143
|
+
// summarizeChange(base, next) -> { isRewrite, changedLines, totalLines, symbols }
|
|
144
|
+
export function summarizeChange(base, next) {
|
|
145
|
+
const b = (base || '').split('\n')
|
|
146
|
+
const n = (next || '').split('\n')
|
|
147
|
+
if (!base) return { isRewrite: false, changedLines: n.length, totalLines: n.length, symbols: symbolsIn(next || '') }
|
|
148
|
+
const r = changedRange(b, n)
|
|
149
|
+
const churn = Math.max(r.endBase - r.startBase, r.newLines.length) // lines replaced or added
|
|
150
|
+
const total = Math.max(b.length, n.length)
|
|
151
|
+
// a rewrite = at least half the file churned (and more than a couple of lines)
|
|
152
|
+
const isRewrite = total > 0 && churn >= 4 && churn / total >= 0.5
|
|
153
|
+
return { isRewrite, changedLines: churn, totalLines: total, symbols: symbolsIn(r.newLines.join('\n')) }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Best-effort "what was defined here" across common languages — for the board.
|
|
157
|
+
function symbolsIn(text) {
|
|
158
|
+
const names = new Set()
|
|
159
|
+
const add = (re) => { let m; while ((m = re.exec(text)) && names.size < 8) names.add(m[1]) }
|
|
160
|
+
add(/\bfunction\s+(\w+)/g) // function foo
|
|
161
|
+
add(/\bclass\s+(\w+)/g) // class Foo
|
|
162
|
+
add(/\bdef\s+(\w+)/g) // def foo (python)
|
|
163
|
+
add(/\b(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=/g) // const foo =
|
|
164
|
+
return [...names].slice(0, 8)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Which base line-range an edit replaced, and the replacement lines.
|
|
168
|
+
export function changedRange(base, other) {
|
|
169
|
+
let p = 0
|
|
170
|
+
while (p < base.length && p < other.length && base[p] === other[p]) p++
|
|
171
|
+
let s = 0
|
|
172
|
+
while (s < base.length - p && s < other.length - p && base[base.length - 1 - s] === other[other.length - 1 - s]) s++
|
|
173
|
+
return { startBase: p, endBase: base.length - s, newLines: other.slice(p, other.length - s) }
|
|
174
|
+
}
|
package/hive-mcp.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Hivecode MCP server — lets ANY MCP-capable AI agent join a hive room and
|
|
3
|
+
// coordinate through native tool calls (no script-running, no human setup).
|
|
4
|
+
//
|
|
5
|
+
// Register it with an MCP client (e.g. Claude Code / Claude Desktop):
|
|
6
|
+
// command: node args: ["<abs-path>/hive-mcp.js"]
|
|
7
|
+
// env (optional): HIVE_RELAY = wss://your-relay (defaults to the hosted relay)
|
|
8
|
+
//
|
|
9
|
+
// Tools exposed:
|
|
10
|
+
// hive_join join/host a room for a folder (auto kind:ai) -> returns the rules
|
|
11
|
+
// hive_say post a coordination message to the room
|
|
12
|
+
// hive_read_chat read the room conversation (read this to coordinate)
|
|
13
|
+
// hive_read_board read recent whole-file rewrites (read before editing)
|
|
14
|
+
// hive_members who is in the room
|
|
15
|
+
// hive_status current session info
|
|
16
|
+
// hive_leave leave the room
|
|
17
|
+
//
|
|
18
|
+
// This operationalizes SPEC.md: one server turns the protocol into agent-native
|
|
19
|
+
// actions. The agent's FIRST move after hive_join should be to follow the rules
|
|
20
|
+
// it returns (read chat + board before editing, announce intent, prefer patches).
|
|
21
|
+
|
|
22
|
+
import fs from 'fs'
|
|
23
|
+
import path from 'path'
|
|
24
|
+
import crypto from 'crypto'
|
|
25
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
26
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
27
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
28
|
+
import { startSync, parseLink } from './sync.js'
|
|
29
|
+
|
|
30
|
+
const DEFAULT_RELAY = process.env.HIVE_RELAY || 'wss://livecode-xoss.onrender.com'
|
|
31
|
+
let session = null // { hive, room, relay, dir, name }
|
|
32
|
+
|
|
33
|
+
const text = (s) => ({ content: [{ type: 'text', text: typeof s === 'string' ? s : JSON.stringify(s, null, 2) }] })
|
|
34
|
+
const err = (s) => ({ content: [{ type: 'text', text: s }], isError: true })
|
|
35
|
+
|
|
36
|
+
function resolveRoom(link, dir) {
|
|
37
|
+
if (link) { const { relay, room, token } = parseLink(link); return { relay: relay || DEFAULT_RELAY, room, token, mode: 'joined (link)' } }
|
|
38
|
+
const cfgPath = path.join(path.resolve(dir), '.hive.json')
|
|
39
|
+
if (fs.existsSync(cfgPath)) {
|
|
40
|
+
try { const c = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); if (c.room) return { relay: c.relay || DEFAULT_RELAY, room: c.room, token: c.token || '', mode: 'joined (.hive.json)' } } catch {}
|
|
41
|
+
}
|
|
42
|
+
return { relay: DEFAULT_RELAY, room: 'room-' + crypto.randomBytes(13).toString('base64url'), token: '', mode: 'HOSTED (new room)' }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function joinRoom({ link = '', dir = './workspace', name = `agent-${crypto.randomBytes(2).toString('hex')}`, owner = '', token = '' }) {
|
|
46
|
+
if (session) { try { session.hive.stop() } catch {} ; session = null }
|
|
47
|
+
const ROOT = path.resolve(dir)
|
|
48
|
+
fs.mkdirSync(ROOT, { recursive: true })
|
|
49
|
+
const { relay, room, token: linkToken, mode } = resolveRoom(link, dir)
|
|
50
|
+
// token precedence: explicit arg > token baked into the join link > $HIVE_TOKEN.
|
|
51
|
+
const useToken = token || linkToken || process.env.HIVE_TOKEN || ''
|
|
52
|
+
// persist the token too, so a re-join from .hive.json keeps the grant (the
|
|
53
|
+
// invitee pastes the link once; subsequent joins just read the folder config).
|
|
54
|
+
fs.writeFileSync(path.join(ROOT, '.hive.json'), JSON.stringify({ relay, room, ...(useToken ? { token: useToken } : {}) }, null, 2))
|
|
55
|
+
const hive = startSync({ relay, room, dir, name, kind: 'ai', owner, token: useToken, log: () => {} })
|
|
56
|
+
session = { hive, room, relay, dir, name }
|
|
57
|
+
// wait for the first sync so members/board/rules are ready
|
|
58
|
+
await new Promise((resolve) => {
|
|
59
|
+
let done = false
|
|
60
|
+
const finish = () => { if (!done) { done = true; resolve() } }
|
|
61
|
+
hive.provider.on('sync', (s) => s && finish())
|
|
62
|
+
setTimeout(finish, 8000) // don't hang if the relay is cold
|
|
63
|
+
})
|
|
64
|
+
hive.say(`${name} joined and is ready to work.`)
|
|
65
|
+
const rules = (() => { try { return fs.readFileSync(path.join(ROOT, 'HIVE_RULES.md'), 'utf8') } catch { return '(rules not yet written)' } })()
|
|
66
|
+
const others = hive.members().filter((m) => m.name !== name).map((m) => `${m.name}(${m.kind})`)
|
|
67
|
+
return `${mode}\nroom: ${room}\nrelay: ${relay}\nfolder: ${ROOT}\nothers here: ${others.join(', ') || 'none yet'}\ninvite link: ${relay}|${room}\n\n--- FOLLOW THESE RULES ---\n${rules}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function requireSession() { if (!session) throw new Error('not in a room — call hive_join first'); return session }
|
|
71
|
+
function chatArray() { return requireSession().hive.doc.getArray('chat').toArray() }
|
|
72
|
+
function boardMap() { const b = requireSession().hive.doc.getMap('board'); return [...b.entries()].map(([file, e]) => ({ file, ...e })) }
|
|
73
|
+
function taskList() { return [...requireSession().hive.doc.getMap('tasks').values()] }
|
|
74
|
+
const fmtTask = (t) => `${t.id} [${t.status}] ${t.by} -> ${t.to}: ${t.text}${t.decidedBy ? ` (by ${t.decidedBy})` : ''}`
|
|
75
|
+
|
|
76
|
+
// Block until there is approved work for me (or a new chat message), then return.
|
|
77
|
+
// This is how the agent reacts the INSTANT the owner approves — no polling loop,
|
|
78
|
+
// no interval to tune. Resolves immediately if approved work already exists.
|
|
79
|
+
function waitForWork(timeoutMs) {
|
|
80
|
+
const s = requireSession()
|
|
81
|
+
const tmap = s.hive.doc.getMap('tasks')
|
|
82
|
+
const chat = s.hive.doc.getArray('chat')
|
|
83
|
+
const controls = s.hive.doc.getMap('controls')
|
|
84
|
+
const me = s.name
|
|
85
|
+
const paused = () => { const c = controls.get(me); return !!(c && c.state === 'paused') }
|
|
86
|
+
const accepted = () => (paused() ? [] : [...tmap.values()].filter((t) => t.to === me && t.status === 'accepted'))
|
|
87
|
+
const baseChatLen = chat.length
|
|
88
|
+
const rx = new RegExp('@' + me.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')
|
|
89
|
+
const mentionsMe = (t) => rx.test(String(t || ''))
|
|
90
|
+
// PAUSED: stop auto-working, but STAY LISTENING. A paused agent doesn't pull its
|
|
91
|
+
// queued work — yet it still wakes the moment it's directly addressed: a new
|
|
92
|
+
// @mention in chat, or a new task assigned to it. It handles that, then calls
|
|
93
|
+
// hive_wait again (still paused) until a human resumes it.
|
|
94
|
+
if (paused()) {
|
|
95
|
+
const baseTaskIds = new Set([...tmap.values()].filter((t) => t.to === me).map((t) => t.id))
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
let done = false
|
|
98
|
+
const finish = (v) => { if (!done) { done = true; controls.unobserve(onCtl); chat.unobserve(onChat); tmap.unobserve(onTask); resolve(v) } }
|
|
99
|
+
const mentions = () => chat.toArray().slice(baseChatLen).filter((m) => mentionsMe(m.text))
|
|
100
|
+
const directTasks = () => [...tmap.values()].filter((t) => t.to === me && t.status === 'accepted' && !baseTaskIds.has(t.id))
|
|
101
|
+
const check = () => {
|
|
102
|
+
if (!paused()) return finish({ tasks: accepted(), newChat: [], resumed: true })
|
|
103
|
+
const dt = directTasks(), mn = mentions()
|
|
104
|
+
if (dt.length || mn.length) finish({ paused: true, mentioned: true, tasks: dt, newChat: mn })
|
|
105
|
+
}
|
|
106
|
+
const onCtl = check, onChat = check, onTask = check
|
|
107
|
+
controls.observe(onCtl); chat.observe(onChat); tmap.observe(onTask)
|
|
108
|
+
setTimeout(() => finish({ paused: true }), timeoutMs)
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
const ready = accepted()
|
|
112
|
+
if (ready.length) return Promise.resolve({ tasks: ready, newChat: [] })
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
let done = false
|
|
115
|
+
const finish = (v) => { if (!done) { done = true; tmap.unobserve(onTask); chat.unobserve(onChat); controls.unobserve(onCtl); resolve(v) } }
|
|
116
|
+
const onTask = () => { const a = accepted(); if (a.length) finish({ tasks: a, newChat: [] }) }
|
|
117
|
+
const onChat = () => { const extra = chat.toArray().slice(baseChatLen); if (extra.length) finish({ tasks: accepted(), newChat: extra }) }
|
|
118
|
+
const onCtl = () => { if (paused()) finish({ paused: true }) } // paused mid-wait -> stop handing out work
|
|
119
|
+
tmap.observe(onTask); chat.observe(onChat); controls.observe(onCtl)
|
|
120
|
+
setTimeout(() => finish(null), timeoutMs)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const TOOLS = [
|
|
125
|
+
{ name: 'hive_join', description: 'Join (or host) a Hivecode room for a project folder. Joins automatically as an AI participant. If a join link is given, uses it; else reads <dir>/.hive.json; else hosts a new room. Returns the room info and the HIVE_RULES you must follow.',
|
|
126
|
+
inputSchema: { type: 'object', properties: { link: { type: 'string', description: 'optional join link "wss://relay|room"' }, dir: { type: 'string', description: 'project folder to sync (default ./workspace)' }, name: { type: 'string', description: 'your display name in the room' }, owner: { type: 'string', description: 'the human responsible for you — only they may approve tasks directed at you' }, token: { type: 'string', description: 'access token for a secured room (else uses $HIVE_TOKEN). Required only if the relay runs in auth-required mode.' } } } },
|
|
127
|
+
{ name: 'hive_assign', description: 'Direct a task at another participant by name (e.g. ask another agent to work on a file). If the target is an AI with an owner, it stays PENDING until that owner approves.',
|
|
128
|
+
inputSchema: { type: 'object', properties: { to: { type: 'string', description: 'participant name to assign to' }, text: { type: 'string', description: 'what to do' } }, required: ['to', 'text'] } },
|
|
129
|
+
{ name: 'hive_read_tasks', description: 'Read tasks. Shows tasks directed at YOU (act only on accepted ones) and all pending tasks awaiting approval.',
|
|
130
|
+
inputSchema: { type: 'object', properties: {} } },
|
|
131
|
+
{ name: 'hive_wait', description: 'BLOCK until there is approved work for you (or new chat), then return it. Use this as your main loop: call hive_wait; when it returns an accepted task, do the work and call hive_complete; then call hive_wait again. Returns instantly if approved work already exists. Times out after timeoutSeconds (default 60) so you can re-call.',
|
|
132
|
+
inputSchema: { type: 'object', properties: { timeoutSeconds: { type: 'number', description: 'max seconds to wait (default 60, max 300)' } } } },
|
|
133
|
+
{ name: 'hive_approve', description: 'Approve a pending task (you must be the owner of the target AI). The AI may then act on it.',
|
|
134
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
|
|
135
|
+
{ name: 'hive_deny', description: 'Deny a pending task (you must be the owner of the target AI).',
|
|
136
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
|
|
137
|
+
{ name: 'hive_complete', description: 'Mark a task you were doing as done (optionally with a note).',
|
|
138
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' }, note: { type: 'string' } }, required: ['id'] } },
|
|
139
|
+
{ name: 'hive_say', description: 'Post a coordination message to the room chat. Use this to announce what you are about to work on BEFORE editing (e.g. "taking auth.js: adding login").',
|
|
140
|
+
inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'the message' } }, required: ['text'] } },
|
|
141
|
+
{ name: 'hive_read_chat', description: 'Read the room conversation. Read this to see what humans and other agents are doing before and while you work.',
|
|
142
|
+
inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'how many recent messages (default 30)' } } } },
|
|
143
|
+
{ name: 'hive_read_board', description: 'Read recent whole-file rewrites (who rewrote what, which symbols). Read this before editing a file — if it was just rewritten, re-read the file first.',
|
|
144
|
+
inputSchema: { type: 'object', properties: {} } },
|
|
145
|
+
{ name: 'hive_members', description: 'List who is currently in the room (humans and AI agents).', inputSchema: { type: 'object', properties: {} } },
|
|
146
|
+
{ name: 'hive_status', description: 'Show the current session (room, relay, folder, name).', inputSchema: { type: 'object', properties: {} } },
|
|
147
|
+
{ name: 'hive_leave', description: 'Leave the room and stop syncing.', inputSchema: { type: 'object', properties: {} } },
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
const server = new Server({ name: 'hivecode', version: '0.1.0' }, { capabilities: { tools: {} } })
|
|
151
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
152
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
153
|
+
const { name, arguments: args = {} } = req.params
|
|
154
|
+
try {
|
|
155
|
+
switch (name) {
|
|
156
|
+
case 'hive_join': return text(await joinRoom(args))
|
|
157
|
+
case 'hive_say': { requireSession().hive.say(String(args.text || '')); return text(`sent: ${args.text}`) }
|
|
158
|
+
case 'hive_read_chat': { const n = args.limit || 30; const msgs = chatArray().slice(-n).map((m) => `${m.at} ${m.by}(${m.kind}): ${m.text}`); return text(msgs.join('\n') || '(no messages yet)') }
|
|
159
|
+
case 'hive_read_board': { const b = boardMap(); return text(b.length ? b.map((e) => `${e.at} ${e.by} rewrote ${e.file} (${e.churn}) — touched: ${(e.symbols || []).join(', ')}`).join('\n') : '(no rewrites logged)') }
|
|
160
|
+
case 'hive_assign': { const id = requireSession().hive.assign(String(args.to || ''), String(args.text || '')); return id ? text(`assigned task ${id} to ${args.to} (pending approval if they have an owner)`) : err('need to and text') }
|
|
161
|
+
case 'hive_read_tasks': {
|
|
162
|
+
const all = taskList(); const me = requireSession().name
|
|
163
|
+
const mine = all.filter((t) => t.to === me)
|
|
164
|
+
const pending = all.filter((t) => t.status === 'pending')
|
|
165
|
+
return text(`TASKS FOR YOU (act only on 'accepted'):\n${mine.map(fmtTask).join('\n') || '(none)'}\n\nPENDING (awaiting approval):\n${pending.map(fmtTask).join('\n') || '(none)'}`)
|
|
166
|
+
}
|
|
167
|
+
case 'hive_wait': {
|
|
168
|
+
const ms = Math.min(Math.max(Number(args.timeoutSeconds) || 60, 1), 300) * 1000
|
|
169
|
+
const r = await waitForWork(ms)
|
|
170
|
+
if (r && r.paused) {
|
|
171
|
+
if (r.mentioned) {
|
|
172
|
+
const parts = []
|
|
173
|
+
if (r.newChat && r.newChat.length) parts.push(`💬 Mentioned while paused:\n${r.newChat.map((m) => `${m.by}(${m.kind}): ${m.text}`).join('\n')}`)
|
|
174
|
+
if (r.tasks && r.tasks.length) parts.push(`📌 Directed task(s):\n${r.tasks.map(fmtTask).join('\n')}`)
|
|
175
|
+
return text(`⏸ You are PAUSED, but were directly addressed. Handle ONLY this, then call hive_wait again (you stay paused until a human resumes you — don't pick up other queued work):\n\n${parts.join('\n\n')}`)
|
|
176
|
+
}
|
|
177
|
+
return text('⏸ You are PAUSED by mission control. Stop new work and wait. You will be woken if someone @mentions you or assigns you a task; otherwise just call hive_wait again.')
|
|
178
|
+
}
|
|
179
|
+
if (!r) return text('(nothing yet — no approved work or new messages in the wait window. Call hive_wait again to keep waiting.)')
|
|
180
|
+
const parts = []
|
|
181
|
+
if (r.resumed) parts.push('▶ You were RESUMED by mission control — continue.')
|
|
182
|
+
if (r.tasks.length) parts.push(`APPROVED WORK — do these now, then hive_complete each:\n${r.tasks.map(fmtTask).join('\n')}`)
|
|
183
|
+
if (r.newChat.length) parts.push(`NEW MESSAGES:\n${r.newChat.map((m) => `${m.at} ${m.by}(${m.kind}): ${m.text}`).join('\n')}`)
|
|
184
|
+
return text(parts.join('\n\n') || '(resumed — no queued work yet. Call hive_wait again.)')
|
|
185
|
+
}
|
|
186
|
+
case 'hive_approve': { const r = requireSession().hive.decide(String(args.id), true); return r.ok ? text(`approved ${args.id}`) : err(r.error) }
|
|
187
|
+
case 'hive_deny': { const r = requireSession().hive.decide(String(args.id), false); return r.ok ? text(`denied ${args.id}`) : err(r.error) }
|
|
188
|
+
case 'hive_complete': { const r = requireSession().hive.complete(String(args.id), String(args.note || '')); return r.ok ? text(`completed ${args.id}`) : err(r.error) }
|
|
189
|
+
case 'hive_members': { const now = Date.now(); return text(requireSession().hive.members().map((m) => `${m.name} (${m.kind})${m.editing && now - m.editing.at < 15000 ? ` — editing ${m.editing.file}` : ''}`).join('\n') || '(none)') }
|
|
190
|
+
case 'hive_status': { const s = requireSession(); return text({ room: s.room, relay: s.relay, dir: s.dir, name: s.name }) }
|
|
191
|
+
case 'hive_leave': { requireSession().hive.stop(); session = null; return text('left the room') }
|
|
192
|
+
default: return err(`unknown tool: ${name}`)
|
|
193
|
+
}
|
|
194
|
+
} catch (e) { return err(`error: ${e.message}`) }
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const transport = new StdioServerTransport()
|
|
198
|
+
await server.connect(transport)
|
|
199
|
+
// stderr is fine for logs; stdout is the JSON-RPC channel and must stay clean.
|
|
200
|
+
console.error('[hive-mcp] Hivecode MCP server ready (stdio). Default relay:', DEFAULT_RELAY)
|