agentgui 1.0.534 → 1.0.536

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/database.js CHANGED
@@ -1038,6 +1038,44 @@ export const queries = {
1038
1038
  }
1039
1039
  },
1040
1040
 
1041
+ deleteAllConversations() {
1042
+ try {
1043
+ const conversations = prep('SELECT id, claudeSessionId FROM conversations').all();
1044
+
1045
+ for (const conv of conversations) {
1046
+ if (conv.claudeSessionId) {
1047
+ this.deleteClaudeSessionFile(conv.claudeSessionId);
1048
+ }
1049
+ }
1050
+
1051
+ const deleteAllStmt = db.transaction(() => {
1052
+ const allSessionIds = prep('SELECT id FROM sessions').all().map(r => r.id);
1053
+
1054
+ prep('DELETE FROM stream_updates');
1055
+ prep('DELETE FROM chunks');
1056
+ prep('DELETE FROM events');
1057
+
1058
+ if (allSessionIds.length > 0) {
1059
+ const placeholders = allSessionIds.map(() => '?').join(',');
1060
+ db.prepare(`DELETE FROM stream_updates WHERE sessionId IN (${placeholders})`).run(...allSessionIds);
1061
+ db.prepare(`DELETE FROM chunks WHERE sessionId IN (${placeholders})`).run(...allSessionIds);
1062
+ db.prepare(`DELETE FROM events WHERE sessionId IN (${placeholders})`).run(...allSessionIds);
1063
+ }
1064
+
1065
+ prep('DELETE FROM sessions');
1066
+ prep('DELETE FROM messages');
1067
+ prep('DELETE FROM conversations');
1068
+ });
1069
+
1070
+ deleteAllStmt();
1071
+ console.log('[deleteAllConversations] Deleted all conversations and associated Claude Code files');
1072
+ return true;
1073
+ } catch (err) {
1074
+ console.error('[deleteAllConversations] Error deleting all conversations:', err.message);
1075
+ return false;
1076
+ }
1077
+ },
1078
+
1041
1079
  cleanup() {
1042
1080
  const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
1043
1081
  const now = Date.now();
@@ -0,0 +1,125 @@
1
+ /**
2
+ * ACP HTTP Client with comprehensive request/response logging
3
+ */
4
+
5
+ function logACPCall(method, url, requestData, responseData, error = null) {
6
+ const timestamp = new Date().toISOString();
7
+ const logEntry = {
8
+ timestamp,
9
+ method,
10
+ url,
11
+ request: requestData,
12
+ response: responseData,
13
+ error: error ? error.message : null
14
+ };
15
+
16
+ console.log('[ACP-HTTP]', JSON.stringify(logEntry, null, 2));
17
+ return logEntry;
18
+ }
19
+
20
+ export async function fetchACPProvider(baseUrl, port) {
21
+ const url = baseUrl + ':' + port + '/provider';
22
+ const startTime = Date.now();
23
+
24
+ try {
25
+ console.log('[ACP-HTTP] → GET ' + url);
26
+
27
+ const response = await fetch(url, {
28
+ method: 'GET',
29
+ headers: { 'Accept': 'application/json' },
30
+ signal: AbortSignal.timeout(3000)
31
+ });
32
+
33
+ const data = response.ok ? await response.json() : null;
34
+ const duration = Date.now() - startTime;
35
+
36
+ logACPCall('GET', url, {
37
+ headers: { 'Accept': 'application/json' },
38
+ timeout: 3000
39
+ }, {
40
+ status: response.status,
41
+ statusText: response.statusText,
42
+ headers: Object.fromEntries(response.headers.entries()),
43
+ body: data,
44
+ duration_ms: duration
45
+ });
46
+
47
+ return { ok: response.ok, status: response.status, data };
48
+ } catch (error) {
49
+ logACPCall('GET', url, { headers: { 'Accept': 'application/json' } }, null, error);
50
+ return { ok: false, status: 0, data: null, error: error.message };
51
+ }
52
+ }
53
+
54
+ export async function fetchACPAgents(baseUrl) {
55
+ const endpoint = baseUrl.endsWith('/') ? baseUrl + 'agents/search' : baseUrl + '/agents/search';
56
+ const requestBody = {};
57
+ const startTime = Date.now();
58
+
59
+ try {
60
+ console.log('[ACP-HTTP] → POST ' + endpoint);
61
+ console.log('[ACP-HTTP] Request body: ' + JSON.stringify(requestBody));
62
+
63
+ const response = await fetch(endpoint, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ 'Accept': 'application/json'
68
+ },
69
+ body: JSON.stringify(requestBody),
70
+ signal: AbortSignal.timeout(5000)
71
+ });
72
+
73
+ const data = response.ok ? await response.json() : null;
74
+ const duration = Date.now() - startTime;
75
+
76
+ logACPCall('POST', endpoint, {
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ 'Accept': 'application/json'
80
+ },
81
+ body: requestBody,
82
+ timeout: 5000
83
+ }, {
84
+ status: response.status,
85
+ statusText: response.statusText,
86
+ headers: Object.fromEntries(response.headers.entries()),
87
+ body: data,
88
+ duration_ms: duration
89
+ });
90
+
91
+ return { ok: response.ok, status: response.status, data };
92
+ } catch (error) {
93
+ logACPCall('POST', endpoint, { body: requestBody }, null, error);
94
+ return { ok: false, status: 0, data: null, error: error.message };
95
+ }
96
+ }
97
+
98
+ export function extractCompleteAgentData(agent) {
99
+ return {
100
+ id: agent.agent_id || agent.id,
101
+ name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
102
+ metadata: {
103
+ ref: {
104
+ name: agent.metadata?.ref?.name,
105
+ version: agent.metadata?.ref?.version,
106
+ url: agent.metadata?.ref?.url,
107
+ tags: agent.metadata?.ref?.tags
108
+ },
109
+ description: agent.metadata?.description,
110
+ author: agent.metadata?.author,
111
+ license: agent.metadata?.license
112
+ },
113
+ specs: agent.specs ? {
114
+ capabilities: agent.specs.capabilities,
115
+ input_schema: agent.specs.input_schema || agent.specs.input,
116
+ output_schema: agent.specs.output_schema || agent.specs.output,
117
+ thread_state_schema: agent.specs.thread_state_schema || agent.specs.thread_state,
118
+ config_schema: agent.specs.config_schema || agent.specs.config,
119
+ custom_streaming_update_schema: agent.specs.custom_streaming_update_schema || agent.specs.custom_streaming_update
120
+ } : null,
121
+ custom_data: agent.custom_data,
122
+ icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
123
+ protocol: 'acp'
124
+ };
125
+ }
@@ -1,74 +1,35 @@
1
- import { spawn } from 'child_process';
2
- import path from 'path';
3
- import os from 'os';
4
- import fs from 'fs';
5
- import { fileURLToPath } from 'url';
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- const projectRoot = path.resolve(__dirname, '..');
9
- const isWindows = os.platform() === 'win32';
1
+ import { startProcess as startProc, scheduleRestart as scheduleRestart, MAX_RESTARTS, RESTART_WINDOW_MS, IDLE_TIMEOUT_MS } from './acp-process-lifecycle.js';
10
2
 
11
3
  const ACP_TOOLS = [
12
4
  { id: 'opencode', cmd: 'opencode', args: ['acp'], port: 18100, npxPkg: 'opencode-ai' },
13
5
  { id: 'kilo', cmd: 'kilo', args: ['acp'], port: 18101, npxPkg: '@kilocode/cli' },
14
6
  ];
15
7
 
16
- const MAX_RESTARTS = 10;
17
- const RESTART_WINDOW_MS = 300000;
18
8
  const HEALTH_INTERVAL_MS = 30000;
19
9
  const STARTUP_GRACE_MS = 5000;
20
- const IDLE_TIMEOUT_MS = 120000;
21
10
  const processes = new Map();
22
11
  let healthTimer = null;
23
12
  let shuttingDown = false;
24
13
 
25
14
  function log(msg) { console.log('[ACP] ' + msg); }
26
15
 
27
- function resolveBinary(cmd) {
28
- const ext = isWindows ? '.cmd' : '';
29
- const localBin = path.join(projectRoot, 'node_modules', '.bin', cmd + ext);
30
- if (fs.existsSync(localBin)) return localBin;
31
- return cmd;
32
- }
33
-
34
16
  function startProcess(tool) {
35
17
  if (shuttingDown) return null;
36
18
  const existing = processes.get(tool.id);
37
19
  if (existing?.process && !existing.process.killed) return existing;
38
20
 
39
- const bin = resolveBinary(tool.cmd);
40
- const args = [...tool.args, '--port', String(tool.port)];
41
- const opts = { stdio: ['pipe', 'pipe', 'pipe'], cwd: process.cwd() };
42
- if (isWindows) opts.shell = true;
43
-
44
- let proc;
45
- try { proc = spawn(bin, args, opts); }
46
- catch (err) { log(tool.id + ' spawn failed: ' + err.message); return null; }
47
-
48
- const entry = {
49
- id: tool.id, port: tool.port, process: proc, pid: proc.pid,
50
- startedAt: Date.now(), restarts: [], healthy: false, lastHealthCheck: 0,
51
- lastUsed: Date.now(), idleTimer: null,
52
- };
53
-
54
- proc.stdout.on('data', () => {});
55
- proc.stderr.on('data', (d) => {
56
- const t = d.toString().trim();
57
- if (t) log(tool.id + ': ' + t.substring(0, 200));
58
- });
59
- proc.stdout.on('error', () => {});
60
- proc.stderr.on('error', () => {});
61
- proc.on('error', (err) => { log(tool.id + ' error: ' + err.message); entry.healthy = false; });
21
+ const entry = startProc(tool, log);
22
+ if (!entry) return null;
62
23
 
63
- proc.on('close', (code) => {
24
+ entry.process.on('close', (code) => {
64
25
  entry.healthy = false;
65
26
  if (shuttingDown || entry._stopping) return;
66
27
  log(tool.id + ' exited code ' + code);
67
- scheduleRestart(tool, entry.restarts);
28
+ scheduleRestart(tool, entry.restarts, log, startProcess, () => shuttingDown);
68
29
  });
69
30
 
70
31
  processes.set(tool.id, entry);
71
- log(tool.id + ' started port ' + tool.port + ' pid ' + proc.pid);
32
+ log(tool.id + ' started port ' + tool.port + ' pid ' + entry.process.pid);
72
33
  setTimeout(() => checkHealth(tool.id), STARTUP_GRACE_MS);
73
34
  resetIdleTimer(tool.id);
74
35
  return entry;
@@ -93,34 +54,19 @@ function stopTool(toolId) {
93
54
  processes.delete(toolId);
94
55
  }
95
56
 
96
- function scheduleRestart(tool, prevRestarts = []) {
97
- if (shuttingDown) return;
98
- const now = Date.now();
99
- const recent = prevRestarts.filter(t => now - t < RESTART_WINDOW_MS);
100
- if (recent.length >= MAX_RESTARTS) {
101
- log(tool.id + ' exceeded restart limit, giving up');
102
- processes.delete(tool.id);
103
- return;
104
- }
105
- const delay = Math.min(1000 * Math.pow(2, recent.length), 30000);
106
- log(tool.id + ' restarting in ' + delay + 'ms');
107
- setTimeout(() => {
108
- if (shuttingDown) return;
109
- const entry = startProcess(tool);
110
- if (entry) entry.restarts = [...recent, Date.now()];
111
- }, delay);
112
- }
113
-
114
57
  async function checkHealth(toolId) {
115
58
  const entry = processes.get(toolId);
116
59
  if (!entry || shuttingDown) return;
117
- try {
118
- const res = await fetch('http://127.0.0.1:' + entry.port + '/provider', {
119
- signal: AbortSignal.timeout(3000), headers: { 'Accept': 'application/json' }
120
- });
121
- entry.healthy = res.ok;
122
- } catch (_) { entry.healthy = false; }
60
+
61
+ const { fetchACPProvider } = await import('./acp-http-client.js');
62
+ const result = await fetchACPProvider('http://127.0.0.1', entry.port);
63
+
64
+ entry.healthy = result.ok;
123
65
  entry.lastHealthCheck = Date.now();
66
+
67
+ if (result.data) {
68
+ entry.providerInfo = result.data;
69
+ }
124
70
  }
125
71
 
126
72
  export async function ensureRunning(agentId) {
@@ -177,6 +123,7 @@ export function getStatus() {
177
123
  id: tool.id, port: tool.port, running: !!e, healthy: e?.healthy || false,
178
124
  pid: e?.pid, uptime: e ? Date.now() - e.startedAt : 0,
179
125
  restartCount: e?.restarts.length || 0, idleMs: e ? Date.now() - e.lastUsed : 0,
126
+ providerInfo: e?.providerInfo || null,
180
127
  };
181
128
  });
182
129
  }
@@ -201,33 +148,17 @@ export async function restart(agentId) {
201
148
  }
202
149
 
203
150
  export async function queryModels(agentId) {
204
- const port = await ensureRunning(agentId);
151
+ const port = getPort(agentId);
205
152
  if (!port) return [];
206
153
  try {
207
- const res = await fetch('http://127.0.0.1:' + port + '/provider', {
208
- signal: AbortSignal.timeout(5000), headers: { 'Accept': 'application/json' }
209
- });
154
+ const res = await fetch('http://127.0.0.1:' + port + '/models');
210
155
  if (!res.ok) return [];
211
156
  const data = await res.json();
212
- const connected = new Set(data.connected || []);
213
- const providers = (data.all || []).filter(p => connected.has(p.id));
214
- const seen = new Map();
215
- for (const prov of providers) {
216
- for (const m of Object.values(prov.models || {})) {
217
- if (!seen.has(m.id)) {
218
- seen.set(m.id, { id: m.id, label: m.name || m.id, provider: prov.name || prov.id });
219
- }
220
- }
221
- }
222
- return Array.from(seen.values());
157
+ return data.models || [];
223
158
  } catch (_) { return []; }
224
159
  }
225
160
 
226
161
  export function isAvailable(agentId) {
227
162
  const tool = ACP_TOOLS.find(t => t.id === agentId);
228
- if (!tool) return false;
229
- const bin = resolveBinary(tool.cmd);
230
- return bin !== tool.cmd || fs.existsSync(bin);
163
+ return !!tool;
231
164
  }
232
-
233
- export const ACP_TOOL_CONFIGS = ACP_TOOLS;
@@ -0,0 +1,65 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import fs from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const projectRoot = path.resolve(__dirname, '..');
9
+ const isWindows = os.platform() === 'win32';
10
+
11
+ export const MAX_RESTARTS = 10;
12
+ export const RESTART_WINDOW_MS = 300000;
13
+ export const IDLE_TIMEOUT_MS = 120000;
14
+
15
+ export function resolveBinary(cmd) {
16
+ const ext = isWindows ? '.cmd' : '';
17
+ const localBin = path.join(projectRoot, 'node_modules', '.bin', cmd + ext);
18
+ if (fs.existsSync(localBin)) return localBin;
19
+ return cmd;
20
+ }
21
+
22
+ export function startProcess(tool, log) {
23
+ const bin = resolveBinary(tool.cmd);
24
+ const args = [...tool.args, '--port', String(tool.port)];
25
+ const opts = { stdio: ['pipe', 'pipe', 'pipe'], cwd: process.cwd() };
26
+ if (isWindows) opts.shell = true;
27
+
28
+ let proc;
29
+ try { proc = spawn(bin, args, opts); }
30
+ catch (err) { log(tool.id + ' spawn failed: ' + err.message); return null; }
31
+
32
+ const entry = {
33
+ id: tool.id, port: tool.port, process: proc, pid: proc.pid,
34
+ startedAt: Date.now(), restarts: [], healthy: false, lastHealthCheck: 0,
35
+ lastUsed: Date.now(), idleTimer: null,
36
+ };
37
+
38
+ proc.stdout.on('data', () => {});
39
+ proc.stderr.on('data', (d) => {
40
+ const t = d.toString().trim();
41
+ if (t) log(tool.id + ': ' + t.substring(0, 200));
42
+ });
43
+ proc.stdout.on('error', () => {});
44
+ proc.stderr.on('error', () => {});
45
+ proc.on('error', (err) => { log(tool.id + ' error: ' + err.message); entry.healthy = false; });
46
+
47
+ return entry;
48
+ }
49
+
50
+ export function scheduleRestart(tool, prevRestarts, log, startProcessFn, shuttingDown) {
51
+ if (shuttingDown()) return;
52
+ const now = Date.now();
53
+ const recent = prevRestarts.filter(t => now - t < RESTART_WINDOW_MS);
54
+ if (recent.length >= MAX_RESTARTS) {
55
+ log(tool.id + ' exceeded restart limit, giving up');
56
+ return null;
57
+ }
58
+ const delay = Math.min(1000 * Math.pow(2, recent.length), 30000);
59
+ log(tool.id + ' restarting in ' + delay + 'ms');
60
+ setTimeout(() => {
61
+ if (shuttingDown()) return;
62
+ const entry = startProcessFn(tool);
63
+ if (entry) entry.restarts = [...recent, Date.now()];
64
+ }, delay);
65
+ }
@@ -43,6 +43,12 @@ export function register(router, deps) {
43
43
  return { deleted: true };
44
44
  });
45
45
 
46
+ router.handle('conv.del.all', (p) => {
47
+ if (!queries.deleteAllConversations()) fail(500, 'Failed to delete all conversations');
48
+ broadcastSync({ type: 'all_conversations_deleted', timestamp: Date.now() });
49
+ return { deleted: true, message: 'All conversations deleted' };
50
+ });
51
+
46
52
  router.handle('conv.full', (p) => {
47
53
  const conv = queries.getConversation(p.id);
48
54
  if (!conv) notFound();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.534",
3
+ "version": "1.0.536",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -397,41 +397,27 @@ function findCommand(cmd) {
397
397
  }
398
398
 
399
399
  async function queryACPServerAgents(baseUrl) {
400
- try {
401
- // Ensure correct endpoint format (handle both /api and /api/ cases)
402
- const endpoint = baseUrl.endsWith('/') ? `${baseUrl}agents/search` : `${baseUrl}/agents/search`;
403
- const response = await fetch(endpoint, {
404
- method: 'POST',
405
- headers: {
406
- 'Content-Type': 'application/json',
407
- },
408
- body: JSON.stringify({}),
409
- });
410
-
411
- if (!response.ok) {
412
- console.error(`Failed to query ACP agents from ${baseUrl}: ${response.status}`);
413
- return [];
414
- }
415
-
416
- const data = await response.json();
417
- if (!data.agents || !Array.isArray(data.agents)) {
418
- console.error(`Invalid agents response from ${baseUrl}`);
419
- return [];
420
- }
421
-
422
- // Convert ACP agent format to our internal format
423
- return data.agents.map(agent => ({
424
- id: agent.agent_id || agent.id,
425
- name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
426
- icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
427
- path: baseUrl,
428
- protocol: 'acp',
429
- description: agent.metadata?.description || '',
430
- }));
431
- } catch (error) {
432
- console.error(`Error querying ACP server ${baseUrl}:`, error.message);
400
+ const { fetchACPAgents, extractCompleteAgentData } = await import('./lib/acp-http-client.js');
401
+
402
+ const result = await fetchACPAgents(baseUrl);
403
+
404
+ if (!result.ok) {
405
+ console.error(`Failed to query ACP agents from ${baseUrl}: ${result.status} ${result.error || ''}`);
406
+ return [];
407
+ }
408
+
409
+ if (!result.data?.agents || !Array.isArray(result.data.agents)) {
410
+ console.error(`Invalid agents response from ${baseUrl}`);
433
411
  return [];
434
412
  }
413
+
414
+ return result.data.agents.map(agent => {
415
+ const complete = extractCompleteAgentData(agent);
416
+ return {
417
+ ...complete,
418
+ path: baseUrl
419
+ };
420
+ });
435
421
  }
436
422
 
437
423
  function discoverAgents() {
package/static/index.html CHANGED
@@ -3043,6 +3043,7 @@
3043
3043
  <div class="sidebar-header">
3044
3044
  <h2>History</h2>
3045
3045
  <div class="sidebar-header-actions">
3046
+ <button id="deleteAllConversationsBtn" class="sidebar-clone-btn" data-delete-all-conversations title="Delete all conversations and Claude Code artifacts">Clear All</button>
3046
3047
  <button id="cloneRepoBtn" class="sidebar-clone-btn" data-clone-repo title="Clone a GitHub repo">Clone</button>
3047
3048
  <button id="newConversationBtn" class="sidebar-new-btn" data-new-conversation title="Start new conversation">+ New</button>
3048
3049
  </div>
@@ -47,6 +47,7 @@ class ConversationManager {
47
47
  this.setupWebSocketListener();
48
48
  this.setupFolderBrowser();
49
49
  this.setupCloneUI();
50
+ this.setupDeleteAllButton();
50
51
 
51
52
  this._pollInterval = setInterval(() => this.loadConversations(), 30000);
52
53
 
@@ -245,6 +246,40 @@ class ConversationManager {
245
246
  }));
246
247
  }
247
248
 
249
+ setupDeleteAllButton() {
250
+ this.deleteAllBtn = document.getElementById('deleteAllConversationsBtn');
251
+ if (!this.deleteAllBtn) return;
252
+ this.deleteAllBtn.addEventListener('click', () => this.confirmDeleteAll());
253
+ }
254
+
255
+ async confirmDeleteAll() {
256
+ if (this.conversations.length === 0) {
257
+ window.UIDialog.alert('No conversations to delete', 'Information');
258
+ return;
259
+ }
260
+
261
+ const confirmed = await window.UIDialog.confirm(
262
+ `Delete all ${this.conversations.length} conversation(s) and associated Claude Code artifacts?\n\nThis action cannot be undone.`,
263
+ 'Delete All Conversations'
264
+ );
265
+ if (!confirmed) return;
266
+
267
+ try {
268
+ this.deleteAllBtn.disabled = true;
269
+ await window.wsClient.rpc('conv.del.all', {});
270
+ console.log('[ConversationManager] Deleted all conversations');
271
+ this.conversations = [];
272
+ this.activeId = null;
273
+ window.dispatchEvent(new CustomEvent('conversation-deselected'));
274
+ this.render();
275
+ } catch (err) {
276
+ console.error('[ConversationManager] Delete all error:', err);
277
+ window.UIDialog.alert('Failed to delete all conversations: ' + (err.message || 'Unknown error'), 'Error');
278
+ } finally {
279
+ this.deleteAllBtn.disabled = false;
280
+ }
281
+ }
282
+
248
283
  setupCloneUI() {
249
284
  this.cloneBtn = document.getElementById('cloneRepoBtn');
250
285
  this.cloneBar = document.getElementById('cloneInputBar');