@yu_robotics/remote-cli 1.6.53 → 1.6.66

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 (64) hide show
  1. package/README.md +18 -8
  2. package/dist/client/MessageHandler.d.ts +7 -0
  3. package/dist/client/MessageHandler.d.ts.map +1 -1
  4. package/dist/client/MessageHandler.js +96 -3
  5. package/dist/client/MessageHandler.js.map +1 -1
  6. package/dist/client/TaskRecovery.d.ts +7 -3
  7. package/dist/client/TaskRecovery.d.ts.map +1 -1
  8. package/dist/client/TaskRecovery.js +80 -27
  9. package/dist/client/TaskRecovery.js.map +1 -1
  10. package/dist/client/WebSocketClient.d.ts.map +1 -1
  11. package/dist/client/WebSocketClient.js +2 -10
  12. package/dist/client/WebSocketClient.js.map +1 -1
  13. package/dist/commands/start.d.ts +3 -11
  14. package/dist/commands/start.d.ts.map +1 -1
  15. package/dist/commands/start.js +22 -99
  16. package/dist/commands/start.js.map +1 -1
  17. package/dist/executor/ClaudePersistentExecutor.d.ts +34 -2
  18. package/dist/executor/ClaudePersistentExecutor.d.ts.map +1 -1
  19. package/dist/executor/ClaudePersistentExecutor.js +313 -9
  20. package/dist/executor/ClaudePersistentExecutor.js.map +1 -1
  21. package/dist/executor/CodexAppServerExecutor.d.ts +13 -0
  22. package/dist/executor/CodexAppServerExecutor.d.ts.map +1 -1
  23. package/dist/executor/CodexAppServerExecutor.js +208 -5
  24. package/dist/executor/CodexAppServerExecutor.js.map +1 -1
  25. package/dist/executor/CodexSandbox.d.ts +23 -0
  26. package/dist/executor/CodexSandbox.d.ts.map +1 -0
  27. package/dist/executor/CodexSandbox.js +211 -0
  28. package/dist/executor/CodexSandbox.js.map +1 -0
  29. package/dist/executor/IExecutor.d.ts +5 -1
  30. package/dist/executor/IExecutor.d.ts.map +1 -1
  31. package/dist/executor/claude/ClaudeFilePolicy.d.ts +7 -0
  32. package/dist/executor/claude/ClaudeFilePolicy.d.ts.map +1 -0
  33. package/dist/executor/claude/ClaudeFilePolicy.js +88 -0
  34. package/dist/executor/claude/ClaudeFilePolicy.js.map +1 -0
  35. package/dist/executor/claude/ClaudeSandbox.d.ts +32 -0
  36. package/dist/executor/claude/ClaudeSandbox.d.ts.map +1 -0
  37. package/dist/executor/claude/ClaudeSandbox.js +245 -0
  38. package/dist/executor/claude/ClaudeSandbox.js.map +1 -0
  39. package/dist/executor/claude/approvalMcpServer.d.ts +2 -0
  40. package/dist/executor/claude/approvalMcpServer.d.ts.map +1 -0
  41. package/dist/executor/claude/approvalMcpServer.js +159 -0
  42. package/dist/executor/claude/approvalMcpServer.js.map +1 -0
  43. package/dist/executor/claude/filePolicyHook.d.ts +2 -0
  44. package/dist/executor/claude/filePolicyHook.d.ts.map +1 -0
  45. package/dist/executor/claude/filePolicyHook.js +63 -0
  46. package/dist/executor/claude/filePolicyHook.js.map +1 -0
  47. package/dist/executor/index.d.ts.map +1 -1
  48. package/dist/executor/index.js +4 -3
  49. package/dist/executor/index.js.map +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/thread/ThreadManager.d.ts.map +1 -1
  53. package/dist/thread/ThreadManager.js +2 -0
  54. package/dist/thread/ThreadManager.js.map +1 -1
  55. package/dist/types/config.d.ts +32 -2
  56. package/dist/types/config.d.ts.map +1 -1
  57. package/dist/types/config.js.map +1 -1
  58. package/dist/types/index.d.ts +39 -0
  59. package/dist/types/index.d.ts.map +1 -1
  60. package/dist/update/AutomaticUpdater.d.ts +7 -1
  61. package/dist/update/AutomaticUpdater.d.ts.map +1 -1
  62. package/dist/update/AutomaticUpdater.js +25 -2
  63. package/dist/update/AutomaticUpdater.js.map +1 -1
  64. package/package.json +1 -1
@@ -6,9 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ClaudePersistentExecutor = void 0;
7
7
  const child_process_1 = require("child_process");
8
8
  const ClaudeCodeHooks_1 = require("../hooks/ClaudeCodeHooks");
9
+ const ClaudeSandbox_1 = require("./claude/ClaudeSandbox");
10
+ const ClaudeFilePolicy_1 = require("./claude/ClaudeFilePolicy");
9
11
  const fs_1 = __importDefault(require("fs"));
10
12
  const path_1 = __importDefault(require("path"));
11
13
  const os_1 = __importDefault(require("os"));
14
+ const net_1 = __importDefault(require("net"));
15
+ const crypto_1 = require("crypto");
12
16
  const events_1 = require("events");
13
17
  /**
14
18
  * Claude Persistent Executor
@@ -61,12 +65,32 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
61
65
  inputRequestCallbacks = [];
62
66
  inputDetectionTimer;
63
67
  lastOutputTime = 0;
68
+ // Sandbox and approval state
69
+ sandbox;
70
+ claudeCommand;
71
+ approvalServer = null;
72
+ approvalSocketPath;
73
+ approvalGeneration = 0;
74
+ approvalTurnActive = false;
75
+ pendingApprovals = new Map();
76
+ currentApprovalRequestCallback;
77
+ currentApprovalResolvedCallback;
78
+ filePolicyToken;
79
+ filePolicyRoots = [];
80
+ filePolicyStarted = false;
81
+ onFilePolicyStarted;
82
+ static APPROVAL_TIMEOUT_MS = 5 * 60 * 1000;
83
+ static APPROVAL_SERVER_SCRIPT = path_1.default.join(__dirname, 'claude', 'approvalMcpServer.js');
84
+ static APPROVAL_TOOL_NAME = 'mcp__remote-cli-approval__approval_prompt';
85
+ static FILE_POLICY_SCRIPT = path_1.default.join(__dirname, 'claude', 'filePolicyHook.js');
64
86
  // Activity tracking for timeout extension (optional, can be disabled)
65
87
  activityTrackingEnabled = false;
66
- constructor(directoryGuard, initialWorkingDirectory, threadId, model) {
88
+ constructor(directoryGuard, initialWorkingDirectory, threadId, model, sandbox, claudeCommand) {
67
89
  super();
68
90
  this.directoryGuard = directoryGuard;
69
91
  this.model = model;
92
+ this.sandbox = new ClaudeSandbox_1.ClaudeSandbox(directoryGuard, sandbox, threadId);
93
+ this.claudeCommand = claudeCommand ?? 'claude';
70
94
  // Use provided working directory or fall back to process.cwd()
71
95
  // If a working directory is provided, validate it first
72
96
  if (initialWorkingDirectory) {
@@ -213,14 +237,48 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
213
237
  }
214
238
  // Build arguments
215
239
  // Note: --output-format=stream-json requires --verbose
240
+ const restricted = this.sandbox.isRestricted();
216
241
  const args = [
217
242
  '--input-format=stream-json',
218
243
  '--output-format=stream-json',
219
244
  '--include-partial-messages',
220
245
  '--verbose',
221
- '--dangerously-skip-permissions',
222
- '--disallowedTools=AskUserQuestion'
223
246
  ];
247
+ if (restricted) {
248
+ // Sandboxed mode: OS-enforced Bash isolation; widening requests surface
249
+ // through the permission-prompt MCP tool as approval cards. Skipping
250
+ // permissions would silence that channel, so it is omitted on purpose.
251
+ this.sandbox.assertAvailable(this.currentWorkingDirectory);
252
+ // Override native acceptEdits/auto/bypass defaults for this process.
253
+ args.push('--permission-mode', 'default');
254
+ const socketPath = await this.ensureApprovalServer();
255
+ let fileHookCommand;
256
+ if (this.sandbox.getConfig()?.mode === 'workspace-write') {
257
+ this.filePolicyToken = (0, crypto_1.randomUUID)();
258
+ this.filePolicyStarted = false;
259
+ this.filePolicyRoots = [this.sandbox.normalize(this.currentWorkingDirectory), ...this.sandbox.extraWritableRoots()];
260
+ const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
261
+ // A failed hook launch must block the tool, not fall through to native allow rules.
262
+ fileHookCommand = [process.execPath, ClaudePersistentExecutor.FILE_POLICY_SCRIPT, socketPath, this.filePolicyToken]
263
+ .map(quote).join(' ') + ' || exit 2';
264
+ }
265
+ const settings = this.sandbox.spawnSettings(this.currentWorkingDirectory, fileHookCommand);
266
+ if (settings)
267
+ args.push('--settings', JSON.stringify(settings));
268
+ args.push('--mcp-config', JSON.stringify({
269
+ mcpServers: {
270
+ 'remote-cli-approval': {
271
+ command: process.execPath,
272
+ args: [ClaudePersistentExecutor.APPROVAL_SERVER_SCRIPT, socketPath],
273
+ },
274
+ },
275
+ }));
276
+ args.push('--permission-prompt-tool', ClaudePersistentExecutor.APPROVAL_TOOL_NAME);
277
+ }
278
+ else {
279
+ args.push('--dangerously-skip-permissions');
280
+ }
281
+ args.push('--disallowedTools=AskUserQuestion');
224
282
  if (this.model) {
225
283
  args.push('--model', this.model);
226
284
  }
@@ -231,10 +289,10 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
231
289
  else {
232
290
  console.log('[ClaudePersistent] Starting new session');
233
291
  }
234
- console.log(`[ClaudePersistent] Starting: claude ${args.join(' ')}`);
292
+ console.log(`[ClaudePersistent] Starting: ${this.claudeCommand} ${args.join(' ')}`);
235
293
  console.log(`[ClaudePersistent] Working directory: ${this.currentWorkingDirectory}`);
236
294
  // Spawn the process
237
- const child = (0, child_process_1.spawn)('claude', args, {
295
+ const child = (0, child_process_1.spawn)(this.claudeCommand, args, {
238
296
  cwd: this.currentWorkingDirectory,
239
297
  stdio: ['pipe', 'pipe', 'pipe'],
240
298
  env: {
@@ -299,6 +357,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
299
357
  // Use 'close' event (fires after all I/O streams are closed, i.e. after all stdout
300
358
  // data events have been processed) instead of 'exit' for command completion logic.
301
359
  child.on('close', (code, signal) => {
360
+ this.clearApprovals('Claude process exited');
302
361
  this.claudeProcess = null;
303
362
  // Check if this was an intentional stop (abort/reset)
304
363
  const wasIntentionalStop = this.isStopping;
@@ -363,9 +422,21 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
363
422
  }
364
423
  throw err;
365
424
  }
425
+ if (this.filePolicyToken) {
426
+ try {
427
+ // SessionStart runs before stdin is consumed. Do not send a user task
428
+ // unless the native hook is active; disabled/managed-only hooks fail closed.
429
+ await this.waitForFilePolicy(child);
430
+ }
431
+ catch (error) {
432
+ await this.stopProcess();
433
+ throw error;
434
+ }
435
+ }
366
436
  // Long-running error handler for errors that occur after startup
367
437
  child.on('error', (error) => {
368
438
  console.error('[ClaudePersistent] Process error:', error);
439
+ this.clearApprovals('Claude process failed');
369
440
  this.claudeProcess = null;
370
441
  this.isStarting = false;
371
442
  if (this.currentCommandReject) {
@@ -397,6 +468,10 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
397
468
  * Stop the persistent Claude process
398
469
  */
399
470
  async stopProcess() {
471
+ this.filePolicyToken = undefined;
472
+ this.filePolicyRoots = [];
473
+ this.filePolicyStarted = false;
474
+ this.clearApprovals('Claude process stopped');
400
475
  if (!this.claudeProcess) {
401
476
  return;
402
477
  }
@@ -425,6 +500,212 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
425
500
  this.claudeProcess = null;
426
501
  console.log('[ClaudePersistent] Process stopped');
427
502
  }
503
+ // ── Sandbox approval channel ─────────────────────────────────────────────
504
+ waitForFilePolicy(child) {
505
+ if (this.filePolicyStarted && this.claudeProcess === child)
506
+ return Promise.resolve();
507
+ return new Promise((resolve, reject) => {
508
+ const finish = (error) => {
509
+ clearTimeout(timer);
510
+ this.onFilePolicyStarted = undefined;
511
+ child.removeListener('close', onClose);
512
+ child.removeListener('error', onError);
513
+ error ? reject(error) : resolve();
514
+ };
515
+ const onClose = () => finish(new Error('Claude exited before sandbox file checks started.'));
516
+ const onError = (error) => finish(error);
517
+ const timer = setTimeout(() => finish(new Error('Claude sandbox file checks did not start. Check native hook restrictions (disableAllHooks or allowManagedHooksOnly).')), 10000);
518
+ this.onFilePolicyStarted = () => finish();
519
+ child.once('close', onClose);
520
+ child.once('error', onError);
521
+ if (this.claudeProcess !== child)
522
+ onClose();
523
+ });
524
+ }
525
+ async ensureApprovalServer() {
526
+ if (this.approvalServer && this.approvalSocketPath)
527
+ return this.approvalSocketPath;
528
+ const directory = path_1.default.join(os_1.default.homedir(), '.remote-cli', 'claude-approval');
529
+ fs_1.default.mkdirSync(directory, { recursive: true, mode: 0o700 });
530
+ const socketPath = path_1.default.join(directory, `${encodeURIComponent(this.threadId || (0, crypto_1.randomUUID)())}.sock`);
531
+ if (fs_1.default.existsSync(socketPath))
532
+ fs_1.default.unlinkSync(socketPath); // stale socket from a previous run
533
+ this.approvalServer = net_1.default.createServer((socket) => {
534
+ const generation = this.approvalGeneration;
535
+ let buffer = '';
536
+ socket.on('error', () => { });
537
+ socket.on('data', (chunk) => {
538
+ buffer += chunk;
539
+ let index;
540
+ while ((index = buffer.indexOf('\n')) >= 0) {
541
+ const line = buffer.slice(0, index);
542
+ buffer = buffer.slice(index + 1);
543
+ if (!line.trim())
544
+ continue;
545
+ try {
546
+ this.handleApprovalSocketRequest(JSON.parse(line), socket, generation);
547
+ }
548
+ catch (error) {
549
+ console.error('[ClaudePersistent] Failed to handle approval request:', error);
550
+ socket.end();
551
+ }
552
+ }
553
+ });
554
+ });
555
+ await new Promise((resolve, reject) => {
556
+ this.approvalServer.once('error', reject);
557
+ this.approvalServer.listen(socketPath, () => resolve());
558
+ });
559
+ fs_1.default.chmodSync(socketPath, 0o600);
560
+ this.approvalSocketPath = socketPath;
561
+ return socketPath;
562
+ }
563
+ handleApprovalSocketRequest(payload, socket, generation) {
564
+ if (payload?.type === 'file_policy') {
565
+ let answer = { decision: 'deny', reason: 'File policy request is no longer active.' };
566
+ if (this.filePolicyToken && payload.token === this.filePolicyToken && this.claudeProcess
567
+ && !this.isDestroyed && !this.isStopping) {
568
+ if (payload.startup === true) {
569
+ this.filePolicyStarted = true;
570
+ this.onFilePolicyStarted?.();
571
+ answer = { decision: 'allow', reason: 'File policy ready.' };
572
+ }
573
+ else if (this.isProcessing && this.approvalTurnActive && generation === this.approvalGeneration) {
574
+ answer = (0, ClaudeFilePolicy_1.evaluateFileWrite)(payload.tool_name, payload.input ?? {}, this.filePolicyRoots);
575
+ }
576
+ }
577
+ socket.end(JSON.stringify(answer) + '\n');
578
+ return;
579
+ }
580
+ const requestId = typeof payload?.id === 'string' ? payload.id : (0, crypto_1.randomUUID)();
581
+ if (this.isDestroyed || this.isStopping || !this.isProcessing || !this.approvalTurnActive
582
+ || (!this.currentCommandResolve && !this.currentCommandReject)
583
+ || generation !== this.approvalGeneration || this.pendingApprovals.has(requestId)) {
584
+ socket.end(JSON.stringify({ id: requestId, behavior: 'deny', message: 'Approval request is no longer active.' }) + '\n');
585
+ return;
586
+ }
587
+ const toolName = typeof payload?.tool_name === 'string' ? payload.tool_name : 'unknown';
588
+ const input = (payload?.input && typeof payload.input === 'object') ? payload.input : {};
589
+ const kind = toolName === 'Bash' ? 'command'
590
+ : (toolName === 'Write' || toolName === 'Edit' || toolName === 'NotebookEdit') ? 'file'
591
+ : 'permissions';
592
+ const description = toolName === 'Bash'
593
+ ? String(input.command ?? '').slice(0, 500)
594
+ : `${toolName}: ${String(input.file_path ?? input.notebook_path ?? input.path ?? JSON.stringify(input)).slice(0, 500)}`;
595
+ const timer = setTimeout(() => {
596
+ this.answerApproval(requestId, { behavior: 'deny', message: 'Approval request expired' }, 'expired');
597
+ }, ClaudePersistentExecutor.APPROVAL_TIMEOUT_MS);
598
+ timer.unref?.();
599
+ socket.on('close', () => {
600
+ const pending = this.pendingApprovals.get(requestId);
601
+ if (pending?.socket === socket)
602
+ this.answerApproval(requestId, { behavior: 'deny', message: 'Approval connection closed' }, 'expired');
603
+ });
604
+ this.pendingApprovals.set(requestId, { socket, timer, input, description, generation,
605
+ resolved: this.currentApprovalResolvedCallback });
606
+ let cardShown = false;
607
+ try {
608
+ cardShown = this.currentApprovalRequestCallback?.({ requestId, kind, description, canRemember: false }) === true;
609
+ }
610
+ catch { /* A rendering failure can fall back to the text prompt. */ }
611
+ if (!cardShown && this.pendingApprovals.has(requestId)) {
612
+ if (this.currentStreamCallback) {
613
+ this.currentStreamCallback(`\nApproval required (${requestId}): ${description}\nReply yes or no. For multiple requests, reply yes <request ID> or no <request ID>.\n`);
614
+ }
615
+ else {
616
+ this.answerApproval(requestId, { behavior: 'deny', message: 'No interactive approval channel is available.' }, 'denied');
617
+ }
618
+ }
619
+ }
620
+ answerApproval(requestId, answer, status) {
621
+ const pending = this.pendingApprovals.get(requestId);
622
+ if (!pending)
623
+ return false;
624
+ this.pendingApprovals.delete(requestId);
625
+ clearTimeout(pending.timer);
626
+ let delivered = !pending.socket.destroyed && pending.socket.writable;
627
+ if (delivered) {
628
+ try {
629
+ pending.socket.end(JSON.stringify({ id: requestId, ...answer }) + '\n');
630
+ }
631
+ catch {
632
+ delivered = false;
633
+ }
634
+ }
635
+ try {
636
+ pending.resolved?.(requestId, delivered ? status : 'expired');
637
+ }
638
+ catch { /* Card cleanup failures must not reactivate an answered request. */ }
639
+ return delivered;
640
+ }
641
+ /** MessageHandler entry point for approval card actions. */
642
+ respondToApproval(requestId, action) {
643
+ const pending = this.pendingApprovals.get(requestId);
644
+ if (!pending || !['approve', 'deny'].includes(action))
645
+ return false;
646
+ if (pending.generation !== this.approvalGeneration || !this.approvalTurnActive
647
+ || !this.isProcessing || this.isStopping || this.isDestroyed) {
648
+ this.answerApproval(requestId, { behavior: 'deny', message: 'Approval request expired' }, 'expired');
649
+ return false;
650
+ }
651
+ if (action === 'deny') {
652
+ return this.answerApproval(requestId, { behavior: 'deny', message: 'Denied by user' }, 'denied');
653
+ }
654
+ return this.answerApproval(requestId, { behavior: 'allow', updatedInput: pending.input }, 'approved');
655
+ }
656
+ denyAllPendingApprovals(message) {
657
+ for (const requestId of Array.from(this.pendingApprovals.keys())) {
658
+ this.answerApproval(requestId, { behavior: 'deny', message }, 'expired');
659
+ }
660
+ }
661
+ clearApprovals(message) {
662
+ this.approvalGeneration++;
663
+ this.approvalTurnActive = false;
664
+ this.currentApprovalRequestCallback = undefined;
665
+ this.currentApprovalResolvedCallback = undefined;
666
+ this.denyAllPendingApprovals(message);
667
+ }
668
+ getSandboxStatus() {
669
+ return this.sandbox.describe(this.currentWorkingDirectory);
670
+ }
671
+ async configureSandbox(command) {
672
+ if (this.isProcessing || this.commandQueue.length > 0) {
673
+ return { success: false, error: 'Wait for the running task before changing sandbox settings.' };
674
+ }
675
+ try {
676
+ const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(command.trim());
677
+ const action = match?.[1] ?? '';
678
+ const value = match?.[2] ?? '';
679
+ const config = this.sandbox.getConfig() ?? { mode: 'workspace-write' };
680
+ let next = { ...config };
681
+ if (action === 'default' && !value)
682
+ next = undefined;
683
+ else if (['on', 'off', 'read-only'].includes(action) && !value) {
684
+ next.mode = action === 'on' ? 'workspace-write' : action === 'off' ? 'danger-full-access' : 'read-only';
685
+ }
686
+ else if (action === 'network' && ['on', 'off'].includes(value))
687
+ next.networkAccess = value === 'on';
688
+ else if (['allow', 'remove'].includes(action) && value) {
689
+ const directory = this.sandbox.normalize(value, this.currentWorkingDirectory);
690
+ next.writableRoots = action === 'allow'
691
+ ? [...new Set([...(config.writableRoots ?? []), directory])]
692
+ : (config.writableRoots ?? []).filter(root => root !== directory);
693
+ if (action === 'remove' && next.writableRoots.length === (config.writableRoots ?? []).length) {
694
+ return { success: false, error: 'That directory is not an extra grant.' };
695
+ }
696
+ }
697
+ else
698
+ return { success: false, error: 'Usage: /sandbox on|off|read-only|default; /sandbox network on|off; /sandbox allow|remove <directory>' };
699
+ this.sandbox.configure(next);
700
+ // Sandbox settings are spawn-time arguments; restart to apply. The saved
701
+ // session id keeps the conversation intact across the respawn.
702
+ await this.stopProcess();
703
+ return { success: true, output: this.getSandboxStatus() };
704
+ }
705
+ catch (error) {
706
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
707
+ }
708
+ }
428
709
  /**
429
710
  * Reset the command timeout timer when activity is detected
430
711
  * This prevents timeout during long-running tasks with continuous output
@@ -866,6 +1147,16 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
866
1147
  * This is called by MessageHandler when user sends a message while waiting for input
867
1148
  */
868
1149
  sendInput(input) {
1150
+ if (this.pendingApprovals.size > 0) {
1151
+ const match = /^(yes|y|approve|accept|no|n|deny|decline|cancel)(?:\s+(\S+))?$/i.exec(input.trim());
1152
+ const requestId = match?.[2] ?? (this.pendingApprovals.size === 1 ? this.pendingApprovals.keys().next().value : undefined);
1153
+ if (!match || !requestId || !this.pendingApprovals.has(requestId)) {
1154
+ this.currentStreamCallback?.(`\nReply yes or no for a single approval, or specify yes <request ID> / no <request ID>:\n${Array.from(this.pendingApprovals, ([id, pending]) => `${id}: ${pending.description}`).join('\n')}\n`);
1155
+ return false;
1156
+ }
1157
+ const action = ['yes', 'y', 'approve', 'accept'].includes(match[1].toLowerCase()) ? 'approve' : 'deny';
1158
+ return this.respondToApproval(requestId, action);
1159
+ }
869
1160
  if (!this.claudeProcess || !this.isWaitingForInput) {
870
1161
  console.log('[ClaudePersistent] Cannot send input - process not running or not waiting for input');
871
1162
  return false;
@@ -892,7 +1183,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
892
1183
  * Check if currently waiting for user input
893
1184
  */
894
1185
  isWaitingInput() {
895
- return this.isWaitingForInput;
1186
+ return this.pendingApprovals.size > 0 || this.isWaitingForInput;
896
1187
  }
897
1188
  /**
898
1189
  * Complete the current command
@@ -958,6 +1249,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
958
1249
  * Reset current command state
959
1250
  */
960
1251
  resetCurrentCommand() {
1252
+ this.clearApprovals('Task is no longer active');
961
1253
  this.currentOutputBuffer = [];
962
1254
  this.textStreams.clear();
963
1255
  this.currentStreamCallback = undefined;
@@ -984,7 +1276,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
984
1276
  * Process the command queue
985
1277
  */
986
1278
  async processQueue() {
987
- if (this.isProcessing || this.commandQueue.length === 0) {
1279
+ if (this.isProcessing || this.isStarting || this.commandQueue.length === 0) {
988
1280
  return;
989
1281
  }
990
1282
  // Ensure process is running
@@ -1022,6 +1314,9 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
1022
1314
  this.currentToolResultCallback = command.options.onToolResult;
1023
1315
  this.currentRedactedThinkingCallback = command.options.onRedactedThinking;
1024
1316
  this.currentPlanModeCallback = command.options.onPlanMode;
1317
+ this.currentApprovalRequestCallback = command.options.onApprovalRequest;
1318
+ this.currentApprovalResolvedCallback = command.options.onApprovalResolved;
1319
+ this.approvalTurnActive = true;
1025
1320
  this.currentCommandResolve = command.resolve;
1026
1321
  this.currentCommandReject = command.reject;
1027
1322
  // Reset output buffer: Claude Code 2.x can emit autonomous turns while
@@ -1122,7 +1417,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
1122
1417
  * Stops the current process and rejects the pending command
1123
1418
  */
1124
1419
  async abort() {
1125
- if (!this.isProcessing && !this.isWaitingForInput) {
1420
+ if (!this.isProcessing && !this.isWaitingInput()) {
1126
1421
  console.log('[ClaudePersistent] No command is currently executing');
1127
1422
  return false;
1128
1423
  }
@@ -1136,6 +1431,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
1136
1431
  }
1137
1432
  }
1138
1433
  console.log('[ClaudePersistent] Aborting current command...');
1434
+ this.denyAllPendingApprovals('Task aborted by user');
1139
1435
  // Emit task aborted hook before stopping
1140
1436
  if (this.currentTaskId) {
1141
1437
  ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskAborted({
@@ -1268,7 +1564,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
1268
1564
  await this.stopProcess();
1269
1565
  // Step 2: Run compact as a one-shot process outside the full context
1270
1566
  const compactResult = await new Promise((resolve) => {
1271
- const child = (0, child_process_1.spawn)('claude', ['--resume', sessionIdBeforeCompact, '--print', '/compact'], {
1567
+ const child = (0, child_process_1.spawn)(this.claudeCommand, ['--resume', sessionIdBeforeCompact, '--print', '/compact'], {
1272
1568
  cwd: this.currentWorkingDirectory,
1273
1569
  stdio: ['ignore', 'pipe', 'pipe'],
1274
1570
  env: { ...process.env, FORCE_COLOR: '0', CLAUDECODE: '', CLAUDE_CODE: '' },
@@ -1317,6 +1613,13 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
1317
1613
  command.reject(new Error('Executor has been destroyed'));
1318
1614
  }
1319
1615
  this.commandQueue = [];
1616
+ this.clearApprovals('Executor destroyed');
1617
+ if (this.approvalServer) {
1618
+ const server = this.approvalServer;
1619
+ this.approvalServer = null;
1620
+ this.approvalSocketPath = undefined;
1621
+ await new Promise(resolve => server.close(() => resolve()));
1622
+ }
1320
1623
  if (this.currentCommandReject) {
1321
1624
  this.currentCommandReject(new Error('Executor has been destroyed'));
1322
1625
  this.resetCurrentCommand();
@@ -1338,6 +1641,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
1338
1641
  catch {
1339
1642
  // File may not exist — not an error
1340
1643
  }
1644
+ this.sandbox.deleteData();
1341
1645
  }
1342
1646
  /**
1343
1647
  * Check if process is running