@chatpanel/bridge 0.8.0 → 0.10.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/package.json +1 -1
- package/src/engines/antigravity.js +1 -0
- package/src/engines/claude.js +1 -0
- package/src/engines/cli-agents.js +8 -1
- package/src/engines/codex.js +1 -0
- package/src/engines/custom.js +36 -3
- package/src/server.js +32 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local bridge that exposes the AI coding agents installed on your machine — Claude Code (CLI), Codex (CLI), and Gemini CLI — to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
|
|
6
6
|
"keywords": [
|
|
@@ -115,6 +115,7 @@ export async function chat({ messages, system, options, images }, emit) {
|
|
|
115
115
|
const args = ['-p', prompt];
|
|
116
116
|
if (options.model) args.push('--model', options.model);
|
|
117
117
|
if (options.permissionMode === 'bypassPermissions') args.push('--dangerously-skip-permissions');
|
|
118
|
+
if (options.extraArgs) args.push(...String(options.extraArgs).split(/\s+/).filter(Boolean));
|
|
118
119
|
|
|
119
120
|
await new Promise((resolve, reject) => {
|
|
120
121
|
let child;
|
package/src/engines/claude.js
CHANGED
|
@@ -244,6 +244,7 @@ export async function chat({ messages, system, options, images }, emit) {
|
|
|
244
244
|
}: ${imageFiles.join(', ')}`;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
if (options.extraArgs) args.push(...String(options.extraArgs).split(/\s+/).filter(Boolean));
|
|
247
248
|
const run = runClaude({ prompt, args, cwd, emit });
|
|
248
249
|
if (run === null) {
|
|
249
250
|
cleanup(); // SDK fallback doesn't take images yet
|
|
@@ -54,10 +54,17 @@ export const pi = makeCliAgent(
|
|
|
54
54
|
export const opencode = makeCliAgent(
|
|
55
55
|
'opencode',
|
|
56
56
|
{
|
|
57
|
-
|
|
57
|
+
// `--format json` → clean NDJSON events (the default emits a TUI that's
|
|
58
|
+
// garbage when piped). --dangerously-skip-permissions so headless tool use
|
|
59
|
+
// (incl. our relayed browser tools) doesn't block on an approval prompt.
|
|
60
|
+
args: 'run --format json --dangerously-skip-permissions',
|
|
58
61
|
promptVia: 'arg',
|
|
59
62
|
modelArg: '-m {model}',
|
|
60
63
|
imageArg: '-f {path}',
|
|
64
|
+
format: 'opencode-json',
|
|
65
|
+
// Browser tools come via the bridge's STABLE /mcp endpoint, registered once
|
|
66
|
+
// with `opencode mcp add chatpanel --url http://127.0.0.1:4319/mcp` (opencode
|
|
67
|
+
// only loads MCP from its global config, not a per-run file).
|
|
61
68
|
listModelsArgs: 'models',
|
|
62
69
|
label: 'OpenCode',
|
|
63
70
|
},
|
package/src/engines/codex.js
CHANGED
|
@@ -156,6 +156,7 @@ export async function chat({ messages, system, options, images }, emit) {
|
|
|
156
156
|
args.push('-c', `mcp_servers.${name}.args=${JSON.stringify(pargs)}`);
|
|
157
157
|
}
|
|
158
158
|
if (options.model) args.push('-m', options.model);
|
|
159
|
+
if (options.extraArgs) args.push(...String(options.extraArgs).split(/\s+/).filter(Boolean));
|
|
159
160
|
for (const f of imageFiles) args.push('-i', f); // attach images to the initial prompt
|
|
160
161
|
args.push('-');
|
|
161
162
|
|
package/src/engines/custom.js
CHANGED
|
@@ -158,9 +158,9 @@ export async function runSpec(spec, { messages, system, options = {}, images },
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
const prompt = buildPrompt(messages, system);
|
|
161
|
-
|
|
161
|
+
let cwd = options.workingDir ? path.resolve(options.workingDir) : null;
|
|
162
162
|
const label = spec.label || spec.command;
|
|
163
|
-
const fmt =
|
|
163
|
+
const fmt = ['claude-stream-json', 'opencode-json'].includes(spec.format) ? spec.format : 'text';
|
|
164
164
|
|
|
165
165
|
// Args: either a real array or a space-split string. With promptVia:'arg' we
|
|
166
166
|
// substitute {prompt} (or append it if there's no placeholder); otherwise the
|
|
@@ -171,6 +171,10 @@ export async function runSpec(spec, { messages, system, options = {}, images },
|
|
|
171
171
|
: spec.args
|
|
172
172
|
? String(spec.args).split(/\s+/).filter(Boolean)
|
|
173
173
|
: [];
|
|
174
|
+
// User-supplied extra CLI flags (Settings → agent → "Extra arguments"), placed
|
|
175
|
+
// right after the base args/subcommand — e.g. opencode `run --format json
|
|
176
|
+
// --dangerously-skip-permissions`. Applies to every built-in & custom CLI agent.
|
|
177
|
+
if (options.extraArgs) args.push(...String(options.extraArgs).split(/\s+/).filter(Boolean));
|
|
174
178
|
// Inject the selected model via the agent's CONFIGURED model-arg template
|
|
175
179
|
// (e.g. "--model {model}" or, for opencode, "-m {model}" with provider/model).
|
|
176
180
|
// Without a template we can't know how this CLI takes a model, so options.model
|
|
@@ -180,7 +184,10 @@ export async function runSpec(spec, { messages, system, options = {}, images },
|
|
|
180
184
|
const injected = tmpl.includes('{model}')
|
|
181
185
|
? tmpl.replaceAll('{model}', options.model).split(/\s+/).filter(Boolean)
|
|
182
186
|
: [...tmpl.split(/\s+/).filter(Boolean), options.model];
|
|
183
|
-
|
|
187
|
+
// APPEND (not prepend): subcommand CLIs (opencode `run`, kiro `chat`) must
|
|
188
|
+
// keep the subcommand first — `opencode -m X run` makes `run` look like a
|
|
189
|
+
// project path, so it never loads opencode.json / its MCP servers.
|
|
190
|
+
args = [...args, ...injected];
|
|
184
191
|
}
|
|
185
192
|
// Images: write to temp files, expand the agent's imageArg template, then place
|
|
186
193
|
// the tokens. An explicit {images} placeholder in args wins; otherwise they go
|
|
@@ -206,6 +213,10 @@ export async function runSpec(spec, { messages, system, options = {}, images },
|
|
|
206
213
|
: [...tmpl.split(/\s+/).filter(Boolean), cfgFile];
|
|
207
214
|
args = [...tokens, ...args];
|
|
208
215
|
}
|
|
216
|
+
// NOTE: opencode only loads MCP from its GLOBAL config (~/.config/opencode),
|
|
217
|
+
// never a per-run/project file — so we can't inject it here. opencode reaches
|
|
218
|
+
// the browser tools via the bridge's STABLE /mcp endpoint, registered once with
|
|
219
|
+
// `opencode mcp add chatpanel --url http://127.0.0.1:4319/mcp`.
|
|
209
220
|
|
|
210
221
|
const imageTokens = imageTokensFor(spec.imageArg, imageFiles);
|
|
211
222
|
let placedImages = false;
|
|
@@ -285,6 +296,28 @@ export async function runSpec(spec, { messages, system, options = {}, images },
|
|
|
285
296
|
if (r.streamed) streamedAny = true;
|
|
286
297
|
if (r.result != null) resultText = r.result;
|
|
287
298
|
}
|
|
299
|
+
} else if (fmt === 'opencode-json') {
|
|
300
|
+
// opencode `run --format json` emits newline-delimited events: text parts,
|
|
301
|
+
// tool/tool_use, and errors. Extract the answer text + surface tools/errors.
|
|
302
|
+
jsonBuf += s;
|
|
303
|
+
let nl;
|
|
304
|
+
while ((nl = jsonBuf.indexOf('\n')) >= 0) {
|
|
305
|
+
const line = jsonBuf.slice(0, nl).trim();
|
|
306
|
+
jsonBuf = jsonBuf.slice(nl + 1);
|
|
307
|
+
if (!line.startsWith('{')) continue;
|
|
308
|
+
let ev;
|
|
309
|
+
try { ev = JSON.parse(line); } catch { continue; }
|
|
310
|
+
if (ev.type === 'text' && ev.part?.text) {
|
|
311
|
+
streamedAny = true;
|
|
312
|
+
emit({ type: 'delta', text: ev.part.text });
|
|
313
|
+
} else if (ev.type === 'tool' || ev.type === 'tool_use') {
|
|
314
|
+
const p = ev.part || {};
|
|
315
|
+
emit({ type: 'tool', name: p.tool || p.name || p.type || 'tool', summary: '' });
|
|
316
|
+
} else if (ev.type === 'error') {
|
|
317
|
+
const msg = ev.error?.data?.message || ev.error?.message || ev.error?.name || 'error';
|
|
318
|
+
emit({ type: 'status', text: String(msg).slice(0, 300) });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
288
321
|
} else {
|
|
289
322
|
streamedAny = true;
|
|
290
323
|
emit({ type: 'delta', text: stripAnsi(s) });
|
package/src/server.js
CHANGED
|
@@ -30,7 +30,7 @@ import { callLocalMcp } from './mcp-local.js';
|
|
|
30
30
|
// Hardcoded (not read from package.json) so it survives Bun's single-file
|
|
31
31
|
// --compile, where package.json isn't on a readable FS. CI fails the publish if
|
|
32
32
|
// this drifts from package.json, so the two can't silently diverge.
|
|
33
|
-
const VERSION = '0.
|
|
33
|
+
const VERSION = '0.10.0';
|
|
34
34
|
const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
|
|
35
35
|
const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
|
|
36
36
|
|
|
@@ -55,11 +55,13 @@ const ENGINES = {
|
|
|
55
55
|
// result POSTed back to /tool-result. The bridge itself never touches the page.
|
|
56
56
|
// --------------------------------------------------------------------------
|
|
57
57
|
const sessions = new Map(); // sessionId -> { id, emit, specs, pending: Map, nextId }
|
|
58
|
+
let latestSessionId = null; // for the stable /mcp endpoint (CLIs configured once)
|
|
58
59
|
|
|
59
60
|
function createSession(emit, specs) {
|
|
60
61
|
const id = randomUUID();
|
|
61
62
|
const s = { id, emit, specs, pending: new Map(), nextId: 0 };
|
|
62
63
|
sessions.set(id, s);
|
|
64
|
+
latestSessionId = id;
|
|
63
65
|
return s;
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -68,6 +70,17 @@ function deleteSession(id) {
|
|
|
68
70
|
if (!s) return;
|
|
69
71
|
for (const p of s.pending.values()) p.reject(new Error('chat ended'));
|
|
70
72
|
sessions.delete(id);
|
|
73
|
+
if (latestSessionId === id) {
|
|
74
|
+
// fall back to the most-recently-created surviving session, if any
|
|
75
|
+
const ids = [...sessions.keys()];
|
|
76
|
+
latestSessionId = ids.length ? ids[ids.length - 1] : null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The session a sessionless /mcp request maps to (CLIs configured once with a
|
|
81
|
+
// stable URL — e.g. `opencode mcp add chatpanel --url …/mcp`). The active chat.
|
|
82
|
+
function activeSession() {
|
|
83
|
+
return (latestSessionId && sessions.get(latestSessionId)) || null;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
// Ask the extension to run a tool and await its result. Resolves to MCP content.
|
|
@@ -228,8 +241,9 @@ async function handleChat(req, res) {
|
|
|
228
241
|
}
|
|
229
242
|
}
|
|
230
243
|
|
|
231
|
-
// POST /mcp/<session>
|
|
232
|
-
//
|
|
244
|
+
// POST /mcp/<session> (per-run, bridge-injected) OR POST /mcp (stable: routes to
|
|
245
|
+
// the active chat — for CLIs configured once, e.g. `opencode mcp add … …/mcp`).
|
|
246
|
+
// JSON-RPC; tools/call relays to the extension and waits for /tool-result.
|
|
233
247
|
async function handleMcp(req, res, sessionId) {
|
|
234
248
|
let msg;
|
|
235
249
|
try {
|
|
@@ -237,9 +251,10 @@ async function handleMcp(req, res, sessionId) {
|
|
|
237
251
|
} catch {
|
|
238
252
|
return json(res, 200, { jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
239
253
|
}
|
|
240
|
-
|
|
254
|
+
// Explicit session id (per-run URL) or the active chat (stable /mcp).
|
|
255
|
+
const session = sessionId ? sessions.get(sessionId) : activeSession();
|
|
241
256
|
const reply = (result) => {
|
|
242
|
-
if (
|
|
257
|
+
if (session) res.setHeader('Mcp-Session-Id', session.id);
|
|
243
258
|
json(res, 200, { jsonrpc: '2.0', id: msg.id ?? null, result });
|
|
244
259
|
};
|
|
245
260
|
const fail = (code, message) => json(res, 200, { jsonrpc: '2.0', id: msg.id ?? null, error: { code, message } });
|
|
@@ -254,7 +269,12 @@ async function handleMcp(req, res, sessionId) {
|
|
|
254
269
|
serverInfo: { name: 'chatpanel-browser', version: VERSION },
|
|
255
270
|
});
|
|
256
271
|
}
|
|
257
|
-
|
|
272
|
+
// No active chat → advertise zero tools rather than erroring, so a CLI with a
|
|
273
|
+
// standing /mcp config (run outside ChatPanel) starts cleanly instead of failing.
|
|
274
|
+
if (!session) {
|
|
275
|
+
if (msg.method === 'tools/list') return reply({ tools: [] });
|
|
276
|
+
return fail(-32001, 'No active ChatPanel session — open a chat with “Act on page” on.');
|
|
277
|
+
}
|
|
258
278
|
if (msg.method === 'tools/list') {
|
|
259
279
|
return reply({
|
|
260
280
|
tools: session.specs.map((s) => ({
|
|
@@ -424,6 +444,12 @@ const server = createServer(async (req, res) => {
|
|
|
424
444
|
});
|
|
425
445
|
}
|
|
426
446
|
if (req.method === 'POST' && url.pathname === '/chat') return handleChat(req, res);
|
|
447
|
+
// Stable endpoint: routes to the active chat. For CLIs configured once with a
|
|
448
|
+
// fixed URL (e.g. `opencode mcp add chatpanel --url http://127.0.0.1:4319/mcp`).
|
|
449
|
+
if (url.pathname === '/mcp') {
|
|
450
|
+
if (req.method === 'POST') return handleMcp(req, res, null);
|
|
451
|
+
if (req.method === 'GET') { res.writeHead(405); return res.end(); }
|
|
452
|
+
}
|
|
427
453
|
if (url.pathname.startsWith('/mcp/')) {
|
|
428
454
|
const sid = decodeURIComponent(url.pathname.slice(5));
|
|
429
455
|
if (req.method === 'POST') return handleMcp(req, res, sid);
|