@chatpanel/bridge 0.10.2 → 0.10.4

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.2",
3
+ "version": "0.10.4",
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 = buildPrompt(messages, system);
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
  }
@@ -18,6 +18,7 @@ import { writeFile, unlink } from 'node:fs/promises';
18
18
  import os from 'node:os';
19
19
  import path from 'node:path';
20
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,20 +80,6 @@ export async function listModels() {
79
80
  return ['opus', 'sonnet', 'haiku'];
80
81
  }
81
82
 
82
- // The bridge is stateless, so we replay the conversation as a single prompt.
83
- function buildPrompt(messages) {
84
- const history = messages.slice(0, -1);
85
- const last = messages[messages.length - 1];
86
- let prompt = '';
87
- if (history.length) {
88
- prompt += 'Conversation so far:\n';
89
- for (const m of history) prompt += `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}\n\n`;
90
- prompt += '---\n\n';
91
- }
92
- prompt += last ? last.content : '';
93
- return prompt;
94
- }
95
-
96
83
  export function claudeMcpConfig(mcp) {
97
84
  if (!mcp?.url || !Array.isArray(mcp.specs) || !mcp.specs.length) return null;
98
85
  const serverName = mcp.serverName || 'chatpanel_browser';
@@ -250,7 +237,7 @@ export async function chat({ messages, system, options, images }, emit) {
250
237
  imageFiles.forEach((f) => unlink(f).catch(() => {}));
251
238
  mcpFiles.forEach((f) => unlink(f).catch(() => {}));
252
239
  };
253
- let prompt = buildPrompt(messages);
240
+ let prompt = buildCliPrompt(messages);
254
241
  if (imageFiles.length) {
255
242
  prompt += `\n\nThe user attached ${imageFiles.length} image file(s). Use the Read tool to view ${
256
243
  imageFiles.length === 1 ? 'it' : 'them'
@@ -336,7 +323,7 @@ async function sdkChat({ messages, system, options }, emit) {
336
323
  let streamedAny = false;
337
324
  let resultText = '';
338
325
  const iterator = query({
339
- prompt: buildPrompt(messages),
326
+ prompt: buildCliPrompt(messages),
340
327
  options: {
341
328
  cwd,
342
329
  permissionMode,
@@ -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(buildPrompt(messages, system));
238
+ child.stdin.write(buildCliPrompt(messages, system));
251
239
  child.stdin.end();
252
240
  });
253
241
  }
@@ -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 = buildPrompt(messages, system);
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/mcp-local.js CHANGED
Binary file
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.2';
33
+ const VERSION = '0.10.4';
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