agentgui 1.0.39 → 1.0.41

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/CLAUDE.md ADDED
File without changes
package/acp-launcher.js CHANGED
@@ -1,37 +1,4 @@
1
- import { spawn } from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
-
6
- // Common paths where claude-code-acp might be installed
7
- const CLAUDE_CODE_ACP_PATHS = [
8
- '/config/.gmweb/npm-global/bin/claude-code-acp',
9
- '/usr/local/bin/claude-code-acp',
10
- '/usr/bin/claude-code-acp',
11
- path.join(os.homedir(), '.local/bin/claude-code-acp'),
12
- path.join(os.homedir(), '.gmweb/npm-global/bin/claude-code-acp'),
13
- 'claude-code-acp', // fallback to PATH
14
- ];
15
-
16
- // Common paths where opencode might be installed
17
- const OPENCODE_PATHS = [
18
- '/usr/local/bin/opencode',
19
- '/usr/bin/opencode',
20
- path.join(os.homedir(), '.local/bin/opencode'),
21
- 'opencode', // fallback to PATH
22
- ];
23
-
24
- function findBinary(paths) {
25
- for (const p of paths) {
26
- try {
27
- fs.accessSync(p, fs.constants.X_OK);
28
- return p;
29
- } catch (_) {
30
- continue;
31
- }
32
- }
33
- return null;
34
- }
1
+ import { createClient } from 'claude-code-acp';
35
2
 
36
3
  const RIPPLEUI_SYSTEM_PROMPT = `CRITICAL INSTRUCTION: You are responding in a web-based HTML interface. EVERY response must be formatted as beautiful, styled HTML using RippleUI and Tailwind CSS. This is NOT a text-based interface - users see raw HTML rendered in their browser.
37
4
 
@@ -94,289 +61,138 @@ MANDATORY RULES:
94
61
  ✓ Use color classes: text-gray-700, bg-blue-50, border-blue-500
95
62
  ✓ Make visual hierarchy clear: use different font sizes, colors, cards
96
63
 
97
- EXAMPLES OF COMPLETE RESPONSES:
98
-
99
- Example 1 - Answer:
100
- <div class="space-y-4 p-6"><h2 class="text-2xl font-bold">Explanation</h2><p class="text-gray-700">Here is the detailed explanation...</p></div>
101
-
102
- Example 2 - Code:
103
- <div class="space-y-4 p-6"><h3 class="text-xl font-bold">JavaScript Function</h3><pre class="bg-gray-900 text-white p-4 rounded overflow-x-auto"><code>const greet = () => console.log('Hello');</code></pre></div>
104
-
105
- Example 3 - Multiple sections:
106
- <div class="space-y-4 p-6"><h2 class="text-2xl font-bold">Topic</h2><div class="card bg-white shadow p-4"><h3 class="font-bold">Section 1</h3><p>Content here</p></div><div class="card bg-white shadow p-4"><h3 class="font-bold">Section 2</h3><p>More content</p></div></div>
107
-
108
64
  YOU MUST ALWAYS OUTPUT VALID, COMPLETE HTML.
109
65
  The user's interface shows YOUR HTML directly - make it beautiful, well-organized, and professional.`;
110
66
 
111
67
  export default class ACPConnection {
112
68
  constructor() {
113
- this.child = null;
114
- this.buffer = '';
115
- this.nextRequestId = 1;
116
- this.pendingRequests = new Map();
69
+ this.client = null;
117
70
  this.sessionId = null;
118
71
  this.onUpdate = null;
119
- this.printMode = false;
120
- this.cwd = '/config';
121
72
  }
122
73
 
74
+ /**
75
+ * Connect to ACP bridge and create session
76
+ */
123
77
  async connect(agentType, cwd) {
124
- this.cwd = cwd;
125
-
126
- const acpSetup = async () => {
127
- await this._spawnACP(agentType, cwd);
128
- await this.sendRequest('initialize', {
129
- protocolVersion: 1,
130
- clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
131
- }, 10000);
132
- const result = await this.sendRequest('session/new', { cwd, mcpServers: [] }, 30000);
133
- this.sessionId = result.sessionId;
134
- await this.sendRequest('session/set_mode', { sessionId: this.sessionId, modeId: 'bypassPermissions' }, 10000);
135
- };
136
-
137
- const deadline = new Promise((_, reject) => setTimeout(() => reject(new Error('ACP handshake timeout (60s)')), 60000));
138
-
139
78
  try {
140
- await Promise.race([acpSetup(), deadline]);
141
- console.log(`[ACP] Connected via ACP bridge (${agentType})`);
142
- } catch (acpErr) {
143
- console.log(`[ACP] Bridge failed: ${acpErr.message}`);
144
- console.log(`[ACP] Falling back to claude --print mode`);
145
- this.printMode = true;
146
- this.sessionId = 'print-' + Date.now();
147
- if (this.child) {
148
- try { this.child.kill('SIGTERM'); } catch (_) {}
149
- this.child = null;
150
- }
151
- for (const [id, req] of this.pendingRequests) {
152
- clearTimeout(req.timeoutId);
153
- }
154
- this.pendingRequests.clear();
155
- }
156
- }
157
-
158
- _spawnACP(agentType, cwd) {
159
- return new Promise((resolve, reject) => {
160
- const env = { ...process.env };
161
- delete env.NODE_OPTIONS;
162
- delete env.NODE_INSPECT;
163
- delete env.NODE_DEBUG;
164
-
165
- // Ensure npm global bin directories are in PATH
166
- const npmGlobalBins = [
167
- '/config/.gmweb/npm-global/bin',
168
- path.join(os.homedir(), '.gmweb/npm-global/bin'),
169
- path.join(os.homedir(), '.local/bin'),
170
- '/usr/local/bin',
171
- ];
172
- const currentPath = env.PATH || '';
173
- const newPathEntries = npmGlobalBins.filter(p => !currentPath.includes(p));
174
- if (newPathEntries.length > 0) {
175
- env.PATH = [...newPathEntries, currentPath].join(':');
176
- }
79
+ console.log(`[ACP] Connecting to ${agentType}...`);
177
80
 
178
- try {
179
- let cmd;
180
- let args;
181
- if (agentType === 'opencode') {
182
- cmd = findBinary(OPENCODE_PATHS);
183
- args = ['acp'];
184
- } else {
185
- cmd = findBinary(CLAUDE_CODE_ACP_PATHS);
186
- args = [];
187
- }
188
-
189
- if (!cmd) {
190
- reject(new Error(`Could not find ${agentType} ACP binary. Please ensure ${agentType === 'opencode' ? 'opencode' : 'claude-code-acp'} is installed and in your PATH.`));
191
- return;
192
- }
193
-
194
- this.child = spawn(cmd, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env, shell: false });
195
- } catch (err) {
196
- reject(new Error(`Failed to spawn ACP: ${err.message}`));
197
- return;
198
- }
199
-
200
- this.child.stderr.on('data', d => console.error(`[ACP:stderr]`, d.toString().trim()));
201
- this.child.on('error', err => reject(new Error(`ACP spawn error: ${err.message}`)));
202
- this.child.on('exit', () => {
203
- this.child = null;
204
- for (const [id, req] of this.pendingRequests) {
205
- req.reject(new Error('ACP process exited'));
206
- clearTimeout(req.timeoutId);
207
- }
208
- this.pendingRequests.clear();
209
- });
210
-
211
- this.child.stdout.setEncoding('utf8');
212
- this.child.stdout.on('data', data => {
213
- this.buffer += data;
214
- const lines = this.buffer.split('\n');
215
- this.buffer = lines.pop() || '';
216
- for (const line of lines) {
217
- if (!line.trim()) continue;
218
- try { this.handleMessage(JSON.parse(line)); }
219
- catch (e) { console.error('[ACP:parse]', line.substring(0, 200), e.message); }
220
- }
81
+ // Create client directly from npm module
82
+ this.client = await createClient({
83
+ agent: agentType === 'opencode' ? 'opencode' : 'claude-code',
84
+ cwd
221
85
  });
222
86
 
223
- setTimeout(resolve, 300);
224
- });
225
- }
226
-
227
- handleMessage(msg) {
228
- if (msg.method) { this.handleIncoming(msg); return; }
229
- if (msg.id !== undefined && this.pendingRequests.has(msg.id)) {
230
- const req = this.pendingRequests.get(msg.id);
231
- this.pendingRequests.delete(msg.id);
232
- clearTimeout(req.timeoutId);
233
- if (msg.error) req.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
234
- else req.resolve(msg.result);
87
+ console.log(`[ACP] ✅ Connected to ${agentType} (direct module)`);
88
+ } catch (err) {
89
+ console.error(`[ACP] ❌ FATAL: Connection failed: ${err.message}`);
90
+ throw new Error(`ACP connection failed for ${agentType}: ${err.message}`);
235
91
  }
236
92
  }
237
93
 
238
- handleIncoming(msg) {
239
- if (msg.method === 'session/update' && msg.params) {
240
- if (this.onUpdate) this.onUpdate(msg.params);
241
- this.resetPromptTimeout();
242
- return;
243
- }
244
- if (msg.method === 'session/request_permission' && msg.id !== undefined) {
245
- this.sendResponse(msg.id, { outcome: { outcome: 'selected', optionId: 'allow' } });
246
- this.resetPromptTimeout();
247
- return;
248
- }
249
- if (msg.method === 'fs/read_text_file' && msg.id !== undefined) {
250
- try { this.sendResponse(msg.id, { content: fs.readFileSync(msg.params?.path, 'utf-8') }); }
251
- catch (e) { this.sendError(msg.id, -32000, e.message); }
252
- return;
253
- }
254
- if (msg.method === 'fs/write_text_file' && msg.id !== undefined) {
255
- try { fs.writeFileSync(msg.params?.path, msg.params?.content, 'utf-8'); this.sendResponse(msg.id, null); }
256
- catch (e) { this.sendError(msg.id, -32000, e.message); }
257
- return;
258
- }
259
- }
260
-
261
- resetPromptTimeout() {
262
- for (const [id, req] of this.pendingRequests) {
263
- if (req.method === 'session/prompt') {
264
- clearTimeout(req.timeoutId);
265
- req.timeoutId = setTimeout(() => {
266
- this.pendingRequests.delete(id);
267
- req.reject(new Error('session/prompt timeout'));
268
- }, 300000);
269
- }
270
- }
271
- }
272
-
273
- sendRequest(method, params, timeoutMs = 60000) {
274
- return new Promise((resolve, reject) => {
275
- if (!this.child) { reject(new Error('ACP not connected')); return; }
276
- const id = this.nextRequestId++;
277
- const timeoutId = setTimeout(() => {
278
- this.pendingRequests.delete(id);
279
- reject(new Error(`${method} timeout (${timeoutMs}ms)`));
280
- }, timeoutMs);
281
- this.pendingRequests.set(id, { resolve, reject, timeoutId, method });
282
- this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, ...(params && { params }) }) + '\n');
283
- });
284
- }
285
-
286
- sendResponse(id, result) {
287
- if (!this.child) return;
288
- this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
289
- }
290
-
291
- sendError(id, code, message) {
292
- if (!this.child) return;
293
- this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n');
294
- }
295
-
94
+ /**
95
+ * Initialize ACP session
96
+ */
296
97
  async initialize() {
297
- if (this.printMode) return {};
298
- return this.sendRequest('initialize', {
98
+ if (!this.client) throw new Error('ACP not connected');
99
+ return this.client.request('initialize', {
299
100
  protocolVersion: 1,
300
- clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
101
+ clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }
301
102
  });
302
103
  }
303
104
 
105
+ /**
106
+ * Create new session
107
+ */
304
108
  async newSession(cwd) {
305
- if (this.printMode) {
306
- this.cwd = cwd;
307
- return { sessionId: this.sessionId };
308
- }
309
- const result = await this.sendRequest('session/new', { cwd, mcpServers: [] }, 120000);
109
+ if (!this.client) throw new Error('ACP not connected');
110
+ const result = await this.client.request('session/new', { cwd, mcpServers: [] });
310
111
  this.sessionId = result.sessionId;
311
112
  return result;
312
113
  }
313
114
 
115
+ /**
116
+ * Set session mode
117
+ */
314
118
  async setSessionMode(modeId) {
315
- if (this.printMode) return {};
316
- return this.sendRequest('session/set_mode', { sessionId: this.sessionId, modeId });
119
+ if (!this.client) throw new Error('ACP not connected');
120
+ return this.client.request('session/set_mode', { sessionId: this.sessionId, modeId });
317
121
  }
318
122
 
123
+ /**
124
+ * Inject skills and system prompt
125
+ */
319
126
  async injectSkills(additionalContext = '') {
320
- if (this.printMode) return {};
321
-
322
- // Combine the system prompt with any additional context
323
- const systemPrompt = additionalContext ? `${RIPPLEUI_SYSTEM_PROMPT}\n\n---\n\n${additionalContext}` : RIPPLEUI_SYSTEM_PROMPT;
324
-
325
- try {
326
- const result = await this.sendRequest('session/skill_inject', {
327
- sessionId: this.sessionId,
328
- skills: [],
329
- notification: [{ type: 'text', text: systemPrompt }]
330
- });
331
- return result;
332
- } catch (e) {
333
- // skill_inject may not be supported, try alternative methods
334
- console.log(`[ACP] skill_inject failed (may not be supported): ${e.message}`);
335
- return null;
336
- }
127
+ if (!this.client) throw new Error('ACP not connected');
128
+
129
+ const systemPrompt = additionalContext
130
+ ? `${RIPPLEUI_SYSTEM_PROMPT}\n\n---\n\n${additionalContext}`
131
+ : RIPPLEUI_SYSTEM_PROMPT;
132
+
133
+ return this.client.request('session/skill_inject', {
134
+ sessionId: this.sessionId,
135
+ skills: [],
136
+ notification: [{ type: 'text', text: systemPrompt }]
137
+ });
337
138
  }
338
139
 
339
140
  /**
340
- * Inject system prompt as initial context
141
+ * Inject system context
341
142
  */
342
143
  async injectSystemContext() {
343
- if (this.printMode) return {};
344
-
345
- // Some versions may need the system prompt sent as the first message context
346
- try {
347
- await this.sendRequest('session/context', {
348
- sessionId: this.sessionId,
349
- context: RIPPLEUI_SYSTEM_PROMPT,
350
- role: 'system'
351
- });
352
- return { success: true };
353
- } catch (e) {
354
- // This method may not be supported, silently fail
355
- return null;
356
- }
144
+ if (!this.client) throw new Error('ACP not connected');
145
+
146
+ return this.client.request('session/context', {
147
+ sessionId: this.sessionId,
148
+ context: RIPPLEUI_SYSTEM_PROMPT,
149
+ role: 'system'
150
+ });
357
151
  }
358
152
 
153
+ /**
154
+ * Send prompt and stream updates
155
+ */
359
156
  async sendPrompt(prompt) {
360
- if (this.printMode) return this._sendPrintPrompt(prompt);
157
+ if (!this.client) throw new Error('ACP not connected');
158
+
361
159
  const promptContent = Array.isArray(prompt) ? prompt : [{ type: 'text', text: prompt }];
362
- return this.sendRequest('session/prompt', { sessionId: this.sessionId, prompt: promptContent }, 300000);
363
- }
364
160
 
365
- async _sendPrintPrompt(prompt) {
366
- throw new Error('Claude Code uses OAuth and requires the ACP bridge. The fallback to direct API calls is not supported because OAuth tokens cannot be used with the Anthropic API directly. Please ensure claude-code-acp is available in your PATH.');
161
+ // Setup update handler before sending
162
+ if (this.onUpdate) {
163
+ this.client.on('update', (update) => {
164
+ // Forward updates immediately with no delay
165
+ this.onUpdate({ update });
166
+ });
167
+ }
168
+
169
+ // Send prompt and get result
170
+ return this.client.request('session/prompt', {
171
+ sessionId: this.sessionId,
172
+ prompt: promptContent
173
+ }, 300000);
367
174
  }
368
175
 
176
+ /**
177
+ * Check if connection is running
178
+ */
369
179
  isRunning() {
370
- if (this.printMode) return true;
371
- return this.child && !this.child.killed;
180
+ return this.client !== null;
372
181
  }
373
182
 
183
+ /**
184
+ * Terminate connection
185
+ */
374
186
  async terminate() {
375
- if (this.printMode) { this.printMode = false; return; }
376
- if (!this.child) return;
377
- this.child.stdin.end();
378
- this.child.kill('SIGTERM');
379
- await new Promise(r => { this.child?.on('exit', r); setTimeout(r, 5000); });
380
- this.child = null;
187
+ if (!this.client) return;
188
+
189
+ try {
190
+ await this.client.close();
191
+ } catch (err) {
192
+ console.error(`[ACP] Error during terminate: ${err.message}`);
193
+ } finally {
194
+ this.client = null;
195
+ this.sessionId = null;
196
+ }
381
197
  }
382
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -18,15 +18,11 @@
18
18
  "homepage": "https://github.com/AnEntrypoint/agentgui#readme",
19
19
  "scripts": {
20
20
  "start": "node server.js",
21
- "start:bun": "bun run server-bun.js",
22
- "dev": "node server.js --watch",
23
- "dev:bun": "bun run server-bun.js --watch",
24
- "test": "node run-browser-tests.js",
25
- "test:integration": "./test-integration.sh",
26
- "test:all": "npm run test:integration && npm run test"
21
+ "dev": "node server.js --watch"
27
22
  },
28
23
  "dependencies": {
29
24
  "better-sqlite3": "^12.6.2",
25
+ "claude-code-acp": "^1.0.0",
30
26
  "ws": "^8.14.2"
31
27
  }
32
28
  }
package/server.js CHANGED
@@ -551,10 +551,10 @@ async function processMessage(conversationId, messageId, sessionId, content, age
551
551
  console.error(`[processMessage] State history: ${JSON.stringify(summary, null, 2)}`);
552
552
 
553
553
  } finally {
554
- // Cleanup: remove from state store after completion
555
- setTimeout(() => {
554
+ // Cleanup: remove from state store immediately (async to not block)
555
+ setImmediate(() => {
556
556
  sessionStateStore.remove(sessionId);
557
- }, 5000);
557
+ });
558
558
 
559
559
  // Log final state
560
560
  console.log(`[processMessage] Final state: ${stateManager.getState()}`);
package/stream-handler.js CHANGED
@@ -62,15 +62,9 @@ export class StreamHandler {
62
62
  this.sequence = persistedUpdate.sequence;
63
63
  this.updateCount++;
64
64
 
65
- // Validate consistency after write
66
- const validation = StateValidator.validateSession(this.sessionId);
67
- if (!validation.valid) {
68
- console.error(`[StreamHandler] State validation failed after update:`, validation);
69
- // Log but continue - database is still source of truth
70
- }
71
-
72
65
  // CRITICAL: Broadcast happens AFTER database write confirms
73
66
  // This ensures clients see data that's already persisted
67
+ // Broadcast immediately with zero delay
74
68
  this.broadcastFn({
75
69
  type: 'stream_update',
76
70
  sessionId: this.sessionId,
@@ -79,8 +73,19 @@ export class StreamHandler {
79
73
  update: persistedUpdate.content,
80
74
  sequence: this.sequence,
81
75
  persisted: true,
82
- timestamp: persistedUpdate.created_at,
83
- validation: validation.valid ? undefined : { error: validation.error }
76
+ timestamp: persistedUpdate.created_at
77
+ });
78
+
79
+ // Validate consistency asynchronously (don't block broadcast)
80
+ setImmediate(() => {
81
+ try {
82
+ const validation = StateValidator.validateSession(this.sessionId);
83
+ if (!validation.valid) {
84
+ console.error(`[StreamHandler] State validation failed: ${validation.error}`);
85
+ }
86
+ } catch (validationErr) {
87
+ console.error(`[StreamHandler] Validation error: ${validationErr.message}`);
88
+ }
84
89
  });
85
90
  } catch (err) {
86
91
  console.error(`[StreamHandler] Error persisting update: ${err.message}`);
Binary file