@cmdctrl/claude-code 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 (83) hide show
  1. package/dist/adapter/claude-cli.d.ts +41 -0
  2. package/dist/adapter/claude-cli.d.ts.map +1 -0
  3. package/dist/adapter/claude-cli.js +525 -0
  4. package/dist/adapter/claude-cli.js.map +1 -0
  5. package/dist/adapter/events.d.ts +52 -0
  6. package/dist/adapter/events.d.ts.map +1 -0
  7. package/dist/adapter/events.js +134 -0
  8. package/dist/adapter/events.js.map +1 -0
  9. package/dist/client/messages.d.ts +140 -0
  10. package/dist/client/messages.d.ts.map +1 -0
  11. package/dist/client/messages.js +6 -0
  12. package/dist/client/messages.js.map +1 -0
  13. package/dist/client/websocket.d.ts +115 -0
  14. package/dist/client/websocket.d.ts.map +1 -0
  15. package/dist/client/websocket.js +434 -0
  16. package/dist/client/websocket.js.map +1 -0
  17. package/dist/commands/register.d.ts +10 -0
  18. package/dist/commands/register.d.ts.map +1 -0
  19. package/dist/commands/register.js +175 -0
  20. package/dist/commands/register.js.map +1 -0
  21. package/dist/commands/start.d.ts +9 -0
  22. package/dist/commands/start.d.ts.map +1 -0
  23. package/dist/commands/start.js +54 -0
  24. package/dist/commands/start.js.map +1 -0
  25. package/dist/commands/status.d.ts +5 -0
  26. package/dist/commands/status.d.ts.map +1 -0
  27. package/dist/commands/status.js +38 -0
  28. package/dist/commands/status.js.map +1 -0
  29. package/dist/commands/stop.d.ts +5 -0
  30. package/dist/commands/stop.d.ts.map +1 -0
  31. package/dist/commands/stop.js +59 -0
  32. package/dist/commands/stop.js.map +1 -0
  33. package/dist/commands/unregister.d.ts +5 -0
  34. package/dist/commands/unregister.d.ts.map +1 -0
  35. package/dist/commands/unregister.js +28 -0
  36. package/dist/commands/unregister.js.map +1 -0
  37. package/dist/config/config.d.ts +68 -0
  38. package/dist/config/config.d.ts.map +1 -0
  39. package/dist/config/config.js +193 -0
  40. package/dist/config/config.js.map +1 -0
  41. package/dist/handlers/context-handler.d.ts +37 -0
  42. package/dist/handlers/context-handler.d.ts.map +1 -0
  43. package/dist/handlers/context-handler.js +303 -0
  44. package/dist/handlers/context-handler.js.map +1 -0
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +39 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/message-reader.d.ts +25 -0
  50. package/dist/message-reader.d.ts.map +1 -0
  51. package/dist/message-reader.js +454 -0
  52. package/dist/message-reader.js.map +1 -0
  53. package/dist/session-discovery.d.ts +48 -0
  54. package/dist/session-discovery.d.ts.map +1 -0
  55. package/dist/session-discovery.js +496 -0
  56. package/dist/session-discovery.js.map +1 -0
  57. package/dist/session-watcher.d.ts +92 -0
  58. package/dist/session-watcher.d.ts.map +1 -0
  59. package/dist/session-watcher.js +494 -0
  60. package/dist/session-watcher.js.map +1 -0
  61. package/dist/session-watcher.test.d.ts +9 -0
  62. package/dist/session-watcher.test.d.ts.map +1 -0
  63. package/dist/session-watcher.test.js +149 -0
  64. package/dist/session-watcher.test.js.map +1 -0
  65. package/jest.config.js +8 -0
  66. package/package.json +42 -0
  67. package/src/adapter/claude-cli.ts +591 -0
  68. package/src/adapter/events.ts +186 -0
  69. package/src/client/messages.ts +193 -0
  70. package/src/client/websocket.ts +509 -0
  71. package/src/commands/register.ts +201 -0
  72. package/src/commands/start.ts +70 -0
  73. package/src/commands/status.ts +47 -0
  74. package/src/commands/stop.ts +58 -0
  75. package/src/commands/unregister.ts +30 -0
  76. package/src/config/config.ts +163 -0
  77. package/src/handlers/context-handler.ts +337 -0
  78. package/src/index.ts +45 -0
  79. package/src/message-reader.ts +485 -0
  80. package/src/session-discovery.ts +557 -0
  81. package/src/session-watcher.test.ts +141 -0
  82. package/src/session-watcher.ts +560 -0
  83. package/tsconfig.json +19 -0
@@ -0,0 +1,591 @@
1
+ import { spawn, ChildProcess } from 'child_process';
2
+ import * as readline from 'readline';
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import * as path from 'path';
6
+ import {
7
+ StreamEvent,
8
+ AskUserInput,
9
+ QuestionOption,
10
+ extractProgressFromToolUse
11
+ } from './events';
12
+ import { findSessionFile } from '../message-reader';
13
+
14
+ const DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
15
+
16
+ // Find claude CLI in common locations
17
+ function findClaudeCli(): string {
18
+ if (process.env.CLAUDE_CODE_CLI_PATH) {
19
+ return process.env.CLAUDE_CODE_CLI_PATH;
20
+ }
21
+
22
+ const home = os.homedir();
23
+ const commonPaths = [
24
+ path.join(home, '.npm-global', 'bin', 'claude'),
25
+ path.join(home, '.nvm', 'versions', 'node', 'v20.18.0', 'bin', 'claude'),
26
+ '/usr/local/bin/claude',
27
+ '/opt/homebrew/bin/claude',
28
+ 'claude' // Fall back to PATH
29
+ ];
30
+
31
+ for (const p of commonPaths) {
32
+ if (p === 'claude') return p; // PATH fallback
33
+ try {
34
+ if (fs.existsSync(p)) {
35
+ return p;
36
+ }
37
+ } catch {
38
+ continue;
39
+ }
40
+ }
41
+
42
+ return 'claude'; // Fall back to PATH
43
+ }
44
+
45
+ const CLI_PATH = findClaudeCli();
46
+ console.log(`[ClaudeAdapter] Using CLI path: ${CLI_PATH}`);
47
+
48
+ /**
49
+ * Read the last user message UUID from a session JSONL file
50
+ * Used for associating verbose output with the triggering user message
51
+ */
52
+ function getLastUserMessageUuid(sessionId: string): string | undefined {
53
+ const filePath = findSessionFile(sessionId);
54
+ if (!filePath) {
55
+ console.log(`[getLastUserMessageUuid] File not found for session ${sessionId}`);
56
+ return undefined;
57
+ }
58
+
59
+ try {
60
+ const content = fs.readFileSync(filePath, 'utf-8');
61
+ const lines = content.split('\n').filter(l => l.trim());
62
+
63
+ // Count user messages for debugging
64
+ let userMessageCount = 0;
65
+ let lastUserUuid: string | undefined;
66
+ let lastUserContent: string | undefined;
67
+
68
+ // Find the last user message UUID (iterate backwards)
69
+ for (let i = lines.length - 1; i >= 0; i--) {
70
+ try {
71
+ const entry = JSON.parse(lines[i]);
72
+ if (entry.type === 'user' && entry.uuid) {
73
+ userMessageCount++;
74
+ if (!lastUserUuid) {
75
+ lastUserUuid = entry.uuid;
76
+ // Get first 50 chars of message content for debugging
77
+ if (entry.message?.content) {
78
+ const msgContent = typeof entry.message.content === 'string'
79
+ ? entry.message.content
80
+ : JSON.stringify(entry.message.content);
81
+ lastUserContent = msgContent.substring(0, 50);
82
+ }
83
+ }
84
+ }
85
+ } catch {
86
+ continue;
87
+ }
88
+ }
89
+
90
+ console.log(`[getLastUserMessageUuid] Found ${userMessageCount} user messages, last UUID: ${lastUserUuid}, content: "${lastUserContent}"`);
91
+ return lastUserUuid;
92
+ } catch (err) {
93
+ console.log(`[getLastUserMessageUuid] File read error:`, err);
94
+ }
95
+
96
+ return undefined;
97
+ }
98
+
99
+ // Allowed tools for Claude CLI
100
+ const ALLOWED_TOOLS = [
101
+ 'Read',
102
+ 'Glob',
103
+ 'Grep',
104
+ 'WebSearch',
105
+ 'WebFetch',
106
+ 'LSP',
107
+ 'Task',
108
+ 'TodoWrite',
109
+ 'Bash',
110
+ 'Edit',
111
+ 'Write',
112
+ 'NotebookEdit',
113
+ 'AskUserQuestion' // Required for pause/resume workflow
114
+ ].join(',');
115
+
116
+ interface RunningTask {
117
+ taskId: string;
118
+ sessionId: string;
119
+ question: string;
120
+ options: QuestionOption[];
121
+ context: string;
122
+ process: ChildProcess | null;
123
+ timeoutHandle: NodeJS.Timeout | null;
124
+ userMessageUuid?: string; // UUID of the triggering user message (for verbose output positioning)
125
+ }
126
+
127
+ type EventCallback = (
128
+ taskId: string,
129
+ eventType: string,
130
+ data: Record<string, unknown>
131
+ ) => void;
132
+
133
+ export class ClaudeAdapter {
134
+ private running: Map<string, RunningTask> = new Map();
135
+ private onEvent: EventCallback;
136
+
137
+ constructor(onEvent: EventCallback) {
138
+ this.onEvent = onEvent;
139
+ }
140
+
141
+ /**
142
+ * Start a new task
143
+ */
144
+ async startTask(
145
+ taskId: string,
146
+ instruction: string,
147
+ projectPath?: string
148
+ ): Promise<void> {
149
+ console.log(`[${taskId}] Starting task: ${instruction.substring(0, 50)}...`);
150
+
151
+ const rt: RunningTask = {
152
+ taskId,
153
+ sessionId: '',
154
+ question: '',
155
+ options: [],
156
+ context: '',
157
+ process: null,
158
+ timeoutHandle: null
159
+ };
160
+
161
+ this.running.set(taskId, rt);
162
+
163
+ // Validate cwd exists
164
+ let cwd: string | undefined = undefined;
165
+ if (projectPath && fs.existsSync(projectPath)) {
166
+ cwd = projectPath;
167
+ } else if (projectPath) {
168
+ console.log(`[${taskId}] Warning: project path does not exist: ${projectPath}, using home dir`);
169
+ cwd = os.homedir();
170
+ // Notify user that the path doesn't exist
171
+ this.onEvent(taskId, 'WARNING', {
172
+ warning: `Project path "${projectPath}" does not exist. Running in home directory instead.`
173
+ });
174
+ }
175
+
176
+ // Build command arguments
177
+ const args = [
178
+ '-p', instruction,
179
+ '--output-format', 'stream-json',
180
+ '--verbose',
181
+ '--permission-mode', 'acceptEdits',
182
+ '--allowedTools', ALLOWED_TOOLS
183
+ ];
184
+
185
+ console.log(`[${taskId}] Spawning: ${CLI_PATH} with cwd: ${cwd || 'default'}`);
186
+
187
+ // Spawn Claude CLI (no shell - direct execution preserves arguments correctly)
188
+ const proc = spawn(CLI_PATH, args, {
189
+ cwd,
190
+ stdio: ['ignore', 'pipe', 'pipe']
191
+ });
192
+
193
+ rt.process = proc;
194
+
195
+ // Set timeout
196
+ rt.timeoutHandle = setTimeout(() => {
197
+ console.log(`[${taskId}] Task timed out`);
198
+ proc.kill('SIGKILL');
199
+ this.onEvent(taskId, 'ERROR', { error: 'execution timeout' });
200
+ }, DEFAULT_TIMEOUT);
201
+
202
+ // Handle process events
203
+ this.handleProcessOutput(taskId, proc, rt);
204
+ }
205
+
206
+ /**
207
+ * Resume a task with user's reply
208
+ * Falls back to startTask if session doesn't exist
209
+ */
210
+ async resumeTask(
211
+ taskId: string,
212
+ sessionId: string,
213
+ message: string,
214
+ projectPath?: string
215
+ ): Promise<void> {
216
+ console.log(`[${taskId}] ===== RESUME TASK START =====`);
217
+ console.log(`[${taskId}] Session: ${sessionId.slice(-8)}, Message: "${message.slice(0, 50)}..."`);
218
+
219
+ const rt: RunningTask = {
220
+ taskId,
221
+ sessionId,
222
+ question: '',
223
+ options: [],
224
+ context: '',
225
+ process: null,
226
+ timeoutHandle: null,
227
+ userMessageUuid: undefined
228
+ };
229
+
230
+ // Note: userMessageUuid starts as undefined - will be read from JSONL on first assistant event
231
+ console.log(`[${taskId}] Initial state: sessionId=${sessionId.slice(-8)}, userMessageUuid=none`);
232
+
233
+ this.running.set(taskId, rt);
234
+
235
+ // Validate cwd exists (same logic as startTask)
236
+ let cwd: string | undefined = undefined;
237
+ if (projectPath && fs.existsSync(projectPath)) {
238
+ cwd = projectPath;
239
+ } else if (projectPath) {
240
+ console.log(`[${taskId}] Warning: project path does not exist: ${projectPath}, using home dir`);
241
+ cwd = os.homedir();
242
+ }
243
+
244
+ // Build command arguments with --resume
245
+ const args = [
246
+ '-p', message,
247
+ '--resume', sessionId,
248
+ '--output-format', 'stream-json',
249
+ '--verbose',
250
+ '--permission-mode', 'acceptEdits',
251
+ '--allowedTools', ALLOWED_TOOLS
252
+ ];
253
+
254
+ console.log(`[${taskId}] Spawning resume: ${CLI_PATH} --resume ${sessionId} with cwd: ${cwd || 'default'}`);
255
+
256
+ // Spawn Claude CLI with same cwd as original task (no shell - direct execution)
257
+ const proc = spawn(CLI_PATH, args, {
258
+ cwd,
259
+ stdio: ['ignore', 'pipe', 'pipe']
260
+ });
261
+
262
+ rt.process = proc;
263
+
264
+ // Track if we've seen the "no conversation found" error
265
+ let sessionNotFound = false;
266
+ let stderrBuffer = '';
267
+
268
+ // Check stderr for session not found error
269
+ proc.stderr?.on('data', (data) => {
270
+ const text = data.toString();
271
+ stderrBuffer += text;
272
+ console.log(`[${taskId}] stderr: ${text}`);
273
+ if (text.includes('No conversation found')) {
274
+ sessionNotFound = true;
275
+ }
276
+ });
277
+
278
+ // Handle quick exit with session not found - fall back to new session
279
+ proc.on('close', (code) => {
280
+ if (code !== 0 && sessionNotFound) {
281
+ console.log(`[${taskId}] Session ${sessionId} not found, falling back to new session`);
282
+ // Clean up this attempt
283
+ if (rt.timeoutHandle) {
284
+ clearTimeout(rt.timeoutHandle);
285
+ }
286
+ this.running.delete(taskId);
287
+ // Start fresh instead
288
+ this.startTask(taskId, message, projectPath);
289
+ return;
290
+ }
291
+ // Normal exit handling
292
+ console.log(`[${taskId}] Process exited with code ${code}`);
293
+ if (rt.timeoutHandle) {
294
+ clearTimeout(rt.timeoutHandle);
295
+ }
296
+ this.running.delete(taskId);
297
+ });
298
+
299
+ // Set timeout
300
+ rt.timeoutHandle = setTimeout(() => {
301
+ console.log(`[${taskId}] Task timed out`);
302
+ proc.kill('SIGKILL');
303
+ this.onEvent(taskId, 'ERROR', { error: 'execution timeout' });
304
+ }, DEFAULT_TIMEOUT);
305
+
306
+ // Handle process events (but skip the close handler since we handle it above)
307
+ this.handleProcessOutputWithoutClose(taskId, proc, rt);
308
+ }
309
+
310
+ /**
311
+ * Cancel a running task
312
+ */
313
+ async cancelTask(taskId: string): Promise<void> {
314
+ const rt = this.running.get(taskId);
315
+ if (!rt) {
316
+ console.log(`[${taskId}] Task not found for cancellation`);
317
+ return;
318
+ }
319
+
320
+ if (rt.process) {
321
+ rt.process.kill('SIGTERM');
322
+ }
323
+ if (rt.timeoutHandle) {
324
+ clearTimeout(rt.timeoutHandle);
325
+ }
326
+
327
+ this.running.delete(taskId);
328
+ console.log(`[${taskId}] Task cancelled`);
329
+ }
330
+
331
+ /**
332
+ * Stop all running tasks
333
+ */
334
+ async stopAll(): Promise<void> {
335
+ for (const [taskId, rt] of this.running) {
336
+ console.log(`[${taskId}] Stopping task`);
337
+ if (rt.process) {
338
+ rt.process.kill('SIGTERM');
339
+ }
340
+ if (rt.timeoutHandle) {
341
+ clearTimeout(rt.timeoutHandle);
342
+ }
343
+ }
344
+ this.running.clear();
345
+ }
346
+
347
+ /**
348
+ * Get list of running task IDs
349
+ */
350
+ getRunningTasks(): string[] {
351
+ return Array.from(this.running.keys());
352
+ }
353
+
354
+ /**
355
+ * Handle process stdout/stderr and emit events
356
+ */
357
+ private handleProcessOutput(
358
+ taskId: string,
359
+ proc: ChildProcess,
360
+ rt: RunningTask
361
+ ): void {
362
+ // Create readline interface for NDJSON parsing
363
+ const rl = readline.createInterface({
364
+ input: proc.stdout!,
365
+ crlfDelay: Infinity
366
+ });
367
+
368
+ // Parse each line as JSON to track state (userMessageUuid)
369
+ // NOTE: Verbose output is now handled by SessionWatcher via JSONL-based VERBOSE events
370
+ // We still parse stream events here to track userMessageUuid for TASK_COMPLETE
371
+ rl.on('line', (line) => {
372
+ try {
373
+ const event = JSON.parse(line) as StreamEvent;
374
+ this.handleStreamEvent(taskId, event, rt);
375
+ } catch {
376
+ // Not valid JSON, skip
377
+ }
378
+ });
379
+
380
+ // Log stderr
381
+ proc.stderr?.on('data', (data) => {
382
+ console.log(`[${taskId}] stderr: ${data.toString()}`);
383
+ });
384
+
385
+ // Handle process exit
386
+ proc.on('close', (code) => {
387
+ console.log(`[${taskId}] Process exited with code ${code}`);
388
+
389
+ if (rt.timeoutHandle) {
390
+ clearTimeout(rt.timeoutHandle);
391
+ }
392
+ this.running.delete(taskId);
393
+ });
394
+
395
+ proc.on('error', (err) => {
396
+ console.error(`[${taskId}] Process error:`, err);
397
+ this.onEvent(taskId, 'ERROR', { error: err.message });
398
+
399
+ if (rt.timeoutHandle) {
400
+ clearTimeout(rt.timeoutHandle);
401
+ }
402
+ this.running.delete(taskId);
403
+ });
404
+ }
405
+
406
+ /**
407
+ * Handle process stdout and emit events (without close handler - for resumeTask fallback)
408
+ */
409
+ private handleProcessOutputWithoutClose(
410
+ taskId: string,
411
+ proc: ChildProcess,
412
+ rt: RunningTask
413
+ ): void {
414
+ // Create readline interface for NDJSON parsing
415
+ const rl = readline.createInterface({
416
+ input: proc.stdout!,
417
+ crlfDelay: Infinity
418
+ });
419
+
420
+ // Parse each line as JSON to track state (userMessageUuid)
421
+ // NOTE: Verbose output is now handled by SessionWatcher via JSONL-based VERBOSE events
422
+ // We still parse stream events here to track userMessageUuid for TASK_COMPLETE
423
+ rl.on('line', (line) => {
424
+ try {
425
+ const event = JSON.parse(line) as StreamEvent;
426
+ this.handleStreamEvent(taskId, event, rt);
427
+ } catch {
428
+ // Not valid JSON, skip
429
+ }
430
+ });
431
+
432
+ // Note: stderr is handled by resumeTask caller
433
+ // Note: close is handled by resumeTask caller
434
+
435
+ proc.on('error', (err) => {
436
+ console.error(`[${taskId}] Process error:`, err);
437
+ this.onEvent(taskId, 'ERROR', { error: err.message });
438
+
439
+ if (rt.timeoutHandle) {
440
+ clearTimeout(rt.timeoutHandle);
441
+ }
442
+ this.running.delete(taskId);
443
+ });
444
+ }
445
+
446
+ /**
447
+ * Handle a parsed stream event from Claude CLI
448
+ */
449
+ private handleStreamEvent(
450
+ taskId: string,
451
+ event: StreamEvent,
452
+ rt: RunningTask
453
+ ): void {
454
+ // Debug logging for all events
455
+ console.log(`[${taskId}] Event: type=${event.type}, subtype=${event.subtype || 'none'}`);
456
+ if (event.permission_denials?.length) {
457
+ console.log(`[${taskId}] Permission denials:`, JSON.stringify(event.permission_denials));
458
+ }
459
+
460
+ switch (event.type) {
461
+ case 'system':
462
+ if (event.subtype === 'init' && event.session_id) {
463
+ rt.sessionId = event.session_id;
464
+ console.log(`[${taskId}] Session initialized: ${event.session_id}`);
465
+ // DON'T read UUID here - Claude CLI hasn't written the new user message yet
466
+ // The frontend has the correct UUID from when the user sent the message
467
+ // By not setting UUID here, frontend will use its verboseOutputUserUuid fallback
468
+ // Emit SESSION_STARTED to trigger file watching for unified notifications
469
+ this.onEvent(taskId, 'SESSION_STARTED', {
470
+ session_id: event.session_id
471
+ });
472
+ }
473
+ break;
474
+
475
+ case 'assistant':
476
+ // DON'T read UUID here - Claude may not have written the new user message to JSONL yet
477
+ // Reading now would get the PREVIOUS message's UUID, causing wrong positioning
478
+ // The frontend has the correct UUID from when the user sent the message
479
+ // We only read UUID at TASK_COMPLETE where we need it for the final message
480
+ console.log(`[${taskId}] assistant event: currentUuid=${rt.userMessageUuid?.slice(-8) || 'none'}, sessionId=${rt.sessionId?.slice(-8) || 'none'}`);
481
+ if (event.message?.content) {
482
+ for (const block of event.message.content) {
483
+ // Skip thinking blocks if they come as separate type
484
+ if (block.type === 'thinking') {
485
+ continue;
486
+ }
487
+
488
+ // Accumulate text for context
489
+ if (block.type === 'text' && block.text) {
490
+ // Strip <thinking>...</thinking> tags (may be embedded in text)
491
+ const text = block.text.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim();
492
+ if (text) {
493
+ if (rt.context) {
494
+ rt.context += '\n\n';
495
+ }
496
+ rt.context += text;
497
+ }
498
+ }
499
+
500
+ // Track tool use for progress
501
+ if (block.type === 'tool_use' && block.name) {
502
+ const progress = extractProgressFromToolUse(
503
+ block.name,
504
+ block.input
505
+ );
506
+ if (progress) {
507
+ this.onEvent(taskId, 'PROGRESS', {
508
+ action: progress.action,
509
+ target: progress.target
510
+ });
511
+ }
512
+
513
+ // Check for AskUserQuestion
514
+ if (block.name === 'AskUserQuestion' && block.input) {
515
+ const input = block.input as AskUserInput;
516
+ if (input.questions?.length > 0) {
517
+ const q = input.questions[0];
518
+ rt.question = q.question;
519
+ rt.options = q.options || [];
520
+ console.log(`[${taskId}] Question detected: ${q.question}`);
521
+ }
522
+ }
523
+ }
524
+ }
525
+ }
526
+ break;
527
+
528
+ case 'result':
529
+ // Check for permission denials (user input needed)
530
+ if (event.permission_denials?.length) {
531
+ // Any permission denial means the task is waiting for user input
532
+ const denials = event.permission_denials;
533
+ const firstDenial = denials[0];
534
+
535
+ // Build a descriptive prompt based on the denied tool
536
+ let prompt = rt.question; // Use AskUserQuestion prompt if available
537
+ let options = rt.options;
538
+
539
+ if (!prompt) {
540
+ // Construct prompt from permission denial info
541
+ const toolName = firstDenial.tool_name;
542
+ if (toolName === 'AskUserQuestion') {
543
+ prompt = 'Agent is asking a question';
544
+ } else {
545
+ // Permission request for file/bash operations
546
+ prompt = `Permission required for: ${toolName}`;
547
+ if (denials.length > 1) {
548
+ prompt += ` (and ${denials.length - 1} more)`;
549
+ }
550
+ }
551
+ }
552
+
553
+ console.log(`[${taskId}] Task waiting for user input - tool: ${firstDenial.tool_name}, prompt: ${prompt}`);
554
+
555
+ this.onEvent(taskId, 'WAIT_FOR_USER', {
556
+ session_id: rt.sessionId,
557
+ prompt: prompt,
558
+ options: options,
559
+ context: rt.context,
560
+ user_message_uuid: rt.userMessageUuid,
561
+ permission_tool: firstDenial.tool_name
562
+ });
563
+ return;
564
+ }
565
+
566
+ // Task completed - re-read the UUID to ensure we have the correct one
567
+ // (the first assistant event may arrive before the JSONL is fully written)
568
+ console.log(`[${taskId}] TASK_COMPLETE: checking UUID. Current=${rt.userMessageUuid?.slice(-8) || 'none'}`);
569
+ if (rt.sessionId) {
570
+ console.log(`[${taskId}] Re-reading UUID from JSONL at completion...`);
571
+ const freshUuid = getLastUserMessageUuid(rt.sessionId);
572
+ console.log(`[${taskId}] Fresh UUID from JSONL: ${freshUuid?.slice(-8) || 'none'}`);
573
+ if (freshUuid && freshUuid !== rt.userMessageUuid) {
574
+ console.log(`[${taskId}] UUID UPDATED at completion: ${rt.userMessageUuid?.slice(-8) || 'none'} -> ${freshUuid.slice(-8)}`);
575
+ rt.userMessageUuid = freshUuid;
576
+ } else if (freshUuid === rt.userMessageUuid) {
577
+ console.log(`[${taskId}] UUID unchanged at completion: ${freshUuid?.slice(-8) || 'none'}`);
578
+ }
579
+ }
580
+ console.log(`[${taskId}] EMITTING TASK_COMPLETE with uuid=${rt.userMessageUuid?.slice(-8) || 'none'}`);
581
+ // Use accumulated context as result, fall back to event.result
582
+ const finalResult = rt.context || event.result || '';
583
+ this.onEvent(taskId, 'TASK_COMPLETE', {
584
+ session_id: rt.sessionId,
585
+ result: finalResult,
586
+ user_message_uuid: rt.userMessageUuid
587
+ });
588
+ break;
589
+ }
590
+ }
591
+ }