@cmdctrl/cursor-cli 0.1.1 → 0.2.1

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 (51) hide show
  1. package/dist/adapter/cursor-cli.d.ts +23 -19
  2. package/dist/adapter/cursor-cli.d.ts.map +1 -1
  3. package/dist/adapter/cursor-cli.js +156 -126
  4. package/dist/adapter/cursor-cli.js.map +1 -1
  5. package/dist/adapter/events.d.ts +36 -20
  6. package/dist/adapter/events.d.ts.map +1 -1
  7. package/dist/adapter/events.js +40 -35
  8. package/dist/adapter/events.js.map +1 -1
  9. package/dist/commands/register.d.ts +0 -3
  10. package/dist/commands/register.d.ts.map +1 -1
  11. package/dist/commands/register.js +23 -122
  12. package/dist/commands/register.js.map +1 -1
  13. package/dist/commands/start.d.ts +1 -8
  14. package/dist/commands/start.d.ts.map +1 -1
  15. package/dist/commands/start.js +117 -30
  16. package/dist/commands/start.js.map +1 -1
  17. package/dist/commands/status.d.ts +1 -4
  18. package/dist/commands/status.d.ts.map +1 -1
  19. package/dist/commands/status.js +25 -22
  20. package/dist/commands/status.js.map +1 -1
  21. package/dist/commands/stop.d.ts +1 -4
  22. package/dist/commands/stop.d.ts.map +1 -1
  23. package/dist/commands/stop.js +21 -26
  24. package/dist/commands/stop.js.map +1 -1
  25. package/dist/commands/unregister.d.ts +2 -0
  26. package/dist/commands/unregister.d.ts.map +1 -0
  27. package/dist/commands/unregister.js +43 -0
  28. package/dist/commands/unregister.js.map +1 -0
  29. package/dist/commands/update.d.ts.map +1 -1
  30. package/dist/commands/update.js +21 -2
  31. package/dist/commands/update.js.map +1 -1
  32. package/dist/index.js +8 -4
  33. package/dist/index.js.map +1 -1
  34. package/dist/message-store.d.ts +18 -0
  35. package/dist/message-store.d.ts.map +1 -0
  36. package/dist/message-store.js +49 -0
  37. package/dist/message-store.js.map +1 -0
  38. package/package.json +2 -2
  39. package/src/adapter/cursor-cli.ts +165 -147
  40. package/src/adapter/events.ts +65 -51
  41. package/src/commands/register.ts +28 -170
  42. package/src/commands/start.ts +132 -41
  43. package/src/commands/status.ts +23 -28
  44. package/src/commands/stop.ts +21 -32
  45. package/src/commands/unregister.ts +43 -0
  46. package/src/commands/update.ts +24 -3
  47. package/src/index.ts +9 -4
  48. package/src/message-store.ts +61 -0
  49. package/src/client/messages.ts +0 -75
  50. package/src/client/websocket.ts +0 -308
  51. 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, extractProgressFromAction } from './events';
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' // Fall back to PATH
32
+ 'cursor-agent'
23
33
  ];
24
34
 
25
35
  for (const p of commonPaths) {
26
- if (p === 'cursor-agent') return p; // PATH fallback
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'; // Fall back to PATH
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}, using home dir`);
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: ${CLI_PATH} with cwd: ${cwd || 'default'}`);
98
+ console.log(`[${taskId}] Spawning: ${findCursorCli()} with cwd: ${cwd || 'default'}`);
106
99
 
107
- // Spawn Cursor CLI
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
- // Pass through Cursor API key if set
114
- CURSOR_API_KEY: process.env.CURSOR_API_KEY
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 task with session ${sessionId}`);
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: ${CLI_PATH} --resume=${sessionId} with cwd: ${cwd || 'default'}`);
149
+ console.log(`[${taskId}] Spawning resume: cursor-agent --resume ${sessionId}`);
171
150
 
172
- // Spawn Cursor CLI
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
- // Set timeout
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
- // Handle process events
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
- console.log(`[${taskId}] Task not found for cancellation`);
202
- return;
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
- rt.process.kill('SIGTERM');
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 (rt.timeoutHandle) {
276
- clearTimeout(rt.timeoutHandle);
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
- if (rt.timeoutHandle) {
286
- clearTimeout(rt.timeoutHandle);
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 Cursor CLI
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 'start':
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 content for context
310
- if (event.content) {
311
- if (rt.context) {
312
- rt.context += '\n\n';
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 'action':
319
- // Track action for progress
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 'approval_request':
330
- // User input needed - pause and wait
331
- console.log(`[${taskId}] Approval requested: ${event.action}`);
332
- rt.question = event.action || 'Approve this action?';
333
-
334
- this.onEvent(taskId, 'WAIT_FOR_USER', {
335
- session_id: rt.sessionId,
336
- prompt: rt.question,
337
- options: [],
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 'error':
362
- // Error occurred
363
- console.error(`[${taskId}] Error: ${event.error}`);
364
- this.onEvent(taskId, 'ERROR', {
365
- error: event.error || 'Unknown error'
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
  }
@@ -1,28 +1,46 @@
1
1
  /**
2
- * Types for Cursor CLI stream-json output
3
- * Note: These are based on research and may need adjustment
4
- * once we verify actual Cursor CLI output format
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: 'start' | 'thinking' | 'action' | 'approval_request' | 'action_complete' | 'result' | 'error';
17
+ type: 'system' | 'user' | 'thinking' | 'assistant' | 'tool_call' | 'tool_result' | 'result';
18
+ subtype?: string;
9
19
  session_id?: string;
10
- timestamp?: string;
11
- content?: string;
12
- tool?: string;
13
- file?: string;
14
- diff?: string;
15
- action?: string;
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
- error?: string;
18
- }
19
-
20
- export interface ApprovalRequest {
21
- action: string;
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 action event
52
+ * Extract progress info from a tool_call event.
35
53
  */
36
- export function extractProgressFromAction(event: StreamEvent): ProgressInfo | null {
37
- if (event.type !== 'action' || !event.tool) {
38
- return null;
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.tool) {
59
+ switch (event.tool_name) {
42
60
  case 'file_read':
43
- return {
44
- action: 'Reading',
45
- target: event.file || 'file'
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
- return {
50
- action: 'Editing',
51
- target: event.file || 'file'
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
- const cmd = event.content || '';
56
- return {
57
- action: 'Running',
58
- target: cmd.length > 30 ? cmd.substring(0, 30) + '...' : cmd
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
- return {
63
- action: 'Searching',
64
- target: event.content || 'files'
65
- };
66
- case 'web_search':
67
- return {
68
- action: 'Searching web',
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
  }