@cmdctrl/aider 0.1.0
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/dist/adapter/agentapi.d.ts +100 -0
- package/dist/adapter/agentapi.d.ts.map +1 -0
- package/dist/adapter/agentapi.js +578 -0
- package/dist/adapter/agentapi.js.map +1 -0
- package/dist/client/messages.d.ts +89 -0
- package/dist/client/messages.d.ts.map +1 -0
- package/dist/client/messages.js +6 -0
- package/dist/client/messages.js.map +1 -0
- package/dist/client/websocket.d.ts +66 -0
- package/dist/client/websocket.d.ts.map +1 -0
- package/dist/client/websocket.js +276 -0
- package/dist/client/websocket.js.map +1 -0
- package/dist/commands/register.d.ts +10 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +175 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/start.d.ts +9 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +54 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +37 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +5 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +59 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/config/config.d.ts +60 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +176 -0
- package/dist/config/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
- package/src/adapter/agentapi.ts +656 -0
- package/src/client/messages.ts +125 -0
- package/src/client/websocket.ts +317 -0
- package/src/commands/register.ts +201 -0
- package/src/commands/start.ts +70 -0
- package/src/commands/status.ts +45 -0
- package/src/commands/stop.ts +58 -0
- package/src/config/config.ts +146 -0
- package/src/index.ts +39 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import * as pty from 'node-pty';
|
|
2
|
+
import { IPty } from 'node-pty';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import * as crypto from 'crypto';
|
|
7
|
+
import { EventSource, ErrorEvent } from 'eventsource';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
10
|
+
const AGENTAPI_PORT = 3284;
|
|
11
|
+
const AGENTAPI_STARTUP_TIMEOUT = 60000; // 60 seconds to wait for agentapi to start (Aider needs time for repo map)
|
|
12
|
+
|
|
13
|
+
// Find agentapi binary
|
|
14
|
+
function findAgentApi(): string {
|
|
15
|
+
if (process.env.AGENTAPI_PATH) {
|
|
16
|
+
return process.env.AGENTAPI_PATH;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const home = os.homedir();
|
|
20
|
+
const commonPaths = [
|
|
21
|
+
path.join(home, '.local', 'bin', 'agentapi'),
|
|
22
|
+
'/usr/local/bin/agentapi',
|
|
23
|
+
'/opt/homebrew/bin/agentapi',
|
|
24
|
+
'agentapi' // Fall back to PATH
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (const p of commonPaths) {
|
|
28
|
+
if (p === 'agentapi') return p;
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(p)) {
|
|
31
|
+
return p;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return 'agentapi';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Find aider binary
|
|
42
|
+
function findAider(): string {
|
|
43
|
+
if (process.env.AIDER_PATH) {
|
|
44
|
+
return process.env.AIDER_PATH;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const home = os.homedir();
|
|
48
|
+
const commonPaths = [
|
|
49
|
+
path.join(home, '.local', 'bin', 'aider'),
|
|
50
|
+
'/usr/local/bin/aider',
|
|
51
|
+
'/opt/homebrew/bin/aider',
|
|
52
|
+
'aider' // Fall back to PATH
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const p of commonPaths) {
|
|
56
|
+
if (p === 'aider') return p;
|
|
57
|
+
try {
|
|
58
|
+
if (fs.existsSync(p)) {
|
|
59
|
+
return p;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return 'aider';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const AGENTAPI_PATH = findAgentApi();
|
|
70
|
+
const AIDER_PATH = findAider();
|
|
71
|
+
|
|
72
|
+
console.log(`[AiderAdapter] Using agentapi path: ${AGENTAPI_PATH}`);
|
|
73
|
+
console.log(`[AiderAdapter] Using aider path: ${AIDER_PATH}`);
|
|
74
|
+
|
|
75
|
+
interface RunningTask {
|
|
76
|
+
taskId: string;
|
|
77
|
+
sessionId: string; // For aider, we generate a session ID
|
|
78
|
+
context: string;
|
|
79
|
+
agentApiProcess: IPty | null;
|
|
80
|
+
eventSource: EventSource | null;
|
|
81
|
+
timeoutHandle: NodeJS.Timeout | null;
|
|
82
|
+
port: number;
|
|
83
|
+
lastStatus: 'stable' | 'running';
|
|
84
|
+
firstSpinnerEmitted: boolean; // Track if we've shown the first spinner message
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface QuestionOption {
|
|
88
|
+
label: string;
|
|
89
|
+
description?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type EventCallback = (
|
|
93
|
+
taskId: string,
|
|
94
|
+
eventType: string,
|
|
95
|
+
data: Record<string, unknown>
|
|
96
|
+
) => void;
|
|
97
|
+
|
|
98
|
+
export class AiderAdapter {
|
|
99
|
+
private running: Map<string, RunningTask> = new Map();
|
|
100
|
+
private sessionIdToTaskId: Map<string, string> = new Map(); // Reverse mapping: sessionId -> taskId
|
|
101
|
+
private onEvent: EventCallback;
|
|
102
|
+
private nextPort = AGENTAPI_PORT;
|
|
103
|
+
|
|
104
|
+
constructor(onEvent: EventCallback) {
|
|
105
|
+
this.onEvent = onEvent;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get next available port for AgentAPI
|
|
110
|
+
*/
|
|
111
|
+
private getNextPort(): number {
|
|
112
|
+
// Simple port allocation - in production you'd want to check if port is free
|
|
113
|
+
const port = this.nextPort;
|
|
114
|
+
this.nextPort++;
|
|
115
|
+
if (this.nextPort > AGENTAPI_PORT + 100) {
|
|
116
|
+
this.nextPort = AGENTAPI_PORT; // Wrap around
|
|
117
|
+
}
|
|
118
|
+
return port;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Start a new task by launching AgentAPI with Aider
|
|
123
|
+
*/
|
|
124
|
+
async startTask(
|
|
125
|
+
taskId: string,
|
|
126
|
+
instruction: string,
|
|
127
|
+
projectPath?: string
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
console.log(`[${taskId}] Starting Aider task: ${instruction.substring(0, 50)}...`);
|
|
130
|
+
|
|
131
|
+
const port = this.getNextPort();
|
|
132
|
+
// Generate a real session ID to resolve PENDING placeholder in database
|
|
133
|
+
// This allows the backend to know the session is active and fetch messages
|
|
134
|
+
const sessionId = `aider-${crypto.randomUUID()}`;
|
|
135
|
+
|
|
136
|
+
const rt: RunningTask = {
|
|
137
|
+
taskId,
|
|
138
|
+
sessionId,
|
|
139
|
+
context: '',
|
|
140
|
+
agentApiProcess: null,
|
|
141
|
+
eventSource: null,
|
|
142
|
+
timeoutHandle: null,
|
|
143
|
+
port,
|
|
144
|
+
lastStatus: 'stable',
|
|
145
|
+
firstSpinnerEmitted: false
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
this.running.set(taskId, rt);
|
|
149
|
+
this.sessionIdToTaskId.set(sessionId, taskId); // Register reverse mapping
|
|
150
|
+
|
|
151
|
+
// Determine working directory
|
|
152
|
+
let cwd = os.homedir();
|
|
153
|
+
if (projectPath && fs.existsSync(projectPath)) {
|
|
154
|
+
cwd = projectPath;
|
|
155
|
+
} else if (projectPath) {
|
|
156
|
+
console.log(`[${taskId}] Warning: project path does not exist: ${projectPath}`);
|
|
157
|
+
this.onEvent(taskId, 'WARNING', {
|
|
158
|
+
warning: `Project path "${projectPath}" does not exist. Running in home directory.`
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Build AgentAPI command
|
|
163
|
+
// agentapi server --port <port> -- aider [aider options]
|
|
164
|
+
const args = [
|
|
165
|
+
'server',
|
|
166
|
+
'--port', port.toString(),
|
|
167
|
+
'--type', 'aider',
|
|
168
|
+
'--',
|
|
169
|
+
AIDER_PATH,
|
|
170
|
+
'--yes-always' // Auto-accept changes for automation
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// Add model if specified in environment
|
|
174
|
+
if (process.env.AIDER_MODEL) {
|
|
175
|
+
args.push('--model', process.env.AIDER_MODEL);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`[${taskId}] Spawning: ${AGENTAPI_PATH} ${args.join(' ')} in ${cwd}`);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Use node-pty to spawn agentapi with a proper PTY
|
|
182
|
+
// This is required because agentapi uses terminal emulation internally
|
|
183
|
+
const proc = pty.spawn(AGENTAPI_PATH, args, {
|
|
184
|
+
name: 'xterm-color',
|
|
185
|
+
cols: 120,
|
|
186
|
+
rows: 40,
|
|
187
|
+
cwd,
|
|
188
|
+
env: process.env as { [key: string]: string }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
rt.agentApiProcess = proc;
|
|
192
|
+
|
|
193
|
+
// Log output for debugging (PTY combines stdout/stderr)
|
|
194
|
+
proc.onData((data) => {
|
|
195
|
+
console.log(`[${taskId}] agentapi output: ${data.substring(0, 200)}`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
proc.onExit(({ exitCode }) => {
|
|
199
|
+
console.log(`[${taskId}] AgentAPI process exited with code ${exitCode}`);
|
|
200
|
+
this.cleanup(taskId);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Wait for AgentAPI to be ready
|
|
204
|
+
await this.waitForAgentApi(taskId, port);
|
|
205
|
+
|
|
206
|
+
// Set up SSE event stream
|
|
207
|
+
this.connectEventStream(taskId, port, rt);
|
|
208
|
+
|
|
209
|
+
// Set timeout
|
|
210
|
+
rt.timeoutHandle = setTimeout(() => {
|
|
211
|
+
console.log(`[${taskId}] Task timed out`);
|
|
212
|
+
this.onEvent(taskId, 'ERROR', { error: 'execution timeout' });
|
|
213
|
+
this.cancelTask(taskId);
|
|
214
|
+
}, DEFAULT_TIMEOUT);
|
|
215
|
+
|
|
216
|
+
// Send the initial instruction
|
|
217
|
+
await this.sendMessage(taskId, port, instruction);
|
|
218
|
+
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error(`[${taskId}] Failed to start AgentAPI:`, err);
|
|
221
|
+
this.onEvent(taskId, 'ERROR', { error: (err as Error).message });
|
|
222
|
+
this.cleanup(taskId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resume a task - for Aider, this just sends another message
|
|
228
|
+
*/
|
|
229
|
+
async resumeTask(
|
|
230
|
+
taskId: string,
|
|
231
|
+
sessionId: string,
|
|
232
|
+
message: string,
|
|
233
|
+
projectPath?: string
|
|
234
|
+
): Promise<void> {
|
|
235
|
+
console.log(`[${taskId}] Resuming Aider task with message`);
|
|
236
|
+
|
|
237
|
+
const rt = this.running.get(taskId);
|
|
238
|
+
if (!rt) {
|
|
239
|
+
// Task not running, need to restart
|
|
240
|
+
console.log(`[${taskId}] Task not found, starting fresh`);
|
|
241
|
+
return this.startTask(taskId, message, projectPath);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
await this.sendMessage(taskId, rt.port, message);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.error(`[${taskId}] Failed to send message:`, err);
|
|
248
|
+
this.onEvent(taskId, 'ERROR', { error: (err as Error).message });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Cancel a running task
|
|
254
|
+
*/
|
|
255
|
+
async cancelTask(taskId: string): Promise<void> {
|
|
256
|
+
console.log(`[${taskId}] Cancelling task`);
|
|
257
|
+
this.cleanup(taskId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Stop all running tasks
|
|
262
|
+
*/
|
|
263
|
+
async stopAll(): Promise<void> {
|
|
264
|
+
for (const taskId of this.running.keys()) {
|
|
265
|
+
await this.cancelTask(taskId);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get list of running task IDs
|
|
271
|
+
*/
|
|
272
|
+
getRunningTasks(): string[] {
|
|
273
|
+
return Array.from(this.running.keys());
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get messages for a session from AgentAPI
|
|
278
|
+
* Returns messages if the session's AgentAPI is running, null otherwise
|
|
279
|
+
* @param id - Either the canonical taskId or the generated sessionId
|
|
280
|
+
*/
|
|
281
|
+
async getMessages(id: string): Promise<{ id: number; role: string; content: string; time: string }[] | null> {
|
|
282
|
+
// First try as taskId (canonical ID)
|
|
283
|
+
let rt = this.running.get(id);
|
|
284
|
+
|
|
285
|
+
// If not found, try as sessionId using reverse mapping
|
|
286
|
+
if (!rt) {
|
|
287
|
+
const taskId = this.sessionIdToTaskId.get(id);
|
|
288
|
+
if (taskId) {
|
|
289
|
+
rt = this.running.get(taskId);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!rt) {
|
|
294
|
+
console.log(`[${id}] Cannot get messages - task not running (checked both taskId and sessionId)`);
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const response = await fetch(`http://localhost:${rt.port}/messages`);
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
console.log(`[${id}] Failed to fetch messages: ${response.status}`);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const data = await response.json() as { messages: { id: number; role: string; content: string; time: string }[] };
|
|
306
|
+
return data.messages || [];
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error(`[${id}] Error fetching messages:`, (err as Error).message);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get the port for a running task's AgentAPI
|
|
315
|
+
*/
|
|
316
|
+
getTaskPort(taskId: string): number | null {
|
|
317
|
+
const rt = this.running.get(taskId);
|
|
318
|
+
return rt ? rt.port : null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Wait for AgentAPI server to be ready and agent to be stable
|
|
323
|
+
*
|
|
324
|
+
* AgentAPI requires the agent status to be "stable" before accepting messages.
|
|
325
|
+
* When first started, Aider may be "running" while it builds the repo map.
|
|
326
|
+
*/
|
|
327
|
+
private async waitForAgentApi(taskId: string, port: number): Promise<void> {
|
|
328
|
+
const startTime = Date.now();
|
|
329
|
+
const url = `http://localhost:${port}/status`;
|
|
330
|
+
|
|
331
|
+
// First, wait for the server to be up
|
|
332
|
+
let serverUp = false;
|
|
333
|
+
while (Date.now() - startTime < AGENTAPI_STARTUP_TIMEOUT) {
|
|
334
|
+
try {
|
|
335
|
+
const response = await fetch(url);
|
|
336
|
+
if (response.ok) {
|
|
337
|
+
serverUp = true;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
// Not ready yet
|
|
342
|
+
}
|
|
343
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!serverUp) {
|
|
347
|
+
throw new Error(`AgentAPI failed to start within ${AGENTAPI_STARTUP_TIMEOUT}ms`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.log(`[${taskId}] AgentAPI server up on port ${port}, waiting for stable status...`);
|
|
351
|
+
|
|
352
|
+
// Now wait for the agent to be stable (not running/initializing)
|
|
353
|
+
while (Date.now() - startTime < AGENTAPI_STARTUP_TIMEOUT) {
|
|
354
|
+
try {
|
|
355
|
+
const response = await fetch(url);
|
|
356
|
+
if (response.ok) {
|
|
357
|
+
const data = await response.json() as { status: string };
|
|
358
|
+
if (data.status === 'stable') {
|
|
359
|
+
console.log(`[${taskId}] AgentAPI ready and stable on port ${port}`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
console.log(`[${taskId}] AgentAPI status: ${data.status}, waiting for stable...`);
|
|
363
|
+
}
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.log(`[${taskId}] Error checking status:`, (err as Error).message);
|
|
366
|
+
}
|
|
367
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
throw new Error(`Agent failed to reach stable status within ${AGENTAPI_STARTUP_TIMEOUT}ms`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Connect to AgentAPI SSE event stream
|
|
375
|
+
*
|
|
376
|
+
* AgentAPI sends named events: 'message_update' and 'status_change'
|
|
377
|
+
* We need to listen for these specifically (onmessage only handles unnamed events)
|
|
378
|
+
*/
|
|
379
|
+
private connectEventStream(taskId: string, port: number, rt: RunningTask): void {
|
|
380
|
+
const url = `http://localhost:${port}/events`;
|
|
381
|
+
console.log(`[${taskId}] Connecting to SSE stream: ${url}`);
|
|
382
|
+
|
|
383
|
+
const es = new EventSource(url);
|
|
384
|
+
rt.eventSource = es;
|
|
385
|
+
|
|
386
|
+
// Handle message_update events (agent messages)
|
|
387
|
+
es.addEventListener('message_update', (event) => {
|
|
388
|
+
try {
|
|
389
|
+
const data = JSON.parse(event.data as string);
|
|
390
|
+
console.log(`[${taskId}] SSE message_update:`, JSON.stringify(data).substring(0, 100));
|
|
391
|
+
this.handleMessageUpdate(taskId, data, rt);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.error(`[${taskId}] Failed to parse message_update:`, err);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Handle status_change events
|
|
398
|
+
es.addEventListener('status_change', (event) => {
|
|
399
|
+
try {
|
|
400
|
+
const data = JSON.parse(event.data as string);
|
|
401
|
+
console.log(`[${taskId}] SSE status_change:`, data.status);
|
|
402
|
+
this.handleStatusChange(taskId, data, rt);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.error(`[${taskId}] Failed to parse status_change:`, err);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Fallback for any unnamed events
|
|
409
|
+
es.onmessage = (event) => {
|
|
410
|
+
try {
|
|
411
|
+
const data = JSON.parse(event.data as string);
|
|
412
|
+
console.log(`[${taskId}] SSE unnamed event:`, JSON.stringify(data).substring(0, 100));
|
|
413
|
+
this.handleAgentApiEvent(taskId, data, rt);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
console.error(`[${taskId}] Failed to parse SSE event:`, err);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
es.onerror = (err) => {
|
|
420
|
+
const errorEvent = err as ErrorEvent;
|
|
421
|
+
console.error(`[${taskId}] SSE error:`, errorEvent.message || 'unknown error');
|
|
422
|
+
// Don't cleanup here - AgentAPI might still be running
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Check if a message is a spinner/progress line that should be filtered
|
|
428
|
+
* These lines use \r to overwrite in terminal but create noise in our UI
|
|
429
|
+
*/
|
|
430
|
+
private isSpinnerLine(message: string): boolean {
|
|
431
|
+
// Spinner characters
|
|
432
|
+
if (message.includes('░') || message.includes('█')) {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
// Common progress patterns (partial lines without timestamps)
|
|
436
|
+
if (message.includes('Waiting for anthropic/') || message.includes('Updating repo map:')) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Handle message_update SSE event
|
|
444
|
+
*/
|
|
445
|
+
private handleMessageUpdate(
|
|
446
|
+
taskId: string,
|
|
447
|
+
event: { id: number; role: string; message: string; time: string },
|
|
448
|
+
rt: RunningTask
|
|
449
|
+
): void {
|
|
450
|
+
if (event.role === 'agent') {
|
|
451
|
+
// Skip empty/whitespace-only messages (often from spinner line clears)
|
|
452
|
+
if (!event.message.trim()) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Skip spinner/progress lines - but let the first one through for progress indication
|
|
457
|
+
if (this.isSpinnerLine(event.message)) {
|
|
458
|
+
if (rt.firstSpinnerEmitted) {
|
|
459
|
+
return; // Skip subsequent spinner lines
|
|
460
|
+
}
|
|
461
|
+
rt.firstSpinnerEmitted = true;
|
|
462
|
+
// Clean up the spinner message to just show the status text
|
|
463
|
+
const cleanMessage = event.message.replace(/[░█\s]+/g, '').trim();
|
|
464
|
+
if (cleanMessage) {
|
|
465
|
+
this.onEvent(taskId, 'OUTPUT', { output: cleanMessage, session_id: rt.sessionId });
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Accumulate context
|
|
471
|
+
if (rt.context) {
|
|
472
|
+
rt.context += '\n\n';
|
|
473
|
+
}
|
|
474
|
+
rt.context += event.message;
|
|
475
|
+
|
|
476
|
+
// Emit output for streaming display (include session_id to resolve PENDING)
|
|
477
|
+
this.onEvent(taskId, 'OUTPUT', { output: event.message, session_id: rt.sessionId });
|
|
478
|
+
|
|
479
|
+
// Check if aider is asking for input
|
|
480
|
+
if (this.looksLikeQuestion(event.message)) {
|
|
481
|
+
console.log(`[${taskId}] Detected question from Aider`);
|
|
482
|
+
this.onEvent(taskId, 'WAIT_FOR_USER', {
|
|
483
|
+
session_id: rt.sessionId,
|
|
484
|
+
prompt: this.extractQuestion(event.message),
|
|
485
|
+
options: [],
|
|
486
|
+
context: rt.context
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Handle status_change SSE event
|
|
494
|
+
*/
|
|
495
|
+
private handleStatusChange(
|
|
496
|
+
taskId: string,
|
|
497
|
+
event: { status: string; agent_type: string },
|
|
498
|
+
rt: RunningTask
|
|
499
|
+
): void {
|
|
500
|
+
const newStatus = event.status as 'stable' | 'running';
|
|
501
|
+
|
|
502
|
+
// Detect transition from running -> stable (task completed)
|
|
503
|
+
if (rt.lastStatus === 'running' && newStatus === 'stable') {
|
|
504
|
+
console.log(`[${taskId}] Task completed (running -> stable)`);
|
|
505
|
+
this.onEvent(taskId, 'TASK_COMPLETE', {
|
|
506
|
+
session_id: rt.sessionId,
|
|
507
|
+
result: rt.context
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
rt.lastStatus = newStatus;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Handle an event from AgentAPI
|
|
516
|
+
*/
|
|
517
|
+
private handleAgentApiEvent(
|
|
518
|
+
taskId: string,
|
|
519
|
+
event: { type: string; [key: string]: unknown },
|
|
520
|
+
rt: RunningTask
|
|
521
|
+
): void {
|
|
522
|
+
console.log(`[${taskId}] AgentAPI event:`, JSON.stringify(event).substring(0, 200));
|
|
523
|
+
|
|
524
|
+
if (event.type === 'status') {
|
|
525
|
+
const newStatus = event.status as 'stable' | 'running';
|
|
526
|
+
|
|
527
|
+
// Detect transition from running -> stable (task completed)
|
|
528
|
+
if (rt.lastStatus === 'running' && newStatus === 'stable') {
|
|
529
|
+
console.log(`[${taskId}] Task completed (running -> stable)`);
|
|
530
|
+
this.onEvent(taskId, 'TASK_COMPLETE', {
|
|
531
|
+
session_id: rt.sessionId,
|
|
532
|
+
result: rt.context
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
rt.lastStatus = newStatus;
|
|
537
|
+
} else if (event.type === 'message') {
|
|
538
|
+
// Message from the agent
|
|
539
|
+
const content = event.content as string || '';
|
|
540
|
+
const role = event.role as string || 'assistant';
|
|
541
|
+
|
|
542
|
+
if (role === 'assistant') {
|
|
543
|
+
// Skip spinner/progress lines
|
|
544
|
+
if (this.isSpinnerLine(content)) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Accumulate context
|
|
549
|
+
if (rt.context) {
|
|
550
|
+
rt.context += '\n\n';
|
|
551
|
+
}
|
|
552
|
+
rt.context += content;
|
|
553
|
+
|
|
554
|
+
// Emit output for streaming display (include session_id to resolve PENDING)
|
|
555
|
+
this.onEvent(taskId, 'OUTPUT', { output: content, session_id: rt.sessionId });
|
|
556
|
+
|
|
557
|
+
// Check if aider is asking for input
|
|
558
|
+
// Aider typically asks questions ending with ? or prompts for y/n
|
|
559
|
+
if (this.looksLikeQuestion(content)) {
|
|
560
|
+
console.log(`[${taskId}] Detected question from Aider`);
|
|
561
|
+
this.onEvent(taskId, 'WAIT_FOR_USER', {
|
|
562
|
+
session_id: rt.sessionId,
|
|
563
|
+
prompt: this.extractQuestion(content),
|
|
564
|
+
options: [],
|
|
565
|
+
context: rt.context
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Check if content looks like a question/prompt
|
|
574
|
+
*/
|
|
575
|
+
private looksLikeQuestion(content: string): boolean {
|
|
576
|
+
const lower = content.toLowerCase().trim();
|
|
577
|
+
|
|
578
|
+
// Common Aider prompts
|
|
579
|
+
const questionPatterns = [
|
|
580
|
+
/\?\s*$/, // Ends with question mark
|
|
581
|
+
/\(y\/n\)/i, // Yes/no prompt
|
|
582
|
+
/\[y\/n\]/i,
|
|
583
|
+
/proceed\?/i,
|
|
584
|
+
/continue\?/i,
|
|
585
|
+
/confirm/i,
|
|
586
|
+
/would you like/i,
|
|
587
|
+
/do you want/i,
|
|
588
|
+
/should i/i
|
|
589
|
+
];
|
|
590
|
+
|
|
591
|
+
return questionPatterns.some(pattern => pattern.test(lower));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Extract the question from content
|
|
596
|
+
*/
|
|
597
|
+
private extractQuestion(content: string): string {
|
|
598
|
+
// Take the last few lines which typically contain the question
|
|
599
|
+
const lines = content.trim().split('\n');
|
|
600
|
+
const lastLines = lines.slice(-3).join('\n');
|
|
601
|
+
return lastLines.length > 200 ? lastLines.substring(0, 200) + '...' : lastLines;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Send a message to AgentAPI
|
|
606
|
+
*/
|
|
607
|
+
private async sendMessage(taskId: string, port: number, content: string): Promise<void> {
|
|
608
|
+
const url = `http://localhost:${port}/message`;
|
|
609
|
+
console.log(`[${taskId}] Sending message to AgentAPI`);
|
|
610
|
+
|
|
611
|
+
const response = await fetch(url, {
|
|
612
|
+
method: 'POST',
|
|
613
|
+
headers: {
|
|
614
|
+
'Content-Type': 'application/json'
|
|
615
|
+
},
|
|
616
|
+
body: JSON.stringify({
|
|
617
|
+
content,
|
|
618
|
+
type: 'user'
|
|
619
|
+
})
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
if (!response.ok) {
|
|
623
|
+
throw new Error(`Failed to send message: ${response.status} ${response.statusText}`);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
console.log(`[${taskId}] Message sent successfully`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Clean up a task
|
|
631
|
+
*/
|
|
632
|
+
private cleanup(taskId: string): void {
|
|
633
|
+
const rt = this.running.get(taskId);
|
|
634
|
+
if (!rt) return;
|
|
635
|
+
|
|
636
|
+
if (rt.timeoutHandle) {
|
|
637
|
+
clearTimeout(rt.timeoutHandle);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (rt.eventSource) {
|
|
641
|
+
rt.eventSource.close();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (rt.agentApiProcess) {
|
|
645
|
+
rt.agentApiProcess.kill();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Clean up reverse mapping
|
|
649
|
+
if (rt.sessionId) {
|
|
650
|
+
this.sessionIdToTaskId.delete(rt.sessionId);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
this.running.delete(taskId);
|
|
654
|
+
console.log(`[${taskId}] Task cleaned up`);
|
|
655
|
+
}
|
|
656
|
+
}
|