@wlv-zedd/dsh-chatgpt-web 1.0.0 → 1.0.2

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 (81) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +140 -140
  3. package/cordis.patch.yml +4 -4
  4. package/lib/cli.js +13269 -11860
  5. package/lib/plugin.js +3 -50
  6. package/package.json +89 -88
  7. package/src/adapters/base.ts +16 -16
  8. package/src/adapters/chatgpt-web/adapter-error.ts +59 -59
  9. package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -513
  10. package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -27
  11. package/src/adapters/chatgpt-web/browser-worker.ts +4944 -4944
  12. package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -628
  13. package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -533
  14. package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -142
  15. package/src/adapters/chatgpt-web/concurrency.ts +6 -6
  16. package/src/adapters/chatgpt-web/conversation-key.ts +58 -58
  17. package/src/adapters/chatgpt-web/environment.ts +669 -669
  18. package/src/adapters/chatgpt-web/index.ts +1544 -1544
  19. package/src/adapters/chatgpt-web/input-tokens.ts +74 -74
  20. package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -695
  21. package/src/adapters/chatgpt-web/markdown.ts +418 -418
  22. package/src/adapters/chatgpt-web/mcp-main.ts +25 -25
  23. package/src/adapters/chatgpt-web/mcp-server.ts +933 -933
  24. package/src/adapters/chatgpt-web/model.ts +70 -70
  25. package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -74
  26. package/src/adapters/chatgpt-web/output-validation.ts +62 -62
  27. package/src/adapters/chatgpt-web/process-line-writer.ts +46 -46
  28. package/src/adapters/chatgpt-web/prompt.ts +702 -702
  29. package/src/adapters/chatgpt-web/retry-policy.ts +73 -73
  30. package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -384
  31. package/src/adapters/chatgpt-web/thread-environment.ts +238 -238
  32. package/src/adapters/chatgpt-web/tool-stream-parser.ts +1 -1
  33. package/src/adapters/chatgpt-web/turn-broker.ts +1481 -1481
  34. package/src/adapters/chatgpt-web/turn-execution.ts +816 -816
  35. package/src/adapters/chatgpt-web/turn-progress.ts +292 -292
  36. package/src/adapters/chatgpt-web/usage.ts +121 -121
  37. package/src/adapters/image.ts +9 -9
  38. package/src/bridge.ts +1083 -1083
  39. package/src/browser-login.ts +521 -521
  40. package/src/chatgpt-session.ts +240 -240
  41. package/src/chatgpt-web-models.ts +400 -400
  42. package/src/cli.ts +568 -568
  43. package/src/codex-integration-document.ts +824 -824
  44. package/src/codex-integration-journal.ts +212 -212
  45. package/src/codex-integration-route.ts +515 -515
  46. package/src/codex-integration-shared.ts +332 -332
  47. package/src/codex-integration.ts +529 -529
  48. package/src/codex-interrupt-hook.ts +158 -158
  49. package/src/config.ts +616 -616
  50. package/src/dev-chat/cli.ts +432 -432
  51. package/src/dev-chat/constants.ts +3 -3
  52. package/src/dev-chat/driver.ts +655 -655
  53. package/src/dev-chat/profile.ts +223 -223
  54. package/src/dev-chat/session.ts +287 -287
  55. package/src/dev-chat/transport.ts +54 -54
  56. package/src/doctor.ts +237 -237
  57. package/src/event-queue.ts +45 -45
  58. package/src/http-body.ts +30 -30
  59. package/src/launcher-browser-host.ts +695 -695
  60. package/src/lib/errors.ts +281 -281
  61. package/src/lib/token-estimate.ts +42 -42
  62. package/src/login-helper.cjs +1 -1
  63. package/src/model-catalog.ts +197 -197
  64. package/src/native-passthrough.ts +261 -261
  65. package/src/plugin.ts +191 -191
  66. package/src/process.ts +45 -45
  67. package/src/responses/compaction.ts +199 -199
  68. package/src/responses/parser.ts +633 -633
  69. package/src/responses/reasoning-envelope.ts +49 -49
  70. package/src/responses/schema.ts +172 -172
  71. package/src/responses/state.ts +230 -230
  72. package/src/server.ts +1111 -1111
  73. package/src/service.ts +315 -315
  74. package/src/setup.ts +671 -671
  75. package/src/stall-timeout.ts +23 -23
  76. package/src/tunnel-service.ts +160 -160
  77. package/src/tunnel.ts +417 -417
  78. package/src/turndown-plugin-gfm.d.ts +5 -5
  79. package/src/types.ts +307 -307
  80. package/src/usage/totals.ts +12 -12
  81. package/src/version.ts +1 -1
@@ -1,529 +1,529 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
- import { dirname } from "node:path";
3
- import type { AppConfig } from "./config";
4
- import { atomicWriteFile, getConfigPath, loadConfig, saveConfig } from "./config";
5
- import { installCodexInterruptHook, installCodexInterruptHookCommand } from "./codex-interrupt-hook";
6
- import {
7
- CODEX_REALTIME_WEBRTC_CALL_BASE_URL,
8
- getCodexConfigPath,
9
- getCodexJournalPath,
10
- getCodexJournalRecoveryPath,
11
- getCodexModelsCachePath,
12
- restoreFileSnapshot,
13
- routeUrl,
14
- sha256,
15
- snapshotFile,
16
- writeIntegrationState,
17
- } from "./codex-integration-shared";
18
- import type {
19
- AnyCodexIntegrationJournal,
20
- CodexIntegrationJournal,
21
- InstallCodexIntegrationOptions,
22
- LegacyCodexIntegrationJournalV4,
23
- LegacyCodexIntegrationJournalV5,
24
- LegacyCodexIntegrationJournalV6,
25
- LegacyCodexIntegrationJournalV7,
26
- LegacyCodexIntegrationJournalV8,
27
- LegacyCodexIntegrationJournalV9,
28
- SetCodexIntegrationActiveResult,
29
- UninstallCodexIntegrationResult,
30
- } from "./codex-integration-shared";
31
- import { assertJournalTargetsConfig, readJournal } from "./codex-integration-journal";
32
- import {
33
- findTopLevelAssignment,
34
- installCompatibilityV1Features,
35
- splitLines,
36
- textFormat,
37
- } from "./codex-integration-document";
38
- import {
39
- assertPreservedPreviousAssignments,
40
- assertPreservedPreviousRealtimeAssignment,
41
- installRoute,
42
- managedJournalIsActive,
43
- replacementBaseline,
44
- restoreLegacyV2,
45
- restoreManagedRoute,
46
- verifyInstalledRoute,
47
- verifyManagedJournalState,
48
- verifyRestoredRoute,
49
- } from "./codex-integration-route";
50
-
51
- function installConfiguredRoute(
52
- baseline: string,
53
- installedUrl: string,
54
- config: Pick<AppConfig, "subagentProtocol"> & (
55
- Pick<AppConfig, "runtimeCommand"> | { interruptHookCommand: string }
56
- ),
57
- replaceExistingRoute: boolean,
58
- replaceExistingRealtimeRoute: boolean,
59
- ): {
60
- text: string;
61
- previous: CodexIntegrationJournal["previous"];
62
- previousRealtimeWebrtcCallBaseUrl: CodexIntegrationJournal["previousRealtimeWebrtcCallBaseUrl"];
63
- previousMultiAgent?: CodexIntegrationJournal["previousMultiAgent"];
64
- previousMultiAgentV2?: CodexIntegrationJournal["previousMultiAgentV2"];
65
- previousAgentMaxDepth?: CodexIntegrationJournal["previousAgentMaxDepth"];
66
- installedAgentMaxDepth?: number;
67
- interruptHook: CodexIntegrationJournal["interruptHook"];
68
- } {
69
- const route = installRoute(
70
- baseline,
71
- installedUrl,
72
- replaceExistingRoute,
73
- replaceExistingRealtimeRoute,
74
- );
75
- const configured = config.subagentProtocol === "compatibility-v1"
76
- ? (() => {
77
- const features = installCompatibilityV1Features(route.text);
78
- return {
79
- text: features.text,
80
- previous: route.previous,
81
- previousRealtimeWebrtcCallBaseUrl: route.previousRealtimeWebrtcCallBaseUrl,
82
- previousMultiAgent: features.previousMultiAgent,
83
- previousMultiAgentV2: features.previousMultiAgentV2,
84
- previousAgentMaxDepth: features.previousAgentMaxDepth,
85
- installedAgentMaxDepth: features.installedAgentMaxDepth,
86
- };
87
- })()
88
- : route;
89
- const hook = "interruptHookCommand" in config
90
- ? installCodexInterruptHookCommand(configured.text, getCodexConfigPath(), config.interruptHookCommand)
91
- : installCodexInterruptHook(configured.text, getCodexConfigPath(), config);
92
- return { ...configured, text: hook.text, interruptHook: hook.installed };
93
- }
94
-
95
- function journalProtocol(journal: Exclude<AnyCodexIntegrationJournal, { version: 2 }>): AppConfig["subagentProtocol"] {
96
- return journal.version === 8 || journal.version === 9 || journal.version === 10
97
- ? journal.installed.subagent_protocol
98
- : "native";
99
- }
100
-
101
- export {
102
- getCodexConfigPath,
103
- getCodexHome,
104
- getCodexJournalPath,
105
- getCodexJournalRecoveryPath,
106
- getCodexModelsCachePath,
107
- } from "./codex-integration-shared";
108
- export { readCodexModelContextOverride } from "./codex-integration-document";
109
- export type {
110
- CodexIntegrationJournal,
111
- CodexModelContextOverride,
112
- InstallCodexIntegrationOptions,
113
- SetCodexIntegrationActiveResult,
114
- UninstallCodexIntegrationResult,
115
- } from "./codex-integration-shared";
116
-
117
- export function readCodexSubagentProtocol(
118
- fallback: AppConfig["subagentProtocol"] = "compatibility-v1",
119
- ): AppConfig["subagentProtocol"] {
120
- const journal = readJournal();
121
- return journal?.version === 8 || journal?.version === 9 || journal?.version === 10
122
- ? journal.installed.subagent_protocol
123
- : fallback;
124
- }
125
-
126
- export function setCodexSubagentProtocol(
127
- config: AppConfig,
128
- protocol: AppConfig["subagentProtocol"],
129
- ): CodexIntegrationJournal {
130
- const status = inspectCodexIntegration();
131
- if (!status.installed) throw new Error("Codex integration is not installed; run setup first");
132
- if (!status.active) {
133
- throw new Error("Codex integration is disconnected; reconnect it before changing the subagent protocol");
134
- }
135
- const nextConfig = { ...config, subagentProtocol: protocol };
136
- // The runtime catalog and Codex feature surface are two halves of one protocol selection. If
137
- // either write fails, restore every participant so the next launcher/Codex restart cannot load a
138
- // split V1/V2 state.
139
- const snapshots = [
140
- getConfigPath(),
141
- getCodexConfigPath(),
142
- getCodexModelsCachePath(),
143
- getCodexJournalPath(),
144
- getCodexJournalRecoveryPath(),
145
- ].map(snapshotFile);
146
- try {
147
- const journal = installCodexIntegration(nextConfig);
148
- saveConfig(nextConfig);
149
- return journal;
150
- } catch (error) {
151
- const rollbackFailures: string[] = [];
152
- for (const snapshot of [...snapshots].reverse()) {
153
- try {
154
- restoreFileSnapshot(snapshot);
155
- } catch (rollbackError) {
156
- rollbackFailures.push(
157
- `${snapshot.path}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,
158
- );
159
- }
160
- }
161
- const primary = error instanceof Error ? error.message : String(error);
162
- throw new Error(rollbackFailures.length > 0
163
- ? `${primary}; subagent protocol rollback also failed: ${rollbackFailures.join("; ")}`
164
- : primary);
165
- }
166
- }
167
-
168
- export function preflightCodexIntegration(
169
- config: AppConfig,
170
- options: InstallCodexIntegrationOptions = {},
171
- ): void {
172
- const configPath = getCodexConfigPath();
173
- const configExists = existsSync(configPath);
174
- const currentText = configExists ? readFileSync(configPath, "utf8") : "";
175
- const existing = readJournal();
176
- const installedUrl = routeUrl(config);
177
- if (existing) assertJournalTargetsConfig(existing, configPath);
178
- if (existing && existing.version !== 2) {
179
- if (!configExists) {
180
- if (options.replaceExistingRoute !== true) {
181
- throw new Error(`Codex config is missing: ${configPath}`);
182
- }
183
- installConfiguredRoute("", installedUrl, config, true, true);
184
- return;
185
- }
186
- try {
187
- verifyManagedJournalState(currentText, existing);
188
- } catch (error) {
189
- if (options.replaceExistingRoute !== true) throw error;
190
- installConfiguredRoute(
191
- replacementBaseline(currentText, configExists, existing),
192
- installedUrl,
193
- config,
194
- true,
195
- true,
196
- );
197
- return;
198
- }
199
- if (existing.version === 10) return;
200
- const baseline = managedJournalIsActive(existing)
201
- ? restoreManagedRoute(currentText, existing)
202
- : currentText;
203
- installConfiguredRoute(
204
- baseline,
205
- installedUrl,
206
- config,
207
- true,
208
- options.replaceExistingRoute === true,
209
- );
210
- return;
211
- }
212
- let baseline = currentText;
213
- if (existing?.version === 2) {
214
- if (existsSync(existing.catalogPath) && sha256(readFileSync(existing.catalogPath)) !== existing.catalogSha256) {
215
- throw new Error(`Managed legacy catalog changed after setup; refusing migration: ${existing.catalogPath}`);
216
- }
217
- baseline = restoreLegacyV2(currentText, existing);
218
- }
219
- installConfiguredRoute(
220
- baseline,
221
- installedUrl,
222
- config,
223
- options.replaceExistingRoute === true,
224
- options.replaceExistingRoute === true,
225
- );
226
- }
227
- export function installCodexIntegration(
228
- config: AppConfig,
229
- options: InstallCodexIntegrationOptions = {},
230
- ): CodexIntegrationJournal {
231
- const configPath = getCodexConfigPath();
232
- mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
233
- const configExists = existsSync(configPath);
234
- const currentText = configExists ? readFileSync(configPath, "utf8") : "";
235
- const existing = readJournal();
236
- const installedUrl = routeUrl(config);
237
- if (existing) assertJournalTargetsConfig(existing, configPath);
238
-
239
- const hasManagedJournal = Boolean(existing && existing.version !== 2);
240
- if (hasManagedJournal && !configExists && options.replaceExistingRoute !== true) {
241
- throw new Error(`Codex config is missing: ${configPath}`);
242
- }
243
-
244
- if (hasManagedJournal && existing && existing.version !== 2) {
245
- let baseline: string;
246
- let preservePrevious = true;
247
- try {
248
- verifyManagedJournalState(currentText, existing);
249
- baseline = managedJournalIsActive(existing)
250
- ? restoreManagedRoute(currentText, existing)
251
- : currentText;
252
- } catch (error) {
253
- if (options.replaceExistingRoute !== true) throw error;
254
- baseline = replacementBaseline(currentText, configExists, existing);
255
- preservePrevious = false;
256
- }
257
- const patched = installConfiguredRoute(
258
- baseline,
259
- installedUrl,
260
- config,
261
- true,
262
- !preservePrevious || existing.version === 9 || existing.version === 10 || options.replaceExistingRoute === true,
263
- );
264
- if (preservePrevious) {
265
- assertPreservedPreviousAssignments(patched.previous, existing.previous);
266
- if (existing.version === 9 || existing.version === 10) {
267
- assertPreservedPreviousRealtimeAssignment(
268
- patched.previousRealtimeWebrtcCallBaseUrl,
269
- existing.previousRealtimeWebrtcCallBaseUrl,
270
- );
271
- }
272
- }
273
- const updated: CodexIntegrationJournal = {
274
- version: 10,
275
- active: true,
276
- configPath,
277
- installed: {
278
- openai_base_url: installedUrl,
279
- experimental_realtime_webrtc_call_base_url: CODEX_REALTIME_WEBRTC_CALL_BASE_URL,
280
- subagent_protocol: config.subagentProtocol,
281
- ...(config.subagentProtocol === "compatibility-v1" ? {
282
- agent_max_depth: patched.installedAgentMaxDepth,
283
- } : {}),
284
- },
285
- previous: preservePrevious ? existing.previous : patched.previous,
286
- previousRealtimeWebrtcCallBaseUrl: preservePrevious && (existing.version === 9 || existing.version === 10)
287
- ? existing.previousRealtimeWebrtcCallBaseUrl
288
- : patched.previousRealtimeWebrtcCallBaseUrl,
289
- interruptHook: patched.interruptHook,
290
- ...(config.subagentProtocol === "compatibility-v1" ? {
291
- previousMultiAgent: patched.previousMultiAgent,
292
- previousMultiAgentV2: patched.previousMultiAgentV2,
293
- previousAgentMaxDepth: patched.previousAgentMaxDepth,
294
- } : {}),
295
- ...(existing.format ? { format: existing.format } : {}),
296
- };
297
- writeIntegrationState(updated, { path: configPath, data: patched.text }, [getCodexModelsCachePath()]);
298
- return updated;
299
- }
300
-
301
- let baseline = currentText;
302
- if (existing?.version === 2) {
303
- if (existsSync(existing.catalogPath) && sha256(readFileSync(existing.catalogPath)) !== existing.catalogSha256) {
304
- throw new Error(`Managed legacy catalog changed after setup; refusing migration: ${existing.catalogPath}`);
305
- }
306
- baseline = restoreLegacyV2(currentText, existing);
307
- }
308
- const patched = installConfiguredRoute(
309
- baseline,
310
- installedUrl,
311
- config,
312
- options.replaceExistingRoute === true,
313
- options.replaceExistingRoute === true,
314
- );
315
- const journal: CodexIntegrationJournal = {
316
- version: 10,
317
- active: true,
318
- configPath,
319
- installed: {
320
- openai_base_url: installedUrl,
321
- experimental_realtime_webrtc_call_base_url: CODEX_REALTIME_WEBRTC_CALL_BASE_URL,
322
- subagent_protocol: config.subagentProtocol,
323
- ...(config.subagentProtocol === "compatibility-v1" ? {
324
- agent_max_depth: patched.installedAgentMaxDepth,
325
- } : {}),
326
- },
327
- previous: patched.previous,
328
- previousRealtimeWebrtcCallBaseUrl: patched.previousRealtimeWebrtcCallBaseUrl,
329
- interruptHook: patched.interruptHook,
330
- ...(config.subagentProtocol === "compatibility-v1" ? {
331
- previousMultiAgent: patched.previousMultiAgent,
332
- previousMultiAgentV2: patched.previousMultiAgentV2,
333
- previousAgentMaxDepth: patched.previousAgentMaxDepth,
334
- } : {}),
335
- format: textFormat(baseline),
336
- };
337
- writeIntegrationState(journal, { path: configPath, data: patched.text }, [getCodexModelsCachePath()]);
338
- if (existing?.version === 2 && existsSync(existing.catalogPath)) rmSync(existing.catalogPath);
339
- return journal;
340
- }
341
-
342
- export function deactivateCodexIntegration(): SetCodexIntegrationActiveResult {
343
- const existing = readJournal();
344
- if (!existing) return { changed: false, active: false };
345
- if (existing.version === 2) {
346
- throw new Error("Legacy Codex integration must be upgraded by Setup before the bridge can be disconnected");
347
- }
348
- assertJournalTargetsConfig(existing, getCodexConfigPath());
349
- if (!existsSync(existing.configPath)) throw new Error(`Codex config is missing: ${existing.configPath}`);
350
- const current = readFileSync(existing.configPath, "utf8");
351
- if ((existing.version === 4 || existing.version === 5 || existing.version === 6 || existing.version === 7 || existing.version === 8 || existing.version === 9 || existing.version === 10) && !existing.active) {
352
- verifyRestoredRoute(current, existing);
353
- return { changed: false, active: false };
354
- }
355
- const restored = restoreManagedRoute(current, existing);
356
- const disconnected:
357
- | CodexIntegrationJournal
358
- | LegacyCodexIntegrationJournalV9
359
- | LegacyCodexIntegrationJournalV8
360
- | LegacyCodexIntegrationJournalV6
361
- | LegacyCodexIntegrationJournalV7
362
- | LegacyCodexIntegrationJournalV5
363
- | LegacyCodexIntegrationJournalV4 = existing.version === 6 || existing.version === 5
364
- || existing.version === 7 || existing.version === 8 || existing.version === 9 || existing.version === 10
365
- ? { ...existing, active: false }
366
- : { ...existing, version: 4, active: false };
367
- writeIntegrationState(disconnected, { path: existing.configPath, data: restored }, [getCodexModelsCachePath()]);
368
- return { changed: true, active: false };
369
- }
370
-
371
- export function activateCodexIntegration(): SetCodexIntegrationActiveResult {
372
- const existing = readJournal();
373
- if (!existing) throw new Error("Codex integration is not installed");
374
- if (existing.version === 2) {
375
- throw new Error("Legacy Codex integration must be upgraded by Setup before the bridge can be reconnected");
376
- }
377
- assertJournalTargetsConfig(existing, getCodexConfigPath());
378
- if (!existsSync(existing.configPath)) throw new Error(`Codex config is missing: ${existing.configPath}`);
379
- const current = readFileSync(existing.configPath, "utf8");
380
- if (existing.version === 10 && existing.active) {
381
- verifyInstalledRoute(current, existing);
382
- return { changed: false, active: true };
383
- }
384
- let baseline: string;
385
- if ((existing.version === 4 || existing.version === 5 || existing.version === 6 || existing.version === 7 || existing.version === 8 || existing.version === 9 || existing.version === 10) && !existing.active) {
386
- verifyRestoredRoute(current, existing);
387
- baseline = current;
388
- } else {
389
- verifyInstalledRoute(current, existing);
390
- baseline = restoreManagedRoute(current, existing);
391
- }
392
- const protocol = journalProtocol(existing);
393
- const hookConfig = existing.version === 10
394
- ? { interruptHookCommand: existing.interruptHook.command }
395
- : { runtimeCommand: loadConfig().runtimeCommand };
396
- const route = installConfiguredRoute(
397
- baseline,
398
- existing.installed.openai_base_url,
399
- { subagentProtocol: protocol, ...hookConfig },
400
- true,
401
- existing.version === 9 || existing.version === 10,
402
- );
403
- assertPreservedPreviousAssignments(route.previous, existing.previous);
404
- if (existing.version === 9 || existing.version === 10) {
405
- assertPreservedPreviousRealtimeAssignment(
406
- route.previousRealtimeWebrtcCallBaseUrl,
407
- existing.previousRealtimeWebrtcCallBaseUrl,
408
- );
409
- }
410
- const connected: CodexIntegrationJournal = {
411
- version: 10,
412
- active: true,
413
- configPath: existing.configPath,
414
- installed: {
415
- openai_base_url: existing.installed.openai_base_url,
416
- experimental_realtime_webrtc_call_base_url: CODEX_REALTIME_WEBRTC_CALL_BASE_URL,
417
- subagent_protocol: protocol,
418
- ...(protocol === "compatibility-v1" ? {
419
- agent_max_depth: route.installedAgentMaxDepth,
420
- } : {}),
421
- },
422
- previous: existing.previous,
423
- previousRealtimeWebrtcCallBaseUrl: existing.version === 9 || existing.version === 10
424
- ? existing.previousRealtimeWebrtcCallBaseUrl
425
- : route.previousRealtimeWebrtcCallBaseUrl,
426
- interruptHook: route.interruptHook,
427
- ...(protocol === "compatibility-v1" ? {
428
- previousMultiAgent: route.previousMultiAgent,
429
- previousMultiAgentV2: route.previousMultiAgentV2,
430
- previousAgentMaxDepth: route.previousAgentMaxDepth,
431
- } : {}),
432
- ...(existing.format ? { format: existing.format } : {}),
433
- };
434
- writeIntegrationState(connected, { path: existing.configPath, data: route.text }, [getCodexModelsCachePath()]);
435
- return { changed: true, active: true };
436
- }
437
-
438
- export function uninstallCodexIntegration(): UninstallCodexIntegrationResult {
439
- const journal = readJournal();
440
- if (!journal) return { changed: false };
441
- if (!existsSync(journal.configPath)) throw new Error(`Codex config is missing: ${journal.configPath}`);
442
- const current = readFileSync(journal.configPath, "utf8");
443
- let restored: string;
444
- if (journal.version === 2) {
445
- if (existsSync(journal.catalogPath) && sha256(readFileSync(journal.catalogPath)) !== journal.catalogSha256) {
446
- throw new Error(`Managed legacy catalog changed after setup: ${journal.catalogPath}`);
447
- }
448
- restored = restoreLegacyV2(current, journal);
449
- } else if ((journal.version === 4 || journal.version === 5 || journal.version === 6 || journal.version === 7 || journal.version === 8 || journal.version === 9 || journal.version === 10) && !journal.active) {
450
- verifyRestoredRoute(current, journal);
451
- restored = current;
452
- } else {
453
- restored = restoreManagedRoute(current, journal);
454
- }
455
- const configSnapshot = snapshotFile(journal.configPath);
456
- const catalogSnapshot = journal.version === 2 ? snapshotFile(journal.catalogPath) : undefined;
457
- const modelsCacheSnapshot = snapshotFile(getCodexModelsCachePath());
458
- const journalSnapshot = snapshotFile(getCodexJournalPath());
459
- const recoverySnapshot = snapshotFile(getCodexJournalRecoveryPath());
460
- try {
461
- atomicWriteFile(journal.configPath, restored);
462
- if (catalogSnapshot?.exists) rmSync(catalogSnapshot.path);
463
- rmSync(modelsCacheSnapshot.path, { force: true });
464
- rmSync(getCodexJournalPath(), { force: true });
465
- rmSync(getCodexJournalRecoveryPath(), { force: true });
466
- } catch (error) {
467
- const rollbackFailures: string[] = [];
468
- for (const snapshot of [recoverySnapshot, journalSnapshot, modelsCacheSnapshot, catalogSnapshot, configSnapshot]) {
469
- if (!snapshot) continue;
470
- try {
471
- restoreFileSnapshot(snapshot);
472
- } catch (caught) {
473
- rollbackFailures.push(`${snapshot.path}: ${caught instanceof Error ? caught.message : String(caught)}`);
474
- }
475
- }
476
- const primary = error instanceof Error ? error.message : String(error);
477
- throw new Error(rollbackFailures.length > 0
478
- ? `${primary}; Codex integration rollback also failed: ${rollbackFailures.join("; ")}`
479
- : primary);
480
- }
481
- return { changed: true };
482
- }
483
-
484
- export function inspectCodexIntegration(): {
485
- installed: boolean;
486
- active: boolean;
487
- configPath: string;
488
- routeUrl?: string;
489
- journal?: AnyCodexIntegrationJournal;
490
- errors: string[];
491
- } {
492
- const journal = readJournal();
493
- const errors: string[] = [];
494
- if (journal) {
495
- try {
496
- assertJournalTargetsConfig(journal, getCodexConfigPath());
497
- const text = readFileSync(journal.configPath, "utf8");
498
- if ((journal.version === 4 || journal.version === 5 || journal.version === 6 || journal.version === 7 || journal.version === 8 || journal.version === 9 || journal.version === 10) && !journal.active) {
499
- verifyRestoredRoute(text, journal);
500
- }
501
- else if (journal.version === 3 || journal.version === 4 || journal.version === 5 || journal.version === 6 || journal.version === 7 || journal.version === 8 || journal.version === 9 || journal.version === 10) {
502
- verifyInstalledRoute(text, journal);
503
- }
504
- else {
505
- const lines = splitLines(text);
506
- for (const key of ["model_provider", "model_catalog_json"] as const) {
507
- if (findTopLevelAssignment(lines, key).value !== journal.installed[key]) {
508
- errors.push(`Codex ${key} no longer matches this installation`);
509
- }
510
- }
511
- if (!text.includes(journal.providerBlock)) errors.push("Managed legacy Codex provider block no longer matches this installation");
512
- }
513
- } catch (error) {
514
- errors.push(error instanceof Error ? error.message : String(error));
515
- }
516
- }
517
- return {
518
- installed: Boolean(journal),
519
- active: journal?.version === 4 || journal?.version === 5 || journal?.version === 6 || journal?.version === 7 || journal?.version === 8 || journal?.version === 9 || journal?.version === 10
520
- ? journal.active
521
- : Boolean(journal),
522
- configPath: getCodexConfigPath(),
523
- ...(journal?.version === 3 || journal?.version === 4 || journal?.version === 5 || journal?.version === 6 || journal?.version === 7 || journal?.version === 8 || journal?.version === 9 || journal?.version === 10
524
- ? { routeUrl: journal.installed.openai_base_url }
525
- : {}),
526
- ...(journal ? { journal } : {}),
527
- errors,
528
- };
529
- }
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import type { AppConfig } from "./config";
4
+ import { atomicWriteFile, getConfigPath, loadConfig, saveConfig } from "./config";
5
+ import { installCodexInterruptHook, installCodexInterruptHookCommand } from "./codex-interrupt-hook";
6
+ import {
7
+ CODEX_REALTIME_WEBRTC_CALL_BASE_URL,
8
+ getCodexConfigPath,
9
+ getCodexJournalPath,
10
+ getCodexJournalRecoveryPath,
11
+ getCodexModelsCachePath,
12
+ restoreFileSnapshot,
13
+ routeUrl,
14
+ sha256,
15
+ snapshotFile,
16
+ writeIntegrationState,
17
+ } from "./codex-integration-shared";
18
+ import type {
19
+ AnyCodexIntegrationJournal,
20
+ CodexIntegrationJournal,
21
+ InstallCodexIntegrationOptions,
22
+ LegacyCodexIntegrationJournalV4,
23
+ LegacyCodexIntegrationJournalV5,
24
+ LegacyCodexIntegrationJournalV6,
25
+ LegacyCodexIntegrationJournalV7,
26
+ LegacyCodexIntegrationJournalV8,
27
+ LegacyCodexIntegrationJournalV9,
28
+ SetCodexIntegrationActiveResult,
29
+ UninstallCodexIntegrationResult,
30
+ } from "./codex-integration-shared";
31
+ import { assertJournalTargetsConfig, readJournal } from "./codex-integration-journal";
32
+ import {
33
+ findTopLevelAssignment,
34
+ installCompatibilityV1Features,
35
+ splitLines,
36
+ textFormat,
37
+ } from "./codex-integration-document";
38
+ import {
39
+ assertPreservedPreviousAssignments,
40
+ assertPreservedPreviousRealtimeAssignment,
41
+ installRoute,
42
+ managedJournalIsActive,
43
+ replacementBaseline,
44
+ restoreLegacyV2,
45
+ restoreManagedRoute,
46
+ verifyInstalledRoute,
47
+ verifyManagedJournalState,
48
+ verifyRestoredRoute,
49
+ } from "./codex-integration-route";
50
+
51
+ function installConfiguredRoute(
52
+ baseline: string,
53
+ installedUrl: string,
54
+ config: Pick<AppConfig, "subagentProtocol"> & (
55
+ Pick<AppConfig, "runtimeCommand"> | { interruptHookCommand: string }
56
+ ),
57
+ replaceExistingRoute: boolean,
58
+ replaceExistingRealtimeRoute: boolean,
59
+ ): {
60
+ text: string;
61
+ previous: CodexIntegrationJournal["previous"];
62
+ previousRealtimeWebrtcCallBaseUrl: CodexIntegrationJournal["previousRealtimeWebrtcCallBaseUrl"];
63
+ previousMultiAgent?: CodexIntegrationJournal["previousMultiAgent"];
64
+ previousMultiAgentV2?: CodexIntegrationJournal["previousMultiAgentV2"];
65
+ previousAgentMaxDepth?: CodexIntegrationJournal["previousAgentMaxDepth"];
66
+ installedAgentMaxDepth?: number;
67
+ interruptHook: CodexIntegrationJournal["interruptHook"];
68
+ } {
69
+ const route = installRoute(
70
+ baseline,
71
+ installedUrl,
72
+ replaceExistingRoute,
73
+ replaceExistingRealtimeRoute,
74
+ );
75
+ const configured = config.subagentProtocol === "compatibility-v1"
76
+ ? (() => {
77
+ const features = installCompatibilityV1Features(route.text);
78
+ return {
79
+ text: features.text,
80
+ previous: route.previous,
81
+ previousRealtimeWebrtcCallBaseUrl: route.previousRealtimeWebrtcCallBaseUrl,
82
+ previousMultiAgent: features.previousMultiAgent,
83
+ previousMultiAgentV2: features.previousMultiAgentV2,
84
+ previousAgentMaxDepth: features.previousAgentMaxDepth,
85
+ installedAgentMaxDepth: features.installedAgentMaxDepth,
86
+ };
87
+ })()
88
+ : route;
89
+ const hook = "interruptHookCommand" in config
90
+ ? installCodexInterruptHookCommand(configured.text, getCodexConfigPath(), config.interruptHookCommand)
91
+ : installCodexInterruptHook(configured.text, getCodexConfigPath(), config);
92
+ return { ...configured, text: hook.text, interruptHook: hook.installed };
93
+ }
94
+
95
+ function journalProtocol(journal: Exclude<AnyCodexIntegrationJournal, { version: 2 }>): AppConfig["subagentProtocol"] {
96
+ return journal.version === 8 || journal.version === 9 || journal.version === 10
97
+ ? journal.installed.subagent_protocol
98
+ : "native";
99
+ }
100
+
101
+ export {
102
+ getCodexConfigPath,
103
+ getCodexHome,
104
+ getCodexJournalPath,
105
+ getCodexJournalRecoveryPath,
106
+ getCodexModelsCachePath,
107
+ } from "./codex-integration-shared";
108
+ export { readCodexModelContextOverride } from "./codex-integration-document";
109
+ export type {
110
+ CodexIntegrationJournal,
111
+ CodexModelContextOverride,
112
+ InstallCodexIntegrationOptions,
113
+ SetCodexIntegrationActiveResult,
114
+ UninstallCodexIntegrationResult,
115
+ } from "./codex-integration-shared";
116
+
117
+ export function readCodexSubagentProtocol(
118
+ fallback: AppConfig["subagentProtocol"] = "compatibility-v1",
119
+ ): AppConfig["subagentProtocol"] {
120
+ const journal = readJournal();
121
+ return journal?.version === 8 || journal?.version === 9 || journal?.version === 10
122
+ ? journal.installed.subagent_protocol
123
+ : fallback;
124
+ }
125
+
126
+ export function setCodexSubagentProtocol(
127
+ config: AppConfig,
128
+ protocol: AppConfig["subagentProtocol"],
129
+ ): CodexIntegrationJournal {
130
+ const status = inspectCodexIntegration();
131
+ if (!status.installed) throw new Error("Codex integration is not installed; run setup first");
132
+ if (!status.active) {
133
+ throw new Error("Codex integration is disconnected; reconnect it before changing the subagent protocol");
134
+ }
135
+ const nextConfig = { ...config, subagentProtocol: protocol };
136
+ // The runtime catalog and Codex feature surface are two halves of one protocol selection. If
137
+ // either write fails, restore every participant so the next launcher/Codex restart cannot load a
138
+ // split V1/V2 state.
139
+ const snapshots = [
140
+ getConfigPath(),
141
+ getCodexConfigPath(),
142
+ getCodexModelsCachePath(),
143
+ getCodexJournalPath(),
144
+ getCodexJournalRecoveryPath(),
145
+ ].map(snapshotFile);
146
+ try {
147
+ const journal = installCodexIntegration(nextConfig);
148
+ saveConfig(nextConfig);
149
+ return journal;
150
+ } catch (error) {
151
+ const rollbackFailures: string[] = [];
152
+ for (const snapshot of [...snapshots].reverse()) {
153
+ try {
154
+ restoreFileSnapshot(snapshot);
155
+ } catch (rollbackError) {
156
+ rollbackFailures.push(
157
+ `${snapshot.path}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`,
158
+ );
159
+ }
160
+ }
161
+ const primary = error instanceof Error ? error.message : String(error);
162
+ throw new Error(rollbackFailures.length > 0
163
+ ? `${primary}; subagent protocol rollback also failed: ${rollbackFailures.join("; ")}`
164
+ : primary);
165
+ }
166
+ }
167
+
168
+ export function preflightCodexIntegration(
169
+ config: AppConfig,
170
+ options: InstallCodexIntegrationOptions = {},
171
+ ): void {
172
+ const configPath = getCodexConfigPath();
173
+ const configExists = existsSync(configPath);
174
+ const currentText = configExists ? readFileSync(configPath, "utf8") : "";
175
+ const existing = readJournal();
176
+ const installedUrl = routeUrl(config);
177
+ if (existing) assertJournalTargetsConfig(existing, configPath);
178
+ if (existing && existing.version !== 2) {
179
+ if (!configExists) {
180
+ if (options.replaceExistingRoute !== true) {
181
+ throw new Error(`Codex config is missing: ${configPath}`);
182
+ }
183
+ installConfiguredRoute("", installedUrl, config, true, true);
184
+ return;
185
+ }
186
+ try {
187
+ verifyManagedJournalState(currentText, existing);
188
+ } catch (error) {
189
+ if (options.replaceExistingRoute !== true) throw error;
190
+ installConfiguredRoute(
191
+ replacementBaseline(currentText, configExists, existing),
192
+ installedUrl,
193
+ config,
194
+ true,
195
+ true,
196
+ );
197
+ return;
198
+ }
199
+ if (existing.version === 10) return;
200
+ const baseline = managedJournalIsActive(existing)
201
+ ? restoreManagedRoute(currentText, existing)
202
+ : currentText;
203
+ installConfiguredRoute(
204
+ baseline,
205
+ installedUrl,
206
+ config,
207
+ true,
208
+ options.replaceExistingRoute === true,
209
+ );
210
+ return;
211
+ }
212
+ let baseline = currentText;
213
+ if (existing?.version === 2) {
214
+ if (existsSync(existing.catalogPath) && sha256(readFileSync(existing.catalogPath)) !== existing.catalogSha256) {
215
+ throw new Error(`Managed legacy catalog changed after setup; refusing migration: ${existing.catalogPath}`);
216
+ }
217
+ baseline = restoreLegacyV2(currentText, existing);
218
+ }
219
+ installConfiguredRoute(
220
+ baseline,
221
+ installedUrl,
222
+ config,
223
+ options.replaceExistingRoute === true,
224
+ options.replaceExistingRoute === true,
225
+ );
226
+ }
227
+ export function installCodexIntegration(
228
+ config: AppConfig,
229
+ options: InstallCodexIntegrationOptions = {},
230
+ ): CodexIntegrationJournal {
231
+ const configPath = getCodexConfigPath();
232
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
233
+ const configExists = existsSync(configPath);
234
+ const currentText = configExists ? readFileSync(configPath, "utf8") : "";
235
+ const existing = readJournal();
236
+ const installedUrl = routeUrl(config);
237
+ if (existing) assertJournalTargetsConfig(existing, configPath);
238
+
239
+ const hasManagedJournal = Boolean(existing && existing.version !== 2);
240
+ if (hasManagedJournal && !configExists && options.replaceExistingRoute !== true) {
241
+ throw new Error(`Codex config is missing: ${configPath}`);
242
+ }
243
+
244
+ if (hasManagedJournal && existing && existing.version !== 2) {
245
+ let baseline: string;
246
+ let preservePrevious = true;
247
+ try {
248
+ verifyManagedJournalState(currentText, existing);
249
+ baseline = managedJournalIsActive(existing)
250
+ ? restoreManagedRoute(currentText, existing)
251
+ : currentText;
252
+ } catch (error) {
253
+ if (options.replaceExistingRoute !== true) throw error;
254
+ baseline = replacementBaseline(currentText, configExists, existing);
255
+ preservePrevious = false;
256
+ }
257
+ const patched = installConfiguredRoute(
258
+ baseline,
259
+ installedUrl,
260
+ config,
261
+ true,
262
+ !preservePrevious || existing.version === 9 || existing.version === 10 || options.replaceExistingRoute === true,
263
+ );
264
+ if (preservePrevious) {
265
+ assertPreservedPreviousAssignments(patched.previous, existing.previous);
266
+ if (existing.version === 9 || existing.version === 10) {
267
+ assertPreservedPreviousRealtimeAssignment(
268
+ patched.previousRealtimeWebrtcCallBaseUrl,
269
+ existing.previousRealtimeWebrtcCallBaseUrl,
270
+ );
271
+ }
272
+ }
273
+ const updated: CodexIntegrationJournal = {
274
+ version: 10,
275
+ active: true,
276
+ configPath,
277
+ installed: {
278
+ openai_base_url: installedUrl,
279
+ experimental_realtime_webrtc_call_base_url: CODEX_REALTIME_WEBRTC_CALL_BASE_URL,
280
+ subagent_protocol: config.subagentProtocol,
281
+ ...(config.subagentProtocol === "compatibility-v1" ? {
282
+ agent_max_depth: patched.installedAgentMaxDepth,
283
+ } : {}),
284
+ },
285
+ previous: preservePrevious ? existing.previous : patched.previous,
286
+ previousRealtimeWebrtcCallBaseUrl: preservePrevious && (existing.version === 9 || existing.version === 10)
287
+ ? existing.previousRealtimeWebrtcCallBaseUrl
288
+ : patched.previousRealtimeWebrtcCallBaseUrl,
289
+ interruptHook: patched.interruptHook,
290
+ ...(config.subagentProtocol === "compatibility-v1" ? {
291
+ previousMultiAgent: patched.previousMultiAgent,
292
+ previousMultiAgentV2: patched.previousMultiAgentV2,
293
+ previousAgentMaxDepth: patched.previousAgentMaxDepth,
294
+ } : {}),
295
+ ...(existing.format ? { format: existing.format } : {}),
296
+ };
297
+ writeIntegrationState(updated, { path: configPath, data: patched.text }, [getCodexModelsCachePath()]);
298
+ return updated;
299
+ }
300
+
301
+ let baseline = currentText;
302
+ if (existing?.version === 2) {
303
+ if (existsSync(existing.catalogPath) && sha256(readFileSync(existing.catalogPath)) !== existing.catalogSha256) {
304
+ throw new Error(`Managed legacy catalog changed after setup; refusing migration: ${existing.catalogPath}`);
305
+ }
306
+ baseline = restoreLegacyV2(currentText, existing);
307
+ }
308
+ const patched = installConfiguredRoute(
309
+ baseline,
310
+ installedUrl,
311
+ config,
312
+ options.replaceExistingRoute === true,
313
+ options.replaceExistingRoute === true,
314
+ );
315
+ const journal: CodexIntegrationJournal = {
316
+ version: 10,
317
+ active: true,
318
+ configPath,
319
+ installed: {
320
+ openai_base_url: installedUrl,
321
+ experimental_realtime_webrtc_call_base_url: CODEX_REALTIME_WEBRTC_CALL_BASE_URL,
322
+ subagent_protocol: config.subagentProtocol,
323
+ ...(config.subagentProtocol === "compatibility-v1" ? {
324
+ agent_max_depth: patched.installedAgentMaxDepth,
325
+ } : {}),
326
+ },
327
+ previous: patched.previous,
328
+ previousRealtimeWebrtcCallBaseUrl: patched.previousRealtimeWebrtcCallBaseUrl,
329
+ interruptHook: patched.interruptHook,
330
+ ...(config.subagentProtocol === "compatibility-v1" ? {
331
+ previousMultiAgent: patched.previousMultiAgent,
332
+ previousMultiAgentV2: patched.previousMultiAgentV2,
333
+ previousAgentMaxDepth: patched.previousAgentMaxDepth,
334
+ } : {}),
335
+ format: textFormat(baseline),
336
+ };
337
+ writeIntegrationState(journal, { path: configPath, data: patched.text }, [getCodexModelsCachePath()]);
338
+ if (existing?.version === 2 && existsSync(existing.catalogPath)) rmSync(existing.catalogPath);
339
+ return journal;
340
+ }
341
+
342
+ export function deactivateCodexIntegration(): SetCodexIntegrationActiveResult {
343
+ const existing = readJournal();
344
+ if (!existing) return { changed: false, active: false };
345
+ if (existing.version === 2) {
346
+ throw new Error("Legacy Codex integration must be upgraded by Setup before the bridge can be disconnected");
347
+ }
348
+ assertJournalTargetsConfig(existing, getCodexConfigPath());
349
+ if (!existsSync(existing.configPath)) throw new Error(`Codex config is missing: ${existing.configPath}`);
350
+ const current = readFileSync(existing.configPath, "utf8");
351
+ if ((existing.version === 4 || existing.version === 5 || existing.version === 6 || existing.version === 7 || existing.version === 8 || existing.version === 9 || existing.version === 10) && !existing.active) {
352
+ verifyRestoredRoute(current, existing);
353
+ return { changed: false, active: false };
354
+ }
355
+ const restored = restoreManagedRoute(current, existing);
356
+ const disconnected:
357
+ | CodexIntegrationJournal
358
+ | LegacyCodexIntegrationJournalV9
359
+ | LegacyCodexIntegrationJournalV8
360
+ | LegacyCodexIntegrationJournalV6
361
+ | LegacyCodexIntegrationJournalV7
362
+ | LegacyCodexIntegrationJournalV5
363
+ | LegacyCodexIntegrationJournalV4 = existing.version === 6 || existing.version === 5
364
+ || existing.version === 7 || existing.version === 8 || existing.version === 9 || existing.version === 10
365
+ ? { ...existing, active: false }
366
+ : { ...existing, version: 4, active: false };
367
+ writeIntegrationState(disconnected, { path: existing.configPath, data: restored }, [getCodexModelsCachePath()]);
368
+ return { changed: true, active: false };
369
+ }
370
+
371
+ export function activateCodexIntegration(): SetCodexIntegrationActiveResult {
372
+ const existing = readJournal();
373
+ if (!existing) throw new Error("Codex integration is not installed");
374
+ if (existing.version === 2) {
375
+ throw new Error("Legacy Codex integration must be upgraded by Setup before the bridge can be reconnected");
376
+ }
377
+ assertJournalTargetsConfig(existing, getCodexConfigPath());
378
+ if (!existsSync(existing.configPath)) throw new Error(`Codex config is missing: ${existing.configPath}`);
379
+ const current = readFileSync(existing.configPath, "utf8");
380
+ if (existing.version === 10 && existing.active) {
381
+ verifyInstalledRoute(current, existing);
382
+ return { changed: false, active: true };
383
+ }
384
+ let baseline: string;
385
+ if ((existing.version === 4 || existing.version === 5 || existing.version === 6 || existing.version === 7 || existing.version === 8 || existing.version === 9 || existing.version === 10) && !existing.active) {
386
+ verifyRestoredRoute(current, existing);
387
+ baseline = current;
388
+ } else {
389
+ verifyInstalledRoute(current, existing);
390
+ baseline = restoreManagedRoute(current, existing);
391
+ }
392
+ const protocol = journalProtocol(existing);
393
+ const hookConfig = existing.version === 10
394
+ ? { interruptHookCommand: existing.interruptHook.command }
395
+ : { runtimeCommand: loadConfig().runtimeCommand };
396
+ const route = installConfiguredRoute(
397
+ baseline,
398
+ existing.installed.openai_base_url,
399
+ { subagentProtocol: protocol, ...hookConfig },
400
+ true,
401
+ existing.version === 9 || existing.version === 10,
402
+ );
403
+ assertPreservedPreviousAssignments(route.previous, existing.previous);
404
+ if (existing.version === 9 || existing.version === 10) {
405
+ assertPreservedPreviousRealtimeAssignment(
406
+ route.previousRealtimeWebrtcCallBaseUrl,
407
+ existing.previousRealtimeWebrtcCallBaseUrl,
408
+ );
409
+ }
410
+ const connected: CodexIntegrationJournal = {
411
+ version: 10,
412
+ active: true,
413
+ configPath: existing.configPath,
414
+ installed: {
415
+ openai_base_url: existing.installed.openai_base_url,
416
+ experimental_realtime_webrtc_call_base_url: CODEX_REALTIME_WEBRTC_CALL_BASE_URL,
417
+ subagent_protocol: protocol,
418
+ ...(protocol === "compatibility-v1" ? {
419
+ agent_max_depth: route.installedAgentMaxDepth,
420
+ } : {}),
421
+ },
422
+ previous: existing.previous,
423
+ previousRealtimeWebrtcCallBaseUrl: existing.version === 9 || existing.version === 10
424
+ ? existing.previousRealtimeWebrtcCallBaseUrl
425
+ : route.previousRealtimeWebrtcCallBaseUrl,
426
+ interruptHook: route.interruptHook,
427
+ ...(protocol === "compatibility-v1" ? {
428
+ previousMultiAgent: route.previousMultiAgent,
429
+ previousMultiAgentV2: route.previousMultiAgentV2,
430
+ previousAgentMaxDepth: route.previousAgentMaxDepth,
431
+ } : {}),
432
+ ...(existing.format ? { format: existing.format } : {}),
433
+ };
434
+ writeIntegrationState(connected, { path: existing.configPath, data: route.text }, [getCodexModelsCachePath()]);
435
+ return { changed: true, active: true };
436
+ }
437
+
438
+ export function uninstallCodexIntegration(): UninstallCodexIntegrationResult {
439
+ const journal = readJournal();
440
+ if (!journal) return { changed: false };
441
+ if (!existsSync(journal.configPath)) throw new Error(`Codex config is missing: ${journal.configPath}`);
442
+ const current = readFileSync(journal.configPath, "utf8");
443
+ let restored: string;
444
+ if (journal.version === 2) {
445
+ if (existsSync(journal.catalogPath) && sha256(readFileSync(journal.catalogPath)) !== journal.catalogSha256) {
446
+ throw new Error(`Managed legacy catalog changed after setup: ${journal.catalogPath}`);
447
+ }
448
+ restored = restoreLegacyV2(current, journal);
449
+ } else if ((journal.version === 4 || journal.version === 5 || journal.version === 6 || journal.version === 7 || journal.version === 8 || journal.version === 9 || journal.version === 10) && !journal.active) {
450
+ verifyRestoredRoute(current, journal);
451
+ restored = current;
452
+ } else {
453
+ restored = restoreManagedRoute(current, journal);
454
+ }
455
+ const configSnapshot = snapshotFile(journal.configPath);
456
+ const catalogSnapshot = journal.version === 2 ? snapshotFile(journal.catalogPath) : undefined;
457
+ const modelsCacheSnapshot = snapshotFile(getCodexModelsCachePath());
458
+ const journalSnapshot = snapshotFile(getCodexJournalPath());
459
+ const recoverySnapshot = snapshotFile(getCodexJournalRecoveryPath());
460
+ try {
461
+ atomicWriteFile(journal.configPath, restored);
462
+ if (catalogSnapshot?.exists) rmSync(catalogSnapshot.path);
463
+ rmSync(modelsCacheSnapshot.path, { force: true });
464
+ rmSync(getCodexJournalPath(), { force: true });
465
+ rmSync(getCodexJournalRecoveryPath(), { force: true });
466
+ } catch (error) {
467
+ const rollbackFailures: string[] = [];
468
+ for (const snapshot of [recoverySnapshot, journalSnapshot, modelsCacheSnapshot, catalogSnapshot, configSnapshot]) {
469
+ if (!snapshot) continue;
470
+ try {
471
+ restoreFileSnapshot(snapshot);
472
+ } catch (caught) {
473
+ rollbackFailures.push(`${snapshot.path}: ${caught instanceof Error ? caught.message : String(caught)}`);
474
+ }
475
+ }
476
+ const primary = error instanceof Error ? error.message : String(error);
477
+ throw new Error(rollbackFailures.length > 0
478
+ ? `${primary}; Codex integration rollback also failed: ${rollbackFailures.join("; ")}`
479
+ : primary);
480
+ }
481
+ return { changed: true };
482
+ }
483
+
484
+ export function inspectCodexIntegration(): {
485
+ installed: boolean;
486
+ active: boolean;
487
+ configPath: string;
488
+ routeUrl?: string;
489
+ journal?: AnyCodexIntegrationJournal;
490
+ errors: string[];
491
+ } {
492
+ const journal = readJournal();
493
+ const errors: string[] = [];
494
+ if (journal) {
495
+ try {
496
+ assertJournalTargetsConfig(journal, getCodexConfigPath());
497
+ const text = readFileSync(journal.configPath, "utf8");
498
+ if ((journal.version === 4 || journal.version === 5 || journal.version === 6 || journal.version === 7 || journal.version === 8 || journal.version === 9 || journal.version === 10) && !journal.active) {
499
+ verifyRestoredRoute(text, journal);
500
+ }
501
+ else if (journal.version === 3 || journal.version === 4 || journal.version === 5 || journal.version === 6 || journal.version === 7 || journal.version === 8 || journal.version === 9 || journal.version === 10) {
502
+ verifyInstalledRoute(text, journal);
503
+ }
504
+ else {
505
+ const lines = splitLines(text);
506
+ for (const key of ["model_provider", "model_catalog_json"] as const) {
507
+ if (findTopLevelAssignment(lines, key).value !== journal.installed[key]) {
508
+ errors.push(`Codex ${key} no longer matches this installation`);
509
+ }
510
+ }
511
+ if (!text.includes(journal.providerBlock)) errors.push("Managed legacy Codex provider block no longer matches this installation");
512
+ }
513
+ } catch (error) {
514
+ errors.push(error instanceof Error ? error.message : String(error));
515
+ }
516
+ }
517
+ return {
518
+ installed: Boolean(journal),
519
+ active: journal?.version === 4 || journal?.version === 5 || journal?.version === 6 || journal?.version === 7 || journal?.version === 8 || journal?.version === 9 || journal?.version === 10
520
+ ? journal.active
521
+ : Boolean(journal),
522
+ configPath: getCodexConfigPath(),
523
+ ...(journal?.version === 3 || journal?.version === 4 || journal?.version === 5 || journal?.version === 6 || journal?.version === 7 || journal?.version === 8 || journal?.version === 9 || journal?.version === 10
524
+ ? { routeUrl: journal.installed.openai_base_url }
525
+ : {}),
526
+ ...(journal ? { journal } : {}),
527
+ errors,
528
+ };
529
+ }