cookbook-bridge 0.1.8 → 0.1.11

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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # Cookbook Bridge
2
2
 
3
+ ## Changelog
4
+
5
+ **0.1.11** (2026-09-02)
6
+ - The Bridge has a home: `~/.cookbook/config.json` (file 0600, folder 0700) for every command. `bridge.state.json`, `local.json` and `bridge.log` sit next to it. Before, the config lived next to `bridge.mjs`, so every `npx cookbook-bridge@latest` landed in a fresh cache folder and lost it. A config found next to `bridge.mjs` is copied to the home once (the old file stays) and the move is announced in one line. `--config <path>` and `COOKBOOK_CONFIG` still win.
7
+ - `connect` runs the Bridge right after the approval (pass `--no-run` to stop at "connected"). With no agent CLI found it prints the doctor instead.
8
+ - Every hint is a command that works where you are: `npx cookbook-bridge@latest <cmd>` from an npm install, `node bridge/bridge.mjs <cmd>` from a tarball.
9
+ - `doctor` gained rows for the config home (path, exists, mode), other Bridge processes on this machine (pids and config paths), Bridge Local (does the port answer), and whether the local files are behind the app deploy, with the same update line the running Bridge prints. `connect` prints that line too.
10
+ - Visiting agents can read `~/.cookbook/bridge.state.json` as a projection; `~/.cookbook/local.json` is never readable, like the desktop app's copy.
11
+
12
+ **0.1.10** (2026-09-02)
13
+ - Works on every Node from 18 up. Before, on Node older than 22.18 every command exited silently, doing nothing.
14
+ - 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.
15
+ - 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.
16
+ - Ctrl-C now hands running tasks back to the board the same way a SIGTERM does.
17
+ - When Claude reports its plan window is full, the Bridge waits for the reset instead of burning retries; that attempt is not counted.
18
+ - 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.
19
+ - Connector sync never rewrites a `~/.claude.json` it cannot parse, handles the last TOML section correctly, and escapes TOML strings.
20
+ - Bearer tokens no longer appear on agent command lines (`--mcp-config` is a private temp file now).
21
+ - A first `connect` writes a config shaped to the CLIs on your machine (default agent Claude when present, no placeholder entries).
22
+ - Workspace summaries run with no tools and without vendor API keys.
23
+ - 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.
24
+ - Live output and results are redacted on this machine before they leave it; `~/.gemini/config/mcp_config.json` is written owner-only.
25
+ - Set `COOKBOOK_NO_BROWSER=1` to stop `login`/`connect` from opening a browser (the URL is still printed).
26
+
27
+
3
28
  Runs your **own AI agents** (Claude Code, Codex, Gemini) on **your own subscriptions**,
4
29
  against your Cookbook workspaces — so tasks on the board get done by your agents
5
30
  automatically, on your machine, with **no API credits**.
@@ -19,49 +44,49 @@ Trust model: your Cookbook's `/security` page.
19
44
  ## Quick start (2 minutes)
20
45
 
21
46
  ```bash
22
- npx cookbook-bridge@latest connect # one-time: ONE approval connects the Bridge AND every
23
- # installed agent CLI (claude, codex, agy, openclaw),
24
- # each with its own attributed token
47
+ npx cookbook-bridge@latest connect # ONE approval connects the Bridge AND every installed
48
+ # agent CLI (claude, codex, agy, openclaw), each with its
49
+ # own attributed token, then RUNS the Bridge. Leave it open.
50
+ ```
51
+
52
+ That is the whole setup. Later:
53
+
54
+ ```bash
55
+ npx cookbook-bridge@latest # run it again (config is remembered in ~/.cookbook)
25
56
  npx cookbook-bridge@latest doctor # preflight: checks every prerequisite, with exact fixes
26
- npx cookbook-bridge@latest # run it (leave it running)
27
57
  ```
28
58
 
29
59
  Always `@latest`: bare `npx cookbook-bridge` happily runs a weeks-old cached copy
30
- that predates subcommands you need (`host` shipped in 0.1.1).
60
+ that predates subcommands you need (`host` shipped in 0.1.1). Your config is not in
61
+ that cache, so `@latest` never loses it.
31
62
 
32
63
  Node 18+. No dependencies, nothing to configure by hand: `connect` writes
33
- `config.json` for you and never prints or stores a secret you have to copy.
64
+ `~/.cookbook/config.json` for you and never prints or stores a secret you have to copy.
65
+ Pass `--no-run` to stop at "connected" without starting the Bridge.
34
66
 
35
67
  <details>
36
68
  <summary>Prefer no package manager? Download the tarball instead.</summary>
37
69
 
38
70
  ```bash
39
71
  curl -fsSL https://cookbook.team/api/bridge/download | tar xz
40
- node bridge/bridge.mjs connect
72
+ node bridge/bridge.mjs connect # same flow: one approval, then it runs
41
73
  ```
42
74
 
43
- The commands below are written for this layout (`node bridge/bridge.mjs <command>`);
44
- with the npm install, every one of them is `npx cookbook-bridge <command>`.
75
+ From a tarball every command is `node bridge/bridge.mjs <command>` (run it from the
76
+ folder you unpacked, the one that contains `bridge/`); with the npm install it is
77
+ `npx cookbook-bridge@latest <command>`. The Bridge knows which layout it runs from and
78
+ prints the right one in every hint. The config lives in `~/.cookbook` either way.
45
79
 
46
80
  </details>
47
81
 
48
- ```bash
49
- # from the folder you unpacked (it contains bridge/)
50
- node bridge/bridge.mjs connect-agents # one-time: ONE approval connects the Bridge
51
- # AND every installed agent CLI (claude, agy, codex)
52
- # to Cookbook, each with its own attributed token
53
- node bridge/bridge.mjs doctor # preflight: checks EVERY prerequisite, prints
54
- # the exact fix for anything that's missing
55
- node bridge/bridge.mjs # run it (leave it running)
56
- ```
57
-
58
- No hand-pasting tokens: `connect-agents` is the intended path (device flow, like a TV
59
- app; the code expires in ~10 minutes — just re-run it if it lapses). One browser click
60
- authorizes the Bridge **and** mints a named token per detected agent CLI — the name is
61
- the attribution label ("Claude · via you") — then configures each CLI via its own
62
- `mcp add`. Prefer just the Bridge? `login` does the device flow without touching your
63
- CLIs. Fully manual: copy `config.example.json` → `config.json` and paste a token from
64
- your Cookbook **Account → Tokens** page.
82
+ No hand-pasting tokens: `connect` is the intended path (device flow, like a TV app; the
83
+ code expires in ~10 minutes, just re-run it if it lapses). One browser click authorizes
84
+ the Bridge **and** mints a named token per detected agent CLI (the name is the
85
+ attribution label, "Claude · via you"), then configures each CLI via its own `mcp add`.
86
+ `connect-agents` is the same command under its original name. Prefer just the Bridge?
87
+ `login` does the device flow without touching your CLIs. Fully manual: copy
88
+ `config.example.json` to `~/.cookbook/config.json` and paste a token from your Cookbook
89
+ **Account Tokens** page.
65
90
 
66
91
  Requires **Node 18+** (built-in `fetch`, no npm install) and at least one agent CLI
67
92
  installed and logged in (`claude`, `agy` — the Antigravity CLI for Gemini — or the Codex app). Each agent must also be
@@ -70,15 +95,33 @@ you exactly which parts are ready and how to fix the rest.
70
95
 
71
96
  ## Commands
72
97
 
98
+ Written as `npx cookbook-bridge@latest <command>`; from a tarball, `node bridge/bridge.mjs <command>`.
99
+
73
100
  | Command | What it does |
74
101
  |---|---|
75
- | `node bridge.mjs` | Run the Bridge (uses `config.json` next to it; or pass a path) |
76
- | `node bridge.mjs connect-agents` | One approval connects the Bridge + every installed agent CLI (attributed tokens) |
77
- | `node bridge.mjs login` | Device-flow auth for the Bridge only, writes your config |
78
- | `node bridge.mjs doctor` | Preflight every prerequisite with exact fixes |
79
- | `node bridge.mjs status` | Liveness + agent readiness |
80
- | `node bridge.mjs update` | Update the Bridge to match the app (see Self-updating) |
81
- | `cookbook-bridge host` | Open the door: let an agent someone else runs help you set this machine up, inside a grant you approve. `--off` closes it. |
102
+ | (none) | Run the Bridge (uses `~/.cookbook/config.json`; or pass a path) |
103
+ | `connect` | One approval connects the Bridge + every installed agent CLI (attributed tokens), then runs the Bridge. `--no-run` stops at connected. `connect-agents` is the same command. |
104
+ | `login` | Device-flow auth for the Bridge only, writes your config |
105
+ | `doctor` | Preflight every prerequisite with exact fixes: config home, token, agents, other Bridge processes, Bridge Local, and whether the files are behind the app |
106
+ | `status` | Liveness + agent readiness |
107
+ | `update` | Update the Bridge to match the app (see Self-updating) |
108
+ | `host` | Open the door: let an agent someone else runs help you set this machine up, inside a grant you approve. `--off` closes it. |
109
+ | `connectors` | Survey MCP connectors across Claude, Codex and Gemini; `sync` gives every agent the same tools; `approve <name>` installs a team stdio connector |
110
+
111
+ ## Where things live (the config home)
112
+
113
+ Every command resolves the config the same way, first match wins:
114
+
115
+ 1. `--config <path>` (or, for the run command, a positional path)
116
+ 2. `COOKBOOK_CONFIG` in the environment
117
+ 3. `~/.cookbook/config.json` (folder 0700, file 0600)
118
+ 4. A `config.json` next to `bridge.mjs`, only when 3 does not exist yet. It is copied to
119
+ the home once, the old file is left in place, and one line says where the config
120
+ now lives (pre-0.1.11 installs kept it next to the code).
121
+
122
+ `bridge.state.json` (attempt counters), `local.json` (the Bridge Local loopback port and
123
+ token) and `bridge.log` sit in the same folder as whichever config is in use. The desktop
124
+ app passes its own `--config` path and keeps its files in its data folder.
82
125
 
83
126
  ## Self-updating
84
127
 
@@ -86,8 +129,11 @@ you exactly which parts are ready and how to fix the rest.
86
129
  to the deploy's manifest (`/api/bridge/manifest`) and — with `"autoUpdate": true`, the
87
130
  default — replaces them (every file hash-verified first, originals kept in
88
131
  a per-version `bridge.backup/<deploy>/` dir, your `config.json` and token never touched) and restarts itself.
89
- Set `"autoUpdate": false` to pin your version; `node bridge.mjs update` updates manually
90
- and works even from a broken install.
132
+ Set `"autoUpdate": false` to pin your version; `node bridge/bridge.mjs update` updates
133
+ manually and works even from a broken install. An npm install is updated by npm: re-run
134
+ `npx cookbook-bridge@latest`, and your config in `~/.cookbook` comes along untouched.
135
+ `doctor` and `connect` run the same comparison and print the same update line when the
136
+ local files are behind.
91
137
 
92
138
  ## Agents (config.json)
93
139
 
@@ -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
+ }