@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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/assets/demo.gif +0 -0
  4. package/assets/hero-demo.png +0 -0
  5. package/assets/promo-dshmarket-official.png +0 -0
  6. package/cordis.patch.yml +4 -0
  7. package/lib/cli.js +239642 -0
  8. package/lib/plugin.js +195 -0
  9. package/package.json +88 -0
  10. package/screenshots.json +5 -0
  11. package/src/adapters/base.ts +16 -0
  12. package/src/adapters/chatgpt-web/adapter-error.ts +59 -0
  13. package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -0
  14. package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -0
  15. package/src/adapters/chatgpt-web/browser-worker.ts +4944 -0
  16. package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -0
  17. package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -0
  18. package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -0
  19. package/src/adapters/chatgpt-web/concurrency.ts +6 -0
  20. package/src/adapters/chatgpt-web/conversation-key.ts +58 -0
  21. package/src/adapters/chatgpt-web/environment.ts +669 -0
  22. package/src/adapters/chatgpt-web/index.ts +1544 -0
  23. package/src/adapters/chatgpt-web/input-tokens.ts +74 -0
  24. package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -0
  25. package/src/adapters/chatgpt-web/markdown.ts +418 -0
  26. package/src/adapters/chatgpt-web/mcp-main.ts +25 -0
  27. package/src/adapters/chatgpt-web/mcp-server.ts +933 -0
  28. package/src/adapters/chatgpt-web/model.ts +70 -0
  29. package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -0
  30. package/src/adapters/chatgpt-web/output-validation.ts +62 -0
  31. package/src/adapters/chatgpt-web/process-line-writer.ts +46 -0
  32. package/src/adapters/chatgpt-web/prompt.ts +702 -0
  33. package/src/adapters/chatgpt-web/retry-policy.ts +73 -0
  34. package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -0
  35. package/src/adapters/chatgpt-web/thread-environment.ts +238 -0
  36. package/src/adapters/chatgpt-web/tool-stream-parser.ts +601 -0
  37. package/src/adapters/chatgpt-web/turn-broker.ts +1481 -0
  38. package/src/adapters/chatgpt-web/turn-execution.ts +816 -0
  39. package/src/adapters/chatgpt-web/turn-progress.ts +292 -0
  40. package/src/adapters/chatgpt-web/usage.ts +121 -0
  41. package/src/adapters/image.ts +9 -0
  42. package/src/bridge.ts +1083 -0
  43. package/src/browser-login.ts +521 -0
  44. package/src/chatgpt-session.ts +240 -0
  45. package/src/chatgpt-web-models.ts +400 -0
  46. package/src/cli.ts +568 -0
  47. package/src/codex-integration-document.ts +824 -0
  48. package/src/codex-integration-journal.ts +212 -0
  49. package/src/codex-integration-route.ts +515 -0
  50. package/src/codex-integration-shared.ts +332 -0
  51. package/src/codex-integration.ts +529 -0
  52. package/src/codex-interrupt-hook.ts +158 -0
  53. package/src/config.ts +616 -0
  54. package/src/dev-chat/cli.ts +432 -0
  55. package/src/dev-chat/constants.ts +3 -0
  56. package/src/dev-chat/driver.ts +655 -0
  57. package/src/dev-chat/profile.ts +223 -0
  58. package/src/dev-chat/session.ts +287 -0
  59. package/src/dev-chat/transport.ts +54 -0
  60. package/src/doctor.ts +237 -0
  61. package/src/event-queue.ts +45 -0
  62. package/src/http-body.ts +30 -0
  63. package/src/launcher-browser-host.ts +695 -0
  64. package/src/lib/errors.ts +281 -0
  65. package/src/lib/token-estimate.ts +42 -0
  66. package/src/login-helper.cjs +140 -0
  67. package/src/model-catalog.ts +197 -0
  68. package/src/native-passthrough.ts +261 -0
  69. package/src/plugin.ts +191 -0
  70. package/src/process.ts +45 -0
  71. package/src/responses/compaction.ts +199 -0
  72. package/src/responses/parser.ts +633 -0
  73. package/src/responses/reasoning-envelope.ts +49 -0
  74. package/src/responses/schema.ts +172 -0
  75. package/src/responses/state.ts +230 -0
  76. package/src/server.ts +1111 -0
  77. package/src/service.ts +315 -0
  78. package/src/setup.ts +671 -0
  79. package/src/stall-timeout.ts +23 -0
  80. package/src/tunnel-service.ts +160 -0
  81. package/src/tunnel.ts +417 -0
  82. package/src/turndown-plugin-gfm.d.ts +5 -0
  83. package/src/types.ts +307 -0
  84. package/src/usage/totals.ts +12 -0
  85. package/src/version.ts +1 -0
package/src/cli.ts ADDED
@@ -0,0 +1,568 @@
1
+ #!/usr/bin/env bun
2
+ import { createInterface } from "node:readline/promises";
3
+ import { Writable } from "node:stream";
4
+ import { timingSafeEqual } from "node:crypto";
5
+ import { existsSync, rmSync } from "node:fs";
6
+ import { isAbsolute } from "node:path";
7
+ import { stdin, stdout } from "node:process";
8
+ import { captureSystemBrowserLoginToFile, checkBrowserEngine, loginToChatGpt } from "./browser-login";
9
+ import { CHATGPT_CONNECTOR_NAME, defaultConfig, getConfigDir, getConfigPath, loadConfig, loadConfigForSetup } from "./config";
10
+ import {
11
+ inspectLauncherBrowserHost,
12
+ inspectLauncherBrowserHostLiveness,
13
+ readLauncherBrowserHostDescriptor,
14
+ } from "./launcher-browser-host";
15
+ import {
16
+ activateCodexIntegration,
17
+ deactivateCodexIntegration,
18
+ inspectCodexIntegration,
19
+ readCodexSubagentProtocol,
20
+ setCodexSubagentProtocol,
21
+ uninstallCodexIntegration,
22
+ } from "./codex-integration";
23
+ import { formatDoctorReport, runDoctor } from "./doctor";
24
+ import { runChatGptMcpMain } from "./adapters/chatgpt-web/mcp-main";
25
+ import { runCommand } from "./process";
26
+ import { startServer } from "./server";
27
+ import { assertServiceIdle, cancelActiveTurns, getServiceStatus, installService, interruptActiveTurn, restartService, startService, stopService, uninstallService } from "./service";
28
+ import { existingFullSetupCredentials, preflightSetup, setup, type SetupOptions } from "./setup";
29
+ import { installRuntimeKeyBytes, managedRuntimeKeyPath, stopTunnel, tunnelStatus, waitForTunnelReady } from "./tunnel";
30
+ import { getTunnelServiceStatus, restartTunnelService, startTunnelService, stopTunnelService, uninstallTunnelService } from "./tunnel-service";
31
+ import { VERSION } from "./version";
32
+ import { runDevCommand } from "./dev-chat/cli";
33
+
34
+ const HELP = `dsh-chatgpt-web ${VERSION}
35
+
36
+ DeepSeek Harness bridge & Cordis plugin for free ChatGPT Web (GPT 5.6 Luna) pure chat and zero API fees.
37
+
38
+ Usage:
39
+ dsh-chatgpt-web setup [options]
40
+ dsh-chatgpt-web login
41
+ dsh-chatgpt-web doctor [--json]
42
+ dsh-chatgpt-web serve
43
+ dsh-chatgpt-web browser check
44
+ dsh-chatgpt-web uninstall --yes
45
+
46
+ Options:
47
+ --host HOST Host to bind (default: 127.0.0.1)
48
+ --port PORT Port to listen on (default: 17841)
49
+ --chrome PATH Path to Chrome/Chromium executable
50
+ --home PATH Override config storage path (default: ~/.dsh/storages/chatgpt-web)
51
+ --login Trigger interactive browser login
52
+ -h, --help Show this help message
53
+ -v, --version Show version
54
+ `;
55
+
56
+ function takeOption(args: string[], name: string): string | undefined {
57
+ const index = args.indexOf(name);
58
+ if (index < 0) return undefined;
59
+ const value = args[index + 1];
60
+ if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
61
+ args.splice(index, 2);
62
+ return value;
63
+ }
64
+
65
+ function takeFlag(args: string[], name: string): boolean {
66
+ const index = args.indexOf(name);
67
+ if (index < 0) return false;
68
+ args.splice(index, 1);
69
+ return true;
70
+ }
71
+
72
+ async function confirm(question: string): Promise<boolean> {
73
+ if (!stdin.isTTY || !stdout.isTTY) return false;
74
+ const reader = createInterface({ input: stdin, output: stdout });
75
+ try {
76
+ const answer = (await reader.question(`${question} [y/N] `)).trim().toLowerCase();
77
+ return answer === "y" || answer === "yes";
78
+ } finally {
79
+ reader.close();
80
+ }
81
+ }
82
+
83
+ async function prompt(question: string): Promise<string> {
84
+ if (!stdin.isTTY || !stdout.isTTY) return "";
85
+ const reader = createInterface({ input: stdin, output: stdout });
86
+ try { return (await reader.question(question)).trim(); }
87
+ finally { reader.close(); }
88
+ }
89
+
90
+ async function secretPrompt(question: string): Promise<string> {
91
+ if (!stdin.isTTY || !stdout.isTTY) return "";
92
+ stdout.write(question);
93
+ const muted = new Writable({ write(_chunk, _encoding, callback) { callback(); } });
94
+ const reader = createInterface({ input: stdin, output: muted, terminal: true });
95
+ try { return (await reader.question("")).trim(); }
96
+ finally {
97
+ reader.close();
98
+ stdout.write("\n");
99
+ }
100
+ }
101
+
102
+ function assertNoArgs(args: string[]): void {
103
+ if (args.length > 0) throw new Error(`Unknown arguments: ${args.join(" ")}`);
104
+ }
105
+
106
+ function authorizeLauncherControl(operation: string): void {
107
+ const descriptorPath = process.env.DSH_CHATGPT_FREE_BROWSER_HOST_DESCRIPTOR?.trim();
108
+ const supplied = process.env.DSH_CHATGPT_FREE_LAUNCHER_CONTROL_TOKEN?.trim();
109
+ delete process.env.DSH_CHATGPT_FREE_LAUNCHER_CONTROL_TOKEN;
110
+ if (!descriptorPath || !supplied) {
111
+ throw new Error(`Launcher-controlled ${operation} requires a live launcher authorization`);
112
+ }
113
+ const descriptor = readLauncherBrowserHostDescriptor(descriptorPath);
114
+ const expectedBytes = Buffer.from(descriptor.control.token);
115
+ const suppliedBytes = Buffer.from(supplied);
116
+ if (expectedBytes.length !== suppliedBytes.length || !timingSafeEqual(expectedBytes, suppliedBytes)) {
117
+ throw new Error(`Launcher-controlled ${operation} authorization is invalid`);
118
+ }
119
+ }
120
+
121
+ function launcherLoginContinuation(): { promise: Promise<void>; close: () => void } {
122
+ const maxBytes = 1_024;
123
+ let buffered = "";
124
+ let bytes = 0;
125
+ let settled = false;
126
+ let resolveContinuation!: () => void;
127
+ let rejectContinuation!: (error: Error) => void;
128
+ const promise = new Promise<void>((resolve, reject) => {
129
+ resolveContinuation = resolve;
130
+ rejectContinuation = reject;
131
+ });
132
+ const cleanup = () => {
133
+ stdin.off("data", onData);
134
+ stdin.off("end", onEnd);
135
+ stdin.pause();
136
+ };
137
+ const fail = (message: string) => {
138
+ if (settled) return;
139
+ settled = true;
140
+ cleanup();
141
+ rejectContinuation(new Error(message));
142
+ };
143
+ const onData = (chunk: Buffer | string) => {
144
+ if (settled) return;
145
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
146
+ bytes += data.length;
147
+ if (bytes > maxBytes) {
148
+ fail("Launcher passkey control message is too large");
149
+ return;
150
+ }
151
+ buffered += data.toString("utf8");
152
+ const newline = buffered.indexOf("\n");
153
+ if (newline < 0) return;
154
+ const line = buffered.slice(0, newline);
155
+ if (buffered.slice(newline + 1).trim()) {
156
+ fail("Launcher passkey control sent unexpected trailing data");
157
+ return;
158
+ }
159
+ let message: unknown;
160
+ try {
161
+ message = JSON.parse(line);
162
+ } catch {
163
+ fail("Launcher passkey control sent invalid JSON");
164
+ return;
165
+ }
166
+ if (!message || typeof message !== "object"
167
+ || (message as { version?: unknown }).version !== 1
168
+ || (message as { type?: unknown }).type !== "passkey-login-continue") {
169
+ fail("Launcher passkey control sent an invalid continuation message");
170
+ return;
171
+ }
172
+ settled = true;
173
+ cleanup();
174
+ resolveContinuation();
175
+ };
176
+ const onEnd = () => fail("Launcher closed the passkey control channel before Continue");
177
+ stdin.on("data", onData);
178
+ stdin.once("end", onEnd);
179
+ stdin.resume();
180
+ return {
181
+ promise,
182
+ close: () => {
183
+ if (settled) return;
184
+ settled = true;
185
+ cleanup();
186
+ },
187
+ };
188
+ }
189
+
190
+ async function loginCommand(args: string[]): Promise<void> {
191
+ const launcherControl = takeFlag(args, "--launcher-control");
192
+ if (!launcherControl) {
193
+ assertNoArgs(args);
194
+ const config = loadConfig();
195
+ if (config.browserHost === "launcher") {
196
+ throw new Error("ChatGPT login is owned by the launcher; open Codex Web GPT and use its Sign in step");
197
+ }
198
+ const result = await loginToChatGpt(config);
199
+ stdout.write(`ChatGPT login stored at ${result.storageStatePath}\n`);
200
+ return;
201
+ }
202
+
203
+ const chromeExecutablePath = takeOption(args, "--chrome");
204
+ const storageStatePath = takeOption(args, "--storage-state");
205
+ assertNoArgs(args);
206
+ authorizeLauncherControl("passkey login");
207
+ if (process.platform !== "darwin") throw new Error("Passkey sign-in is currently supported only on macOS");
208
+ if (!chromeExecutablePath || !isAbsolute(chromeExecutablePath)) {
209
+ throw new Error("Launcher passkey sign-in requires --chrome with an absolute path");
210
+ }
211
+ if (!storageStatePath || !isAbsolute(storageStatePath)) {
212
+ throw new Error("Launcher passkey sign-in requires --storage-state with an absolute path");
213
+ }
214
+ const continuation = launcherLoginContinuation();
215
+ try {
216
+ await captureSystemBrowserLoginToFile({
217
+ ...defaultConfig(),
218
+ chromeExecutablePath,
219
+ storageStatePath,
220
+ }, { continuation: continuation.promise });
221
+ } finally {
222
+ continuation.close();
223
+ }
224
+ stdout.write("Passkey session captured for Launcher verification.\n");
225
+ }
226
+
227
+ async function setupCommand(args: string[]): Promise<void> {
228
+ const preflightOnly = takeFlag(args, "--preflight-only");
229
+ const browserOnly = takeFlag(args, "--browser-only");
230
+ const full = takeFlag(args, "--full");
231
+ if (browserOnly && full) throw new Error("Choose at most one setup mode: --browser-only or --full");
232
+ const portRaw = takeOption(args, "--port");
233
+ let acknowledged = takeFlag(args, "--acknowledge-unofficial");
234
+ const options: SetupOptions = {
235
+ mode: full ? "full" : "browser-only",
236
+ ...(portRaw ? { port: Number(portRaw) } : {}),
237
+ };
238
+ const automaticBrowserInteraction = takeFlag(args, "--automatic-browser-interaction");
239
+ const manualBrowserInteraction = takeFlag(args, "--zero-risk-browser-interaction");
240
+ if (automaticBrowserInteraction && manualBrowserInteraction) {
241
+ throw new Error(
242
+ "Choose at most one browser interaction mode: --automatic-browser-interaction or --zero-risk-browser-interaction",
243
+ );
244
+ }
245
+ if (automaticBrowserInteraction || manualBrowserInteraction) {
246
+ options.browserInteractionMode = manualBrowserInteraction ? "manual" : "automatic";
247
+ }
248
+ const subagentProtocol = takeOption(args, "--subagent-protocol");
249
+ if (subagentProtocol !== undefined) {
250
+ if (subagentProtocol !== "compatibility-v1" && subagentProtocol !== "native") {
251
+ throw new Error("--subagent-protocol must be compatibility-v1 or native");
252
+ }
253
+ options.subagentProtocol = subagentProtocol;
254
+ }
255
+ const appName = takeOption(args, "--app-name");
256
+ const tunnelId = takeOption(args, "--tunnel-id");
257
+ const runtimeKeyFile = takeOption(args, "--runtime-key-file");
258
+ const chrome = takeOption(args, "--chrome");
259
+ const browserHostDescriptorPath = takeOption(args, "--browser-host-descriptor");
260
+ if (chrome) options.chromeExecutablePath = chrome;
261
+ if (browserHostDescriptorPath) options.browserHostDescriptorPath = browserHostDescriptorPath;
262
+ options.refreshAccountCapabilities = takeFlag(args, "--refresh-account-capabilities");
263
+ if (appName) options.appName = appName;
264
+ if (tunnelId) options.tunnelId = tunnelId;
265
+ if (runtimeKeyFile) options.runtimeKeyFile = runtimeKeyFile;
266
+ options.forceLogin = takeFlag(args, "--login");
267
+ options.autoApproveToolCalls = takeFlag(args, "--auto-approve-tool-calls");
268
+ const biggerContext = takeFlag(args, "--bigger-context");
269
+ const standardContext = takeFlag(args, "--standard-context");
270
+ if (biggerContext && standardContext) {
271
+ throw new Error("Choose at most one context mode: --bigger-context or --standard-context");
272
+ }
273
+ if (biggerContext || standardContext) options.experimentalBiggerContext = biggerContext;
274
+ const zeroRiskPro = takeFlag(args, "--zero-risk-pro");
275
+ const zeroRiskDefault = takeFlag(args, "--zero-risk-default");
276
+ if (zeroRiskPro && zeroRiskDefault) {
277
+ throw new Error("Choose at most one Zero Risk model profile: --zero-risk-pro or --zero-risk-default");
278
+ }
279
+ if (zeroRiskPro || zeroRiskDefault) options.zeroRiskProEnabled = zeroRiskPro;
280
+ options.replaceCodexRoute = takeFlag(args, "--replace-codex-route");
281
+ options.restartService = takeFlag(args, "--restart-service");
282
+ assertNoArgs(args);
283
+
284
+ if (!acknowledged) {
285
+ stdout.write(
286
+ "This is independent, unofficial software. It automates your ChatGPT web session, can break when the UI changes, "
287
+ + "and must not be used to evade usage limits or access controls.\n",
288
+ );
289
+ acknowledged = await confirm("Continue and store this acknowledgement?");
290
+ }
291
+ if (!acknowledged) throw new Error("Setup cancelled: acknowledgement was not provided");
292
+ options.acknowledgedUnofficial = true;
293
+
294
+ if (preflightOnly) {
295
+ preflightSetup(options);
296
+ stdout.write("Setup preflight complete.\n");
297
+ return;
298
+ }
299
+
300
+ const existing = existsSync(getConfigPath()) ? loadConfigForSetup() : undefined;
301
+ const interactionMode = options.browserInteractionMode ?? existing?.browserInteractionMode ?? "automatic";
302
+ const reusableCredentials = existingFullSetupCredentials(existing, interactionMode);
303
+ const needsTunnelId = !options.tunnelId && !reusableCredentials.tunnelId;
304
+ const needsRuntimeKey = !options.runtimeKeyFile
305
+ && !reusableCredentials.runtimeKey
306
+ && !existsSync(managedRuntimeKeyPath(interactionMode));
307
+
308
+ if (full && (needsTunnelId || needsRuntimeKey) && stdin.isTTY) {
309
+ stdout.write("Full mode needs an OpenAI tunnel and a runtime key with Tunnels Read + Use.\n");
310
+ stdout.write("Tunnels: https://platform.openai.com/settings/organization/tunnels\n");
311
+ stdout.write("Runtime keys: https://platform.openai.com/settings/organization/api-keys\n");
312
+ if (needsTunnelId) options.tunnelId = await prompt("Tunnel id: ");
313
+ if (needsRuntimeKey) {
314
+ options.runtimeKeyValue = await secretPrompt("Runtime key (hidden): ");
315
+ }
316
+ }
317
+
318
+ const result = await setup(options);
319
+ stdout.write(`\n✓ DSH ChatGPT Free setup complete (${result.mode})!\n`);
320
+ stdout.write(`Config: ${result.configPath}\n`);
321
+ stdout.write("\nTo use with DeepSeek Harness, ensure your ~/.dsh/settings.yaml includes:\n\n");
322
+ stdout.write(" providers:\n chatgpt-web:\n displayName: \"ChatGPT Web (Free)\"\n api: openai-responses\n baseURL: http://127.0.0.1:17841/v1\n headers:\n Authorization: \"Bearer chatgpt-web-free\"\n models:\n - id: chatgpt-web/luna\n name: \"ChatGPT Web — Luna (Free)\"\n contextWindow: 1050000\n maxTokens: 32768\n\n");
323
+ }
324
+
325
+ async function doctorCommand(args: string[]): Promise<void> {
326
+ const json = takeFlag(args, "--json");
327
+ assertNoArgs(args);
328
+ const report = await runDoctor();
329
+ stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : formatDoctorReport(report));
330
+ if (!report.ok) process.exitCode = 1;
331
+ }
332
+
333
+ async function routeCommand(args: string[]): Promise<void> {
334
+ const action = args.shift() ?? "status";
335
+ assertNoArgs(args);
336
+ const result = action === "status"
337
+ ? (() => {
338
+ const status = inspectCodexIntegration();
339
+ return {
340
+ installed: status.installed,
341
+ active: status.active,
342
+ ...(status.routeUrl ? { routeUrl: status.routeUrl } : {}),
343
+ errors: status.errors,
344
+ };
345
+ })()
346
+ : action === "connect"
347
+ ? activateCodexIntegration()
348
+ : action === "disconnect"
349
+ ? deactivateCodexIntegration()
350
+ : undefined;
351
+ if (!result) throw new Error(`Unknown route action: ${action}`);
352
+ stdout.write(`${JSON.stringify(result, null, 2)}\n`);
353
+ }
354
+
355
+ async function subagentsCommand(args: string[]): Promise<void> {
356
+ const action = args.shift() ?? "status";
357
+ assertNoArgs(args);
358
+ const config = loadConfig();
359
+ if (config.purpose === "dev-harness") {
360
+ throw new Error("The isolated DEV harness has no Codex subagent protocol to configure");
361
+ }
362
+ if (action === "status") {
363
+ const integration = inspectCodexIntegration();
364
+ stdout.write(`${JSON.stringify({
365
+ protocol: readCodexSubagentProtocol(config.subagentProtocol),
366
+ installed: integration.installed,
367
+ active: integration.active,
368
+ }, null, 2)}\n`);
369
+ return;
370
+ }
371
+ if (action !== "compatibility-v1" && action !== "native") {
372
+ throw new Error("Subagent protocol must be one of: status, compatibility-v1, native");
373
+ }
374
+ const journal = setCodexSubagentProtocol(config, action);
375
+ stdout.write(`${JSON.stringify({
376
+ protocol: journal.installed.subagent_protocol,
377
+ codexRestartRequired: true,
378
+ launcherRestartRequired: true,
379
+ }, null, 2)}\n`);
380
+ }
381
+
382
+ async function serviceCommand(args: string[]): Promise<void> {
383
+ const action = args.shift() ?? "status";
384
+ assertNoArgs(args);
385
+ const config = action === "status" ? undefined : loadConfig();
386
+ if (action === "cancel-turns") {
387
+ stdout.write(`${JSON.stringify(await cancelActiveTurns(config!), null, 2)}\n`);
388
+ return;
389
+ }
390
+ const status = action === "status" ? getServiceStatus()
391
+ : action === "install" ? installService(config!)
392
+ : action === "start" ? startService()
393
+ : action === "restart" ? await restartService(config!)
394
+ : action === "stop" ? await stopService(config!)
395
+ : undefined;
396
+ if (!status) throw new Error(`Unknown service action: ${action}`);
397
+ stdout.write(`${JSON.stringify(status, null, 2)}\n`);
398
+ }
399
+
400
+ async function interruptHookCommand(args: string[]): Promise<void> {
401
+ assertNoArgs(args);
402
+ const chunks: Buffer[] = [];
403
+ let bytes = 0;
404
+ for await (const chunk of stdin) {
405
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
406
+ bytes += buffer.byteLength;
407
+ if (bytes > 32 * 1024) throw new Error("Codex Interrupt hook payload is too large");
408
+ chunks.push(buffer);
409
+ }
410
+ let payload: { hook_event_name?: unknown; session_id?: unknown; turn_id?: unknown };
411
+ try {
412
+ payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
413
+ } catch {
414
+ throw new Error("Codex Interrupt hook payload is not valid JSON");
415
+ }
416
+ const threadId = typeof payload.session_id === "string" ? payload.session_id.trim() : "";
417
+ const turnId = typeof payload.turn_id === "string" ? payload.turn_id.trim() : "";
418
+ if (payload.hook_event_name !== "Interrupt"
419
+ || !/^[A-Za-z0-9_-]{6,128}$/.test(threadId)
420
+ || !/^[A-Za-z0-9_-]{6,128}$/.test(turnId)) {
421
+ throw new Error("Codex Interrupt hook payload has no valid session_id or turn_id");
422
+ }
423
+ await interruptActiveTurn(loadConfig(), { threadId, turnId });
424
+ }
425
+
426
+ async function tunnelCommand(args: string[]): Promise<void> {
427
+ const action = args.shift() ?? "status";
428
+ assertNoArgs(args);
429
+ if (action === "key-import") {
430
+ const key = await secretPrompt("Runtime key (hidden): ");
431
+ if (!key) throw new Error("A non-empty runtime key is required");
432
+ installRuntimeKeyBytes(key);
433
+ stdout.write(`Runtime key stored privately at ${managedRuntimeKeyPath()}\n`);
434
+ return;
435
+ }
436
+ const config = loadConfig();
437
+ if (action === "start") startTunnelService();
438
+ else if (action === "restart") {
439
+ await assertServiceIdle(config);
440
+ await restartTunnelService();
441
+ }
442
+ else if (action === "stop") {
443
+ await assertServiceIdle(config);
444
+ await stopTunnelService();
445
+ stopTunnel(config);
446
+ }
447
+ else if (action !== "status") throw new Error(`Unknown tunnel action: ${action}`);
448
+ const status = action === "start" || action === "restart"
449
+ ? await waitForTunnelReady(config)
450
+ : tunnelStatus(config);
451
+ const service = getTunnelServiceStatus();
452
+ stdout.write(`${JSON.stringify({ service, runtime: status }, null, 2)}\n`);
453
+ if (action !== "stop" && (!service.running || !status.ok)) process.exitCode = 1;
454
+ }
455
+
456
+ async function openCommand(args: string[]): Promise<void> {
457
+ const target = args.shift();
458
+ assertNoArgs(args);
459
+ const urls: Record<string, string> = {
460
+ tunnels: "https://platform.openai.com/settings/organization/tunnels",
461
+ "runtime-keys": "https://platform.openai.com/settings/organization/api-keys",
462
+ connectors: "https://chatgpt.com/#settings/Plugins",
463
+ };
464
+ const url = target ? urls[target] : undefined;
465
+ if (!url) throw new Error("Choose one of: tunnels, runtime-keys, connectors");
466
+ if (process.platform === "darwin") {
467
+ const result = runCommand("open", [url]);
468
+ if (result.status !== 0) throw new Error(result.stderr.trim() || `Could not open ${url}`);
469
+ } else {
470
+ stdout.write(`${url}\n`);
471
+ }
472
+ }
473
+
474
+ async function uninstallCommand(args: string[]): Promise<void> {
475
+ const yes = takeFlag(args, "--yes");
476
+ const keepData = takeFlag(args, "--keep-data");
477
+ const launcherControl = takeFlag(args, "--launcher-control");
478
+ assertNoArgs(args);
479
+ if (launcherControl) authorizeLauncherControl("uninstall");
480
+ if (!yes && !await confirm("Restore Codex config, stop services, and remove this installation?")) {
481
+ throw new Error("Uninstall cancelled");
482
+ }
483
+ const config = existsSync(getConfigPath()) ? loadConfig() : undefined;
484
+ if (config?.browserHost === "launcher" && !launcherControl) {
485
+ throw new Error(
486
+ "Launcher-owned integration must be removed from Codex Web GPT Settings so the active runtime can be drained safely.",
487
+ );
488
+ }
489
+ if (!config && process.platform === "darwin" && getServiceStatus().installed) {
490
+ throw new Error("Service exists but configuration is missing; refusing an unverifiable uninstall");
491
+ }
492
+ const launcherRuntimeStopped = config?.browserHost === "launcher" && launcherControl;
493
+ if (config && process.platform === "darwin" && !launcherRuntimeStopped) await assertServiceIdle(config);
494
+ if (config?.mode === "full" && !launcherRuntimeStopped) {
495
+ if (process.platform === "darwin") await uninstallTunnelService();
496
+ stopTunnel(config);
497
+ }
498
+ if (config && process.platform === "darwin" && !launcherRuntimeStopped) await uninstallService(config);
499
+ uninstallCodexIntegration();
500
+ if (!keepData) rmSync(getConfigDir(), { recursive: true, force: true });
501
+ stdout.write(keepData ? "Uninstalled; private application data was preserved.\n" : "Uninstalled and removed private application data.\n");
502
+ }
503
+
504
+ async function main(): Promise<void> {
505
+ const args = process.argv.slice(2);
506
+ const home = takeOption(args, "--home");
507
+ if (home) {
508
+ process.env.DSH_CHATGPT_FREE_HOME = home;
509
+ }
510
+ if (takeFlag(args, "--help") || takeFlag(args, "-h")) {
511
+ stdout.write(HELP);
512
+ return;
513
+ }
514
+ if (takeFlag(args, "--version") || takeFlag(args, "-v")) {
515
+ stdout.write(`${VERSION}\n`);
516
+ return;
517
+ }
518
+ const command = args.shift() ?? "help";
519
+ if (command === "dev" && home) {
520
+ throw new Error("--home does not apply to DEV mode; use DSH_CHATGPT_FREE_DEV_HOME for an explicit isolated DEV profile");
521
+ }
522
+ if (command === "help") stdout.write(HELP);
523
+ else if (command === "setup") await setupCommand(args);
524
+ else if (command === "login") await loginCommand(args);
525
+ else if (command === "doctor" || command === "status") await doctorCommand(args);
526
+ else if (command === "route") await routeCommand(args);
527
+ else if (command === "subagents") await subagentsCommand(args);
528
+ else if (command === "browser") {
529
+ const action = args.shift();
530
+ assertNoArgs(args);
531
+ if (action !== "check") throw new Error("Browser command must be: browser check");
532
+ const config = loadConfig();
533
+ if (config.browserHost === "launcher") {
534
+ if (config.browserInteractionMode === "manual") {
535
+ await inspectLauncherBrowserHostLiveness(config.browserHostDescriptorPath!);
536
+ stdout.write("The launcher browser is reachable; ChatGPT DOM inspection is intentionally disabled in Zero Risk.\n");
537
+ } else {
538
+ await inspectLauncherBrowserHost(config.browserHostDescriptorPath!);
539
+ stdout.write("Playwright can reach the authenticated ChatGPT surface embedded in the launcher.\n");
540
+ }
541
+ } else {
542
+ await checkBrowserEngine(config);
543
+ stdout.write("Playwright can launch the configured Chrome executable.\n");
544
+ }
545
+ } else if (command === "serve") {
546
+ assertNoArgs(args);
547
+ const config = loadConfig();
548
+ const server = startServer(config);
549
+ stdout.write(`dsh-chatgpt-web ${VERSION} listening on http://${config.host}:${server.port}/v1 (${config.mode})\n`);
550
+ await new Promise<void>(() => {});
551
+ } else if (command === "dev") await runDevCommand(args);
552
+ else if (command === "mcp") await runChatGptMcpMain(args);
553
+ else if (command === "service") await serviceCommand(args);
554
+ else if (command === "hook") {
555
+ const action = args.shift();
556
+ if (action !== "interrupt") throw new Error("Hook command must be: hook interrupt");
557
+ await interruptHookCommand(args);
558
+ }
559
+ else if (command === "tunnel") await tunnelCommand(args);
560
+ else if (command === "open") await openCommand(args);
561
+ else if (command === "uninstall") await uninstallCommand(args);
562
+ else throw new Error(`Unknown command: ${command}\n\n${HELP}`);
563
+ }
564
+
565
+ main().catch(error => {
566
+ process.stderr.write(`dsh-chatgpt-web: ${error instanceof Error ? error.message : String(error)}\n`);
567
+ process.exitCode = 1;
568
+ });