@chatpanel/bridge 0.2.10 → 0.2.12
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 +3 -5
- package/src/engines/claude.js +147 -142
- package/src/server.js +1 -1
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Local bridge that exposes the AI coding agents installed on your machine — Claude Code (
|
|
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": [
|
|
7
7
|
"chatpanel",
|
|
8
8
|
"claude-code",
|
|
@@ -36,9 +36,7 @@
|
|
|
36
36
|
"dev": "node --watch src/server.js",
|
|
37
37
|
"build:bin": "bash scripts/build-binaries.sh"
|
|
38
38
|
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"@anthropic-ai/claude-agent-sdk": "^0.1.0"
|
|
41
|
-
},
|
|
39
|
+
"dependencies": {},
|
|
42
40
|
"publishConfig": {
|
|
43
41
|
"registry": "https://registry.npmjs.org/",
|
|
44
42
|
"access": "public"
|
package/src/engines/claude.js
CHANGED
|
@@ -1,184 +1,189 @@
|
|
|
1
|
-
// Claude Code engine —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Claude Code engine — drives the Claude Code CLI (`claude --print`) directly,
|
|
2
|
+
// the SAME way the Codex engine drives `codex exec`. No Agent SDK, no bundled
|
|
3
|
+
// cli.js, no native `sharp` dependency — so it works in any environment that has
|
|
4
|
+
// `claude` on PATH (npm install, native installer, or `npx`), and there's nothing
|
|
5
|
+
// to resolve inside a compiled binary (the old "/$bunfs/root/cli.js" failure).
|
|
4
6
|
//
|
|
5
|
-
// By default the agent can READ your code
|
|
6
|
-
// write or run shell commands unless the
|
|
7
|
-
// 'acceptEdits' or 'bypassPermissions' in ChatPanel
|
|
8
|
-
// directory comes from the agent config
|
|
7
|
+
// It uses your *local* Claude Code login. By default the agent can READ your code
|
|
8
|
+
// (Read/Grep/Glob/WebFetch/…) but cannot write or run shell commands unless the
|
|
9
|
+
// agent's permissionMode is 'acceptEdits' or 'bypassPermissions' in ChatPanel
|
|
10
|
+
// Settings. The working directory comes from the agent config.
|
|
9
11
|
|
|
10
|
-
import
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
11
13
|
import os from 'node:os';
|
|
12
|
-
import
|
|
13
|
-
import { findAgentBin
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { findAgentBin } from '../env.js';
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Where the Claude Code CLI lives. The SDK ships a bundled cli.js, but inside a
|
|
22
|
-
// compiled binary that file is on a virtual FS that child processes can't reach
|
|
23
|
-
// (it fails on Windows as "B:\~BUN\cli.js"). So in a binary we point the SDK at
|
|
24
|
-
// the user's INSTALLED Claude Code instead — preferring the real cli.js next to
|
|
25
|
-
// the npm shim (modern Node won't spawn a .cmd directly).
|
|
26
|
-
function claudeExecutable() {
|
|
27
|
-
if (process.env.CHATPANEL_CLAUDE_PATH) return process.env.CHATPANEL_CLAUDE_PATH;
|
|
28
|
-
if (!isCompiledBinary()) return undefined; // under node/bun the bundled cli.js works
|
|
29
|
-
const bin = findAgentBin('claude');
|
|
30
|
-
if (!bin) return undefined;
|
|
31
|
-
const dir = path.dirname(bin);
|
|
32
|
-
const candidates = [
|
|
33
|
-
path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
34
|
-
path.join(dir, '..', 'lib', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'),
|
|
35
|
-
];
|
|
36
|
-
for (const c of candidates) if (existsSync(c)) return c;
|
|
37
|
-
return bin;
|
|
38
|
-
}
|
|
17
|
+
const TIMEOUT_MS = Number(process.env.CHATPANEL_CLAUDE_TIMEOUT_MS) || 180_000;
|
|
18
|
+
// Read-only tools allowed without approval in headless mode; writes/shell are
|
|
19
|
+
// gated behind the agent's permission mode.
|
|
20
|
+
const READONLY_TOOLS = ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'TodoWrite', 'Task'];
|
|
39
21
|
|
|
22
|
+
let installed = false;
|
|
23
|
+
let lastProbe = 0;
|
|
40
24
|
export async function available() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
reason: 'Claude Code not found. Install it (npm i -g @anthropic-ai/claude-code), or run the bridge with `npx @chatpanel/bridge`.',
|
|
51
|
-
};
|
|
25
|
+
// Availability = "is claude findable on PATH" (mirrors the codex engine), NOT
|
|
26
|
+
// "does `claude --version` exit 0" (which fails when it just needs login).
|
|
27
|
+
if (!installed && Date.now() - lastProbe > 4000) {
|
|
28
|
+
lastProbe = Date.now();
|
|
29
|
+
try {
|
|
30
|
+
installed = !!findAgentBin('claude');
|
|
31
|
+
} catch {
|
|
32
|
+
installed = false;
|
|
33
|
+
}
|
|
52
34
|
}
|
|
53
|
-
return
|
|
35
|
+
return installed
|
|
36
|
+
? { ok: true }
|
|
37
|
+
: { ok: false, reason: 'Claude Code not found on PATH. Install it (npm i -g @anthropic-ai/claude-code) and run `claude` once to log in.' };
|
|
54
38
|
}
|
|
55
39
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// Build a single prompt that carries the conversation history. The bridge is
|
|
59
|
-
// stateless, so we replay the chat each turn.
|
|
40
|
+
// The bridge is stateless, so we replay the conversation as a single prompt.
|
|
60
41
|
function buildPrompt(messages) {
|
|
61
42
|
const history = messages.slice(0, -1);
|
|
62
43
|
const last = messages[messages.length - 1];
|
|
63
44
|
let prompt = '';
|
|
64
45
|
if (history.length) {
|
|
65
46
|
prompt += 'Conversation so far:\n';
|
|
66
|
-
for (const m of history) {
|
|
67
|
-
prompt += `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}\n\n`;
|
|
68
|
-
}
|
|
47
|
+
for (const m of history) prompt += `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}\n\n`;
|
|
69
48
|
prompt += '---\n\n';
|
|
70
49
|
}
|
|
71
50
|
prompt += last ? last.content : '';
|
|
72
51
|
return prompt;
|
|
73
52
|
}
|
|
74
53
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
54
|
+
// Spawn `claude` and stream its stream-json output, forwarding events via `emit`.
|
|
55
|
+
// `extraArgs` lets complete() run a tool-free single shot. Resolves with the final
|
|
56
|
+
// result text once the process closes 0.
|
|
57
|
+
function runClaude({ prompt, args, cwd, emit }) {
|
|
58
|
+
const bin = findAgentBin('claude') || 'claude';
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
let child;
|
|
61
|
+
try {
|
|
62
|
+
child = spawn(bin, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: process.env });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return reject(new Error(`Failed to start claude: ${e.message}`));
|
|
65
|
+
}
|
|
79
66
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const writesAllowed = permissionMode === 'acceptEdits' || permissionMode === 'bypassPermissions';
|
|
85
|
-
|
|
86
|
-
// Approve read-only tools always; gate writes/shell behind the chosen mode.
|
|
87
|
-
const canUseTool = async (toolName) => {
|
|
88
|
-
if (READONLY_TOOLS.has(toolName) || writesAllowed) return { behavior: 'allow', updatedInput: undefined };
|
|
89
|
-
return { behavior: 'deny', message: `${toolName} blocked — set this agent's permission mode to acceptEdits/bypassPermissions in ChatPanel to enable it.` };
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
let streamedAny = false;
|
|
93
|
-
let resultText = '';
|
|
94
|
-
|
|
95
|
-
const iterator = query({
|
|
96
|
-
prompt: buildPrompt(messages),
|
|
97
|
-
options: {
|
|
98
|
-
cwd,
|
|
99
|
-
permissionMode,
|
|
100
|
-
includePartialMessages: true,
|
|
101
|
-
canUseTool,
|
|
102
|
-
// Default: load your ~/.claude + project settings so your skills, MCP
|
|
103
|
-
// servers and CLAUDE.md apply. Turn the agent's "Use my local skills &
|
|
104
|
-
// config" off to run clean.
|
|
105
|
-
settingSources: options.useLocalConfig === false ? [] : ['user', 'project'],
|
|
106
|
-
// Native Claude Code system prompt. Only append the user's OWN system
|
|
107
|
-
// prompt if they set one — no ChatPanel persona is injected, so the agent
|
|
108
|
-
// is exactly as capable as it is in the terminal.
|
|
109
|
-
systemPrompt: system
|
|
110
|
-
? { type: 'preset', preset: 'claude_code', append: system }
|
|
111
|
-
: { type: 'preset', preset: 'claude_code' },
|
|
112
|
-
...(options.model ? { model: options.model } : {}),
|
|
113
|
-
...(claudeExecutable() ? { pathToClaudeCodeExecutable: claudeExecutable() } : {}),
|
|
114
|
-
...(process.env.CHATPANEL_MAX_TURNS ? { maxTurns: Number(process.env.CHATPANEL_MAX_TURNS) } : {}),
|
|
115
|
-
},
|
|
116
|
-
});
|
|
67
|
+
let stdout = '';
|
|
68
|
+
let stderr = '';
|
|
69
|
+
let streamedAny = false;
|
|
70
|
+
let resultText = '';
|
|
117
71
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
child.kill('SIGKILL');
|
|
74
|
+
reject(new Error(`Claude Code timed out after ${Math.round(TIMEOUT_MS / 1000)}s.`));
|
|
75
|
+
}, TIMEOUT_MS);
|
|
76
|
+
|
|
77
|
+
child.stdout.on('data', (d) => {
|
|
78
|
+
stdout += d.toString();
|
|
79
|
+
let nl;
|
|
80
|
+
while ((nl = stdout.indexOf('\n')) >= 0) {
|
|
81
|
+
const line = stdout.slice(0, nl).trim();
|
|
82
|
+
stdout = stdout.slice(nl + 1);
|
|
83
|
+
if (!line.startsWith('{')) continue;
|
|
84
|
+
let msg;
|
|
85
|
+
try {
|
|
86
|
+
msg = JSON.parse(line);
|
|
87
|
+
} catch {
|
|
88
|
+
continue; // not a JSON event line
|
|
128
89
|
}
|
|
90
|
+
const r = handleMessage(msg, emit, streamedAny);
|
|
91
|
+
if (r.streamed) streamedAny = true;
|
|
92
|
+
if (r.result != null) resultText = r.result;
|
|
129
93
|
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
94
|
+
});
|
|
95
|
+
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
96
|
+
child.on('error', (e) => {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
reject(e);
|
|
99
|
+
});
|
|
100
|
+
child.on('close', (code) => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
if (code === 0) resolve({ streamedAny, resultText });
|
|
103
|
+
else reject(new Error(`Claude Code exited ${code}: ${stderr.trim().split('\n').pop() || 'failed'}`));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
child.stdin.write(prompt);
|
|
107
|
+
child.stdin.end();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Map one stream-json message to emit() calls. Returns { streamed, result }.
|
|
112
|
+
// The CLI's stream-json mirrors the SDK message shapes.
|
|
113
|
+
function handleMessage(msg, emit, alreadyStreamed) {
|
|
114
|
+
const out = { streamed: false, result: null };
|
|
115
|
+
if (msg.type === 'stream_event') {
|
|
116
|
+
const ev = msg.event;
|
|
117
|
+
if (ev?.type === 'content_block_delta') {
|
|
118
|
+
if (ev.delta?.type === 'text_delta') {
|
|
119
|
+
out.streamed = true;
|
|
120
|
+
emit({ type: 'delta', text: ev.delta.text });
|
|
121
|
+
} else if (ev.delta?.type === 'thinking_delta') {
|
|
122
|
+
emit({ type: 'reasoning', text: ev.delta.thinking || '' });
|
|
139
123
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
124
|
+
}
|
|
125
|
+
} else if (msg.type === 'assistant') {
|
|
126
|
+
for (const block of msg.message?.content || []) {
|
|
127
|
+
if (block.type === 'tool_use') {
|
|
128
|
+
emit({ type: 'tool', name: block.name, summary: toolSummary(block) });
|
|
129
|
+
} else if (block.type === 'text' && !alreadyStreamed) {
|
|
130
|
+
out.streamed = true;
|
|
131
|
+
emit({ type: 'delta', text: block.text });
|
|
144
132
|
}
|
|
145
133
|
}
|
|
134
|
+
} else if (msg.type === 'result') {
|
|
135
|
+
if (msg.subtype === 'success') out.result = msg.result || '';
|
|
136
|
+
else emit({ type: 'status', text: `(${msg.subtype})` });
|
|
146
137
|
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
147
140
|
|
|
141
|
+
export async function chat({ messages, system, options }, emit) {
|
|
142
|
+
const permissionMode = options.permissionMode || 'default';
|
|
143
|
+
const cwd = options.workingDir ? path.resolve(options.workingDir) : os.homedir();
|
|
144
|
+
|
|
145
|
+
const args = ['--print', '--output-format', 'stream-json', '--include-partial-messages', '--verbose'];
|
|
146
|
+
|
|
147
|
+
// Gate writes/shell behind the chosen mode; otherwise restrict to read-only
|
|
148
|
+
// tools so headless runs never block on an approval prompt.
|
|
149
|
+
if (permissionMode === 'bypassPermissions') args.push('--permission-mode', 'bypassPermissions');
|
|
150
|
+
else if (permissionMode === 'acceptEdits') args.push('--permission-mode', 'acceptEdits');
|
|
151
|
+
else args.push('--allowedTools', ...READONLY_TOOLS);
|
|
152
|
+
|
|
153
|
+
// Native Claude Code behavior; append the user's own system prompt if they set
|
|
154
|
+
// one (no ChatPanel persona injected).
|
|
155
|
+
if (system) args.push('--append-system-prompt', system);
|
|
156
|
+
if (options.model) args.push('--model', options.model);
|
|
157
|
+
// Default loads your ~/.claude + project settings (skills, MCP, CLAUDE.md).
|
|
158
|
+
// "Use my local skills & config" off → run clean.
|
|
159
|
+
if (options.useLocalConfig === false) args.push('--setting-sources', '');
|
|
160
|
+
|
|
161
|
+
const { streamedAny, resultText } = await runClaude({ prompt: buildPrompt(messages), args, cwd, emit });
|
|
148
162
|
emit({ type: 'done', text: streamedAny ? '' : resultText });
|
|
149
163
|
}
|
|
150
164
|
|
|
151
165
|
// A fast, tool-free single-shot completion — used for prompt autocomplete. No
|
|
152
|
-
//
|
|
153
|
-
// from a fast model (Haiku by default). Returns the completion string.
|
|
166
|
+
// tools, no local config: just a quick text continuation from a fast model.
|
|
154
167
|
export async function complete({ prompt, system, model }) {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
const args = [
|
|
169
|
+
'--print',
|
|
170
|
+
'--output-format', 'stream-json',
|
|
171
|
+
'--verbose',
|
|
172
|
+
'--disallowedTools', 'Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'Bash', 'Edit', 'Write', 'Task', 'TodoWrite',
|
|
173
|
+
'--setting-sources', '',
|
|
174
|
+
'--model', model || 'haiku',
|
|
175
|
+
'--system-prompt', system || "Continue the user's text briefly. Reply with only the continuation.",
|
|
176
|
+
];
|
|
158
177
|
let text = '';
|
|
159
|
-
const
|
|
178
|
+
const { resultText } = await runClaude({
|
|
160
179
|
prompt,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
maxTurns: 1,
|
|
166
|
-
settingSources: [], // skip CLAUDE.md / MCP for a tiny completion
|
|
167
|
-
systemPrompt: system || "Continue the user's text briefly. Reply with only the continuation.",
|
|
168
|
-
model: model || 'haiku',
|
|
169
|
-
...(claudeExecutable() ? { pathToClaudeCodeExecutable: claudeExecutable() } : {}),
|
|
180
|
+
args,
|
|
181
|
+
cwd: os.homedir(),
|
|
182
|
+
emit: (e) => {
|
|
183
|
+
if (e.type === 'delta') text += e.text;
|
|
170
184
|
},
|
|
171
185
|
});
|
|
172
|
-
|
|
173
|
-
if (message.type === 'assistant') {
|
|
174
|
-
for (const block of message.message.content) {
|
|
175
|
-
if (block.type === 'text') text += block.text;
|
|
176
|
-
}
|
|
177
|
-
} else if (message.type === 'result' && message.subtype === 'success' && !text) {
|
|
178
|
-
text = message.result || '';
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return text.trim();
|
|
186
|
+
return (text || resultText || '').trim();
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
function toolSummary(block) {
|
package/src/server.js
CHANGED
|
@@ -22,7 +22,7 @@ import * as gemini from './engines/gemini.js';
|
|
|
22
22
|
import { installService, uninstallService, serviceStatus } from './service.js';
|
|
23
23
|
import { enrichPath, findAgentBin } from './env.js';
|
|
24
24
|
|
|
25
|
-
const VERSION = '0.2.
|
|
25
|
+
const VERSION = '0.2.12';
|
|
26
26
|
const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
|
|
27
27
|
const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
|
|
28
28
|
|