@chatpanel/bridge 0.10.33 → 0.10.35

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/bridge",
3
- "version": "0.10.33",
3
+ "version": "0.10.35",
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": [
@@ -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})` });
@@ -6,6 +6,7 @@
6
6
  // pi — pi -p "<prompt>" · --model · @{path} images · --list-models
7
7
  // opencode — opencode run "<prompt>" · -m provider/model · -f {path} images · models
8
8
  // kiro — kiro-cli chat --no-interactive "<prompt>" · --model · --list-models
9
+ // hermes — hermes -z "<prompt>" · -m model · config get model (no list command)
9
10
 
10
11
  import { runSpec, listSpecModels, runForStdout } from './custom.js';
11
12
  import { findAgentBin } from '../env.js';
@@ -248,3 +249,43 @@ export async function listDshModels(command = 'dsh', workingDir) {
248
249
  }
249
250
  return [...DSH_KNOWN_MODELS];
250
251
  }
252
+
253
+ // Hermes Agent (Nous Research). `-z/--oneshot` prints ONLY the final response text — no
254
+ // banner, no spinner, no tool previews — which is exactly the contract the shared runner
255
+ // wants, and it auto-bypasses approvals so a headless turn never blocks on a prompt.
256
+ //
257
+ // Its own tools and skills (82 of them here) stay ACTIVE: Hermes loads them itself, and the
258
+ // bridge's browser tools arrive over the stable /mcp endpoint like opencode and kiro, since
259
+ // Hermes only reads MCP servers from its own config rather than a per-run file.
260
+ //
261
+ // `hermes model` is interactive with no --list flag, so model discovery goes through
262
+ // `hermes config get model`, which prints the resolved default + provider as `key: value`
263
+ // lines. That yields the one model the user has actually configured — honest, if short.
264
+ export const hermes = makeCliAgent(
265
+ 'hermes',
266
+ {
267
+ args: '-z',
268
+ promptVia: 'arg',
269
+ modelArg: '-m {model}',
270
+ requiresStableMcp: true,
271
+ autoSetupStableMcp: true,
272
+ stableMcpConfigCheck: 'hermes',
273
+ stableMcpSetupArgs: ['mcp', 'add', 'chatpanel_browser', '--url', 'http://127.0.0.1:4319/mcp'],
274
+ stableMcpSetupCommand: 'hermes mcp add chatpanel_browser --url http://127.0.0.1:4319/mcp',
275
+ label: 'Hermes',
276
+ },
277
+ 'hermes not found on PATH. Install Hermes Agent, then run `hermes setup` to sign in.',
278
+ {
279
+ // `config get model` prints resolved settings as `key: value`; we want the default model
280
+ // id, not the provider or base_url lines around it.
281
+ listModels: async (command, options = {}) => {
282
+ try {
283
+ const out = await runForStdout(command, ['config', 'get', 'model'], options.workingDir);
284
+ const m = /^\s*default:\s*(\S+)/m.exec(out || '');
285
+ return m ? [m[1]] : [];
286
+ } catch {
287
+ return []; // not configured yet — the picker still accepts a typed id
288
+ }
289
+ },
290
+ },
291
+ );
@@ -307,9 +307,17 @@ async function kiroHasStableMcpConfig(command, cwd) {
307
307
  return false;
308
308
  }
309
309
 
310
+ // Hermes keeps MCP servers in its own config; `hermes mcp list` is the one honest way to ask
311
+ // whether ours is already registered (no scopes, unlike kiro).
312
+ async function hermesHasStableMcpConfig(command, cwd) {
313
+ const out = await commandOutput(command, ['mcp', 'list'], cwd);
314
+ return out.includes(CHATPANEL_STABLE_MCP_URL) || /chatpanel_browser/i.test(out);
315
+ }
316
+
310
317
  async function hasStableMcpConfig(spec, cwd) {
311
318
  if (spec.stableMcpConfigCheck === 'kiro') return kiroHasStableMcpConfig(spec.command || 'kiro-cli', cwd);
312
319
  if (spec.stableMcpConfigCheck === 'opencode') return opencodeHasStableMcpConfig();
320
+ if (spec.stableMcpConfigCheck === 'hermes') return hermesHasStableMcpConfig(spec.command || 'hermes', cwd);
313
321
  return false;
314
322
  }
315
323
 
@@ -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
@@ -33,7 +33,7 @@ import { join } from 'node:path';
33
33
  import * as claude from './engines/claude.js';
34
34
  import * as codex from './engines/codex.js';
35
35
  import * as antigravity from './engines/antigravity.js';
36
- import { pi, opencode, kiro, copilot, deepseek } from './engines/cli-agents.js';
36
+ import { pi, opencode, kiro, copilot, deepseek , hermes } from './engines/cli-agents.js';
37
37
  import { connectorsFor } from './connectors.js';
38
38
  import * as custom from './engines/custom.js';
39
39
  import { installService, uninstallService, serviceStatus, restartService } from './service.js';
@@ -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.33';
70
+ const VERSION = '0.10.35';
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
 
@@ -77,6 +77,7 @@ const ENGINES = {
77
77
  antigravity: { engine: antigravity, label: 'Antigravity' },
78
78
  pi: { engine: pi, label: 'Pi' },
79
79
  opencode: { engine: opencode, label: 'OpenCode' },
80
+ hermes: { engine: hermes, label: 'Hermes' },
80
81
  kiro: { engine: kiro, label: 'Kiro' },
81
82
  copilot: { engine: copilot, label: 'GitHub Copilot' },
82
83
  deepseek: { engine: deepseek, label: 'DeepSeek Harness' },