@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.
Files changed (47) hide show
  1. package/dist/adapter/agentapi.d.ts +100 -0
  2. package/dist/adapter/agentapi.d.ts.map +1 -0
  3. package/dist/adapter/agentapi.js +578 -0
  4. package/dist/adapter/agentapi.js.map +1 -0
  5. package/dist/client/messages.d.ts +89 -0
  6. package/dist/client/messages.d.ts.map +1 -0
  7. package/dist/client/messages.js +6 -0
  8. package/dist/client/messages.js.map +1 -0
  9. package/dist/client/websocket.d.ts +66 -0
  10. package/dist/client/websocket.d.ts.map +1 -0
  11. package/dist/client/websocket.js +276 -0
  12. package/dist/client/websocket.js.map +1 -0
  13. package/dist/commands/register.d.ts +10 -0
  14. package/dist/commands/register.d.ts.map +1 -0
  15. package/dist/commands/register.js +175 -0
  16. package/dist/commands/register.js.map +1 -0
  17. package/dist/commands/start.d.ts +9 -0
  18. package/dist/commands/start.d.ts.map +1 -0
  19. package/dist/commands/start.js +54 -0
  20. package/dist/commands/start.js.map +1 -0
  21. package/dist/commands/status.d.ts +5 -0
  22. package/dist/commands/status.d.ts.map +1 -0
  23. package/dist/commands/status.js +37 -0
  24. package/dist/commands/status.js.map +1 -0
  25. package/dist/commands/stop.d.ts +5 -0
  26. package/dist/commands/stop.d.ts.map +1 -0
  27. package/dist/commands/stop.js +59 -0
  28. package/dist/commands/stop.js.map +1 -0
  29. package/dist/config/config.d.ts +60 -0
  30. package/dist/config/config.d.ts.map +1 -0
  31. package/dist/config/config.js +176 -0
  32. package/dist/config/config.js.map +1 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +34 -0
  36. package/dist/index.js.map +1 -0
  37. package/package.json +42 -0
  38. package/src/adapter/agentapi.ts +656 -0
  39. package/src/client/messages.ts +125 -0
  40. package/src/client/websocket.ts +317 -0
  41. package/src/commands/register.ts +201 -0
  42. package/src/commands/start.ts +70 -0
  43. package/src/commands/status.ts +45 -0
  44. package/src/commands/stop.ts +58 -0
  45. package/src/config/config.ts +146 -0
  46. package/src/index.ts +39 -0
  47. 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
+ }