@openagents-org/agent-launcher 0.2.99 → 0.2.101
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/package.json +1 -1
- package/src/adapters/codex.js +349 -115
package/package.json
CHANGED
package/src/adapters/codex.js
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
* Codex adapter for OpenAgents workspace.
|
|
3
3
|
*
|
|
4
4
|
* Bridges OpenAI Codex CLI to an OpenAgents workspace via:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
5
|
+
* - Codex CLI subprocess (exec --json --full-auto) as primary mode
|
|
6
|
+
* - Direct HTTP mode for OpenAI-compatible LLM APIs as fallback
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Similar to ClaudeAdapter: spawns the CLI per message, processes
|
|
9
|
+
* structured JSON events, maintains session/thread IDs per channel,
|
|
10
|
+
* and sends real-time status updates.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
'use strict';
|
|
12
14
|
|
|
13
15
|
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
14
17
|
const path = require('path');
|
|
15
18
|
const { execSync, spawn } = require('child_process');
|
|
16
19
|
const http = require('http');
|
|
@@ -30,23 +33,113 @@ class CodexAdapter extends BaseAdapter {
|
|
|
30
33
|
constructor(opts) {
|
|
31
34
|
super(opts);
|
|
32
35
|
this.disabledModules = opts.disabledModules || new Set();
|
|
33
|
-
this._codexThreadId = null;
|
|
34
36
|
|
|
35
|
-
// Direct LLM API mode
|
|
36
37
|
const env = this.agentEnv || process.env;
|
|
37
38
|
this._directApiKey = env.OPENAI_API_KEY || '';
|
|
38
39
|
this._directBaseUrl = (env.OPENAI_BASE_URL || '').replace(/\/+$/, '');
|
|
39
40
|
this._directModel = env.CODEX_MODEL || env.OPENCLAW_MODEL || '';
|
|
40
|
-
this._directMode = !!(this._directApiKey && this._directBaseUrl);
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// Per-channel thread tracking (like Claude's session IDs)
|
|
43
|
+
this._channelThreads = {};
|
|
44
|
+
this._channelProcesses = {};
|
|
45
|
+
this._sessionsFile = path.join(
|
|
46
|
+
os.homedir(), '.openagents', 'sessions',
|
|
47
|
+
`${this.workspaceId}_${this.agentName}_codex.json`
|
|
48
|
+
);
|
|
49
|
+
this._loadSessions();
|
|
50
|
+
|
|
51
|
+
// Determine mode: CLI binary preferred, direct API as fallback
|
|
52
|
+
this._codexBin = this._findCodexBinary();
|
|
53
|
+
this._directMode = false;
|
|
54
|
+
|
|
55
|
+
if (this._codexBin) {
|
|
56
|
+
this._log(`CLI mode: ${this._codexBin}`);
|
|
57
|
+
} else if (this._directApiKey && this._directBaseUrl) {
|
|
58
|
+
this._directMode = true;
|
|
59
|
+
this._log(`Direct LLM mode (CLI not found): ${this._directBaseUrl} model=${this._directModel || 'gpt-4o'}`);
|
|
60
|
+
} else {
|
|
61
|
+
this._log('Warning: No codex CLI binary found and no direct API configured');
|
|
44
62
|
}
|
|
45
63
|
|
|
46
|
-
// Conversation history
|
|
64
|
+
// Conversation history (direct API mode only)
|
|
47
65
|
this._conversationHistory = [];
|
|
48
66
|
}
|
|
49
67
|
|
|
68
|
+
// ------------------------------------------------------------------
|
|
69
|
+
// Session persistence (per-channel thread IDs)
|
|
70
|
+
// ------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
_loadSessions() {
|
|
73
|
+
try {
|
|
74
|
+
if (fs.existsSync(this._sessionsFile)) {
|
|
75
|
+
const data = JSON.parse(fs.readFileSync(this._sessionsFile, 'utf-8'));
|
|
76
|
+
if (data && typeof data === 'object') {
|
|
77
|
+
Object.assign(this._channelThreads, data);
|
|
78
|
+
this._log(`Loaded ${Object.keys(data).length} thread(s)`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
this._log('Could not load sessions file, starting fresh');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_saveSessions() {
|
|
87
|
+
try {
|
|
88
|
+
const dir = path.dirname(this._sessionsFile);
|
|
89
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
90
|
+
fs.writeFileSync(this._sessionsFile, JSON.stringify(this._channelThreads));
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ------------------------------------------------------------------
|
|
95
|
+
// Find codex binary (multi-tier, like Claude adapter)
|
|
96
|
+
// ------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
_findCodexBinary() {
|
|
99
|
+
const home = os.homedir();
|
|
100
|
+
const ext = IS_WINDOWS ? '.cmd' : '';
|
|
101
|
+
|
|
102
|
+
// Tier 0: Isolated runtime prefix (~/.openagents/runtimes/codex/)
|
|
103
|
+
const runtimeCandidate = path.join(home, '.openagents', 'runtimes', 'codex', 'node_modules', '.bin', `codex${ext}`);
|
|
104
|
+
if (fs.existsSync(runtimeCandidate)) return runtimeCandidate;
|
|
105
|
+
|
|
106
|
+
// Tier 0b: Legacy portable install
|
|
107
|
+
const portableCandidate = path.join(home, '.openagents', 'nodejs', 'node_modules', '.bin', `codex${ext}`);
|
|
108
|
+
if (fs.existsSync(portableCandidate)) return portableCandidate;
|
|
109
|
+
|
|
110
|
+
// Tier 1: PATH search
|
|
111
|
+
try {
|
|
112
|
+
if (IS_WINDOWS) {
|
|
113
|
+
const r = execSync('where codex.cmd 2>nul || where codex.exe 2>nul || where codex 2>nul', {
|
|
114
|
+
encoding: 'utf-8', timeout: 5000,
|
|
115
|
+
});
|
|
116
|
+
return r.split(/\r?\n/)[0].trim();
|
|
117
|
+
} else {
|
|
118
|
+
return execSync('which codex', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
119
|
+
}
|
|
120
|
+
} catch {}
|
|
121
|
+
|
|
122
|
+
// Tier 2: Next to current Node.js interpreter (npm global)
|
|
123
|
+
const nodeBinDir = path.dirname(process.execPath);
|
|
124
|
+
const nearNode = path.join(nodeBinDir, `codex${ext}`);
|
|
125
|
+
if (fs.existsSync(nearNode)) return nearNode;
|
|
126
|
+
|
|
127
|
+
// Tier 3: Common install locations
|
|
128
|
+
const candidates = IS_WINDOWS ? [
|
|
129
|
+
path.join(process.env.APPDATA || '', 'npm', 'codex.cmd'),
|
|
130
|
+
] : [
|
|
131
|
+
path.join(home, '.local', 'bin', 'codex'),
|
|
132
|
+
path.join(home, '.npm-global', 'bin', 'codex'),
|
|
133
|
+
'/opt/homebrew/bin/codex',
|
|
134
|
+
'/usr/local/bin/codex',
|
|
135
|
+
];
|
|
136
|
+
for (const c of candidates) {
|
|
137
|
+
if (fs.existsSync(c)) return c;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
50
143
|
_buildSystemContext(channelName) {
|
|
51
144
|
return buildOpenclawSystemPrompt({
|
|
52
145
|
agentName: this.agentName,
|
|
@@ -59,6 +152,42 @@ class CodexAdapter extends BaseAdapter {
|
|
|
59
152
|
});
|
|
60
153
|
}
|
|
61
154
|
|
|
155
|
+
// ------------------------------------------------------------------
|
|
156
|
+
// Process management
|
|
157
|
+
// ------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
async _stopProcess(proc) {
|
|
160
|
+
if (!proc || proc.exitCode !== null) return;
|
|
161
|
+
try {
|
|
162
|
+
if (IS_WINDOWS) {
|
|
163
|
+
try { execSync(`taskkill /F /T /PID ${proc.pid}`, { timeout: 5000 }); } catch {}
|
|
164
|
+
} else {
|
|
165
|
+
try { process.kill(-proc.pid, 'SIGTERM'); } catch {
|
|
166
|
+
proc.kill('SIGTERM');
|
|
167
|
+
}
|
|
168
|
+
await new Promise((resolve) => {
|
|
169
|
+
const timeout = setTimeout(() => {
|
|
170
|
+
try { process.kill(-proc.pid, 'SIGKILL'); } catch {
|
|
171
|
+
proc.kill('SIGKILL');
|
|
172
|
+
}
|
|
173
|
+
resolve();
|
|
174
|
+
}, 5000);
|
|
175
|
+
proc.on('exit', () => { clearTimeout(timeout); resolve(); });
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async _onControlAction(action, _payload) {
|
|
182
|
+
if (action === 'stop') {
|
|
183
|
+
for (const [channel, proc] of Object.entries(this._channelProcesses)) {
|
|
184
|
+
await this._stopProcess(proc);
|
|
185
|
+
delete this._channelProcesses[channel];
|
|
186
|
+
try { await this.sendStatus(channel, 'Execution stopped by user'); } catch {}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
62
191
|
// ------------------------------------------------------------------
|
|
63
192
|
// Message handler
|
|
64
193
|
// ------------------------------------------------------------------
|
|
@@ -74,14 +203,198 @@ class CodexAdapter extends BaseAdapter {
|
|
|
74
203
|
await this._autoTitleChannel(msgChannel, content);
|
|
75
204
|
await this.sendStatus(msgChannel, 'thinking...');
|
|
76
205
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
206
|
+
if (this._codexBin) {
|
|
207
|
+
await this._handleViaSubprocess(content, msgChannel);
|
|
208
|
+
} else if (this._directMode) {
|
|
209
|
+
await this._handleViaDirectApi(content, msgChannel);
|
|
210
|
+
} else {
|
|
211
|
+
await this.sendError(msgChannel, 'codex CLI not found. Install with: npm install -g @openai/codex');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ------------------------------------------------------------------
|
|
216
|
+
// CLI subprocess mode (primary)
|
|
217
|
+
// ------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
async _handleViaSubprocess(content, msgChannel) {
|
|
220
|
+
const env = { ...(this.agentEnv || process.env) };
|
|
221
|
+
|
|
222
|
+
// Set model via env if configured
|
|
223
|
+
if (this._directModel) env.CODEX_MODEL = this._directModel;
|
|
224
|
+
if (this._directApiKey) env.OPENAI_API_KEY = this._directApiKey;
|
|
225
|
+
if (this._directBaseUrl) env.OPENAI_BASE_URL = this._directBaseUrl;
|
|
226
|
+
|
|
227
|
+
const context = this._buildSystemContext(msgChannel);
|
|
228
|
+
const fullPrompt = `${context}\n\n---\n\nUser message:\n${content}`;
|
|
229
|
+
|
|
230
|
+
// Run up to 2 attempts: first with resume, then fresh if stale
|
|
231
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
232
|
+
const cmd = [this._codexBin, 'exec'];
|
|
233
|
+
|
|
234
|
+
// Resume existing thread for this channel
|
|
235
|
+
const threadId = this._channelThreads[msgChannel];
|
|
236
|
+
if (threadId && attempt === 0) {
|
|
237
|
+
cmd.push('resume', threadId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
cmd.push('--json', '--full-auto');
|
|
241
|
+
|
|
242
|
+
// Model override
|
|
243
|
+
if (this._directModel) {
|
|
244
|
+
cmd.push('-m', this._directModel);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Working directory
|
|
248
|
+
if (this.workingDir) {
|
|
249
|
+
cmd.push('-C', this.workingDir);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
cmd.push(fullPrompt);
|
|
253
|
+
|
|
254
|
+
this._log(`Spawning: codex exec ${threadId && attempt === 0 ? `resume ${threadId} ` : ''}--json --full-auto -m ${this._directModel || 'default'}`);
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const result = await this._spawnCodex(cmd, env, msgChannel);
|
|
258
|
+
|
|
259
|
+
if (result.responseText) {
|
|
260
|
+
await this.sendResponse(msgChannel, result.responseText);
|
|
261
|
+
return;
|
|
262
|
+
} else if (result.exitCode !== 0 && threadId && attempt === 0) {
|
|
263
|
+
// Stale thread — clear and retry fresh
|
|
264
|
+
this._log(`Stale thread detected for ${msgChannel}, clearing and retrying`);
|
|
265
|
+
delete this._channelThreads[msgChannel];
|
|
266
|
+
this._saveSessions();
|
|
267
|
+
continue;
|
|
268
|
+
} else {
|
|
269
|
+
await this.sendResponse(msgChannel, 'No response generated. Please try again.');
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
} catch (e) {
|
|
273
|
+
this._log(`Error in subprocess: ${e.message}`);
|
|
274
|
+
await this.sendError(msgChannel, `Error: ${e.message}`);
|
|
275
|
+
return;
|
|
83
276
|
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
84
279
|
|
|
280
|
+
async _spawnCodex(cmd, env, msgChannel) {
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
283
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
284
|
+
env,
|
|
285
|
+
cwd: this.workingDir,
|
|
286
|
+
detached: !IS_WINDOWS,
|
|
287
|
+
windowsHide: true,
|
|
288
|
+
});
|
|
289
|
+
this._channelProcesses[msgChannel] = proc;
|
|
290
|
+
|
|
291
|
+
const responseTexts = [];
|
|
292
|
+
let hasToolUseSinceLastText = false;
|
|
293
|
+
let lineBuffer = '';
|
|
294
|
+
let stderrBuf = '';
|
|
295
|
+
let _pendingLines = Promise.resolve();
|
|
296
|
+
|
|
297
|
+
if (proc.stderr) {
|
|
298
|
+
proc.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8'); });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const processLine = async (line) => {
|
|
302
|
+
line = line.trim();
|
|
303
|
+
if (!line) return;
|
|
304
|
+
|
|
305
|
+
let event;
|
|
306
|
+
try { event = JSON.parse(line); } catch { return; }
|
|
307
|
+
|
|
308
|
+
const eventType = event.type;
|
|
309
|
+
|
|
310
|
+
if (eventType === 'thread.started') {
|
|
311
|
+
if (event.thread_id) {
|
|
312
|
+
this._channelThreads[msgChannel] = event.thread_id;
|
|
313
|
+
this._saveSessions();
|
|
314
|
+
this._log(`Thread started: ${event.thread_id}`);
|
|
315
|
+
}
|
|
316
|
+
} else if (eventType === 'item.completed') {
|
|
317
|
+
const item = event.item || {};
|
|
318
|
+
if (item.type === 'agent_message' && item.text) {
|
|
319
|
+
if (hasToolUseSinceLastText) {
|
|
320
|
+
responseTexts.length = 0;
|
|
321
|
+
hasToolUseSinceLastText = false;
|
|
322
|
+
}
|
|
323
|
+
responseTexts.push(item.text);
|
|
324
|
+
// Stream as thinking (like Claude adapter)
|
|
325
|
+
try { await this.sendThinking(msgChannel, item.text); } catch {}
|
|
326
|
+
} else if (item.type === 'command_execution') {
|
|
327
|
+
hasToolUseSinceLastText = true;
|
|
328
|
+
const cmdText = (item.command || '').slice(0, 200);
|
|
329
|
+
const exitCode = item.exit_code;
|
|
330
|
+
const output = (item.output || '').slice(0, 500);
|
|
331
|
+
let status = `**Running:** \`${cmdText}\``;
|
|
332
|
+
if (exitCode !== undefined && exitCode !== null) {
|
|
333
|
+
status += ` (exit ${exitCode})`;
|
|
334
|
+
}
|
|
335
|
+
try { await this.sendStatus(msgChannel, status); } catch {}
|
|
336
|
+
this._log(`Command: ${cmdText} → exit ${exitCode}`);
|
|
337
|
+
} else if (item.type === 'file_change') {
|
|
338
|
+
hasToolUseSinceLastText = true;
|
|
339
|
+
const filename = item.filename || '';
|
|
340
|
+
try { await this.sendStatus(msgChannel, `**Editing:** \`${filename}\``); } catch {}
|
|
341
|
+
this._log(`File change: ${filename}`);
|
|
342
|
+
}
|
|
343
|
+
} else if (eventType === 'turn.failed') {
|
|
344
|
+
const error = event.error || {};
|
|
345
|
+
const errMsg = error.message || JSON.stringify(error);
|
|
346
|
+
this._log(`Turn failed: ${errMsg}`);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
proc.stdout.on('data', (chunk) => {
|
|
351
|
+
lineBuffer += chunk.toString('utf-8');
|
|
352
|
+
const lines = lineBuffer.split('\n');
|
|
353
|
+
lineBuffer = lines.pop();
|
|
354
|
+
for (const line of lines) {
|
|
355
|
+
_pendingLines = _pendingLines.then(() => processLine(line)).catch(() => {});
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
proc.on('exit', async (code) => {
|
|
360
|
+
// Wait for all in-flight processLine calls
|
|
361
|
+
try { await _pendingLines; } catch {}
|
|
362
|
+
|
|
363
|
+
// Process remaining buffer
|
|
364
|
+
for (const line of lineBuffer.split('\n')) {
|
|
365
|
+
try { await processLine(line); } catch {}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
delete this._channelProcesses[msgChannel];
|
|
369
|
+
|
|
370
|
+
if (code !== 0) {
|
|
371
|
+
this._log(`Codex CLI exited with code ${code}`);
|
|
372
|
+
if (stderrBuf.trim()) {
|
|
373
|
+
this._log(`stderr: ${stderrBuf.trim().slice(0, 500)}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
resolve({
|
|
378
|
+
responseText: responseTexts.join('\n').trim(),
|
|
379
|
+
exitCode: code,
|
|
380
|
+
stderr: stderrBuf,
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
proc.on('error', (err) => {
|
|
385
|
+
delete this._channelProcesses[msgChannel];
|
|
386
|
+
reject(err);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ------------------------------------------------------------------
|
|
392
|
+
// Direct HTTP mode (fallback when CLI not available)
|
|
393
|
+
// ------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
async _handleViaDirectApi(content, msgChannel) {
|
|
396
|
+
try {
|
|
397
|
+
const responseText = await this._callCompletionApi(content, msgChannel);
|
|
85
398
|
if (responseText) {
|
|
86
399
|
this._conversationHistory.push({ role: 'user', content });
|
|
87
400
|
this._conversationHistory.push({ role: 'assistant', content: responseText });
|
|
@@ -93,15 +406,11 @@ class CodexAdapter extends BaseAdapter {
|
|
|
93
406
|
await this.sendResponse(msgChannel, 'No response generated. Please try again.');
|
|
94
407
|
}
|
|
95
408
|
} catch (e) {
|
|
96
|
-
this._log(`Error
|
|
97
|
-
await this.sendError(msgChannel, `Error
|
|
409
|
+
this._log(`Error in direct API: ${e.message}`);
|
|
410
|
+
await this.sendError(msgChannel, `Error: ${e.message}`);
|
|
98
411
|
}
|
|
99
412
|
}
|
|
100
413
|
|
|
101
|
-
// ------------------------------------------------------------------
|
|
102
|
-
// Direct HTTP mode (OpenAI chat completions API)
|
|
103
|
-
// ------------------------------------------------------------------
|
|
104
|
-
|
|
105
414
|
async _callCompletionApi(userMessage, channel) {
|
|
106
415
|
const systemPrompt = this._buildSystemContext(channel);
|
|
107
416
|
const messages = [{ role: 'system', content: systemPrompt }];
|
|
@@ -135,6 +444,7 @@ class CodexAdapter extends BaseAdapter {
|
|
|
135
444
|
}
|
|
136
445
|
|
|
137
446
|
let fullText = '';
|
|
447
|
+
let toolCallText = '';
|
|
138
448
|
let buffer = '';
|
|
139
449
|
res.on('data', (chunk) => {
|
|
140
450
|
buffer += chunk.toString('utf-8');
|
|
@@ -151,11 +461,28 @@ class CodexAdapter extends BaseAdapter {
|
|
|
151
461
|
if (choices.length > 0) {
|
|
152
462
|
const delta = choices[0].delta || {};
|
|
153
463
|
if (delta.content) fullText += delta.content;
|
|
464
|
+
if (delta.tool_calls) {
|
|
465
|
+
for (const tc of delta.tool_calls) {
|
|
466
|
+
if (tc.function && tc.function.arguments) {
|
|
467
|
+
toolCallText += tc.function.arguments;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
154
471
|
}
|
|
155
472
|
} catch {}
|
|
156
473
|
}
|
|
157
474
|
});
|
|
158
|
-
res.on('end', () =>
|
|
475
|
+
res.on('end', () => {
|
|
476
|
+
if (!fullText && toolCallText) {
|
|
477
|
+
try {
|
|
478
|
+
const args = JSON.parse(toolCallText);
|
|
479
|
+
fullText = args.command || args.input || args.content || args.text || toolCallText;
|
|
480
|
+
} catch {
|
|
481
|
+
fullText = toolCallText;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
resolve(fullText.trim());
|
|
485
|
+
});
|
|
159
486
|
});
|
|
160
487
|
|
|
161
488
|
req.on('error', reject);
|
|
@@ -163,99 +490,6 @@ class CodexAdapter extends BaseAdapter {
|
|
|
163
490
|
req.end();
|
|
164
491
|
});
|
|
165
492
|
}
|
|
166
|
-
|
|
167
|
-
// ------------------------------------------------------------------
|
|
168
|
-
// Subprocess mode (codex exec --json --full-auto)
|
|
169
|
-
// ------------------------------------------------------------------
|
|
170
|
-
|
|
171
|
-
_findCodexBinary() {
|
|
172
|
-
try {
|
|
173
|
-
if (IS_WINDOWS) {
|
|
174
|
-
const r = execSync('where codex.cmd 2>nul || where codex.exe 2>nul || where codex 2>nul', {
|
|
175
|
-
encoding: 'utf-8', timeout: 5000,
|
|
176
|
-
});
|
|
177
|
-
return r.split(/\r?\n/)[0].trim();
|
|
178
|
-
} else {
|
|
179
|
-
return execSync('which codex', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
180
|
-
}
|
|
181
|
-
} catch {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async _runCodexSubprocess(content, msgChannel) {
|
|
187
|
-
const codexBin = this._findCodexBinary();
|
|
188
|
-
if (!codexBin) {
|
|
189
|
-
await this.sendError(msgChannel, 'codex CLI not found. Install with: npm install -g @openai/codex');
|
|
190
|
-
return '';
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const context = this._buildSystemContext(msgChannel);
|
|
194
|
-
const fullPrompt = `${context}\n\n---\n\n${content}`;
|
|
195
|
-
|
|
196
|
-
let cmd = [codexBin, 'exec'];
|
|
197
|
-
if (this._codexThreadId) {
|
|
198
|
-
cmd.push('resume', this._codexThreadId);
|
|
199
|
-
}
|
|
200
|
-
cmd.push('--json', '--full-auto', fullPrompt);
|
|
201
|
-
|
|
202
|
-
if (IS_WINDOWS && cmd[0].toLowerCase().endsWith('.cmd')) {
|
|
203
|
-
cmd = ['cmd.exe', '/c', ...cmd];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return new Promise((resolve) => {
|
|
207
|
-
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
208
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const responseTexts = [];
|
|
212
|
-
let lineBuffer = '';
|
|
213
|
-
|
|
214
|
-
proc.stdout.on('data', async (chunk) => {
|
|
215
|
-
lineBuffer += chunk.toString('utf-8');
|
|
216
|
-
const lines = lineBuffer.split('\n');
|
|
217
|
-
lineBuffer = lines.pop();
|
|
218
|
-
|
|
219
|
-
for (const line of lines) {
|
|
220
|
-
const trimmed = line.trim();
|
|
221
|
-
if (!trimmed) continue;
|
|
222
|
-
let event;
|
|
223
|
-
try { event = JSON.parse(trimmed); } catch { continue; }
|
|
224
|
-
|
|
225
|
-
const eventType = event.type;
|
|
226
|
-
|
|
227
|
-
if (eventType === 'thread.started') {
|
|
228
|
-
if (event.thread_id) this._codexThreadId = event.thread_id;
|
|
229
|
-
} else if (eventType === 'item.completed') {
|
|
230
|
-
const item = event.item || {};
|
|
231
|
-
if (item.type === 'agent_message' && item.text) {
|
|
232
|
-
responseTexts.push(item.text);
|
|
233
|
-
} else if (item.type === 'command_execution') {
|
|
234
|
-
const cmdText = (item.command || '').slice(0, 200);
|
|
235
|
-
try { await this.sendStatus(msgChannel, `**Running:** \`${cmdText}\``); } catch {}
|
|
236
|
-
} else if (item.type === 'file_change') {
|
|
237
|
-
try { await this.sendStatus(msgChannel, `**Editing:** \`${item.filename || ''}\``); } catch {}
|
|
238
|
-
}
|
|
239
|
-
} else if (eventType === 'turn.failed') {
|
|
240
|
-
const error = event.error || {};
|
|
241
|
-
this._log(`Codex turn failed: ${error.message || JSON.stringify(error)}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
proc.on('exit', (code) => {
|
|
247
|
-
if (code !== 0) {
|
|
248
|
-
this._log(`Codex CLI exited with code ${code}`);
|
|
249
|
-
}
|
|
250
|
-
resolve(responseTexts.join('\n').trim());
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
proc.on('error', (err) => {
|
|
254
|
-
this._log(`Codex spawn error: ${err.message}`);
|
|
255
|
-
resolve('');
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
493
|
}
|
|
260
494
|
|
|
261
495
|
module.exports = CodexAdapter;
|