@yu_robotics/remote-cli 1.0.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/bin/remote-cli.js +2 -0
  4. package/dist/client/MessageHandler.d.ts +92 -0
  5. package/dist/client/MessageHandler.d.ts.map +1 -0
  6. package/dist/client/MessageHandler.js +496 -0
  7. package/dist/client/MessageHandler.js.map +1 -0
  8. package/dist/client/WebSocketClient.d.ts +109 -0
  9. package/dist/client/WebSocketClient.d.ts.map +1 -0
  10. package/dist/client/WebSocketClient.js +234 -0
  11. package/dist/client/WebSocketClient.js.map +1 -0
  12. package/dist/commands/config.d.ts +35 -0
  13. package/dist/commands/config.d.ts.map +1 -0
  14. package/dist/commands/config.js +195 -0
  15. package/dist/commands/config.js.map +1 -0
  16. package/dist/commands/init.d.ts +25 -0
  17. package/dist/commands/init.d.ts.map +1 -0
  18. package/dist/commands/init.js +112 -0
  19. package/dist/commands/init.js.map +1 -0
  20. package/dist/commands/start.d.ts +20 -0
  21. package/dist/commands/start.d.ts.map +1 -0
  22. package/dist/commands/start.js +108 -0
  23. package/dist/commands/start.js.map +1 -0
  24. package/dist/commands/status.d.ts +37 -0
  25. package/dist/commands/status.d.ts.map +1 -0
  26. package/dist/commands/status.js +71 -0
  27. package/dist/commands/status.js.map +1 -0
  28. package/dist/commands/stop.d.ts +23 -0
  29. package/dist/commands/stop.d.ts.map +1 -0
  30. package/dist/commands/stop.js +52 -0
  31. package/dist/commands/stop.js.map +1 -0
  32. package/dist/config/ConfigManager.d.ts +109 -0
  33. package/dist/config/ConfigManager.d.ts.map +1 -0
  34. package/dist/config/ConfigManager.js +262 -0
  35. package/dist/config/ConfigManager.js.map +1 -0
  36. package/dist/executor/ClaudeExecutor.d.ts +89 -0
  37. package/dist/executor/ClaudeExecutor.d.ts.map +1 -0
  38. package/dist/executor/ClaudeExecutor.js +365 -0
  39. package/dist/executor/ClaudeExecutor.js.map +1 -0
  40. package/dist/executor/ClaudePersistentExecutor.d.ts +175 -0
  41. package/dist/executor/ClaudePersistentExecutor.d.ts.map +1 -0
  42. package/dist/executor/ClaudePersistentExecutor.js +958 -0
  43. package/dist/executor/ClaudePersistentExecutor.js.map +1 -0
  44. package/dist/executor/index.d.ts +20 -0
  45. package/dist/executor/index.d.ts.map +1 -0
  46. package/dist/executor/index.js +48 -0
  47. package/dist/executor/index.js.map +1 -0
  48. package/dist/hooks/ClaudeCodeHooks.d.ts +281 -0
  49. package/dist/hooks/ClaudeCodeHooks.d.ts.map +1 -0
  50. package/dist/hooks/ClaudeCodeHooks.js +350 -0
  51. package/dist/hooks/ClaudeCodeHooks.js.map +1 -0
  52. package/dist/hooks/FeishuNotificationAdapter.d.ts +87 -0
  53. package/dist/hooks/FeishuNotificationAdapter.d.ts.map +1 -0
  54. package/dist/hooks/FeishuNotificationAdapter.js +280 -0
  55. package/dist/hooks/FeishuNotificationAdapter.js.map +1 -0
  56. package/dist/hooks/index.d.ts +4 -0
  57. package/dist/hooks/index.d.ts.map +1 -0
  58. package/dist/hooks/index.js +10 -0
  59. package/dist/hooks/index.js.map +1 -0
  60. package/dist/index.d.ts +3 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +333 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/security/DirectoryGuard.d.ts +54 -0
  65. package/dist/security/DirectoryGuard.d.ts.map +1 -0
  66. package/dist/security/DirectoryGuard.js +143 -0
  67. package/dist/security/DirectoryGuard.js.map +1 -0
  68. package/dist/types/config.d.ts +46 -0
  69. package/dist/types/config.d.ts.map +1 -0
  70. package/dist/types/config.js +22 -0
  71. package/dist/types/config.js.map +1 -0
  72. package/dist/types/index.d.ts +110 -0
  73. package/dist/types/index.d.ts.map +1 -0
  74. package/dist/types/index.js +3 -0
  75. package/dist/types/index.js.map +1 -0
  76. package/dist/utils/FeishuMessageFormatter.d.ts +84 -0
  77. package/dist/utils/FeishuMessageFormatter.d.ts.map +1 -0
  78. package/dist/utils/FeishuMessageFormatter.js +395 -0
  79. package/dist/utils/FeishuMessageFormatter.js.map +1 -0
  80. package/dist/utils/stripAnsi.d.ts +21 -0
  81. package/dist/utils/stripAnsi.d.ts.map +1 -0
  82. package/dist/utils/stripAnsi.js +30 -0
  83. package/dist/utils/stripAnsi.js.map +1 -0
  84. package/package.json +63 -0
@@ -0,0 +1,958 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ClaudePersistentExecutor = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const ClaudeCodeHooks_1 = require("../hooks/ClaudeCodeHooks");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const events_1 = require("events");
13
+ /**
14
+ * Get Claude's global sessions directory
15
+ */
16
+ function getClaudeSessionsDir() {
17
+ const homeDir = os_1.default.homedir();
18
+ const possiblePaths = [
19
+ path_1.default.join(homeDir, '.claude', 'sessions'),
20
+ path_1.default.join(homeDir, '.config', 'claude', 'sessions'),
21
+ path_1.default.join(homeDir, 'Library', 'Application Support', 'Claude', 'sessions'),
22
+ ];
23
+ for (const dir of possiblePaths) {
24
+ if (fs_1.default.existsSync(dir)) {
25
+ return dir;
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ /**
31
+ * Claude Persistent Executor
32
+ *
33
+ * Maintains a long-running Claude process and communicates via stdin/stdout
34
+ * using stream-json format for real-time bidirectional communication.
35
+ */
36
+ class ClaudePersistentExecutor extends events_1.EventEmitter {
37
+ directoryGuard;
38
+ currentWorkingDirectory;
39
+ isDestroyed = false;
40
+ defaultTimeout = 600000; // 10 minutes default, but will extend on activity
41
+ sessionId = null;
42
+ sessionFilePath;
43
+ // Persistent process
44
+ claudeProcess = null;
45
+ isStarting = false;
46
+ isStopping = false; // Flag to indicate intentional process stop
47
+ commandQueue = [];
48
+ isProcessing = false;
49
+ // Output handling
50
+ currentOutputBuffer = [];
51
+ currentStreamCallback;
52
+ currentToolUseCallback;
53
+ currentToolResultCallback;
54
+ currentCommandResolve;
55
+ currentCommandReject;
56
+ currentTimeoutTimer;
57
+ // Structured content collection for rich formatting
58
+ structuredContentBlocks = [];
59
+ currentStructuredCallback;
60
+ // Current task context for hooks
61
+ currentTaskId = null;
62
+ currentTaskStartTime = 0;
63
+ // Interactive input handling
64
+ isWaitingForInput = false;
65
+ inputRequestCallbacks = [];
66
+ inputDetectionTimer;
67
+ lastOutputTime = 0;
68
+ // Activity tracking for timeout extension (optional, can be disabled)
69
+ activityTrackingEnabled = false;
70
+ constructor(directoryGuard) {
71
+ super();
72
+ this.directoryGuard = directoryGuard;
73
+ this.currentWorkingDirectory = process.cwd();
74
+ this.sessionFilePath = path_1.default.join(this.currentWorkingDirectory, '.claude-session');
75
+ this.loadSessionId();
76
+ }
77
+ /**
78
+ * Load session ID from file if exists
79
+ */
80
+ loadSessionId() {
81
+ try {
82
+ if (fs_1.default.existsSync(this.sessionFilePath)) {
83
+ const data = fs_1.default.readFileSync(this.sessionFilePath, 'utf-8');
84
+ const session = JSON.parse(data);
85
+ if (session.id) {
86
+ this.sessionId = session.id;
87
+ console.log(`[ClaudePersistent] Loaded session ID: ${this.sessionId}`);
88
+ }
89
+ }
90
+ }
91
+ catch (error) {
92
+ console.error('[ClaudePersistent] Failed to load session ID:', error);
93
+ this.sessionId = null;
94
+ }
95
+ }
96
+ /**
97
+ * Save session ID to file
98
+ */
99
+ saveSessionId(sessionId) {
100
+ try {
101
+ const data = JSON.stringify({
102
+ id: sessionId,
103
+ savedAt: new Date().toISOString(),
104
+ });
105
+ fs_1.default.writeFileSync(this.sessionFilePath, data, 'utf-8');
106
+ console.log(`[ClaudePersistent] Saved session ID: ${sessionId}`);
107
+ }
108
+ catch (error) {
109
+ console.error('[ClaudePersistent] Failed to save session ID:', error);
110
+ }
111
+ }
112
+ /**
113
+ * Get recent session ID from Claude's sessions directory
114
+ */
115
+ async getRecentSessionId() {
116
+ try {
117
+ const sessionsDir = getClaudeSessionsDir();
118
+ if (!sessionsDir) {
119
+ return null;
120
+ }
121
+ const entries = fs_1.default.readdirSync(sessionsDir, { withFileTypes: true });
122
+ const sessionDirs = entries
123
+ .filter(entry => entry.isDirectory())
124
+ .map(entry => {
125
+ const sessionPath = path_1.default.join(sessionsDir, entry.name);
126
+ const stats = fs_1.default.statSync(sessionPath);
127
+ return {
128
+ id: entry.name,
129
+ mtime: stats.mtime,
130
+ };
131
+ })
132
+ .filter(session => {
133
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
134
+ return uuidPattern.test(session.id);
135
+ })
136
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
137
+ if (sessionDirs.length === 0) {
138
+ return null;
139
+ }
140
+ return sessionDirs[0].id;
141
+ }
142
+ catch (error) {
143
+ console.error('[ClaudePersistent] Failed to get recent session ID:', error);
144
+ return null;
145
+ }
146
+ }
147
+ /**
148
+ * Get current working directory
149
+ */
150
+ getCurrentWorkingDirectory() {
151
+ return this.currentWorkingDirectory;
152
+ }
153
+ /**
154
+ * Set working directory
155
+ */
156
+ setWorkingDirectory(targetPath) {
157
+ const resolvedPath = this.directoryGuard.resolveWorkingDirectory(targetPath, this.currentWorkingDirectory);
158
+ // If directory changes, we need to restart the process
159
+ const needsRestart = this.currentWorkingDirectory !== resolvedPath && this.claudeProcess !== null;
160
+ this.currentWorkingDirectory = resolvedPath;
161
+ this.sessionFilePath = path_1.default.join(this.currentWorkingDirectory, '.claude-session');
162
+ this.loadSessionId();
163
+ if (needsRestart) {
164
+ console.log('[ClaudePersistent] Working directory changed, restarting process...');
165
+ this.stopProcess().then(() => this.startProcess());
166
+ }
167
+ }
168
+ /**
169
+ * Start the persistent Claude process
170
+ */
171
+ async startProcess() {
172
+ if (this.claudeProcess || this.isStarting) {
173
+ return;
174
+ }
175
+ this.isStarting = true;
176
+ try {
177
+ // If no session ID, try to get recent one
178
+ if (!this.sessionId) {
179
+ console.log('[ClaudePersistent] No session ID, checking for recent sessions...');
180
+ this.sessionId = await this.getRecentSessionId();
181
+ if (this.sessionId) {
182
+ this.saveSessionId(this.sessionId);
183
+ }
184
+ }
185
+ // Build arguments
186
+ // Note: --output-format=stream-json requires --verbose
187
+ const args = [
188
+ '--input-format=stream-json',
189
+ '--output-format=stream-json',
190
+ '--include-partial-messages',
191
+ '--verbose',
192
+ '--dangerously-skip-permissions'
193
+ ];
194
+ if (this.sessionId) {
195
+ args.push('--resume', this.sessionId);
196
+ console.log(`[ClaudePersistent] Resuming session: ${this.sessionId}`);
197
+ }
198
+ else {
199
+ console.log('[ClaudePersistent] Starting new session');
200
+ }
201
+ console.log(`[ClaudePersistent] Starting: claude ${args.join(' ')}`);
202
+ console.log(`[ClaudePersistent] Working directory: ${this.currentWorkingDirectory}`);
203
+ // Spawn the process
204
+ const child = (0, child_process_1.spawn)('claude', args, {
205
+ cwd: this.currentWorkingDirectory,
206
+ stdio: ['pipe', 'pipe', 'pipe'],
207
+ env: {
208
+ ...process.env,
209
+ FORCE_COLOR: '0',
210
+ // Prevent nested session error
211
+ CLAUDECODE: '',
212
+ },
213
+ });
214
+ this.claudeProcess = child;
215
+ // Buffer for collecting stderr on startup (for error reporting)
216
+ let stderrBuffer = [];
217
+ let isStartupPhase = true;
218
+ // Handle stdout (JSON stream)
219
+ let buffer = '';
220
+ child.stdout?.on('data', (data) => {
221
+ buffer += data.toString();
222
+ // Process complete JSON lines
223
+ const lines = buffer.split('\n');
224
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
225
+ for (const line of lines) {
226
+ if (line.trim()) {
227
+ this.handleOutputLine(line.trim());
228
+ }
229
+ }
230
+ });
231
+ // Handle stderr
232
+ child.stderr?.on('data', (data) => {
233
+ const text = data.toString();
234
+ // Collect stderr during startup for error reporting
235
+ if (isStartupPhase) {
236
+ stderrBuffer.push(text);
237
+ // Keep only last 20 lines to avoid memory issues
238
+ if (stderrBuffer.length > 20) {
239
+ stderrBuffer.shift();
240
+ }
241
+ }
242
+ console.error('[ClaudePersistent stderr]', text);
243
+ // Forward to current stream callback if available
244
+ if (this.currentStreamCallback) {
245
+ this.currentStreamCallback(text);
246
+ }
247
+ this.currentOutputBuffer.push(text);
248
+ });
249
+ // Handle process exit
250
+ child.on('exit', (code, signal) => {
251
+ isStartupPhase = false;
252
+ const stderrOutput = stderrBuffer.join('');
253
+ stderrBuffer = []; // Clear buffer
254
+ if (code !== 0 && code !== null) {
255
+ console.error(`[ClaudePersistent] Process exited with code ${code}, signal ${signal}`);
256
+ if (stderrOutput) {
257
+ console.error('[ClaudePersistent] stderr output:\n', stderrOutput);
258
+ }
259
+ }
260
+ else {
261
+ console.log(`[ClaudePersistent] Process exited with code ${code}, signal ${signal}`);
262
+ }
263
+ this.claudeProcess = null;
264
+ // Check if this was an intentional stop (abort/reset)
265
+ const wasIntentionalStop = this.isStopping;
266
+ this.isStopping = false; // Reset the flag
267
+ // Reject current command if any (only for unexpected exits)
268
+ if (this.currentCommandReject && !wasIntentionalStop) {
269
+ let errorMsg = `Claude process exited unexpectedly (code: ${code})`;
270
+ if (stderrOutput) {
271
+ errorMsg += `\nstderr: ${stderrOutput.substring(0, 500)}`;
272
+ }
273
+ this.currentCommandReject(new Error(errorMsg));
274
+ this.resetCurrentCommand();
275
+ }
276
+ // Emit event for external handling
277
+ this.emit('processExit', { code, signal, intentional: wasIntentionalStop });
278
+ // Auto-restart if not destroyed and there are pending commands
279
+ // Don't auto-restart if this was an intentional stop (let processQueue handle it)
280
+ if (!this.isDestroyed && !wasIntentionalStop && this.commandQueue.length > 0) {
281
+ console.log('[ClaudePersistent] Auto-restarting process...');
282
+ setTimeout(() => this.startProcess(), 1000);
283
+ }
284
+ });
285
+ // Handle process error
286
+ child.on('error', (error) => {
287
+ console.error('[ClaudePersistent] Process error:', error);
288
+ this.claudeProcess = null;
289
+ this.isStarting = false;
290
+ if (this.currentCommandReject) {
291
+ if (error.message.includes('ENOENT')) {
292
+ this.currentCommandReject(new Error('Claude Code CLI not found. Please ensure Claude Code is installed and available in PATH.'));
293
+ }
294
+ else {
295
+ this.currentCommandReject(error);
296
+ }
297
+ this.resetCurrentCommand();
298
+ }
299
+ });
300
+ // Wait a bit for process to be ready
301
+ await new Promise(resolve => setTimeout(resolve, 1000));
302
+ // Clear startup phase after a few seconds (stderr won't be buffered anymore)
303
+ setTimeout(() => {
304
+ isStartupPhase = false;
305
+ stderrBuffer = [];
306
+ }, 5000);
307
+ console.log(`[ClaudePersistent] Process started with PID: ${child.pid}`);
308
+ this.isStarting = false;
309
+ // Process any pending commands
310
+ this.processQueue();
311
+ }
312
+ catch (error) {
313
+ this.isStarting = false;
314
+ throw error;
315
+ }
316
+ }
317
+ /**
318
+ * Stop the persistent Claude process
319
+ */
320
+ async stopProcess() {
321
+ if (!this.claudeProcess) {
322
+ return;
323
+ }
324
+ console.log('[ClaudePersistent] Stopping process...');
325
+ this.isStopping = true;
326
+ // Send EOF to stdin to gracefully close
327
+ this.claudeProcess.stdin?.end();
328
+ // Kill after timeout
329
+ const killTimeout = setTimeout(() => {
330
+ if (this.claudeProcess) {
331
+ console.log('[ClaudePersistent] Force killing process...');
332
+ this.claudeProcess.kill('SIGTERM');
333
+ }
334
+ }, 5000);
335
+ // Wait for process to exit
336
+ await new Promise(resolve => {
337
+ if (!this.claudeProcess) {
338
+ resolve();
339
+ return;
340
+ }
341
+ this.claudeProcess.on('exit', () => {
342
+ clearTimeout(killTimeout);
343
+ resolve();
344
+ });
345
+ });
346
+ this.claudeProcess = null;
347
+ console.log('[ClaudePersistent] Process stopped');
348
+ }
349
+ /**
350
+ * Reset the command timeout timer when activity is detected
351
+ * This prevents timeout during long-running tasks with continuous output
352
+ *
353
+ * NOTE: This method is currently disabled because inactivity timeout can cause
354
+ * state inconsistency - the executor may timeout while Claude process is still
355
+ * running, leading to confusing behavior where user sees timeout error but
356
+ * Claude is still working. Users can use /abort to cancel long-running tasks.
357
+ */
358
+ resetActivityTimeout() {
359
+ // Activity-based timeout is disabled to prevent state inconsistency.
360
+ // If a command hangs, users can manually abort with /abort.
361
+ if (!this.activityTrackingEnabled) {
362
+ return;
363
+ }
364
+ if (!this.isProcessing || !this.currentTimeoutTimer) {
365
+ return;
366
+ }
367
+ // Clear existing timer and set a new one
368
+ clearTimeout(this.currentTimeoutTimer);
369
+ this.currentTimeoutTimer = setTimeout(() => {
370
+ console.error('[ClaudePersistent] Command timeout due to inactivity');
371
+ this.completeCurrentCommand(false, 'Execution timeout: No response from Claude for 10 minutes');
372
+ }, this.defaultTimeout);
373
+ }
374
+ /**
375
+ * Handle a line of JSON output from Claude
376
+ */
377
+ handleOutputLine(line) {
378
+ try {
379
+ // Parse message first to check type
380
+ const parsedMessage = JSON.parse(line);
381
+ // Skip logging for stream_event messages to avoid console spam
382
+ // These are internal protocol messages (content_block_start, content_block_delta, etc.)
383
+ if (parsedMessage.type !== 'stream_event') {
384
+ const timestamp = new Date().toISOString();
385
+ console.log(`[ClaudePersistent RAW ${timestamp}] ${line}`);
386
+ }
387
+ const message = parsedMessage;
388
+ // Reset timeout on any activity to prevent timeout during long tasks
389
+ this.resetActivityTimeout();
390
+ switch (message.type) {
391
+ case 'message':
392
+ case 'thinking':
393
+ const contentLength = typeof message.content === 'string' ? message.content.length : JSON.stringify(message.content).length;
394
+ console.log(`[ClaudePersistent] Received ${message.type} message, partial=${message.partial}, content length=${contentLength}`);
395
+ if (message.content) {
396
+ const contentStr = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);
397
+ this.currentOutputBuffer.push(contentStr);
398
+ if (this.currentStreamCallback) {
399
+ this.currentStreamCallback(contentStr);
400
+ }
401
+ // Note: Don't complete on partial=false here
402
+ // The command completion should be handled by the 'result' message
403
+ // which is the definitive end-of-response signal
404
+ }
405
+ break;
406
+ case 'error':
407
+ console.error('[ClaudePersistent] Error from Claude:', message.content);
408
+ this.completeCurrentCommand(false, typeof message.content === 'string' ? message.content : 'Unknown error from Claude');
409
+ break;
410
+ case 'usage':
411
+ // Usage info, can be logged or ignored
412
+ if (message.usage) {
413
+ console.log(`[ClaudePersistent] Usage: ${message.usage.input_tokens} in / ${message.usage.output_tokens} out`);
414
+ }
415
+ break;
416
+ case 'system':
417
+ // System messages (e.g., init)
418
+ if (message.subtype === 'init' && message.session_id) {
419
+ console.log(`[ClaudePersistent] Session initialized: ${message.session_id}`);
420
+ this.sessionId = message.session_id;
421
+ this.saveSessionId(message.session_id);
422
+ }
423
+ break;
424
+ case 'stream_event':
425
+ // Stream events are internal protocol messages, silently ignore
426
+ // These include content_block_start, content_block_delta, etc.
427
+ break;
428
+ case 'result':
429
+ // Result messages contain the final response and completion status
430
+ console.log(`[ClaudePersistent] Result received, subtype=${message.subtype}, has result=${!!message.result}, has error=${message.is_error}`);
431
+ console.log(`[ClaudePersistent] Result message FULL: ${JSON.stringify(message)}`);
432
+ // NOTE: Do NOT send message.result to stream callback here!
433
+ // The result content has already been sent via 'assistant' messages (type: 'text' blocks)
434
+ // Sending it again would cause duplicate display in the UI
435
+ // Only complete if we're not waiting for user input
436
+ // The command completion should happen after all content is processed
437
+ if (!this.isWaitingForInput) {
438
+ // Complete the command with success or error based on is_error flag
439
+ if (message.is_error) {
440
+ this.completeCurrentCommand(false, message.result || 'Command failed');
441
+ }
442
+ else {
443
+ this.completeCurrentCommand(true);
444
+ }
445
+ }
446
+ else {
447
+ console.log('[ClaudePersistent] Result received but waiting for input, not completing yet');
448
+ }
449
+ break;
450
+ case 'assistant':
451
+ // Assistant messages contain the actual response content
452
+ // They can be partial (streaming) or complete
453
+ // partial=true: streaming chunk, partial=false or undefined: complete message
454
+ const isPartial = message.partial === true;
455
+ console.log(`[ClaudePersistent] Assistant message, partial=${isPartial}`);
456
+ // Check for tool_use in the nested message.content array
457
+ const contentBlocks = message.message?.content || (Array.isArray(message.content) ? message.content : null);
458
+ if (contentBlocks && contentBlocks.length > 0) {
459
+ for (const block of contentBlocks) {
460
+ if (block.type === 'tool_use') {
461
+ console.log(`[ClaudePersistent] Tool use detected: ${block.name}, id=${block.id}`);
462
+ // Send structured tool use event if callback is available
463
+ if (this.currentToolUseCallback) {
464
+ this.currentToolUseCallback({
465
+ name: block.name || 'unknown',
466
+ id: block.id || 'unknown',
467
+ input: block.input || {}
468
+ });
469
+ }
470
+ // Note: We no longer send text-based tool use separators.
471
+ // Tool use is now communicated via structured events (onToolUse callback)
472
+ // which are converted to Feishu Card 2.0 elements by the router.
473
+ // NOTE: Do NOT emit tool:afterExecution hook here!
474
+ // tool_use is just a REQUEST to execute the tool, not the actual execution result.
475
+ // The tool will be executed by Claude CLI, and we'll receive the result in a 'user' message
476
+ // with tool_result content. We should emit the hook when we receive tool_result.
477
+ }
478
+ else if (block.type === 'text' && block.text) {
479
+ // Stream text content to callback for real-time display
480
+ this.currentOutputBuffer.push(block.text);
481
+ if (this.currentStreamCallback) {
482
+ this.currentStreamCallback(block.text);
483
+ }
484
+ }
485
+ }
486
+ }
487
+ // Also handle simple string content (fallback)
488
+ if (typeof message.content === 'string' && message.content) {
489
+ this.currentOutputBuffer.push(message.content);
490
+ if (this.currentStreamCallback) {
491
+ this.currentStreamCallback(message.content);
492
+ }
493
+ this.startInputDetectionTimer(message.content);
494
+ }
495
+ // Check if this is the final message (stop_reason indicates completion)
496
+ const isComplete = message.message?.stop_reason !== null && message.message?.stop_reason !== undefined;
497
+ if (isComplete) {
498
+ console.log(`[ClaudePersistent] Assistant message complete, stop_reason=${message.message?.stop_reason}`);
499
+ }
500
+ break;
501
+ case 'user':
502
+ // User messages can contain tool_result blocks - need to process these
503
+ const userTimestamp = new Date().toISOString();
504
+ console.log(`[ClaudePersistent] User message at ${userTimestamp}, checking for tool_result...`);
505
+ // Check if this message contains tool results
506
+ const userContentBlocks = message.message?.content || (Array.isArray(message.content) ? message.content : null);
507
+ if (userContentBlocks && Array.isArray(userContentBlocks)) {
508
+ for (const block of userContentBlocks) {
509
+ if (block.type === 'tool_result') {
510
+ const isError = block.is_error === true;
511
+ console.log(`[ClaudePersistent] Tool result received: tool_use_id=${block.id}, is_error=${isError}`);
512
+ console.log(`[ClaudePersistent] Tool result full: ${JSON.stringify(message).substring(0, 500)}`);
513
+ // Send structured tool result event if callback is available
514
+ if (this.currentToolResultCallback) {
515
+ this.currentToolResultCallback({
516
+ tool_use_id: block.id || 'unknown',
517
+ content: block.content || '(no content)',
518
+ is_error: isError
519
+ });
520
+ }
521
+ // Note: We no longer send text-based tool result messages.
522
+ // Tool results are now communicated via structured events (onToolResult callback)
523
+ // which are converted to Feishu Card 2.0 elements by the router.
524
+ // Emit hook for tool execution completion (this is the ACTUAL execution result)
525
+ // Note: We don't have the original tool name here, but we have the tool_use_id
526
+ ClaudeCodeHooks_1.claudeCodeHooks.notifyToolExecuted({
527
+ toolName: block.id || 'unknown', // Use tool_use_id as identifier
528
+ params: {}, // Original params not available in tool_result
529
+ timestamp: Date.now(),
530
+ taskId: this.currentTaskId || undefined,
531
+ }, {
532
+ success: !isError,
533
+ result: block.content || '',
534
+ error: isError ? (block.content || 'Tool execution failed') : undefined,
535
+ duration: 0, // Duration not available
536
+ });
537
+ }
538
+ }
539
+ }
540
+ break;
541
+ default:
542
+ // Log unknown message types for debugging
543
+ console.log('[ClaudePersistent] Unknown message type:', message.type, 'Full message:', JSON.stringify(message).substring(0, 200));
544
+ }
545
+ }
546
+ catch (error) {
547
+ // Not valid JSON, treat as plain text output
548
+ this.currentOutputBuffer.push(line);
549
+ if (this.currentStreamCallback) {
550
+ this.currentStreamCallback(line + '\n');
551
+ }
552
+ }
553
+ }
554
+ /**
555
+ * Start a timer to detect if user input is requested
556
+ * If no new output arrives within the timeout, check if the last output contains input request patterns
557
+ */
558
+ startInputDetectionTimer(content) {
559
+ // Clear any existing timer
560
+ if (this.inputDetectionTimer) {
561
+ clearTimeout(this.inputDetectionTimer);
562
+ }
563
+ this.lastOutputTime = Date.now();
564
+ // Set a timer to check for input request after a short delay
565
+ this.inputDetectionTimer = setTimeout(() => {
566
+ if (this.isWaitingForInput || !this.isProcessing) {
567
+ return;
568
+ }
569
+ const output = this.currentOutputBuffer.join('');
570
+ if (this.isInputRequest(output)) {
571
+ console.log('[ClaudePersistent] Detected input request in output');
572
+ this.handleInputRequest(output);
573
+ }
574
+ }, 2000); // 2 second delay to wait for more output
575
+ }
576
+ /**
577
+ * Handle input request by notifying user and waiting for response
578
+ */
579
+ async handleInputRequest(output) {
580
+ // Extract the last line or prompt from output
581
+ const lines = output.trim().split('\n');
582
+ const lastLine = lines[lines.length - 1] || 'Please provide your response';
583
+ // Pause processing state
584
+ this.isWaitingForInput = true;
585
+ // Notify through hooks
586
+ const userInput = await this.requestInteractiveInput(lastLine);
587
+ if (userInput !== null) {
588
+ this.sendInput(userInput);
589
+ }
590
+ else {
591
+ // User cancelled or timed out
592
+ console.log('[ClaudePersistent] Input request cancelled or timed out');
593
+ this.isWaitingForInput = false;
594
+ }
595
+ }
596
+ /**
597
+ * Check if the output indicates a request for user input
598
+ * This detects patterns like "Press Enter to continue", "(y/n)", etc.
599
+ */
600
+ isInputRequest(output) {
601
+ const inputPatterns = [
602
+ /\(y\/n\?*\)/i, // (y/n) or (y/n?)
603
+ /\[y\/n\]/i, // [y/n]
604
+ /press enter to continue/i, // Press Enter to continue
605
+ /type 'yes' to continue/i, // Type 'yes' to continue
606
+ /waiting for your (response|input)/i, // Waiting for response/input
607
+ /please confirm/i, // Please confirm
608
+ /do you want to/i, // Do you want to...
609
+ /would you like to/i, // Would you like to...
610
+ />\s*$/, // Prompt ending with >
611
+ /:\s*$/, // Prompt ending with :
612
+ ];
613
+ return inputPatterns.some(pattern => pattern.test(output));
614
+ }
615
+ /**
616
+ * Request user input through hooks
617
+ * Returns a promise that resolves when user provides input via sendInput()
618
+ */
619
+ async requestInteractiveInput(prompt) {
620
+ console.log('[ClaudePersistent] Requesting interactive input:', prompt);
621
+ this.isWaitingForInput = true;
622
+ // Emit hook to notify user (fire and forget)
623
+ ClaudeCodeHooks_1.claudeCodeHooks.requestUserInput({
624
+ prompt,
625
+ type: 'text',
626
+ timeout: 300000, // 5 minutes timeout for input
627
+ }).catch(() => {
628
+ // Ignore errors from notification
629
+ });
630
+ // Wait for input via sendInput() or timeout
631
+ return new Promise((resolve) => {
632
+ const timeoutId = setTimeout(() => {
633
+ console.log('[ClaudePersistent] Input request timed out');
634
+ this.isWaitingForInput = false;
635
+ resolve(null);
636
+ }, 300000); // 5 minutes timeout
637
+ // Store callback to be called when sendInput is invoked
638
+ this.inputRequestCallbacks.push((input) => {
639
+ clearTimeout(timeoutId);
640
+ resolve(input);
641
+ });
642
+ });
643
+ }
644
+ /**
645
+ * Send user input to the Claude process
646
+ * This is called by MessageHandler when user sends a message while waiting for input
647
+ */
648
+ sendInput(input) {
649
+ if (!this.claudeProcess || !this.isWaitingForInput) {
650
+ console.log('[ClaudePersistent] Cannot send input - process not running or not waiting for input');
651
+ return false;
652
+ }
653
+ const inputMessage = {
654
+ type: 'user',
655
+ message: {
656
+ role: 'user',
657
+ content: input,
658
+ },
659
+ };
660
+ const inputLine = JSON.stringify(inputMessage);
661
+ console.log(`[ClaudePersistent] Sending user input: ${input}`);
662
+ this.claudeProcess.stdin?.write(inputLine + '\n');
663
+ this.isWaitingForInput = false;
664
+ // Notify any waiting callbacks (for requestInteractiveInput)
665
+ for (const callback of this.inputRequestCallbacks) {
666
+ callback(input);
667
+ }
668
+ this.inputRequestCallbacks = [];
669
+ return true;
670
+ }
671
+ /**
672
+ * Check if currently waiting for user input
673
+ */
674
+ isWaitingInput() {
675
+ return this.isWaitingForInput;
676
+ }
677
+ /**
678
+ * Complete the current command
679
+ */
680
+ completeCurrentCommand(success, errorMessage) {
681
+ // Guard against double completion
682
+ if (!this.currentCommandResolve && !this.currentCommandReject) {
683
+ console.log('[ClaudePersistent] Command already completed, ignoring duplicate completion');
684
+ return;
685
+ }
686
+ if (this.currentTimeoutTimer) {
687
+ clearTimeout(this.currentTimeoutTimer);
688
+ this.currentTimeoutTimer = undefined;
689
+ }
690
+ const output = this.currentOutputBuffer.join('');
691
+ console.log(`[ClaudePersistent] Completing command, success=${success}, output length=${output.length}, output preview: ${output.substring(0, 100)}...`);
692
+ // Emit task completion hooks
693
+ if (this.currentTaskId) {
694
+ const endTime = Date.now();
695
+ const duration = endTime - this.currentTaskStartTime;
696
+ if (success) {
697
+ ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskCompleted({
698
+ taskId: this.currentTaskId,
699
+ description: '', // Will be populated from queue if needed
700
+ workingDirectory: this.currentWorkingDirectory,
701
+ sessionId: this.sessionId || undefined,
702
+ startTime: this.currentTaskStartTime,
703
+ }, {
704
+ success: true,
705
+ output: output.trim(),
706
+ endTime,
707
+ duration,
708
+ });
709
+ }
710
+ else {
711
+ ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskFailed({
712
+ taskId: this.currentTaskId,
713
+ description: '',
714
+ workingDirectory: this.currentWorkingDirectory,
715
+ sessionId: this.sessionId || undefined,
716
+ startTime: this.currentTaskStartTime,
717
+ }, new Error(errorMessage || 'Command failed'));
718
+ }
719
+ }
720
+ if (success && this.currentCommandResolve) {
721
+ // Get session abbreviation (last 8 characters of session ID)
722
+ const sessionAbbr = this.sessionId ? this.sessionId.slice(-8) : undefined;
723
+ this.currentCommandResolve({
724
+ success: true,
725
+ output: output.trim(),
726
+ sessionAbbr,
727
+ });
728
+ }
729
+ else if (this.currentCommandReject) {
730
+ this.currentCommandReject(new Error(errorMessage || 'Command failed'));
731
+ }
732
+ this.resetCurrentCommand();
733
+ // Process next command in queue
734
+ this.isProcessing = false;
735
+ this.processQueue();
736
+ }
737
+ /**
738
+ * Reset current command state
739
+ */
740
+ resetCurrentCommand() {
741
+ this.currentOutputBuffer = [];
742
+ this.currentStreamCallback = undefined;
743
+ this.currentToolUseCallback = undefined;
744
+ this.currentToolResultCallback = undefined;
745
+ this.currentCommandResolve = undefined;
746
+ this.currentCommandReject = undefined;
747
+ if (this.currentTimeoutTimer) {
748
+ clearTimeout(this.currentTimeoutTimer);
749
+ this.currentTimeoutTimer = undefined;
750
+ }
751
+ if (this.inputDetectionTimer) {
752
+ clearTimeout(this.inputDetectionTimer);
753
+ this.inputDetectionTimer = undefined;
754
+ }
755
+ this.isWaitingForInput = false;
756
+ // Note: currentTaskId and currentTaskStartTime are cleared separately after hooks
757
+ }
758
+ /**
759
+ * Process the command queue
760
+ */
761
+ processQueue() {
762
+ if (this.isProcessing || this.commandQueue.length === 0) {
763
+ return;
764
+ }
765
+ // Ensure process is running
766
+ if (!this.claudeProcess) {
767
+ this.startProcess();
768
+ return;
769
+ }
770
+ const command = this.commandQueue.shift();
771
+ if (!command) {
772
+ return;
773
+ }
774
+ this.isProcessing = true;
775
+ this.currentStreamCallback = command.options.onStream;
776
+ this.currentStructuredCallback = command.options.onStructuredContent;
777
+ this.currentToolUseCallback = command.options.onToolUse;
778
+ this.currentToolResultCallback = command.options.onToolResult;
779
+ this.currentCommandResolve = command.resolve;
780
+ this.currentCommandReject = command.reject;
781
+ // Reset structured content collection
782
+ this.structuredContentBlocks = [];
783
+ // Generate task ID and track start time for hooks
784
+ this.currentTaskId = `task_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
785
+ this.currentTaskStartTime = Date.now();
786
+ // Emit task started hook
787
+ ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskStarted({
788
+ taskId: this.currentTaskId,
789
+ description: command.prompt,
790
+ workingDirectory: this.currentWorkingDirectory,
791
+ sessionId: this.sessionId || undefined,
792
+ startTime: this.currentTaskStartTime,
793
+ });
794
+ // Set timeout (only if explicitly requested via options.timeout)
795
+ // Note: We don't set a default timeout because it can cause state inconsistency
796
+ // - the executor times out but Claude process keeps running
797
+ // Users can cancel long-running tasks with /abort
798
+ if (command.options.timeout) {
799
+ this.currentTimeoutTimer = setTimeout(() => {
800
+ console.error('[ClaudePersistent] Command timeout');
801
+ this.completeCurrentCommand(false, 'Execution timeout exceeded');
802
+ }, command.options.timeout);
803
+ }
804
+ // Send the command
805
+ const inputMessage = {
806
+ type: 'user',
807
+ message: {
808
+ role: 'user',
809
+ content: command.prompt,
810
+ },
811
+ };
812
+ const inputLine = JSON.stringify(inputMessage);
813
+ console.log(`[ClaudePersistent] Sending command: ${command.prompt.substring(0, 100)}...`);
814
+ this.claudeProcess.stdin?.write(inputLine + '\n');
815
+ }
816
+ /**
817
+ * Execute a command through the persistent Claude process
818
+ */
819
+ async execute(prompt, options = {}) {
820
+ if (this.isDestroyed) {
821
+ return {
822
+ success: false,
823
+ error: 'Executor has been destroyed',
824
+ };
825
+ }
826
+ return new Promise((resolve, reject) => {
827
+ // Add to queue
828
+ this.commandQueue.push({
829
+ prompt,
830
+ options,
831
+ resolve: (result) => {
832
+ // Extract and save session ID if it's a new session
833
+ if (!this.sessionId && result.success) {
834
+ const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
835
+ const matches = result.output?.match(uuidPattern);
836
+ if (matches && matches.length > 0) {
837
+ this.sessionId = matches[0];
838
+ this.saveSessionId(this.sessionId);
839
+ }
840
+ }
841
+ resolve(result);
842
+ },
843
+ reject,
844
+ });
845
+ // Try to process queue
846
+ this.processQueue();
847
+ });
848
+ }
849
+ /**
850
+ * Abort current command execution
851
+ * Stops the current process and rejects the pending command
852
+ */
853
+ async abort() {
854
+ if (!this.isProcessing && !this.isWaitingForInput) {
855
+ console.log('[ClaudePersistent] No command is currently executing');
856
+ return false;
857
+ }
858
+ // If waiting for input, cancel the input request
859
+ if (this.isWaitingForInput) {
860
+ console.log('[ClaudePersistent] Cancelling input request...');
861
+ this.isWaitingForInput = false;
862
+ if (this.inputDetectionTimer) {
863
+ clearTimeout(this.inputDetectionTimer);
864
+ this.inputDetectionTimer = undefined;
865
+ }
866
+ }
867
+ console.log('[ClaudePersistent] Aborting current command...');
868
+ // Emit task aborted hook before stopping
869
+ if (this.currentTaskId) {
870
+ ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskAborted({
871
+ taskId: this.currentTaskId,
872
+ description: '',
873
+ workingDirectory: this.currentWorkingDirectory,
874
+ sessionId: this.sessionId || undefined,
875
+ startTime: this.currentTaskStartTime,
876
+ }, 'Command aborted by user via /abort');
877
+ }
878
+ // Mark as intentional stop before rejecting and stopping
879
+ this.isStopping = true;
880
+ // Reject current command if any
881
+ if (this.currentCommandReject) {
882
+ this.currentCommandReject(new Error('Command aborted by user'));
883
+ }
884
+ // Reset command state
885
+ this.resetCurrentCommand();
886
+ this.isProcessing = false;
887
+ this.currentTaskId = null;
888
+ this.currentTaskStartTime = 0;
889
+ // Stop the process to ensure clean state
890
+ await this.stopProcess();
891
+ console.log('[ClaudePersistent] Process stopped after abort');
892
+ // Clear any pending commands in queue
893
+ if (this.commandQueue.length > 0) {
894
+ console.log(`[ClaudePersistent] Clearing ${this.commandQueue.length} pending commands from queue`);
895
+ for (const command of this.commandQueue) {
896
+ command.reject(new Error('Command aborted by user'));
897
+ }
898
+ this.commandQueue = [];
899
+ }
900
+ return true;
901
+ }
902
+ /**
903
+ * Reset execution context
904
+ */
905
+ resetContext() {
906
+ console.log('[ClaudePersistent] Resetting session context');
907
+ this.sessionId = null;
908
+ // Stop current process
909
+ this.stopProcess();
910
+ // Clear queue
911
+ this.commandQueue = [];
912
+ this.resetCurrentCommand();
913
+ this.isProcessing = false;
914
+ // Remove session file
915
+ try {
916
+ if (fs_1.default.existsSync(this.sessionFilePath)) {
917
+ fs_1.default.unlinkSync(this.sessionFilePath);
918
+ console.log('[ClaudePersistent] Session file removed');
919
+ }
920
+ }
921
+ catch (error) {
922
+ console.error('[ClaudePersistent] Failed to remove session file:', error);
923
+ }
924
+ }
925
+ /**
926
+ * Destroy executor and cleanup
927
+ */
928
+ async destroy() {
929
+ this.isDestroyed = true;
930
+ // Clear queue and reject pending commands
931
+ for (const command of this.commandQueue) {
932
+ command.reject(new Error('Executor has been destroyed'));
933
+ }
934
+ this.commandQueue = [];
935
+ if (this.currentCommandReject) {
936
+ this.currentCommandReject(new Error('Executor has been destroyed'));
937
+ this.resetCurrentCommand();
938
+ }
939
+ // Stop process
940
+ await this.stopProcess();
941
+ // Remove all listeners
942
+ this.removeAllListeners();
943
+ }
944
+ /**
945
+ * Check if process is running
946
+ */
947
+ isProcessRunning() {
948
+ return this.claudeProcess !== null && !this.claudeProcess.killed;
949
+ }
950
+ /**
951
+ * Get current session ID
952
+ */
953
+ getSessionId() {
954
+ return this.sessionId;
955
+ }
956
+ }
957
+ exports.ClaudePersistentExecutor = ClaudePersistentExecutor;
958
+ //# sourceMappingURL=ClaudePersistentExecutor.js.map