hermes-to-claude 0.1.0 → 0.1.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.
- package/DESIGN.md +18 -18
- package/README.md +19 -19
- package/build.mjs +4 -4
- package/dist/hbridge.mjs +30 -30
- package/dist/statusline.mjs +2 -2
- package/docs/local-mode.md +15 -15
- package/docs/mcp-spec.md +85 -85
- package/package.json +2 -2
- package/scripts/setup-mcp.cjs +8 -8
package/DESIGN.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# H-Bridge (
|
|
1
|
+
# H-Bridge (h2c) Design
|
|
2
2
|
|
|
3
3
|
Hermes Bridge — local HTTP bridge from Hermes Agent to Claude Code.
|
|
4
4
|
|
|
@@ -6,11 +6,11 @@ Hermes Bridge — local HTTP bridge from Hermes Agent to Claude Code.
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
npm install
|
|
9
|
-
├─ preinstall: build → dist/
|
|
9
|
+
├─ preinstall: build → dist/h2c.mjs + dist/statusline.mjs
|
|
10
10
|
├─ install: npm deps
|
|
11
11
|
└─ postinstall: register statusLine + skill in ~/.claude configs
|
|
12
12
|
|
|
13
|
-
/
|
|
13
|
+
/h2c enable
|
|
14
14
|
└─ HTTP server on homePort(cwd)
|
|
15
15
|
├─ GET /health → {"status":"ok"}
|
|
16
16
|
├─ POST /v1/task/create → Bridge.createTask(prompt)
|
|
@@ -98,7 +98,7 @@ data: {"type":"done","exitCode":0}
|
|
|
98
98
|
### Task Lifecycle Diagram
|
|
99
99
|
|
|
100
100
|
```
|
|
101
|
-
Hermes
|
|
101
|
+
Hermes h2c:<port> Session (Claude Code)
|
|
102
102
|
│ │ │
|
|
103
103
|
│ POST /v1/task/create │ │
|
|
104
104
|
│ {"prompt":"fix bug"} │ │
|
|
@@ -153,21 +153,21 @@ Hermes hbridge:<port> Session (Claud
|
|
|
153
153
|
|
|
154
154
|
**Key is machine-global**, **port is per-directory**. This follows Claude Code's working-directory-centric model.
|
|
155
155
|
|
|
156
|
-
Claude Code reads its context — `CLAUDE.md`, project skills, `.claude/settings.json` — from the **current working directory**. Each project has its own conventions, its own skills, its own CLAUDE.md. To serve the right context,
|
|
156
|
+
Claude Code reads its context — `CLAUDE.md`, project skills, `.claude/settings.json` — from the **current working directory**. Each project has its own conventions, its own skills, its own CLAUDE.md. To serve the right context, h2c must spawn Claude Code in the right directory.
|
|
157
157
|
|
|
158
158
|
```
|
|
159
|
-
/project-a/CLAUDE.md →
|
|
160
|
-
/project-b/CLAUDE.md →
|
|
159
|
+
/project-a/CLAUDE.md → h2c :9200 → Claude Code (cwd=/project-a)
|
|
160
|
+
/project-b/CLAUDE.md → h2c :9201 → Claude Code (cwd=/project-b)
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
**Port per directory** (`9200 + MD5(cwd) % 600`) means:
|
|
164
164
|
- Each project gets a stable, predictable port
|
|
165
|
-
- You can run
|
|
165
|
+
- You can run h2c for multiple projects on the same machine without conflict
|
|
166
166
|
- The port is deterministic — Hermes computes it from the directory path without asking the server
|
|
167
167
|
|
|
168
168
|
**Key per machine** means:
|
|
169
169
|
- Hermes only needs one credential per machine, not one per project
|
|
170
|
-
- The key is random, generated once, persisted in `~/.
|
|
170
|
+
- The key is random, generated once, persisted in `~/.h2c_key`
|
|
171
171
|
- Same auth works across all directories on the same host
|
|
172
172
|
|
|
173
173
|
Key format: `hb_` + 8 base52 chars (≈ 45 bits entropy, `crypto.randomBytes(8)`, stored once).
|
|
@@ -175,19 +175,19 @@ Key format: `hb_` + 8 base52 chars (≈ 45 bits entropy, `crypto.randomBytes(8)`
|
|
|
175
175
|
## Key & Port Derivation
|
|
176
176
|
|
|
177
177
|
- **Port**: `9200 + (MD5(cwd)[0:2] % 600)` — range [9200, 9799], stable per directory
|
|
178
|
-
- **Key**: `hb_` + 8 random base52 characters — generated once, stored in `~/.
|
|
178
|
+
- **Key**: `hb_` + 8 random base52 characters — generated once, stored in `~/.h2c_key`. Same for all directories on one machine.
|
|
179
179
|
|
|
180
180
|
## Task Persistence
|
|
181
181
|
|
|
182
|
-
Completed tasks are saved to `~/.
|
|
182
|
+
Completed tasks are saved to `~/.h2c_tasks.jsonl` (JSONL, append-only, max 2000 entries). On server restart, tasks are loaded into memory. `getTaskOutput` checks session → memory cache → disk.
|
|
183
183
|
|
|
184
184
|
## Files
|
|
185
185
|
|
|
186
186
|
### Runtime (gitignored)
|
|
187
187
|
| File | Purpose |
|
|
188
188
|
|------|---------|
|
|
189
|
-
| `~/.
|
|
190
|
-
| `~/.
|
|
189
|
+
| `~/.h2c_state.json` | running/stopped flag + port + lastClientIP/lastActiveAt |
|
|
190
|
+
| `~/.h2c_tasks.jsonl` | Completed task records (JSONL, survive restart) |
|
|
191
191
|
|
|
192
192
|
### Config (written by postinstall)
|
|
193
193
|
| File | Purpose |
|
|
@@ -199,8 +199,8 @@ Completed tasks are saved to `~/.hbridge_tasks.jsonl` (JSONL, append-only, max 2
|
|
|
199
199
|
Claude Code polls ~3-5s:
|
|
200
200
|
|
|
201
201
|
```
|
|
202
|
-
▶️
|
|
203
|
-
⏹️
|
|
202
|
+
▶️ h2c: on | :<port>
|
|
203
|
+
⏹️ h2c: off
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
Liveness is determined by polling `127.0.0.1:<port>/health` — no state-file dependency.
|
|
@@ -212,7 +212,7 @@ Liveness is determined by polling `127.0.0.1:<port>/health` — no state-file de
|
|
|
212
212
|
- Old: `createTask` polled `setInterval` until previous task finished
|
|
213
213
|
- New: Session pool with promise queue — no busy-polling
|
|
214
214
|
- **Task timeout kills queue** ✅ Fixed (see #52)
|
|
215
|
-
- **Persistence**: Completed tasks saved to `~/.
|
|
215
|
+
- **Persistence**: Completed tasks saved to `~/.h2c_tasks.jsonl`, survive restart
|
|
216
216
|
- **Session class**: One Claude Code child process per task
|
|
217
217
|
- **Parallelism**: Up to 3 concurrent tasks (configurable via `maxConcurrent`)
|
|
218
218
|
|
|
@@ -221,9 +221,9 @@ Liveness is determined by polling `127.0.0.1:<port>/health` — no state-file de
|
|
|
221
221
|
- Timeout is now opt-in: pass `taskTimeoutMs` to Bridge/Session constructor
|
|
222
222
|
|
|
223
223
|
### #53 — Home mode direct enable
|
|
224
|
-
- Home mode (`
|
|
224
|
+
- Home mode (`H2C_HOME=1`) now calls `cmd_enable()` directly
|
|
225
225
|
- No longer goes through `--stdio` MCP layer
|
|
226
|
-
- `--stdio` removed —
|
|
226
|
+
- `--stdio` removed — h2c is HTTP-only
|
|
227
227
|
|
|
228
228
|
### #54+ — Permission pipeline
|
|
229
229
|
- `--dangerously-skip-permissions` replaced by Hermes-controlled permission pipeline
|
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
# Hermes-Claude-Bridge (
|
|
1
|
+
# Hermes-Claude-Bridge (h2c)
|
|
2
2
|
|
|
3
3
|
**Hermes-Agent** controls multiple **Claude Code** instances via HTTP — one agent, many Claude workers. No Pro/Max subscription required.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
📱 User ----HTTP----> 🤖 Hermes-Agent ----
|
|
7
|
-
├───
|
|
8
|
-
├───
|
|
9
|
-
└───
|
|
6
|
+
📱 User ----HTTP----> 🤖 Hermes-Agent ----h2c----> 🏭 Claude Code (deploy role)
|
|
7
|
+
├───h2c───> 🔧 Claude Code (coding role)
|
|
8
|
+
├───h2c───> 🧪 Claude Code (testing role)
|
|
9
|
+
└───h2c───> 🔬 Claude Code (building role)
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
## 1.
|
|
14
|
+
## 1. h2c Advantages
|
|
15
15
|
|
|
16
16
|
- **No Pro/Max required** — works with any Claude Code via stdio; no Anthropic subscription needed.
|
|
17
17
|
- **One agent, many Claudes** — one Hermes-Agent routes tasks to multiple Claude Code instances, each in its own project directory with its own CLAUDE.md and skills.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
### Prepare
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
h2c requires **Node.js ≥ 20**. Install it for your platform:
|
|
29
29
|
|
|
30
30
|
| Platform | Command |
|
|
31
31
|
|----------|---------|
|
|
@@ -43,24 +43,24 @@ npm install && npm run build
|
|
|
43
43
|
|
|
44
44
|
### Start in Claude Code
|
|
45
45
|
|
|
46
|
-
After installation, enable
|
|
46
|
+
After installation, enable h2c from inside Claude Code:
|
|
47
47
|
|
|
48
48
|
```
|
|
49
|
-
/
|
|
49
|
+
/h2c enable
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
This starts the HTTP server. The port is derived from the working directory (see Authentication and Port below). Check status at any time:
|
|
53
53
|
|
|
54
54
|
```
|
|
55
|
-
/
|
|
55
|
+
/h2c status
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
### Home Mode — For Headless Machines
|
|
59
59
|
|
|
60
|
-
When Hermes-Agent runs on a server with no display, no manual command is needed. Export the **environment variable** before starting
|
|
60
|
+
When Hermes-Agent runs on a server with no display, no manual command is needed. Export the **environment variable** before starting h2c:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
export
|
|
63
|
+
export H2C_HOME=1 # environment variable — auto-starts h2c
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
With Home mode active, the server listens on `127.0.0.1` only. Authentication is **disabled** — safe because only local processes can reach it. Hermes-Agent connects without managing keys.
|
|
@@ -73,17 +73,17 @@ The port is **deterministic**: `MD5(cwd)` → first 2 bytes → `9200 + (value %
|
|
|
73
73
|
- Claude reads its own CLAUDE.md, skills, and project files from that directory
|
|
74
74
|
- Port = project — you always know which Claude you're talking to
|
|
75
75
|
|
|
76
|
-
The auth key (`hb_` + 8 random base52 characters) is written to `~/.
|
|
76
|
+
The auth key (`hb_` + 8 random base52 characters) is written to `~/.h2c_key` once and reused across all directories on the same machine. All HTTP endpoints (except `/health`) require HTTP Basic Auth with username `bridge` and the key as password.
|
|
77
77
|
|
|
78
78
|
---
|
|
79
79
|
|
|
80
80
|
## 3. For Hermes-Agent
|
|
81
81
|
|
|
82
|
-
Hermes-Agent discovers and controls
|
|
82
|
+
Hermes-Agent discovers and controls h2c via HTTP. Here is the complete API.
|
|
83
83
|
|
|
84
84
|
### Security
|
|
85
85
|
|
|
86
|
-
Like Claude Code CLI,
|
|
86
|
+
Like Claude Code CLI, h2c requires authentication for all mutating endpoints. But Hermes-Agent can simplify — either handle the auth flow itself, or bypass it entirely with `skip_permissions` for trusted workloads. Hermes-Agent must handle the same authentication and permission flow that human users would, or explicitly choose to bypass it.
|
|
87
87
|
|
|
88
88
|
### Create a Task
|
|
89
89
|
|
|
@@ -145,7 +145,7 @@ curl -X POST http://<host>:<port>/v1/task/cancel \
|
|
|
145
145
|
|
|
146
146
|
### Permission Pipeline
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
h2c includes a full permission pipeline — just like CI tools. Hermes-Agent can decide per-task how to handle tool approvals:
|
|
149
149
|
|
|
150
150
|
| Mode | Behavior |
|
|
151
151
|
|------|----------|
|
|
@@ -158,7 +158,7 @@ For quick development tasks, use `skip_permissions`. For production workloads th
|
|
|
158
158
|
### Architecture
|
|
159
159
|
|
|
160
160
|
```
|
|
161
|
-
Hermes ──HTTP──▶
|
|
161
|
+
Hermes ──HTTP──▶ h2c :<port> ──spawn──▶ Session (Claude Code)
|
|
162
162
|
│ │ │
|
|
163
163
|
│ Control Layer │ server.mjs (HTTP routing) │ reads CLAUDE.md
|
|
164
164
|
│ (create/cancel/ │ bridge.mjs (session pool) │ loads skills
|
|
@@ -206,7 +206,7 @@ Event types pushed over SSE:
|
|
|
206
206
|
|
|
207
207
|
#### NDJSON Transcript
|
|
208
208
|
|
|
209
|
-
The raw Claude Code stdout is saved to `~/.
|
|
209
|
+
The raw Claude Code stdout is saved to `~/.h2c_transcript.jsonl`. Each line is a complete NDJSON message from Claude Code — `assistant` messages, `stream_event` deltas, `tool_use` blocks, and `result` messages with usage data:
|
|
210
210
|
|
|
211
211
|
```jsonl
|
|
212
212
|
{"type":"stream_event","event":{"type":"content_block_delta","delta":{"type":"text_delta","text":"Fixing"}}}
|
|
@@ -223,7 +223,7 @@ Hermes-Agent can tail this file for full Claude Code session history, or consume
|
|
|
223
223
|
| Document | Contents |
|
|
224
224
|
|----------|----------|
|
|
225
225
|
| [DESIGN.md](DESIGN.md) | Architecture, protocol, session lifecycle, permission pipeline, API reference, changelog |
|
|
226
|
-
| [docs/local-mode.md](docs/local-mode.md) |
|
|
226
|
+
| [docs/local-mode.md](docs/local-mode.md) | H2C_HOME auto-start mode |
|
|
227
227
|
| [docs/spawn-mechanism.md](docs/spawn-mechanism.md) | Claude Code spawn protocol, NDJSON format |
|
|
228
228
|
| [docs/mcp-spec.md](docs/mcp-spec.md) | MCP tool definitions (Claude Code integration) |
|
|
229
229
|
|
package/build.mjs
CHANGED
|
@@ -9,10 +9,10 @@ const SRC_DIR = path.resolve(__dirname, 'src')
|
|
|
9
9
|
// Generate version from git
|
|
10
10
|
const count = execSync("git rev-list --count HEAD", { encoding: "utf8" }).trim();
|
|
11
11
|
const hash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim();
|
|
12
|
-
const
|
|
13
|
-
console.log(`✓ version ${
|
|
12
|
+
const H2C_VERSION = `v${count}.0.${hash}`;
|
|
13
|
+
console.log(`✓ version ${H2C_VERSION}`);
|
|
14
14
|
|
|
15
|
-
// Build
|
|
15
|
+
// Build h2c CLI (dist/hbridge.mjs)
|
|
16
16
|
await esbuild.build({
|
|
17
17
|
entryPoints: [path.join(SRC_DIR, 'hbridge', 'cli.mjs')],
|
|
18
18
|
bundle: true,
|
|
@@ -21,7 +21,7 @@ await esbuild.build({
|
|
|
21
21
|
outfile: 'dist/hbridge.mjs',
|
|
22
22
|
external: ['zod', 'chalk', 'axios', '@anthropic-ai/sdk', 'qrcode', 'lodash-es', 'get-east-asian-width'],
|
|
23
23
|
define: {
|
|
24
|
-
'globalThis.
|
|
24
|
+
'globalThis.H2C_VERSION': JSON.stringify(H2C_VERSION),
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
27
|
|
package/dist/hbridge.mjs
CHANGED
|
@@ -248,7 +248,7 @@ var StdioTransport = class {
|
|
|
248
248
|
});
|
|
249
249
|
this._transcriptPath = opts.transcriptPath;
|
|
250
250
|
if (this._transcriptPath === void 0) {
|
|
251
|
-
this._transcriptPath = join(homedir(), ".
|
|
251
|
+
this._transcriptPath = join(homedir(), ".h2c_transcript.jsonl");
|
|
252
252
|
}
|
|
253
253
|
this._transcriptStream = null;
|
|
254
254
|
this._uploader = new SerialBatchEventUploader({
|
|
@@ -982,7 +982,7 @@ ${resultText}
|
|
|
982
982
|
import { readFileSync, appendFileSync, writeFileSync, existsSync } from "fs";
|
|
983
983
|
import { join as join2 } from "path";
|
|
984
984
|
import { homedir as homedir2 } from "os";
|
|
985
|
-
var TASKS_FILE = join2(homedir2(), ".
|
|
985
|
+
var TASKS_FILE = join2(homedir2(), ".h2c_tasks.jsonl");
|
|
986
986
|
var MAX_TASKS = 2e3;
|
|
987
987
|
function appendCompletedTask(task) {
|
|
988
988
|
if (!task || !task.id) return;
|
|
@@ -1051,7 +1051,7 @@ function trimCompletedTasks() {
|
|
|
1051
1051
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
1052
1052
|
import { join as join3 } from "path";
|
|
1053
1053
|
import { homedir as homedir3 } from "os";
|
|
1054
|
-
var STATE_FILE = join3(homedir3(), ".
|
|
1054
|
+
var STATE_FILE = join3(homedir3(), ".h2c_state.json");
|
|
1055
1055
|
var DEFAULT_STATE = {
|
|
1056
1056
|
running: false,
|
|
1057
1057
|
port: 9190,
|
|
@@ -1412,9 +1412,9 @@ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsS
|
|
|
1412
1412
|
import { join as join4 } from "path";
|
|
1413
1413
|
import { homedir as homedir4 } from "os";
|
|
1414
1414
|
var BASE52 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
1415
|
-
var KEY_FILE = join4(homedir4(), ".
|
|
1415
|
+
var KEY_FILE = join4(homedir4(), ".h2c_key");
|
|
1416
1416
|
function isHome() {
|
|
1417
|
-
return process.env.
|
|
1417
|
+
return process.env.H2C_HOME == 1;
|
|
1418
1418
|
}
|
|
1419
1419
|
function homePort(cwd) {
|
|
1420
1420
|
const hash = createHash("md5").update(Buffer.from(cwd, "utf-8")).digest();
|
|
@@ -1423,12 +1423,12 @@ function homePort(cwd) {
|
|
|
1423
1423
|
function homeKey(_cwd) {
|
|
1424
1424
|
if (existsSync3(KEY_FILE)) {
|
|
1425
1425
|
const existing = readFileSync3(KEY_FILE, "utf8").trim();
|
|
1426
|
-
if (existing && existing.startsWith("
|
|
1426
|
+
if (existing && existing.startsWith("h2c_")) {
|
|
1427
1427
|
return existing;
|
|
1428
1428
|
}
|
|
1429
1429
|
}
|
|
1430
1430
|
const bytes = randomBytes(8);
|
|
1431
|
-
let key = "
|
|
1431
|
+
let key = "h2c_";
|
|
1432
1432
|
for (const byte of bytes) {
|
|
1433
1433
|
key += BASE52[byte % 52];
|
|
1434
1434
|
}
|
|
@@ -1571,7 +1571,7 @@ function createServer(expectedKey, bridge = new Bridge()) {
|
|
|
1571
1571
|
function startStatusBar(port) {
|
|
1572
1572
|
function render() {
|
|
1573
1573
|
const uptime = Math.floor((Date.now() - startTime) / 6e4);
|
|
1574
|
-
process.stdout.write(`\r
|
|
1574
|
+
process.stdout.write(`\r h2c: on | port: ${port} | ${taskCount} tasks | \u2191 ${uptime}min `);
|
|
1575
1575
|
}
|
|
1576
1576
|
render();
|
|
1577
1577
|
return setInterval(render, 5e3);
|
|
@@ -1582,7 +1582,7 @@ function stopStatusBar(intervalId) {
|
|
|
1582
1582
|
|
|
1583
1583
|
// src/hbridge/cli.mjs
|
|
1584
1584
|
import { networkInterfaces } from "os";
|
|
1585
|
-
var
|
|
1585
|
+
var H2C_VERSION = "v184.0.572153f";
|
|
1586
1586
|
var server = null;
|
|
1587
1587
|
var statusBarInterval = null;
|
|
1588
1588
|
function getLocalIPs() {
|
|
@@ -1594,13 +1594,13 @@ async function cmd_enable() {
|
|
|
1594
1594
|
const key = homeKey(cwd);
|
|
1595
1595
|
const ips = getLocalIPs();
|
|
1596
1596
|
const ip = isHome() ? "127.0.0.1" : ips[0] || "127.0.0.1";
|
|
1597
|
-
console.log("
|
|
1597
|
+
console.log("h2c enabled");
|
|
1598
1598
|
console.log("\u{1F4C2} " + cwd);
|
|
1599
|
-
console.log("\u{1F511} " + ip + ":" + port + " | " + key + " | " +
|
|
1599
|
+
console.log("\u{1F511} " + ip + ":" + port + " | " + key + " | " + H2C_VERSION);
|
|
1600
1600
|
server = createServer(key);
|
|
1601
1601
|
server.on("error", (err) => {
|
|
1602
1602
|
if (err.code === "EADDRINUSE") {
|
|
1603
|
-
process.stderr.write(`Port ${port} already in use \u2014
|
|
1603
|
+
process.stderr.write(`Port ${port} already in use \u2014 h2c may already be running
|
|
1604
1604
|
`);
|
|
1605
1605
|
} else {
|
|
1606
1606
|
process.stderr.write(`Server error: ${err.message}
|
|
@@ -1617,14 +1617,14 @@ function cmd_disable() {
|
|
|
1617
1617
|
server.close(() => {
|
|
1618
1618
|
server = null;
|
|
1619
1619
|
markStopped();
|
|
1620
|
-
console.log("
|
|
1620
|
+
console.log(" h2c: off");
|
|
1621
1621
|
stopStatusBar(statusBarInterval);
|
|
1622
1622
|
process.exit(0);
|
|
1623
1623
|
});
|
|
1624
1624
|
setTimeout(() => process.exit(0), 1e3);
|
|
1625
1625
|
} else {
|
|
1626
1626
|
markStopped();
|
|
1627
|
-
console.log("
|
|
1627
|
+
console.log(" h2c: off");
|
|
1628
1628
|
process.exit(0);
|
|
1629
1629
|
}
|
|
1630
1630
|
}
|
|
@@ -1636,26 +1636,26 @@ function cmd_status() {
|
|
|
1636
1636
|
const state = readState();
|
|
1637
1637
|
const ip = isHome() ? "127.0.0.1" : ips[0] || "127.0.0.1";
|
|
1638
1638
|
const running = state.running;
|
|
1639
|
-
console.log(running ? "
|
|
1639
|
+
console.log(running ? "h2c enabled" : "h2c stopped");
|
|
1640
1640
|
console.log("\u{1F4C2} " + cwd);
|
|
1641
|
-
console.log("\u{1F511} " + ip + ":" + port + " | " + key + " | " +
|
|
1641
|
+
console.log("\u{1F511} " + ip + ":" + port + " | " + key + " | " + H2C_VERSION);
|
|
1642
1642
|
}
|
|
1643
1643
|
function showHelp() {
|
|
1644
1644
|
console.log(`
|
|
1645
|
-
|
|
1645
|
+
h2c \u2014 Hermes Bridge
|
|
1646
1646
|
|
|
1647
1647
|
COMMANDS:
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1648
|
+
h2c --enable Start bridge with deterministic key
|
|
1649
|
+
h2c --disable Stop bridge
|
|
1650
|
+
h2c --status Show status + last client connection
|
|
1651
|
+
h2c --help Show this help
|
|
1652
1652
|
|
|
1653
|
-
Port is derived from the working directory; key is machine-global from ~/.
|
|
1654
|
-
Home mode (
|
|
1653
|
+
Port is derived from the working directory; key is machine-global from ~/.h2c_key.
|
|
1654
|
+
Home mode (H2C_HOME=1) skips auth; remote mode enforces key.
|
|
1655
1655
|
|
|
1656
1656
|
EXAMPLES:
|
|
1657
|
-
|
|
1658
|
-
|
|
1657
|
+
h2c --enable Enable with dir-derived key
|
|
1658
|
+
h2c --status Show connected IP + last active time
|
|
1659
1659
|
`);
|
|
1660
1660
|
process.exit(0);
|
|
1661
1661
|
}
|
|
@@ -1669,9 +1669,9 @@ else if (cmd === "--status") cmd_status();
|
|
|
1669
1669
|
else if (cmd === "--help" || cmd === "-h") {
|
|
1670
1670
|
showHelp();
|
|
1671
1671
|
} else {
|
|
1672
|
-
console.log("
|
|
1673
|
-
console.log("
|
|
1674
|
-
console.log("
|
|
1675
|
-
console.log("
|
|
1676
|
-
console.log("
|
|
1672
|
+
console.log("h2c \u2014 Hermes Bridge");
|
|
1673
|
+
console.log(" h2c --enable Start bridge");
|
|
1674
|
+
console.log(" h2c --disable Stop bridge");
|
|
1675
|
+
console.log(" h2c --status Show status");
|
|
1676
|
+
console.log(" h2c --help Show help");
|
|
1677
1677
|
}
|
package/dist/statusline.mjs
CHANGED
|
@@ -35,8 +35,8 @@ async function main() {
|
|
|
35
35
|
const port = portFromCwd(process.cwd());
|
|
36
36
|
const alive = await livenessCheck(port);
|
|
37
37
|
const lastDir = process.cwd().split(/[/\\]/).filter(Boolean).pop() || process.cwd();
|
|
38
|
-
const hbStatus = alive ? `\u25B6\uFE0F
|
|
39
|
-
const userCmdFile = join(homedir(), ".
|
|
38
|
+
const hbStatus = alive ? `\u25B6\uFE0F h2c: on | :${port} | ${lastDir}` : `\u23F9\uFE0F h2c: off`;
|
|
39
|
+
const userCmdFile = join(homedir(), ".h2c_user_statusline_cmd");
|
|
40
40
|
let userPart = "";
|
|
41
41
|
if (existsSync(userCmdFile)) {
|
|
42
42
|
try {
|
package/docs/local-mode.md
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
#
|
|
1
|
+
# h2c Home Local Mode
|
|
2
2
|
|
|
3
|
-
Zero-config local collaboration between Hermes and Claude (OOB — Out-Of-Box). Unlike remote mode (manual `/
|
|
3
|
+
Zero-config local collaboration between Hermes and Claude (OOB — Out-Of-Box). Unlike remote mode (manual `/h2c enable` + authentication required), local mode starts **automatically, with no auth, and lives as long as Claude does**.
|
|
4
4
|
|
|
5
5
|
## Motivation
|
|
6
6
|
|
|
7
|
-
A general-purpose AI agent (Hermes) cannot remember domain specifics — experience is locked in skill files.
|
|
7
|
+
A general-purpose AI agent (Hermes) cannot remember domain specifics — experience is locked in skill files. h2c Home Local Mode lets Hermes focus on orchestration only, delegating skills, scripts, and workflows to Claude. Claude owns one task at a time with zero memory pollution.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
Hermes →
|
|
10
|
+
Hermes → H2C_HOME=1 → h2c auto-starts → compute port from cwd → connect localhost → send task → read result
|
|
11
11
|
Claude → read CLAUDE.md → load skill → execute → respond
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Trigger
|
|
15
15
|
|
|
16
|
-
Hermes sets the environment variable `
|
|
16
|
+
Hermes sets the environment variable `H2C_HOME=1` and h2c picks it up automatically — zero user action. No MCP config changes, no Claude involvement needed.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
19
|
+
H2C_HOME=1 node dist/h2c.mjs
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
When running as a standalone CLI,
|
|
22
|
+
When running as a standalone CLI, h2c auto-starts if no subcommand is given and `H2C_HOME=1`:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
|
|
26
|
-
# →
|
|
25
|
+
H2C_HOME=1 node dist/h2c.mjs
|
|
26
|
+
# → h2c enabled (auto-start)
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
## Behavioral Differences
|
|
30
30
|
|
|
31
31
|
| | Remote Mode | Home Mode |
|
|
32
32
|
|------|-------------|-----------|
|
|
33
|
-
| Startup | Manual `/
|
|
33
|
+
| Startup | Manual `/h2c enable` | **Auto-start** (calls `h2c enable` internally) |
|
|
34
34
|
| Auth | Basic Auth required (machine key) | **No auth** (localhost trust) |
|
|
35
35
|
| Port | hash(cwd) → [9200, 9799] | **hash(cwd)** → [9200, 9799] |
|
|
36
36
|
| Listen | All interfaces (0.0.0.0) | **127.0.0.1 only** |
|
|
37
37
|
| Lifecycle | Manual `--disable` | **Auto-stop on process exit** (HTTP server lives in same Node process) |
|
|
38
38
|
|
|
39
|
-
Both modes use the same machine-global key (stored in `~/.
|
|
39
|
+
Both modes use the same machine-global key (stored in `~/.h2c_key`); home mode simply skips the auth check.
|
|
40
40
|
|
|
41
41
|
## Port Mapping
|
|
42
42
|
|
|
@@ -48,11 +48,11 @@ Each working directory gets a deterministic port, stable across runs and machine
|
|
|
48
48
|
|
|
49
49
|
## Implementation
|
|
50
50
|
|
|
51
|
-
Implemented in `src/
|
|
51
|
+
Implemented in `src/h2c/home.mjs`:
|
|
52
52
|
|
|
53
|
-
1. `isHome()` — checks `
|
|
53
|
+
1. `isHome()` — checks `H2C_HOME == 1` (strict)
|
|
54
54
|
2. `homePort(cwd)` — returns `9200 + MD5(cwd)[0:2] % 600` (range [9200, 9799])
|
|
55
|
-
3. `homeKey()` — reads/generates machine-global key in `~/.
|
|
55
|
+
3. `homeKey()` — reads/generates machine-global key in `~/.h2c_key`
|
|
56
56
|
|
|
57
57
|
Server behavior when home mode is active:
|
|
58
58
|
- **HTTP server** (`server.mjs`): skips auth, listens on `127.0.0.1`
|
|
@@ -64,4 +64,4 @@ Identical to remote mode — only port derivation and auth differ.
|
|
|
64
64
|
|
|
65
65
|
## Status
|
|
66
66
|
|
|
67
|
-
**Implemented and active.** First-class feature, not experimental. See `src/
|
|
67
|
+
**Implemented and active.** First-class feature, not experimental. See `src/h2c/home.mjs` for the implementation and `tests/test_home.mjs` for test coverage.
|
package/docs/mcp-spec.md
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
# MCP Protocol Spec —
|
|
2
|
-
|
|
3
|
-
Spec: https://modelcontextprotocol.io/specification/2024-11-05
|
|
4
|
-
|
|
5
|
-
## 1. Lifecycle — Initialization
|
|
6
|
-
|
|
7
|
-
**Spec ref**: `Specification → Base Protocol → Lifecycle → Initialization`
|
|
8
|
-
|
|
9
|
-
**Sequence**:
|
|
10
|
-
```
|
|
11
|
-
Client → Server: initialize (protocolVersion + capabilities + clientInfo)
|
|
12
|
-
Server → Client: initialize response (protocolVersion + capabilities + serverInfo)
|
|
13
|
-
Client → Server: notifications/initialized (no response needed)
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
**
|
|
17
|
-
|
|
18
|
-
## 2. Capabilities — tools MUST declare listChanged
|
|
19
|
-
|
|
20
|
-
**Spec ref**: `Specification → Server Features → Tools → Capabilities`
|
|
21
|
-
|
|
22
|
-
**Spec quote**: "Servers that support tools MUST declare the tools capability"
|
|
23
|
-
```
|
|
24
|
-
{"capabilities": {"tools": {"listChanged": true}}}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
**
|
|
28
|
-
|
|
29
|
-
## 3. Tool Definition Format
|
|
30
|
-
|
|
31
|
-
**Spec ref**: `Specification → Server Features → Tools → Data Types → Tool`
|
|
32
|
-
|
|
33
|
-
**Fields**:
|
|
34
|
-
- `name` (string, required) — unique tool name
|
|
35
|
-
- `description` (string, optional) — human-readable description
|
|
36
|
-
- `inputSchema` (object, required) — JSON Schema for tool parameters
|
|
37
|
-
|
|
38
|
-
**
|
|
39
|
-
|
|
40
|
-
## 4. tools/list Response
|
|
41
|
-
|
|
42
|
-
**Spec ref**: `Specification → Server Features → Tools → Protocol Messages → Listing Tools`
|
|
43
|
-
|
|
44
|
-
**Response format**:
|
|
45
|
-
```
|
|
46
|
-
{"tools": [Tool, Tool, ...]}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**
|
|
50
|
-
|
|
51
|
-
## 5. tools/call Response
|
|
52
|
-
|
|
53
|
-
**Spec ref**: `Specification → Server Features → Tools → Data Types → Text Content`
|
|
54
|
-
|
|
55
|
-
**Format**:
|
|
56
|
-
```
|
|
57
|
-
{"content": [{"type": "text", "text": "result string"}]}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
**
|
|
61
|
-
|
|
62
|
-
## 6. Available Tools
|
|
63
|
-
|
|
64
|
-
| Tool | Description |
|
|
65
|
-
|------|-------------|
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
|
|
71
|
-
## 7. Additional Features
|
|
72
|
-
|
|
73
|
-
- **Stdout guard** (`mcp.mjs` lines 25-32): Non-JSON stdout writes are redirected to stderr to prevent third-party console.log from corrupting the MCP JSON-RPC stream.
|
|
74
|
-
- **Crash protection** (`mcp.mjs` lines 35-40): `uncaughtException` and `unhandledRejection` handlers keep the MCP process alive.
|
|
75
|
-
- **HTTP inbox server** (`mcp.mjs` lines 180-285): When `
|
|
76
|
-
- **Server info**: `serverInfo: { name: "
|
|
77
|
-
|
|
78
|
-
## 8. Validation Results
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
✅ initialize → protocolVersion:2024-11-05, capabilities.listChanged:true
|
|
82
|
-
✅ tools/list → 4 tools with name+description+inputSchema
|
|
83
|
-
✅ tools/call → content[{type:text,text:...}]
|
|
84
|
-
✅ notified/init → silent accept
|
|
85
|
-
```
|
|
1
|
+
# MCP Protocol Spec — h2c Mapping
|
|
2
|
+
|
|
3
|
+
Spec: https://modelcontextprotocol.io/specification/2024-11-05
|
|
4
|
+
|
|
5
|
+
## 1. Lifecycle — Initialization
|
|
6
|
+
|
|
7
|
+
**Spec ref**: `Specification → Base Protocol → Lifecycle → Initialization`
|
|
8
|
+
|
|
9
|
+
**Sequence**:
|
|
10
|
+
```
|
|
11
|
+
Client → Server: initialize (protocolVersion + capabilities + clientInfo)
|
|
12
|
+
Server → Client: initialize response (protocolVersion + capabilities + serverInfo)
|
|
13
|
+
Client → Server: notifications/initialized (no response needed)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**h2c implementation**: `mcp.mjs` lines 79-88
|
|
17
|
+
|
|
18
|
+
## 2. Capabilities — tools MUST declare listChanged
|
|
19
|
+
|
|
20
|
+
**Spec ref**: `Specification → Server Features → Tools → Capabilities`
|
|
21
|
+
|
|
22
|
+
**Spec quote**: "Servers that support tools MUST declare the tools capability"
|
|
23
|
+
```
|
|
24
|
+
{"capabilities": {"tools": {"listChanged": true}}}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**h2c implementation**: `mcp.mjs` line 85 — `capabilities: { tools: { listChanged: true } }`
|
|
28
|
+
|
|
29
|
+
## 3. Tool Definition Format
|
|
30
|
+
|
|
31
|
+
**Spec ref**: `Specification → Server Features → Tools → Data Types → Tool`
|
|
32
|
+
|
|
33
|
+
**Fields**:
|
|
34
|
+
- `name` (string, required) — unique tool name
|
|
35
|
+
- `description` (string, optional) — human-readable description
|
|
36
|
+
- `inputSchema` (object, required) — JSON Schema for tool parameters
|
|
37
|
+
|
|
38
|
+
**h2c implementation**: `mcp.mjs` TOOLS array, lines 292-322 — all 4 tools have name + description + inputSchema
|
|
39
|
+
|
|
40
|
+
## 4. tools/list Response
|
|
41
|
+
|
|
42
|
+
**Spec ref**: `Specification → Server Features → Tools → Protocol Messages → Listing Tools`
|
|
43
|
+
|
|
44
|
+
**Response format**:
|
|
45
|
+
```
|
|
46
|
+
{"tools": [Tool, Tool, ...]}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**h2c implementation**: `mcp.mjs` line 89 — respond({ result: { tools: TOOLS } })
|
|
50
|
+
|
|
51
|
+
## 5. tools/call Response
|
|
52
|
+
|
|
53
|
+
**Spec ref**: `Specification → Server Features → Tools → Data Types → Text Content`
|
|
54
|
+
|
|
55
|
+
**Format**:
|
|
56
|
+
```
|
|
57
|
+
{"content": [{"type": "text", "text": "result string"}]}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**h2c implementation**: `mcp.mjs` lines 161-164 — respond({ result: { content: [{ type: "text", text: t }] } })
|
|
61
|
+
|
|
62
|
+
## 6. Available Tools
|
|
63
|
+
|
|
64
|
+
| Tool | Description |
|
|
65
|
+
|------|-------------|
|
|
66
|
+
| `h2c_enable` | Start h2c HTTP server, show access key |
|
|
67
|
+
| `h2c_disable` | Stop h2c server |
|
|
68
|
+
| `h2c_status` | Show server status + last client connection info |
|
|
69
|
+
| `h2c_status_bar` | Show/hide h2c status in Claude Code status bar |
|
|
70
|
+
|
|
71
|
+
## 7. Additional Features
|
|
72
|
+
|
|
73
|
+
- **Stdout guard** (`mcp.mjs` lines 25-32): Non-JSON stdout writes are redirected to stderr to prevent third-party console.log from corrupting the MCP JSON-RPC stream.
|
|
74
|
+
- **Crash protection** (`mcp.mjs` lines 35-40): `uncaughtException` and `unhandledRejection` handlers keep the MCP process alive.
|
|
75
|
+
- **HTTP inbox server** (`mcp.mjs` lines 180-285): When `H2C_HOME=1`, the MCP server also auto-starts an HTTP server on the deterministic port.
|
|
76
|
+
- **Server info**: `serverInfo: { name: "h2c", version: "1.0.0" }` in initialize response.
|
|
77
|
+
|
|
78
|
+
## 8. Validation Results
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
✅ initialize → protocolVersion:2024-11-05, capabilities.listChanged:true
|
|
82
|
+
✅ tools/list → 4 tools with name+description+inputSchema
|
|
83
|
+
✅ tools/call → content[{type:text,text:...}]
|
|
84
|
+
✅ notified/init → silent accept
|
|
85
|
+
```
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermes-to-claude",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local HTTP bridge connecting Hermes Agent to Claude Code — no Pro/Max subscription required",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"repository": "github:xuhancn/hermes-claude
|
|
7
|
+
"repository": "github:xuhancn/hermes-to-claude",
|
|
8
8
|
"bin": {
|
|
9
9
|
"h2c": "dist/hbridge.mjs"
|
|
10
10
|
},
|
package/scripts/setup-mcp.cjs
CHANGED
|
@@ -12,14 +12,14 @@ const USER_SKILL_DIR = path.join(
|
|
|
12
12
|
process.env.HOME || os.homedir(),
|
|
13
13
|
".claude",
|
|
14
14
|
"skills",
|
|
15
|
-
"
|
|
15
|
+
"h2c",
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
19
|
// ---- statusLine hook (default ON — attaches to user's existing bar) ----
|
|
20
20
|
const STATUSBAR_PATH = path.join(__dirname, "..", "dist", "statusline.mjs");
|
|
21
21
|
const cmd = `node ${STATUSBAR_PATH}`;
|
|
22
|
-
const USER_CMD_FILE = path.join(os.homedir(), ".
|
|
22
|
+
const USER_CMD_FILE = path.join(os.homedir(), ".h2c_user_statusline_cmd");
|
|
23
23
|
|
|
24
24
|
let settings = {};
|
|
25
25
|
if (fs.existsSync(USER_SETTINGS)) {
|
|
@@ -35,19 +35,19 @@ try {
|
|
|
35
35
|
|
|
36
36
|
fs.mkdirSync(path.dirname(USER_SETTINGS), { recursive: true });
|
|
37
37
|
fs.writeFileSync(USER_SETTINGS, JSON.stringify(settings, null, 2));
|
|
38
|
-
console.log("✓
|
|
38
|
+
console.log("✓ h2c statusLine registered (attaches to existing bar)");
|
|
39
39
|
|
|
40
|
-
// ---- /
|
|
41
|
-
const SKILL_SRC = path.join(__dirname, "..", ".claude", "skills", "
|
|
40
|
+
// ---- /h2c slash command skill (global ~/.claude/skills/h2c/) ----
|
|
41
|
+
const SKILL_SRC = path.join(__dirname, "..", ".claude", "skills", "h2c", "SKILL.md");
|
|
42
42
|
const SKILL_DST = path.join(USER_SKILL_DIR, "SKILL.md");
|
|
43
43
|
|
|
44
44
|
if (fs.existsSync(SKILL_SRC)) {
|
|
45
45
|
fs.mkdirSync(USER_SKILL_DIR, { recursive: true });
|
|
46
46
|
fs.copyFileSync(SKILL_SRC, SKILL_DST);
|
|
47
|
-
console.log("✓
|
|
47
|
+
console.log("✓ h2c slash command installed (/h2c)");
|
|
48
48
|
} else {
|
|
49
|
-
console.warn("⚠
|
|
49
|
+
console.warn("⚠ h2c skill file not found — skipping slash command install");
|
|
50
50
|
}
|
|
51
51
|
} catch (e) {
|
|
52
|
-
console.error("⚠ Could not register
|
|
52
|
+
console.error("⚠ Could not register h2c in Claude Code config:", e.message);
|
|
53
53
|
}
|