cookbook-bridge 0.1.3 → 0.1.4
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/bridge.mjs +33 -16
- package/cookbook.mjs +8 -1
- package/device.mjs +13 -0
- package/hands.mjs +35 -2
- package/harden.mjs +39 -0
- package/local.mjs +1 -1
- package/package.json +1 -1
package/bridge.mjs
CHANGED
|
@@ -36,8 +36,8 @@ import { spawn } from "node:child_process";
|
|
|
36
36
|
// repair itself — the update path depends ONLY on update.mjs (node built-ins only).
|
|
37
37
|
// The e2e that forced this: a stale install missing volunteer.mjs couldn't even reach
|
|
38
38
|
// the updater when these were static imports.
|
|
39
|
-
let listWorkspaces, listTasks, listOpenWork, getTask, threadResumeContext, completeTaskApi, resolveDelegation, reportTaskUsage, reportTaskProgress, volunteerClaim, dispatchClaim, abandonTask, recallMemories, recallAcrossWorkspaces, creditRecall, getVolunteerSettings;
|
|
40
|
-
let agentEnv, checkGeminiVersion, isGeminiCommand, GEMINI_MIN_VERSION, checkAgyVersion, isAgyCommand, AGY_MIN_VERSION;
|
|
39
|
+
let listWorkspaces, listTasks, listOpenWork, getTask, threadResumeContext, completeTaskApi, resolveDelegation, reportTaskUsage, reportTaskProgress, volunteerClaim, dispatchClaim, abandonTask, recallMemories, recallAcrossWorkspaces, creditRecall, getVolunteerSettings, agentsQuery;
|
|
40
|
+
let agentEnv, checkGeminiVersion, isGeminiCommand, GEMINI_MIN_VERSION, checkAgyVersion, isAgyCommand, AGY_MIN_VERSION, withCookbookMcp, isClaudeCommand;
|
|
41
41
|
let extractUsage, displayText;
|
|
42
42
|
let volunteeringEnabled, volunteerCandidates, decisionPrompt, parseDecision, MAX_DECISIONS_PER_POLL, mergeVolunteerSettings, effectiveCapabilities;
|
|
43
43
|
let buildPrompt, buildThreadFollowUpPrompt;
|
|
@@ -46,15 +46,15 @@ let hasCodexThread, reapCodexServer, killCodexServer;
|
|
|
46
46
|
let checkForUpdate, applyUpdate;
|
|
47
47
|
let createLocalServer, toolsForMode, modeForTools, vendorOf;
|
|
48
48
|
let connectAgentsProgrammatic, detectClis;
|
|
49
|
-
let serveCalls, describeCall;
|
|
49
|
+
let serveCalls, describeCall, hostingMode;
|
|
50
50
|
let fetchHands, claimHandsCall, reportHandsResult;
|
|
51
51
|
|
|
52
52
|
async function loadRuntime() {
|
|
53
53
|
({ createLocalServer, toolsForMode, modeForTools, vendorOf } = await import("./local.mjs"));
|
|
54
54
|
({ connectAgentsProgrammatic, detectClis } = await import("./device.mjs"));
|
|
55
|
-
({ listWorkspaces, listTasks, listOpenWork, getTask, threadResumeContext, completeTaskApi, resolveDelegation, reportTaskUsage, reportTaskProgress, volunteerClaim, dispatchClaim, abandonTask, recallMemories, recallAcrossWorkspaces, creditRecall, getVolunteerSettings, fetchHands, claimHandsCall, reportHandsResult } = await import("./cookbook.mjs"));
|
|
56
|
-
({ serveCalls, describeCall } = await import("./hands.mjs"));
|
|
57
|
-
({ agentEnv, checkGeminiVersion, isGeminiCommand, GEMINI_MIN_VERSION, checkAgyVersion, isAgyCommand, AGY_MIN_VERSION } = await import("./harden.mjs"));
|
|
55
|
+
({ listWorkspaces, listTasks, listOpenWork, getTask, threadResumeContext, completeTaskApi, resolveDelegation, reportTaskUsage, reportTaskProgress, volunteerClaim, dispatchClaim, abandonTask, recallMemories, recallAcrossWorkspaces, creditRecall, getVolunteerSettings, fetchHands, claimHandsCall, reportHandsResult, agentsQuery } = await import("./cookbook.mjs"));
|
|
56
|
+
({ serveCalls, describeCall, hostingMode } = await import("./hands.mjs"));
|
|
57
|
+
({ agentEnv, checkGeminiVersion, isGeminiCommand, GEMINI_MIN_VERSION, checkAgyVersion, isAgyCommand, AGY_MIN_VERSION, withCookbookMcp, isClaudeCommand } = await import("./harden.mjs"));
|
|
58
58
|
({ extractUsage, displayText } = await import("./usage.mjs"));
|
|
59
59
|
({ volunteeringEnabled, volunteerCandidates, decisionPrompt, parseDecision, MAX_DECISIONS_PER_POLL, mergeVolunteerSettings, effectiveCapabilities } = await import("./volunteer.mjs"));
|
|
60
60
|
({ buildPrompt, buildThreadFollowUpPrompt } = await import("./prompt.mjs"));
|
|
@@ -164,7 +164,7 @@ function loadConfig() {
|
|
|
164
164
|
// browser, run granted verbs on this machine? Off unless explicitly enabled —
|
|
165
165
|
// `cookbook-bridge host` sets it. A Bridge that never hosts never even asks the
|
|
166
166
|
// server for calls, so this costs nothing when unused.
|
|
167
|
-
cfg.hosting = cfg.hosting ?? { enabled: false
|
|
167
|
+
cfg.hosting = cfg.hosting ?? {}; // enabled: true=always, false=off, absent=grants you approved (hands.mjs hostingMode)
|
|
168
168
|
cfg.maxAttempts = cfg.maxAttempts ?? 2;
|
|
169
169
|
// Phase 1 semantics: taskTimeoutSeconds is the ABSOLUTE CEILING (cost backstop),
|
|
170
170
|
// livenessTimeoutSeconds is the stall detector (no output for this long = dead).
|
|
@@ -176,6 +176,7 @@ function loadConfig() {
|
|
|
176
176
|
// execution let one long run block every workspace's queue).
|
|
177
177
|
cfg.maxConcurrentRuns = Math.max(1, cfg.maxConcurrentRuns ?? 2);
|
|
178
178
|
cfg.agents = (cfg.agents ?? []).filter((a) => a.enabled !== false);
|
|
179
|
+
for (const a of cfg.agents) a.cookbookUrl = cfg.cookbookUrl; // for per-run MCP pinning (spawnAgent)
|
|
179
180
|
ensureAgentPath(); // so bare `claude`/`gemini` commands resolve under the app's minimal PATH
|
|
180
181
|
loadRunState(); // restore attempts/given-up so a restart can't grant doomed tasks fresh attempts
|
|
181
182
|
return cfg;
|
|
@@ -340,7 +341,13 @@ export function resumeCommand(command, sessionId) {
|
|
|
340
341
|
|
|
341
342
|
function spawnAgent(agent, prompt, timeoutSeconds, env, onProgress, opts = {}) {
|
|
342
343
|
return new Promise((resolve, reject) => {
|
|
343
|
-
|
|
344
|
+
// IDENTITY: a claude run with a per-agent token carries its OWN Cookbook
|
|
345
|
+
// connection (--strict-mcp-config), so it acts as this Bridge's member under the
|
|
346
|
+
// agent's name — never as whatever the CLI is logged in as, and blind to stale
|
|
347
|
+
// claude.ai connectors that poison headless runs (2026-08-25).
|
|
348
|
+
const baseCommand = withCookbookMcp
|
|
349
|
+
? withCookbookMcp(opts.command ?? agent.command, { token: agent.token, cookbookUrl: agent.cookbookUrl }).command
|
|
350
|
+
: (opts.command ?? agent.command);
|
|
344
351
|
const { command, streaming } = onProgress ? streamingCommand(baseCommand) : { command: baseCommand, streaming: false };
|
|
345
352
|
const [cmd, ...rawArgs] = command;
|
|
346
353
|
const args = rawArgs.map((a) => a.replaceAll("{prompt}", prompt));
|
|
@@ -628,8 +635,9 @@ function applyConfigFromDisk(cfg) {
|
|
|
628
635
|
if (raw.cookbookUrl) cfg.cookbookUrl = String(raw.cookbookUrl).replace(/\/$/, "");
|
|
629
636
|
cfg.default = raw.default;
|
|
630
637
|
cfg.localWorkspaces = raw.localWorkspaces ?? {};
|
|
631
|
-
cfg.hosting = raw.hosting ?? {
|
|
638
|
+
cfg.hosting = raw.hosting ?? {};
|
|
632
639
|
const agents = (raw.agents ?? []).filter((a) => a.enabled !== false);
|
|
640
|
+
for (const a of agents) a.cookbookUrl = cfg.cookbookUrl;
|
|
633
641
|
cfg.agents.splice(0, cfg.agents.length, ...agents);
|
|
634
642
|
// A reload usually follows connect-agents fixing the token — let the next poll
|
|
635
643
|
// re-verify from scratch instead of staying stuck in the rejected state.
|
|
@@ -1203,7 +1211,7 @@ function noteAwaiting(awaiting) {
|
|
|
1203
1211
|
}
|
|
1204
1212
|
|
|
1205
1213
|
async function serveHands(cfg, calls) {
|
|
1206
|
-
if (
|
|
1214
|
+
if (hostingMode(cfg) === "off" || handsBusy || !calls || calls.length === 0) return;
|
|
1207
1215
|
handsBusy = true;
|
|
1208
1216
|
try {
|
|
1209
1217
|
await serveCalls(calls, {
|
|
@@ -1236,7 +1244,7 @@ async function serveHands(cfg, calls) {
|
|
|
1236
1244
|
/** Poll for granted calls (the net under the push channel, and the whole story on a
|
|
1237
1245
|
* server or network without SSE). No-ops entirely when not hosting. */
|
|
1238
1246
|
async function pollHands(cfg) {
|
|
1239
|
-
if (
|
|
1247
|
+
if (hostingMode(cfg) === "off" || !hands.supported || handsBusy) return;
|
|
1240
1248
|
try {
|
|
1241
1249
|
const r = await fetchHands(cfg);
|
|
1242
1250
|
if (!r.supported) {
|
|
@@ -1274,7 +1282,7 @@ async function socketLoop(cfg) {
|
|
|
1274
1282
|
let announced = false;
|
|
1275
1283
|
while (!sseStopped && sse.supported) {
|
|
1276
1284
|
try {
|
|
1277
|
-
const res = await fetch(`${cfg.cookbookUrl}/api/bridge/stream`, {
|
|
1285
|
+
const res = await fetch(`${cfg.cookbookUrl}/api/bridge/stream${agentsQuery(cfg)}`, {
|
|
1278
1286
|
headers: { Authorization: `Bearer ${cfg.token}` },
|
|
1279
1287
|
});
|
|
1280
1288
|
if (res.status === 404 || res.status === 405) {
|
|
@@ -1721,9 +1729,12 @@ async function main() {
|
|
|
1721
1729
|
tokenOk = true;
|
|
1722
1730
|
lastContactAt = Date.now();
|
|
1723
1731
|
log(`Connected — watching ${ws.length} workspace(s).`);
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1732
|
+
{
|
|
1733
|
+
const mode = hostingMode(cfg);
|
|
1734
|
+
if (mode === "always") log("⌂ Hosting is ON — an agent you invite can run granted checks on this machine. You'll see every step; `cookbook-bridge host --off` closes the door.");
|
|
1735
|
+
else if (mode === "grants") log("⌂ Hosting: grants you approve in Cookbook run here (every change still waits for your click). `cookbook-bridge host --off` refuses all.");
|
|
1736
|
+
else log("⌂ Hosting is OFF — no visiting agent can act on this machine. `cookbook-bridge host` opens it.");
|
|
1737
|
+
if (mode !== "off") await pollHands(cfg);
|
|
1727
1738
|
}
|
|
1728
1739
|
if (cfg.persistentThreads) {
|
|
1729
1740
|
for (const sig of ["SIGINT", "SIGTERM"]) process.on(sig, () => { killAllRunners(); killCodexServer(); process.exit(0); });
|
|
@@ -1773,7 +1784,7 @@ async function main() {
|
|
|
1773
1784
|
}
|
|
1774
1785
|
// HOSTING: granted calls ride the same cadence as work. When the push channel
|
|
1775
1786
|
// is healthy it has already delivered them; this is the net.
|
|
1776
|
-
if (cfg
|
|
1787
|
+
if (hostingMode(cfg) !== "off" && !pushHealthy) await pollHands(cfg);
|
|
1777
1788
|
// A clean poll means the token is good — clear any prior rejection so the app's
|
|
1778
1789
|
// /status flips back to connected once the user fixes it.
|
|
1779
1790
|
lastContactAt = Date.now();
|
|
@@ -1899,6 +1910,7 @@ async function doctorReport(args) {
|
|
|
1899
1910
|
cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
1900
1911
|
cfg.cookbookUrl = (cfg.cookbookUrl || "").replace(/\/$/, "");
|
|
1901
1912
|
cfg.agents = (cfg.agents ?? []).filter((a) => a.enabled !== false);
|
|
1913
|
+
for (const a of cfg.agents) a.cookbookUrl = cfg.cookbookUrl; // for per-run MCP pinning (spawnAgent)
|
|
1902
1914
|
if (!cfg.cookbookUrl || !cfg.token || String(cfg.token).startsWith("PASTE")) {
|
|
1903
1915
|
bad("Config is missing cookbookUrl or a real token", "set both in config.json (token from your Cookbook → Tokens page)");
|
|
1904
1916
|
cfg = null;
|
|
@@ -1998,6 +2010,11 @@ async function doctorReport(args) {
|
|
|
1998
2010
|
}
|
|
1999
2011
|
}
|
|
2000
2012
|
|
|
2013
|
+
if (isClaudeCommand && isClaudeCommand(agent.command)) {
|
|
2014
|
+
if (agent.token) ok(`${agent.name}: runs carry their own Cookbook connection (per-agent token) — identity is this Bridge's member`);
|
|
2015
|
+
else warn(`${agent.name}: no per-agent token — runs use the claude CLI's OWN Cookbook login, which may be a different account and inherits stale claude.ai connectors`,
|
|
2016
|
+
"run `cookbook-bridge connect` (mints a token for this agent) or add \"token\" to this agent in config.json");
|
|
2017
|
+
}
|
|
2001
2018
|
if ((agent.command || []).join(" ").includes("mcp__claude_ai_Cookbook__")) {
|
|
2002
2019
|
warn(`${agent.name}: allowedTools uses mcp__claude_ai_Cookbook__* — a CLI-added server is usually mcp__cookbook__*`,
|
|
2003
2020
|
"if tasks 'run but never complete', switch allowedTools to mcp__cookbook__*");
|
package/cookbook.mjs
CHANGED
|
@@ -239,8 +239,15 @@ export async function recallAcrossWorkspaces(cfg, query, excludeWorkspaceId, lim
|
|
|
239
239
|
// server that predates grants 404s, which every caller treats as "not hosting".
|
|
240
240
|
|
|
241
241
|
/** Pending calls (and the live grants they belong to) for THIS Bridge's token. */
|
|
242
|
+
/** `?agents=Claude,Gemini,Chef` — what this Bridge manages, so the server can say
|
|
243
|
+
* "your Bridge is running but doesn't run X" instead of "start a Bridge". */
|
|
244
|
+
export function agentsQuery(cfg) {
|
|
245
|
+
const names = (cfg?.agents ?? []).filter((a) => a && a.enabled !== false && a.name).map((a) => String(a.name));
|
|
246
|
+
return names.length ? `?agents=${encodeURIComponent(names.join(","))}` : "";
|
|
247
|
+
}
|
|
248
|
+
|
|
242
249
|
export async function fetchHands(cfg) {
|
|
243
|
-
const res = await fetch(`${cfg.cookbookUrl}/api/bridge/hands`, {
|
|
250
|
+
const res = await fetch(`${cfg.cookbookUrl}/api/bridge/hands${agentsQuery(cfg)}`, {
|
|
244
251
|
headers: { Authorization: `Bearer ${cfg.token}` },
|
|
245
252
|
});
|
|
246
253
|
if (res.status === 404) return { supported: false, calls: [], grants: [] };
|
package/device.mjs
CHANGED
|
@@ -291,6 +291,9 @@ export function configureClis(found, { baseUrl, agentTokens, cfgPath }) {
|
|
|
291
291
|
const add = spawnSync(cli.path, ["mcp", "add", "--scope", "user", "--transport", "http", "cookbook", mcpUrl, "--header", `Authorization: Bearer ${token}`], { encoding: "utf8", timeout: 30_000 });
|
|
292
292
|
if (add.status === 0) results.push({ agent: cli.agent, ok: true, detail: "connected (server 'cookbook', user scope)" });
|
|
293
293
|
else results.push({ agent: cli.agent, ok: false, detail: String(add.stderr || add.stdout || "add failed").trim().slice(0, 200) });
|
|
294
|
+
// The Bridge's OWN runs must not depend on the CLI's global server: store the
|
|
295
|
+
// token on the agent so spawnAgent pins each run to it (--strict-mcp-config).
|
|
296
|
+
if (/claude/i.test(cli.agent)) setAgentToken(cfgPath, /claude/i, token);
|
|
294
297
|
} else if (cli.kind === "file") {
|
|
295
298
|
const wrote = agyConfigure(mcpUrl, token);
|
|
296
299
|
results.push({ agent: cli.agent, ok: true, detail: `connected (${wrote})` });
|
|
@@ -314,6 +317,16 @@ export function configureClis(found, { baseUrl, agentTokens, cfgPath }) {
|
|
|
314
317
|
return results;
|
|
315
318
|
}
|
|
316
319
|
|
|
320
|
+
/** Bridge config: give the agent whose command matches `re` its attributed token. */
|
|
321
|
+
function setAgentToken(cfgPath, re, token) {
|
|
322
|
+
const raw = readConfig(cfgPath);
|
|
323
|
+
if (!raw || !Array.isArray(raw.agents)) return;
|
|
324
|
+
const agent = raw.agents.find((a) => a && re.test(String(a.command?.[0] ?? a.name ?? "")));
|
|
325
|
+
if (!agent) return;
|
|
326
|
+
agent.token = token;
|
|
327
|
+
fs.writeFileSync(cfgPath, JSON.stringify(raw, null, 2) + "\n", { mode: 0o600 });
|
|
328
|
+
}
|
|
329
|
+
|
|
317
330
|
/** Bridge config: make sure a Codex agent exists, is enabled, and carries its pieces. */
|
|
318
331
|
function enableCodexAgent(cfgPath, { binary, codexHome, token }) {
|
|
319
332
|
const raw = readConfig(cfgPath);
|
package/hands.mjs
CHANGED
|
@@ -568,14 +568,26 @@ export const RUN_TEMPLATES = Object.freeze({
|
|
|
568
568
|
|
|
569
569
|
tail_log: (params, ctx) => {
|
|
570
570
|
const n = Math.min(Math.max(parseInt(params?.lines ?? 120, 10) || 120, 10), 400);
|
|
571
|
-
|
|
571
|
+
// Where this Bridge's log actually is: `bridge.log` beside the config (terminal),
|
|
572
|
+
// the newest `*.log` beside it (a LaunchAgent), or the newest in `logs/` (the
|
|
573
|
+
// desktop app). Chef's first look at a desktop-app machine got "no bridge.log
|
|
574
|
+
// yet" for a Bridge that was logging fine (2026-08-25).
|
|
575
|
+
const dir = path.dirname(ctx.cfgPath || "");
|
|
576
|
+
const newest = (d) => {
|
|
577
|
+
try {
|
|
578
|
+
return fs.readdirSync(d).filter((f) => f.endsWith(".log")).map((f) => path.join(d, f))
|
|
579
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] ?? null;
|
|
580
|
+
} catch { return null; }
|
|
581
|
+
};
|
|
582
|
+
const logPath = [path.join(dir, "bridge.log"), newest(path.join(dir, "logs")), newest(dir)]
|
|
583
|
+
.find((p) => p && fs.existsSync(p)) ?? path.join(dir, "bridge.log");
|
|
572
584
|
return { local: () => {
|
|
573
585
|
let text = "";
|
|
574
586
|
try {
|
|
575
587
|
const buf = fs.readFileSync(logPath, "utf8");
|
|
576
588
|
text = buf.split("\n").slice(-n).join("\n");
|
|
577
589
|
} catch (e) {
|
|
578
|
-
return { error: `no
|
|
590
|
+
return { error: `no Bridge log yet (${e.code || e.message})` };
|
|
579
591
|
}
|
|
580
592
|
return { path: logPath, lines: n, text };
|
|
581
593
|
} };
|
|
@@ -922,3 +934,24 @@ export function tomlLooksValid(text) {
|
|
|
922
934
|
}
|
|
923
935
|
return !multi && depth === 0;
|
|
924
936
|
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* HOSTING MODE — may this Bridge serve a visiting agent's calls?
|
|
940
|
+
*
|
|
941
|
+
* "always" — `hosting.enabled: true` (`cookbook-bridge host`): serve any grant.
|
|
942
|
+
* "off" — `hosting.enabled: false` (`cookbook-bridge host --off`): refuse all.
|
|
943
|
+
* "grants" — nothing configured: serve the grants this member approved themselves.
|
|
944
|
+
*
|
|
945
|
+
* The third is the default ON PURPOSE (2026-08-25). A grant only exists because the
|
|
946
|
+
* host clicked Allow, in their own account, and it binds to this Bridge's own
|
|
947
|
+
* credential — that click IS the consent. Making it also require a separate "hosting"
|
|
948
|
+
* switch produced the failure Diego hit: Allow in a browser, a Bridge with the switch
|
|
949
|
+
* off, and every call sat queued until the grant expired. The Bridge's LOCAL_CEILING
|
|
950
|
+
* still holds every write at "ask", whatever the mode.
|
|
951
|
+
*/
|
|
952
|
+
export function hostingMode(cfg) {
|
|
953
|
+
const v = cfg?.hosting?.enabled;
|
|
954
|
+
if (v === true) return "always";
|
|
955
|
+
if (v === false) return "off";
|
|
956
|
+
return "grants";
|
|
957
|
+
}
|
package/harden.mjs
CHANGED
|
@@ -115,3 +115,42 @@ export async function checkAgyVersion(argv, timeoutMs = 10_000) {
|
|
|
115
115
|
const version = await probeVersion(argv, timeoutMs);
|
|
116
116
|
return { version, tooOld: version ? versionLt(version, AGY_MIN_VERSION) : false };
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
// ── identity: a run acts as the Bridge's member, not as whoever the CLI is ──────
|
|
120
|
+
//
|
|
121
|
+
// Verified failure (2026-08-25): a Bridge-run `claude -p` refused every Cookbook
|
|
122
|
+
// tool because a stale claude.ai-synced connector poisoned the headless session,
|
|
123
|
+
// and when tools did work they acted as the CLI's own login — a different account
|
|
124
|
+
// that couldn't see the workspace, so complete_task returned "Not found". Both are
|
|
125
|
+
// the same mistake: letting the run inherit the CLI's global MCP state.
|
|
126
|
+
//
|
|
127
|
+
// `--strict-mcp-config --mcp-config <json>` makes the run see ONLY Cookbook, as the
|
|
128
|
+
// member whose token this is, attributed under that token's name.
|
|
129
|
+
|
|
130
|
+
/** True when this command runs the claude CLI (any path, any wrapper flags). */
|
|
131
|
+
export function isClaudeCommand(command) {
|
|
132
|
+
const base = String(command?.[0] ?? "").split(/[\\/]/).pop().toLowerCase();
|
|
133
|
+
return base === "claude";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Rewrite a claude command so the run carries its own Cookbook connection.
|
|
138
|
+
* Pure. No-op (and says so) when it isn't claude, has no token, or the command
|
|
139
|
+
* already pins an MCP config by hand.
|
|
140
|
+
*/
|
|
141
|
+
export function withCookbookMcp(command, { token, cookbookUrl } = {}) {
|
|
142
|
+
if (!Array.isArray(command) || !isClaudeCommand(command)) return { command, injected: false, reason: "not claude" };
|
|
143
|
+
if (!token) return { command, injected: false, reason: "no token" };
|
|
144
|
+
if (!cookbookUrl) return { command, injected: false, reason: "no cookbookUrl" };
|
|
145
|
+
if (command.includes("--mcp-config") || command.includes("--strict-mcp-config")) return { command, injected: false, reason: "already pinned" };
|
|
146
|
+
const cfg = JSON.stringify({
|
|
147
|
+
mcpServers: {
|
|
148
|
+
cookbook: {
|
|
149
|
+
type: "http",
|
|
150
|
+
url: `${String(cookbookUrl).replace(/\/$/, "")}/api/mcp`,
|
|
151
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
return { command: [command[0], "--strict-mcp-config", "--mcp-config", cfg, ...command.slice(1)], injected: true, reason: null };
|
|
156
|
+
}
|
package/local.mjs
CHANGED
|
@@ -238,7 +238,7 @@ export function createLocalServer(deps) {
|
|
|
238
238
|
localWorkspaces: localWorkspacesView(),
|
|
239
239
|
// HARDWARE GRANTS (0069): is this machine currently willing to host a visiting
|
|
240
240
|
// agent, and what is live right now? The desktop app renders this as the door.
|
|
241
|
-
hosting: { enabled: !!cfg.hosting?.enabled, activeGrants: deps.activeGrants ? deps.activeGrants() : [] },
|
|
241
|
+
hosting: { enabled: !!cfg.hosting?.enabled, mode: cfg.hosting?.enabled === true ? "always" : cfg.hosting?.enabled === false ? "off" : "grants", activeGrants: deps.activeGrants ? deps.activeGrants() : [] },
|
|
242
242
|
hotWorkspaceIds: deps.hotWorkspaceIds ? [...deps.hotWorkspaceIds()] : [],
|
|
243
243
|
lastError: deps.lastError ? deps.lastError() : null,
|
|
244
244
|
connect: { state: connect.state },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cookbook-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Run your own Claude, Codex and Gemini subscriptions against your Cookbook workspaces. One approval connects every agent CLI on your machine, with a receipt for every run.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|