@yu_robotics/remote-cli 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,697 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CodexAppServerExecutor = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const CodexAppServerClient_1 = require("./CodexAppServerClient");
41
+ const DEFAULT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
42
+ const DEFAULT_COMPACT_TIMEOUT_MS = 10 * 60 * 1000;
43
+ /**
44
+ * Persistent Codex executor backed by the official app-server protocol.
45
+ * One instance is owned by one remote-cli thread, preserving the existing
46
+ * ThreadExecutorPool isolation model.
47
+ */
48
+ class CodexAppServerExecutor {
49
+ directoryGuard;
50
+ currentWorkingDirectory;
51
+ model;
52
+ autoApprove;
53
+ remoteThreadId;
54
+ sessionFilePath;
55
+ inactivityTimeoutMs;
56
+ compactTimeoutMs;
57
+ client;
58
+ unsubscribeClient = null;
59
+ codexThreadId = null;
60
+ threadReady = false;
61
+ activeTurn = null;
62
+ compactWaiter = null;
63
+ pendingUserInputs = [];
64
+ inactivityTimer = null;
65
+ destroyed = false;
66
+ constructor(directoryGuard, options = {}) {
67
+ this.directoryGuard = directoryGuard;
68
+ this.model = options.model;
69
+ this.autoApprove = options.autoApprove ?? true;
70
+ this.remoteThreadId = options.threadId;
71
+ this.inactivityTimeoutMs = options.inactivityTimeoutMs ?? DEFAULT_INACTIVITY_TIMEOUT_MS;
72
+ this.compactTimeoutMs = options.compactTimeoutMs ?? DEFAULT_COMPACT_TIMEOUT_MS;
73
+ if (options.initialWorkingDirectory) {
74
+ try {
75
+ this.currentWorkingDirectory = directoryGuard.resolveWorkingDirectory(options.initialWorkingDirectory);
76
+ }
77
+ catch (error) {
78
+ console.warn(`[CodexAppServerExecutor] Failed to use initial working directory: ${options.initialWorkingDirectory}`, error);
79
+ this.currentWorkingDirectory = process.cwd();
80
+ }
81
+ }
82
+ else {
83
+ this.currentWorkingDirectory = process.cwd();
84
+ }
85
+ if (this.remoteThreadId) {
86
+ const sessionsDir = path.join(process.env.HOME || os.homedir(), '.remote-cli', 'codex-sessions');
87
+ fs.mkdirSync(sessionsDir, { recursive: true });
88
+ this.sessionFilePath = path.join(sessionsDir, `${this.remoteThreadId}.json`);
89
+ }
90
+ else {
91
+ this.sessionFilePath = path.join(this.currentWorkingDirectory, '.codex-session');
92
+ }
93
+ this.loadThreadId();
94
+ this.client = options.clientFactory
95
+ ? options.clientFactory(this.currentWorkingDirectory)
96
+ : new CodexAppServerClient_1.CodexAppServerClient({
97
+ command: options.codexCommand,
98
+ cwd: this.currentWorkingDirectory,
99
+ });
100
+ this.unsubscribeClient = this.client.onMessage((message) => this.handleMessage(message));
101
+ }
102
+ async execute(prompt, options = {}) {
103
+ if (this.destroyed)
104
+ throw new Error('Executor has been destroyed');
105
+ if (this.activeTurn || this.compactWaiter) {
106
+ return { success: false, error: 'Codex thread is already running a command' };
107
+ }
108
+ try {
109
+ await this.ensureThread();
110
+ }
111
+ catch (error) {
112
+ return { success: false, error: this.errorMessage(error) };
113
+ }
114
+ const temporaryFiles = [];
115
+ try {
116
+ const input = await this.buildInput(prompt, options.attachments, temporaryFiles);
117
+ return await new Promise((resolve) => {
118
+ const active = {
119
+ options,
120
+ resolve,
121
+ turnId: null,
122
+ output: [],
123
+ sideEffectsStarted: false,
124
+ emittedTools: new Set(),
125
+ temporaryFiles,
126
+ };
127
+ if (options.timeout && options.timeout > 0) {
128
+ active.timeoutTimer = setTimeout(() => {
129
+ void this.failAndRestart(`Command timed out after ${options.timeout}ms`);
130
+ }, options.timeout);
131
+ }
132
+ this.activeTurn = active;
133
+ this.armInactivityTimer();
134
+ const params = {
135
+ threadId: this.codexThreadId,
136
+ input,
137
+ cwd: this.currentWorkingDirectory,
138
+ ...(this.model ? { model: this.model } : {}),
139
+ ...(this.autoApprove ? {
140
+ approvalPolicy: 'never',
141
+ sandboxPolicy: { type: 'dangerFullAccess' },
142
+ } : {}),
143
+ };
144
+ void this.client.request('turn/start', params).then((response) => {
145
+ if (this.activeTurn !== active)
146
+ return;
147
+ const turnId = response?.turn?.id;
148
+ if (typeof turnId !== 'string' || !turnId) {
149
+ this.completeActive({ success: false, error: 'Codex app-server returned no turn ID' });
150
+ return;
151
+ }
152
+ active.turnId = turnId;
153
+ }).catch((error) => {
154
+ if (this.activeTurn === active) {
155
+ this.completeActive({ success: false, error: this.errorMessage(error) });
156
+ }
157
+ });
158
+ });
159
+ }
160
+ catch (error) {
161
+ await this.cleanupTemporaryFiles(temporaryFiles);
162
+ return { success: false, error: this.errorMessage(error) };
163
+ }
164
+ }
165
+ getCurrentWorkingDirectory() {
166
+ return this.currentWorkingDirectory;
167
+ }
168
+ async setWorkingDirectory(targetPath) {
169
+ this.currentWorkingDirectory = this.directoryGuard.resolveWorkingDirectory(targetPath, this.currentWorkingDirectory);
170
+ this.client.setWorkingDirectory(this.currentWorkingDirectory);
171
+ }
172
+ resetContext() {
173
+ const previousId = this.codexThreadId;
174
+ if (this.activeTurn || this.compactWaiter) {
175
+ this.completeActive({ success: false, error: 'Conversation cleared by user' });
176
+ this.completeCompact({ success: false, error: 'Conversation cleared by user' });
177
+ void this.client.stop();
178
+ }
179
+ this.codexThreadId = null;
180
+ this.threadReady = false;
181
+ this.pendingUserInputs = [];
182
+ this.clearThreadId();
183
+ if (previousId && this.client.isRunning()) {
184
+ void this.client.request('thread/unsubscribe', { threadId: previousId }).catch(() => undefined);
185
+ }
186
+ }
187
+ async abort() {
188
+ const active = this.activeTurn;
189
+ if (!active || !this.codexThreadId)
190
+ return false;
191
+ for (const pending of this.pendingUserInputs.splice(0)) {
192
+ this.client.respond(pending.id, pending.method === 'item/tool/requestUserInput'
193
+ ? { answers: {} }
194
+ : { decision: 'cancel' });
195
+ }
196
+ if (!active.turnId) {
197
+ await this.failAndRestart('Aborted by user');
198
+ return true;
199
+ }
200
+ try {
201
+ await this.client.request('turn/interrupt', {
202
+ threadId: this.codexThreadId,
203
+ turnId: active.turnId,
204
+ });
205
+ }
206
+ catch {
207
+ await this.failAndRestart('Aborted by user');
208
+ }
209
+ return true;
210
+ }
211
+ async destroy() {
212
+ if (this.destroyed)
213
+ return;
214
+ this.destroyed = true;
215
+ this.unsubscribeClient?.();
216
+ this.unsubscribeClient = null;
217
+ if (this.activeTurn)
218
+ this.completeActive({ success: false, error: 'Executor destroyed' });
219
+ this.completeCompact({ success: false, error: 'Executor destroyed' });
220
+ await this.client.stop();
221
+ }
222
+ isProcessRunning() {
223
+ return this.client.isRunning();
224
+ }
225
+ getSessionId() {
226
+ return this.codexThreadId;
227
+ }
228
+ isWaitingInput() {
229
+ return this.pendingUserInputs.length > 0;
230
+ }
231
+ sendInput(input) {
232
+ const pending = this.pendingUserInputs.shift();
233
+ if (!pending)
234
+ return false;
235
+ const normalized = input.trim().toLowerCase();
236
+ if (pending.method === 'item/tool/requestUserInput') {
237
+ const questions = Array.isArray(pending.params?.questions) ? pending.params.questions : [];
238
+ const answers = {};
239
+ for (const question of questions) {
240
+ if (typeof question?.id === 'string')
241
+ answers[question.id] = { answers: [input.trim()] };
242
+ }
243
+ this.client.respond(pending.id, { answers });
244
+ return true;
245
+ }
246
+ const decision = normalized === 'always' ? 'acceptForSession'
247
+ : normalized === 'yes' || normalized === 'y' || normalized === 'accept' ? 'accept'
248
+ : normalized === 'cancel' ? 'cancel'
249
+ : normalized === 'no' || normalized === 'n' || normalized === 'decline' ? 'decline'
250
+ : null;
251
+ if (!decision) {
252
+ this.pendingUserInputs.unshift(pending);
253
+ return false;
254
+ }
255
+ this.client.respond(pending.id, { decision });
256
+ return true;
257
+ }
258
+ async listModels() {
259
+ const models = [];
260
+ let cursor = null;
261
+ do {
262
+ const response = await this.client.request('model/list', {
263
+ cursor,
264
+ limit: 100,
265
+ includeHidden: false,
266
+ });
267
+ for (const model of Array.isArray(response?.data) ? response.data : []) {
268
+ if (typeof model?.id !== 'string')
269
+ continue;
270
+ models.push({
271
+ id: model.id,
272
+ displayName: model.displayName ?? model.id,
273
+ description: model.description,
274
+ isDefault: model.isDefault === true,
275
+ supportedReasoningEfforts: Array.isArray(model.supportedReasoningEfforts)
276
+ ? model.supportedReasoningEfforts.map((entry) => entry?.reasoningEffort).filter(Boolean)
277
+ : undefined,
278
+ inputModalities: Array.isArray(model.inputModalities) ? model.inputModalities : undefined,
279
+ });
280
+ }
281
+ cursor = typeof response?.nextCursor === 'string' ? response.nextCursor : null;
282
+ } while (cursor);
283
+ return models;
284
+ }
285
+ async setModel(model) {
286
+ try {
287
+ const models = await this.listModels();
288
+ if (!models.some((entry) => entry.id === model)) {
289
+ return { success: false, error: `Unknown Codex model: ${model}. Use /model to list available models.` };
290
+ }
291
+ this.model = model;
292
+ return { success: true, output: `Model set to ${model}. Takes effect on the next command.` };
293
+ }
294
+ catch (error) {
295
+ return { success: false, error: this.errorMessage(error) };
296
+ }
297
+ }
298
+ clearModel() {
299
+ this.model = undefined;
300
+ }
301
+ async compactWhenFull(_onStream) {
302
+ if (!this.codexThreadId)
303
+ return { success: true, output: 'No active conversation to compact.' };
304
+ if (this.activeTurn || this.compactWaiter) {
305
+ return { success: false, error: 'Cannot compact while the Codex thread is busy' };
306
+ }
307
+ try {
308
+ await this.ensureThread();
309
+ return await new Promise((resolve) => {
310
+ const timer = setTimeout(() => {
311
+ this.completeCompact({ success: false, error: 'Codex compaction timed out' });
312
+ }, this.compactTimeoutMs);
313
+ this.compactWaiter = { resolve, timer };
314
+ void this.client.request('thread/compact/start', { threadId: this.codexThreadId }).catch((error) => {
315
+ this.completeCompact({ success: false, error: this.errorMessage(error) });
316
+ });
317
+ });
318
+ }
319
+ catch (error) {
320
+ return { success: false, error: this.errorMessage(error) };
321
+ }
322
+ }
323
+ async deleteThreadData(_threadId) {
324
+ await this.destroy();
325
+ this.clearThreadId();
326
+ this.codexThreadId = null;
327
+ this.threadReady = false;
328
+ }
329
+ async ensureThread() {
330
+ if (this.threadReady && this.client.isRunning())
331
+ return;
332
+ await this.client.start();
333
+ const common = {
334
+ ...(this.model ? { model: this.model } : {}),
335
+ cwd: this.currentWorkingDirectory,
336
+ ...(this.autoApprove ? {
337
+ approvalPolicy: 'never',
338
+ sandbox: 'danger-full-access',
339
+ } : {}),
340
+ };
341
+ if (this.codexThreadId) {
342
+ try {
343
+ const response = await this.client.request('thread/resume', {
344
+ threadId: this.codexThreadId,
345
+ excludeTurns: true,
346
+ ...common,
347
+ });
348
+ if (response?.thread?.id !== this.codexThreadId) {
349
+ throw new Error('Codex app-server resumed an unexpected thread');
350
+ }
351
+ }
352
+ catch (error) {
353
+ throw new Error(`Stored Codex thread could not be resumed: ${this.errorMessage(error)}. Use /clear to start fresh.`);
354
+ }
355
+ }
356
+ else {
357
+ const response = await this.client.request('thread/start', {
358
+ ...common,
359
+ serviceName: 'remote_cli',
360
+ });
361
+ const id = response?.thread?.id;
362
+ if (typeof id !== 'string' || !id)
363
+ throw new Error('Codex app-server returned no thread ID');
364
+ this.setThreadId(id);
365
+ }
366
+ this.threadReady = true;
367
+ }
368
+ handleMessage(message) {
369
+ const method = message.method;
370
+ if (!method)
371
+ return;
372
+ const params = message.params ?? {};
373
+ if (method === 'client/disconnected') {
374
+ this.threadReady = false;
375
+ if (this.activeTurn) {
376
+ this.completeActive({ success: false, error: params.error ?? 'Codex app-server disconnected' });
377
+ }
378
+ this.completeCompact({ success: false, error: params.error ?? 'Codex app-server disconnected' });
379
+ return;
380
+ }
381
+ if (message.id !== undefined) {
382
+ this.handleServerRequest(message.id, method, params);
383
+ return;
384
+ }
385
+ if (params.threadId && params.threadId !== this.codexThreadId)
386
+ return;
387
+ if (this.activeTurn)
388
+ this.armInactivityTimer();
389
+ switch (method) {
390
+ case 'turn/started':
391
+ if (this.activeTurn && typeof params.turn?.id === 'string')
392
+ this.activeTurn.turnId = params.turn.id;
393
+ break;
394
+ case 'item/agentMessage/delta':
395
+ if (this.activeTurn && typeof params.delta === 'string') {
396
+ this.activeTurn.output.push(params.delta);
397
+ this.activeTurn.options.onStream?.(params.delta);
398
+ }
399
+ break;
400
+ case 'item/reasoning/summaryTextDelta':
401
+ if (this.activeTurn && typeof params.delta === 'string') {
402
+ this.activeTurn.options.onStream?.(params.delta);
403
+ }
404
+ break;
405
+ case 'turn/plan/updated':
406
+ if (this.activeTurn && Array.isArray(params.plan)) {
407
+ const plan = params.plan.map((entry) => `${entry.status === 'completed' ? '✅' : entry.status === 'inProgress' ? '🔄' : '⬜'} ${entry.step}`).join('\n');
408
+ if (plan)
409
+ this.activeTurn.options.onPlanMode?.(plan);
410
+ }
411
+ break;
412
+ case 'item/started':
413
+ this.handleItemStarted(params.item);
414
+ break;
415
+ case 'item/completed':
416
+ this.handleItemCompleted(params.item);
417
+ break;
418
+ case 'error': {
419
+ const error = params.error ?? params;
420
+ if (this.activeTurn) {
421
+ this.activeTurn.error = error?.message ?? 'Codex turn failed';
422
+ this.activeTurn.errorCode = this.extractErrorCode(error?.codexErrorInfo);
423
+ }
424
+ break;
425
+ }
426
+ case 'turn/completed':
427
+ this.handleTurnCompleted(params.turn);
428
+ break;
429
+ case 'thread/compacted':
430
+ if (this.compactWaiter)
431
+ this.completeCompact({ success: true });
432
+ break;
433
+ default:
434
+ break;
435
+ }
436
+ }
437
+ handleServerRequest(id, method, params) {
438
+ if (params.threadId && params.threadId !== this.codexThreadId) {
439
+ this.client.respondError(id, 'Request does not belong to this remote-cli thread');
440
+ return;
441
+ }
442
+ this.armInactivityTimer();
443
+ if (method === 'item/commandExecution/requestApproval' || method === 'item/fileChange/requestApproval') {
444
+ if (this.autoApprove) {
445
+ this.client.respond(id, { decision: 'accept' });
446
+ return;
447
+ }
448
+ this.pendingUserInputs.push({ id, method, params });
449
+ const subject = method.includes('commandExecution')
450
+ ? params.command ?? params.reason ?? 'command execution'
451
+ : params.reason ?? params.grantRoot ?? 'file changes';
452
+ this.activeTurn?.options.onStream?.(`\nApproval required for ${subject}. Reply yes, always, no, or cancel.\n`);
453
+ return;
454
+ }
455
+ if (method === 'item/tool/requestUserInput') {
456
+ this.pendingUserInputs.push({ id, method, params });
457
+ const questions = Array.isArray(params.questions) ? params.questions : [];
458
+ const text = questions.map((question) => question?.question).filter(Boolean).join('\n');
459
+ this.activeTurn?.options.onStream?.(`\n${text || 'Codex requires input.'}\nReply with your answer.\n`);
460
+ return;
461
+ }
462
+ this.client.respondError(id, `Unsupported Codex app-server request: ${method}`);
463
+ }
464
+ handleItemStarted(item) {
465
+ const active = this.activeTurn;
466
+ if (!active || !item || typeof item.id !== 'string')
467
+ return;
468
+ const id = item.id;
469
+ switch (item.type) {
470
+ case 'commandExecution':
471
+ active.sideEffectsStarted = true;
472
+ this.emitToolUse(active, id, 'Bash', { command: item.command ?? '' });
473
+ break;
474
+ case 'fileChange':
475
+ active.sideEffectsStarted = true;
476
+ this.emitToolUse(active, id, 'Edit', { file_path: item.changes?.[0]?.path ?? '' });
477
+ break;
478
+ case 'webSearch':
479
+ this.emitToolUse(active, id, 'WebSearch', { query: item.query ?? '' });
480
+ break;
481
+ case 'mcpToolCall':
482
+ case 'dynamicToolCall':
483
+ active.sideEffectsStarted = true;
484
+ this.emitToolUse(active, id, item.tool ?? 'MCP', item.arguments ?? {});
485
+ break;
486
+ default:
487
+ break;
488
+ }
489
+ }
490
+ handleItemCompleted(item) {
491
+ if (this.compactWaiter && item?.type === 'contextCompaction') {
492
+ this.completeCompact({ success: true });
493
+ return;
494
+ }
495
+ const active = this.activeTurn;
496
+ if (!active || !item || typeof item.id !== 'string')
497
+ return;
498
+ const id = item.id;
499
+ switch (item.type) {
500
+ case 'commandExecution':
501
+ this.emitToolUse(active, id, 'Bash', { command: item.command ?? '' });
502
+ active.options.onToolResult?.({
503
+ tool_use_id: id,
504
+ content: item.aggregatedOutput ?? '',
505
+ is_error: item.status !== 'completed' || (typeof item.exitCode === 'number' && item.exitCode !== 0),
506
+ });
507
+ break;
508
+ case 'fileChange': {
509
+ this.emitToolUse(active, id, 'Edit', { file_path: item.changes?.[0]?.path ?? '' });
510
+ const changes = Array.isArray(item.changes) ? item.changes : [];
511
+ active.options.onToolResult?.({
512
+ tool_use_id: id,
513
+ content: changes.map((change) => `${change.kind ?? 'update'}: ${change.path ?? ''}`).join('\n'),
514
+ is_error: item.status !== 'completed',
515
+ });
516
+ break;
517
+ }
518
+ case 'webSearch':
519
+ this.emitToolUse(active, id, 'WebSearch', { query: item.query ?? '' });
520
+ active.options.onToolResult?.({ tool_use_id: id, content: '', is_error: false });
521
+ break;
522
+ case 'mcpToolCall':
523
+ case 'dynamicToolCall':
524
+ this.emitToolUse(active, id, item.tool ?? 'MCP', item.arguments ?? {});
525
+ active.options.onToolResult?.({
526
+ tool_use_id: id,
527
+ content: this.stringifyResult(item.result ?? item.contentItems ?? item.error ?? ''),
528
+ is_error: item.status !== 'completed' || item.success === false || Boolean(item.error),
529
+ });
530
+ break;
531
+ case 'plan':
532
+ if (typeof item.text === 'string' && item.text)
533
+ active.options.onPlanMode?.(item.text);
534
+ break;
535
+ default:
536
+ break;
537
+ }
538
+ }
539
+ handleTurnCompleted(turn) {
540
+ const active = this.activeTurn;
541
+ if (!active || !turn)
542
+ return;
543
+ if (active.turnId && turn.id && active.turnId !== turn.id)
544
+ return;
545
+ if (turn.status === 'completed') {
546
+ this.completeActive({
547
+ success: true,
548
+ output: active.output.join(''),
549
+ sessionAbbr: this.codexThreadId?.slice(0, 8),
550
+ });
551
+ return;
552
+ }
553
+ if (turn.status === 'interrupted') {
554
+ this.completeActive({ success: false, error: 'Aborted by user' });
555
+ return;
556
+ }
557
+ const rawError = turn.error?.message ?? active.error ?? 'Codex turn failed';
558
+ const code = this.extractErrorCode(turn.error?.codexErrorInfo) ?? active.errorCode;
559
+ const error = code === 'ContextWindowExceeded' && !active.sideEffectsStarted
560
+ ? `Prompt too long: ${rawError}`
561
+ : rawError;
562
+ this.completeActive({ success: false, error });
563
+ }
564
+ emitToolUse(active, id, name, input) {
565
+ if (active.emittedTools.has(id))
566
+ return;
567
+ active.emittedTools.add(id);
568
+ active.options.onToolUse?.({ id, name, input });
569
+ }
570
+ completeActive(result) {
571
+ const active = this.activeTurn;
572
+ if (!active)
573
+ return;
574
+ this.activeTurn = null;
575
+ this.pendingUserInputs = [];
576
+ this.clearInactivityTimer();
577
+ if (active.timeoutTimer)
578
+ clearTimeout(active.timeoutTimer);
579
+ void this.cleanupTemporaryFiles(active.temporaryFiles);
580
+ active.resolve(result);
581
+ }
582
+ completeCompact(result) {
583
+ const waiter = this.compactWaiter;
584
+ if (!waiter)
585
+ return;
586
+ this.compactWaiter = null;
587
+ clearTimeout(waiter.timer);
588
+ waiter.resolve(result);
589
+ }
590
+ async failAndRestart(error) {
591
+ this.threadReady = false;
592
+ this.completeActive({ success: false, error });
593
+ this.completeCompact({ success: false, error });
594
+ await this.client.stop();
595
+ }
596
+ armInactivityTimer() {
597
+ this.clearInactivityTimer();
598
+ if (!this.activeTurn)
599
+ return;
600
+ this.inactivityTimer = setTimeout(() => {
601
+ void this.failAndRestart(`No output from Codex for ${this.inactivityTimeoutMs}ms (inactivity timeout)`);
602
+ }, this.inactivityTimeoutMs);
603
+ }
604
+ clearInactivityTimer() {
605
+ if (this.inactivityTimer)
606
+ clearTimeout(this.inactivityTimer);
607
+ this.inactivityTimer = null;
608
+ }
609
+ async buildInput(prompt, attachments, temporaryFiles) {
610
+ const input = [];
611
+ if (prompt)
612
+ input.push({ type: 'text', text: prompt, text_elements: [] });
613
+ for (const attachment of attachments ?? []) {
614
+ if (attachment.type !== 'image')
615
+ continue;
616
+ const extension = this.extensionForMimeType(attachment.mimeType);
617
+ const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'remote-cli-codex-image-'));
618
+ const filePath = path.join(directory, `attachment.${extension}`);
619
+ await fs.promises.writeFile(filePath, Buffer.from(attachment.data, 'base64'), { mode: 0o600 });
620
+ temporaryFiles.push(filePath);
621
+ input.push({ type: 'localImage', path: filePath });
622
+ }
623
+ return input;
624
+ }
625
+ async cleanupTemporaryFiles(files) {
626
+ const directories = new Set(files.map((file) => path.dirname(file)));
627
+ await Promise.all(Array.from(directories).map((directory) => fs.promises.rm(directory, { recursive: true, force: true }).catch(() => undefined)));
628
+ }
629
+ extensionForMimeType(mimeType) {
630
+ if (mimeType === 'image/jpeg')
631
+ return 'jpg';
632
+ if (mimeType === 'image/webp')
633
+ return 'webp';
634
+ if (mimeType === 'image/gif')
635
+ return 'gif';
636
+ return 'png';
637
+ }
638
+ loadThreadId() {
639
+ try {
640
+ const session = JSON.parse(fs.readFileSync(this.sessionFilePath, 'utf8'));
641
+ if (typeof session.id === 'string' && session.id)
642
+ this.codexThreadId = session.id;
643
+ }
644
+ catch {
645
+ this.codexThreadId = null;
646
+ }
647
+ }
648
+ setThreadId(id) {
649
+ this.codexThreadId = id;
650
+ const temporaryPath = `${this.sessionFilePath}.${process.pid}.tmp`;
651
+ const data = JSON.stringify({ id, savedAt: new Date().toISOString() });
652
+ try {
653
+ fs.writeFileSync(temporaryPath, data, { encoding: 'utf8', mode: 0o600 });
654
+ fs.renameSync(temporaryPath, this.sessionFilePath);
655
+ }
656
+ catch (error) {
657
+ try {
658
+ fs.unlinkSync(temporaryPath);
659
+ }
660
+ catch { /* Ignore cleanup failure. */ }
661
+ console.error('[CodexAppServerExecutor] Failed to save thread ID:', error);
662
+ }
663
+ }
664
+ clearThreadId() {
665
+ try {
666
+ fs.unlinkSync(this.sessionFilePath);
667
+ }
668
+ catch (error) {
669
+ if (error?.code !== 'ENOENT')
670
+ console.error('[CodexAppServerExecutor] Failed to delete session file:', error);
671
+ }
672
+ }
673
+ extractErrorCode(info) {
674
+ if (typeof info === 'string')
675
+ return info;
676
+ if (!info || typeof info !== 'object')
677
+ return undefined;
678
+ if (typeof info.type === 'string')
679
+ return info.type;
680
+ return Object.keys(info)[0];
681
+ }
682
+ stringifyResult(value) {
683
+ if (typeof value === 'string')
684
+ return value;
685
+ try {
686
+ return JSON.stringify(value).slice(0, 2_000);
687
+ }
688
+ catch {
689
+ return String(value);
690
+ }
691
+ }
692
+ errorMessage(error) {
693
+ return error instanceof Error ? error.message : String(error);
694
+ }
695
+ }
696
+ exports.CodexAppServerExecutor = CodexAppServerExecutor;
697
+ //# sourceMappingURL=CodexAppServerExecutor.js.map