@yu_robotics/remote-cli 1.2.0 → 1.4.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.
@@ -0,0 +1,696 @@
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.CodexExecutor = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const string_decoder_1 = require("string_decoder");
39
+ const fs = __importStar(require("fs"));
40
+ const os = __importStar(require("os"));
41
+ const path = __importStar(require("path"));
42
+ const compactHandoff_1 = require("./compactHandoff");
43
+ /** Default inactivity timeout (ms) — a silent codex process fails the command. */
44
+ const DEFAULT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
45
+ /** Default grace period (ms) before SIGTERM escalates to SIGKILL. */
46
+ const DEFAULT_KILL_ESCALATION_MS = 3_000;
47
+ /** How much stderr (chars) to keep for error diagnostics. */
48
+ const STDERR_TAIL_LIMIT = 4_000;
49
+ /** How much of the stderr tail (chars) to include in error messages. */
50
+ const STDERR_TAIL_IN_ERROR = 500;
51
+ /**
52
+ * IExecutor implementation for the OpenAI Codex CLI (`codex`), exec mode.
53
+ *
54
+ * Unlike AgyExecutor (one persistent process per conversation), codex exec
55
+ * is one-shot per command: each execute() spawns
56
+ * codex exec --json --skip-git-repo-check [--dangerously-bypass-approvals-and-sandbox] [-m model] "<prompt>"
57
+ * and resolves when the process exits. Context continuity is achieved by
58
+ * persisting the thread_id from `thread.started` and passing it to
59
+ * codex exec resume --skip-git-repo-check --json ... <thread_id> "<prompt>"
60
+ * on subsequent commands.
61
+ *
62
+ * Caveats (verified against codex-cli 0.153.4):
63
+ * - stdin must be ended immediately after spawn — codex reads piped stdin
64
+ * to EOF before starting ("Reading additional input from stdin...").
65
+ * - `codex exec resume` does NOT accept the --sandbox flag (usage error).
66
+ * We only ever pass --dangerously-bypass-approvals-and-sandbox, which is
67
+ * accepted on both first run and resume.
68
+ * - item type "error" is non-fatal (e.g. model-metadata fallback warnings);
69
+ * turn.failed / top-level error events are the fatal ones.
70
+ *
71
+ * Note: exec mode accepts text prompts only — image attachments are silently
72
+ * dropped (with a log line).
73
+ */
74
+ class CodexExecutor {
75
+ directoryGuard;
76
+ currentWorkingDirectory;
77
+ /** Model for the next spawn. Mutable: setModel() updates it in place. */
78
+ model;
79
+ autoApprove;
80
+ codexCommand;
81
+ threadId;
82
+ proc = null;
83
+ codexThreadId = null;
84
+ sessionFilePath;
85
+ commandQueue = [];
86
+ isProcessing = false;
87
+ isDestroyed = false;
88
+ activeCommand = null;
89
+ currentOutputBuffer = [];
90
+ stdoutBuffer = '';
91
+ stderrTail = '';
92
+ decoder = new string_decoder_1.StringDecoder('utf8');
93
+ inactivityTimer = null;
94
+ inactivityTimeoutMs;
95
+ killEscalationMs;
96
+ attachmentWarningShown = false;
97
+ /** Handoff summary from compactWhenFull, wrapped into the next prompt (consumed once). */
98
+ pendingContextSeed = null;
99
+ constructor(directoryGuard, options = {}) {
100
+ this.directoryGuard = directoryGuard;
101
+ this.model = options.model;
102
+ this.autoApprove = options.autoApprove ?? true;
103
+ this.codexCommand = options.codexCommand ?? 'codex';
104
+ this.threadId = options.threadId;
105
+ this.inactivityTimeoutMs = options.inactivityTimeoutMs ?? DEFAULT_INACTIVITY_TIMEOUT_MS;
106
+ this.killEscalationMs = options.killEscalationMs ?? DEFAULT_KILL_ESCALATION_MS;
107
+ if (options.initialWorkingDirectory) {
108
+ try {
109
+ this.currentWorkingDirectory = this.directoryGuard.resolveWorkingDirectory(options.initialWorkingDirectory);
110
+ }
111
+ catch (error) {
112
+ console.warn(`[CodexExecutor] Failed to use initial working directory: ${options.initialWorkingDirectory}`, error);
113
+ this.currentWorkingDirectory = process.cwd();
114
+ }
115
+ }
116
+ else {
117
+ this.currentWorkingDirectory = process.cwd();
118
+ }
119
+ if (this.threadId) {
120
+ const sessionsDir = path.join(os.homedir(), '.remote-cli', 'codex-sessions');
121
+ fs.mkdirSync(sessionsDir, { recursive: true });
122
+ this.sessionFilePath = path.join(sessionsDir, `${this.threadId}.json`);
123
+ }
124
+ else {
125
+ // Legacy: session file in working directory
126
+ this.sessionFilePath = path.join(this.currentWorkingDirectory, '.codex-session');
127
+ }
128
+ this.loadThreadId();
129
+ }
130
+ // ─── IExecutor required ───────────────────────────────────────────────────
131
+ async execute(prompt, options = {}) {
132
+ if (this.isDestroyed) {
133
+ return Promise.reject(new Error('Executor has been destroyed'));
134
+ }
135
+ return new Promise((resolve, reject) => {
136
+ this.commandQueue.push({ prompt, options, resolve, reject });
137
+ void this.processQueue();
138
+ });
139
+ }
140
+ getCurrentWorkingDirectory() {
141
+ return this.currentWorkingDirectory;
142
+ }
143
+ async setWorkingDirectory(targetPath) {
144
+ // One-shot processes: only the next spawn uses the new directory; a
145
+ // running command keeps its own cwd.
146
+ this.currentWorkingDirectory = this.directoryGuard.resolveWorkingDirectory(targetPath);
147
+ }
148
+ resetContext() {
149
+ this.killProcess('Context reset');
150
+ this.clearThreadId();
151
+ // /clear means a real fresh start — drop any carried-over compact seed
152
+ this.pendingContextSeed = null;
153
+ }
154
+ async abort() {
155
+ if (!this.proc || !this.activeCommand)
156
+ return false;
157
+ // codex exec has no cancel request in exec mode — abort = kill the
158
+ // process. The thread id survives, so the next command resumes context.
159
+ console.log('[CodexExecutor] Aborting: killing codex process');
160
+ this.killProcess('Aborted by user');
161
+ return true;
162
+ }
163
+ async destroy() {
164
+ if (this.isDestroyed)
165
+ return;
166
+ this.isDestroyed = true;
167
+ // Reject everything still queued — nothing may spawn after destroy
168
+ const queued = this.commandQueue.splice(0);
169
+ for (const command of queued) {
170
+ command.reject(new Error('Executor has been destroyed'));
171
+ }
172
+ const proc = this.proc;
173
+ if (proc) {
174
+ try {
175
+ proc.stdin?.end();
176
+ }
177
+ catch { /* already closed */ }
178
+ }
179
+ this.killProcess('Executor destroyed');
180
+ }
181
+ // ─── IExecutor optional ───────────────────────────────────────────────────
182
+ isProcessRunning() {
183
+ return this.proc !== null;
184
+ }
185
+ getSessionId() {
186
+ return this.codexThreadId;
187
+ }
188
+ /**
189
+ * Switch the model for future commands. codex exec is one-shot per command
190
+ * and takes -m at spawn, so the running command (if any) is undisturbed and
191
+ * the next spawn picks the new model up. MessageHandler persists the choice
192
+ * on the thread; ThreadExecutorPool passes it back via the factory.
193
+ */
194
+ async setModel(model, _onStream) {
195
+ this.model = model;
196
+ console.log(`[CodexExecutor] Model set to ${model} (applies from the next command)`);
197
+ return {
198
+ success: true,
199
+ output: `Model set to ${model}. Takes effect on the next command.`,
200
+ };
201
+ }
202
+ /**
203
+ * Compact via summarize-then-reset. codex has compaction in its interactive
204
+ * TUI (plus internal auto-compaction at the token limit), but in exec mode
205
+ * a '/compact' prompt reaches the model as plain text (verified live against
206
+ * codex-cli 0.153.4). So we:
207
+ * 1. ask the current thread for a dense handoff summary,
208
+ * 2. reset the thread,
209
+ * 3. wrap the summary into the next user prompt (consumed once).
210
+ * If the summary turn fails (e.g. context already too full), fall back to
211
+ * a plain reset and say so honestly.
212
+ */
213
+ async compactWhenFull(onStream) {
214
+ if (!this.proc && !this.codexThreadId) {
215
+ return { success: true, output: 'No active conversation to compact.' };
216
+ }
217
+ onStream?.('Summarizing the conversation before resetting...\n');
218
+ let summary = null;
219
+ try {
220
+ const r = await this.execute(compactHandoff_1.COMPACT_HANDOFF_PROMPT, onStream ? { onStream } : {});
221
+ if (r.success && r.output?.trim())
222
+ summary = r.output.trim();
223
+ }
224
+ catch (error) {
225
+ console.warn('[CodexExecutor] Handoff summary failed, falling back to plain reset:', error);
226
+ }
227
+ this.resetContext();
228
+ if (summary) {
229
+ this.pendingContextSeed = summary;
230
+ return {
231
+ success: true,
232
+ output: '✅ Context compacted: the conversation was summarized and carried over to a fresh session.',
233
+ };
234
+ }
235
+ return {
236
+ success: true,
237
+ output: '⚠️ Handoff summary failed — the conversation was reset without carryover. Context starts fresh.',
238
+ };
239
+ }
240
+ /**
241
+ * Delete per-thread session state. Only removes OUR stored thread id
242
+ * mapping — codex's own session store (~/.codex/sessions) is left untouched.
243
+ */
244
+ async deleteThreadData(_threadId) {
245
+ await this.destroy();
246
+ this.clearThreadId();
247
+ }
248
+ // ─── Queue processing ─────────────────────────────────────────────────────
249
+ async processQueue() {
250
+ if (this.isProcessing || this.isDestroyed || this.commandQueue.length === 0)
251
+ return;
252
+ const command = this.commandQueue.shift();
253
+ if (!command)
254
+ return;
255
+ this.isProcessing = true;
256
+ try {
257
+ const result = await this.executeQueued(command.prompt, command.options);
258
+ command.resolve(result);
259
+ }
260
+ catch (error) {
261
+ command.reject(error instanceof Error ? error : new Error(String(error)));
262
+ }
263
+ finally {
264
+ this.isProcessing = false;
265
+ void this.processQueue();
266
+ }
267
+ }
268
+ executeQueued(prompt, options) {
269
+ return new Promise((resolve) => {
270
+ if (options.attachments?.length && !this.attachmentWarningShown) {
271
+ this.attachmentWarningShown = true;
272
+ console.warn('[CodexExecutor] codex exec mode accepts text prompts only — attachments were dropped');
273
+ }
274
+ let proc;
275
+ try {
276
+ // A carried-over compact summary wraps the first prompt after a reset
277
+ const finalPrompt = this.pendingContextSeed
278
+ ? (0, compactHandoff_1.seedPromptWithHandoff)(this.pendingContextSeed, prompt)
279
+ : prompt;
280
+ this.pendingContextSeed = null;
281
+ proc = this.spawnProcess(finalPrompt);
282
+ }
283
+ catch (error) {
284
+ resolve({
285
+ success: false,
286
+ error: error instanceof Error ? error.message : String(error),
287
+ });
288
+ return;
289
+ }
290
+ const active = { options, resolve };
291
+ if (options.timeout && options.timeout > 0) {
292
+ active.timeoutTimer = setTimeout(() => {
293
+ console.warn(`[CodexExecutor] Command timed out after ${options.timeout}ms, killing process`);
294
+ this.killProcess(`Command timed out after ${options.timeout}ms`);
295
+ }, options.timeout);
296
+ }
297
+ this.activeCommand = active;
298
+ this.currentOutputBuffer = [];
299
+ this.armInactivityTimer();
300
+ });
301
+ }
302
+ completeActiveCommand(result) {
303
+ const active = this.activeCommand;
304
+ if (!active)
305
+ return;
306
+ this.activeCommand = null;
307
+ this.clearInactivityTimer();
308
+ if (active.timeoutTimer)
309
+ clearTimeout(active.timeoutTimer);
310
+ if (result.success && result.output === undefined) {
311
+ result.output = this.currentOutputBuffer.join('');
312
+ }
313
+ if (result.success) {
314
+ result.sessionAbbr = this.codexThreadId?.slice(0, 8);
315
+ }
316
+ active.resolve(result);
317
+ }
318
+ /**
319
+ * Arm the inactivity watchdog: any stdout line counts as activity. If codex
320
+ * goes silent mid-command for longer than the limit, fail the command and
321
+ * kill the process (the thread resumes on the next command).
322
+ */
323
+ armInactivityTimer() {
324
+ this.clearInactivityTimer();
325
+ this.inactivityTimer = setTimeout(() => {
326
+ console.warn(`[CodexExecutor] No output for ${this.inactivityTimeoutMs}ms, killing process`);
327
+ this.killProcess(`No output from codex for ${this.inactivityTimeoutMs}ms (inactivity timeout)`);
328
+ }, this.inactivityTimeoutMs);
329
+ }
330
+ clearInactivityTimer() {
331
+ if (this.inactivityTimer !== null) {
332
+ clearTimeout(this.inactivityTimer);
333
+ this.inactivityTimer = null;
334
+ }
335
+ }
336
+ // ─── Process lifecycle ────────────────────────────────────────────────────
337
+ spawnProcess(prompt) {
338
+ if (this.isDestroyed) {
339
+ throw new Error('Executor has been destroyed');
340
+ }
341
+ const args = this.buildArgs(prompt);
342
+ console.log(`[CodexExecutor] Spawning: ${this.codexCommand} ${args.join(' ')} (cwd: ${this.currentWorkingDirectory})`);
343
+ // Fresh stream state for the new process
344
+ this.stdoutBuffer = '';
345
+ this.stderrTail = '';
346
+ this.decoder = new string_decoder_1.StringDecoder('utf8');
347
+ const proc = (0, child_process_1.spawn)(this.codexCommand, args, {
348
+ cwd: this.currentWorkingDirectory,
349
+ env: { ...process.env },
350
+ stdio: ['pipe', 'pipe', 'pipe'],
351
+ });
352
+ proc.stdout.on('data', (data) => this.handleStdout(proc, data));
353
+ proc.stderr.on('data', (data) => {
354
+ if (proc !== this.proc)
355
+ return; // stale process — don't pollute the next command's diagnostics
356
+ const text = data.toString();
357
+ // Keep a bounded tail for error diagnostics (usage errors, auth failures)
358
+ this.stderrTail = (this.stderrTail + text).slice(-STDERR_TAIL_LIMIT);
359
+ console.log(`[CodexExecutor stderr] ${text.trim().slice(0, 300)}`);
360
+ });
361
+ // Async stdin socket errors (e.g. EPIPE if codex exits early) must not
362
+ // crash the whole CLI process — the exit/error handler reports the failure.
363
+ proc.stdin.on('error', (error) => {
364
+ console.warn(`[CodexExecutor] stdin error: ${error.message}`);
365
+ });
366
+ proc.on('error', (error) => {
367
+ if (proc !== this.proc)
368
+ return;
369
+ console.error('[CodexExecutor] Process error:', error);
370
+ this.proc = null;
371
+ const friendly = error.message.includes('ENOENT')
372
+ ? 'Codex CLI (codex) is not installed or not found on PATH. Install it with `npm i -g @openai/codex`, or use /backend to switch to another backend.'
373
+ : `codex process error: ${error.message}`;
374
+ this.completeActiveCommand({ success: false, error: friendly });
375
+ });
376
+ proc.on('exit', (code, signal) => {
377
+ if (proc !== this.proc)
378
+ return;
379
+ // 'exit' fires when the process terminates, but its stdio streams may
380
+ // still hold undelivered data — terminal resolution happens on 'close'
381
+ // (all stdio flushed) so the final turn.completed line is never lost.
382
+ console.log(`[CodexExecutor] Process exited (code=${code}, signal=${signal})`);
383
+ });
384
+ proc.on('close', (code, signal) => {
385
+ if (proc !== this.proc)
386
+ return;
387
+ this.proc = null;
388
+ // Flush the decoder and any unterminated trailing line before judging
389
+ // the outcome — a crash mid-write can leave a complete final event
390
+ // (e.g. turn.failed with the real error) without a trailing newline.
391
+ this.stdoutBuffer += this.decoder.end();
392
+ const remaining = this.stdoutBuffer;
393
+ this.stdoutBuffer = '';
394
+ for (const line of remaining.split('\n')) {
395
+ if (line.trim())
396
+ this.handleLine(line);
397
+ }
398
+ if (this.activeCommand) {
399
+ const recorded = this.activeCommand.result;
400
+ if (recorded) {
401
+ // A terminal event (turn.completed / turn.failed / error) already
402
+ // determined the outcome — trust it over the exit code.
403
+ this.completeActiveCommand(recorded);
404
+ }
405
+ else {
406
+ console.error(`[CodexExecutor] Process exited without a terminal event (code=${code}, signal=${signal})`);
407
+ const tail = this.stderrTail.trim().slice(-STDERR_TAIL_IN_ERROR);
408
+ this.completeActiveCommand({
409
+ success: false,
410
+ error: `codex process exited unexpectedly (code ${code ?? signal})${tail ? `: ${tail}` : ''}`,
411
+ });
412
+ }
413
+ }
414
+ });
415
+ this.proc = proc;
416
+ // codex exec reads piped stdin to EOF before starting — end it immediately.
417
+ try {
418
+ proc.stdin.end();
419
+ }
420
+ catch { /* already closed */ }
421
+ return proc;
422
+ }
423
+ buildArgs(prompt) {
424
+ const args = ['exec'];
425
+ if (this.codexThreadId) {
426
+ // NOTE: `codex exec resume` rejects --sandbox (usage error in 0.153.4).
427
+ // --dangerously-bypass-approvals-and-sandbox IS accepted on resume.
428
+ args.push('resume', '--skip-git-repo-check', '--json');
429
+ if (this.autoApprove) {
430
+ args.push('--dangerously-bypass-approvals-and-sandbox');
431
+ }
432
+ if (this.model) {
433
+ args.push('-m', this.model);
434
+ }
435
+ // `--` guards prompts that start with a dash from clap's flag parser
436
+ args.push(this.codexThreadId, '--', prompt);
437
+ return args;
438
+ }
439
+ args.push('--json', '--skip-git-repo-check');
440
+ if (this.autoApprove) {
441
+ args.push('--dangerously-bypass-approvals-and-sandbox');
442
+ }
443
+ if (this.model) {
444
+ args.push('-m', this.model);
445
+ }
446
+ args.push('--', prompt);
447
+ return args;
448
+ }
449
+ killProcess(reason) {
450
+ const proc = this.proc;
451
+ this.proc = null;
452
+ if (proc) {
453
+ let exited = false;
454
+ proc.once('exit', () => { exited = true; });
455
+ proc.kill(); // SIGTERM
456
+ // Escalate to SIGKILL if the process ignores SIGTERM — a lingering
457
+ // process would share the thread rollout store with the next spawn.
458
+ const escalate = setTimeout(() => {
459
+ if (!exited) {
460
+ console.warn('[CodexExecutor] Process ignored SIGTERM, escalating to SIGKILL');
461
+ proc.kill('SIGKILL');
462
+ }
463
+ }, this.killEscalationMs);
464
+ // Never let the watchdog hold the event loop open by itself
465
+ if (typeof escalate.unref === 'function')
466
+ escalate.unref();
467
+ }
468
+ // Never leave an in-flight command hanging when its process dies. If a
469
+ // terminal event already recorded an outcome (e.g. turn.completed arrived
470
+ // but the process then hung), prefer it over the kill reason.
471
+ if (this.activeCommand) {
472
+ this.completeActiveCommand(this.activeCommand.result ?? {
473
+ success: false,
474
+ error: reason ?? 'codex process was terminated mid-command',
475
+ });
476
+ }
477
+ }
478
+ // ─── Wire protocol parsing ────────────────────────────────────────────────
479
+ handleStdout(proc, data) {
480
+ if (proc !== this.proc)
481
+ return; // stale process
482
+ // StringDecoder handles multi-byte UTF-8 characters split across chunks
483
+ this.stdoutBuffer += this.decoder.write(data);
484
+ const lines = this.stdoutBuffer.split('\n');
485
+ this.stdoutBuffer = lines.pop() ?? '';
486
+ for (const line of lines) {
487
+ if (line.trim())
488
+ this.handleLine(line);
489
+ }
490
+ }
491
+ handleLine(line) {
492
+ let message;
493
+ try {
494
+ message = JSON.parse(line);
495
+ }
496
+ catch {
497
+ console.warn(`[CodexExecutor] Skipping malformed NDJSON line: ${line.slice(0, 120)}`);
498
+ return;
499
+ }
500
+ // Any valid line counts as activity for the inactivity watchdog
501
+ if (this.activeCommand)
502
+ this.armInactivityTimer();
503
+ switch (message.type) {
504
+ case 'thread.started': {
505
+ const threadId = message.thread_id;
506
+ if (typeof threadId === 'string' && threadId) {
507
+ if (threadId !== this.codexThreadId) {
508
+ console.log(`[CodexExecutor] Thread started: ${threadId}`);
509
+ this.setThreadId(threadId);
510
+ }
511
+ }
512
+ break;
513
+ }
514
+ case 'turn.started':
515
+ // Marks the start of model work for this turn — nothing to emit
516
+ break;
517
+ case 'item.started':
518
+ this.handleItemStarted(message.item);
519
+ break;
520
+ case 'item.completed':
521
+ this.handleItemCompleted(message.item);
522
+ break;
523
+ case 'turn.completed':
524
+ if (this.activeCommand) {
525
+ this.activeCommand.result = {
526
+ success: true,
527
+ output: this.currentOutputBuffer.join(''),
528
+ };
529
+ }
530
+ break;
531
+ case 'turn.failed': {
532
+ const errMsg = message.error?.message || 'turn failed (no details)';
533
+ if (this.activeCommand) {
534
+ this.activeCommand.result = { success: false, error: errMsg };
535
+ }
536
+ break;
537
+ }
538
+ case 'error': {
539
+ const errMsg = typeof message.message === 'string' ? message.message : 'codex error';
540
+ // Transient reconnect/fallback notices are informational only
541
+ if (/Reconnecting|Falling back/i.test(errMsg)) {
542
+ console.warn(`[CodexExecutor] Transient error event: ${errMsg}`);
543
+ break;
544
+ }
545
+ if (this.activeCommand) {
546
+ this.activeCommand.result = { success: false, error: errMsg };
547
+ }
548
+ break;
549
+ }
550
+ default:
551
+ // Unknown events are safely ignored (codex may add new ones)
552
+ console.warn(`[CodexExecutor] Ignoring unknown event: ${message.type}`);
553
+ }
554
+ }
555
+ handleItemStarted(item) {
556
+ if (!item || !this.activeCommand)
557
+ return;
558
+ const { options } = this.activeCommand;
559
+ // command_execution has its full command string at start; other tool
560
+ // types (web_search etc.) have empty fields until item.completed.
561
+ if (item.type === 'command_execution' && typeof item.id === 'string') {
562
+ options.onToolUse?.({
563
+ id: item.id,
564
+ name: 'Bash',
565
+ input: { command: item.command ?? '' },
566
+ });
567
+ }
568
+ }
569
+ handleItemCompleted(item) {
570
+ if (!item || !this.activeCommand)
571
+ return;
572
+ const { options } = this.activeCommand;
573
+ const itemId = typeof item.id === 'string' ? item.id : 'codex-item';
574
+ switch (item.type) {
575
+ case 'agent_message':
576
+ case 'message': {
577
+ const text = item.text;
578
+ if (typeof text === 'string' && text.length > 0) {
579
+ this.currentOutputBuffer.push(text);
580
+ options.onStream?.(text);
581
+ }
582
+ return;
583
+ }
584
+ case 'command_execution': {
585
+ const output = typeof item.aggregated_output === 'string' ? item.aggregated_output : '';
586
+ const isError = !codexItemSucceeded(item.status, item.exit_code);
587
+ options.onToolResult?.({ tool_use_id: itemId, content: output, is_error: isError });
588
+ return;
589
+ }
590
+ case 'file_change': {
591
+ const changes = Array.isArray(item.changes) ? item.changes : [];
592
+ const firstPath = changes[0]?.path ?? '';
593
+ const kinds = changes.map((c) => c?.kind).filter(Boolean).join(', ');
594
+ const status = typeof item.status === 'string' ? item.status : '';
595
+ options.onToolUse?.({ id: itemId, name: 'Edit', input: { file_path: firstPath } });
596
+ options.onToolResult?.({
597
+ tool_use_id: itemId,
598
+ content: `file_change (${kinds || 'unknown'}): ${status}`,
599
+ is_error: !codexItemSucceeded(item.status, null),
600
+ });
601
+ return;
602
+ }
603
+ case 'web_search': {
604
+ const query = item.query ?? item.action?.queries?.[0] ?? '';
605
+ options.onToolUse?.({ id: itemId, name: 'WebSearch', input: { query } });
606
+ options.onToolResult?.({ tool_use_id: itemId, content: '', is_error: false });
607
+ return;
608
+ }
609
+ case 'reasoning': {
610
+ // Surface model reasoning summaries as streamed thinking text
611
+ // (aligned with the Claude backend, which streams thinking through
612
+ // the same channel) — but keep it out of the final result output.
613
+ const summary = Array.isArray(item.summary) ? item.summary : [];
614
+ const text = summary
615
+ .map((s) => (typeof s?.text === 'string' ? s.text : ''))
616
+ .filter(Boolean)
617
+ .join('\n');
618
+ if (text)
619
+ options.onStream?.(text);
620
+ return;
621
+ }
622
+ case 'function_call_output':
623
+ // Companion record of a function_call — already rendered by it
624
+ return;
625
+ case 'error':
626
+ // Non-fatal item-level error (e.g. model-metadata fallback warnings)
627
+ console.warn(`[CodexExecutor] Item error: ${item.message ?? '(no message)'}`);
628
+ return;
629
+ default: {
630
+ // Unknown tool-ish items (mcp_tool_call, file_search, future types):
631
+ // render a generic card with the raw type as the tool name — same
632
+ // pass-through policy as the AGY backend.
633
+ const { id: _id, type, status, result: itemResult, error: itemError, ...rest } = item;
634
+ options.onToolUse?.({ id: itemId, name: type, input: rest });
635
+ const isError = !!itemError ||
636
+ (typeof status === 'string' && !codexItemSucceeded(status, null));
637
+ const content = typeof itemResult === 'string' ? itemResult
638
+ : itemResult !== undefined ? JSON.stringify(itemResult).slice(0, 500)
639
+ : (itemError?.message ?? '');
640
+ options.onToolResult?.({ tool_use_id: itemId, content, is_error: isError });
641
+ return;
642
+ }
643
+ }
644
+ }
645
+ // ─── Thread persistence ───────────────────────────────────────────────────
646
+ loadThreadId() {
647
+ try {
648
+ if (fs.existsSync(this.sessionFilePath)) {
649
+ const data = fs.readFileSync(this.sessionFilePath, 'utf-8');
650
+ const session = JSON.parse(data);
651
+ if (session.id) {
652
+ this.codexThreadId = session.id;
653
+ console.log(`[CodexExecutor] Loaded thread ID: ${this.codexThreadId}`);
654
+ }
655
+ }
656
+ }
657
+ catch (error) {
658
+ console.error('[CodexExecutor] Failed to load thread ID:', error);
659
+ this.codexThreadId = null;
660
+ }
661
+ }
662
+ setThreadId(threadId) {
663
+ this.codexThreadId = threadId;
664
+ try {
665
+ const data = JSON.stringify({ id: threadId, savedAt: new Date().toISOString() });
666
+ fs.writeFileSync(this.sessionFilePath, data, 'utf-8');
667
+ }
668
+ catch (error) {
669
+ console.error('[CodexExecutor] Failed to save thread ID:', error);
670
+ }
671
+ }
672
+ clearThreadId() {
673
+ this.codexThreadId = null;
674
+ try {
675
+ if (fs.existsSync(this.sessionFilePath)) {
676
+ fs.unlinkSync(this.sessionFilePath);
677
+ }
678
+ }
679
+ catch (error) {
680
+ console.error('[CodexExecutor] Failed to delete session file:', error);
681
+ }
682
+ }
683
+ }
684
+ exports.CodexExecutor = CodexExecutor;
685
+ /**
686
+ * A command_execution / file_change item succeeded when its exit code is 0
687
+ * (if present) or its status is a success word.
688
+ */
689
+ function codexItemSucceeded(status, exitCode) {
690
+ if (typeof exitCode === 'number') {
691
+ return exitCode === 0;
692
+ }
693
+ const s = typeof status === 'string' ? status.trim().toLowerCase() : '';
694
+ return s === 'completed' || s === 'success' || s === 'succeeded' || s === 'ok';
695
+ }
696
+ //# sourceMappingURL=CodexExecutor.js.map