cookbook-bridge 0.1.7 → 0.1.10
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/README.md +27 -6
- package/approve-mcp.mjs +121 -0
- package/bridge.mjs +559 -56
- package/codex-runner.mjs +54 -10
- package/config.example.json +2 -5
- package/connectors.mjs +235 -22
- package/cookbook.mjs +20 -3
- package/device.mjs +70 -21
- package/hands.mjs +169 -13
- package/harden.mjs +71 -0
- package/hook-reporter.mjs +43 -0
- package/local.mjs +17 -2
- package/package.json +8 -2
- package/plan.mjs +125 -0
- package/realtime.mjs +122 -0
- package/sessions.mjs +187 -0
- package/synthesis.mjs +328 -0
- package/thread-runner.mjs +42 -10
package/README.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Cookbook Bridge
|
|
2
2
|
|
|
3
|
+
## Changelog
|
|
4
|
+
|
|
5
|
+
**0.1.10** (2026-09-02)
|
|
6
|
+
- Works on every Node from 18 up. Before, on Node older than 22.18 every command exited silently, doing nothing.
|
|
7
|
+
- The npm package and the desktop app now ship every runtime file (realtime, sessions, synthesis, the approval relay, the hook reporter). Install-time crashes on import are gone, and a test now fails the build if a file is left out.
|
|
8
|
+
- Prompts reach Claude on stdin, never on the command line. On Windows a prompt is never routed through a `.cmd` shim and cmd.exe; the Bridge runs the shim's node script directly or refuses with the fix.
|
|
9
|
+
- Ctrl-C now hands running tasks back to the board the same way a SIGTERM does.
|
|
10
|
+
- When Claude reports its plan window is full, the Bridge waits for the reset instead of burning retries; that attempt is not counted.
|
|
11
|
+
- Team connectors that run a command (stdio) are no longer installed automatically. They wait for `cookbook-bridge connectors approve <name>`; `connectors pending` lists them. URL connectors sync as before.
|
|
12
|
+
- Connector sync never rewrites a `~/.claude.json` it cannot parse, handles the last TOML section correctly, and escapes TOML strings.
|
|
13
|
+
- Bearer tokens no longer appear on agent command lines (`--mcp-config` is a private temp file now).
|
|
14
|
+
- A first `connect` writes a config shaped to the CLIs on your machine (default agent Claude when present, no placeholder entries).
|
|
15
|
+
- Workspace summaries run with no tools and without vendor API keys.
|
|
16
|
+
- Captions, image descriptions and answers now run on your subscription too, through the same lane as summaries. An image job downloads the picture into a private temp folder, gives Claude read access to that one file only, and deletes the folder when the run ends. Synthesis jobs that arrive while another is running now wait their turn instead of being dropped.
|
|
17
|
+
- Live output and results are redacted on this machine before they leave it; `~/.gemini/config/mcp_config.json` is written owner-only.
|
|
18
|
+
- Set `COOKBOOK_NO_BROWSER=1` to stop `login`/`connect` from opening a browser (the URL is still printed).
|
|
19
|
+
|
|
20
|
+
|
|
3
21
|
Runs your **own AI agents** (Claude Code, Codex, Gemini) on **your own subscriptions**,
|
|
4
22
|
against your Cookbook workspaces — so tasks on the board get done by your agents
|
|
5
23
|
automatically, on your machine, with **no API credits**.
|
|
@@ -19,13 +37,16 @@ Trust model: your Cookbook's `/security` page.
|
|
|
19
37
|
## Quick start (2 minutes)
|
|
20
38
|
|
|
21
39
|
```bash
|
|
22
|
-
npx cookbook-bridge connect # one-time: ONE approval connects the Bridge AND every
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
npx cookbook-bridge doctor # preflight: checks every prerequisite, with exact fixes
|
|
26
|
-
npx cookbook-bridge # run it (leave it running)
|
|
40
|
+
npx cookbook-bridge@latest connect # one-time: ONE approval connects the Bridge AND every
|
|
41
|
+
# installed agent CLI (claude, codex, agy, openclaw),
|
|
42
|
+
# each with its own attributed token
|
|
43
|
+
npx cookbook-bridge@latest doctor # preflight: checks every prerequisite, with exact fixes
|
|
44
|
+
npx cookbook-bridge@latest # run it (leave it running)
|
|
27
45
|
```
|
|
28
46
|
|
|
47
|
+
Always `@latest`: bare `npx cookbook-bridge` happily runs a weeks-old cached copy
|
|
48
|
+
that predates subcommands you need (`host` shipped in 0.1.1).
|
|
49
|
+
|
|
29
50
|
Node 18+. No dependencies, nothing to configure by hand: `connect` writes
|
|
30
51
|
`config.json` for you and never prints or stores a secret you have to copy.
|
|
31
52
|
|
|
@@ -154,7 +175,7 @@ Setting up agents is the one thing you can't ask your agents to do, because they
|
|
|
154
175
|
what's broken. So a **hardware grant** lets someone else's agent do it:
|
|
155
176
|
|
|
156
177
|
```bash
|
|
157
|
-
npx cookbook-bridge host # open the door (Node + one browser approval; no agents needed)
|
|
178
|
+
npx cookbook-bridge@latest host # open the door (Node + one browser approval; no agents needed)
|
|
158
179
|
```
|
|
159
180
|
|
|
160
181
|
Then invite one from Cookbook. What that actually means:
|
package/approve-mcp.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* THE PERMISSION RELAY (0086) — Claude Code's --permission-prompt-tool server.
|
|
4
|
+
*
|
|
5
|
+
* In an ask-mode folder, Claude Code pre-approves only reads; any other tool call
|
|
6
|
+
* makes the CLI ask THIS process (a one-tool MCP server over stdio, spawned by
|
|
7
|
+
* the CLI itself). We redact the request ON the machine, POST it to Cookbook as
|
|
8
|
+
* a pending approval, poll for the owner's click, and answer the CLI with
|
|
9
|
+
* {"behavior":"allow"|"deny"}. No decision in 4.5 minutes = deny — the fail-safe
|
|
10
|
+
* direction, matching the row's 5-minute server expiry.
|
|
11
|
+
*
|
|
12
|
+
* Standalone on purpose: newline-delimited JSON-RPC, zero dependencies beyond
|
|
13
|
+
* the redaction corpus it shares with everything else that leaves this machine.
|
|
14
|
+
*/
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { redact } from "./hands.mjs";
|
|
18
|
+
|
|
19
|
+
const URL_BASE = String(process.env.CBK_URL ?? "").replace(/\/$/, "");
|
|
20
|
+
const TOKEN = process.env.CBK_TOKEN ?? "";
|
|
21
|
+
const WORKSPACE = process.env.CBK_WORKSPACE ?? "";
|
|
22
|
+
const TASK = process.env.CBK_TASK ?? "";
|
|
23
|
+
const FOLDER = process.env.CBK_FOLDER ?? "";
|
|
24
|
+
const AGENT = process.env.CBK_AGENT ?? "Claude";
|
|
25
|
+
const POLL_MS = 2000;
|
|
26
|
+
const WAIT_MS = 4.5 * 60_000;
|
|
27
|
+
|
|
28
|
+
const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
|
|
29
|
+
|
|
30
|
+
/** One line a human can judge: the command for Bash, the path for edits. */
|
|
31
|
+
export function summarize(toolName, input) {
|
|
32
|
+
try {
|
|
33
|
+
const i = input && typeof input === "object" ? input : {};
|
|
34
|
+
const core = toolName === "Bash" ? String(i.command ?? "")
|
|
35
|
+
: (i.file_path ?? i.notebook_path ?? i.path ?? i.url ?? JSON.stringify(i));
|
|
36
|
+
return redact(String(core)).slice(0, 500);
|
|
37
|
+
} catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The CLI contract: a JSON string in the tool result's text content. */
|
|
43
|
+
export function verdictPayload(status) {
|
|
44
|
+
if (status === "allowed") return { behavior: "allow", updatedInput: undefined };
|
|
45
|
+
return { behavior: "deny", message: status === "expired" ? "Timed out waiting for approval in Cookbook (5 minutes). Ask again if still needed." : "Denied by the owner in Cookbook." };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function decide(toolName, input) {
|
|
49
|
+
if (!URL_BASE || !TOKEN || !WORKSPACE) return { behavior: "deny", message: "Approval relay is not configured." };
|
|
50
|
+
const res = await fetch(`${URL_BASE}/api/bridge/approvals`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${TOKEN}` },
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
workspace_id: WORKSPACE,
|
|
55
|
+
task_id: TASK || undefined,
|
|
56
|
+
folder: FOLDER || undefined,
|
|
57
|
+
agent: AGENT,
|
|
58
|
+
tool_name: String(toolName ?? "tool").slice(0, 60),
|
|
59
|
+
input_summary: summarize(toolName, input),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
if (res.status === 403) return { behavior: "deny", message: "The drive layer isn't enabled for this account." };
|
|
63
|
+
if (!res.ok) return { behavior: "deny", message: `Approval relay error (${res.status}).` };
|
|
64
|
+
const { id } = await res.json();
|
|
65
|
+
const until = Date.now() + WAIT_MS;
|
|
66
|
+
while (Date.now() < until) {
|
|
67
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
68
|
+
try {
|
|
69
|
+
const poll = await fetch(`${URL_BASE}/api/bridge/approvals/${id}`, { headers: { Authorization: `Bearer ${TOKEN}` } });
|
|
70
|
+
if (!poll.ok) continue;
|
|
71
|
+
const { status } = await poll.json();
|
|
72
|
+
if (status && status !== "pending") return verdictPayload(status);
|
|
73
|
+
} catch { /* transient; keep polling */ }
|
|
74
|
+
}
|
|
75
|
+
return verdictPayload("expired");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── minimal MCP stdio server: initialize, tools/list, tools/call(approve) ────
|
|
79
|
+
const TOOL = {
|
|
80
|
+
name: "approve",
|
|
81
|
+
description: "Ask the owner in Cookbook whether this tool call may run.",
|
|
82
|
+
inputSchema: { type: "object", properties: { tool_name: { type: "string" }, input: { type: "object" }, tool_use_id: { type: "string" } } },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Start the server ONLY when run as the entry script — importers (tests) get the
|
|
86
|
+
// pure exports without a live stdin listener holding their process open.
|
|
87
|
+
// `import.meta.main` is undefined before Node 22.18 / 24.2; without the argv[1]
|
|
88
|
+
// fallback the relay never started on older Nodes and every ask-mode call was denied.
|
|
89
|
+
const IS_MAIN = import.meta.main === true
|
|
90
|
+
|| (import.meta.main !== false && !!process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url));
|
|
91
|
+
let buf = "";
|
|
92
|
+
if (IS_MAIN) {
|
|
93
|
+
process.stdin.setEncoding("utf8");
|
|
94
|
+
process.stdin.on("data", (chunk) => {
|
|
95
|
+
buf += chunk;
|
|
96
|
+
let nl;
|
|
97
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
98
|
+
const line = buf.slice(0, nl).trim();
|
|
99
|
+
buf = buf.slice(nl + 1);
|
|
100
|
+
if (!line) continue;
|
|
101
|
+
let msg;
|
|
102
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
103
|
+
void handle(msg);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function handle(msg) {
|
|
109
|
+
const { id, method, params } = msg ?? {};
|
|
110
|
+
if (method === "initialize") {
|
|
111
|
+
return send({ jsonrpc: "2.0", id, result: { protocolVersion: params?.protocolVersion ?? "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "cbapprove", version: "1" } } });
|
|
112
|
+
}
|
|
113
|
+
if (method === "tools/list") return send({ jsonrpc: "2.0", id, result: { tools: [TOOL] } });
|
|
114
|
+
if (method === "tools/call" && params?.name === "approve") {
|
|
115
|
+
const a = params.arguments ?? {};
|
|
116
|
+
let verdict;
|
|
117
|
+
try { verdict = await decide(a.tool_name, a.input); } catch (e) { verdict = { behavior: "deny", message: `Relay failed: ${e.message}` }; }
|
|
118
|
+
return send({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(verdict) }] } });
|
|
119
|
+
}
|
|
120
|
+
if (id !== undefined && method) return send({ jsonrpc: "2.0", id, error: { code: -32601, message: `unknown method ${method}` } });
|
|
121
|
+
}
|