@yu_robotics/remote-cli 1.6.49 → 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.
- package/README.md +18 -8
- package/dist/client/MessageHandler.d.ts +8 -0
- package/dist/client/MessageHandler.d.ts.map +1 -1
- package/dist/client/MessageHandler.js +108 -3
- package/dist/client/MessageHandler.js.map +1 -1
- package/dist/client/TaskRecovery.d.ts +35 -0
- package/dist/client/TaskRecovery.d.ts.map +1 -0
- package/dist/client/TaskRecovery.js +237 -0
- package/dist/client/TaskRecovery.js.map +1 -0
- package/dist/client/WebSocketClient.d.ts +6 -0
- package/dist/client/WebSocketClient.d.ts.map +1 -1
- package/dist/client/WebSocketClient.js +39 -11
- package/dist/client/WebSocketClient.js.map +1 -1
- package/dist/commands/start.d.ts +3 -11
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +22 -99
- package/dist/commands/start.js.map +1 -1
- package/dist/executor/ClaudePersistentExecutor.d.ts +38 -2
- package/dist/executor/ClaudePersistentExecutor.d.ts.map +1 -1
- package/dist/executor/ClaudePersistentExecutor.js +369 -21
- package/dist/executor/ClaudePersistentExecutor.js.map +1 -1
- package/dist/executor/CodexAppServerExecutor.d.ts +13 -0
- package/dist/executor/CodexAppServerExecutor.d.ts.map +1 -1
- package/dist/executor/CodexAppServerExecutor.js +208 -5
- package/dist/executor/CodexAppServerExecutor.js.map +1 -1
- package/dist/executor/CodexSandbox.d.ts +23 -0
- package/dist/executor/CodexSandbox.d.ts.map +1 -0
- package/dist/executor/CodexSandbox.js +211 -0
- package/dist/executor/CodexSandbox.js.map +1 -0
- package/dist/executor/IExecutor.d.ts +5 -1
- package/dist/executor/IExecutor.d.ts.map +1 -1
- package/dist/executor/claude/ClaudeFilePolicy.d.ts +7 -0
- package/dist/executor/claude/ClaudeFilePolicy.d.ts.map +1 -0
- package/dist/executor/claude/ClaudeFilePolicy.js +88 -0
- package/dist/executor/claude/ClaudeFilePolicy.js.map +1 -0
- package/dist/executor/claude/ClaudeSandbox.d.ts +32 -0
- package/dist/executor/claude/ClaudeSandbox.d.ts.map +1 -0
- package/dist/executor/claude/ClaudeSandbox.js +245 -0
- package/dist/executor/claude/ClaudeSandbox.js.map +1 -0
- package/dist/executor/claude/approvalMcpServer.d.ts +2 -0
- package/dist/executor/claude/approvalMcpServer.d.ts.map +1 -0
- package/dist/executor/claude/approvalMcpServer.js +159 -0
- package/dist/executor/claude/approvalMcpServer.js.map +1 -0
- package/dist/executor/claude/filePolicyHook.d.ts +2 -0
- package/dist/executor/claude/filePolicyHook.d.ts.map +1 -0
- package/dist/executor/claude/filePolicyHook.js +63 -0
- package/dist/executor/claude/filePolicyHook.js.map +1 -0
- package/dist/executor/index.d.ts.map +1 -1
- package/dist/executor/index.js +4 -3
- package/dist/executor/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/thread/ThreadManager.d.ts.map +1 -1
- package/dist/thread/ThreadManager.js +2 -0
- package/dist/thread/ThreadManager.js.map +1 -1
- package/dist/types/config.d.ts +32 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +53 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/update/AutomaticUpdater.d.ts +7 -1
- package/dist/update/AutomaticUpdater.d.ts.map +1 -1
- package/dist/update/AutomaticUpdater.js +25 -2
- package/dist/update/AutomaticUpdater.js.map +1 -1
- 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
|
|
@@ -37,6 +41,8 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
37
41
|
isProcessing = false;
|
|
38
42
|
// Output handling
|
|
39
43
|
currentOutputBuffer = [];
|
|
44
|
+
// Main-agent and subagent streams have independent message IDs and block indices.
|
|
45
|
+
textStreams = new Map();
|
|
40
46
|
currentStreamCallback;
|
|
41
47
|
currentToolUseCallback;
|
|
42
48
|
currentToolResultCallback;
|
|
@@ -59,12 +65,32 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
59
65
|
inputRequestCallbacks = [];
|
|
60
66
|
inputDetectionTimer;
|
|
61
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');
|
|
62
86
|
// Activity tracking for timeout extension (optional, can be disabled)
|
|
63
87
|
activityTrackingEnabled = false;
|
|
64
|
-
constructor(directoryGuard, initialWorkingDirectory, threadId, model) {
|
|
88
|
+
constructor(directoryGuard, initialWorkingDirectory, threadId, model, sandbox, claudeCommand) {
|
|
65
89
|
super();
|
|
66
90
|
this.directoryGuard = directoryGuard;
|
|
67
91
|
this.model = model;
|
|
92
|
+
this.sandbox = new ClaudeSandbox_1.ClaudeSandbox(directoryGuard, sandbox, threadId);
|
|
93
|
+
this.claudeCommand = claudeCommand ?? 'claude';
|
|
68
94
|
// Use provided working directory or fall back to process.cwd()
|
|
69
95
|
// If a working directory is provided, validate it first
|
|
70
96
|
if (initialWorkingDirectory) {
|
|
@@ -211,14 +237,48 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
211
237
|
}
|
|
212
238
|
// Build arguments
|
|
213
239
|
// Note: --output-format=stream-json requires --verbose
|
|
240
|
+
const restricted = this.sandbox.isRestricted();
|
|
214
241
|
const args = [
|
|
215
242
|
'--input-format=stream-json',
|
|
216
243
|
'--output-format=stream-json',
|
|
217
244
|
'--include-partial-messages',
|
|
218
245
|
'--verbose',
|
|
219
|
-
'--dangerously-skip-permissions',
|
|
220
|
-
'--disallowedTools=AskUserQuestion'
|
|
221
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');
|
|
222
282
|
if (this.model) {
|
|
223
283
|
args.push('--model', this.model);
|
|
224
284
|
}
|
|
@@ -229,10 +289,10 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
229
289
|
else {
|
|
230
290
|
console.log('[ClaudePersistent] Starting new session');
|
|
231
291
|
}
|
|
232
|
-
console.log(`[ClaudePersistent] Starting:
|
|
292
|
+
console.log(`[ClaudePersistent] Starting: ${this.claudeCommand} ${args.join(' ')}`);
|
|
233
293
|
console.log(`[ClaudePersistent] Working directory: ${this.currentWorkingDirectory}`);
|
|
234
294
|
// Spawn the process
|
|
235
|
-
const child = (0, child_process_1.spawn)(
|
|
295
|
+
const child = (0, child_process_1.spawn)(this.claudeCommand, args, {
|
|
236
296
|
cwd: this.currentWorkingDirectory,
|
|
237
297
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
238
298
|
env: {
|
|
@@ -297,6 +357,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
297
357
|
// Use 'close' event (fires after all I/O streams are closed, i.e. after all stdout
|
|
298
358
|
// data events have been processed) instead of 'exit' for command completion logic.
|
|
299
359
|
child.on('close', (code, signal) => {
|
|
360
|
+
this.clearApprovals('Claude process exited');
|
|
300
361
|
this.claudeProcess = null;
|
|
301
362
|
// Check if this was an intentional stop (abort/reset)
|
|
302
363
|
const wasIntentionalStop = this.isStopping;
|
|
@@ -361,9 +422,21 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
361
422
|
}
|
|
362
423
|
throw err;
|
|
363
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
|
+
}
|
|
364
436
|
// Long-running error handler for errors that occur after startup
|
|
365
437
|
child.on('error', (error) => {
|
|
366
438
|
console.error('[ClaudePersistent] Process error:', error);
|
|
439
|
+
this.clearApprovals('Claude process failed');
|
|
367
440
|
this.claudeProcess = null;
|
|
368
441
|
this.isStarting = false;
|
|
369
442
|
if (this.currentCommandReject) {
|
|
@@ -395,6 +468,10 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
395
468
|
* Stop the persistent Claude process
|
|
396
469
|
*/
|
|
397
470
|
async stopProcess() {
|
|
471
|
+
this.filePolicyToken = undefined;
|
|
472
|
+
this.filePolicyRoots = [];
|
|
473
|
+
this.filePolicyStarted = false;
|
|
474
|
+
this.clearApprovals('Claude process stopped');
|
|
398
475
|
if (!this.claudeProcess) {
|
|
399
476
|
return;
|
|
400
477
|
}
|
|
@@ -423,6 +500,212 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
423
500
|
this.claudeProcess = null;
|
|
424
501
|
console.log('[ClaudePersistent] Process stopped');
|
|
425
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
|
+
}
|
|
426
709
|
/**
|
|
427
710
|
* Reset the command timeout timer when activity is detected
|
|
428
711
|
* This prevents timeout during long-running tasks with continuous output
|
|
@@ -448,6 +731,55 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
448
731
|
this.completeCurrentCommand(false, 'Execution timeout: No response from Claude for 10 minutes');
|
|
449
732
|
}, this.defaultTimeout);
|
|
450
733
|
}
|
|
734
|
+
emitText(text) {
|
|
735
|
+
if (!text)
|
|
736
|
+
return;
|
|
737
|
+
this.currentOutputBuffer.push(text);
|
|
738
|
+
if (this.isInPlanMode)
|
|
739
|
+
this.planModeBuffer.push(text);
|
|
740
|
+
this.currentStreamCallback?.(text);
|
|
741
|
+
}
|
|
742
|
+
handleStreamEvent(message) {
|
|
743
|
+
const event = message.event;
|
|
744
|
+
if (!event || !this.isProcessing)
|
|
745
|
+
return;
|
|
746
|
+
const source = message.parent_tool_use_id ?? '';
|
|
747
|
+
if (event.type === 'message_start') {
|
|
748
|
+
this.textStreams.delete(source);
|
|
749
|
+
if (event.message?.id) {
|
|
750
|
+
this.textStreams.set(source, { messageId: event.message.id, blocks: new Map() });
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const stream = this.textStreams.get(source);
|
|
755
|
+
if (!stream || event.index === undefined)
|
|
756
|
+
return;
|
|
757
|
+
if (event.type === 'content_block_start' && event.content_block?.type === 'text') {
|
|
758
|
+
const text = event.content_block.text ?? '';
|
|
759
|
+
stream.blocks.set(event.index, text);
|
|
760
|
+
this.emitText(text);
|
|
761
|
+
}
|
|
762
|
+
else if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta'
|
|
763
|
+
&& stream.blocks.has(event.index)) {
|
|
764
|
+
const text = event.delta.text ?? '';
|
|
765
|
+
stream.blocks.set(event.index, stream.blocks.get(event.index) + text);
|
|
766
|
+
this.emitText(text);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
unstreamedText(message, text) {
|
|
770
|
+
const stream = this.textStreams.get(message.parent_tool_use_id ?? '');
|
|
771
|
+
if (!stream || stream.messageId !== message.message?.id)
|
|
772
|
+
return text;
|
|
773
|
+
// Claude emits complete assistant blocks without their original stream index.
|
|
774
|
+
// Consume each matching block once, preserving repeated text in later blocks.
|
|
775
|
+
for (const [index, streamedText] of stream.blocks) {
|
|
776
|
+
if (text.startsWith(streamedText)) {
|
|
777
|
+
stream.blocks.delete(index);
|
|
778
|
+
return text.slice(streamedText.length);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return text;
|
|
782
|
+
}
|
|
451
783
|
/**
|
|
452
784
|
* Handle a line of JSON output from Claude
|
|
453
785
|
*/
|
|
@@ -456,7 +788,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
456
788
|
// Parse message first to check type
|
|
457
789
|
const parsedMessage = JSON.parse(line);
|
|
458
790
|
// Skip logging for stream_event messages to avoid console spam
|
|
459
|
-
//
|
|
791
|
+
// Token deltas are handled below without logging every fragment.
|
|
460
792
|
if (parsedMessage.type !== 'stream_event') {
|
|
461
793
|
const timestamp = new Date().toISOString();
|
|
462
794
|
console.log(`[ClaudePersistent RAW ${timestamp}] ${line}`);
|
|
@@ -530,8 +862,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
530
862
|
}
|
|
531
863
|
break;
|
|
532
864
|
case 'stream_event':
|
|
533
|
-
|
|
534
|
-
// These include content_block_start, content_block_delta, etc.
|
|
865
|
+
this.handleStreamEvent(message);
|
|
535
866
|
break;
|
|
536
867
|
case 'result':
|
|
537
868
|
// Result messages contain the final response and completion status
|
|
@@ -625,15 +956,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
625
956
|
// with tool_result content. We should emit the hook when we receive tool_result.
|
|
626
957
|
}
|
|
627
958
|
else if (block.type === 'text' && block.text) {
|
|
628
|
-
|
|
629
|
-
this.currentOutputBuffer.push(block.text);
|
|
630
|
-
if (this.isInPlanMode) {
|
|
631
|
-
// Buffer plan content instead of streaming; still stream for real-time visibility
|
|
632
|
-
this.planModeBuffer.push(block.text);
|
|
633
|
-
}
|
|
634
|
-
if (this.currentStreamCallback) {
|
|
635
|
-
this.currentStreamCallback(block.text);
|
|
636
|
-
}
|
|
959
|
+
this.emitText(this.unstreamedText(message, block.text));
|
|
637
960
|
}
|
|
638
961
|
else if (block.type === 'redacted_thinking') {
|
|
639
962
|
// Handle redacted thinking blocks (Claude 3.7 Sonnet, Gemini models)
|
|
@@ -824,6 +1147,16 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
824
1147
|
* This is called by MessageHandler when user sends a message while waiting for input
|
|
825
1148
|
*/
|
|
826
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
|
+
}
|
|
827
1160
|
if (!this.claudeProcess || !this.isWaitingForInput) {
|
|
828
1161
|
console.log('[ClaudePersistent] Cannot send input - process not running or not waiting for input');
|
|
829
1162
|
return false;
|
|
@@ -850,7 +1183,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
850
1183
|
* Check if currently waiting for user input
|
|
851
1184
|
*/
|
|
852
1185
|
isWaitingInput() {
|
|
853
|
-
return this.isWaitingForInput;
|
|
1186
|
+
return this.pendingApprovals.size > 0 || this.isWaitingForInput;
|
|
854
1187
|
}
|
|
855
1188
|
/**
|
|
856
1189
|
* Complete the current command
|
|
@@ -916,7 +1249,9 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
916
1249
|
* Reset current command state
|
|
917
1250
|
*/
|
|
918
1251
|
resetCurrentCommand() {
|
|
1252
|
+
this.clearApprovals('Task is no longer active');
|
|
919
1253
|
this.currentOutputBuffer = [];
|
|
1254
|
+
this.textStreams.clear();
|
|
920
1255
|
this.currentStreamCallback = undefined;
|
|
921
1256
|
this.currentToolUseCallback = undefined;
|
|
922
1257
|
this.currentToolResultCallback = undefined;
|
|
@@ -941,7 +1276,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
941
1276
|
* Process the command queue
|
|
942
1277
|
*/
|
|
943
1278
|
async processQueue() {
|
|
944
|
-
if (this.isProcessing || this.commandQueue.length === 0) {
|
|
1279
|
+
if (this.isProcessing || this.isStarting || this.commandQueue.length === 0) {
|
|
945
1280
|
return;
|
|
946
1281
|
}
|
|
947
1282
|
// Ensure process is running
|
|
@@ -979,6 +1314,9 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
979
1314
|
this.currentToolResultCallback = command.options.onToolResult;
|
|
980
1315
|
this.currentRedactedThinkingCallback = command.options.onRedactedThinking;
|
|
981
1316
|
this.currentPlanModeCallback = command.options.onPlanMode;
|
|
1317
|
+
this.currentApprovalRequestCallback = command.options.onApprovalRequest;
|
|
1318
|
+
this.currentApprovalResolvedCallback = command.options.onApprovalResolved;
|
|
1319
|
+
this.approvalTurnActive = true;
|
|
982
1320
|
this.currentCommandResolve = command.resolve;
|
|
983
1321
|
this.currentCommandReject = command.reject;
|
|
984
1322
|
// Reset output buffer: Claude Code 2.x can emit autonomous turns while
|
|
@@ -987,6 +1325,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
987
1325
|
// guard, which returns early without clearing it — so without this reset
|
|
988
1326
|
// the stale text would leak into this command's final output.
|
|
989
1327
|
this.currentOutputBuffer = [];
|
|
1328
|
+
this.textStreams.clear();
|
|
990
1329
|
// Reset structured content collection
|
|
991
1330
|
this.structuredContentBlocks = [];
|
|
992
1331
|
// Reset plan mode state
|
|
@@ -1078,7 +1417,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1078
1417
|
* Stops the current process and rejects the pending command
|
|
1079
1418
|
*/
|
|
1080
1419
|
async abort() {
|
|
1081
|
-
if (!this.isProcessing && !this.
|
|
1420
|
+
if (!this.isProcessing && !this.isWaitingInput()) {
|
|
1082
1421
|
console.log('[ClaudePersistent] No command is currently executing');
|
|
1083
1422
|
return false;
|
|
1084
1423
|
}
|
|
@@ -1092,6 +1431,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1092
1431
|
}
|
|
1093
1432
|
}
|
|
1094
1433
|
console.log('[ClaudePersistent] Aborting current command...');
|
|
1434
|
+
this.denyAllPendingApprovals('Task aborted by user');
|
|
1095
1435
|
// Emit task aborted hook before stopping
|
|
1096
1436
|
if (this.currentTaskId) {
|
|
1097
1437
|
ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskAborted({
|
|
@@ -1224,7 +1564,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1224
1564
|
await this.stopProcess();
|
|
1225
1565
|
// Step 2: Run compact as a one-shot process outside the full context
|
|
1226
1566
|
const compactResult = await new Promise((resolve) => {
|
|
1227
|
-
const child = (0, child_process_1.spawn)(
|
|
1567
|
+
const child = (0, child_process_1.spawn)(this.claudeCommand, ['--resume', sessionIdBeforeCompact, '--print', '/compact'], {
|
|
1228
1568
|
cwd: this.currentWorkingDirectory,
|
|
1229
1569
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1230
1570
|
env: { ...process.env, FORCE_COLOR: '0', CLAUDECODE: '', CLAUDE_CODE: '' },
|
|
@@ -1273,6 +1613,13 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1273
1613
|
command.reject(new Error('Executor has been destroyed'));
|
|
1274
1614
|
}
|
|
1275
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
|
+
}
|
|
1276
1623
|
if (this.currentCommandReject) {
|
|
1277
1624
|
this.currentCommandReject(new Error('Executor has been destroyed'));
|
|
1278
1625
|
this.resetCurrentCommand();
|
|
@@ -1294,6 +1641,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1294
1641
|
catch {
|
|
1295
1642
|
// File may not exist — not an error
|
|
1296
1643
|
}
|
|
1644
|
+
this.sandbox.deleteData();
|
|
1297
1645
|
}
|
|
1298
1646
|
/**
|
|
1299
1647
|
* Check if process is running
|