@chatpanel/bridge 0.10.1 → 0.10.3
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 +2 -15
- package/src/engines/claude.js +21 -21
- package/src/engines/codex.js +2 -14
- package/src/engines/custom.js +2 -15
- package/src/engines/prompt.js +28 -0
- package/src/server.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/bridge",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
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": [
|
|
@@ -15,6 +15,7 @@ import { mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
|
15
15
|
import os from 'node:os';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { findAgentBin } from '../env.js';
|
|
18
|
+
import { buildCliPrompt } from './prompt.js';
|
|
18
19
|
|
|
19
20
|
const IDLE_MS = Number(process.env.CHATPANEL_AGY_TIMEOUT_MS) || 180_000;
|
|
20
21
|
const SCRATCH = path.join(os.tmpdir(), 'chatpanel-agy-scratch');
|
|
@@ -77,20 +78,6 @@ function writeImages(images, dir) {
|
|
|
77
78
|
return files;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
// The bridge is stateless, so replay the conversation as a single prompt.
|
|
81
|
-
function buildPrompt(messages, system) {
|
|
82
|
-
let p = system ? `${system}\n\n` : '';
|
|
83
|
-
const history = messages.slice(0, -1);
|
|
84
|
-
const last = messages[messages.length - 1];
|
|
85
|
-
if (history.length) {
|
|
86
|
-
p += 'Conversation so far:\n';
|
|
87
|
-
for (const m of history) p += `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}\n\n`;
|
|
88
|
-
p += '---\n\n';
|
|
89
|
-
}
|
|
90
|
-
p += last ? last.content : '';
|
|
91
|
-
return p;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
81
|
export async function chat({ messages, system, options, images }, emit) {
|
|
95
82
|
try {
|
|
96
83
|
mkdirSync(SCRATCH, { recursive: true });
|
|
@@ -104,7 +91,7 @@ export async function chat({ messages, system, options, images }, emit) {
|
|
|
104
91
|
// read-tool approval is needed in headless `-p` mode. (Confirmed working.)
|
|
105
92
|
const imageFiles = writeImages(images, cwd);
|
|
106
93
|
const cleanup = () => imageFiles.forEach((f) => { try { unlinkSync(f); } catch { /* gone */ } });
|
|
107
|
-
let prompt =
|
|
94
|
+
let prompt = buildCliPrompt(messages, system);
|
|
108
95
|
if (imageFiles.length) {
|
|
109
96
|
prompt += `\n\nThe user attached image(s): ${imageFiles.map((f) => '@' + path.basename(f)).join(' ')}`;
|
|
110
97
|
}
|
package/src/engines/claude.js
CHANGED
|
@@ -17,7 +17,8 @@ import { spawn } from 'node:child_process';
|
|
|
17
17
|
import { writeFile, unlink } from 'node:fs/promises';
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
-
import { resolveClaude, buildSpawnSpec, isCompiledBinary } from '../env.js';
|
|
20
|
+
import { resolveClaude, buildSpawnSpec, isCompiledBinary, selfMcpStdio } from '../env.js';
|
|
21
|
+
import { buildCliPrompt } from './prompt.js';
|
|
21
22
|
|
|
22
23
|
// Write base64 data-URL images to temp files. Claude Code reads them with its
|
|
23
24
|
// Read tool (which feeds images to the model as vision), so we just reference the
|
|
@@ -79,18 +80,16 @@ export async function listModels() {
|
|
|
79
80
|
return ['opus', 'sonnet', 'haiku'];
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
prompt += last ? last.content : '';
|
|
93
|
-
return prompt;
|
|
83
|
+
export function claudeMcpConfig(mcp) {
|
|
84
|
+
if (!mcp?.url || !Array.isArray(mcp.specs) || !mcp.specs.length) return null;
|
|
85
|
+
const serverName = mcp.serverName || 'chatpanel_browser';
|
|
86
|
+
const { command, args } = selfMcpStdio(mcp.url);
|
|
87
|
+
const toolNames = [...new Set(mcp.specs.map((s) => s?.name).filter(Boolean))];
|
|
88
|
+
return {
|
|
89
|
+
serverName,
|
|
90
|
+
config: { mcpServers: { [serverName]: { command, args } } },
|
|
91
|
+
allowedTools: toolNames.map((name) => `mcp__${serverName}__${name}`),
|
|
92
|
+
};
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
// Spawn claude (however it resolves) and stream its stream-json output via
|
|
@@ -198,19 +197,20 @@ export async function chat({ messages, system, options, images }, emit) {
|
|
|
198
197
|
const tag = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
199
198
|
const args = ['--print', '--output-format', 'stream-json', '--include-partial-messages', '--verbose'];
|
|
200
199
|
|
|
201
|
-
// Browser-tools relay: ChatPanel hands this CLI the page-action tools
|
|
202
|
-
//
|
|
200
|
+
// Browser-tools relay: ChatPanel hands this CLI the page-action tools through
|
|
201
|
+
// the bridge's stdio MCP proxy, which forwards each call to the per-chat HTTP
|
|
202
|
+
// MCP session and then to the extension.
|
|
203
203
|
// options.mcp = { url, serverName, specs }. We pre-allow the tools so a headless
|
|
204
204
|
// run doesn't block on approval, and merge alongside the user's own MCP servers.
|
|
205
205
|
const mcpFiles = [];
|
|
206
206
|
const mcpAllow = [];
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
const mcpConfig = claudeMcpConfig(options.mcp);
|
|
208
|
+
if (mcpConfig) {
|
|
209
209
|
const cfgFile = path.join(os.tmpdir(), `chatpanel-mcp-${tag}.json`);
|
|
210
|
-
await writeFile(cfgFile, JSON.stringify(
|
|
210
|
+
await writeFile(cfgFile, JSON.stringify(mcpConfig.config));
|
|
211
211
|
mcpFiles.push(cfgFile);
|
|
212
212
|
args.push('--mcp-config', cfgFile);
|
|
213
|
-
|
|
213
|
+
mcpAllow.push(...mcpConfig.allowedTools);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
// Gate writes/shell behind the chosen mode; otherwise restrict to read-only
|
|
@@ -237,7 +237,7 @@ export async function chat({ messages, system, options, images }, emit) {
|
|
|
237
237
|
imageFiles.forEach((f) => unlink(f).catch(() => {}));
|
|
238
238
|
mcpFiles.forEach((f) => unlink(f).catch(() => {}));
|
|
239
239
|
};
|
|
240
|
-
let prompt =
|
|
240
|
+
let prompt = buildCliPrompt(messages);
|
|
241
241
|
if (imageFiles.length) {
|
|
242
242
|
prompt += `\n\nThe user attached ${imageFiles.length} image file(s). Use the Read tool to view ${
|
|
243
243
|
imageFiles.length === 1 ? 'it' : 'them'
|
|
@@ -323,7 +323,7 @@ async function sdkChat({ messages, system, options }, emit) {
|
|
|
323
323
|
let streamedAny = false;
|
|
324
324
|
let resultText = '';
|
|
325
325
|
const iterator = query({
|
|
326
|
-
prompt:
|
|
326
|
+
prompt: buildCliPrompt(messages),
|
|
327
327
|
options: {
|
|
328
328
|
cwd,
|
|
329
329
|
permissionMode,
|
package/src/engines/codex.js
CHANGED
|
@@ -20,6 +20,7 @@ import { existsSync, mkdirSync, symlinkSync, readFileSync } from 'node:fs';
|
|
|
20
20
|
import os from 'node:os';
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import { findAgentBin, selfMcpStdio } from '../env.js';
|
|
23
|
+
import { buildCliPrompt } from './prompt.js';
|
|
23
24
|
|
|
24
25
|
// Idle timeout: re-armed on every stdout/stderr chunk, so a long run that keeps
|
|
25
26
|
// streaming never trips it — only true silence does. Override with
|
|
@@ -97,19 +98,6 @@ export async function available() {
|
|
|
97
98
|
: { ok: false, reason: 'codex not found on PATH. Install it and run `codex login`.' };
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
function buildPrompt(messages, system) {
|
|
101
|
-
let p = system ? `${system}\n\n` : '';
|
|
102
|
-
const history = messages.slice(0, -1);
|
|
103
|
-
const last = messages[messages.length - 1];
|
|
104
|
-
if (history.length) {
|
|
105
|
-
p += 'Conversation so far:\n';
|
|
106
|
-
for (const m of history) p += `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}\n\n`;
|
|
107
|
-
p += '---\n\n';
|
|
108
|
-
}
|
|
109
|
-
p += last ? last.content : '';
|
|
110
|
-
return p;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
101
|
export function codexMcpConfigArgs(mcp) {
|
|
114
102
|
if (!mcp?.url) return [];
|
|
115
103
|
const name = mcp.serverName || 'chatpanel_browser';
|
|
@@ -247,7 +235,7 @@ export async function chat({ messages, system, options, images }, emit) {
|
|
|
247
235
|
}
|
|
248
236
|
});
|
|
249
237
|
|
|
250
|
-
child.stdin.write(
|
|
238
|
+
child.stdin.write(buildCliPrompt(messages, system));
|
|
251
239
|
child.stdin.end();
|
|
252
240
|
});
|
|
253
241
|
}
|
package/src/engines/custom.js
CHANGED
|
@@ -22,6 +22,7 @@ import path from 'node:path';
|
|
|
22
22
|
import { resolveCommand, buildSpawnSpec, selfMcpStdio } from '../env.js';
|
|
23
23
|
import { isProEntitled } from '../entitlement.js';
|
|
24
24
|
import { handleMessage } from './claude.js';
|
|
25
|
+
import { buildCliPrompt } from './prompt.js';
|
|
25
26
|
|
|
26
27
|
// Write base64 data-URL images to temp files so a custom CLI can take them via
|
|
27
28
|
// its configured `imageArg` template (e.g. "-i {path}", "@{path}"). Returns paths.
|
|
@@ -125,20 +126,6 @@ export async function listSpecModels(command, listModelsArgs, workingDir) {
|
|
|
125
126
|
return parseModelList(stdout);
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
// The bridge is stateless, so replay the conversation as a single prompt.
|
|
129
|
-
function buildPrompt(messages, system) {
|
|
130
|
-
let p = system ? `${system}\n\n` : '';
|
|
131
|
-
const history = messages.slice(0, -1);
|
|
132
|
-
const last = messages[messages.length - 1];
|
|
133
|
-
if (history.length) {
|
|
134
|
-
p += 'Conversation so far:\n';
|
|
135
|
-
for (const m of history) p += `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}\n\n`;
|
|
136
|
-
p += '---\n\n';
|
|
137
|
-
}
|
|
138
|
-
p += last ? last.content : '';
|
|
139
|
-
return p;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
129
|
function mcpToolSpecs(mcp) {
|
|
143
130
|
return (mcp?.specs || []).filter((s) => s?.name);
|
|
144
131
|
}
|
|
@@ -272,7 +259,7 @@ export async function runSpec(spec, { messages, system, options = {}, images },
|
|
|
272
259
|
throw new Error(`Couldn't find "${spec.command}". Enter its full path, or install it on your PATH (or in WSL).`);
|
|
273
260
|
}
|
|
274
261
|
|
|
275
|
-
const prompt =
|
|
262
|
+
const prompt = buildCliPrompt(messages, system);
|
|
276
263
|
let cwd = options.workingDir ? path.resolve(options.workingDir) : null;
|
|
277
264
|
const label = spec.label || spec.command;
|
|
278
265
|
const fmt = ['claude-stream-json', 'opencode-json'].includes(spec.format) ? spec.format : 'text';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function roleLabel(role) {
|
|
2
|
+
return role === 'assistant' ? 'Assistant' : 'User';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function buildCliPrompt(messages = [], system = '') {
|
|
6
|
+
const cleanSystem = String(system || '').trim();
|
|
7
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
8
|
+
const history = list.slice(0, -1);
|
|
9
|
+
const current = list[list.length - 1];
|
|
10
|
+
const parts = [];
|
|
11
|
+
|
|
12
|
+
if (cleanSystem) parts.push(cleanSystem);
|
|
13
|
+
|
|
14
|
+
if (history.length) {
|
|
15
|
+
const transcript = history
|
|
16
|
+
.map((m) => `${roleLabel(m.role)}: ${String(m.content || '').trim()}`)
|
|
17
|
+
.join('\n\n');
|
|
18
|
+
parts.push(`Conversation history (context only; do not answer these prior turns):\n${transcript}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (current) {
|
|
22
|
+
parts.push(
|
|
23
|
+
`Current ${roleLabel(current.role).toLowerCase()} message (answer this message now; use the history above only as context):\n${String(current.content || '').trim()}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return parts.join('\n\n---\n\n');
|
|
28
|
+
}
|
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.10.
|
|
33
|
+
const VERSION = '0.10.3';
|
|
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
|
|