agentgui 1.0.831 → 1.0.833
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/CHANGELOG.md +9 -0
- package/database-migrations-acp.js +133 -0
- package/database-migrations.js +149 -0
- package/database-schema.js +175 -0
- package/database.js +80 -650
- package/lib/claude-runner-acp.js +155 -0
- package/lib/claude-runner-agents.js +104 -0
- package/lib/claude-runner-direct.js +116 -0
- package/lib/claude-runner-run.js +49 -0
- package/lib/claude-runner.js +55 -1266
- package/lib/plugins/agents-plugin.js +0 -3
- package/lib/speech-manager.js +1 -7
- package/lib/ws-handlers-conv.js +0 -144
- package/lib/ws-handlers-conv2.js +169 -0
- package/lib/ws-handlers-session.js +3 -117
- package/lib/ws-handlers-session2.js +106 -0
- package/package.json +1 -1
- package/server.js +12 -1
package/lib/speech-manager.js
CHANGED
|
@@ -2,7 +2,6 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
|
-
|
|
6
5
|
let speechModule = null;
|
|
7
6
|
let _broadcastSync = null;
|
|
8
7
|
let _syncClients = null;
|
|
@@ -13,18 +12,15 @@ export function initSpeechManager({ broadcastSync, syncClients, queries }) {
|
|
|
13
12
|
_syncClients = syncClients;
|
|
14
13
|
_queries = queries;
|
|
15
14
|
}
|
|
16
|
-
|
|
17
15
|
export async function ensurePocketTtsSetup(onProgress) {
|
|
18
16
|
const r = createRequire(import.meta.url);
|
|
19
17
|
const serverTTS = r('webtalk/server-tts');
|
|
20
18
|
return serverTTS.ensureInstalled(onProgress);
|
|
21
19
|
}
|
|
22
|
-
|
|
23
20
|
export async function getSpeech() {
|
|
24
21
|
if (!speechModule) speechModule = await import('./speech.js');
|
|
25
22
|
return speechModule;
|
|
26
23
|
}
|
|
27
|
-
|
|
28
24
|
const ttsTextAccumulators = new Map();
|
|
29
25
|
|
|
30
26
|
export const voiceCacheManager = {
|
|
@@ -58,7 +54,6 @@ export const voiceCacheManager = {
|
|
|
58
54
|
}
|
|
59
55
|
}
|
|
60
56
|
};
|
|
61
|
-
|
|
62
57
|
export const modelDownloadState = {
|
|
63
58
|
downloading: false,
|
|
64
59
|
progress: null,
|
|
@@ -68,7 +63,6 @@ export const modelDownloadState = {
|
|
|
68
63
|
downloadMetrics: new Map(),
|
|
69
64
|
waiters: []
|
|
70
65
|
};
|
|
71
|
-
|
|
72
66
|
export function broadcastModelProgress(progress) {
|
|
73
67
|
modelDownloadState.progress = progress;
|
|
74
68
|
const broadcastData = {
|
|
@@ -203,4 +197,4 @@ function pushTTSAudio(cacheKey, wav, conversationId, sessionId, voiceId) {
|
|
|
203
197
|
sessionId,
|
|
204
198
|
timestamp: Date.now()
|
|
205
199
|
});
|
|
206
|
-
}
|
|
200
|
+
}
|
package/lib/ws-handlers-conv.js
CHANGED
|
@@ -14,7 +14,6 @@ function validate(schema, params) {
|
|
|
14
14
|
|
|
15
15
|
const ConvNewSchema = z.object({ agentId: z.string().optional(), title: z.string().optional(), workingDirectory: z.string().optional(), model: z.string().optional(), subAgent: z.string().optional() }).passthrough();
|
|
16
16
|
const ConvUpdSchema = z.object({ id: z.string().min(1, 'id required') }).passthrough();
|
|
17
|
-
const ConvSteerSchema = z.object({ id: z.string().min(1, 'conversation id required'), content: z.union([z.string(), z.record(z.any())]).refine(v => v !== undefined && v !== null && v !== '', { message: 'content required' }) }).passthrough();
|
|
18
17
|
|
|
19
18
|
export function register(router, deps) {
|
|
20
19
|
const { queries, activeExecutions, rateLimitState,
|
|
@@ -72,7 +71,6 @@ export function register(router, deps) {
|
|
|
72
71
|
});
|
|
73
72
|
|
|
74
73
|
router.handle('conv.del.all', (p) => {
|
|
75
|
-
// Clean up all execution machine actors
|
|
76
74
|
const convs = queries.getConversationsList();
|
|
77
75
|
for (const c of convs) { cleanupExecution(c.id); execMachine.remove(c.id); }
|
|
78
76
|
if (!queries.deleteAllConversations()) fail(500, 'Failed to delete all conversations');
|
|
@@ -137,146 +135,4 @@ export function register(router, deps) {
|
|
|
137
135
|
}
|
|
138
136
|
return { ok: true };
|
|
139
137
|
});
|
|
140
|
-
|
|
141
|
-
router.handle('conv.export', (p) => {
|
|
142
|
-
const conv = queries.getConversation(p.id);
|
|
143
|
-
if (!conv) notFound();
|
|
144
|
-
const msgs = queries.getConversationMessages(p.id);
|
|
145
|
-
const format = p.format || 'markdown';
|
|
146
|
-
if (format === 'json') return { conversation: conv, messages: msgs };
|
|
147
|
-
let md = `# ${conv.title || 'Conversation'}\n\n`;
|
|
148
|
-
md += `Agent: ${conv.agentType || 'unknown'} | Created: ${new Date(conv.created_at).toISOString()}\n\n---\n\n`;
|
|
149
|
-
for (const m of msgs) {
|
|
150
|
-
const role = m.role === 'user' ? 'User' : 'Assistant';
|
|
151
|
-
md += `## ${role}\n\n`;
|
|
152
|
-
let content = m.content;
|
|
153
|
-
try { const parsed = JSON.parse(content); if (Array.isArray(parsed)) { content = parsed.map(b => b.text || b.content || JSON.stringify(b)).join('\n'); } } catch {}
|
|
154
|
-
md += content + '\n\n---\n\n';
|
|
155
|
-
}
|
|
156
|
-
return { markdown: md, title: conv.title };
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
router.handle('conv.import', (p) => {
|
|
160
|
-
if (!p.conversation || !p.messages) throw Object.assign(new Error('Missing conversation or messages'), { code: 400 });
|
|
161
|
-
const src = p.conversation;
|
|
162
|
-
const conv = queries.createConversation(
|
|
163
|
-
src.agentId || src.agentType || 'claude-code',
|
|
164
|
-
src.title || 'Imported Conversation',
|
|
165
|
-
src.workingDirectory || null,
|
|
166
|
-
src.model || null
|
|
167
|
-
);
|
|
168
|
-
for (const msg of p.messages) {
|
|
169
|
-
queries.createMessage(conv.id, msg.role || 'user', msg.content || '');
|
|
170
|
-
}
|
|
171
|
-
broadcastSync({ type: 'conversation_created', conversation: queries.getConversation(conv.id) });
|
|
172
|
-
return { conversation: queries.getConversation(conv.id), importedMessages: p.messages.length };
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
router.handle('conv.sync', (p) => {
|
|
176
|
-
const conv = queries.getConversation(p.id);
|
|
177
|
-
if (!conv) notFound();
|
|
178
|
-
const machineSnap = execMachine.snapshot(p.id);
|
|
179
|
-
const executionState = machineSnap?.value || 'idle';
|
|
180
|
-
const latestSession = queries.getLatestSession(p.id);
|
|
181
|
-
const sinceSeq = parseInt(p.sinceSeq || '-1');
|
|
182
|
-
const since = parseInt(p.since || '0');
|
|
183
|
-
let missedChunks = [];
|
|
184
|
-
if (latestSession && sinceSeq >= 0) {
|
|
185
|
-
missedChunks = queries.getChunksSinceSeq(latestSession.id, sinceSeq);
|
|
186
|
-
} else if (latestSession && since > 0) {
|
|
187
|
-
missedChunks = queries.getConversationChunksSince(p.id, since);
|
|
188
|
-
}
|
|
189
|
-
return {
|
|
190
|
-
conversation: conv,
|
|
191
|
-
executionState,
|
|
192
|
-
isActivelyStreaming: execMachine.isActive(p.id),
|
|
193
|
-
latestSession,
|
|
194
|
-
missedChunks,
|
|
195
|
-
missedCount: missedChunks.length,
|
|
196
|
-
queueLength: machineSnap?.context?.queue?.length || 0
|
|
197
|
-
};
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
router.handle('conv.search', (p) => {
|
|
201
|
-
if (!p.query || typeof p.query !== 'string' || p.query.trim().length < 2) return { results: [] };
|
|
202
|
-
const limit = Math.min(parseInt(p.limit || '50'), 200);
|
|
203
|
-
return { results: queries.searchMessages(p.query.trim(), limit) };
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
router.handle('conv.prune', (p) => {
|
|
207
|
-
const conv = queries.getConversation(p.id);
|
|
208
|
-
if (!conv) notFound();
|
|
209
|
-
const keep = Math.max(parseInt(p.keep || '500'), 100);
|
|
210
|
-
const sessions = queries.getConversationSessions(p.id);
|
|
211
|
-
if (!sessions || sessions.length === 0) return { pruned: 0 };
|
|
212
|
-
const latestSessionId = sessions[0]?.id;
|
|
213
|
-
let pruned = 0;
|
|
214
|
-
for (const sess of sessions) {
|
|
215
|
-
if (sess.id === latestSessionId) continue;
|
|
216
|
-
const count = queries.getSessionChunks(sess.id)?.length || 0;
|
|
217
|
-
if (count > 0) { queries.deleteSessionChunks(sess.id); pruned += count; }
|
|
218
|
-
}
|
|
219
|
-
return { pruned, kept: latestSessionId, sessionsProcessed: sessions.length };
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
router.handle('conv.cancel', (p) => {
|
|
223
|
-
if (!execMachine.isActive(p.id)) notFound('No active execution to cancel');
|
|
224
|
-
const ctx = execMachine.getContext(p.id);
|
|
225
|
-
const pid = ctx?.pid || activeExecutions.get(p.id)?.pid;
|
|
226
|
-
const sessionId = ctx?.sessionId || activeExecutions.get(p.id)?.sessionId;
|
|
227
|
-
if (pid) { try { process.kill(-pid, 'SIGKILL'); } catch { try { process.kill(pid, 'SIGKILL'); } catch {} } }
|
|
228
|
-
if (sessionId) queries.updateSession(sessionId, { status: 'interrupted', completed_at: Date.now() });
|
|
229
|
-
cleanupExecution(p.id, false);
|
|
230
|
-
broadcastSync({ type: 'streaming_complete', sessionId, conversationId: p.id, interrupted: true, timestamp: Date.now() });
|
|
231
|
-
return { ok: true, cancelled: true, conversationId: p.id, sessionId };
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
router.handle('conv.inject', (p) => {
|
|
235
|
-
const conv = queries.getConversation(p.id);
|
|
236
|
-
if (!conv) notFound('Conversation not found');
|
|
237
|
-
if (!p.content) fail(400, 'Missing content');
|
|
238
|
-
const message = queries.createMessage(p.id, 'user', '[INJECTED] ' + p.content);
|
|
239
|
-
if (!execMachine.isActive(p.id)) {
|
|
240
|
-
const agentId = conv.agentId || 'claude-code';
|
|
241
|
-
const session = queries.createSession(p.id, agentId, 'pending');
|
|
242
|
-
processMessageWithStreaming(p.id, message.id, session.id, message.content, agentId, conv.model || null, conv.subAgent || null);
|
|
243
|
-
}
|
|
244
|
-
return { ok: true, injected: true, conversationId: p.id, messageId: message.id };
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
router.handle('conv.steer', (p) => {
|
|
248
|
-
p = validate(ConvSteerSchema, p);
|
|
249
|
-
const conv = queries.getConversation(p.id);
|
|
250
|
-
if (!conv) notFound('Conversation not found');
|
|
251
|
-
|
|
252
|
-
if (!execMachine.isActive(p.id)) fail(409, 'No active execution to steer');
|
|
253
|
-
|
|
254
|
-
const ctx = execMachine.getContext(p.id);
|
|
255
|
-
const pid = ctx?.pid || activeExecutions.get(p.id)?.pid;
|
|
256
|
-
const sessionId = ctx?.sessionId || activeExecutions.get(p.id)?.sessionId;
|
|
257
|
-
|
|
258
|
-
if (pid) {
|
|
259
|
-
try { process.kill(-pid, 'SIGKILL'); } catch { try { process.kill(pid, 'SIGKILL'); } catch (e) {} }
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (sessionId) queries.updateSession(sessionId, { status: 'interrupted', completed_at: Date.now() });
|
|
263
|
-
cleanupExecution(p.id);
|
|
264
|
-
|
|
265
|
-
// Clear claudeSessionId so new execution starts fresh without --resume on a killed session
|
|
266
|
-
queries.setClaudeSessionId(p.id, null);
|
|
267
|
-
|
|
268
|
-
broadcastSync({ type: 'streaming_complete', sessionId, conversationId: p.id, interrupted: true, timestamp: Date.now() });
|
|
269
|
-
|
|
270
|
-
const agentId = conv.agentType || conv.agentId || 'claude-code';
|
|
271
|
-
const model = conv.model || null;
|
|
272
|
-
const subAgent = conv.subAgent || null;
|
|
273
|
-
const steerContent = typeof p.content === 'string' ? p.content : (p.content ? JSON.stringify(p.content) : '');
|
|
274
|
-
const message = queries.createMessage(p.id, 'user', steerContent);
|
|
275
|
-
queries.createEvent('message.created', { role: 'user', messageId: message.id }, p.id);
|
|
276
|
-
broadcastSync({ type: 'message_created', conversationId: p.id, message, timestamp: Date.now() });
|
|
277
|
-
startExecution(p.id, message, agentId, model, steerContent, subAgent);
|
|
278
|
-
|
|
279
|
-
return { ok: true, steered: true, conversationId: p.id, messageId: message.id };
|
|
280
|
-
});
|
|
281
|
-
|
|
282
138
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as execMachine from './execution-machine.js';
|
|
3
|
+
|
|
4
|
+
function fail(code, message) { const e = new Error(message); e.code = code; throw e; }
|
|
5
|
+
function notFound(msg = 'Not found') { fail(404, msg); }
|
|
6
|
+
function validate(schema, params) {
|
|
7
|
+
const result = schema.safeParse(params);
|
|
8
|
+
if (!result.success) fail(400, result.error.issues.map(i => i.message).join('; '));
|
|
9
|
+
return result.data;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ConvSteerSchema = z.object({ id: z.string().min(1, 'conversation id required'), content: z.union([z.string(), z.record(z.any())]).refine(v => v !== undefined && v !== null && v !== '', { message: 'content required' }) }).passthrough();
|
|
13
|
+
|
|
14
|
+
export function register(router, deps) {
|
|
15
|
+
const { queries, activeExecutions, rateLimitState,
|
|
16
|
+
broadcastSync, processMessageWithStreaming, cleanupExecution,
|
|
17
|
+
getJsonlWatcher = () => null, logError = () => {} } = deps;
|
|
18
|
+
|
|
19
|
+
function startExecution(convId, message, agentId, model, content, subAgent) {
|
|
20
|
+
const session = queries.createSession(convId);
|
|
21
|
+
queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, convId, session.id);
|
|
22
|
+
execMachine.send(convId, { type: 'START', sessionId: session.id });
|
|
23
|
+
activeExecutions.set(convId, { pid: null, startTime: Date.now(), sessionId: session.id, lastActivity: Date.now() });
|
|
24
|
+
queries.setIsStreaming(convId, true);
|
|
25
|
+
broadcastSync({ type: 'streaming_start', sessionId: session.id, conversationId: convId, messageId: message.id, agentId, timestamp: Date.now() });
|
|
26
|
+
processMessageWithStreaming(convId, message.id, session.id, content, agentId, model, subAgent).catch(e => logError('startExecution', e, { convId }));
|
|
27
|
+
return session;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
router.handle('conv.export', (p) => {
|
|
31
|
+
const conv = queries.getConversation(p.id);
|
|
32
|
+
if (!conv) notFound();
|
|
33
|
+
const msgs = queries.getConversationMessages(p.id);
|
|
34
|
+
const format = p.format || 'markdown';
|
|
35
|
+
if (format === 'json') return { conversation: conv, messages: msgs };
|
|
36
|
+
let md = `# ${conv.title || 'Conversation'}\n\n`;
|
|
37
|
+
md += `Agent: ${conv.agentType || 'unknown'} | Created: ${new Date(conv.created_at).toISOString()}\n\n---\n\n`;
|
|
38
|
+
for (const m of msgs) {
|
|
39
|
+
const role = m.role === 'user' ? 'User' : 'Assistant';
|
|
40
|
+
md += `## ${role}\n\n`;
|
|
41
|
+
let content = m.content;
|
|
42
|
+
try { const parsed = JSON.parse(content); if (Array.isArray(parsed)) { content = parsed.map(b => b.text || b.content || JSON.stringify(b)).join('\n'); } } catch {}
|
|
43
|
+
md += content + '\n\n---\n\n';
|
|
44
|
+
}
|
|
45
|
+
return { markdown: md, title: conv.title };
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
router.handle('conv.import', (p) => {
|
|
49
|
+
if (!p.conversation || !p.messages) throw Object.assign(new Error('Missing conversation or messages'), { code: 400 });
|
|
50
|
+
const src = p.conversation;
|
|
51
|
+
const conv = queries.createConversation(
|
|
52
|
+
src.agentId || src.agentType || 'claude-code',
|
|
53
|
+
src.title || 'Imported Conversation',
|
|
54
|
+
src.workingDirectory || null,
|
|
55
|
+
src.model || null
|
|
56
|
+
);
|
|
57
|
+
for (const msg of p.messages) {
|
|
58
|
+
queries.createMessage(conv.id, msg.role || 'user', msg.content || '');
|
|
59
|
+
}
|
|
60
|
+
broadcastSync({ type: 'conversation_created', conversation: queries.getConversation(conv.id) });
|
|
61
|
+
return { conversation: queries.getConversation(conv.id), importedMessages: p.messages.length };
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
router.handle('conv.sync', (p) => {
|
|
65
|
+
const conv = queries.getConversation(p.id);
|
|
66
|
+
if (!conv) notFound();
|
|
67
|
+
const machineSnap = execMachine.snapshot(p.id);
|
|
68
|
+
const executionState = machineSnap?.value || 'idle';
|
|
69
|
+
const latestSession = queries.getLatestSession(p.id);
|
|
70
|
+
const sinceSeq = parseInt(p.sinceSeq || '-1');
|
|
71
|
+
const since = parseInt(p.since || '0');
|
|
72
|
+
let missedChunks = [];
|
|
73
|
+
if (latestSession && sinceSeq >= 0) {
|
|
74
|
+
missedChunks = queries.getChunksSinceSeq(latestSession.id, sinceSeq);
|
|
75
|
+
} else if (latestSession && since > 0) {
|
|
76
|
+
missedChunks = queries.getConversationChunksSince(p.id, since);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
conversation: conv,
|
|
80
|
+
executionState,
|
|
81
|
+
isActivelyStreaming: execMachine.isActive(p.id),
|
|
82
|
+
latestSession,
|
|
83
|
+
missedChunks,
|
|
84
|
+
missedCount: missedChunks.length,
|
|
85
|
+
queueLength: machineSnap?.context?.queue?.length || 0
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
router.handle('conv.search', (p) => {
|
|
90
|
+
if (!p.query || typeof p.query !== 'string' || p.query.trim().length < 2) return { results: [] };
|
|
91
|
+
const limit = Math.min(parseInt(p.limit || '50'), 200);
|
|
92
|
+
return { results: queries.searchMessages(p.query.trim(), limit) };
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
router.handle('conv.prune', (p) => {
|
|
96
|
+
const conv = queries.getConversation(p.id);
|
|
97
|
+
if (!conv) notFound();
|
|
98
|
+
const keep = Math.max(parseInt(p.keep || '500'), 100);
|
|
99
|
+
const sessions = queries.getConversationSessions(p.id);
|
|
100
|
+
if (!sessions || sessions.length === 0) return { pruned: 0 };
|
|
101
|
+
const latestSessionId = sessions[0]?.id;
|
|
102
|
+
let pruned = 0;
|
|
103
|
+
for (const sess of sessions) {
|
|
104
|
+
if (sess.id === latestSessionId) continue;
|
|
105
|
+
const count = queries.getSessionChunks(sess.id)?.length || 0;
|
|
106
|
+
if (count > 0) { queries.deleteSessionChunks(sess.id); pruned += count; }
|
|
107
|
+
}
|
|
108
|
+
return { pruned, kept: latestSessionId, sessionsProcessed: sessions.length };
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
router.handle('conv.cancel', (p) => {
|
|
112
|
+
if (!execMachine.isActive(p.id)) notFound('No active execution to cancel');
|
|
113
|
+
const ctx = execMachine.getContext(p.id);
|
|
114
|
+
const pid = ctx?.pid || activeExecutions.get(p.id)?.pid;
|
|
115
|
+
const sessionId = ctx?.sessionId || activeExecutions.get(p.id)?.sessionId;
|
|
116
|
+
if (pid) { try { process.kill(-pid, 'SIGKILL'); } catch { try { process.kill(pid, 'SIGKILL'); } catch {} } }
|
|
117
|
+
if (sessionId) queries.updateSession(sessionId, { status: 'interrupted', completed_at: Date.now() });
|
|
118
|
+
cleanupExecution(p.id, false);
|
|
119
|
+
broadcastSync({ type: 'streaming_complete', sessionId, conversationId: p.id, interrupted: true, timestamp: Date.now() });
|
|
120
|
+
return { ok: true, cancelled: true, conversationId: p.id, sessionId };
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
router.handle('conv.inject', (p) => {
|
|
124
|
+
const conv = queries.getConversation(p.id);
|
|
125
|
+
if (!conv) notFound('Conversation not found');
|
|
126
|
+
if (!p.content) fail(400, 'Missing content');
|
|
127
|
+
const message = queries.createMessage(p.id, 'user', '[INJECTED] ' + p.content);
|
|
128
|
+
if (!execMachine.isActive(p.id)) {
|
|
129
|
+
const agentId = conv.agentId || 'claude-code';
|
|
130
|
+
const session = queries.createSession(p.id, agentId, 'pending');
|
|
131
|
+
processMessageWithStreaming(p.id, message.id, session.id, message.content, agentId, conv.model || null, conv.subAgent || null);
|
|
132
|
+
}
|
|
133
|
+
return { ok: true, injected: true, conversationId: p.id, messageId: message.id };
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
router.handle('conv.steer', (p) => {
|
|
137
|
+
p = validate(ConvSteerSchema, p);
|
|
138
|
+
const conv = queries.getConversation(p.id);
|
|
139
|
+
if (!conv) notFound('Conversation not found');
|
|
140
|
+
|
|
141
|
+
if (!execMachine.isActive(p.id)) fail(409, 'No active execution to steer');
|
|
142
|
+
|
|
143
|
+
const ctx = execMachine.getContext(p.id);
|
|
144
|
+
const pid = ctx?.pid || activeExecutions.get(p.id)?.pid;
|
|
145
|
+
const sessionId = ctx?.sessionId || activeExecutions.get(p.id)?.sessionId;
|
|
146
|
+
|
|
147
|
+
if (pid) {
|
|
148
|
+
try { process.kill(-pid, 'SIGKILL'); } catch { try { process.kill(pid, 'SIGKILL'); } catch (e) {} }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (sessionId) queries.updateSession(sessionId, { status: 'interrupted', completed_at: Date.now() });
|
|
152
|
+
cleanupExecution(p.id);
|
|
153
|
+
|
|
154
|
+
queries.setClaudeSessionId(p.id, null);
|
|
155
|
+
|
|
156
|
+
broadcastSync({ type: 'streaming_complete', sessionId, conversationId: p.id, interrupted: true, timestamp: Date.now() });
|
|
157
|
+
|
|
158
|
+
const agentId = conv.agentType || conv.agentId || 'claude-code';
|
|
159
|
+
const model = conv.model || null;
|
|
160
|
+
const subAgent = conv.subAgent || null;
|
|
161
|
+
const steerContent = typeof p.content === 'string' ? p.content : (p.content ? JSON.stringify(p.content) : '');
|
|
162
|
+
const message = queries.createMessage(p.id, 'user', steerContent);
|
|
163
|
+
queries.createEvent('message.created', { role: 'user', messageId: message.id }, p.id);
|
|
164
|
+
broadcastSync({ type: 'message_created', conversationId: p.id, message, timestamp: Date.now() });
|
|
165
|
+
startExecution(p.id, message, agentId, model, steerContent, subAgent);
|
|
166
|
+
|
|
167
|
+
return { ok: true, steered: true, conversationId: p.id, messageId: message.id };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -1,79 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { execSync, spawn } from 'child_process';
|
|
5
|
-
import { ensureRunning, touch, queryModels } from './acp-sdk-manager.js';
|
|
6
|
-
|
|
7
|
-
function spawnScript(cmd, args, convId, scriptName, agentId, deps) {
|
|
8
|
-
const { activeScripts, broadcastSync, modelCache } = deps;
|
|
9
|
-
if (activeScripts.has(convId)) throw { code: 409, message: 'Process already running' };
|
|
10
|
-
const child = spawn(cmd, args, {
|
|
11
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
12
|
-
env: { ...process.env, FORCE_COLOR: '1' },
|
|
13
|
-
shell: os.platform() === 'win32'
|
|
14
|
-
});
|
|
15
|
-
activeScripts.set(convId, { process: child, script: scriptName, startTime: Date.now() });
|
|
16
|
-
broadcastSync({ type: 'script_started', conversationId: convId, script: scriptName, agentId, timestamp: Date.now() });
|
|
17
|
-
const relay = (stream) => (chunk) => {
|
|
18
|
-
broadcastSync({ type: 'script_output', conversationId: convId, data: chunk.toString(), stream, timestamp: Date.now() });
|
|
19
|
-
};
|
|
20
|
-
child.stdout.on('data', relay('stdout'));
|
|
21
|
-
child.stderr.on('data', relay('stderr'));
|
|
22
|
-
child.on('error', (err) => {
|
|
23
|
-
activeScripts.delete(convId);
|
|
24
|
-
broadcastSync({ type: 'script_stopped', conversationId: convId, code: 1, error: err.message, timestamp: Date.now() });
|
|
25
|
-
});
|
|
26
|
-
child.on('close', (code) => {
|
|
27
|
-
activeScripts.delete(convId);
|
|
28
|
-
if (modelCache && agentId) modelCache.delete(agentId);
|
|
29
|
-
broadcastSync({ type: 'script_stopped', conversationId: convId, code: code || 0, timestamp: Date.now() });
|
|
30
|
-
});
|
|
31
|
-
return child.pid;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function readJson(filePath) {
|
|
35
|
-
try { return fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, 'utf-8')) : null; } catch { return null; }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function checkAgentAuth(agent) {
|
|
39
|
-
const s = { id: agent.id, name: agent.name, authenticated: false, detail: '' };
|
|
40
|
-
try {
|
|
41
|
-
if (agent.id === 'claude-code') {
|
|
42
|
-
const creds = readJson(path.join(os.homedir(), '.claude', '.credentials.json'));
|
|
43
|
-
if (creds?.claudeAiOauth?.expiresAt > Date.now()) {
|
|
44
|
-
s.authenticated = true;
|
|
45
|
-
s.detail = creds.claudeAiOauth.subscriptionType || 'authenticated';
|
|
46
|
-
} else { s.detail = creds ? 'expired' : 'no credentials'; }
|
|
47
|
-
} else if (agent.id === 'gemini') {
|
|
48
|
-
const oauth = readJson(path.join(os.homedir(), '.gemini', 'oauth_creds.json'));
|
|
49
|
-
const accts = readJson(path.join(os.homedir(), '.gemini', 'google_accounts.json'));
|
|
50
|
-
const hasOAuth = !!(oauth?.refresh_token || oauth?.access_token);
|
|
51
|
-
if (accts?.active) { s.authenticated = true; s.detail = accts.active; }
|
|
52
|
-
else if (hasOAuth) { s.authenticated = true; s.detail = 'oauth'; }
|
|
53
|
-
else { s.detail = accts ? 'logged out' : 'no credentials'; }
|
|
54
|
-
} else if (agent.id === 'opencode') {
|
|
55
|
-
const out = execSync('opencode auth list 2>&1', { encoding: 'utf-8', timeout: 5000 });
|
|
56
|
-
const m = out.match(/(\d+)\s+credentials?/);
|
|
57
|
-
if (m && parseInt(m[1], 10) > 0) { s.authenticated = true; s.detail = m[1] + ' credential(s)'; }
|
|
58
|
-
else { s.detail = 'no credentials'; }
|
|
59
|
-
} else { s.detail = 'unknown'; }
|
|
60
|
-
} catch { s.detail = 'check failed'; }
|
|
61
|
-
return s;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function acpFetch(port, pth, body = null) {
|
|
65
|
-
const opts = { headers: { 'Content-Type': 'application/json' }, signal: AbortSignal.timeout(3000) };
|
|
66
|
-
if (body !== null) { opts.method = 'POST'; opts.body = JSON.stringify(body); }
|
|
67
|
-
const res = await fetch(`http://localhost:${port}${pth}`, opts);
|
|
68
|
-
if (!res.ok) return null;
|
|
69
|
-
return res.json();
|
|
70
|
-
}
|
|
71
|
-
|
|
1
|
+
import { queryModels } from './acp-sdk-manager.js';
|
|
72
2
|
|
|
73
3
|
export function register(router, deps) {
|
|
74
|
-
const { db, discoveredAgents, modelCache,
|
|
75
|
-
getAgentDescriptor, activeScripts, broadcastSync, startGeminiOAuth,
|
|
76
|
-
geminiOAuthState } = deps;
|
|
4
|
+
const { db, discoveredAgents, modelCache, getAgentDescriptor } = deps;
|
|
77
5
|
console.log('[ws-handlers-session] register() called with discoveredAgents.length:', discoveredAgents.length);
|
|
78
6
|
|
|
79
7
|
router.handle('sess.get', (p) => {
|
|
@@ -129,8 +57,6 @@ export function register(router, deps) {
|
|
|
129
57
|
}))
|
|
130
58
|
};
|
|
131
59
|
});
|
|
132
|
-
// Note: agent.subagents is handled by ws-handlers-util.js which is registered after this file
|
|
133
|
-
// Keeping this note to avoid duplicate handler registration
|
|
134
60
|
|
|
135
61
|
router.handle('agent.get', (p) => {
|
|
136
62
|
const a = discoveredAgents.find(x => x.id === p.id);
|
|
@@ -165,44 +91,4 @@ export function register(router, deps) {
|
|
|
165
91
|
});
|
|
166
92
|
|
|
167
93
|
router.handle('agent.search', (p) => db.searchAgents(discoveredAgents, p.query || p));
|
|
168
|
-
|
|
169
|
-
router.handle('agent.auth', async (p) => {
|
|
170
|
-
const agentId = p.id;
|
|
171
|
-
if (!discoveredAgents.find(a => a.id === agentId)) throw { code: 404, message: 'Agent not found' };
|
|
172
|
-
if (agentId === 'gemini') {
|
|
173
|
-
const result = await startGeminiOAuth();
|
|
174
|
-
const cid = '__agent_auth__';
|
|
175
|
-
broadcastSync({ type: 'script_started', conversationId: cid, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
|
|
176
|
-
broadcastSync({ type: 'script_output', conversationId: cid, data: `\x1b[36mOpening Google OAuth...\x1b[0m\r\n\r\nVisit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
177
|
-
const pollId = setInterval(() => {
|
|
178
|
-
const st = geminiOAuthState();
|
|
179
|
-
if (st.status === 'success') {
|
|
180
|
-
clearInterval(pollId);
|
|
181
|
-
broadcastSync({ type: 'script_output', conversationId: cid, data: `\r\n\x1b[32mAuth OK${st.email ? ' (' + st.email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
182
|
-
broadcastSync({ type: 'script_stopped', conversationId: cid, code: 0, timestamp: Date.now() });
|
|
183
|
-
} else if (st.status === 'error') {
|
|
184
|
-
clearInterval(pollId);
|
|
185
|
-
broadcastSync({ type: 'script_output', conversationId: cid, data: `\r\n\x1b[31mAuth failed: ${st.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
|
|
186
|
-
broadcastSync({ type: 'script_stopped', conversationId: cid, code: 1, error: st.error, timestamp: Date.now() });
|
|
187
|
-
}
|
|
188
|
-
}, 1000);
|
|
189
|
-
setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
|
|
190
|
-
return { ok: true, agentId, authUrl: result.authUrl, mode: result.mode };
|
|
191
|
-
}
|
|
192
|
-
const cmds = { 'claude-code': { cmd: 'claude', args: ['setup-token'] }, 'opencode': { cmd: 'opencode', args: ['auth', 'login'] } };
|
|
193
|
-
const c = cmds[agentId];
|
|
194
|
-
if (!c) throw { code: 400, message: 'No auth command for this agent' };
|
|
195
|
-
const pid = spawnScript(c.cmd, c.args, '__agent_auth__', 'auth-' + agentId, agentId, { activeScripts, broadcastSync });
|
|
196
|
-
return { ok: true, agentId, pid };
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
router.handle('agent.authstat', () => ({ agents: discoveredAgents.map(checkAgentAuth) }));
|
|
200
|
-
|
|
201
|
-
router.handle('agent.update', (p) => {
|
|
202
|
-
const cmds = { 'claude-code': { cmd: 'claude', args: ['update', '--yes'] } };
|
|
203
|
-
const c = cmds[p.id];
|
|
204
|
-
if (!c) throw { code: 400, message: 'No update command for this agent' };
|
|
205
|
-
const pid = spawnScript(c.cmd, c.args, '__agent_update__', 'update-' + p.id, p.id, { activeScripts, broadcastSync, modelCache });
|
|
206
|
-
return { ok: true, agentId: p.id, pid };
|
|
207
|
-
});
|
|
208
|
-
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync, spawn } from 'child_process';
|
|
5
|
+
|
|
6
|
+
function spawnScript(cmd, args, convId, scriptName, agentId, deps) {
|
|
7
|
+
const { activeScripts, broadcastSync, modelCache } = deps;
|
|
8
|
+
if (activeScripts.has(convId)) throw { code: 409, message: 'Process already running' };
|
|
9
|
+
const child = spawn(cmd, args, {
|
|
10
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
11
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
12
|
+
shell: os.platform() === 'win32'
|
|
13
|
+
});
|
|
14
|
+
activeScripts.set(convId, { process: child, script: scriptName, startTime: Date.now() });
|
|
15
|
+
broadcastSync({ type: 'script_started', conversationId: convId, script: scriptName, agentId, timestamp: Date.now() });
|
|
16
|
+
const relay = (stream) => (chunk) => {
|
|
17
|
+
broadcastSync({ type: 'script_output', conversationId: convId, data: chunk.toString(), stream, timestamp: Date.now() });
|
|
18
|
+
};
|
|
19
|
+
child.stdout.on('data', relay('stdout'));
|
|
20
|
+
child.stderr.on('data', relay('stderr'));
|
|
21
|
+
child.on('error', (err) => {
|
|
22
|
+
activeScripts.delete(convId);
|
|
23
|
+
broadcastSync({ type: 'script_stopped', conversationId: convId, code: 1, error: err.message, timestamp: Date.now() });
|
|
24
|
+
});
|
|
25
|
+
child.on('close', (code) => {
|
|
26
|
+
activeScripts.delete(convId);
|
|
27
|
+
if (modelCache && agentId) modelCache.delete(agentId);
|
|
28
|
+
broadcastSync({ type: 'script_stopped', conversationId: convId, code: code || 0, timestamp: Date.now() });
|
|
29
|
+
});
|
|
30
|
+
return child.pid;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJson(filePath) {
|
|
34
|
+
try { return fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, 'utf-8')) : null; } catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function checkAgentAuth(agent) {
|
|
38
|
+
const s = { id: agent.id, name: agent.name, authenticated: false, detail: '' };
|
|
39
|
+
try {
|
|
40
|
+
if (agent.id === 'claude-code') {
|
|
41
|
+
const creds = readJson(path.join(os.homedir(), '.claude', '.credentials.json'));
|
|
42
|
+
if (creds?.claudeAiOauth?.expiresAt > Date.now()) {
|
|
43
|
+
s.authenticated = true;
|
|
44
|
+
s.detail = creds.claudeAiOauth.subscriptionType || 'authenticated';
|
|
45
|
+
} else { s.detail = creds ? 'expired' : 'no credentials'; }
|
|
46
|
+
} else if (agent.id === 'gemini') {
|
|
47
|
+
const oauth = readJson(path.join(os.homedir(), '.gemini', 'oauth_creds.json'));
|
|
48
|
+
const accts = readJson(path.join(os.homedir(), '.gemini', 'google_accounts.json'));
|
|
49
|
+
const hasOAuth = !!(oauth?.refresh_token || oauth?.access_token);
|
|
50
|
+
if (accts?.active) { s.authenticated = true; s.detail = accts.active; }
|
|
51
|
+
else if (hasOAuth) { s.authenticated = true; s.detail = 'oauth'; }
|
|
52
|
+
else { s.detail = accts ? 'logged out' : 'no credentials'; }
|
|
53
|
+
} else if (agent.id === 'opencode') {
|
|
54
|
+
const out = execSync('opencode auth list 2>&1', { encoding: 'utf-8', timeout: 5000 });
|
|
55
|
+
const m = out.match(/(\d+)\s+credentials?/);
|
|
56
|
+
if (m && parseInt(m[1], 10) > 0) { s.authenticated = true; s.detail = m[1] + ' credential(s)'; }
|
|
57
|
+
else { s.detail = 'no credentials'; }
|
|
58
|
+
} else { s.detail = 'unknown'; }
|
|
59
|
+
} catch { s.detail = 'check failed'; }
|
|
60
|
+
return s;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function register(router, deps) {
|
|
64
|
+
const { discoveredAgents, modelCache, activeScripts, broadcastSync,
|
|
65
|
+
startGeminiOAuth, geminiOAuthState } = deps;
|
|
66
|
+
|
|
67
|
+
router.handle('agent.auth', async (p) => {
|
|
68
|
+
const agentId = p.id;
|
|
69
|
+
if (!discoveredAgents.find(a => a.id === agentId)) throw { code: 404, message: 'Agent not found' };
|
|
70
|
+
if (agentId === 'gemini') {
|
|
71
|
+
const result = await startGeminiOAuth();
|
|
72
|
+
const cid = '__agent_auth__';
|
|
73
|
+
broadcastSync({ type: 'script_started', conversationId: cid, script: 'auth-gemini', agentId: 'gemini', timestamp: Date.now() });
|
|
74
|
+
broadcastSync({ type: 'script_output', conversationId: cid, data: `\x1b[36mOpening Google OAuth...\x1b[0m\r\n\r\nVisit:\r\n${result.authUrl}\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
75
|
+
const pollId = setInterval(() => {
|
|
76
|
+
const st = geminiOAuthState();
|
|
77
|
+
if (st.status === 'success') {
|
|
78
|
+
clearInterval(pollId);
|
|
79
|
+
broadcastSync({ type: 'script_output', conversationId: cid, data: `\r\n\x1b[32mAuth OK${st.email ? ' (' + st.email + ')' : ''}\x1b[0m\r\n`, stream: 'stdout', timestamp: Date.now() });
|
|
80
|
+
broadcastSync({ type: 'script_stopped', conversationId: cid, code: 0, timestamp: Date.now() });
|
|
81
|
+
} else if (st.status === 'error') {
|
|
82
|
+
clearInterval(pollId);
|
|
83
|
+
broadcastSync({ type: 'script_output', conversationId: cid, data: `\r\n\x1b[31mAuth failed: ${st.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
|
|
84
|
+
broadcastSync({ type: 'script_stopped', conversationId: cid, code: 1, error: st.error, timestamp: Date.now() });
|
|
85
|
+
}
|
|
86
|
+
}, 1000);
|
|
87
|
+
setTimeout(() => clearInterval(pollId), 5 * 60 * 1000);
|
|
88
|
+
return { ok: true, agentId, authUrl: result.authUrl, mode: result.mode };
|
|
89
|
+
}
|
|
90
|
+
const cmds = { 'claude-code': { cmd: 'claude', args: ['setup-token'] }, 'opencode': { cmd: 'opencode', args: ['auth', 'login'] } };
|
|
91
|
+
const c = cmds[agentId];
|
|
92
|
+
if (!c) throw { code: 400, message: 'No auth command for this agent' };
|
|
93
|
+
const pid = spawnScript(c.cmd, c.args, '__agent_auth__', 'auth-' + agentId, agentId, { activeScripts, broadcastSync });
|
|
94
|
+
return { ok: true, agentId, pid };
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
router.handle('agent.authstat', () => ({ agents: discoveredAgents.map(checkAgentAuth) }));
|
|
98
|
+
|
|
99
|
+
router.handle('agent.update', (p) => {
|
|
100
|
+
const cmds = { 'claude-code': { cmd: 'claude', args: ['update', '--yes'] } };
|
|
101
|
+
const c = cmds[p.id];
|
|
102
|
+
if (!c) throw { code: 400, message: 'No update command for this agent' };
|
|
103
|
+
const pid = spawnScript(c.cmd, c.args, '__agent_update__', 'update-' + p.id, p.id, { activeScripts, broadcastSync, modelCache });
|
|
104
|
+
return { ok: true, agentId: p.id, pid };
|
|
105
|
+
});
|
|
106
|
+
}
|