@chatpanel/bridge 0.10.32 → 0.10.34
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 +15 -0
- package/package.json +1 -1
- package/scripts/install.ps1 +4 -0
- package/scripts/install.sh +33 -0
- package/src/engines/claude.js +34 -1
- package/src/engines/codex.js +59 -10
- package/src/engines/stream-formats.js +16 -1
- package/src/server.js +1 -1
package/README.md
CHANGED
|
@@ -92,11 +92,26 @@ npm start # → http://127.0.0.1:4319
|
|
|
92
92
|
| `POST` | `/v1/completions` | OpenAI-compatible legacy text Completions (streaming and non-streaming) |
|
|
93
93
|
| `POST` | `/v1/responses` | OpenAI-compatible Responses API (streaming and non-streaming) |
|
|
94
94
|
| `POST` | `/v1/messages` | Anthropic-compatible Messages API (streaming and non-streaming) |
|
|
95
|
+
| `GET` | `/skills` | skills installed on this machine, across every agent harness + configured folders |
|
|
96
|
+
| `GET` | `/skills/<name>` · `/skills/<name>/file/<path>` | one skill's instructions · one reference file |
|
|
97
|
+
| `POST` | `/mcp` | MCP endpoint — `chatpanel_skill_*` tools any CLI can call (plus browser tools while a ChatPanel chat is driving) |
|
|
95
98
|
|
|
96
99
|
`/chat` streams Server-Sent Events: `{type:'delta',text}` as the answer is
|
|
97
100
|
generated, `{type:'tool',name,summary}` / `{type:'status'}` for activity, and a
|
|
98
101
|
final `{type:'done'}` (or `{type:'error',error}`).
|
|
99
102
|
|
|
103
|
+
### Use your skills from any CLI
|
|
104
|
+
|
|
105
|
+
The bridge discovers the skills installed on your machine — Claude Code, Codex, Copilot,
|
|
106
|
+
Gemini, Hermes, `~/.agents/skills`, and any folder you configure — and exposes them as MCP
|
|
107
|
+
tools (`chatpanel_skill_list` / `_open` / `_read`). Point any MCP client at
|
|
108
|
+
`http://127.0.0.1:4319/mcp` and it can list and load them with no browser open.
|
|
109
|
+
|
|
110
|
+
For **one** server that adds your local **history** (chats, meetings, notes — redacted)
|
|
111
|
+
*alongside* your skills, add the
|
|
112
|
+
[Privacy Gateway](https://github.com/chatpanel/chatpanel-gateway) instead and point your CLI
|
|
113
|
+
at `chatpanel-gateway mcp`. The gateway proxies these skill tools and puts redaction in front.
|
|
114
|
+
|
|
100
115
|
`options` per agent (set in ChatPanel Settings):
|
|
101
116
|
|
|
102
117
|
```jsonc
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/bridge",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.34",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local bridge that exposes the AI coding agents installed on your machine \u2014 Claude Code (CLI), Codex (CLI), and Antigravity CLI (formerly Gemini CLI, which remains available for business/enterprise) \u2014 to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
|
|
6
6
|
"keywords": [
|
package/scripts/install.ps1
CHANGED
|
@@ -52,3 +52,7 @@ if ($installed) {
|
|
|
52
52
|
Write-Host " `"$bin`""
|
|
53
53
|
}
|
|
54
54
|
Write-Host "Manage it: `"$bin`" --status | --uninstall"
|
|
55
|
+
Write-Host ""
|
|
56
|
+
Write-Host "Optional upgrade - the Privacy Gateway adds PII redaction, model routing and voice," -ForegroundColor Gray
|
|
57
|
+
Write-Host "and lets any CLI reach your history + skills through one MCP server:" -ForegroundColor Gray
|
|
58
|
+
Write-Host " irm https://dl.chatpanel.net/gateway/install.ps1 | iex"
|
package/scripts/install.sh
CHANGED
|
@@ -4,10 +4,23 @@
|
|
|
4
4
|
#
|
|
5
5
|
# curl -fsSL https://raw.githubusercontent.com/chatpanel/chatpanel-bridge/main/scripts/install.sh | bash
|
|
6
6
|
#
|
|
7
|
+
# Add the optional privacy gateway (redaction, model routing, voice) in the same flow:
|
|
8
|
+
# curl -fsSL https://dl.chatpanel.net/bridge/install.sh | bash -s -- --gateway
|
|
9
|
+
#
|
|
7
10
|
# Downloading via curl means the file is NOT quarantined, so macOS won't show the
|
|
8
11
|
# "damaged / unidentified developer" prompt that browser downloads trigger.
|
|
9
12
|
set -euo pipefail
|
|
10
13
|
|
|
14
|
+
# --gateway also installs the ChatPanel Privacy Gateway — the optional upgrade that adds
|
|
15
|
+
# PII redaction, model routing and voice in front of everything the bridge does. The bridge
|
|
16
|
+
# is the common case and is always installed; the gateway is opt-in and heavier.
|
|
17
|
+
WITH_GATEWAY=0
|
|
18
|
+
for a in "$@"; do
|
|
19
|
+
case "$a" in
|
|
20
|
+
--gateway|--with-gateway) WITH_GATEWAY=1 ;;
|
|
21
|
+
esac
|
|
22
|
+
done
|
|
23
|
+
|
|
11
24
|
os="$(uname -s)"
|
|
12
25
|
arch="$(uname -m)"
|
|
13
26
|
asset=""
|
|
@@ -59,3 +72,23 @@ case ":${PATH}:" in
|
|
|
59
72
|
*":${dest}:"*) : ;;
|
|
60
73
|
*) echo "Tip: add it to your PATH -> export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;
|
|
61
74
|
esac
|
|
75
|
+
|
|
76
|
+
if [ "$WITH_GATEWAY" = "1" ]; then
|
|
77
|
+
echo
|
|
78
|
+
echo "Adding the ChatPanel Privacy Gateway (optional upgrade)..."
|
|
79
|
+
# One download story: the gateway installer lives at the same dl host. A gateway failure
|
|
80
|
+
# must not fail the bridge install — the bridge is already up.
|
|
81
|
+
if curl -fsSL https://dl.chatpanel.net/gateway/install.sh | bash; then
|
|
82
|
+
echo "Gateway installed. One MCP config now gives any CLI your history + skills, redacted:"
|
|
83
|
+
echo " chatpanel-gateway mcp"
|
|
84
|
+
else
|
|
85
|
+
echo "Gateway install didn't complete — the bridge is unaffected. Retry any time:"
|
|
86
|
+
echo " curl -fsSL https://dl.chatpanel.net/gateway/install.sh | bash"
|
|
87
|
+
fi
|
|
88
|
+
else
|
|
89
|
+
echo
|
|
90
|
+
echo "Optional upgrade — the Privacy Gateway adds PII redaction, model routing and voice,"
|
|
91
|
+
echo "and lets any CLI (Codex, Claude Code) reach your history + skills through one MCP server:"
|
|
92
|
+
echo " curl -fsSL https://dl.chatpanel.net/gateway/install.sh | bash"
|
|
93
|
+
echo " (or re-run this with --gateway to add it now.)"
|
|
94
|
+
fi
|
package/src/engines/claude.js
CHANGED
|
@@ -166,6 +166,18 @@ function runClaude({ prompt, args, cwd, emit, signal }) {
|
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// A tool_result's content is either a string or Anthropic's block array. Flatten to text and
|
|
170
|
+
// cap it: the panel truncates for display anyway, and an un-capped file read would otherwise
|
|
171
|
+
// push megabytes through the SSE for no benefit.
|
|
172
|
+
function toolResultText(content) {
|
|
173
|
+
let text = '';
|
|
174
|
+
if (typeof content === 'string') text = content;
|
|
175
|
+
else if (Array.isArray(content)) {
|
|
176
|
+
text = content.map((c) => (typeof c === 'string' ? c : (c?.text || ''))).filter(Boolean).join('\n');
|
|
177
|
+
}
|
|
178
|
+
return text.length > 4000 ? `${text.slice(0, 4000)}…` : text;
|
|
179
|
+
}
|
|
180
|
+
|
|
169
181
|
// Map one stream-json message to emit() calls. Returns { streamed, result }.
|
|
170
182
|
// The CLI's stream-json mirrors the SDK message shapes. Exported so the custom
|
|
171
183
|
// engine can reuse it for agents that emit Claude-style stream-json.
|
|
@@ -184,12 +196,33 @@ export function handleMessage(msg, emit, alreadyStreamed, cwdForSteps = '') {
|
|
|
184
196
|
} else if (msg.type === 'assistant') {
|
|
185
197
|
for (const block of msg.message?.content || []) {
|
|
186
198
|
if (block.type === 'tool_use') {
|
|
187
|
-
|
|
199
|
+
// PHASE-BASED, like the Codex engine: the panel renders a step with the call's
|
|
200
|
+
// arguments and then fills in its status and output when the result arrives. The old
|
|
201
|
+
// single `summary` event produced one anonymous line per call and no outcome — you
|
|
202
|
+
// could see that Claude did something, never what it returned. `summary` is still
|
|
203
|
+
// sent so an older extension keeps the line it used to draw.
|
|
204
|
+
emit({
|
|
205
|
+
type: 'tool', name: block.name, phase: 'start',
|
|
206
|
+
callId: block.id, input: block.input,
|
|
207
|
+
summary: toolSummary(block, cwdForSteps),
|
|
208
|
+
});
|
|
188
209
|
} else if (block.type === 'text' && !alreadyStreamed) {
|
|
189
210
|
out.streamed = true;
|
|
190
211
|
emit({ type: 'delta', text: block.text });
|
|
191
212
|
}
|
|
192
213
|
}
|
|
214
|
+
} else if (msg.type === 'user') {
|
|
215
|
+
// Claude reports every tool's OUTCOME as a tool_result block on a synthetic user message.
|
|
216
|
+
// Pairing it with the call by tool_use_id is what turns the step list into a readable
|
|
217
|
+
// log: each action shows ok / error and the text the tool actually returned.
|
|
218
|
+
for (const block of msg.message?.content || []) {
|
|
219
|
+
if (block.type !== 'tool_result') continue;
|
|
220
|
+
emit({
|
|
221
|
+
type: 'tool', phase: 'done', callId: block.tool_use_id,
|
|
222
|
+
status: block.is_error ? 'error' : 'ok',
|
|
223
|
+
result: toolResultText(block.content),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
193
226
|
} else if (msg.type === 'result') {
|
|
194
227
|
if (msg.subtype === 'success') out.result = msg.result || '';
|
|
195
228
|
else emit({ type: 'status', text: `(${msg.subtype})` });
|
package/src/engines/codex.js
CHANGED
|
@@ -151,6 +151,10 @@ export async function chat({ messages, system, options, images }, emit, { signal
|
|
|
151
151
|
args.push('-s', sandbox, '-c', 'approval_policy=never');
|
|
152
152
|
}
|
|
153
153
|
if (REASONING) args.push('-c', `model_reasoning_effort=${REASONING}`);
|
|
154
|
+
// Ask Codex to emit reasoning SUMMARIES so the panel can stream the model's thinking.
|
|
155
|
+
// Additive + safe: a no-op for models/providers that don't produce summaries (e.g. some
|
|
156
|
+
// hosted models expose none), and cheap at the default effort. forwardEvent renders them.
|
|
157
|
+
args.push('-c', 'model_reasoning_summary=auto');
|
|
154
158
|
// Browser tools: register the bridge's MCP server as a stdio MCP server (the
|
|
155
159
|
// bridge binary in --mcp-stdio mode), so Codex can call our page-action tools.
|
|
156
160
|
// `-c key=value` parses value as TOML; JSON.stringify yields valid TOML here.
|
|
@@ -182,6 +186,9 @@ export async function chat({ messages, system, options, images }, emit, { signal
|
|
|
182
186
|
|
|
183
187
|
let stdout = '';
|
|
184
188
|
let stderr = '';
|
|
189
|
+
// Per-run event state: correlate a command's started/completed events and emit each
|
|
190
|
+
// reasoning summary once (Codex sends the same item id across item.started/updated/completed).
|
|
191
|
+
const evState = { started: new Set(), reasoned: new Set(), n: 0 };
|
|
185
192
|
let idleTimer;
|
|
186
193
|
const armIdle = () => {
|
|
187
194
|
clearTimeout(idleTimer);
|
|
@@ -201,7 +208,7 @@ export async function chat({ messages, system, options, images }, emit, { signal
|
|
|
201
208
|
stdout = stdout.slice(nl + 1);
|
|
202
209
|
if (!line.startsWith('{')) continue;
|
|
203
210
|
try {
|
|
204
|
-
forwardEvent(JSON.parse(line), emit);
|
|
211
|
+
forwardEvent(JSON.parse(line), emit, evState);
|
|
205
212
|
} catch {
|
|
206
213
|
/* not a JSON event line */
|
|
207
214
|
}
|
|
@@ -240,16 +247,58 @@ export async function chat({ messages, system, options, images }, emit, { signal
|
|
|
240
247
|
});
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
|
|
250
|
+
// Translate Codex `exec --json` events into the bridge's streaming vocabulary the panel
|
|
251
|
+
// renders richly. Codex sends item.started (in_progress) then item.completed for each item,
|
|
252
|
+
// reusing the item id — so we correlate a tool's start/done by that id and show its command,
|
|
253
|
+
// its output, and its exit status, the way Codex's own CLI does. Schema (codex-cli 0.15x):
|
|
254
|
+
// command_execution: { id, command, aggregated_output, exit_code, status }
|
|
255
|
+
// reasoning: { id, text } (only some models/efforts emit it)
|
|
256
|
+
// file_change: { id, changes:[{path}] }
|
|
257
|
+
// agent_message: { id, text } (the answer — read from -o outFile at close)
|
|
258
|
+
export function forwardEvent(ev, emit, state = { started: new Set(), reasoned: new Set(), n: 0 }) {
|
|
244
259
|
const t = ev.type || '';
|
|
245
260
|
const item = ev.item || {};
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
261
|
+
const itype = item.type || '';
|
|
262
|
+
const completed = t === 'item.completed' || item.status === 'completed' || item.status === 'failed';
|
|
263
|
+
|
|
264
|
+
if (itype === 'command_execution' || (!itype && t.includes('command'))) {
|
|
265
|
+
const id = item.id || `cmd_${state.n++}`;
|
|
266
|
+
if (!state.started.has(id)) {
|
|
267
|
+
state.started.add(id);
|
|
268
|
+
emit({ type: 'tool', name: 'shell', phase: 'start', callId: id, input: { command: item.command || '' } });
|
|
269
|
+
}
|
|
270
|
+
if (completed) {
|
|
271
|
+
const ok = item.exit_code === 0 || item.exit_code == null;
|
|
272
|
+
emit({
|
|
273
|
+
type: 'tool', name: 'shell', phase: 'done', callId: id,
|
|
274
|
+
status: ok ? 'ok' : `exit ${item.exit_code}`,
|
|
275
|
+
result: String(item.aggregated_output || '').slice(0, 4000),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (itype === 'file_change' || (!itype && t.includes('patch'))) {
|
|
282
|
+
const id = item.id || `edit_${state.n++}`;
|
|
283
|
+
const files = Array.isArray(item.changes) ? item.changes.map((c) => c.path || c.file).filter(Boolean) : [];
|
|
284
|
+
if (!state.started.has(id)) {
|
|
285
|
+
state.started.add(id);
|
|
286
|
+
emit({ type: 'tool', name: 'edit', phase: 'start', callId: id, input: { files } });
|
|
287
|
+
}
|
|
288
|
+
if (completed) emit({ type: 'tool', name: 'edit', phase: 'done', callId: id, status: 'ok' });
|
|
289
|
+
return;
|
|
254
290
|
}
|
|
291
|
+
|
|
292
|
+
if (itype === 'reasoning' || (!itype && t.includes('reasoning'))) {
|
|
293
|
+
// Emit each reasoning summary once (dedup the started/updated/completed repeats).
|
|
294
|
+
const id = item.id || `r_${state.n++}`;
|
|
295
|
+
const text = item.text || item.summary || '';
|
|
296
|
+
if (text && !state.reasoned.has(id)) {
|
|
297
|
+
state.reasoned.add(id);
|
|
298
|
+
emit({ type: 'reasoning', text: `${text}\n` });
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (t === 'turn.started' || t === 'thread.started') emit({ type: 'status', text: 'Codex working' });
|
|
255
304
|
}
|
|
@@ -86,8 +86,23 @@ function opencodeJson(emit) {
|
|
|
86
86
|
streamed = true;
|
|
87
87
|
emit({ type: 'delta', text: ev.part.text });
|
|
88
88
|
} else if (ev.type === 'tool' || ev.type === 'tool_use') {
|
|
89
|
+
// Phase-based where opencode gives us enough to pair a call with its outcome: its tool
|
|
90
|
+
// parts carry a callID and a state that moves running → completed/error. Without that we
|
|
91
|
+
// still emit a start, so the step at least appears (the old behaviour).
|
|
89
92
|
const p = ev.part || {};
|
|
90
|
-
|
|
93
|
+
const name = p.tool || p.name || p.type || 'tool';
|
|
94
|
+
const callId = p.callID || p.callId || p.id || undefined;
|
|
95
|
+
const state = p.state || {};
|
|
96
|
+
const status = String(state.status || '').toLowerCase();
|
|
97
|
+
if (status === 'completed' || status === 'error') {
|
|
98
|
+
emit({
|
|
99
|
+
type: 'tool', name, phase: 'done', callId,
|
|
100
|
+
status: status === 'error' ? 'error' : 'ok',
|
|
101
|
+
result: String(state.output || state.error || '').slice(0, 4000),
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
emit({ type: 'tool', name, phase: 'start', callId, input: state.input || p.input, summary: '' });
|
|
105
|
+
}
|
|
91
106
|
} else if (ev.type === 'error') {
|
|
92
107
|
const msg = ev.error?.data?.message || ev.error?.message || ev.error?.name || 'error';
|
|
93
108
|
emit({ type: 'status', text: String(msg).slice(0, 300) });
|
package/src/server.js
CHANGED
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
// Hardcoded (not read from package.json) so it survives Bun's single-file
|
|
68
68
|
// --compile, where package.json isn't on a readable FS. CI fails the publish if
|
|
69
69
|
// this drifts from package.json, so the two can't silently diverge.
|
|
70
|
-
const VERSION = '0.10.
|
|
70
|
+
const VERSION = '0.10.34';
|
|
71
71
|
const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
|
|
72
72
|
const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
|
|
73
73
|
|