@chatpanel/bridge 0.3.2 → 0.4.0
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/claude.js +41 -5
- package/src/engines/codex.js +40 -3
- package/src/engines/custom.js +62 -2
- package/src/engines/gemini.js +42 -3
- package/src/server.js +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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": [
|
package/src/engines/claude.js
CHANGED
|
@@ -14,10 +14,27 @@
|
|
|
14
14
|
// permissionMode is 'acceptEdits'/'bypassPermissions' in ChatPanel Settings.
|
|
15
15
|
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
|
+
import { writeFile, unlink } from 'node:fs/promises';
|
|
17
18
|
import os from 'node:os';
|
|
18
19
|
import path from 'node:path';
|
|
19
20
|
import { resolveClaude, buildSpawnSpec, isCompiledBinary } from '../env.js';
|
|
20
21
|
|
|
22
|
+
// Write base64 data-URL images to temp files. Claude Code reads them with its
|
|
23
|
+
// Read tool (which feeds images to the model as vision), so we just reference the
|
|
24
|
+
// paths in the prompt — no custom image flag needed. Returns paths (caller cleans).
|
|
25
|
+
async function writeImages(images, tag) {
|
|
26
|
+
const files = [];
|
|
27
|
+
for (let i = 0; i < (images?.length || 0); i++) {
|
|
28
|
+
const m = /^data:([^;]+);base64,(.+)$/s.exec(images[i]?.dataUrl || '');
|
|
29
|
+
if (!m) continue;
|
|
30
|
+
const ext = (m[1].split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
|
|
31
|
+
const file = path.join(os.tmpdir(), `chatpanel-claude-img-${tag}-${i}.${ext}`);
|
|
32
|
+
await writeFile(file, Buffer.from(m[2], 'base64'));
|
|
33
|
+
files.push(file);
|
|
34
|
+
}
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
// Idle timeout: kill the run only after this long with NO output. The timer
|
|
22
39
|
// re-arms on every stdout/stderr chunk, so a task that keeps streaming can run
|
|
23
40
|
// indefinitely — only a truly stuck/silent process is killed. Override with
|
|
@@ -173,7 +190,7 @@ export function handleMessage(msg, emit, alreadyStreamed) {
|
|
|
173
190
|
return out;
|
|
174
191
|
}
|
|
175
192
|
|
|
176
|
-
export async function chat({ messages, system, options }, emit) {
|
|
193
|
+
export async function chat({ messages, system, options, images }, emit) {
|
|
177
194
|
const permissionMode = options.permissionMode || 'default';
|
|
178
195
|
// Explicit project dir, else null → CLI runs in home (or WSL home).
|
|
179
196
|
const cwd = options.workingDir ? path.resolve(options.workingDir) : null;
|
|
@@ -194,10 +211,29 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
194
211
|
// "Use my local skills & config" off → run clean.
|
|
195
212
|
if (options.useLocalConfig === false) args.push('--setting-sources', '');
|
|
196
213
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
214
|
+
// Attach images by writing them to temp files and asking Claude Code to Read
|
|
215
|
+
// them — its Read tool loads images as vision (no special flag needed).
|
|
216
|
+
const tag = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
217
|
+
const imageFiles = await writeImages(images, tag);
|
|
218
|
+
const cleanup = () => imageFiles.forEach((f) => unlink(f).catch(() => {}));
|
|
219
|
+
let prompt = buildPrompt(messages);
|
|
220
|
+
if (imageFiles.length) {
|
|
221
|
+
prompt += `\n\nThe user attached ${imageFiles.length} image file(s). Use the Read tool to view ${
|
|
222
|
+
imageFiles.length === 1 ? 'it' : 'them'
|
|
223
|
+
}: ${imageFiles.join(', ')}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const run = runClaude({ prompt, args, cwd, emit });
|
|
227
|
+
if (run === null) {
|
|
228
|
+
cleanup(); // SDK fallback doesn't take images yet
|
|
229
|
+
return sdkChat({ messages, system, options }, emit);
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const { streamedAny, resultText } = await run;
|
|
233
|
+
emit({ type: 'done', text: streamedAny ? '' : resultText });
|
|
234
|
+
} finally {
|
|
235
|
+
cleanup();
|
|
236
|
+
}
|
|
201
237
|
}
|
|
202
238
|
|
|
203
239
|
// A fast, tool-free single-shot completion — used for prompt autocomplete. No
|
package/src/engines/codex.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
// the agent to point it at a real project.
|
|
16
16
|
|
|
17
17
|
import { spawn, spawnSync } from 'node:child_process';
|
|
18
|
-
import { readFile, unlink } from 'node:fs/promises';
|
|
19
|
-
import { existsSync, mkdirSync, symlinkSync } from 'node:fs';
|
|
18
|
+
import { readFile, unlink, writeFile } from 'node:fs/promises';
|
|
19
|
+
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 } from '../env.js';
|
|
@@ -28,6 +28,22 @@ const IDLE_MS = Number(process.env.CHATPANEL_CODEX_TIMEOUT_MS) || 180_000;
|
|
|
28
28
|
const REASONING = process.env.CHATPANEL_CODEX_EFFORT ?? 'low'; // '' → respect config
|
|
29
29
|
|
|
30
30
|
const SCRATCH = path.join(os.tmpdir(), 'chatpanel-codex-scratch');
|
|
31
|
+
|
|
32
|
+
// Codex has no "list models" command — its model lives in CODEX_HOME/config.toml
|
|
33
|
+
// (e.g. `model = "gpt-5.5"`). Surface the user's REAL configured model(s), read
|
|
34
|
+
// straight from that file, plus a few common ids. The picker still accepts any
|
|
35
|
+
// free-text value, so an out-of-date curated entry is harmless.
|
|
36
|
+
const CODEX_KNOWN = ['gpt-5-codex', 'gpt-5', 'o3', 'o4-mini'];
|
|
37
|
+
export async function listModels() {
|
|
38
|
+
const set = new Set();
|
|
39
|
+
try {
|
|
40
|
+
const home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
41
|
+
const cfg = readFileSync(path.join(home, 'config.toml'), 'utf8');
|
|
42
|
+
for (const m of cfg.matchAll(/(?:^|\n)\s*model\s*=\s*["']([^"'\n]+)["']/g)) set.add(m[1].trim());
|
|
43
|
+
} catch { /* no config — fall back to the curated set */ }
|
|
44
|
+
for (const m of CODEX_KNOWN) set.add(m);
|
|
45
|
+
return [...set];
|
|
46
|
+
}
|
|
31
47
|
const ISO_HOME = path.join(os.homedir(), '.chatpanel', 'codex-home');
|
|
32
48
|
|
|
33
49
|
function ensureScratch() {
|
|
@@ -94,10 +110,27 @@ function buildPrompt(messages, system) {
|
|
|
94
110
|
return p;
|
|
95
111
|
}
|
|
96
112
|
|
|
97
|
-
|
|
113
|
+
// Write base64 data-URL images to temp files so `codex exec -i <file>` can
|
|
114
|
+
// attach them to the prompt as vision input. Returns the paths (caller cleans up).
|
|
115
|
+
async function writeImages(images, tag) {
|
|
116
|
+
const files = [];
|
|
117
|
+
for (let i = 0; i < (images?.length || 0); i++) {
|
|
118
|
+
const m = /^data:([^;]+);base64,(.+)$/s.exec(images[i]?.dataUrl || '');
|
|
119
|
+
if (!m) continue;
|
|
120
|
+
const ext = (m[1].split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
|
|
121
|
+
const file = path.join(os.tmpdir(), `chatpanel-codex-img-${tag}-${i}.${ext}`);
|
|
122
|
+
await writeFile(file, Buffer.from(m[2], 'base64'));
|
|
123
|
+
files.push(file);
|
|
124
|
+
}
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function chat({ messages, system, options, images }, emit) {
|
|
98
129
|
ensureScratch();
|
|
99
130
|
const tag = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
100
131
|
const outFile = path.join(os.tmpdir(), `chatpanel-codex-${tag}.txt`);
|
|
132
|
+
const imageFiles = await writeImages(images, tag);
|
|
133
|
+
const cleanupImages = () => imageFiles.forEach((f) => unlink(f).catch(() => {}));
|
|
101
134
|
|
|
102
135
|
const cwd = options.workingDir ? path.resolve(options.workingDir) : SCRATCH;
|
|
103
136
|
const sandbox =
|
|
@@ -114,6 +147,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
114
147
|
args.push('-c', 'approval_policy=never');
|
|
115
148
|
if (REASONING) args.push('-c', `model_reasoning_effort=${REASONING}`);
|
|
116
149
|
if (options.model) args.push('-m', options.model);
|
|
150
|
+
for (const f of imageFiles) args.push('-i', f); // attach images to the initial prompt
|
|
117
151
|
args.push('-');
|
|
118
152
|
|
|
119
153
|
// Default: use the user's skills/config. Opt-out → isolated home.
|
|
@@ -129,6 +163,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
129
163
|
try {
|
|
130
164
|
child = spawn('codex', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env });
|
|
131
165
|
} catch (e) {
|
|
166
|
+
cleanupImages();
|
|
132
167
|
return reject(new Error(`Failed to start codex: ${e.message}`));
|
|
133
168
|
}
|
|
134
169
|
|
|
@@ -162,6 +197,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
162
197
|
child.stderr.on('data', (d) => { armIdle(); stderr += d.toString(); });
|
|
163
198
|
child.on('error', (e) => {
|
|
164
199
|
clearTimeout(idleTimer);
|
|
200
|
+
cleanupImages();
|
|
165
201
|
reject(e);
|
|
166
202
|
});
|
|
167
203
|
child.on('close', async (code) => {
|
|
@@ -173,6 +209,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
173
209
|
/* no message file */
|
|
174
210
|
}
|
|
175
211
|
unlink(outFile).catch(() => {});
|
|
212
|
+
cleanupImages();
|
|
176
213
|
if (code === 0) {
|
|
177
214
|
emit({ type: 'delta', text: text || '(no output)' });
|
|
178
215
|
emit({ type: 'done', text: '' });
|
package/src/engines/custom.js
CHANGED
|
@@ -16,11 +16,44 @@
|
|
|
16
16
|
// speak it), reusing the Claude engine's parser.
|
|
17
17
|
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
|
+
import { writeFile, unlink } from 'node:fs/promises';
|
|
20
|
+
import os from 'node:os';
|
|
19
21
|
import path from 'node:path';
|
|
20
22
|
import { resolveCommand, buildSpawnSpec } from '../env.js';
|
|
21
23
|
import { isProEntitled } from '../entitlement.js';
|
|
22
24
|
import { handleMessage } from './claude.js';
|
|
23
25
|
|
|
26
|
+
// Write base64 data-URL images to temp files so a custom CLI can take them via
|
|
27
|
+
// its configured `imageArg` template (e.g. "-i {path}", "@{path}"). Returns paths.
|
|
28
|
+
async function writeImages(images, tag) {
|
|
29
|
+
const files = [];
|
|
30
|
+
for (let i = 0; i < (images?.length || 0); i++) {
|
|
31
|
+
const m = /^data:([^;]+);base64,(.+)$/s.exec(images[i]?.dataUrl || '');
|
|
32
|
+
if (!m) continue;
|
|
33
|
+
const ext = (m[1].split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
|
|
34
|
+
const file = path.join(os.tmpdir(), `chatpanel-custom-img-${tag}-${i}.${ext}`);
|
|
35
|
+
await writeFile(file, Buffer.from(m[2], 'base64'));
|
|
36
|
+
files.push(file);
|
|
37
|
+
}
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Expand the user's `imageArg` template across the image files into argv tokens.
|
|
42
|
+
// "{path}" is substituted per image; a template without it appends the path.
|
|
43
|
+
function imageTokensFor(imageArg, files) {
|
|
44
|
+
const tmpl = String(imageArg || '').trim();
|
|
45
|
+
if (!tmpl || !files.length) return [];
|
|
46
|
+
const tokens = [];
|
|
47
|
+
for (const f of files) {
|
|
48
|
+
tokens.push(
|
|
49
|
+
...(tmpl.includes('{path}')
|
|
50
|
+
? tmpl.replaceAll('{path}', f).split(/\s+/).filter(Boolean)
|
|
51
|
+
: [...tmpl.split(/\s+/).filter(Boolean), f]),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return tokens;
|
|
55
|
+
}
|
|
56
|
+
|
|
24
57
|
// Idle timeout: re-armed on every stdout/stderr chunk, so a long run that keeps
|
|
25
58
|
// streaming never trips it — only true silence does. Override with
|
|
26
59
|
// CHATPANEL_CUSTOM_TIMEOUT_MS (ms).
|
|
@@ -92,7 +125,7 @@ function buildPrompt(messages, system) {
|
|
|
92
125
|
return p;
|
|
93
126
|
}
|
|
94
127
|
|
|
95
|
-
export async function chat({ messages, system, options }, emit) {
|
|
128
|
+
export async function chat({ messages, system, options, images }, emit) {
|
|
96
129
|
// Pro gate — verified, not just UI. No valid signed entitlement → no run.
|
|
97
130
|
if (!(await isProEntitled(options.entitlement))) {
|
|
98
131
|
throw new Error('Custom agents require ChatPanel Pro. Upgrade in Settings to bring your own CLI agent.');
|
|
@@ -131,6 +164,24 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
131
164
|
: [...tmpl.split(/\s+/).filter(Boolean), options.model];
|
|
132
165
|
args = [...injected, ...args];
|
|
133
166
|
}
|
|
167
|
+
// Images: write to temp files, expand the agent's imageArg template, then place
|
|
168
|
+
// the tokens. An explicit {images} placeholder in args wins; otherwise they go
|
|
169
|
+
// just before the prompt (arg mode) or get appended (stdin mode).
|
|
170
|
+
const tag = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
171
|
+
const imageFiles = spec.imageArg ? await writeImages(images, tag) : [];
|
|
172
|
+
const cleanup = () => imageFiles.forEach((f) => unlink(f).catch(() => {}));
|
|
173
|
+
const imageTokens = imageTokensFor(spec.imageArg, imageFiles);
|
|
174
|
+
let placedImages = false;
|
|
175
|
+
if (imageTokens.length) {
|
|
176
|
+
args = args.flatMap((a) => {
|
|
177
|
+
if (a === '{images}') {
|
|
178
|
+
placedImages = true;
|
|
179
|
+
return imageTokens;
|
|
180
|
+
}
|
|
181
|
+
return [a];
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
134
185
|
if (promptVia === 'arg') {
|
|
135
186
|
let placed = false;
|
|
136
187
|
args = args.map((a) => {
|
|
@@ -140,7 +191,12 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
140
191
|
}
|
|
141
192
|
return a;
|
|
142
193
|
});
|
|
143
|
-
if (!placed)
|
|
194
|
+
if (!placed) {
|
|
195
|
+
if (imageTokens.length && !placedImages) args.push(...imageTokens); // images, then prompt
|
|
196
|
+
args.push(prompt);
|
|
197
|
+
}
|
|
198
|
+
} else if (imageTokens.length && !placedImages) {
|
|
199
|
+
args.push(...imageTokens); // stdin prompt: image tokens go on argv
|
|
144
200
|
}
|
|
145
201
|
|
|
146
202
|
const [bin, argv, opts] = buildSpawnSpec(resolved, args, cwd);
|
|
@@ -150,6 +206,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
150
206
|
try {
|
|
151
207
|
child = spawn(bin, argv, opts);
|
|
152
208
|
} catch (e) {
|
|
209
|
+
cleanup();
|
|
153
210
|
return reject(new Error(`Failed to start ${label}: ${e.message}`));
|
|
154
211
|
}
|
|
155
212
|
|
|
@@ -163,6 +220,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
163
220
|
clearTimeout(idleTimer);
|
|
164
221
|
idleTimer = setTimeout(() => {
|
|
165
222
|
child.kill('SIGKILL');
|
|
223
|
+
cleanup();
|
|
166
224
|
reject(new Error(`${label} timed out — no output for ${Math.round(IDLE_MS / 1000)}s.`));
|
|
167
225
|
}, IDLE_MS);
|
|
168
226
|
};
|
|
@@ -196,10 +254,12 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
196
254
|
child.stderr.on('data', (d) => { armIdle(); stderr += d.toString(); });
|
|
197
255
|
child.on('error', (e) => {
|
|
198
256
|
clearTimeout(idleTimer);
|
|
257
|
+
cleanup();
|
|
199
258
|
reject(new Error(`Failed to start ${label}: ${e.message}`));
|
|
200
259
|
});
|
|
201
260
|
child.on('close', (code) => {
|
|
202
261
|
clearTimeout(idleTimer);
|
|
262
|
+
cleanup();
|
|
203
263
|
if (code === 0) {
|
|
204
264
|
emit({ type: 'done', text: streamedAny ? '' : resultText });
|
|
205
265
|
resolve();
|
package/src/engines/gemini.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Install: `npm i -g @google/gemini-cli`, then run `gemini` once to sign in.
|
|
10
10
|
|
|
11
11
|
import { spawn, spawnSync } from 'node:child_process';
|
|
12
|
-
import { mkdirSync } from 'node:fs';
|
|
12
|
+
import { mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
13
13
|
import os from 'node:os';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { findAgentBin } from '../env.js';
|
|
@@ -20,6 +20,13 @@ import { findAgentBin } from '../env.js';
|
|
|
20
20
|
const IDLE_MS = Number(process.env.CHATPANEL_GEMINI_TIMEOUT_MS) || 180_000;
|
|
21
21
|
const SCRATCH = path.join(os.tmpdir(), 'chatpanel-gemini-scratch');
|
|
22
22
|
|
|
23
|
+
// Gemini CLI has no "list models" command (--help only lists extensions/sessions)
|
|
24
|
+
// and stores no model in settings.json, so offer the common current ids. Free
|
|
25
|
+
// text still accepted.
|
|
26
|
+
export async function listModels() {
|
|
27
|
+
return ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'];
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
let installed = false;
|
|
24
31
|
let lastProbe = 0;
|
|
25
32
|
export async function available() {
|
|
@@ -38,6 +45,24 @@ export async function available() {
|
|
|
38
45
|
: { ok: false, reason: 'gemini not found on PATH. Install @google/gemini-cli, then run `gemini` once to sign in.' };
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
// Write base64 data-URL images into `dir` so Gemini's `@<file>` can read them.
|
|
49
|
+
function writeImages(images, dir) {
|
|
50
|
+
const files = [];
|
|
51
|
+
for (let i = 0; i < (images?.length || 0); i++) {
|
|
52
|
+
const m = /^data:([^;]+);base64,(.+)$/s.exec(images[i]?.dataUrl || '');
|
|
53
|
+
if (!m) continue;
|
|
54
|
+
const ext = (m[1].split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
|
|
55
|
+
const file = path.join(dir, `chatpanel-img-${Date.now()}-${i}.${ext}`);
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(file, Buffer.from(m[2], 'base64'));
|
|
58
|
+
files.push(file);
|
|
59
|
+
} catch {
|
|
60
|
+
/* skip unwritable */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
|
|
41
66
|
function buildPrompt(messages, system) {
|
|
42
67
|
let p = system ? `${system}\n\n` : '';
|
|
43
68
|
const history = messages.slice(0, -1);
|
|
@@ -51,7 +76,7 @@ function buildPrompt(messages, system) {
|
|
|
51
76
|
return p;
|
|
52
77
|
}
|
|
53
78
|
|
|
54
|
-
export async function chat({ messages, system, options }, emit) {
|
|
79
|
+
export async function chat({ messages, system, options, images }, emit) {
|
|
55
80
|
try {
|
|
56
81
|
mkdirSync(SCRATCH, { recursive: true });
|
|
57
82
|
} catch {
|
|
@@ -59,10 +84,20 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
59
84
|
}
|
|
60
85
|
const cwd = options.workingDir ? path.resolve(options.workingDir) : SCRATCH;
|
|
61
86
|
|
|
87
|
+
// Images: write them into the cwd so Gemini's `@file` reference (which reads
|
|
88
|
+
// files — including images — as multimodal input) can resolve them. Cleaned up
|
|
89
|
+
// after the run. Written into cwd (not tmp) because `@` resolves to the workspace.
|
|
90
|
+
const imageFiles = writeImages(images, cwd);
|
|
91
|
+
const cleanup = () => imageFiles.forEach((f) => { try { unlinkSync(f); } catch { /* gone */ } });
|
|
92
|
+
let prompt = buildPrompt(messages, system);
|
|
93
|
+
if (imageFiles.length) {
|
|
94
|
+
prompt += `\n\nThe user attached image(s): ${imageFiles.map((f) => '@' + path.basename(f)).join(' ')}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
62
97
|
// `-p` is non-interactive (no TTY prompts). `-m` picks the model. `-y` (yolo)
|
|
63
98
|
// auto-approves tool calls when the user opted into bypassPermissions — without
|
|
64
99
|
// it Gemini would block on an approval it can't show in a headless run.
|
|
65
|
-
const args = ['-p',
|
|
100
|
+
const args = ['-p', prompt];
|
|
66
101
|
if (options.model) args.push('-m', options.model);
|
|
67
102
|
if (options.permissionMode === 'bypassPermissions') args.push('-y');
|
|
68
103
|
|
|
@@ -73,6 +108,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
73
108
|
// interactive "trust this folder?" dialog can block us.
|
|
74
109
|
child = spawn('gemini', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env } });
|
|
75
110
|
} catch (e) {
|
|
111
|
+
cleanup();
|
|
76
112
|
return reject(new Error(`Failed to start gemini: ${e.message}`));
|
|
77
113
|
}
|
|
78
114
|
|
|
@@ -84,6 +120,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
84
120
|
clearTimeout(idleTimer);
|
|
85
121
|
idleTimer = setTimeout(() => {
|
|
86
122
|
child.kill('SIGKILL');
|
|
123
|
+
cleanup();
|
|
87
124
|
reject(new Error(`Gemini timed out — no output for ${Math.round(IDLE_MS / 1000)}s.`));
|
|
88
125
|
}, IDLE_MS);
|
|
89
126
|
};
|
|
@@ -99,10 +136,12 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
99
136
|
child.stderr.on('data', (d) => { armIdle(); err += d.toString(); });
|
|
100
137
|
child.on('error', (e) => {
|
|
101
138
|
clearTimeout(idleTimer);
|
|
139
|
+
cleanup();
|
|
102
140
|
reject(new Error(`Failed to start gemini: ${e.message}`));
|
|
103
141
|
});
|
|
104
142
|
child.on('close', (code) => {
|
|
105
143
|
clearTimeout(idleTimer);
|
|
144
|
+
cleanup();
|
|
106
145
|
if (code === 0) {
|
|
107
146
|
if (!streamed) emit({ type: 'delta', text: out.trim() || '(no output)' });
|
|
108
147
|
emit({ type: 'done', text: '' });
|
package/src/server.js
CHANGED
|
@@ -27,7 +27,7 @@ import { checkForUpdate, selfUpdate } from './update.js';
|
|
|
27
27
|
// Hardcoded (not read from package.json) so it survives Bun's single-file
|
|
28
28
|
// --compile, where package.json isn't on a readable FS. CI fails the publish if
|
|
29
29
|
// this drifts from package.json, so the two can't silently diverge.
|
|
30
|
-
const VERSION = '0.
|
|
30
|
+
const VERSION = '0.4.0';
|
|
31
31
|
const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
|
|
32
32
|
const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
|
|
33
33
|
|
|
@@ -117,6 +117,9 @@ async function handleChat(req, res) {
|
|
|
117
117
|
}
|
|
118
118
|
const target = ENGINES[body.agent];
|
|
119
119
|
if (!target) return json(res, 404, { error: `Unknown agent "${body.agent}"` });
|
|
120
|
+
if (Array.isArray(body.images) && body.images.length) {
|
|
121
|
+
log('info', `chat: ${body.agent} received ${body.images.length} image(s)`);
|
|
122
|
+
}
|
|
120
123
|
|
|
121
124
|
// Open the SSE stream.
|
|
122
125
|
res.writeHead(200, {
|
|
@@ -138,6 +141,7 @@ async function handleChat(req, res) {
|
|
|
138
141
|
messages: Array.isArray(body.messages) ? body.messages : [],
|
|
139
142
|
system: body.system || '',
|
|
140
143
|
options: body.options || {},
|
|
144
|
+
images: Array.isArray(body.images) ? body.images : [],
|
|
141
145
|
},
|
|
142
146
|
(obj) => {
|
|
143
147
|
if (!closed) emit(obj);
|