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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +457 -0
  3. package/bin/codex-overleaf-link.mjs +223 -0
  4. package/extension/src/shared/agentTranscript.js +1175 -0
  5. package/extension/src/shared/auditRecords.js +568 -0
  6. package/extension/src/shared/compatibility.js +372 -0
  7. package/extension/src/shared/compileAdapter.js +176 -0
  8. package/extension/src/shared/governanceRules.js +252 -0
  9. package/extension/src/shared/i18n.js +565 -0
  10. package/extension/src/shared/models.js +106 -0
  11. package/extension/src/shared/otText.js +505 -0
  12. package/extension/src/shared/projectFiles.js +180 -0
  13. package/extension/src/shared/reviewing.js +99 -0
  14. package/extension/src/shared/sensitiveScan.js +116 -0
  15. package/extension/src/shared/sessionState.js +1084 -0
  16. package/extension/src/shared/staleGuard.js +150 -0
  17. package/extension/src/shared/storageDb.js +986 -0
  18. package/extension/src/shared/storageKeys.js +29 -0
  19. package/extension/src/shared/storageMigration.js +168 -0
  20. package/extension/src/shared/summary.js +248 -0
  21. package/extension/src/shared/undoOperations.js +369 -0
  22. package/native-host/src/codexArgs.js +43 -0
  23. package/native-host/src/codexHome.js +538 -0
  24. package/native-host/src/codexModels.js +247 -0
  25. package/native-host/src/codexPrompt.js +192 -0
  26. package/native-host/src/codexPromptAssembly.js +411 -0
  27. package/native-host/src/codexSessionRunner.js +1247 -0
  28. package/native-host/src/commandApproval.js +914 -0
  29. package/native-host/src/debugLog.js +78 -0
  30. package/native-host/src/diffEngine.js +247 -0
  31. package/native-host/src/index.js +132 -0
  32. package/native-host/src/launcher.js +81 -0
  33. package/native-host/src/localSkills.js +476 -0
  34. package/native-host/src/manifest.js +226 -0
  35. package/native-host/src/mirrorSensitiveScan.js +119 -0
  36. package/native-host/src/mirrorWorkspace.js +1019 -0
  37. package/native-host/src/nativeDoctor.js +826 -0
  38. package/native-host/src/nativeEnvironment.js +315 -0
  39. package/native-host/src/nativeHostPlatform.js +112 -0
  40. package/native-host/src/nativeMessaging.js +60 -0
  41. package/native-host/src/nativeQuotas.js +294 -0
  42. package/native-host/src/nativeResponseBudget.js +194 -0
  43. package/native-host/src/runtimeInstaller.js +357 -0
  44. package/native-host/src/taskRunner.js +3 -0
  45. package/native-host/src/taskRunnerRuntime.js +1083 -0
  46. package/native-host/src/textPatch.js +287 -0
  47. package/package.json +40 -0
  48. package/scripts/codex-json-agent.mjs +269 -0
  49. package/scripts/install-native-host.mjs +255 -0
  50. package/scripts/npm-package-files-v1.1.1.txt +52 -0
  51. package/scripts/uninstall-native-host.mjs +298 -0
  52. package/scripts/verify-npm-package.mjs +296 -0
@@ -0,0 +1,1247 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('node:child_process');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { collectMirrorChangesDetailed, getProjectMirror, markMirrorDirty, syncOverleafToMirror } = require('./mirrorWorkspace');
7
+ const { computeLineDiff } = require('./diffEngine');
8
+ const { computeTextPatches } = require('./textPatch');
9
+ const { buildCodexHomeEnv } = require('./codexHome');
10
+ const { buildCodexSpeedArgs } = require('./codexArgs');
11
+ const { truncateText } = require('./debugLog');
12
+ const { enforceNativeOkResponseBudget } = require('./nativeResponseBudget');
13
+ const { buildCodexTurnPrompt: buildCodexPromptParts } = require('./codexPromptAssembly');
14
+ const { evaluateSkillCommand } = require('./commandApproval');
15
+ const {
16
+ getCodexOverleafSkillsRoot,
17
+ loadSelectedCodexOverleafSkill,
18
+ loadSelectedProjectSkills
19
+ } = require('./localSkills');
20
+
21
+ const TURN_ATTACHMENTS_DIR = '.codex-overleaf-attachments';
22
+ const MAX_TURN_ATTACHMENT_BYTES = 12 * 1024 * 1024;
23
+ const MAX_TURN_ATTACHMENTS = 8;
24
+ const MAX_TURN_ATTACHMENT_TOTAL_BYTES = MAX_TURN_ATTACHMENT_BYTES * MAX_TURN_ATTACHMENTS;
25
+
26
+ async function runCodexSession({ params = {}, env = process.env, emit = () => {}, rootDir, executeCodex, signal } = {}) {
27
+ throwIfAborted(signal);
28
+ const projectId = params.projectId || params.project?.projectId || params.project?.id || params.project?.url || 'overleaf-project';
29
+ const skillInvocation = normalizeSkillInvocation(params.skillInvocation);
30
+ const skillInstallTurn = isSkillInstallerInvocation(skillInvocation);
31
+ if (skillInstallTurn && Array.isArray(params.attachments) && params.attachments.length) {
32
+ throw new Error('Skill installer turns do not accept attachments');
33
+ }
34
+
35
+ let mirror;
36
+ if (params.skipMirrorSync) {
37
+ mirror = getProjectMirror(projectId, { rootDir });
38
+ mirror.fileCount = 0;
39
+ } else {
40
+ emitCodexEvent(emit, 'overleaf.sync.started', 'Syncing Overleaf project to local workspace', {
41
+ projectId,
42
+ fileCount: Array.isArray(params.project?.files) ? params.project.files.length : 0
43
+ });
44
+
45
+ mirror = await syncOverleafToMirror({
46
+ projectId,
47
+ project: params.project || { files: [] },
48
+ rootDir
49
+ });
50
+ throwIfAborted(signal);
51
+
52
+ emitCodexEvent(emit, 'overleaf.sync.completed', 'Overleaf project synced to local workspace', {
53
+ projectId: mirror.projectKey,
54
+ workspacePath: mirror.workspacePath,
55
+ fileCount: mirror.fileCount
56
+ }, 'completed');
57
+ }
58
+
59
+ const projectLocalSkills = loadProjectLocalSkillsContext(params, mirror);
60
+ if (projectLocalSkills.missing.length) {
61
+ emitCodexEvent(emit, 'codex.local_skills.missing', 'Selected project-local skills were missing', {
62
+ missingSkillIds: projectLocalSkills.missing
63
+ }, 'failed');
64
+ }
65
+ const turnAttachments = materializeTurnAttachments(params.attachments, mirror.workspacePath);
66
+ const settings = buildCodexSettings(params);
67
+ const skillLoading = normalizeSkillLoadingSettings(params);
68
+ const codexSkillInvocationContext = loadCodexSkillInvocationContext({
69
+ skillInvocation,
70
+ loadCodexOverleafSkills: skillLoading.loadCodexOverleafSkills,
71
+ env,
72
+ emit
73
+ });
74
+ const effectiveSkillInvocation = getEffectiveSkillInvocation(codexSkillInvocationContext);
75
+ const runnerWorkspacePath = skillInstallTurn
76
+ ? getCodexOverleafSkillsRoot({ env })
77
+ : mirror.workspacePath;
78
+ if (skillInstallTurn) {
79
+ fs.mkdirSync(runnerWorkspacePath, { recursive: true });
80
+ }
81
+ const runner = executeCodex || runCodexAppServerSession;
82
+ const runnerResult = await runner({
83
+ workspacePath: runnerWorkspacePath,
84
+ task: buildCodexTurnPrompt(params, mirror, projectLocalSkills, turnAttachments, codexSkillInvocationContext),
85
+ userTask: String(params.task || ''),
86
+ session: params.session || null,
87
+ threadId: params.threadId || '',
88
+ mode: params.mode || 'auto',
89
+ model: params.model || '',
90
+ reasoningEffort: params.reasoningEffort || '',
91
+ speedTier: normalizeSpeedTier(params.speedTier),
92
+ loadCodexLocalSkills: skillLoading.loadCodexLocalSkills,
93
+ loadCodexOverleafSkills: skillLoading.loadCodexOverleafSkills,
94
+ skillInvocation: effectiveSkillInvocation,
95
+ installCodexOverleafSkillsTarget: skillInstallTurn,
96
+ projectLocalSkills: null,
97
+ sandboxMode: settings.sandboxMode,
98
+ approvalPolicy: settings.approvalPolicy,
99
+ env,
100
+ emit,
101
+ signal
102
+ });
103
+ throwIfAborted(signal);
104
+
105
+ if (skillInstallTurn) {
106
+ return enforceNativeOkResponseBudget({
107
+ status: 'completed',
108
+ projectId: mirror.projectKey,
109
+ workspacePath: mirror.workspacePath,
110
+ assistantMessage: cleanAssistantMessage(runnerResult?.assistantMessage),
111
+ threadId: runnerResult?.threadId || '',
112
+ syncChanges: [],
113
+ unsupportedChanges: []
114
+ });
115
+ }
116
+
117
+ const collected = await collectMirrorChangesDetailed({
118
+ projectId,
119
+ rootDir
120
+ });
121
+ const filteredChanges = filterSyncChangesForFocus({
122
+ changes: collected.changes || [],
123
+ focusFiles: params.focusFiles || params.session?.focusFiles,
124
+ restrictToFocusFiles: params.restrictToFocusFiles
125
+ });
126
+ const rawSyncChanges = filteredChanges.changes;
127
+ const unsupportedChanges = [
128
+ ...(collected.unsupportedChanges || []),
129
+ ...filteredChanges.unsupportedChanges
130
+ ];
131
+ if (rawSyncChanges.length || unsupportedChanges.length) {
132
+ markMirrorDirty({
133
+ projectId,
134
+ rootDir,
135
+ reason: params.mode === 'ask' ? 'ask_mode_local_changes' : 'codex_run_local_changes'
136
+ });
137
+ }
138
+ throwIfAborted(signal);
139
+
140
+ if (params.mode === 'ask') {
141
+ emitCodexEvent(emit, 'overleaf.sync.changes', 'Ask mode finished without Overleaf writeback', {
142
+ changedCount: 0,
143
+ files: [],
144
+ unsupportedCount: 0,
145
+ unsupportedFiles: [],
146
+ ignoredChangedCount: rawSyncChanges.length,
147
+ ignoredUnsupportedCount: unsupportedChanges.length
148
+ }, rawSyncChanges.length || unsupportedChanges.length ? 'warning' : 'completed');
149
+ return enforceNativeOkResponseBudget({
150
+ status: 'completed',
151
+ projectId: mirror.projectKey,
152
+ workspacePath: mirror.workspacePath,
153
+ assistantMessage: cleanAssistantMessage(runnerResult?.assistantMessage),
154
+ threadId: runnerResult?.threadId || '',
155
+ syncChanges: [],
156
+ unsupportedChanges: []
157
+ });
158
+ }
159
+
160
+ const syncChanges = rawSyncChanges.map(change => {
161
+ if (change.type === 'write' && typeof change.previousContent === 'string') {
162
+ return {
163
+ ...change,
164
+ diff: computeLineDiff(change.previousContent, change.content),
165
+ patches: computeTextPatches(change.previousContent, change.content)
166
+ };
167
+ }
168
+ return change;
169
+ });
170
+ const response = enforceNativeOkResponseBudget({
171
+ status: 'completed',
172
+ projectId: mirror.projectKey,
173
+ workspacePath: mirror.workspacePath,
174
+ assistantMessage: cleanAssistantMessage(runnerResult?.assistantMessage),
175
+ threadId: runnerResult?.threadId || '',
176
+ syncChanges,
177
+ unsupportedChanges
178
+ });
179
+
180
+ emitCodexEvent(emit, 'overleaf.sync.changes', 'Local Codex changes collected for Overleaf sync', {
181
+ changedCount: response.syncChanges.length,
182
+ files: response.syncChanges.map(change => change.path),
183
+ unsupportedCount: response.unsupportedChanges.length,
184
+ unsupportedFiles: response.unsupportedChanges.map(change => change.path)
185
+ }, 'completed');
186
+
187
+ return response;
188
+ }
189
+
190
+ function buildCodexTurnPrompt(params = {}, mirror = {}, projectLocalSkills, turnAttachments = [], codexSkillInvocationContext = null) {
191
+ const prompt = buildCodexPromptParts({
192
+ params,
193
+ mirror,
194
+ projectLocalSkills,
195
+ turnAttachments,
196
+ codexSkillInvocationContext
197
+ });
198
+ return [prompt.systemPrompt, prompt.userPrompt].filter(Boolean).join('\n\n');
199
+ }
200
+
201
+ function materializeTurnAttachments(attachments = [], workspacePath = '') {
202
+ if (!workspacePath) {
203
+ return [];
204
+ }
205
+ const attachmentDir = path.join(workspacePath, TURN_ATTACHMENTS_DIR);
206
+ fs.rmSync(attachmentDir, { recursive: true, force: true });
207
+
208
+ const normalized = normalizeTurnAttachments(attachments);
209
+ if (!normalized.length) {
210
+ return [];
211
+ }
212
+
213
+ fs.mkdirSync(attachmentDir, { recursive: true });
214
+ const usedNames = new Set();
215
+ return normalized.map(attachment => {
216
+ const fileName = dedupeAttachmentFileName(attachment.name, usedNames);
217
+ const target = path.join(attachmentDir, fileName);
218
+ const resolvedTarget = path.resolve(target);
219
+ const resolvedDir = path.resolve(attachmentDir);
220
+ if (!resolvedTarget.startsWith(resolvedDir + path.sep)) {
221
+ throw new Error('Unsafe attachment path');
222
+ }
223
+ fs.writeFileSync(target, attachment.bytes);
224
+ return {
225
+ name: fileName,
226
+ path: `${TURN_ATTACHMENTS_DIR}/${fileName}`,
227
+ mimeType: attachment.mimeType,
228
+ size: attachment.bytes.length
229
+ };
230
+ });
231
+ }
232
+
233
+ function normalizeTurnAttachments(value) {
234
+ const input = Array.isArray(value) ? value : [];
235
+ if (input.length > MAX_TURN_ATTACHMENTS) {
236
+ throw new Error(`Too many attachments (${input.length}/${MAX_TURN_ATTACHMENTS})`);
237
+ }
238
+ const result = [];
239
+ let totalBytes = 0;
240
+ for (const item of input) {
241
+ const name = sanitizeAttachmentFileName(item?.name);
242
+ const contentBase64 = String(item?.contentBase64 || '').replace(/\s+/g, '');
243
+ if (!name || !contentBase64) {
244
+ continue;
245
+ }
246
+ const declared = Number(item?.size);
247
+ const estimatedBytes = Math.max(
248
+ Number.isFinite(declared) && declared > 0 ? declared : 0,
249
+ estimateBase64DecodedBytes(contentBase64)
250
+ );
251
+ if (estimatedBytes > MAX_TURN_ATTACHMENT_BYTES) {
252
+ throw new Error(`Attachment is too large: ${name}`);
253
+ }
254
+ const bytes = Buffer.from(contentBase64, 'base64');
255
+ if (!bytes.length) {
256
+ continue;
257
+ }
258
+ if (bytes.length > MAX_TURN_ATTACHMENT_BYTES) {
259
+ throw new Error(`Attachment is too large: ${name}`);
260
+ }
261
+ totalBytes += Math.max(estimatedBytes, bytes.length);
262
+ if (totalBytes > MAX_TURN_ATTACHMENT_TOTAL_BYTES) {
263
+ throw new Error(`Attachments are too large (${totalBytes}/${MAX_TURN_ATTACHMENT_TOTAL_BYTES} bytes)`);
264
+ }
265
+ result.push({
266
+ name,
267
+ mimeType: String(item?.mimeType || '').trim().slice(0, 120),
268
+ bytes
269
+ });
270
+ }
271
+ return result;
272
+ }
273
+
274
+ function estimateBase64DecodedBytes(value) {
275
+ const clean = String(value || '').replace(/\s+/g, '');
276
+ if (!clean) {
277
+ return 0;
278
+ }
279
+ const padding = clean.endsWith('==') ? 2 : clean.endsWith('=') ? 1 : 0;
280
+ return Math.max(0, Math.floor(clean.length * 3 / 4) - padding);
281
+ }
282
+
283
+ function sanitizeAttachmentFileName(value) {
284
+ const basename = String(value || '')
285
+ .replace(/\0/g, '')
286
+ .replace(/\\/g, '/')
287
+ .split('/')
288
+ .filter(Boolean)
289
+ .pop()
290
+ ?.trim()
291
+ .slice(0, 180) || '';
292
+ return basename.replace(/[/:]/g, '-');
293
+ }
294
+
295
+ function dedupeAttachmentFileName(name, usedNames) {
296
+ let candidate = name || 'attachment';
297
+ if (!usedNames.has(candidate)) {
298
+ usedNames.add(candidate);
299
+ return candidate;
300
+ }
301
+ const parsed = path.parse(candidate);
302
+ let index = 2;
303
+ do {
304
+ candidate = `${parsed.name}-${index}${parsed.ext}`;
305
+ index += 1;
306
+ } while (usedNames.has(candidate));
307
+ usedNames.add(candidate);
308
+ return candidate;
309
+ }
310
+
311
+ function normalizeFocusFiles(value) {
312
+ const seen = new Set();
313
+ const files = [];
314
+ for (const item of Array.isArray(value) ? value : []) {
315
+ const filePath = normalizeProjectPath(item);
316
+ if (!filePath || seen.has(filePath)) {
317
+ continue;
318
+ }
319
+ seen.add(filePath);
320
+ files.push(filePath);
321
+ if (files.length >= 8) {
322
+ break;
323
+ }
324
+ }
325
+ return files;
326
+ }
327
+
328
+ function filterSyncChangesForFocus({ changes = [], focusFiles = [], restrictToFocusFiles = false } = {}) {
329
+ if (!restrictToFocusFiles) {
330
+ return { changes, unsupportedChanges: [] };
331
+ }
332
+ const focusSet = new Set(normalizeFocusFiles(focusFiles));
333
+ if (!focusSet.size) {
334
+ return { changes, unsupportedChanges: [] };
335
+ }
336
+
337
+ const accepted = [];
338
+ const rejected = [];
339
+ for (const change of changes || []) {
340
+ if (focusSet.has(normalizeProjectPath(change?.path))) {
341
+ accepted.push(change);
342
+ } else if (change?.path) {
343
+ rejected.push({
344
+ type: 'ignored-local-change',
345
+ path: change.path,
346
+ reason: 'out_of_focus_partial_snapshot'
347
+ });
348
+ }
349
+ }
350
+ return {
351
+ changes: accepted,
352
+ unsupportedChanges: rejected
353
+ };
354
+ }
355
+
356
+ function normalizeProjectPath(value) {
357
+ return String(value || '')
358
+ .replace(/^@file:/i, '')
359
+ .replace(/\\/g, '/')
360
+ .trim()
361
+ .replace(/^\/+/, '');
362
+ }
363
+
364
+ function loadProjectLocalSkillsContext(params = {}, mirror = {}) {
365
+ const selectedSkillIds = Array.isArray(params.selectedSkillIds) ? params.selectedSkillIds : [];
366
+ if (!selectedSkillIds.length) {
367
+ return { skills: [], missing: [], selected: [] };
368
+ }
369
+ const projectId = mirror.projectKey || params.projectId || params.project?.id || params.project?.projectId;
370
+ return loadSelectedProjectSkills({
371
+ projectId,
372
+ selectedSkillIds,
373
+ rootDir: params.rootDir,
374
+ projectRoot: mirror.projectRoot
375
+ });
376
+ }
377
+
378
+ function normalizeSkillLoadingSettings(params = {}) {
379
+ return {
380
+ loadCodexLocalSkills: params.loadCodexLocalSkills !== false,
381
+ loadCodexOverleafSkills: params.loadCodexOverleafSkills !== false
382
+ };
383
+ }
384
+
385
+ function loadCodexSkillInvocationContext({
386
+ skillInvocation,
387
+ loadCodexOverleafSkills = true,
388
+ env = process.env,
389
+ emit = () => {}
390
+ } = {}) {
391
+ const invocation = normalizeSkillInvocation(skillInvocation);
392
+ if (!invocation) {
393
+ return { invocation: null, skill: null, missing: [], ignored: [] };
394
+ }
395
+ if (isSkillInstallerInvocation(invocation)) {
396
+ return { invocation, skill: null, missing: [], ignored: [] };
397
+ }
398
+
399
+ const result = loadSelectedCodexOverleafSkill({
400
+ skillId: invocation.id,
401
+ loadCodexOverleafSkills,
402
+ env
403
+ });
404
+ if (result.missing.length) {
405
+ emitCodexEvent(emit, 'codex.overleaf_skills.missing', 'Selected Codex Overleaf skill was missing', {
406
+ missingSkillIds: result.missing
407
+ }, 'failed');
408
+ }
409
+ if (result.ignored.length) {
410
+ emitCodexEvent(emit, 'codex.overleaf_skill_invocation.ignored', 'Selected Codex Overleaf skill was ignored', {
411
+ ignoredSkillIds: result.ignored.map(item => item.id),
412
+ reason: result.ignored[0]?.reason || 'ignored'
413
+ }, 'warning');
414
+ }
415
+
416
+ return {
417
+ invocation,
418
+ skill: result.skill,
419
+ missing: result.missing,
420
+ ignored: result.ignored
421
+ };
422
+ }
423
+
424
+ function getEffectiveSkillInvocation(context = {}) {
425
+ const invocation = normalizeSkillInvocation(context.invocation);
426
+ if (!invocation) {
427
+ return null;
428
+ }
429
+ if (isSkillInstallerInvocation(invocation)) {
430
+ return invocation;
431
+ }
432
+ return context.skill ? invocation : null;
433
+ }
434
+
435
+ function normalizeSkillInvocation(value) {
436
+ const id = String(value?.id || '').trim();
437
+ if (!isSafeSkillId(id)) {
438
+ return null;
439
+ }
440
+ const title = String(value?.title || 'Skill Installer').trim().slice(0, 80) || 'Skill Installer';
441
+ if (id === 'skill-installer') {
442
+ return { id, title };
443
+ }
444
+ if (value?.scope !== 'codex-overleaf') {
445
+ return null;
446
+ }
447
+ return { id, title, scope: 'codex-overleaf' };
448
+ }
449
+
450
+ function isSkillInstallerInvocation(value) {
451
+ return normalizeSkillInvocation(value)?.id === 'skill-installer';
452
+ }
453
+
454
+ function isSafeSkillId(id) {
455
+ return /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(String(id || ''))
456
+ && !String(id || '').includes('..');
457
+ }
458
+
459
+ function buildCodexSettings(params = {}) {
460
+ if (isSkillInstallerInvocation(params.skillInvocation)) {
461
+ return {
462
+ sandboxMode: 'workspace-write',
463
+ approvalPolicy: 'never'
464
+ };
465
+ }
466
+ if (params.mode === 'ask') {
467
+ return {
468
+ sandboxMode: 'read-only',
469
+ approvalPolicy: 'never'
470
+ };
471
+ }
472
+ return {
473
+ sandboxMode: 'workspace-write',
474
+ approvalPolicy: 'never'
475
+ };
476
+ }
477
+
478
+ function buildThreadStartParams(input = {}) {
479
+ return {
480
+ cwd: input.workspacePath,
481
+ model: input.model || null,
482
+ approvalPolicy: input.approvalPolicy,
483
+ sandbox: input.sandboxMode,
484
+ experimentalRawEvents: false
485
+ };
486
+ }
487
+
488
+ function buildThreadResumeParams(input = {}) {
489
+ return {
490
+ threadId: input.threadId,
491
+ cwd: input.workspacePath,
492
+ model: input.model || null,
493
+ approvalPolicy: input.approvalPolicy,
494
+ sandbox: input.sandboxMode
495
+ };
496
+ }
497
+
498
+ function buildCodexAppServerArgs(input = {}) {
499
+ const args = [
500
+ ...buildCodexSpeedArgs(normalizeSpeedTier(input.speedTier))
501
+ ];
502
+ if (input.loadCodexLocalSkills === false) {
503
+ args.push('--disable', 'plugins');
504
+ }
505
+ args.push(
506
+ 'app-server',
507
+ '--listen',
508
+ 'stdio://'
509
+ );
510
+ return [
511
+ ...args
512
+ ];
513
+ }
514
+
515
+ async function applyCodexSkillIsolation({ input = {}, childEnv = process.env, request, emit = () => {} } = {}) {
516
+ if (input.loadCodexLocalSkills !== false) {
517
+ return { disabled: [] };
518
+ }
519
+ if (typeof request !== 'function') {
520
+ throw new Error('Codex skill isolation requires an app-server request function');
521
+ }
522
+
523
+ const listResult = await request('skills/list', {
524
+ cwd: input.workspacePath,
525
+ includeDisabled: true
526
+ });
527
+ const skills = flattenCodexSkillsList(listResult);
528
+ const disabled = [];
529
+ for (const skill of skills) {
530
+ if (skill?.enabled === false || !shouldDisableCodexSkillForIsolation(skill, input, childEnv)) {
531
+ continue;
532
+ }
533
+ const params = buildSkillDisableParams(skill);
534
+ if (!params) {
535
+ continue;
536
+ }
537
+ await request('skills/config/write', params);
538
+ disabled.push(String(skill.name || skill.path || '').trim());
539
+ }
540
+ if (disabled.length) {
541
+ emitCodexEvent(emit, 'codex.skill_isolation.applied', 'Disabled non-Overleaf Codex skills for this turn', {
542
+ disabledSkillNames: disabled.filter(Boolean)
543
+ }, 'completed');
544
+ }
545
+ return { disabled };
546
+ }
547
+
548
+ function flattenCodexSkillsList(listResult = {}) {
549
+ const data = Array.isArray(listResult?.data) ? listResult.data : [];
550
+ return data.flatMap(entry => Array.isArray(entry?.skills) ? entry.skills : []);
551
+ }
552
+
553
+ function shouldDisableCodexSkillForIsolation(skill = {}, input = {}, childEnv = process.env) {
554
+ if (isCodexSystemSkill(skill)) {
555
+ return !isAllowedSystemSkillForIsolation(skill, input);
556
+ }
557
+ return !isAllowedCodexOverleafSkillPath(skill.path, input, childEnv);
558
+ }
559
+
560
+ function isCodexSystemSkill(skill = {}) {
561
+ return String(skill.scope || '') === 'system' || isSystemSkillPath(skill.path);
562
+ }
563
+
564
+ function isAllowedSystemSkillForIsolation(skill = {}, input = {}) {
565
+ return input.installCodexOverleafSkillsTarget === true && String(skill.name || '') === 'skill-installer';
566
+ }
567
+
568
+ function isAllowedCodexOverleafSkillPath(skillPath, input = {}, childEnv = process.env) {
569
+ if (input.loadCodexOverleafSkills === false && input.installCodexOverleafSkillsTarget !== true) {
570
+ return false;
571
+ }
572
+ const pathText = String(skillPath || '');
573
+ if (!pathText || !path.isAbsolute(pathText) || isSystemSkillPath(pathText)) {
574
+ return false;
575
+ }
576
+ const roots = [
577
+ path.join(String(childEnv.CODEX_HOME || ''), 'skills'),
578
+ getCodexOverleafSkillsRoot({ env: childEnv })
579
+ ].filter(Boolean);
580
+ return roots.some(root => isInsideOrSamePath(pathText, root));
581
+ }
582
+
583
+ function isSystemSkillPath(skillPath) {
584
+ return String(skillPath || '').split(path.sep).includes('.system');
585
+ }
586
+
587
+ function buildSkillDisableParams(skill = {}) {
588
+ const name = String(skill.name || '').trim();
589
+ if (isCodexSystemSkill(skill) && name) {
590
+ return { name, enabled: false };
591
+ }
592
+ const skillPath = String(skill.path || '').trim();
593
+ if (path.isAbsolute(skillPath)) {
594
+ return { path: skillPath, enabled: false };
595
+ }
596
+ if (name) {
597
+ return { name, enabled: false };
598
+ }
599
+ return null;
600
+ }
601
+
602
+ function isInsideOrSamePath(target, root) {
603
+ const targetPaths = comparablePaths(target);
604
+ const rootPaths = comparablePaths(root);
605
+ return targetPaths.some(targetPath => rootPaths.some(rootPath => (
606
+ targetPath === rootPath || targetPath.startsWith(rootPath + path.sep)
607
+ )));
608
+ }
609
+
610
+ function comparablePaths(value) {
611
+ const resolved = path.resolve(String(value || ''));
612
+ const candidates = [resolved];
613
+ try {
614
+ candidates.push(fs.realpathSync.native(resolved));
615
+ } catch (_) {
616
+ // Fall back to the lexical path when the file is not present yet.
617
+ }
618
+ return Array.from(new Set(candidates));
619
+ }
620
+
621
+ function buildTurnStartParams(input = {}, threadId = input.threadId || '') {
622
+ const params = {
623
+ threadId,
624
+ input: [
625
+ {
626
+ type: 'text',
627
+ text: input.task,
628
+ text_elements: []
629
+ }
630
+ ],
631
+ cwd: input.workspacePath,
632
+ model: input.model || null,
633
+ effort: normalizeReasoningEffort(input.reasoningEffort)
634
+ };
635
+ if (supportsReasoningSummary(input.model)) {
636
+ params.summary = 'detailed';
637
+ }
638
+ return params;
639
+ }
640
+
641
+ function supportsReasoningSummary(model) {
642
+ return String(model || '').toLowerCase() !== 'gpt-5.3-codex-spark';
643
+ }
644
+
645
+ function runCodexAppServerSession(input) {
646
+ return new Promise((resolve, reject) => {
647
+ if (input.signal?.aborted) {
648
+ reject(getAbortReason(input.signal));
649
+ return;
650
+ }
651
+ const childEnv = buildCodexHomeEnv(input.env || process.env, {
652
+ loadCodexLocalSkills: input.loadCodexLocalSkills !== false,
653
+ loadCodexOverleafSkills: input.loadCodexOverleafSkills !== false,
654
+ installCodexOverleafSkillsTarget: input.installCodexOverleafSkillsTarget === true,
655
+ projectLocalSkills: input.projectLocalSkills || null
656
+ });
657
+ const codexCommand = resolveCodexCommand(childEnv);
658
+ if (!codexCommand) {
659
+ reject(new Error('Codex CLI was not found. Install Codex or make sure the `codex` command is available in your login shell.'));
660
+ return;
661
+ }
662
+
663
+ const child = spawn(codexCommand, buildCodexAppServerArgs(input), {
664
+ env: childEnv,
665
+ shell: shouldUseShellForCommand(codexCommand, childEnv),
666
+ stdio: ['pipe', 'pipe', 'pipe']
667
+ });
668
+ const pending = new Map();
669
+ let nextId = 1;
670
+ let stdoutBuffer = '';
671
+ let stderr = '';
672
+ let activeThreadId = '';
673
+ let activeTurnId = '';
674
+ const assistantMessages = new Map();
675
+ const assistantMessageOrder = [];
676
+ let settled = false;
677
+ const timeout = createOptionalTimeout(childEnv.CODEX_OVERLEAF_CODEX_TIMEOUT_MS, timeoutMs => {
678
+ fail(new Error(`Codex app-server did not complete within configured timeout (${timeoutMs}ms)`));
679
+ });
680
+ const onAbort = () => {
681
+ fail(getAbortReason(input.signal));
682
+ };
683
+ input.signal?.addEventListener('abort', onAbort, { once: true });
684
+
685
+ child.stdout.setEncoding('utf8');
686
+ child.stderr.setEncoding('utf8');
687
+ child.stdout.on('data', chunk => {
688
+ stdoutBuffer += chunk;
689
+ const lines = stdoutBuffer.split(/\r?\n/);
690
+ stdoutBuffer = lines.pop() || '';
691
+ for (const line of lines) {
692
+ if (line.trim()) {
693
+ handleMessage(line);
694
+ }
695
+ }
696
+ });
697
+ child.stderr.on('data', chunk => {
698
+ stderr += chunk;
699
+ });
700
+ child.on('error', fail);
701
+ child.on('close', code => {
702
+ if (settled) {
703
+ return;
704
+ }
705
+ fail(new Error(stderr || `codex app-server exited before turn completed with code ${code}`));
706
+ });
707
+
708
+ start().catch(fail);
709
+
710
+ async function start() {
711
+ await request('initialize', {
712
+ clientInfo: {
713
+ name: 'codex-overleaf-link',
714
+ version: '0.1.0'
715
+ },
716
+ capabilities: null
717
+ });
718
+ notify('initialized');
719
+ await applyCodexSkillIsolation({
720
+ input,
721
+ childEnv,
722
+ request,
723
+ emit: input.emit
724
+ });
725
+
726
+ if (input.threadId) {
727
+ try {
728
+ const resumeResponse = await request('thread/resume', buildThreadResumeParams(input));
729
+ activeThreadId = resumeResponse?.thread?.id || resumeResponse?.threadId || input.threadId;
730
+ } catch (resumeError) {
731
+ const error = new Error(resumeError.message || 'thread/resume failed');
732
+ error.code = 'thread_resume_failed';
733
+ throw error;
734
+ }
735
+ } else {
736
+ const threadResponse = await request('thread/start', buildThreadStartParams(input));
737
+ activeThreadId = threadResponse?.thread?.id || threadResponse?.threadId || '';
738
+ if (!activeThreadId) {
739
+ throw new Error('Codex app-server did not return a thread id');
740
+ }
741
+ }
742
+
743
+ const turnResponse = await startTurnWithSummaryFallback(activeThreadId);
744
+ activeTurnId = turnResponse?.turn?.id || '';
745
+ }
746
+
747
+ async function startTurnWithSummaryFallback(threadId) {
748
+ const params = buildTurnStartParams(input, threadId);
749
+ try {
750
+ return await request('turn/start', params);
751
+ } catch (error) {
752
+ if (!params.summary || !isUnsupportedReasoningSummaryError(error)) {
753
+ throw error;
754
+ }
755
+ emitCodexEvent(input.emit, 'codex.session.event', 'reasoning summary unsupported; retrying without it', {
756
+ method: 'turn/start',
757
+ params: {
758
+ model: input.model || '',
759
+ retriedWithoutSummary: true
760
+ }
761
+ }, 'completed');
762
+ const retryParams = { ...params };
763
+ delete retryParams.summary;
764
+ return request('turn/start', retryParams);
765
+ }
766
+ }
767
+
768
+ function request(method, params) {
769
+ const id = nextId++;
770
+ const message = { id, method, params };
771
+ child.stdin.write(`${JSON.stringify(message)}\n`);
772
+ return new Promise((resolveRequest, rejectRequest) => {
773
+ pending.set(id, {
774
+ resolve: resolveRequest,
775
+ reject: rejectRequest
776
+ });
777
+ });
778
+ }
779
+
780
+ function notify(method, params) {
781
+ child.stdin.write(`${JSON.stringify({ method, params })}\n`);
782
+ }
783
+
784
+ function response(id, result) {
785
+ child.stdin.write(`${JSON.stringify({ id, result })}\n`);
786
+ }
787
+
788
+ function handleMessage(line) {
789
+ let message;
790
+ try {
791
+ message = JSON.parse(line);
792
+ } catch {
793
+ emitCodexEvent(input.emit, 'codex.session.raw', 'Codex app-server emitted non-JSON output', {
794
+ text: truncateText(line, 1000)
795
+ });
796
+ return;
797
+ }
798
+
799
+ if (Object.prototype.hasOwnProperty.call(message, 'id') &&
800
+ (Object.prototype.hasOwnProperty.call(message, 'result') || message.error)) {
801
+ const pendingRequest = pending.get(message.id);
802
+ if (!pendingRequest) {
803
+ return;
804
+ }
805
+ pending.delete(message.id);
806
+ if (message.error) {
807
+ pendingRequest.reject(new Error(message.error.message || JSON.stringify(message.error)));
808
+ } else {
809
+ pendingRequest.resolve(message.result);
810
+ }
811
+ return;
812
+ }
813
+
814
+ if (Object.prototype.hasOwnProperty.call(message, 'id') && message.method) {
815
+ handleServerRequest(message);
816
+ return;
817
+ }
818
+
819
+ if (message.method) {
820
+ recordAssistantMessage(message);
821
+ emitCodexEvent(input.emit, 'codex.session.event', message.method, {
822
+ method: message.method,
823
+ params: message.params || {}
824
+ }, inferNotificationStatus(message));
825
+ if (message.method === 'turn/completed' && (!activeTurnId || message.params?.turn?.id === activeTurnId || message.params?.turnId === activeTurnId)) {
826
+ succeed();
827
+ }
828
+ if (message.method === 'error') {
829
+ fail(new Error(message.params?.error?.message || 'Codex turn failed'));
830
+ }
831
+ }
832
+ }
833
+
834
+ function handleServerRequest(message) {
835
+ emitCodexEvent(input.emit, 'codex.session.request', message.method, {
836
+ method: message.method,
837
+ params: message.params || {}
838
+ }, 'running');
839
+
840
+ if (/fileChange\/requestApproval/.test(message.method)) {
841
+ if (isSkillInstallerInvocation(input.skillInvocation)) {
842
+ response(message.id, { decision: 'decline', reason: 'Skill installation must not edit Overleaf workspace files.' });
843
+ return;
844
+ }
845
+ response(message.id, { decision: input.mode === 'ask' ? 'decline' : 'accept' });
846
+ return;
847
+ }
848
+ if (/commandExecution\/requestApproval/.test(message.method)) {
849
+ response(message.id, decideCommandApproval({
850
+ mode: input.mode,
851
+ skillInvocation: input.skillInvocation,
852
+ env: childEnv,
853
+ workspacePath: input.workspacePath,
854
+ params: message.params || {}
855
+ }));
856
+ return;
857
+ }
858
+ response(message.id, { decision: 'decline' });
859
+ }
860
+
861
+ function recordAssistantMessage(message) {
862
+ const method = String(message.method || '');
863
+ const params = message.params || {};
864
+ const item = params.item || {};
865
+ if (method === 'item/agentMessage/delta') {
866
+ const itemId = String(params.itemId || item.id || 'current');
867
+ const next = `${assistantMessages.get(itemId) || ''}${String(params.delta || '')}`;
868
+ setAssistantMessage(itemId, next);
869
+ return;
870
+ }
871
+ if (item.type === 'agentMessage' && typeof item.text === 'string' && item.text.trim()) {
872
+ const itemId = String(item.id || params.itemId || 'current');
873
+ setAssistantMessage(itemId, item.text);
874
+ }
875
+ }
876
+
877
+ function setAssistantMessage(itemId, text) {
878
+ if (!assistantMessages.has(itemId)) {
879
+ assistantMessageOrder.push(itemId);
880
+ }
881
+ assistantMessages.set(itemId, text);
882
+ }
883
+
884
+ function succeed() {
885
+ if (settled) {
886
+ return;
887
+ }
888
+ settled = true;
889
+ cleanup();
890
+ child.kill('SIGTERM');
891
+ resolve({
892
+ assistantMessage: buildFinalAssistantMessage(assistantMessages, assistantMessageOrder),
893
+ threadId: activeThreadId
894
+ });
895
+ }
896
+
897
+ function fail(error) {
898
+ if (settled) {
899
+ return;
900
+ }
901
+ settled = true;
902
+ cleanup();
903
+ for (const pendingRequest of pending.values()) {
904
+ pendingRequest.reject(error);
905
+ }
906
+ pending.clear();
907
+ if (child.exitCode === null && !child.killed) {
908
+ child.kill('SIGTERM');
909
+ }
910
+ reject(error);
911
+ }
912
+
913
+ function cleanup() {
914
+ timeout.cancel();
915
+ input.signal?.removeEventListener('abort', onAbort);
916
+ }
917
+ });
918
+ }
919
+
920
+ function decideCommandApproval({ mode = 'auto', params = {}, skillInvocation = null, env = process.env, workspacePath = '' } = {}) {
921
+ if (isSkillInstallerInvocation(skillInvocation)) {
922
+ return decideSkillInstallerCommandApproval({ params, env, workspacePath });
923
+ }
924
+ if (mode === 'ask') {
925
+ return { decision: 'decline' };
926
+ }
927
+ return isAllowedLocalCommand(params)
928
+ ? { decision: 'accept' }
929
+ : {
930
+ decision: 'decline',
931
+ reason: 'Command is outside the Codex Overleaf local inspection/LaTeX allowlist.'
932
+ };
933
+ }
934
+
935
+ function decideSkillInstallerCommandApproval({ params = {}, env = process.env, workspacePath = '' } = {}) {
936
+ const command = extractCommandValue(params);
937
+ const approval = evaluateSkillCommand({
938
+ command,
939
+ cwd: workspacePath
940
+ }, {
941
+ env,
942
+ workspacePath,
943
+ skillsRoot: getCodexOverleafSkillsRoot({ env })
944
+ });
945
+ return approval.approved
946
+ ? { decision: 'accept' }
947
+ : {
948
+ decision: 'decline',
949
+ reason: approval.reason
950
+ };
951
+ }
952
+
953
+ function isAllowedLocalCommand(params = {}) {
954
+ const command = extractCommandValue(params);
955
+ if (typeof command === 'string' && hasUnsupportedShellSyntax(command)) {
956
+ return false;
957
+ }
958
+ const tokens = Array.isArray(command) ? command.map(String) : tokenizeShellCommand(String(command || ''));
959
+ if (!tokens.length) {
960
+ return false;
961
+ }
962
+
963
+ const executable = pathBasename(tokens[0]);
964
+ if (['bash', 'sh', 'zsh'].includes(executable)) {
965
+ const inline = extractShellInlineCommand(tokens);
966
+ return inline ? isAllowedLocalCommand({ command: inline }) : false;
967
+ }
968
+
969
+ const allowed = new Set([
970
+ 'rg', 'grep', 'cat', 'sed', 'head', 'tail', 'nl', 'find', 'ls',
971
+ 'wc', 'diff', 'sort', 'tr', 'awk', 'printf', 'cut', 'uniq',
972
+ 'stat', 'file', 'basename', 'dirname', 'realpath',
973
+ 'shasum', 'md5', 'md5sum',
974
+ 'latexmk', 'pdflatex', 'xelatex', 'lualatex', 'bibtex', 'biber',
975
+ 'kpsewhich', 'chktex', 'lacheck'
976
+ ]);
977
+ if (!allowed.has(executable)) {
978
+ return false;
979
+ }
980
+
981
+ return !tokens.some(isUnsafeShellToken)
982
+ && !hasDisallowedCommandArguments(executable, tokens.slice(1));
983
+ }
984
+
985
+ function extractCommandValue(params = {}) {
986
+ if (Array.isArray(params.command) || typeof params.command === 'string') {
987
+ return params.command;
988
+ }
989
+ if (Array.isArray(params.cmd) || typeof params.cmd === 'string') {
990
+ return params.cmd;
991
+ }
992
+ if (Array.isArray(params.argv)) {
993
+ return params.argv;
994
+ }
995
+ if (typeof params.shellCommand === 'string') {
996
+ return params.shellCommand;
997
+ }
998
+ return '';
999
+ }
1000
+
1001
+ function extractShellInlineCommand(tokens = []) {
1002
+ const index = tokens.findIndex(token => token === '-c' || token === '-lc' || token === '-ilc');
1003
+ if (index < 0 || index + 1 >= tokens.length || tokens.length !== index + 2) {
1004
+ return '';
1005
+ }
1006
+ return tokens[index + 1];
1007
+ }
1008
+
1009
+ function hasUnsupportedShellSyntax(command) {
1010
+ return hasAmbiguousShellEscape(command) || hasUnbalancedShellQuote(command);
1011
+ }
1012
+
1013
+ function hasAmbiguousShellEscape(command) {
1014
+ return /\\["';&|<>`$(){}\n\r]/.test(command);
1015
+ }
1016
+
1017
+ function hasUnbalancedShellQuote(command) {
1018
+ let quote = '';
1019
+ for (let index = 0; index < command.length; index += 1) {
1020
+ const char = command[index];
1021
+ if (char === '\\' && quote !== "'") {
1022
+ index += 1;
1023
+ continue;
1024
+ }
1025
+ if (quote) {
1026
+ if (char === quote) {
1027
+ quote = '';
1028
+ }
1029
+ continue;
1030
+ }
1031
+ if (char === '"' || char === "'") {
1032
+ quote = char;
1033
+ }
1034
+ }
1035
+ return Boolean(quote);
1036
+ }
1037
+
1038
+ function isUnsafeShellToken(token) {
1039
+ return ['&&', '||', ';', '|', '>', '>>', '<', '<<', '`'].includes(token)
1040
+ || /\$\(/.test(token);
1041
+ }
1042
+
1043
+ function hasDisallowedCommandArguments(executable, args = []) {
1044
+ const flags = args.map(String);
1045
+ if (executable === 'find') {
1046
+ return flags.some(flag => ['-exec', '-execdir', '-delete', '-ok', '-okdir'].includes(flag));
1047
+ }
1048
+ if (executable === 'sed') {
1049
+ return flags.some(flag => flag === '-i' || /^-i[^a-zA-Z0-9]?/.test(flag));
1050
+ }
1051
+ if (executable === 'awk') {
1052
+ return flags.some((flag, index) => flag === '-i' && flags[index + 1] === 'inplace');
1053
+ }
1054
+ if (executable === 'shasum' || executable === 'md5sum') {
1055
+ return flags.some(flag => flag === '-c' || flag === '--check');
1056
+ }
1057
+ return false;
1058
+ }
1059
+
1060
+ function pathBasename(value) {
1061
+ return String(value || '').split(/[\\/]/).pop();
1062
+ }
1063
+
1064
+ function tokenizeShellCommand(command) {
1065
+ const tokens = [];
1066
+ let current = '';
1067
+ let quote = '';
1068
+ for (let index = 0; index < command.length; index += 1) {
1069
+ const char = command[index];
1070
+ if (quote) {
1071
+ if (char === quote) {
1072
+ quote = '';
1073
+ } else {
1074
+ current += char;
1075
+ }
1076
+ continue;
1077
+ }
1078
+ if (char === '"' || char === "'") {
1079
+ quote = char;
1080
+ continue;
1081
+ }
1082
+ if (/\s/.test(char)) {
1083
+ if (current) {
1084
+ tokens.push(current);
1085
+ current = '';
1086
+ }
1087
+ continue;
1088
+ }
1089
+ if (char === '&' && command[index + 1] === '&') {
1090
+ if (current) tokens.push(current);
1091
+ tokens.push('&&');
1092
+ current = '';
1093
+ index += 1;
1094
+ continue;
1095
+ }
1096
+ if (char === '|' && command[index + 1] === '|') {
1097
+ if (current) tokens.push(current);
1098
+ tokens.push('||');
1099
+ current = '';
1100
+ index += 1;
1101
+ continue;
1102
+ }
1103
+ if (';|<>`'.includes(char)) {
1104
+ if (current) tokens.push(current);
1105
+ tokens.push(char);
1106
+ current = '';
1107
+ continue;
1108
+ }
1109
+ current += char;
1110
+ }
1111
+ if (current) {
1112
+ tokens.push(current);
1113
+ }
1114
+ return tokens;
1115
+ }
1116
+
1117
+ function createOptionalTimeout(value, onTimeout) {
1118
+ const timeoutMs = parseOptionalPositiveInteger(value);
1119
+ if (!timeoutMs) {
1120
+ return {
1121
+ cancel() {}
1122
+ };
1123
+ }
1124
+ const timer = setTimeout(() => onTimeout(timeoutMs), timeoutMs);
1125
+ return {
1126
+ cancel() {
1127
+ clearTimeout(timer);
1128
+ }
1129
+ };
1130
+ }
1131
+
1132
+ function parseOptionalPositiveInteger(value) {
1133
+ if (value === undefined || value === null || value === '') {
1134
+ return 0;
1135
+ }
1136
+ const number = Number(value);
1137
+ return Number.isFinite(number) && number > 0 ? Math.floor(number) : 0;
1138
+ }
1139
+
1140
+ function throwIfAborted(signal) {
1141
+ if (!signal?.aborted) {
1142
+ return;
1143
+ }
1144
+ throw getAbortReason(signal);
1145
+ }
1146
+
1147
+ function getAbortReason(signal) {
1148
+ if (signal?.reason instanceof Error) {
1149
+ return signal.reason;
1150
+ }
1151
+ const error = new Error('Codex run was cancelled by the user');
1152
+ error.code = 'codex_cancelled';
1153
+ return error;
1154
+ }
1155
+
1156
+ function resolveCodexCommand(env = process.env) {
1157
+ if (
1158
+ env.CODEX_OVERLEAF_ENV_READY === '1' ||
1159
+ Object.prototype.hasOwnProperty.call(env, 'CODEX_OVERLEAF_CODEX_PATH')
1160
+ ) {
1161
+ return env.CODEX_OVERLEAF_CODEX_PATH || '';
1162
+ }
1163
+ return 'codex';
1164
+ }
1165
+
1166
+ function shouldUseShellForCommand(command, env = process.env) {
1167
+ const platform = env.CODEX_OVERLEAF_PLATFORM || process.platform;
1168
+ if (platform !== 'win32') {
1169
+ return false;
1170
+ }
1171
+ const text = String(command || '');
1172
+ return text === 'codex' || /\.(?:cmd|bat)$/i.test(text);
1173
+ }
1174
+
1175
+ function buildFinalAssistantMessage(messages = new Map(), order = []) {
1176
+ const values = [];
1177
+ const seenIds = new Set();
1178
+ const ids = order.length ? order : Array.from(messages.keys());
1179
+
1180
+ for (const id of ids) {
1181
+ seenIds.add(id);
1182
+ addAssistantMessage(values, messages.get(id));
1183
+ }
1184
+ for (const [id, value] of messages) {
1185
+ if (!seenIds.has(id)) {
1186
+ addAssistantMessage(values, value);
1187
+ }
1188
+ }
1189
+
1190
+ return values.join('\n\n');
1191
+ }
1192
+
1193
+ function addAssistantMessage(values, value) {
1194
+ const clean = cleanAssistantMessage(value);
1195
+ if (clean && !values.includes(clean)) {
1196
+ values.push(clean);
1197
+ }
1198
+ }
1199
+
1200
+ function emitCodexEvent(emit, type, title, detail = {}, status = 'running') {
1201
+ emit({
1202
+ type,
1203
+ title,
1204
+ status,
1205
+ detail,
1206
+ timestamp: new Date().toISOString()
1207
+ });
1208
+ }
1209
+
1210
+ function inferNotificationStatus(message) {
1211
+ if (/completed|updated|delta|started/.test(message.method || '')) {
1212
+ return /completed/.test(message.method || '') ? 'completed' : 'running';
1213
+ }
1214
+ return 'running';
1215
+ }
1216
+
1217
+ function normalizeReasoningEffort(value) {
1218
+ return ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(value) ? value : null;
1219
+ }
1220
+
1221
+ function normalizeSpeedTier(value) {
1222
+ return value === 'fast' ? 'fast' : 'standard';
1223
+ }
1224
+
1225
+ function isUnsupportedReasoningSummaryError(error) {
1226
+ const message = String(error?.message || error || '');
1227
+ return /unsupported_parameter/i.test(message) && /reasoning\.summary|summary/i.test(message);
1228
+ }
1229
+
1230
+ function cleanAssistantMessage(value) {
1231
+ return String(value || '').trim();
1232
+ }
1233
+
1234
+ module.exports = {
1235
+ applyCodexSkillIsolation,
1236
+ buildCodexTurnPrompt,
1237
+ buildCodexAppServerArgs,
1238
+ buildFinalAssistantMessage,
1239
+ buildCodexSettings,
1240
+ buildThreadStartParams,
1241
+ buildThreadResumeParams,
1242
+ buildTurnStartParams,
1243
+ createOptionalTimeout,
1244
+ decideCommandApproval,
1245
+ runCodexAppServerSession,
1246
+ runCodexSession
1247
+ };