@chatpanel/bridge 0.3.0 → 0.3.1

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.3.0",
3
+ "version": "0.3.1",
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": [
@@ -18,7 +18,11 @@ import os from 'node:os';
18
18
  import path from 'node:path';
19
19
  import { resolveClaude, buildSpawnSpec, isCompiledBinary } from '../env.js';
20
20
 
21
- const TIMEOUT_MS = Number(process.env.CHATPANEL_CLAUDE_TIMEOUT_MS) || 180_000;
21
+ // Idle timeout: kill the run only after this long with NO output. The timer
22
+ // re-arms on every stdout/stderr chunk, so a task that keeps streaming can run
23
+ // indefinitely — only a truly stuck/silent process is killed. Override with
24
+ // CHATPANEL_CLAUDE_TIMEOUT_MS (ms).
25
+ const IDLE_MS = Number(process.env.CHATPANEL_CLAUDE_TIMEOUT_MS) || 180_000;
22
26
  // Read-only tools allowed without approval in headless mode; writes/shell are
23
27
  // gated behind the agent's permission mode.
24
28
  const READONLY_TOOLS = ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'TodoWrite', 'Task'];
@@ -86,12 +90,18 @@ function runClaude({ prompt, args, cwd, emit }) {
86
90
  let streamedAny = false;
87
91
  let resultText = '';
88
92
 
89
- const timer = setTimeout(() => {
90
- child.kill('SIGKILL');
91
- reject(new Error(`Claude Code timed out after ${Math.round(TIMEOUT_MS / 1000)}s.`));
92
- }, TIMEOUT_MS);
93
+ let idleTimer;
94
+ const armIdle = () => {
95
+ clearTimeout(idleTimer);
96
+ idleTimer = setTimeout(() => {
97
+ child.kill('SIGKILL');
98
+ reject(new Error(`Claude Code timed out — no output for ${Math.round(IDLE_MS / 1000)}s.`));
99
+ }, IDLE_MS);
100
+ };
101
+ armIdle();
93
102
 
94
103
  child.stdout.on('data', (d) => {
104
+ armIdle();
95
105
  stdout += d.toString();
96
106
  let nl;
97
107
  while ((nl = stdout.indexOf('\n')) >= 0) {
@@ -109,13 +119,13 @@ function runClaude({ prompt, args, cwd, emit }) {
109
119
  if (r.result != null) resultText = r.result;
110
120
  }
111
121
  });
112
- child.stderr.on('data', (d) => (stderr += d.toString()));
122
+ child.stderr.on('data', (d) => { armIdle(); stderr += d.toString(); });
113
123
  child.on('error', (e) => {
114
- clearTimeout(timer);
124
+ clearTimeout(idleTimer);
115
125
  reject(new Error(`Failed to start claude (${bin}): ${e.message}`));
116
126
  });
117
127
  child.on('close', (code) => {
118
- clearTimeout(timer);
128
+ clearTimeout(idleTimer);
119
129
  if (code === 0) resolve({ streamedAny, resultText });
120
130
  else reject(new Error(`Claude Code exited ${code}: ${stderr.trim().split('\n').pop() || 'failed'}`));
121
131
  });
@@ -21,7 +21,10 @@ import os from 'node:os';
21
21
  import path from 'node:path';
22
22
  import { findAgentBin } from '../env.js';
23
23
 
24
- const TIMEOUT_MS = Number(process.env.CHATPANEL_CODEX_TIMEOUT_MS) || 180_000;
24
+ // Idle timeout: re-armed on every stdout/stderr chunk, so a long run that keeps
25
+ // streaming never trips it — only true silence does. Override with
26
+ // CHATPANEL_CODEX_TIMEOUT_MS (ms).
27
+ const IDLE_MS = Number(process.env.CHATPANEL_CODEX_TIMEOUT_MS) || 180_000;
25
28
  const REASONING = process.env.CHATPANEL_CODEX_EFFORT ?? 'low'; // '' → respect config
26
29
 
27
30
  const SCRATCH = path.join(os.tmpdir(), 'chatpanel-codex-scratch');
@@ -131,12 +134,18 @@ export async function chat({ messages, system, options }, emit) {
131
134
 
132
135
  let stdout = '';
133
136
  let stderr = '';
134
- const timer = setTimeout(() => {
135
- child.kill('SIGKILL');
136
- reject(new Error(`Codex timed out after ${Math.round(TIMEOUT_MS / 1000)}s.`));
137
- }, TIMEOUT_MS);
137
+ let idleTimer;
138
+ const armIdle = () => {
139
+ clearTimeout(idleTimer);
140
+ idleTimer = setTimeout(() => {
141
+ child.kill('SIGKILL');
142
+ reject(new Error(`Codex timed out — no output for ${Math.round(IDLE_MS / 1000)}s.`));
143
+ }, IDLE_MS);
144
+ };
145
+ armIdle();
138
146
 
139
147
  child.stdout.on('data', (d) => {
148
+ armIdle();
140
149
  stdout += d.toString();
141
150
  let nl;
142
151
  while ((nl = stdout.indexOf('\n')) >= 0) {
@@ -150,13 +159,13 @@ export async function chat({ messages, system, options }, emit) {
150
159
  }
151
160
  }
152
161
  });
153
- child.stderr.on('data', (d) => (stderr += d.toString()));
162
+ child.stderr.on('data', (d) => { armIdle(); stderr += d.toString(); });
154
163
  child.on('error', (e) => {
155
- clearTimeout(timer);
164
+ clearTimeout(idleTimer);
156
165
  reject(e);
157
166
  });
158
167
  child.on('close', async (code) => {
159
- clearTimeout(timer);
168
+ clearTimeout(idleTimer);
160
169
  let text = '';
161
170
  try {
162
171
  text = (await readFile(outFile, 'utf8')).trim();
@@ -21,7 +21,10 @@ import { resolveCommand, buildSpawnSpec } from '../env.js';
21
21
  import { isProEntitled } from '../entitlement.js';
22
22
  import { handleMessage } from './claude.js';
23
23
 
24
- const TIMEOUT_MS = Number(process.env.CHATPANEL_CUSTOM_TIMEOUT_MS) || 180_000;
24
+ // Idle timeout: re-armed on every stdout/stderr chunk, so a long run that keeps
25
+ // streaming never trips it — only true silence does. Override with
26
+ // CHATPANEL_CUSTOM_TIMEOUT_MS (ms).
27
+ const IDLE_MS = Number(process.env.CHATPANEL_CUSTOM_TIMEOUT_MS) || 180_000;
25
28
 
26
29
  export async function available() {
27
30
  // The engine ships in every bridge; individual custom agents are user-defined
@@ -98,12 +101,18 @@ export async function chat({ messages, system, options }, emit) {
98
101
  let resultText = '';
99
102
  let jsonBuf = '';
100
103
 
101
- const timer = setTimeout(() => {
102
- child.kill('SIGKILL');
103
- reject(new Error(`${label} timed out after ${Math.round(TIMEOUT_MS / 1000)}s.`));
104
- }, TIMEOUT_MS);
104
+ let idleTimer;
105
+ const armIdle = () => {
106
+ clearTimeout(idleTimer);
107
+ idleTimer = setTimeout(() => {
108
+ child.kill('SIGKILL');
109
+ reject(new Error(`${label} timed out — no output for ${Math.round(IDLE_MS / 1000)}s.`));
110
+ }, IDLE_MS);
111
+ };
112
+ armIdle();
105
113
 
106
114
  child.stdout.on('data', (d) => {
115
+ armIdle();
107
116
  const s = d.toString();
108
117
  if (fmt === 'claude-stream-json') {
109
118
  jsonBuf += s;
@@ -127,13 +136,13 @@ export async function chat({ messages, system, options }, emit) {
127
136
  emit({ type: 'delta', text: s });
128
137
  }
129
138
  });
130
- child.stderr.on('data', (d) => (stderr += d.toString()));
139
+ child.stderr.on('data', (d) => { armIdle(); stderr += d.toString(); });
131
140
  child.on('error', (e) => {
132
- clearTimeout(timer);
141
+ clearTimeout(idleTimer);
133
142
  reject(new Error(`Failed to start ${label}: ${e.message}`));
134
143
  });
135
144
  child.on('close', (code) => {
136
- clearTimeout(timer);
145
+ clearTimeout(idleTimer);
137
146
  if (code === 0) {
138
147
  emit({ type: 'done', text: streamedAny ? '' : resultText });
139
148
  resolve();
@@ -14,7 +14,10 @@ import os from 'node:os';
14
14
  import path from 'node:path';
15
15
  import { findAgentBin } from '../env.js';
16
16
 
17
- const TIMEOUT_MS = Number(process.env.CHATPANEL_GEMINI_TIMEOUT_MS) || 180_000;
17
+ // Idle timeout: re-armed on every stdout/stderr chunk, so a long run that keeps
18
+ // streaming never trips it — only true silence does. Override with
19
+ // CHATPANEL_GEMINI_TIMEOUT_MS (ms).
20
+ const IDLE_MS = Number(process.env.CHATPANEL_GEMINI_TIMEOUT_MS) || 180_000;
18
21
  const SCRATCH = path.join(os.tmpdir(), 'chatpanel-gemini-scratch');
19
22
 
20
23
  let installed = false;
@@ -76,24 +79,30 @@ export async function chat({ messages, system, options }, emit) {
76
79
  let out = '';
77
80
  let err = '';
78
81
  let streamed = false;
79
- const timer = setTimeout(() => {
80
- child.kill('SIGKILL');
81
- reject(new Error(`Gemini timed out after ${Math.round(TIMEOUT_MS / 1000)}s.`));
82
- }, TIMEOUT_MS);
82
+ let idleTimer;
83
+ const armIdle = () => {
84
+ clearTimeout(idleTimer);
85
+ idleTimer = setTimeout(() => {
86
+ child.kill('SIGKILL');
87
+ reject(new Error(`Gemini timed out — no output for ${Math.round(IDLE_MS / 1000)}s.`));
88
+ }, IDLE_MS);
89
+ };
90
+ armIdle();
83
91
 
84
92
  child.stdout.on('data', (d) => {
93
+ armIdle();
85
94
  const s = d.toString();
86
95
  out += s;
87
96
  streamed = true;
88
97
  emit({ type: 'delta', text: s });
89
98
  });
90
- child.stderr.on('data', (d) => (err += d.toString()));
99
+ child.stderr.on('data', (d) => { armIdle(); err += d.toString(); });
91
100
  child.on('error', (e) => {
92
- clearTimeout(timer);
101
+ clearTimeout(idleTimer);
93
102
  reject(new Error(`Failed to start gemini: ${e.message}`));
94
103
  });
95
104
  child.on('close', (code) => {
96
- clearTimeout(timer);
105
+ clearTimeout(idleTimer);
97
106
  if (code === 0) {
98
107
  if (!streamed) emit({ type: 'delta', text: out.trim() || '(no output)' });
99
108
  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.3.0';
30
+ const VERSION = '0.3.1';
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