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