@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/doctor.ts ADDED
@@ -0,0 +1,237 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import type { AppConfig } from "./config";
3
+ import { getConfigDir, getConfigPath, loadConfig } from "./config";
4
+ import { join } from "node:path";
5
+ import { inspectCodexIntegration } from "./codex-integration";
6
+ import { browserLoginStateExists, loginVerificationMarkerPath } from "./browser-login";
7
+ import { getServiceStatus } from "./service";
8
+ import { tunnelStatus } from "./tunnel";
9
+ import { getTunnelServiceStatus } from "./tunnel-service";
10
+ import {
11
+ inspectLauncherBrowserHost,
12
+ inspectLauncherBrowserHostLiveness,
13
+ readLauncherBrowserHostDescriptor,
14
+ } from "./launcher-browser-host";
15
+ import { processRunning } from "./process";
16
+
17
+ export type CheckStatus = "ok" | "warning" | "error";
18
+
19
+ export interface DoctorCheck {
20
+ id: string;
21
+ status: CheckStatus;
22
+ message: string;
23
+ detail?: string;
24
+ }
25
+
26
+ export interface DoctorReport {
27
+ ok: boolean;
28
+ mode?: AppConfig["mode"];
29
+ checks: DoctorCheck[];
30
+ }
31
+
32
+ function secureFile(path: string): boolean {
33
+ if (process.platform === "win32") return true;
34
+ return (statSync(path).mode & 0o077) === 0;
35
+ }
36
+
37
+ function launcherOwnershipError(config: AppConfig, health: Record<string, unknown>): string | undefined {
38
+ if (config.browserHost !== "launcher") return undefined;
39
+ const path = join(getConfigDir(), "runtime", "launcher-supervisor.json");
40
+ if (!existsSync(path)) return `Launcher runtime ownership marker is missing: ${path}`;
41
+ let state: Record<string, unknown>;
42
+ try {
43
+ state = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
44
+ } catch (error) {
45
+ return `Launcher runtime ownership marker is invalid: ${error instanceof Error ? error.message : String(error)}`;
46
+ }
47
+ if (state.version !== 1
48
+ || !Number.isInteger(state.ownerPid)
49
+ || (state.ownerPid as number) < 1
50
+ || !Number.isInteger(state.daemonPid)
51
+ || (state.daemonPid as number) < 1
52
+ || state.status !== "ready") {
53
+ return "Launcher runtime ownership marker is incomplete or not ready";
54
+ }
55
+ if (!processRunning(state.ownerPid)) {
56
+ return `Launcher owner process is not running (pid ${String(state.ownerPid)})`;
57
+ }
58
+ if (health.pid !== state.daemonPid) {
59
+ return `Responses proxy pid ${String(health.pid)} does not match launcher-owned pid ${String(state.daemonPid)}`;
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ async function proxyCheck(config: AppConfig): Promise<DoctorCheck> {
65
+ const controller = new AbortController();
66
+ const timeout = setTimeout(() => controller.abort(), 2_000);
67
+ try {
68
+ const response = await fetch(`http://${config.host}:${config.port}/healthz`, { signal: controller.signal });
69
+ if (!response.ok) return { id: "proxy", status: "error", message: `Responses proxy returned HTTP ${response.status}` };
70
+ const body = await response.json() as Record<string, unknown>;
71
+ if ((body.service !== "dsh-chatgpt-web" && body.service !== "codex-chatgpt-web") || body.status !== "ok") {
72
+ return { id: "proxy", status: "error", message: "The configured port belongs to another service" };
73
+ }
74
+ if (body.mode !== config.mode) {
75
+ return { id: "proxy", status: "error", message: `Daemon is running in ${String(body.mode)} mode; config requires ${config.mode}` };
76
+ }
77
+ if (body.version !== config.releaseVersion) {
78
+ return { id: "proxy", status: "error", message: `Daemon version is ${String(body.version)}; config requires ${config.releaseVersion}` };
79
+ }
80
+ if (body.accepting_turns !== true) {
81
+ return {
82
+ id: "proxy",
83
+ status: "error",
84
+ message: "Responses proxy is still drained and is not accepting Codex turns",
85
+ };
86
+ }
87
+ const ownershipError = launcherOwnershipError(config, body);
88
+ if (ownershipError) {
89
+ return { id: "proxy", status: "error", message: "Responses proxy ownership could not be verified", detail: ownershipError };
90
+ }
91
+ return { id: "proxy", status: "ok", message: `Responses proxy is healthy on 127.0.0.1:${config.port}` };
92
+ } catch (error) {
93
+ const detail = error instanceof Error ? error.message : String(error);
94
+ return { id: "proxy", status: "error", message: "Responses proxy is not reachable", detail };
95
+ } finally {
96
+ clearTimeout(timeout);
97
+ }
98
+ }
99
+
100
+ export async function runDoctor(): Promise<DoctorReport> {
101
+ const checks: DoctorCheck[] = [];
102
+ let config: AppConfig;
103
+ try {
104
+ config = loadConfig();
105
+ checks.push({ id: "config", status: "ok", message: `Configuration is valid (${getConfigPath()})` });
106
+ } catch (error) {
107
+ checks.push({ id: "config", status: "error", message: "Configuration is invalid", detail: error instanceof Error ? error.message : String(error) });
108
+ return { ok: false, checks };
109
+ }
110
+
111
+ if (config.browserHost === "launcher") {
112
+ try {
113
+ const descriptor = config.browserInteractionMode === "manual"
114
+ ? await inspectLauncherBrowserHostLiveness(config.browserHostDescriptorPath!, { timeoutMs: 5_000 })
115
+ : readLauncherBrowserHostDescriptor(config.browserHostDescriptorPath!);
116
+ if (config.browserInteractionMode === "automatic") {
117
+ await inspectLauncherBrowserHost(config.browserHostDescriptorPath!, { timeoutMs: 30_000 });
118
+ }
119
+ checks.push({
120
+ id: "browser-host",
121
+ status: "ok",
122
+ message: config.browserInteractionMode === "manual"
123
+ ? `Embedded launcher browser is reachable for Zero Risk (pid ${descriptor.pid})`
124
+ : `Embedded launcher browser is authenticated and reachable (pid ${descriptor.pid})`,
125
+ });
126
+ } catch (error) {
127
+ checks.push({
128
+ id: "browser-host",
129
+ status: "error",
130
+ message: "Embedded launcher browser is unavailable",
131
+ detail: error instanceof Error ? error.message : String(error),
132
+ });
133
+ }
134
+ } else {
135
+ if (!existsSync(config.chromeExecutablePath)) {
136
+ checks.push({ id: "chrome", status: "error", message: `Chrome executable is missing: ${config.chromeExecutablePath}` });
137
+ } else {
138
+ checks.push({ id: "chrome", status: "ok", message: `Chrome executable found: ${config.chromeExecutablePath}` });
139
+ }
140
+ if (!browserLoginStateExists(config)) {
141
+ checks.push({ id: "login", status: "error", message: "ChatGPT login state is missing or unverified; run `dsh-chatgpt-web login`" });
142
+ } else if (!secureFile(config.storageStatePath)) {
143
+ checks.push({ id: "login", status: "error", message: `ChatGPT login state is readable by other users: ${config.storageStatePath}` });
144
+ } else if (!secureFile(loginVerificationMarkerPath(config.storageStatePath))) {
145
+ checks.push({ id: "login", status: "error", message: "ChatGPT login verification marker is readable by other users" });
146
+ } else {
147
+ checks.push({ id: "login", status: "ok", message: "ChatGPT login state has authenticated browser evidence" });
148
+ }
149
+ }
150
+
151
+ const codex = inspectCodexIntegration();
152
+ if (!codex.installed) {
153
+ checks.push({ id: "codex", status: "error", message: "Codex model route is not installed" });
154
+ } else if (codex.errors.length > 0) {
155
+ checks.push({ id: "codex", status: "error", message: "Codex integration is inconsistent", detail: codex.errors.join("; ") });
156
+ } else {
157
+ checks.push({ id: "codex", status: "ok", message: "Codex native model route is installed" });
158
+ }
159
+
160
+ const service = getServiceStatus();
161
+ if (config.browserHost === "launcher") {
162
+ checks.push(service.installed || service.loaded
163
+ ? {
164
+ id: "service",
165
+ status: "warning",
166
+ message: "A legacy OS background service still exists; rerun launcher setup to migrate ownership",
167
+ detail: JSON.stringify(service),
168
+ }
169
+ : { id: "service", status: "ok", message: "Launcher owns the background runtime" });
170
+ } else if (!service.supported) {
171
+ checks.push({ id: "service", status: "warning", message: "Managed service is unavailable on this OS; keep `serve` running manually" });
172
+ } else if (!service.installed || !service.loaded) {
173
+ checks.push({ id: "service", status: "error", message: "macOS background service is not installed and loaded" });
174
+ } else {
175
+ checks.push({ id: "service", status: "ok", message: "macOS background service is loaded" });
176
+ }
177
+ checks.push(await proxyCheck(config));
178
+
179
+ if (config.mode === "full") {
180
+ const settings = config.tunnel!;
181
+ if (!existsSync(settings.binaryPath)) {
182
+ checks.push({ id: "tunnel-binary", status: "error", message: `tunnel-client is missing: ${settings.binaryPath}` });
183
+ } else {
184
+ checks.push({ id: "tunnel-binary", status: "ok", message: "Pinned openai/tunnel-client binary is installed" });
185
+ }
186
+ if (!existsSync(settings.runtimeKeyFile)) {
187
+ checks.push({ id: "tunnel-key", status: "error", message: "Tunnel runtime key file is missing" });
188
+ } else if (!secureFile(settings.runtimeKeyFile)) {
189
+ checks.push({ id: "tunnel-key", status: "error", message: "Tunnel runtime key file has unsafe permissions" });
190
+ } else {
191
+ checks.push({ id: "tunnel-key", status: "ok", message: "Tunnel runtime key is stored privately" });
192
+ }
193
+ const tunnelService = getTunnelServiceStatus();
194
+ if (config.browserHost === "launcher") {
195
+ checks.push(tunnelService.installed || tunnelService.loaded
196
+ ? {
197
+ id: "tunnel-service",
198
+ status: "warning",
199
+ message: "A legacy OS tunnel service still exists; rerun launcher MCP setup to migrate ownership",
200
+ detail: JSON.stringify(tunnelService),
201
+ }
202
+ : { id: "tunnel-service", status: "ok", message: "Launcher owns the tunnel runtime" });
203
+ } else {
204
+ checks.push(tunnelService.installed && tunnelService.loaded && tunnelService.running
205
+ ? { id: "tunnel-service", status: "ok", message: "macOS tunnel service is installed, loaded, and running" }
206
+ : { id: "tunnel-service", status: "error", message: "macOS tunnel service is not fully running", detail: JSON.stringify(tunnelService) });
207
+ }
208
+ const runtime = tunnelStatus(config);
209
+ checks.push(runtime.ok
210
+ ? { id: "tunnel-runtime", status: "ok", message: "Tunnel runtime reports healthy and ready" }
211
+ : { id: "tunnel-runtime", status: "error", message: "Tunnel runtime is not ready", detail: runtime.detail });
212
+ checks.push({
213
+ id: "connector",
214
+ status: "warning",
215
+ message: `Local checks cannot prove that ChatGPT connector ${JSON.stringify(config.appName)} is attached to this tunnel`,
216
+ detail: "Verify it once at https://chatgpt.com/#settings/Plugins while the tunnel is ready.",
217
+ });
218
+ } else {
219
+ checks.push({ id: "tools", status: "warning", message: "Browser-only mode intentionally has no local tools or MCP tunnel" });
220
+ }
221
+
222
+ return {
223
+ ok: !checks.some(check => check.status === "error"),
224
+ mode: config.mode,
225
+ checks,
226
+ };
227
+ }
228
+
229
+ export function formatDoctorReport(report: DoctorReport): string {
230
+ const icon: Record<CheckStatus, string> = { ok: "✓", warning: "!", error: "✗" };
231
+ const lines = report.checks.flatMap(check => [
232
+ `${icon[check.status]} ${check.message}`,
233
+ ...(check.detail ? [` ${check.detail}`] : []),
234
+ ]);
235
+ lines.push(report.ok ? "Doctor result: ready" : "Doctor result: not ready");
236
+ return `${lines.join("\n")}\n`;
237
+ }
@@ -0,0 +1,45 @@
1
+ export class AsyncEventQueue<T> implements AsyncIterable<T> {
2
+ private readonly buffered: T[] = [];
3
+ private readonly waiters: Array<(result: IteratorResult<T>) => void> = [];
4
+ private closed = false;
5
+
6
+ constructor(private readonly maxBuffered = 10_000) {}
7
+
8
+ push(value: T): void {
9
+ if (this.closed) return;
10
+ const waiter = this.waiters.shift();
11
+ if (waiter) {
12
+ waiter({ value, done: false });
13
+ return;
14
+ }
15
+ if (this.buffered.length >= this.maxBuffered) throw new Error("Adapter event backlog exceeded");
16
+ this.buffered.push(value);
17
+ }
18
+
19
+ close(): void {
20
+ if (this.closed) return;
21
+ this.closed = true;
22
+ while (this.waiters.length > 0) this.waiters.shift()!({ value: undefined, done: true });
23
+ }
24
+
25
+ async collect(): Promise<T[]> {
26
+ const values: T[] = [];
27
+ for await (const value of this) values.push(value);
28
+ return values;
29
+ }
30
+
31
+ [Symbol.asyncIterator](): AsyncIterator<T> {
32
+ return {
33
+ next: () => {
34
+ const value = this.buffered.shift();
35
+ if (value !== undefined) return Promise.resolve({ value, done: false });
36
+ if (this.closed) return Promise.resolve({ value: undefined, done: true });
37
+ return new Promise(resolve => this.waiters.push(resolve));
38
+ },
39
+ return: () => {
40
+ this.close();
41
+ return Promise.resolve({ value: undefined, done: true });
42
+ },
43
+ };
44
+ }
45
+ }
@@ -0,0 +1,30 @@
1
+ const MAX_ENCODED_REQUEST_BYTES = 64 * 1024 * 1024;
2
+ const MAX_DECODED_REQUEST_BYTES = 128 * 1024 * 1024;
3
+
4
+ function assertWithinLimit(bytes: number, limit: number, label: string): void {
5
+ if (bytes > limit) throw new Error(`${label} exceeds ${limit} bytes`);
6
+ }
7
+
8
+ export async function readJsonRequestBody(request: Request): Promise<unknown> {
9
+ const declaredLength = Number(request.headers.get("content-length"));
10
+ if (Number.isFinite(declaredLength)) {
11
+ assertWithinLimit(declaredLength, MAX_ENCODED_REQUEST_BYTES, "Encoded request body");
12
+ }
13
+
14
+ const encoded = new Uint8Array(await request.arrayBuffer());
15
+ assertWithinLimit(encoded.byteLength, MAX_ENCODED_REQUEST_BYTES, "Encoded request body");
16
+
17
+ const contentEncoding = (request.headers.get("content-encoding") ?? "identity").trim().toLowerCase();
18
+ let decoded: Uint8Array;
19
+ if (contentEncoding === "" || contentEncoding === "identity") {
20
+ decoded = encoded;
21
+ } else if (contentEncoding === "zstd") {
22
+ decoded = await Bun.zstdDecompress(encoded);
23
+ } else {
24
+ throw new Error(`Unsupported Content-Encoding: ${contentEncoding}`);
25
+ }
26
+ assertWithinLimit(decoded.byteLength, MAX_DECODED_REQUEST_BYTES, "Decoded request body");
27
+
28
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(decoded);
29
+ return JSON.parse(text) as unknown;
30
+ }