codex-overleaf-link 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +457 -0
- package/bin/codex-overleaf-link.mjs +223 -0
- package/extension/src/shared/agentTranscript.js +1175 -0
- package/extension/src/shared/auditRecords.js +568 -0
- package/extension/src/shared/compatibility.js +372 -0
- package/extension/src/shared/compileAdapter.js +176 -0
- package/extension/src/shared/governanceRules.js +252 -0
- package/extension/src/shared/i18n.js +565 -0
- package/extension/src/shared/models.js +106 -0
- package/extension/src/shared/otText.js +505 -0
- package/extension/src/shared/projectFiles.js +180 -0
- package/extension/src/shared/reviewing.js +99 -0
- package/extension/src/shared/sensitiveScan.js +116 -0
- package/extension/src/shared/sessionState.js +1084 -0
- package/extension/src/shared/staleGuard.js +150 -0
- package/extension/src/shared/storageDb.js +986 -0
- package/extension/src/shared/storageKeys.js +29 -0
- package/extension/src/shared/storageMigration.js +168 -0
- package/extension/src/shared/summary.js +248 -0
- package/extension/src/shared/undoOperations.js +369 -0
- package/native-host/src/codexArgs.js +43 -0
- package/native-host/src/codexHome.js +538 -0
- package/native-host/src/codexModels.js +247 -0
- package/native-host/src/codexPrompt.js +192 -0
- package/native-host/src/codexPromptAssembly.js +411 -0
- package/native-host/src/codexSessionRunner.js +1247 -0
- package/native-host/src/commandApproval.js +914 -0
- package/native-host/src/debugLog.js +78 -0
- package/native-host/src/diffEngine.js +247 -0
- package/native-host/src/index.js +132 -0
- package/native-host/src/launcher.js +81 -0
- package/native-host/src/localSkills.js +476 -0
- package/native-host/src/manifest.js +226 -0
- package/native-host/src/mirrorSensitiveScan.js +119 -0
- package/native-host/src/mirrorWorkspace.js +1019 -0
- package/native-host/src/nativeDoctor.js +826 -0
- package/native-host/src/nativeEnvironment.js +315 -0
- package/native-host/src/nativeHostPlatform.js +112 -0
- package/native-host/src/nativeMessaging.js +60 -0
- package/native-host/src/nativeQuotas.js +294 -0
- package/native-host/src/nativeResponseBudget.js +194 -0
- package/native-host/src/runtimeInstaller.js +357 -0
- package/native-host/src/taskRunner.js +3 -0
- package/native-host/src/taskRunnerRuntime.js +1083 -0
- package/native-host/src/textPatch.js +287 -0
- package/package.json +40 -0
- package/scripts/codex-json-agent.mjs +269 -0
- package/scripts/install-native-host.mjs +255 -0
- package/scripts/npm-package-files-v1.1.1.txt +52 -0
- package/scripts/uninstall-native-host.mjs +298 -0
- package/scripts/verify-npm-package.mjs +296 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function computeTextPatches(oldText, newText) {
|
|
4
|
+
const oldValue = String(oldText ?? '');
|
|
5
|
+
const newValue = String(newText ?? '');
|
|
6
|
+
if (oldValue === newValue) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const linePatches = computeLineAnchoredPatches(oldValue, newValue);
|
|
11
|
+
if (linePatches.length) {
|
|
12
|
+
return linePatches;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return [computeSingleTextPatch(oldValue, newValue)];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function computeSingleTextPatch(oldValue, newValue, offset = 0) {
|
|
19
|
+
let prefixLength = 0;
|
|
20
|
+
const sharedLength = Math.min(oldValue.length, newValue.length);
|
|
21
|
+
while (prefixLength < sharedLength && oldValue[prefixLength] === newValue[prefixLength]) {
|
|
22
|
+
prefixLength += 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let oldEnd = oldValue.length;
|
|
26
|
+
let newEnd = newValue.length;
|
|
27
|
+
while (
|
|
28
|
+
oldEnd > prefixLength
|
|
29
|
+
&& newEnd > prefixLength
|
|
30
|
+
&& oldValue[oldEnd - 1] === newValue[newEnd - 1]
|
|
31
|
+
) {
|
|
32
|
+
oldEnd -= 1;
|
|
33
|
+
newEnd -= 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
from: offset + prefixLength,
|
|
38
|
+
to: offset + oldEnd,
|
|
39
|
+
expected: oldValue.slice(prefixLength, oldEnd),
|
|
40
|
+
insert: newValue.slice(prefixLength, newEnd)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function computeLineAnchoredPatches(oldValue, newValue) {
|
|
45
|
+
const oldParts = splitTextParts(oldValue);
|
|
46
|
+
const newParts = splitTextParts(newValue);
|
|
47
|
+
const MAX_PARTS = 5000;
|
|
48
|
+
const MAX_PRODUCT = 4000000;
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
oldParts.length === 0
|
|
52
|
+
|| newParts.length === 0
|
|
53
|
+
|| oldParts.length > MAX_PARTS
|
|
54
|
+
|| newParts.length > MAX_PARTS
|
|
55
|
+
|| oldParts.length * newParts.length > MAX_PRODUCT
|
|
56
|
+
) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const edits = computePartEdits(oldParts, newParts);
|
|
61
|
+
const patches = [];
|
|
62
|
+
let oldOffset = 0;
|
|
63
|
+
let newOffset = 0;
|
|
64
|
+
let group = null;
|
|
65
|
+
|
|
66
|
+
for (const edit of edits) {
|
|
67
|
+
if (edit.type === 'equal') {
|
|
68
|
+
flushGroup();
|
|
69
|
+
oldOffset += oldParts[edit.oldIndex].length;
|
|
70
|
+
newOffset += newParts[edit.newIndex].length;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!group) {
|
|
75
|
+
group = {
|
|
76
|
+
oldStart: oldOffset,
|
|
77
|
+
oldText: '',
|
|
78
|
+
newText: ''
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (edit.type === 'remove') {
|
|
83
|
+
const text = oldParts[edit.oldIndex];
|
|
84
|
+
group.oldText += text;
|
|
85
|
+
oldOffset += text.length;
|
|
86
|
+
} else if (edit.type === 'add') {
|
|
87
|
+
const text = newParts[edit.newIndex];
|
|
88
|
+
group.newText += text;
|
|
89
|
+
newOffset += text.length;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
flushGroup();
|
|
93
|
+
|
|
94
|
+
return patches;
|
|
95
|
+
|
|
96
|
+
function flushGroup() {
|
|
97
|
+
if (!group) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (group.oldText !== group.newText) {
|
|
101
|
+
const tokenPatches = computeTokenAnchoredPatches(group.oldText, group.newText, group.oldStart);
|
|
102
|
+
if (tokenPatches) {
|
|
103
|
+
patches.push(...tokenPatches);
|
|
104
|
+
} else {
|
|
105
|
+
patches.push(computeSingleTextPatch(group.oldText, group.newText, group.oldStart));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
group = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function computeTokenAnchoredPatches(oldValue, newValue, offset = 0) {
|
|
113
|
+
const MAX_GROUP_CHARS = 20000;
|
|
114
|
+
const MAX_TOKENS = 3000;
|
|
115
|
+
const MAX_PRODUCT = 4000000;
|
|
116
|
+
const MAX_PATCHES = 80;
|
|
117
|
+
|
|
118
|
+
if (
|
|
119
|
+
oldValue.length > MAX_GROUP_CHARS
|
|
120
|
+
|| newValue.length > MAX_GROUP_CHARS
|
|
121
|
+
) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const oldTokens = splitTextTokens(oldValue);
|
|
126
|
+
const newTokens = splitTextTokens(newValue);
|
|
127
|
+
if (
|
|
128
|
+
oldTokens.length === 0
|
|
129
|
+
|| newTokens.length === 0
|
|
130
|
+
|| oldTokens.length > MAX_TOKENS
|
|
131
|
+
|| newTokens.length > MAX_TOKENS
|
|
132
|
+
|| oldTokens.length * newTokens.length > MAX_PRODUCT
|
|
133
|
+
) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const edits = computePartEdits(
|
|
138
|
+
oldTokens.map(token => token.text),
|
|
139
|
+
newTokens.map(token => token.text)
|
|
140
|
+
);
|
|
141
|
+
const patches = [];
|
|
142
|
+
let oldOffset = 0;
|
|
143
|
+
let newOffset = 0;
|
|
144
|
+
let group = null;
|
|
145
|
+
|
|
146
|
+
for (const edit of edits) {
|
|
147
|
+
if (edit.type === 'equal') {
|
|
148
|
+
flushGroup();
|
|
149
|
+
oldOffset = oldTokens[edit.oldIndex].end;
|
|
150
|
+
newOffset = newTokens[edit.newIndex].end;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!group) {
|
|
155
|
+
group = {
|
|
156
|
+
oldStart: oldOffset,
|
|
157
|
+
newStart: newOffset,
|
|
158
|
+
oldEnd: oldOffset,
|
|
159
|
+
newEnd: newOffset
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (edit.type === 'remove') {
|
|
164
|
+
group.oldEnd = oldTokens[edit.oldIndex].end;
|
|
165
|
+
oldOffset = group.oldEnd;
|
|
166
|
+
} else if (edit.type === 'add') {
|
|
167
|
+
group.newEnd = newTokens[edit.newIndex].end;
|
|
168
|
+
newOffset = group.newEnd;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
flushGroup();
|
|
172
|
+
|
|
173
|
+
if (!patches.length || patches.length > MAX_PATCHES) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return patches;
|
|
177
|
+
|
|
178
|
+
function flushGroup() {
|
|
179
|
+
if (!group) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const oldText = oldValue.slice(group.oldStart, group.oldEnd);
|
|
183
|
+
const newText = newValue.slice(group.newStart, group.newEnd);
|
|
184
|
+
if (oldText !== newText) {
|
|
185
|
+
patches.push({
|
|
186
|
+
from: offset + group.oldStart,
|
|
187
|
+
to: offset + group.oldEnd,
|
|
188
|
+
expected: oldText,
|
|
189
|
+
insert: newText
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
group = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function splitTextTokens(text) {
|
|
197
|
+
const tokens = [];
|
|
198
|
+
let index = 0;
|
|
199
|
+
while (index < text.length) {
|
|
200
|
+
const start = index;
|
|
201
|
+
const char = text[index];
|
|
202
|
+
if (/\s/.test(char)) {
|
|
203
|
+
index += 1;
|
|
204
|
+
while (index < text.length && /\s/.test(text[index])) {
|
|
205
|
+
index += 1;
|
|
206
|
+
}
|
|
207
|
+
} else if (char === '\\') {
|
|
208
|
+
index += 1;
|
|
209
|
+
if (index < text.length && /[A-Za-z@]/.test(text[index])) {
|
|
210
|
+
while (index < text.length && /[A-Za-z@]/.test(text[index])) {
|
|
211
|
+
index += 1;
|
|
212
|
+
}
|
|
213
|
+
} else if (index < text.length) {
|
|
214
|
+
index += 1;
|
|
215
|
+
}
|
|
216
|
+
} else if (/[A-Za-z0-9_]/.test(char)) {
|
|
217
|
+
index += 1;
|
|
218
|
+
while (index < text.length && /[A-Za-z0-9_]/.test(text[index])) {
|
|
219
|
+
index += 1;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
index += 1;
|
|
223
|
+
while (
|
|
224
|
+
index < text.length
|
|
225
|
+
&& !/\s/.test(text[index])
|
|
226
|
+
&& text[index] !== '\\'
|
|
227
|
+
&& !/[A-Za-z0-9_]/.test(text[index])
|
|
228
|
+
) {
|
|
229
|
+
index += 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
tokens.push({
|
|
233
|
+
text: text.slice(start, index),
|
|
234
|
+
start,
|
|
235
|
+
end: index
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return tokens;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function splitTextParts(text) {
|
|
242
|
+
return String(text || '').match(/[^\n]*\n|[^\n]+/g) || [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function computePartEdits(oldParts, newParts) {
|
|
246
|
+
const n = oldParts.length;
|
|
247
|
+
const m = newParts.length;
|
|
248
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
249
|
+
|
|
250
|
+
for (let i = n - 1; i >= 0; i -= 1) {
|
|
251
|
+
for (let j = m - 1; j >= 0; j -= 1) {
|
|
252
|
+
dp[i][j] = oldParts[i] === newParts[j]
|
|
253
|
+
? dp[i + 1][j + 1] + 1
|
|
254
|
+
: Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const edits = [];
|
|
259
|
+
let i = 0;
|
|
260
|
+
let j = 0;
|
|
261
|
+
while (i < n && j < m) {
|
|
262
|
+
if (oldParts[i] === newParts[j]) {
|
|
263
|
+
edits.push({ type: 'equal', oldIndex: i, newIndex: j });
|
|
264
|
+
i += 1;
|
|
265
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
266
|
+
edits.push({ type: 'remove', oldIndex: i });
|
|
267
|
+
i += 1;
|
|
268
|
+
continue;
|
|
269
|
+
} else {
|
|
270
|
+
edits.push({ type: 'add', newIndex: j });
|
|
271
|
+
}
|
|
272
|
+
j += 1;
|
|
273
|
+
}
|
|
274
|
+
while (i < n) {
|
|
275
|
+
edits.push({ type: 'remove', oldIndex: i });
|
|
276
|
+
i += 1;
|
|
277
|
+
}
|
|
278
|
+
while (j < m) {
|
|
279
|
+
edits.push({ type: 'add', newIndex: j });
|
|
280
|
+
j += 1;
|
|
281
|
+
}
|
|
282
|
+
return edits;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
computeTextPatches
|
|
287
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-overleaf-link",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Cross-platform Chrome bridge that connects Codex to the active Overleaf project.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"agent:codex": "node scripts/codex-json-agent.mjs",
|
|
9
|
+
"test": "node scripts/run-tests.mjs",
|
|
10
|
+
"check:architecture": "node scripts/check-architecture-budget.mjs --enforce-target",
|
|
11
|
+
"benchmark:large": "node scripts/benchmark-large-project.mjs --gate",
|
|
12
|
+
"smoke:extension": "node scripts/smoke-extension.mjs",
|
|
13
|
+
"verify:release": "node scripts/verify-release.mjs",
|
|
14
|
+
"build:release": "node scripts/build-release.mjs",
|
|
15
|
+
"install:native": "node scripts/install-native-host.mjs",
|
|
16
|
+
"uninstall:native": "node scripts/uninstall-native-host.mjs",
|
|
17
|
+
"bridge": "node native-host/src/index.js",
|
|
18
|
+
"verify:npm-package": "node scripts/verify-npm-package.mjs",
|
|
19
|
+
"pack:npm": "node scripts/verify-npm-package.mjs --pack"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
24
|
+
"packageManager": "npm@11.11.0",
|
|
25
|
+
"bin": {
|
|
26
|
+
"codex-overleaf-link": "bin/codex-overleaf-link.mjs"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/",
|
|
30
|
+
"native-host/src/",
|
|
31
|
+
"extension/src/shared/",
|
|
32
|
+
"scripts/codex-json-agent.mjs",
|
|
33
|
+
"scripts/install-native-host.mjs",
|
|
34
|
+
"scripts/uninstall-native-host.mjs",
|
|
35
|
+
"scripts/verify-npm-package.mjs",
|
|
36
|
+
"scripts/npm-package-files-v1.1.1.txt",
|
|
37
|
+
"README.md",
|
|
38
|
+
"package.json"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { buildCodexPrompt, buildOutputSchema } = require('../native-host/src/codexPrompt');
|
|
10
|
+
const { buildCodexExecArgs } = require('../native-host/src/codexArgs');
|
|
11
|
+
|
|
12
|
+
const input = await readStdin();
|
|
13
|
+
const request = JSON.parse(input || '{}');
|
|
14
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-overleaf-agent-'));
|
|
15
|
+
const schemaPath = path.join(tempDir, 'output-schema.json');
|
|
16
|
+
const outputPath = path.join(tempDir, 'last-message.json');
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
emitEvent('agent.snapshot.preparing', 'Preparing project snapshot for Codex', {
|
|
20
|
+
fileCount: request.project?.files?.length || 0,
|
|
21
|
+
totalChars: (request.project?.files || []).reduce((sum, file) => sum + String(file?.content || '').length, 0)
|
|
22
|
+
});
|
|
23
|
+
fs.writeFileSync(schemaPath, JSON.stringify(buildOutputSchema(), null, 2), 'utf8');
|
|
24
|
+
writeSnapshotFiles(tempDir, request.project?.files || []);
|
|
25
|
+
emitEvent('agent.snapshot.ready', 'Project snapshot written to temp workspace', {
|
|
26
|
+
tempDir,
|
|
27
|
+
fileCount: request.project?.files?.length || 0
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const prompt = buildCodexPrompt(request);
|
|
31
|
+
emitEvent('codex.prompt.ready', 'Codex prompt prepared', {
|
|
32
|
+
taskLength: String(request.task || '').length,
|
|
33
|
+
promptLength: prompt.length,
|
|
34
|
+
model: request.model,
|
|
35
|
+
reasoningEffort: request.reasoningEffort,
|
|
36
|
+
speedTier: request.speedTier
|
|
37
|
+
});
|
|
38
|
+
const result = await runCodexExec({
|
|
39
|
+
cwd: tempDir,
|
|
40
|
+
prompt,
|
|
41
|
+
schemaPath,
|
|
42
|
+
outputPath,
|
|
43
|
+
model: request.model,
|
|
44
|
+
reasoningEffort: request.reasoningEffort,
|
|
45
|
+
speedTier: request.speedTier
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
process.stdout.write(JSON.stringify(result));
|
|
49
|
+
} finally {
|
|
50
|
+
if (process.env.CODEX_OVERLEAF_KEEP_TEMP !== '1') {
|
|
51
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function emitEvent(type, title, detail = {}, status = 'running') {
|
|
56
|
+
process.stderr.write(`CODEX_OVERLEAF_EVENT ${JSON.stringify({
|
|
57
|
+
type,
|
|
58
|
+
title,
|
|
59
|
+
status,
|
|
60
|
+
detail,
|
|
61
|
+
timestamp: new Date().toISOString()
|
|
62
|
+
})}\n`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readStdin() {
|
|
66
|
+
return new Promise(resolve => {
|
|
67
|
+
let data = '';
|
|
68
|
+
process.stdin.setEncoding('utf8');
|
|
69
|
+
process.stdin.on('data', chunk => {
|
|
70
|
+
data += chunk;
|
|
71
|
+
});
|
|
72
|
+
process.stdin.on('end', () => resolve(data));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeSnapshotFiles(root, files) {
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
if (!file?.path || typeof file.content !== 'string') {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const target = path.resolve(root, file.path);
|
|
83
|
+
if (!target.startsWith(root + path.sep)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
88
|
+
fs.writeFileSync(target, file.content, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runCodexExec({ cwd, prompt, schemaPath, outputPath, model, reasoningEffort, speedTier }) {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const codexCommand = process.env.CODEX_OVERLEAF_CODEX_PATH || 'codex';
|
|
95
|
+
const child = spawn(codexCommand, buildCodexExecArgs({
|
|
96
|
+
cwd,
|
|
97
|
+
schemaPath,
|
|
98
|
+
outputPath,
|
|
99
|
+
model,
|
|
100
|
+
reasoningEffort,
|
|
101
|
+
speedTier
|
|
102
|
+
}), {
|
|
103
|
+
shell: shouldUseShellForCommand(codexCommand),
|
|
104
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
let stderr = '';
|
|
108
|
+
let stdoutRemainder = '';
|
|
109
|
+
child.stdout.setEncoding('utf8');
|
|
110
|
+
child.stdout.on('data', chunk => {
|
|
111
|
+
const parsed = parseCodexJsonLines(`${stdoutRemainder}${chunk}`);
|
|
112
|
+
stdoutRemainder = parsed.remainder;
|
|
113
|
+
for (const event of parsed.events) {
|
|
114
|
+
emitCodexRuntimeEvent(event);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
child.stderr.setEncoding('utf8');
|
|
118
|
+
child.stderr.on('data', chunk => {
|
|
119
|
+
stderr += chunk;
|
|
120
|
+
});
|
|
121
|
+
child.on('error', reject);
|
|
122
|
+
child.on('close', code => {
|
|
123
|
+
if (stdoutRemainder) {
|
|
124
|
+
const parsed = parseCodexJsonLines(`${stdoutRemainder}\n`);
|
|
125
|
+
stdoutRemainder = '';
|
|
126
|
+
for (const event of parsed.events) {
|
|
127
|
+
emitCodexRuntimeEvent(event);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
emitEvent('codex.exec.completed', 'Codex exec exited', {
|
|
131
|
+
code,
|
|
132
|
+
stderrLength: stderr.length
|
|
133
|
+
}, code === 0 ? 'completed' : 'failed');
|
|
134
|
+
if (code !== 0) {
|
|
135
|
+
reject(new Error(stderr || `codex exec exited with code ${code}`));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
resolve(JSON.parse(fs.readFileSync(outputPath, 'utf8')));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
reject(new Error(`Could not parse Codex output: ${error.message}`));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
emitEvent('codex.exec.started', 'Codex exec started', {
|
|
147
|
+
pid: child.pid,
|
|
148
|
+
cwd,
|
|
149
|
+
model,
|
|
150
|
+
reasoningEffort,
|
|
151
|
+
speedTier
|
|
152
|
+
});
|
|
153
|
+
child.stdin.end(prompt);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function shouldUseShellForCommand(command) {
|
|
158
|
+
if (process.platform !== 'win32') {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const text = String(command || '');
|
|
162
|
+
return text === 'codex' || /\.(?:cmd|bat)$/i.test(text);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseCodexJsonLines(text) {
|
|
166
|
+
const lines = text.split(/\r?\n/);
|
|
167
|
+
const remainder = lines.pop() || '';
|
|
168
|
+
const events = [];
|
|
169
|
+
|
|
170
|
+
for (const line of lines) {
|
|
171
|
+
const trimmed = line.trim();
|
|
172
|
+
if (!trimmed) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
events.push(JSON.parse(trimmed));
|
|
177
|
+
} catch {
|
|
178
|
+
emitEvent('codex.stdout.line', 'Codex produced non-JSON output', {
|
|
179
|
+
text: truncateInline(trimmed, 500)
|
|
180
|
+
}, 'running');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { events, remainder };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function emitCodexRuntimeEvent(event) {
|
|
188
|
+
if (event?.type === 'item.completed' || event?.type === 'item.started') {
|
|
189
|
+
emitCodexItemEvent(event);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (event?.type === 'turn.started') {
|
|
194
|
+
emitEvent('codex.turn.started', 'Codex started this turn', {}, 'running');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (event?.type === 'turn.completed') {
|
|
199
|
+
emitEvent('codex.turn.completed', 'Codex completed this turn', {
|
|
200
|
+
usage: event.usage || {}
|
|
201
|
+
}, 'completed');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
emitEvent(`codex.${event?.type || 'event'}`, event?.type || 'Codex event', {
|
|
206
|
+
event
|
|
207
|
+
}, 'running');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function emitCodexItemEvent(event) {
|
|
211
|
+
const item = event.item || {};
|
|
212
|
+
|
|
213
|
+
if (item.type === 'agent_message') {
|
|
214
|
+
const text = String(item.text || '').trim();
|
|
215
|
+
if (!text) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (looksLikeStructuredFinalMessage(text)) {
|
|
219
|
+
emitEvent('codex.agent.result', 'Codex generated structured result', {
|
|
220
|
+
textLength: text.length
|
|
221
|
+
}, 'completed');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
emitEvent('codex.agent.message', truncateInline(text, 160), {
|
|
225
|
+
text
|
|
226
|
+
}, 'completed');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (item.type === 'command_execution') {
|
|
231
|
+
if (event.type === 'item.started') {
|
|
232
|
+
emitEvent('codex.command.started', `Codex is running: ${truncateInline(item.command, 120)}`, {
|
|
233
|
+
command: item.command || ''
|
|
234
|
+
}, 'running');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
emitEvent('codex.command.completed', item.exit_code === 0
|
|
239
|
+
? `Command completed: ${truncateInline(item.command, 120)}`
|
|
240
|
+
: `Command failed: ${truncateInline(item.command, 120)}`, {
|
|
241
|
+
command: item.command || '',
|
|
242
|
+
output: item.aggregated_output || '',
|
|
243
|
+
exitCode: item.exit_code,
|
|
244
|
+
status: item.status || ''
|
|
245
|
+
}, item.exit_code === 0 ? 'completed' : 'failed');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
emitEvent('codex.item.completed', `Codex item: ${item.type || 'unknown'}`, {
|
|
250
|
+
item
|
|
251
|
+
}, event.type === 'item.completed' ? 'completed' : 'running');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function looksLikeStructuredFinalMessage(text) {
|
|
255
|
+
try {
|
|
256
|
+
const value = JSON.parse(text);
|
|
257
|
+
return value && typeof value === 'object' && (
|
|
258
|
+
Object.prototype.hasOwnProperty.call(value, 'operations') ||
|
|
259
|
+
Object.prototype.hasOwnProperty.call(value, 'status')
|
|
260
|
+
);
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function truncateInline(text, maxLength) {
|
|
267
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
268
|
+
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
|
269
|
+
}
|