@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.
- package/LICENSE +21 -21
- package/README.md +140 -140
- package/cordis.patch.yml +4 -4
- package/lib/cli.js +13269 -11860
- package/lib/plugin.js +3 -50
- package/package.json +89 -88
- package/src/adapters/base.ts +16 -16
- package/src/adapters/chatgpt-web/adapter-error.ts +59 -59
- package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -513
- package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -27
- package/src/adapters/chatgpt-web/browser-worker.ts +4944 -4944
- package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -628
- package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -533
- package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -142
- package/src/adapters/chatgpt-web/concurrency.ts +6 -6
- package/src/adapters/chatgpt-web/conversation-key.ts +58 -58
- package/src/adapters/chatgpt-web/environment.ts +669 -669
- package/src/adapters/chatgpt-web/index.ts +1544 -1544
- package/src/adapters/chatgpt-web/input-tokens.ts +74 -74
- package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -695
- package/src/adapters/chatgpt-web/markdown.ts +418 -418
- package/src/adapters/chatgpt-web/mcp-main.ts +25 -25
- package/src/adapters/chatgpt-web/mcp-server.ts +933 -933
- package/src/adapters/chatgpt-web/model.ts +70 -70
- package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -74
- package/src/adapters/chatgpt-web/output-validation.ts +62 -62
- package/src/adapters/chatgpt-web/process-line-writer.ts +46 -46
- package/src/adapters/chatgpt-web/prompt.ts +702 -702
- package/src/adapters/chatgpt-web/retry-policy.ts +73 -73
- package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -384
- package/src/adapters/chatgpt-web/thread-environment.ts +238 -238
- package/src/adapters/chatgpt-web/tool-stream-parser.ts +1 -1
- package/src/adapters/chatgpt-web/turn-broker.ts +1481 -1481
- package/src/adapters/chatgpt-web/turn-execution.ts +816 -816
- package/src/adapters/chatgpt-web/turn-progress.ts +292 -292
- package/src/adapters/chatgpt-web/usage.ts +121 -121
- package/src/adapters/image.ts +9 -9
- package/src/bridge.ts +1083 -1083
- package/src/browser-login.ts +521 -521
- package/src/chatgpt-session.ts +240 -240
- package/src/chatgpt-web-models.ts +400 -400
- package/src/cli.ts +568 -568
- package/src/codex-integration-document.ts +824 -824
- package/src/codex-integration-journal.ts +212 -212
- package/src/codex-integration-route.ts +515 -515
- package/src/codex-integration-shared.ts +332 -332
- package/src/codex-integration.ts +529 -529
- package/src/codex-interrupt-hook.ts +158 -158
- package/src/config.ts +616 -616
- package/src/dev-chat/cli.ts +432 -432
- package/src/dev-chat/constants.ts +3 -3
- package/src/dev-chat/driver.ts +655 -655
- package/src/dev-chat/profile.ts +223 -223
- package/src/dev-chat/session.ts +287 -287
- package/src/dev-chat/transport.ts +54 -54
- package/src/doctor.ts +237 -237
- package/src/event-queue.ts +45 -45
- package/src/http-body.ts +30 -30
- package/src/launcher-browser-host.ts +695 -695
- package/src/lib/errors.ts +281 -281
- package/src/lib/token-estimate.ts +42 -42
- package/src/login-helper.cjs +1 -1
- package/src/model-catalog.ts +197 -197
- package/src/native-passthrough.ts +261 -261
- package/src/plugin.ts +191 -191
- package/src/process.ts +45 -45
- package/src/responses/compaction.ts +199 -199
- package/src/responses/parser.ts +633 -633
- package/src/responses/reasoning-envelope.ts +49 -49
- package/src/responses/schema.ts +172 -172
- package/src/responses/state.ts +230 -230
- package/src/server.ts +1111 -1111
- package/src/service.ts +315 -315
- package/src/setup.ts +671 -671
- package/src/stall-timeout.ts +23 -23
- package/src/tunnel-service.ts +160 -160
- package/src/tunnel.ts +417 -417
- package/src/turndown-plugin-gfm.d.ts +5 -5
- package/src/types.ts +307 -307
- package/src/usage/totals.ts +12 -12
- package/src/version.ts +1 -1
package/src/codex-integration.ts
CHANGED
|
@@ -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
|
+
}
|