@cmdctrl/cursor-cli 0.1.1 → 0.2.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/cursor-cli.d.ts +23 -19
- package/dist/adapter/cursor-cli.d.ts.map +1 -1
- package/dist/adapter/cursor-cli.js +156 -126
- package/dist/adapter/cursor-cli.js.map +1 -1
- package/dist/adapter/events.d.ts +36 -20
- package/dist/adapter/events.d.ts.map +1 -1
- package/dist/adapter/events.js +40 -35
- package/dist/adapter/events.js.map +1 -1
- package/dist/commands/register.d.ts +0 -3
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +23 -122
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/start.d.ts +1 -8
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +117 -30
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts +1 -4
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +25 -22
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/stop.d.ts +1 -4
- package/dist/commands/stop.d.ts.map +1 -1
- package/dist/commands/stop.js +21 -26
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/unregister.d.ts +2 -0
- package/dist/commands/unregister.d.ts.map +1 -0
- package/dist/commands/unregister.js +43 -0
- package/dist/commands/unregister.js.map +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +0 -3
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/message-store.d.ts +18 -0
- package/dist/message-store.d.ts.map +1 -0
- package/dist/message-store.js +49 -0
- package/dist/message-store.js.map +1 -0
- package/package.json +2 -2
- package/src/adapter/cursor-cli.ts +165 -147
- package/src/adapter/events.ts +65 -51
- package/src/commands/register.ts +28 -170
- package/src/commands/start.ts +132 -41
- package/src/commands/status.ts +23 -28
- package/src/commands/stop.ts +21 -32
- package/src/commands/unregister.ts +43 -0
- package/src/commands/update.ts +0 -3
- package/src/index.ts +9 -4
- package/src/message-store.ts +61 -0
- package/src/client/messages.ts +0 -75
- package/src/client/websocket.ts +0 -308
- package/src/config/config.ts +0 -146
|
@@ -1,13 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Spawns cursor-agent in headless mode with --output-format stream-json
|
|
5
|
+
* and translates the NDJSON event stream into CmdCtrl daemon events.
|
|
6
|
+
*
|
|
7
|
+
* Cursor CLI commands:
|
|
8
|
+
* New session: cursor-agent -p "instruction" --output-format stream-json
|
|
9
|
+
* Resume session: cursor-agent --resume=<session-id> -p "message" --output-format stream-json
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
import { spawn, ChildProcess } from 'child_process';
|
|
2
13
|
import * as readline from 'readline';
|
|
3
14
|
import * as fs from 'fs';
|
|
4
15
|
import * as os from 'os';
|
|
5
16
|
import * as path from 'path';
|
|
6
|
-
import { StreamEvent,
|
|
17
|
+
import { StreamEvent, extractProgressFromToolCall } from './events';
|
|
7
18
|
|
|
8
19
|
const DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
9
20
|
|
|
10
|
-
// Find cursor-agent CLI in common locations
|
|
11
21
|
function findCursorCli(): string {
|
|
12
22
|
if (process.env.CURSOR_CLI_PATH) {
|
|
13
23
|
return process.env.CURSOR_CLI_PATH;
|
|
@@ -15,34 +25,28 @@ function findCursorCli(): string {
|
|
|
15
25
|
|
|
16
26
|
const home = os.homedir();
|
|
17
27
|
const commonPaths = [
|
|
18
|
-
path.join(home, '.cursor', 'bin', 'cursor-agent'),
|
|
19
28
|
path.join(home, '.local', 'bin', 'cursor-agent'),
|
|
29
|
+
path.join(home, '.cursor', 'bin', 'cursor-agent'),
|
|
20
30
|
'/usr/local/bin/cursor-agent',
|
|
21
31
|
'/opt/homebrew/bin/cursor-agent',
|
|
22
|
-
'cursor-agent'
|
|
32
|
+
'cursor-agent'
|
|
23
33
|
];
|
|
24
34
|
|
|
25
35
|
for (const p of commonPaths) {
|
|
26
|
-
if (p === 'cursor-agent') return p;
|
|
36
|
+
if (p === 'cursor-agent') return p;
|
|
27
37
|
try {
|
|
28
|
-
if (fs.existsSync(p))
|
|
29
|
-
return p;
|
|
30
|
-
}
|
|
38
|
+
if (fs.existsSync(p)) return p;
|
|
31
39
|
} catch {
|
|
32
40
|
continue;
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
return 'cursor-agent';
|
|
44
|
+
return 'cursor-agent';
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
const CLI_PATH = findCursorCli();
|
|
40
|
-
console.log(`[CursorAdapter] Using CLI path: ${CLI_PATH}`);
|
|
41
|
-
|
|
42
47
|
interface RunningTask {
|
|
43
48
|
taskId: string;
|
|
44
49
|
sessionId: string;
|
|
45
|
-
question: string;
|
|
46
50
|
context: string;
|
|
47
51
|
process: ChildProcess | null;
|
|
48
52
|
timeoutHandle: NodeJS.Timeout | null;
|
|
@@ -62,199 +66,170 @@ export class CursorAdapter {
|
|
|
62
66
|
this.onEvent = onEvent;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
/**
|
|
66
|
-
* Start a new task
|
|
67
|
-
*/
|
|
68
69
|
async startTask(
|
|
69
70
|
taskId: string,
|
|
70
71
|
instruction: string,
|
|
71
72
|
projectPath?: string
|
|
72
73
|
): Promise<void> {
|
|
73
|
-
console.log(`[${taskId}] Starting task: ${instruction.substring(0, 50)}...`);
|
|
74
|
+
console.log(`[${taskId}] Starting Cursor task: ${instruction.substring(0, 50)}...`);
|
|
74
75
|
|
|
75
76
|
const rt: RunningTask = {
|
|
76
77
|
taskId,
|
|
77
78
|
sessionId: '',
|
|
78
|
-
question: '',
|
|
79
79
|
context: '',
|
|
80
80
|
process: null,
|
|
81
|
-
timeoutHandle: null
|
|
81
|
+
timeoutHandle: null,
|
|
82
82
|
};
|
|
83
|
-
|
|
84
83
|
this.running.set(taskId, rt);
|
|
85
84
|
|
|
86
|
-
// Validate cwd exists
|
|
87
85
|
let cwd: string | undefined = undefined;
|
|
88
86
|
if (projectPath && fs.existsSync(projectPath)) {
|
|
89
87
|
cwd = projectPath;
|
|
90
88
|
} else if (projectPath) {
|
|
91
|
-
console.log(`[${taskId}] Warning: project path does not exist: ${projectPath}
|
|
89
|
+
console.log(`[${taskId}] Warning: project path does not exist: ${projectPath}`);
|
|
92
90
|
cwd = os.homedir();
|
|
93
|
-
this.onEvent(taskId, 'WARNING', {
|
|
94
|
-
warning: `Project path "${projectPath}" does not exist. Running in home directory instead.`
|
|
95
|
-
});
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
// Build command arguments for Cursor CLI
|
|
99
|
-
// cursor-agent -p "instruction" --output-format stream-json
|
|
100
93
|
const args = [
|
|
101
94
|
'-p', instruction,
|
|
102
95
|
'--output-format', 'stream-json'
|
|
103
96
|
];
|
|
104
97
|
|
|
105
|
-
console.log(`[${taskId}] Spawning: ${
|
|
98
|
+
console.log(`[${taskId}] Spawning: ${findCursorCli()} with cwd: ${cwd || 'default'}`);
|
|
106
99
|
|
|
107
|
-
|
|
108
|
-
const proc = spawn(CLI_PATH, args, {
|
|
100
|
+
const proc = spawn(findCursorCli(), args, {
|
|
109
101
|
cwd,
|
|
110
102
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
111
103
|
env: {
|
|
112
104
|
...process.env,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
105
|
+
CURSOR_API_KEY: process.env.CURSOR_API_KEY,
|
|
106
|
+
},
|
|
116
107
|
});
|
|
117
108
|
|
|
118
109
|
rt.process = proc;
|
|
119
|
-
|
|
120
|
-
// Set timeout
|
|
121
110
|
rt.timeoutHandle = setTimeout(() => {
|
|
122
111
|
console.log(`[${taskId}] Task timed out`);
|
|
123
112
|
proc.kill('SIGKILL');
|
|
124
113
|
this.onEvent(taskId, 'ERROR', { error: 'execution timeout' });
|
|
125
114
|
}, DEFAULT_TIMEOUT);
|
|
126
115
|
|
|
127
|
-
// Handle process events
|
|
128
116
|
this.handleProcessOutput(taskId, proc, rt);
|
|
129
117
|
}
|
|
130
118
|
|
|
131
|
-
/**
|
|
132
|
-
* Resume a task with user's reply
|
|
133
|
-
*/
|
|
134
119
|
async resumeTask(
|
|
135
120
|
taskId: string,
|
|
136
121
|
sessionId: string,
|
|
137
122
|
message: string,
|
|
138
123
|
projectPath?: string
|
|
139
124
|
): Promise<void> {
|
|
140
|
-
console.log(`[${taskId}] Resuming
|
|
125
|
+
console.log(`[${taskId}] Resuming Cursor session ${sessionId}: ${message.substring(0, 50)}...`);
|
|
141
126
|
|
|
142
127
|
const rt: RunningTask = {
|
|
143
128
|
taskId,
|
|
144
129
|
sessionId,
|
|
145
|
-
question: '',
|
|
146
130
|
context: '',
|
|
147
131
|
process: null,
|
|
148
|
-
timeoutHandle: null
|
|
132
|
+
timeoutHandle: null,
|
|
149
133
|
};
|
|
150
|
-
|
|
151
134
|
this.running.set(taskId, rt);
|
|
152
135
|
|
|
153
|
-
// Validate cwd exists
|
|
154
136
|
let cwd: string | undefined = undefined;
|
|
155
137
|
if (projectPath && fs.existsSync(projectPath)) {
|
|
156
138
|
cwd = projectPath;
|
|
157
139
|
} else if (projectPath) {
|
|
158
|
-
console.log(`[${taskId}] Warning: project path does not exist: ${projectPath}, using home dir`);
|
|
159
140
|
cwd = os.homedir();
|
|
160
141
|
}
|
|
161
142
|
|
|
162
|
-
// Build command arguments with --resume
|
|
163
|
-
// cursor-agent --resume="session-id" -p "user response" --output-format stream-json
|
|
164
143
|
const args = [
|
|
165
144
|
`--resume=${sessionId}`,
|
|
166
145
|
'-p', message,
|
|
167
146
|
'--output-format', 'stream-json'
|
|
168
147
|
];
|
|
169
148
|
|
|
170
|
-
console.log(`[${taskId}] Spawning resume:
|
|
149
|
+
console.log(`[${taskId}] Spawning resume: cursor-agent --resume ${sessionId}`);
|
|
171
150
|
|
|
172
|
-
|
|
173
|
-
const proc = spawn(CLI_PATH, args, {
|
|
151
|
+
const proc = spawn(findCursorCli(), args, {
|
|
174
152
|
cwd,
|
|
175
153
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
176
154
|
env: {
|
|
177
155
|
...process.env,
|
|
178
|
-
CURSOR_API_KEY: process.env.CURSOR_API_KEY
|
|
179
|
-
}
|
|
156
|
+
CURSOR_API_KEY: process.env.CURSOR_API_KEY,
|
|
157
|
+
},
|
|
180
158
|
});
|
|
181
159
|
|
|
182
160
|
rt.process = proc;
|
|
183
161
|
|
|
184
|
-
|
|
162
|
+
let sessionNotFound = false;
|
|
163
|
+
|
|
164
|
+
proc.stderr?.on('data', (data) => {
|
|
165
|
+
const text = data.toString();
|
|
166
|
+
console.log(`[${taskId}] stderr: ${text}`);
|
|
167
|
+
if (text.includes('not found') || text.includes('No session') || text.includes('invalid session')) {
|
|
168
|
+
sessionNotFound = true;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
proc.on('close', (code) => {
|
|
173
|
+
if (code !== 0 && sessionNotFound) {
|
|
174
|
+
console.log(`[${taskId}] Session not found, falling back to new session`);
|
|
175
|
+
if (rt.timeoutHandle) clearTimeout(rt.timeoutHandle);
|
|
176
|
+
this.running.delete(taskId);
|
|
177
|
+
this.startTask(taskId, message, projectPath);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
console.log(`[${taskId}] Process exited with code ${code}`);
|
|
181
|
+
if (rt.timeoutHandle) clearTimeout(rt.timeoutHandle);
|
|
182
|
+
if (this.running.has(taskId) && code === 0) {
|
|
183
|
+
this.onEvent(taskId, 'TASK_COMPLETE', {
|
|
184
|
+
session_id: rt.sessionId,
|
|
185
|
+
result: rt.context || '',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
this.running.delete(taskId);
|
|
189
|
+
});
|
|
190
|
+
|
|
185
191
|
rt.timeoutHandle = setTimeout(() => {
|
|
186
192
|
console.log(`[${taskId}] Task timed out`);
|
|
187
193
|
proc.kill('SIGKILL');
|
|
188
194
|
this.onEvent(taskId, 'ERROR', { error: 'execution timeout' });
|
|
189
195
|
}, DEFAULT_TIMEOUT);
|
|
190
196
|
|
|
191
|
-
|
|
192
|
-
this.handleProcessOutput(taskId, proc, rt);
|
|
197
|
+
this.handleProcessOutputWithoutClose(taskId, proc, rt);
|
|
193
198
|
}
|
|
194
199
|
|
|
195
|
-
/**
|
|
196
|
-
* Cancel a running task
|
|
197
|
-
*/
|
|
198
200
|
async cancelTask(taskId: string): Promise<void> {
|
|
199
201
|
const rt = this.running.get(taskId);
|
|
200
|
-
if (!rt)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (rt.process) {
|
|
206
|
-
rt.process.kill('SIGTERM');
|
|
207
|
-
}
|
|
208
|
-
if (rt.timeoutHandle) {
|
|
209
|
-
clearTimeout(rt.timeoutHandle);
|
|
210
|
-
}
|
|
211
|
-
|
|
202
|
+
if (!rt) return;
|
|
203
|
+
if (rt.process) rt.process.kill('SIGTERM');
|
|
204
|
+
if (rt.timeoutHandle) clearTimeout(rt.timeoutHandle);
|
|
212
205
|
this.running.delete(taskId);
|
|
213
206
|
console.log(`[${taskId}] Task cancelled`);
|
|
214
207
|
}
|
|
215
208
|
|
|
216
|
-
/**
|
|
217
|
-
* Stop all running tasks
|
|
218
|
-
*/
|
|
219
209
|
async stopAll(): Promise<void> {
|
|
220
210
|
for (const [taskId, rt] of this.running) {
|
|
221
211
|
console.log(`[${taskId}] Stopping task`);
|
|
222
|
-
if (rt.process)
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
if (rt.timeoutHandle) {
|
|
226
|
-
clearTimeout(rt.timeoutHandle);
|
|
227
|
-
}
|
|
212
|
+
if (rt.process) rt.process.kill('SIGTERM');
|
|
213
|
+
if (rt.timeoutHandle) clearTimeout(rt.timeoutHandle);
|
|
228
214
|
}
|
|
229
215
|
this.running.clear();
|
|
230
216
|
}
|
|
231
217
|
|
|
232
|
-
/**
|
|
233
|
-
* Get list of running task IDs
|
|
234
|
-
*/
|
|
235
218
|
getRunningTasks(): string[] {
|
|
236
219
|
return Array.from(this.running.keys());
|
|
237
220
|
}
|
|
238
221
|
|
|
239
|
-
/**
|
|
240
|
-
* Handle process stdout/stderr and emit events
|
|
241
|
-
*/
|
|
242
222
|
private handleProcessOutput(
|
|
243
223
|
taskId: string,
|
|
244
224
|
proc: ChildProcess,
|
|
245
225
|
rt: RunningTask
|
|
246
226
|
): void {
|
|
247
|
-
// Create readline interface for NDJSON parsing
|
|
248
227
|
const rl = readline.createInterface({
|
|
249
228
|
input: proc.stdout!,
|
|
250
|
-
crlfDelay: Infinity
|
|
229
|
+
crlfDelay: Infinity,
|
|
251
230
|
});
|
|
252
231
|
|
|
253
|
-
// Parse each line as JSON
|
|
254
232
|
rl.on('line', (line) => {
|
|
255
|
-
// Emit raw output for verbose mode
|
|
256
|
-
this.onEvent(taskId, 'OUTPUT', { output: line });
|
|
257
|
-
|
|
258
233
|
try {
|
|
259
234
|
const event = JSON.parse(line) as StreamEvent;
|
|
260
235
|
this.handleStreamEvent(taskId, event, rt);
|
|
@@ -263,17 +238,16 @@ export class CursorAdapter {
|
|
|
263
238
|
}
|
|
264
239
|
});
|
|
265
240
|
|
|
266
|
-
// Log stderr
|
|
267
241
|
proc.stderr?.on('data', (data) => {
|
|
268
242
|
console.log(`[${taskId}] stderr: ${data.toString()}`);
|
|
269
243
|
});
|
|
270
244
|
|
|
271
|
-
// Handle process exit
|
|
272
245
|
proc.on('close', (code) => {
|
|
273
246
|
console.log(`[${taskId}] Process exited with code ${code}`);
|
|
274
|
-
|
|
275
|
-
if
|
|
276
|
-
|
|
247
|
+
if (rt.timeoutHandle) clearTimeout(rt.timeoutHandle);
|
|
248
|
+
// Emit error if process exited non-zero and we haven't already emitted a result
|
|
249
|
+
if (this.running.has(taskId) && code !== 0) {
|
|
250
|
+
this.onEvent(taskId, 'ERROR', { error: `cursor-agent exited with code ${code}` });
|
|
277
251
|
}
|
|
278
252
|
this.running.delete(taskId);
|
|
279
253
|
});
|
|
@@ -281,90 +255,134 @@ export class CursorAdapter {
|
|
|
281
255
|
proc.on('error', (err) => {
|
|
282
256
|
console.error(`[${taskId}] Process error:`, err);
|
|
283
257
|
this.onEvent(taskId, 'ERROR', { error: err.message });
|
|
258
|
+
if (rt.timeoutHandle) clearTimeout(rt.timeoutHandle);
|
|
259
|
+
this.running.delete(taskId);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private handleProcessOutputWithoutClose(
|
|
264
|
+
taskId: string,
|
|
265
|
+
proc: ChildProcess,
|
|
266
|
+
rt: RunningTask
|
|
267
|
+
): void {
|
|
268
|
+
const rl = readline.createInterface({
|
|
269
|
+
input: proc.stdout!,
|
|
270
|
+
crlfDelay: Infinity,
|
|
271
|
+
});
|
|
284
272
|
|
|
285
|
-
|
|
286
|
-
|
|
273
|
+
rl.on('line', (line) => {
|
|
274
|
+
try {
|
|
275
|
+
const event = JSON.parse(line) as StreamEvent;
|
|
276
|
+
this.handleStreamEvent(taskId, event, rt);
|
|
277
|
+
} catch {
|
|
278
|
+
// skip
|
|
287
279
|
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
proc.on('error', (err) => {
|
|
283
|
+
console.error(`[${taskId}] Process error:`, err);
|
|
284
|
+
this.onEvent(taskId, 'ERROR', { error: err.message });
|
|
285
|
+
if (rt.timeoutHandle) clearTimeout(rt.timeoutHandle);
|
|
288
286
|
this.running.delete(taskId);
|
|
289
287
|
});
|
|
290
288
|
}
|
|
291
289
|
|
|
292
290
|
/**
|
|
293
|
-
* Handle a parsed stream event from
|
|
291
|
+
* Handle a parsed stream event from cursor-agent and translate to CmdCtrl events.
|
|
292
|
+
*
|
|
293
|
+
* cursor-agent events → CmdCtrl events:
|
|
294
|
+
* system (init) → SESSION_STARTED
|
|
295
|
+
* user → (ignored, echo of input)
|
|
296
|
+
* thinking (delta) → (accumulate context)
|
|
297
|
+
* thinking (completed) → (ignored)
|
|
298
|
+
* assistant → OUTPUT
|
|
299
|
+
* tool_call → PROGRESS
|
|
300
|
+
* tool_result → OUTPUT (verbose)
|
|
301
|
+
* result (success) → TASK_COMPLETE
|
|
302
|
+
* result (error) → ERROR
|
|
294
303
|
*/
|
|
295
304
|
private handleStreamEvent(
|
|
296
305
|
taskId: string,
|
|
297
306
|
event: StreamEvent,
|
|
298
307
|
rt: RunningTask
|
|
299
308
|
): void {
|
|
309
|
+
console.log(`[${taskId}] Cursor event: type=${event.type} subtype=${event.subtype || ''}`);
|
|
310
|
+
|
|
300
311
|
switch (event.type) {
|
|
301
|
-
case '
|
|
302
|
-
if (event.session_id) {
|
|
312
|
+
case 'system':
|
|
313
|
+
if (event.subtype === 'init' && event.session_id) {
|
|
303
314
|
rt.sessionId = event.session_id;
|
|
304
|
-
console.log(`[${taskId}] Session initialized: ${event.session_id}`);
|
|
315
|
+
console.log(`[${taskId}] Session initialized: ${event.session_id} (model: ${event.model})`);
|
|
316
|
+
this.onEvent(taskId, 'SESSION_STARTED', {
|
|
317
|
+
session_id: event.session_id,
|
|
318
|
+
});
|
|
305
319
|
}
|
|
306
320
|
break;
|
|
307
321
|
|
|
322
|
+
case 'user':
|
|
323
|
+
// Echo of user input, ignore
|
|
324
|
+
break;
|
|
325
|
+
|
|
308
326
|
case 'thinking':
|
|
309
|
-
// Accumulate thinking
|
|
310
|
-
if (event.
|
|
311
|
-
|
|
312
|
-
|
|
327
|
+
// Accumulate thinking text but don't send as output (too noisy with deltas)
|
|
328
|
+
if (event.subtype === 'delta' && event.text) {
|
|
329
|
+
rt.context += event.text;
|
|
330
|
+
}
|
|
331
|
+
break;
|
|
332
|
+
|
|
333
|
+
case 'assistant':
|
|
334
|
+
if (event.message?.content) {
|
|
335
|
+
const text = event.message.content
|
|
336
|
+
.map(block => block.text || '')
|
|
337
|
+
.join('')
|
|
338
|
+
.trim();
|
|
339
|
+
if (text) {
|
|
340
|
+
// Reset context to assistant response (thinking was intermediate)
|
|
341
|
+
rt.context = text;
|
|
342
|
+
this.onEvent(taskId, 'OUTPUT', { output: text });
|
|
313
343
|
}
|
|
314
|
-
rt.context += event.content;
|
|
315
344
|
}
|
|
316
345
|
break;
|
|
317
346
|
|
|
318
|
-
case '
|
|
319
|
-
|
|
320
|
-
const progress = extractProgressFromAction(event);
|
|
347
|
+
case 'tool_call': {
|
|
348
|
+
const progress = extractProgressFromToolCall(event);
|
|
321
349
|
if (progress) {
|
|
322
350
|
this.onEvent(taskId, 'PROGRESS', {
|
|
323
351
|
action: progress.action,
|
|
324
|
-
target: progress.target
|
|
352
|
+
target: progress.target,
|
|
325
353
|
});
|
|
326
354
|
}
|
|
327
355
|
break;
|
|
356
|
+
}
|
|
328
357
|
|
|
329
|
-
case '
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
context: rt.context,
|
|
339
|
-
approval_details: {
|
|
340
|
-
action: event.action,
|
|
341
|
-
tool: event.tool,
|
|
342
|
-
file: event.file
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
break;
|
|
346
|
-
|
|
347
|
-
case 'action_complete':
|
|
348
|
-
// Action was approved and completed
|
|
349
|
-
console.log(`[${taskId}] Action complete: ${event.status}`);
|
|
350
|
-
break;
|
|
351
|
-
|
|
352
|
-
case 'result':
|
|
353
|
-
// Task completed
|
|
354
|
-
console.log(`[${taskId}] Task completed`);
|
|
355
|
-
this.onEvent(taskId, 'TASK_COMPLETE', {
|
|
356
|
-
session_id: rt.sessionId,
|
|
357
|
-
result: event.content || ''
|
|
358
|
-
});
|
|
358
|
+
case 'tool_result':
|
|
359
|
+
if (event.output) {
|
|
360
|
+
const truncated = event.output.length > 500
|
|
361
|
+
? event.output.substring(0, 500) + '...'
|
|
362
|
+
: event.output;
|
|
363
|
+
this.onEvent(taskId, 'OUTPUT', {
|
|
364
|
+
output: `[${event.status || 'done'}] ${truncated}`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
359
367
|
break;
|
|
360
368
|
|
|
361
|
-
case '
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
369
|
+
case 'result': {
|
|
370
|
+
if (event.is_error || event.subtype === 'error') {
|
|
371
|
+
console.error(`[${taskId}] Cursor error: ${event.result}`);
|
|
372
|
+
this.onEvent(taskId, 'ERROR', {
|
|
373
|
+
error: event.result || 'Unknown Cursor error',
|
|
374
|
+
});
|
|
375
|
+
} else {
|
|
376
|
+
const finalResult = event.result || rt.context || '';
|
|
377
|
+
console.log(`[${taskId}] Task complete, result length: ${finalResult.length}`);
|
|
378
|
+
this.onEvent(taskId, 'TASK_COMPLETE', {
|
|
379
|
+
session_id: rt.sessionId,
|
|
380
|
+
result: finalResult,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
this.running.delete(taskId);
|
|
367
384
|
break;
|
|
385
|
+
}
|
|
368
386
|
}
|
|
369
387
|
}
|
|
370
388
|
}
|
package/src/adapter/events.ts
CHANGED
|
@@ -1,28 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Types for Cursor CLI stream-json output
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Types for Cursor CLI (cursor-agent) stream-json output.
|
|
3
|
+
*
|
|
4
|
+
* cursor-agent --output-format stream-json emits NDJSON events:
|
|
5
|
+
* system (subtype: init) - Session metadata (session_id, model, cwd)
|
|
6
|
+
* user - Echo of user message
|
|
7
|
+
* thinking (subtype: delta) - Streaming thinking text chunks
|
|
8
|
+
* thinking (subtype: completed) - Thinking finished
|
|
9
|
+
* assistant - Assistant response message
|
|
10
|
+
* tool_call - Tool invocation (file edit, shell, etc.)
|
|
11
|
+
* tool_result - Tool execution result
|
|
12
|
+
* result (subtype: success) - Final result with aggregated response
|
|
13
|
+
* result (subtype: error) - Error result
|
|
5
14
|
*/
|
|
6
15
|
|
|
7
16
|
export interface StreamEvent {
|
|
8
|
-
type: '
|
|
17
|
+
type: 'system' | 'user' | 'thinking' | 'assistant' | 'tool_call' | 'tool_result' | 'result';
|
|
18
|
+
subtype?: string;
|
|
9
19
|
session_id?: string;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
timestamp_ms?: number;
|
|
21
|
+
// system init fields
|
|
22
|
+
model?: string;
|
|
23
|
+
cwd?: string;
|
|
24
|
+
apiKeySource?: string;
|
|
25
|
+
permissionMode?: string;
|
|
26
|
+
// thinking delta fields
|
|
27
|
+
text?: string;
|
|
28
|
+
// assistant fields
|
|
29
|
+
message?: {
|
|
30
|
+
role: string;
|
|
31
|
+
content: Array<{ type: string; text?: string }>;
|
|
32
|
+
};
|
|
33
|
+
// tool_call fields
|
|
34
|
+
tool_name?: string;
|
|
35
|
+
tool_call_id?: string;
|
|
36
|
+
parameters?: Record<string, unknown>;
|
|
37
|
+
// tool_result fields
|
|
16
38
|
status?: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
tool: string;
|
|
23
|
-
file?: string;
|
|
24
|
-
command?: string;
|
|
25
|
-
description?: string;
|
|
39
|
+
output?: string;
|
|
40
|
+
// result fields
|
|
41
|
+
result?: string;
|
|
42
|
+
duration_ms?: number;
|
|
43
|
+
is_error?: boolean;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
export interface ProgressInfo {
|
|
@@ -31,47 +49,43 @@ export interface ProgressInfo {
|
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
/**
|
|
34
|
-
* Extract progress info from
|
|
52
|
+
* Extract progress info from a tool_call event.
|
|
35
53
|
*/
|
|
36
|
-
export function
|
|
37
|
-
if (event.type !== '
|
|
38
|
-
|
|
39
|
-
}
|
|
54
|
+
export function extractProgressFromToolCall(event: StreamEvent): ProgressInfo | null {
|
|
55
|
+
if (event.type !== 'tool_call' || !event.tool_name) return null;
|
|
56
|
+
|
|
57
|
+
const params = event.parameters || {};
|
|
40
58
|
|
|
41
|
-
switch (event.
|
|
59
|
+
switch (event.tool_name) {
|
|
42
60
|
case 'file_read':
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
61
|
+
case 'ReadFile':
|
|
62
|
+
case 'read_file':
|
|
63
|
+
return { action: 'Reading', target: (params.path as string) || 'file' };
|
|
47
64
|
case 'file_write':
|
|
65
|
+
case 'WriteFile':
|
|
66
|
+
case 'write_file':
|
|
67
|
+
return { action: 'Writing', target: (params.path as string) || 'file' };
|
|
48
68
|
case 'file_edit':
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
69
|
+
case 'EditFile':
|
|
70
|
+
case 'edit_file':
|
|
71
|
+
return { action: 'Editing', target: (params.path as string) || 'file' };
|
|
53
72
|
case 'shell':
|
|
54
73
|
case 'terminal':
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
case 'Shell':
|
|
75
|
+
case 'Bash': {
|
|
76
|
+
const cmd = String(params.command || '').substring(0, 40);
|
|
77
|
+
return { action: 'Running', target: cmd };
|
|
78
|
+
}
|
|
60
79
|
case 'search':
|
|
61
80
|
case 'grep':
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
case '
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
target: event.content || ''
|
|
70
|
-
};
|
|
81
|
+
case 'GrepTool':
|
|
82
|
+
case 'SearchFiles':
|
|
83
|
+
return { action: 'Searching', target: (params.pattern as string) || (params.query as string) || 'files' };
|
|
84
|
+
case 'list_directory':
|
|
85
|
+
case 'GlobTool':
|
|
86
|
+
case 'glob':
|
|
87
|
+
return { action: 'Searching', target: (params.pattern as string) || (params.path as string) || 'files' };
|
|
71
88
|
default:
|
|
72
|
-
return {
|
|
73
|
-
action: event.tool,
|
|
74
|
-
target: event.file || event.content || ''
|
|
75
|
-
};
|
|
89
|
+
return { action: event.tool_name, target: '' };
|
|
76
90
|
}
|
|
77
91
|
}
|