@wlv-zedd/dsh-chatgpt-web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/assets/demo.gif +0 -0
- package/assets/hero-demo.png +0 -0
- package/assets/promo-dshmarket-official.png +0 -0
- package/cordis.patch.yml +4 -0
- package/lib/cli.js +239642 -0
- package/lib/plugin.js +195 -0
- package/package.json +88 -0
- package/screenshots.json +5 -0
- package/src/adapters/base.ts +16 -0
- package/src/adapters/chatgpt-web/adapter-error.ts +59 -0
- package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -0
- package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -0
- package/src/adapters/chatgpt-web/browser-worker.ts +4944 -0
- package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -0
- package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -0
- package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -0
- package/src/adapters/chatgpt-web/concurrency.ts +6 -0
- package/src/adapters/chatgpt-web/conversation-key.ts +58 -0
- package/src/adapters/chatgpt-web/environment.ts +669 -0
- package/src/adapters/chatgpt-web/index.ts +1544 -0
- package/src/adapters/chatgpt-web/input-tokens.ts +74 -0
- package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -0
- package/src/adapters/chatgpt-web/markdown.ts +418 -0
- package/src/adapters/chatgpt-web/mcp-main.ts +25 -0
- package/src/adapters/chatgpt-web/mcp-server.ts +933 -0
- package/src/adapters/chatgpt-web/model.ts +70 -0
- package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -0
- package/src/adapters/chatgpt-web/output-validation.ts +62 -0
- package/src/adapters/chatgpt-web/process-line-writer.ts +46 -0
- package/src/adapters/chatgpt-web/prompt.ts +702 -0
- package/src/adapters/chatgpt-web/retry-policy.ts +73 -0
- package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -0
- package/src/adapters/chatgpt-web/thread-environment.ts +238 -0
- package/src/adapters/chatgpt-web/tool-stream-parser.ts +601 -0
- package/src/adapters/chatgpt-web/turn-broker.ts +1481 -0
- package/src/adapters/chatgpt-web/turn-execution.ts +816 -0
- package/src/adapters/chatgpt-web/turn-progress.ts +292 -0
- package/src/adapters/chatgpt-web/usage.ts +121 -0
- package/src/adapters/image.ts +9 -0
- package/src/bridge.ts +1083 -0
- package/src/browser-login.ts +521 -0
- package/src/chatgpt-session.ts +240 -0
- package/src/chatgpt-web-models.ts +400 -0
- package/src/cli.ts +568 -0
- package/src/codex-integration-document.ts +824 -0
- package/src/codex-integration-journal.ts +212 -0
- package/src/codex-integration-route.ts +515 -0
- package/src/codex-integration-shared.ts +332 -0
- package/src/codex-integration.ts +529 -0
- package/src/codex-interrupt-hook.ts +158 -0
- package/src/config.ts +616 -0
- package/src/dev-chat/cli.ts +432 -0
- package/src/dev-chat/constants.ts +3 -0
- package/src/dev-chat/driver.ts +655 -0
- package/src/dev-chat/profile.ts +223 -0
- package/src/dev-chat/session.ts +287 -0
- package/src/dev-chat/transport.ts +54 -0
- package/src/doctor.ts +237 -0
- package/src/event-queue.ts +45 -0
- package/src/http-body.ts +30 -0
- package/src/launcher-browser-host.ts +695 -0
- package/src/lib/errors.ts +281 -0
- package/src/lib/token-estimate.ts +42 -0
- package/src/login-helper.cjs +140 -0
- package/src/model-catalog.ts +197 -0
- package/src/native-passthrough.ts +261 -0
- package/src/plugin.ts +191 -0
- package/src/process.ts +45 -0
- package/src/responses/compaction.ts +199 -0
- package/src/responses/parser.ts +633 -0
- package/src/responses/reasoning-envelope.ts +49 -0
- package/src/responses/schema.ts +172 -0
- package/src/responses/state.ts +230 -0
- package/src/server.ts +1111 -0
- package/src/service.ts +315 -0
- package/src/setup.ts +671 -0
- package/src/stall-timeout.ts +23 -0
- package/src/tunnel-service.ts +160 -0
- package/src/tunnel.ts +417 -0
- package/src/turndown-plugin-gfm.d.ts +5 -0
- package/src/types.ts +307 -0
- package/src/usage/totals.ts +12 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, posix, resolve, win32 } from "node:path";
|
|
4
|
+
import type { AppConfig } from "./config";
|
|
5
|
+
import { getConfigDir } from "./config";
|
|
6
|
+
import type { InstalledCodexInterruptHook } from "./codex-integration-shared";
|
|
7
|
+
|
|
8
|
+
export const MANAGED_INTERRUPT_HOOK_START =
|
|
9
|
+
"# Managed by codex-chatgpt-web: release the exact Responses request when its Codex turn is interrupted.";
|
|
10
|
+
export const MANAGED_INTERRUPT_HOOK_END =
|
|
11
|
+
"# End codex-chatgpt-web interrupt lifecycle hook.";
|
|
12
|
+
|
|
13
|
+
function canonicalJson(value: unknown): unknown {
|
|
14
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
15
|
+
if (!value || typeof value !== "object") return value;
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
Object.entries(value as Record<string, unknown>)
|
|
18
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
19
|
+
.map(([key, item]) => [key, canonicalJson(item)]),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Match codex_config::version_for_toml for the normalized Interrupt command hook. */
|
|
24
|
+
export function codexInterruptHookHash(command: string): string {
|
|
25
|
+
const identity = canonicalJson({
|
|
26
|
+
event_name: "interrupt",
|
|
27
|
+
hooks: [{
|
|
28
|
+
type: "command",
|
|
29
|
+
command,
|
|
30
|
+
timeout: 3,
|
|
31
|
+
async: false,
|
|
32
|
+
}],
|
|
33
|
+
});
|
|
34
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(identity)).digest("hex")}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function posixShellArgument(value: string): string {
|
|
38
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cmdShellArgument(value: string): string {
|
|
42
|
+
if (value.includes('"') || /[\r\n]/.test(value)) {
|
|
43
|
+
throw new Error("Codex interrupt hook command contains an invalid Windows path character");
|
|
44
|
+
}
|
|
45
|
+
// Codex executes command hooks through cmd.exe /C on Windows. Quoting every argument preserves
|
|
46
|
+
// spaces and shell metacharacters in the installed runtime path.
|
|
47
|
+
return `"${value}"`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function codexInterruptHookCommand(
|
|
51
|
+
config: Pick<AppConfig, "runtimeCommand">,
|
|
52
|
+
home = getConfigDir(),
|
|
53
|
+
platform: NodeJS.Platform = process.platform,
|
|
54
|
+
): string {
|
|
55
|
+
const absoluteHome = platform === "win32" ? win32.resolve(home) : posix.resolve(home);
|
|
56
|
+
const args = [...config.runtimeCommand, "--home", absoluteHome, "hook", "interrupt"];
|
|
57
|
+
return args.map(platform === "win32" ? cmdShellArgument : posixShellArgument).join(" ");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function lineEnding(text: string): "\n" | "\r\n" | "\r" {
|
|
61
|
+
return text.includes("\r\n") ? "\r\n" : text.includes("\n") ? "\n" : text.includes("\r") ? "\r" : "\n";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function interruptGroupCount(text: string): number {
|
|
65
|
+
return text.split(/\r\n|\n|\r/).filter(line => /^\s*\[\[hooks\.Interrupt\]\]\s*(?:#.*)?$/.test(line)).length;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function managedMarkerCount(text: string): number {
|
|
69
|
+
return text.split(MANAGED_INTERRUPT_HOOK_START).length - 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function canonicalConfigPath(configPath: string): string {
|
|
73
|
+
const absolute = resolve(configPath);
|
|
74
|
+
try {
|
|
75
|
+
return realpathSync.native(absolute);
|
|
76
|
+
} catch {
|
|
77
|
+
try {
|
|
78
|
+
return join(realpathSync.native(dirname(absolute)), basename(absolute));
|
|
79
|
+
} catch {
|
|
80
|
+
return absolute;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function installCodexInterruptHook(
|
|
86
|
+
text: string,
|
|
87
|
+
configPath: string,
|
|
88
|
+
config: Pick<AppConfig, "runtimeCommand">,
|
|
89
|
+
): { text: string; installed: InstalledCodexInterruptHook } {
|
|
90
|
+
return installCodexInterruptHookCommand(text, configPath, codexInterruptHookCommand(config));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function installCodexInterruptHookCommand(
|
|
94
|
+
text: string,
|
|
95
|
+
configPath: string,
|
|
96
|
+
command: string,
|
|
97
|
+
): { text: string; installed: InstalledCodexInterruptHook } {
|
|
98
|
+
if (managedMarkerCount(text) !== 0 || text.includes(MANAGED_INTERRUPT_HOOK_END)) {
|
|
99
|
+
throw new Error("Codex config already contains a codex-chatgpt-web interrupt hook marker");
|
|
100
|
+
}
|
|
101
|
+
const groupIndex = interruptGroupCount(text);
|
|
102
|
+
const stateKey = `${canonicalConfigPath(configPath)}:interrupt:${groupIndex}:0`;
|
|
103
|
+
const trustedHash = codexInterruptHookHash(command);
|
|
104
|
+
const ending = lineEnding(text);
|
|
105
|
+
const core = [
|
|
106
|
+
MANAGED_INTERRUPT_HOOK_START,
|
|
107
|
+
"[[hooks.Interrupt]]",
|
|
108
|
+
"",
|
|
109
|
+
"[[hooks.Interrupt.hooks]]",
|
|
110
|
+
'type = "command"',
|
|
111
|
+
`command = ${JSON.stringify(command)}`,
|
|
112
|
+
"timeout = 3",
|
|
113
|
+
"",
|
|
114
|
+
`[hooks.state.${JSON.stringify(stateKey)}]`,
|
|
115
|
+
`trusted_hash = ${JSON.stringify(trustedHash)}`,
|
|
116
|
+
MANAGED_INTERRUPT_HOOK_END,
|
|
117
|
+
].join(ending);
|
|
118
|
+
const leading = text.length === 0
|
|
119
|
+
? ""
|
|
120
|
+
: text.endsWith(`${ending}${ending}`)
|
|
121
|
+
? ""
|
|
122
|
+
: text.endsWith(ending)
|
|
123
|
+
? ending
|
|
124
|
+
: `${ending}${ending}`;
|
|
125
|
+
const trailing = text.length > 0 && text.endsWith(ending) ? ending : "";
|
|
126
|
+
const fragment = `${leading}${core}${trailing}`;
|
|
127
|
+
return {
|
|
128
|
+
text: `${text}${fragment}`,
|
|
129
|
+
installed: { command, groupIndex, stateKey, trustedHash, fragment },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function verifyCodexInterruptHook(text: string, installed: InstalledCodexInterruptHook): void {
|
|
134
|
+
const first = text.indexOf(installed.fragment);
|
|
135
|
+
if (first < 0 || text.indexOf(installed.fragment, first + installed.fragment.length) >= 0) {
|
|
136
|
+
throw new Error("Codex interrupt lifecycle hook changed after setup; refusing to overwrite it");
|
|
137
|
+
}
|
|
138
|
+
if (interruptGroupCount(text.slice(0, first)) !== installed.groupIndex) {
|
|
139
|
+
throw new Error("Codex interrupt lifecycle hook order changed after setup; refusing to overwrite it");
|
|
140
|
+
}
|
|
141
|
+
if (managedMarkerCount(text) !== 1 || !text.includes(MANAGED_INTERRUPT_HOOK_END)) {
|
|
142
|
+
throw new Error("Codex interrupt lifecycle hook markers changed after setup; refusing to overwrite them");
|
|
143
|
+
}
|
|
144
|
+
if (codexInterruptHookHash(installed.command) !== installed.trustedHash) {
|
|
145
|
+
throw new Error("Codex interrupt lifecycle hook journal hash is invalid");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function restoreCodexInterruptHook(text: string, installed: InstalledCodexInterruptHook): string {
|
|
150
|
+
verifyCodexInterruptHook(text, installed);
|
|
151
|
+
return text.replace(installed.fragment, "");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function verifyCodexInterruptHookRestored(text: string): void {
|
|
155
|
+
if (managedMarkerCount(text) !== 0 || text.includes(MANAGED_INTERRUPT_HOOK_END)) {
|
|
156
|
+
throw new Error("Codex interrupt lifecycle hook is present while the bridge is disconnected");
|
|
157
|
+
}
|
|
158
|
+
}
|