@openagents-org/agent-launcher 0.2.100 → 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 +330 -120
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);
|
|
83
238
|
}
|
|
84
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;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
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 }];
|
|
@@ -137,7 +446,6 @@ class CodexAdapter extends BaseAdapter {
|
|
|
137
446
|
let fullText = '';
|
|
138
447
|
let toolCallText = '';
|
|
139
448
|
let buffer = '';
|
|
140
|
-
let chunkCount = 0;
|
|
141
449
|
res.on('data', (chunk) => {
|
|
142
450
|
buffer += chunk.toString('utf-8');
|
|
143
451
|
const lines = buffer.split('\n');
|
|
@@ -153,7 +461,6 @@ class CodexAdapter extends BaseAdapter {
|
|
|
153
461
|
if (choices.length > 0) {
|
|
154
462
|
const delta = choices[0].delta || {};
|
|
155
463
|
if (delta.content) fullText += delta.content;
|
|
156
|
-
// Capture tool call arguments as fallback text
|
|
157
464
|
if (delta.tool_calls) {
|
|
158
465
|
for (const tc of delta.tool_calls) {
|
|
159
466
|
if (tc.function && tc.function.arguments) {
|
|
@@ -162,13 +469,10 @@ class CodexAdapter extends BaseAdapter {
|
|
|
162
469
|
}
|
|
163
470
|
}
|
|
164
471
|
}
|
|
165
|
-
chunkCount++;
|
|
166
472
|
} catch {}
|
|
167
473
|
}
|
|
168
474
|
});
|
|
169
475
|
res.on('end', () => {
|
|
170
|
-
this._log(`API response: ${chunkCount} chunks, content=${fullText.length}chars, toolCalls=${toolCallText.length}chars`);
|
|
171
|
-
// If model tried tool calls instead of text, extract the message
|
|
172
476
|
if (!fullText && toolCallText) {
|
|
173
477
|
try {
|
|
174
478
|
const args = JSON.parse(toolCallText);
|
|
@@ -176,7 +480,6 @@ class CodexAdapter extends BaseAdapter {
|
|
|
176
480
|
} catch {
|
|
177
481
|
fullText = toolCallText;
|
|
178
482
|
}
|
|
179
|
-
this._log(`Extracted from tool_calls: ${fullText.slice(0, 100)}`);
|
|
180
483
|
}
|
|
181
484
|
resolve(fullText.trim());
|
|
182
485
|
});
|
|
@@ -187,99 +490,6 @@ class CodexAdapter extends BaseAdapter {
|
|
|
187
490
|
req.end();
|
|
188
491
|
});
|
|
189
492
|
}
|
|
190
|
-
|
|
191
|
-
// ------------------------------------------------------------------
|
|
192
|
-
// Subprocess mode (codex exec --json --full-auto)
|
|
193
|
-
// ------------------------------------------------------------------
|
|
194
|
-
|
|
195
|
-
_findCodexBinary() {
|
|
196
|
-
try {
|
|
197
|
-
if (IS_WINDOWS) {
|
|
198
|
-
const r = execSync('where codex.cmd 2>nul || where codex.exe 2>nul || where codex 2>nul', {
|
|
199
|
-
encoding: 'utf-8', timeout: 5000,
|
|
200
|
-
});
|
|
201
|
-
return r.split(/\r?\n/)[0].trim();
|
|
202
|
-
} else {
|
|
203
|
-
return execSync('which codex', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
204
|
-
}
|
|
205
|
-
} catch {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async _runCodexSubprocess(content, msgChannel) {
|
|
211
|
-
const codexBin = this._findCodexBinary();
|
|
212
|
-
if (!codexBin) {
|
|
213
|
-
await this.sendError(msgChannel, 'codex CLI not found. Install with: npm install -g @openai/codex');
|
|
214
|
-
return '';
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const context = this._buildSystemContext(msgChannel);
|
|
218
|
-
const fullPrompt = `${context}\n\n---\n\n${content}`;
|
|
219
|
-
|
|
220
|
-
let cmd = [codexBin, 'exec'];
|
|
221
|
-
if (this._codexThreadId) {
|
|
222
|
-
cmd.push('resume', this._codexThreadId);
|
|
223
|
-
}
|
|
224
|
-
cmd.push('--json', '--full-auto', fullPrompt);
|
|
225
|
-
|
|
226
|
-
if (IS_WINDOWS && cmd[0].toLowerCase().endsWith('.cmd')) {
|
|
227
|
-
cmd = ['cmd.exe', '/c', ...cmd];
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return new Promise((resolve) => {
|
|
231
|
-
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
232
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const responseTexts = [];
|
|
236
|
-
let lineBuffer = '';
|
|
237
|
-
|
|
238
|
-
proc.stdout.on('data', async (chunk) => {
|
|
239
|
-
lineBuffer += chunk.toString('utf-8');
|
|
240
|
-
const lines = lineBuffer.split('\n');
|
|
241
|
-
lineBuffer = lines.pop();
|
|
242
|
-
|
|
243
|
-
for (const line of lines) {
|
|
244
|
-
const trimmed = line.trim();
|
|
245
|
-
if (!trimmed) continue;
|
|
246
|
-
let event;
|
|
247
|
-
try { event = JSON.parse(trimmed); } catch { continue; }
|
|
248
|
-
|
|
249
|
-
const eventType = event.type;
|
|
250
|
-
|
|
251
|
-
if (eventType === 'thread.started') {
|
|
252
|
-
if (event.thread_id) this._codexThreadId = event.thread_id;
|
|
253
|
-
} else if (eventType === 'item.completed') {
|
|
254
|
-
const item = event.item || {};
|
|
255
|
-
if (item.type === 'agent_message' && item.text) {
|
|
256
|
-
responseTexts.push(item.text);
|
|
257
|
-
} else if (item.type === 'command_execution') {
|
|
258
|
-
const cmdText = (item.command || '').slice(0, 200);
|
|
259
|
-
try { await this.sendStatus(msgChannel, `**Running:** \`${cmdText}\``); } catch {}
|
|
260
|
-
} else if (item.type === 'file_change') {
|
|
261
|
-
try { await this.sendStatus(msgChannel, `**Editing:** \`${item.filename || ''}\``); } catch {}
|
|
262
|
-
}
|
|
263
|
-
} else if (eventType === 'turn.failed') {
|
|
264
|
-
const error = event.error || {};
|
|
265
|
-
this._log(`Codex turn failed: ${error.message || JSON.stringify(error)}`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
proc.on('exit', (code) => {
|
|
271
|
-
if (code !== 0) {
|
|
272
|
-
this._log(`Codex CLI exited with code ${code}`);
|
|
273
|
-
}
|
|
274
|
-
resolve(responseTexts.join('\n').trim());
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
proc.on('error', (err) => {
|
|
278
|
-
this._log(`Codex spawn error: ${err.message}`);
|
|
279
|
-
resolve('');
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
493
|
}
|
|
284
494
|
|
|
285
495
|
module.exports = CodexAdapter;
|