@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/service.ts ADDED
@@ -0,0 +1,315 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
+ import { homedir, userInfo } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import type { AppConfig } from "./config";
5
+ import { assertDurableRuntimeCommand, atomicWriteFile, getConfigDir } from "./config";
6
+ import { runCommand, runChecked } from "./process";
7
+
8
+ const LABEL = "io.github.codex-chatgpt-web.daemon";
9
+
10
+ export interface ServiceStatus {
11
+ supported: boolean;
12
+ installed: boolean;
13
+ loaded: boolean;
14
+ label: string;
15
+ definitionPath?: string;
16
+ }
17
+
18
+ function xml(value: string): string {
19
+ return value
20
+ .replaceAll("&", "&")
21
+ .replaceAll("<", "&lt;")
22
+ .replaceAll(">", "&gt;")
23
+ .replaceAll('"', "&quot;")
24
+ .replaceAll("'", "&apos;");
25
+ }
26
+
27
+ function plistPath(): string {
28
+ return join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
29
+ }
30
+
31
+ function launchDomain(): string {
32
+ return `gui/${userInfo().uid}`;
33
+ }
34
+
35
+ function serviceTarget(): string {
36
+ return `${launchDomain()}/${LABEL}`;
37
+ }
38
+
39
+ async function bootstrapService(path: string, timeoutMs = 20_000): Promise<void> {
40
+ const deadline = Date.now() + timeoutMs;
41
+ let lastError = "unknown launchctl bootstrap failure";
42
+ while (Date.now() < deadline) {
43
+ const result = runCommand("launchctl", ["bootstrap", launchDomain(), path]);
44
+ if (result.status === 0) return;
45
+ lastError = result.stderr.trim() || result.stdout.trim() || `exit status ${result.status}`;
46
+ await new Promise(resolveWait => setTimeout(resolveWait, 100));
47
+ }
48
+ throw new Error(`launchctl bootstrap ${launchDomain()} ${path} failed after ${timeoutMs}ms: ${lastError}`);
49
+ }
50
+
51
+ async function waitForServiceUnloaded(timeoutMs = 20_000): Promise<void> {
52
+ const deadline = Date.now() + timeoutMs;
53
+ while (getServiceStatus().loaded && Date.now() < deadline) {
54
+ await new Promise(resolveWait => setTimeout(resolveWait, 50));
55
+ }
56
+ if (getServiceStatus().loaded) throw new Error(`launchd did not unload ${LABEL} after ${timeoutMs}ms`);
57
+ }
58
+
59
+ function plist(config: AppConfig): string {
60
+ const logDir = join(getConfigDir(), "logs");
61
+ const args = [...config.runtimeCommand, "serve"];
62
+ return `<?xml version="1.0" encoding="UTF-8"?>
63
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
64
+ <plist version="1.0">
65
+ <dict>
66
+ <key>Label</key>
67
+ <string>${LABEL}</string>
68
+ <key>ProgramArguments</key>
69
+ <array>
70
+ ${args.map(arg => ` <string>${xml(arg)}</string>`).join("\n")}
71
+ </array>
72
+ <key>EnvironmentVariables</key>
73
+ <dict>
74
+ <key>DSH_CHATGPT_FREE_HOME</key>
75
+ <string>${xml(getConfigDir())}</string>
76
+ </dict>
77
+ <key>RunAtLoad</key>
78
+ <true/>
79
+ <key>KeepAlive</key>
80
+ <true/>
81
+ <key>ThrottleInterval</key>
82
+ <integer>10</integer>
83
+ <key>StandardOutPath</key>
84
+ <string>${xml(join(logDir, "daemon.stdout.log"))}</string>
85
+ <key>StandardErrorPath</key>
86
+ <string>${xml(join(logDir, "daemon.stderr.log"))}</string>
87
+ <key>ProcessType</key>
88
+ <string>Background</string>
89
+ </dict>
90
+ </plist>
91
+ `;
92
+ }
93
+
94
+ function assertMacOs(): void {
95
+ if (process.platform !== "darwin") {
96
+ throw new Error("Terminal-managed background services require macOS.");
97
+ }
98
+ }
99
+
100
+ export function getServiceStatus(): ServiceStatus {
101
+ if (process.platform !== "darwin") return { supported: false, installed: false, loaded: false, label: LABEL };
102
+ const path = plistPath();
103
+ const result = runCommand("launchctl", ["print", serviceTarget()]);
104
+ return {
105
+ supported: true,
106
+ installed: existsSync(path),
107
+ loaded: result.status === 0,
108
+ label: LABEL,
109
+ definitionPath: path,
110
+ };
111
+ }
112
+
113
+ export function installService(config: AppConfig): ServiceStatus {
114
+ assertMacOs();
115
+ assertDurableRuntimeCommand(config.runtimeCommand);
116
+ const path = plistPath();
117
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
118
+ mkdirSync(join(getConfigDir(), "logs"), { recursive: true, mode: 0o700 });
119
+ const next = plist(config);
120
+ if (!existsSync(path) || readFileSync(path, "utf8") !== next) atomicWriteFile(path, next);
121
+ const status = getServiceStatus();
122
+ if (!status.loaded) runChecked("launchctl", ["bootstrap", launchDomain(), path]);
123
+ return getServiceStatus();
124
+ }
125
+
126
+ export function startService(): ServiceStatus {
127
+ assertMacOs();
128
+ const path = plistPath();
129
+ if (!existsSync(path)) throw new Error(`Service is not installed: ${path}`);
130
+ const status = getServiceStatus();
131
+ if (!status.loaded) runChecked("launchctl", ["bootstrap", launchDomain(), path]);
132
+ return getServiceStatus();
133
+ }
134
+
135
+ export interface DrainLease {
136
+ release: () => Promise<void>;
137
+ }
138
+
139
+ async function control(config: AppConfig, action: "drain" | "resume" | "cancel-turns"): Promise<Record<string, unknown>> {
140
+ const controller = new AbortController();
141
+ const timeout = setTimeout(() => controller.abort(), 5_000);
142
+ try {
143
+ const response = await fetch(`http://${config.host}:${config.port}/admin/${action}`, {
144
+ method: "POST",
145
+ headers: { authorization: `Bearer ${config.controlToken}` },
146
+ signal: controller.signal,
147
+ });
148
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
149
+ return await response.json() as Record<string, unknown>;
150
+ } finally {
151
+ clearTimeout(timeout);
152
+ }
153
+ }
154
+
155
+ export async function interruptActiveTurn(
156
+ config: AppConfig,
157
+ identity: { threadId: string; turnId: string },
158
+ ): Promise<{ cancelledHttpTurns: number; cancelledBrowserTurns: number }> {
159
+ const controller = new AbortController();
160
+ const timeout = setTimeout(() => controller.abort(), 2_000);
161
+ try {
162
+ const response = await fetch(`http://${config.host}:${config.port}/admin/interrupt-turn`, {
163
+ method: "POST",
164
+ headers: {
165
+ authorization: `Bearer ${config.controlToken}`,
166
+ "content-type": "application/json",
167
+ },
168
+ body: JSON.stringify(identity),
169
+ signal: controller.signal,
170
+ });
171
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
172
+ const result = await response.json() as Record<string, unknown>;
173
+ const cancelledHttpTurns = result.cancelled_http_turns;
174
+ const cancelledBrowserTurns = result.cancelled_browser_turns;
175
+ if (result.status !== "ok"
176
+ || !Number.isInteger(cancelledHttpTurns) || (cancelledHttpTurns as number) < 0
177
+ || !Number.isInteger(cancelledBrowserTurns) || (cancelledBrowserTurns as number) < 0) {
178
+ throw new Error("daemon returned an invalid interrupt acknowledgement");
179
+ }
180
+ return {
181
+ cancelledHttpTurns: cancelledHttpTurns as number,
182
+ cancelledBrowserTurns: cancelledBrowserTurns as number,
183
+ };
184
+ } finally {
185
+ clearTimeout(timeout);
186
+ }
187
+ }
188
+
189
+ export async function cancelActiveTurns(config: AppConfig): Promise<{
190
+ cancelledHttpTurns: number;
191
+ cancelledBrowserTurns: number;
192
+ }> {
193
+ const result = await control(config, "cancel-turns");
194
+ const cancelledHttpTurns = result.cancelled_http_turns;
195
+ const cancelledBrowserTurns = result.cancelled_browser_turns;
196
+ if (!Number.isInteger(cancelledHttpTurns) || (cancelledHttpTurns as number) < 0
197
+ || !Number.isInteger(cancelledBrowserTurns) || (cancelledBrowserTurns as number) < 0
198
+ || result.active_http_turns !== 0 || result.active_browser_turns !== 0) {
199
+ throw new Error("daemon did not acknowledge complete active-turn cancellation");
200
+ }
201
+ return {
202
+ cancelledHttpTurns: cancelledHttpTurns as number,
203
+ cancelledBrowserTurns: cancelledBrowserTurns as number,
204
+ };
205
+ }
206
+
207
+ export async function negotiateDrain(
208
+ controlAction: (action: "drain" | "resume") => Promise<Record<string, unknown>>,
209
+ ): Promise<DrainLease> {
210
+ let drained = false;
211
+ let drainAttempted = false;
212
+ try {
213
+ drainAttempted = true;
214
+ const health = await controlAction("drain");
215
+ drained = true;
216
+ const activeHttp = health.active_http_turns;
217
+ const activeBrowser = health.active_browser_turns;
218
+ if (!Number.isInteger(activeHttp) || !Number.isInteger(activeBrowser) || health.accepting_turns !== false) {
219
+ throw new Error("daemon did not acknowledge the drain contract");
220
+ }
221
+ if ((activeHttp as number) > 0 || (activeBrowser as number) > 0) {
222
+ throw new Error(`daemon has ${activeHttp} active HTTP turn(s) and ${activeBrowser} active browser turn(s)`);
223
+ }
224
+ return { release: async () => { if (drained) { await controlAction("resume"); drained = false; } } };
225
+ } catch (error) {
226
+ let resumeError: unknown;
227
+ if (drainAttempted) {
228
+ try {
229
+ await controlAction("resume");
230
+ drained = false;
231
+ } catch (caught) {
232
+ resumeError = caught;
233
+ }
234
+ }
235
+ const message = error instanceof Error ? error.message : String(error);
236
+ const compensation = resumeError
237
+ ? `; compensating resume also failed: ${resumeError instanceof Error ? resumeError.message : String(resumeError)}`
238
+ : "";
239
+ throw new Error(`Refusing to stop or restart because atomic idleness could not be proven: ${message}${compensation}`);
240
+ }
241
+ }
242
+
243
+ async function acquireDrain(config: AppConfig): Promise<DrainLease> {
244
+ if (!getServiceStatus().loaded) return { release: async () => {} };
245
+ return negotiateDrain(action => control(config, action));
246
+ }
247
+
248
+ async function releaseDrainAfterFailure(lease: DrainLease, failure: unknown): Promise<never> {
249
+ try {
250
+ await lease.release();
251
+ } catch (resumeError) {
252
+ const primary = failure instanceof Error ? failure.message : String(failure);
253
+ const compensation = resumeError instanceof Error ? resumeError.message : String(resumeError);
254
+ throw new Error(`${primary}; compensating daemon resume also failed: ${compensation}`);
255
+ }
256
+ throw failure instanceof Error ? failure : new Error(String(failure));
257
+ }
258
+
259
+ export async function assertServiceIdle(config: AppConfig): Promise<void> {
260
+ const lease = await acquireDrain(config);
261
+ await lease.release();
262
+ }
263
+
264
+ export async function restartService(config: AppConfig): Promise<ServiceStatus> {
265
+ assertMacOs();
266
+ if (!getServiceStatus().loaded) return startService();
267
+ const lease = await acquireDrain(config);
268
+ try {
269
+ runChecked("launchctl", ["bootout", serviceTarget()]);
270
+ await waitForServiceUnloaded();
271
+ await bootstrapService(plistPath());
272
+ } catch (error) {
273
+ return releaseDrainAfterFailure(lease, error);
274
+ }
275
+ return getServiceStatus();
276
+ }
277
+
278
+ export function removeLegacyRuntimeArtifacts(config: AppConfig): void {
279
+ const legacyWrapper = join(getConfigDir(), "bin", "serve-with-playwright.sh");
280
+ const legacyVendor = join(getConfigDir(), "vendor");
281
+ if (config.runtimeCommand.some(part => part === legacyWrapper || part.startsWith(`${legacyVendor}/`))) {
282
+ throw new Error("Refusing to remove legacy runtime artifacts while the active service still references them");
283
+ }
284
+ rmSync(legacyWrapper, { force: true });
285
+ rmSync(legacyVendor, { recursive: true, force: true });
286
+ }
287
+
288
+ export async function stopService(config: AppConfig): Promise<ServiceStatus> {
289
+ assertMacOs();
290
+ if (getServiceStatus().loaded) {
291
+ const lease = await acquireDrain(config);
292
+ try {
293
+ runChecked("launchctl", ["bootout", serviceTarget()]);
294
+ await waitForServiceUnloaded();
295
+ } catch (error) {
296
+ return releaseDrainAfterFailure(lease, error);
297
+ }
298
+ }
299
+ return getServiceStatus();
300
+ }
301
+
302
+ export async function uninstallService(config: AppConfig): Promise<ServiceStatus> {
303
+ assertMacOs();
304
+ if (getServiceStatus().loaded) {
305
+ const lease = await acquireDrain(config);
306
+ try {
307
+ runChecked("launchctl", ["bootout", serviceTarget()]);
308
+ await waitForServiceUnloaded();
309
+ } catch (error) {
310
+ return releaseDrainAfterFailure(lease, error);
311
+ }
312
+ }
313
+ rmSync(plistPath(), { force: true });
314
+ return getServiceStatus();
315
+ }