@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 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.32",
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": [
@@ -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"
@@ -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
@@ -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
- emit({ type: 'tool', name: block.name, summary: toolSummary(block, cwdForSteps) });
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})` });
@@ -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
- function forwardEvent(ev, emit) {
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
- if (item.type === 'command_execution' || t.includes('command')) {
247
- emit({ type: 'tool', name: 'shell', summary: (item.command || '').slice(0, 60) });
248
- } else if (item.type === 'reasoning' || t.includes('reasoning')) {
249
- emit({ type: 'reasoning' });
250
- } else if (item.type === 'file_change' || t.includes('patch')) {
251
- emit({ type: 'tool', name: 'edit', summary: '' });
252
- } else if (t === 'turn.started' || t === 'thread.started') {
253
- emit({ type: 'status', text: 'Codex working' });
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
- emit({ type: 'tool', name: p.tool || p.name || p.type || 'tool', summary: '' });
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.32';
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