@yu_robotics/remote-cli 1.6.26 → 1.6.32

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