@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
@@ -0,0 +1,418 @@
1
+ import TurndownService from "turndown";
2
+ import { gfm } from "turndown-plugin-gfm";
3
+
4
+ const turndown = new TurndownService({
5
+ headingStyle: "atx",
6
+ bulletListMarker: "-",
7
+ codeBlockStyle: "fenced",
8
+ fence: "```",
9
+ emDelimiter: "*",
10
+ strongDelimiter: "**",
11
+ linkStyle: "inlined",
12
+ });
13
+
14
+ turndown.use(gfm);
15
+ turndown.remove(["button", "script", "style", "canvas", "noscript", "iframe"]);
16
+ turndown.addRule("removeImages", {
17
+ filter: node => ["IMG", "PICTURE", "SOURCE"].includes(node.nodeName),
18
+ replacement: () => "",
19
+ });
20
+ turndown.addRule("removeSvg", {
21
+ filter: node => node.nodeName === "SVG",
22
+ replacement: () => "",
23
+ });
24
+ turndown.addRule("removeMapAndWidgets", {
25
+ filter: node => {
26
+ if (node.nodeType !== 1) return false;
27
+ const el = node as HTMLElement;
28
+ const testId = el.getAttribute?.("data-testid") || "";
29
+ const ariaLabel = el.getAttribute?.("aria-label") || "";
30
+ const className = typeof el.className === "string" ? el.className : "";
31
+
32
+ if (
33
+ /map|carousel|place-card|places|location-card|poi-/i.test(testId) ||
34
+ /map/i.test(ariaLabel) ||
35
+ /mapbox|leaflet|map-container|places-carousel|place-card/i.test(className)
36
+ ) {
37
+ return true;
38
+ }
39
+
40
+ if (el.children?.length === 0) {
41
+ const text = el.textContent?.trim() || "";
42
+ if (/Use two fingers to move the map|Hold Ctrl to zoom|Map data|Report a map error/i.test(text)) {
43
+ return true;
44
+ }
45
+ }
46
+
47
+ return false;
48
+ },
49
+ replacement: () => "",
50
+ });
51
+ turndown.addRule("linkInlineFilePaths", {
52
+ filter: node => inlineFilePath(node) !== undefined,
53
+ replacement: (_content, node) => {
54
+ const path = node.textContent!;
55
+ const target = path.replaceAll("\\", "/");
56
+ return `[${path}](<${target}>)`;
57
+ },
58
+ });
59
+ turndown.addRule("compactListItem", {
60
+ filter: "li",
61
+ replacement: (content, node, options) => {
62
+ const parent = node.parentNode as HTMLElement | null;
63
+ let prefix = `${options.bulletListMarker} `;
64
+ if (parent?.nodeName === "OL") {
65
+ const start = Number(parent.getAttribute("start") ?? "1");
66
+ const index = Array.prototype.indexOf.call(parent.children, node) as number;
67
+ prefix = `${start + index}. `;
68
+ }
69
+ const normalized = content
70
+ .replace(/^\n+|\n+$/g, "")
71
+ .replace(/\n/g, `\n${" ".repeat(prefix.length)}`);
72
+ return `${prefix}${normalized}${node.nextSibling ? "\n" : ""}`;
73
+ },
74
+ });
75
+
76
+ function inlineFilePath(node: Node): string | undefined {
77
+ if (node.nodeName !== "CODE") return undefined;
78
+ for (let ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) {
79
+ if (["A", "PRE"].includes(ancestor.nodeName)) return undefined;
80
+ }
81
+
82
+ const path = node.textContent ?? "";
83
+ if (path !== path.trim() || /[\s`<>()[\]]/.test(path)) return undefined;
84
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(path)) return undefined;
85
+
86
+ const withoutLocation = path.replace(/:\d+(?::\d+)?$/, "");
87
+ const separator = Math.max(withoutLocation.lastIndexOf("/"), withoutLocation.lastIndexOf("\\"));
88
+ if (separator < 0) return undefined;
89
+
90
+ const basename = withoutLocation.slice(separator + 1);
91
+ if (!/\.[a-z\d][a-z\d._-]*$/i.test(basename)) return undefined;
92
+ return path;
93
+ }
94
+
95
+ function preserveObsidianWikiLinks(markdown: string): string {
96
+ // Turndown escapes literal brackets, but Codex interprets the resulting `\[` as LaTeX.
97
+ // Restore the source syntax before converting it into a regular Markdown file link.
98
+ return markdown.replace(/\\\[\\\[([^\r\n]*?)\\\]\\\]/g, "[[$1]]");
99
+ }
100
+
101
+ function obsidianWikiLink(value: string): string | undefined {
102
+ const separator = value.indexOf("|");
103
+ const target = (separator >= 0 ? value.slice(0, separator) : value).trim();
104
+ const label = (separator >= 0 ? value.slice(separator + 1) : value).trim();
105
+ if (!target || !label || /[<>]/.test(target)) return undefined;
106
+
107
+ const fragmentAt = target.indexOf("#");
108
+ const note = fragmentAt >= 0 ? target.slice(0, fragmentAt) : target;
109
+ const fragment = fragmentAt >= 0 ? target.slice(fragmentAt) : "";
110
+ const extension = note.slice(note.lastIndexOf("/") + 1).includes(".");
111
+ const path = note && !extension ? `${note}.md` : note;
112
+ return `[${label}](<${path}${fragment}>)`;
113
+ }
114
+
115
+ function linkObsidianWikiLinks(markdown: string): string {
116
+ let fence: { marker: "`" | "~"; length: number } | undefined;
117
+ return markdown.split("\n").map(line => {
118
+ const fenceRun = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1];
119
+ if (fence) {
120
+ const closingRun = line.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/)?.[1];
121
+ if (closingRun?.[0] === fence.marker && closingRun.length >= fence.length) fence = undefined;
122
+ return line;
123
+ }
124
+ if (fenceRun) {
125
+ fence = { marker: fenceRun[0] as "`" | "~", length: fenceRun.length };
126
+ return line;
127
+ }
128
+
129
+ let result = "";
130
+ let inlineCodeTicks = 0;
131
+ for (let index = 0; index < line.length;) {
132
+ if (line[index] === "`") {
133
+ let end = index + 1;
134
+ while (line[end] === "`") end += 1;
135
+ const ticks = end - index;
136
+ inlineCodeTicks = inlineCodeTicks === 0 ? ticks : ticks === inlineCodeTicks ? 0 : inlineCodeTicks;
137
+ result += line.slice(index, end);
138
+ index = end;
139
+ continue;
140
+ }
141
+ if (inlineCodeTicks === 0 && line.startsWith("[[", index) && line[index - 1] !== "!") {
142
+ const end = line.indexOf("]]", index + 2);
143
+ if (end >= 0) {
144
+ const linked = obsidianWikiLink(line.slice(index + 2, end));
145
+ if (linked) {
146
+ result += linked;
147
+ index = end + 2;
148
+ continue;
149
+ }
150
+ }
151
+ }
152
+ result += line[index];
153
+ index += 1;
154
+ }
155
+ return result;
156
+ }).join("\n");
157
+ }
158
+
159
+ function restoreToolCallTags(markdown: string): string {
160
+ const restoredTags = markdown
161
+ .replace(/<tool\\_call>/gi, "<tool_call>")
162
+ .replace(/<\/tool\\_call>/gi, "</tool_call>");
163
+ return restoredTags.replace(/(<tool_call>[\s\S]*?<\/tool_call>)/gi, block => block.replace(/\\([_*[\]])/g, "$1"));
164
+ }
165
+
166
+ function cleanCitationBadges(markdown: string): string {
167
+ return markdown.replace(/\[([^\]]+?)\s*\+\d+\]\((https?:\/\/[^)]+)\)/g, "[$1]($2)");
168
+ }
169
+
170
+ export function chatGptHtmlToMarkdown(html: string): string {
171
+ if (!html.trim()) return "";
172
+ return cleanCitationBadges(
173
+ restoreToolCallTags(linkObsidianWikiLinks(preserveObsidianWikiLinks(turndown.turndown(html))))
174
+ ).trim();
175
+ }
176
+
177
+ export interface ChatGptMarkdownSegment {
178
+ key: string;
179
+ tag?: string;
180
+ html: string;
181
+ text: string;
182
+ group?: string;
183
+ sourceStart?: number;
184
+ sourceEnd?: number;
185
+ streamable: boolean;
186
+ }
187
+
188
+ interface ChatGptMarkdownCandidate extends ChatGptMarkdownSegment {
189
+ changedAt: number;
190
+ streamableAt?: number;
191
+ }
192
+
193
+ interface CommittedChatGptMarkdownSegment {
194
+ key: string;
195
+ tag?: string;
196
+ text: string;
197
+ sourceStart?: number;
198
+ sourceEnd?: number;
199
+ }
200
+
201
+ export class ChatGptMarkdownConsistencyError extends Error {
202
+ constructor(message: string) {
203
+ super(message);
204
+ this.name = "ChatGptMarkdownConsistencyError";
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Converts structurally completed ChatGPT DOM blocks into an append-only Markdown stream.
210
+ *
211
+ * ChatGPT can rewrite old HTML while hydrating citations and controls, so a character prefix is
212
+ * not a safe commit boundary. It can also virtualize an already-rendered prefix, so later DOM
213
+ * snapshots are partial observations rather than the response ledger. The browser supplies source
214
+ * ranges for semantic blocks and marks a block streamable only after a following block exists.
215
+ * Once committed, a missing prefix is harmless; changing text at a committed source range remains
216
+ * an explicit protocol error because Responses deltas cannot be retracted.
217
+ */
218
+ export class ChatGptMarkdownBuffer {
219
+ private readonly candidates = new Map<string, ChatGptMarkdownCandidate>();
220
+ private readonly committed: CommittedChatGptMarkdownSegment[] = [];
221
+ private latest: ChatGptMarkdownSegment[] = [];
222
+ private markdown = "";
223
+ private lastGroup: string | undefined;
224
+ private consistencyError: ChatGptMarkdownConsistencyError | undefined;
225
+
226
+ constructor(
227
+ private readonly transform: (markdown: string) => string = markdown => markdown,
228
+ private readonly stabilityMs = 750,
229
+ ) {
230
+ if (!Number.isFinite(stabilityMs) || stabilityMs < 0) {
231
+ throw new Error("ChatGPT Markdown stability window must be a non-negative finite number");
232
+ }
233
+ }
234
+
235
+ observe(segments: ChatGptMarkdownSegment[], now = Date.now()): string {
236
+ const reconciled = this.reconcile(segments);
237
+ if (reconciled instanceof ChatGptMarkdownConsistencyError) {
238
+ this.consistencyError = reconciled;
239
+ return "";
240
+ }
241
+ this.consistencyError = undefined;
242
+ this.latest = reconciled.map(segment => ({ ...segment }));
243
+
244
+ const visibleCandidates = new Set<string>();
245
+ for (const segment of reconciled) {
246
+ const candidateId = this.candidateId(segment);
247
+ visibleCandidates.add(candidateId);
248
+ const previous = this.candidates.get(candidateId);
249
+ const unchanged = previous
250
+ && previous.key === segment.key
251
+ && previous.tag === segment.tag
252
+ && previous.html === segment.html
253
+ && previous.text === segment.text
254
+ && previous.group === segment.group
255
+ && previous.sourceStart === segment.sourceStart
256
+ && previous.sourceEnd === segment.sourceEnd;
257
+ this.candidates.set(candidateId, {
258
+ ...segment,
259
+ changedAt: unchanged ? previous.changedAt : now,
260
+ ...(segment.streamable ? {
261
+ streamableAt: unchanged && previous.streamableAt !== undefined
262
+ ? previous.streamableAt
263
+ : now,
264
+ } : {}),
265
+ });
266
+ }
267
+ for (const candidateId of this.candidates.keys()) {
268
+ if (!visibleCandidates.has(candidateId)) this.candidates.delete(candidateId);
269
+ }
270
+
271
+ let delta = "";
272
+ let committedCount = 0;
273
+ while (committedCount < reconciled.length) {
274
+ const segment = reconciled[committedCount]!;
275
+ const candidateId = this.candidateId(segment);
276
+ const candidate = this.candidates.get(candidateId);
277
+ if (!candidate?.streamable || candidate.streamableAt === undefined) break;
278
+ if (now - Math.max(candidate.changedAt, candidate.streamableAt) < this.stabilityMs) break;
279
+ delta += this.commit(candidate);
280
+ this.committed.push(this.committedSegment(candidate));
281
+ this.candidates.delete(candidateId);
282
+ committedCount += 1;
283
+ }
284
+ this.latest = this.latest.slice(committedCount);
285
+ return delta;
286
+ }
287
+
288
+ finish(): { markdown: string; delta: string } {
289
+ let delta = "";
290
+ for (const segment of this.latest) {
291
+ delta += this.commit(segment);
292
+ this.committed.push(this.committedSegment(segment));
293
+ }
294
+ this.candidates.clear();
295
+ this.latest = [];
296
+ return { markdown: this.markdown, delta };
297
+ }
298
+
299
+ currentSnapshotIsConsistent(): boolean {
300
+ return true;
301
+ }
302
+
303
+ private reconcile(
304
+ segments: ChatGptMarkdownSegment[],
305
+ ): ChatGptMarkdownSegment[] {
306
+ if (this.committed.length === 0 || segments.length === 0) return segments;
307
+
308
+ const pending: ChatGptMarkdownSegment[] = [];
309
+ const lastCommittedEnd = this.committed
310
+ .map(segment => segment.sourceEnd)
311
+ .filter((end): end is number => end !== undefined)
312
+ .at(-1);
313
+ let highestCommittedIndex = -1;
314
+ let sawPending = false;
315
+ let previousSourceStart: number | undefined;
316
+
317
+ for (const segment of segments) {
318
+ if (segment.sourceStart !== undefined) {
319
+ if (previousSourceStart !== undefined && segment.sourceStart <= previousSourceStart) {
320
+ // Monotonic recovery
321
+ previousSourceStart = segment.sourceStart;
322
+ } else {
323
+ previousSourceStart = segment.sourceStart;
324
+ }
325
+ }
326
+ const committedIndex = this.committedIndex(segment);
327
+ if (committedIndex !== undefined) {
328
+ const committed = this.committed[committedIndex]!;
329
+ if (committed.text !== segment.text) {
330
+ // ChatGPT hydrated citations or badges on an already committed block.
331
+ // Update the committed text without throwing a fatal exception.
332
+ committed.text = segment.text;
333
+ }
334
+ highestCommittedIndex = Math.max(highestCommittedIndex, committedIndex);
335
+ continue;
336
+ }
337
+
338
+ if (segment.sourceStart !== undefined && lastCommittedEnd !== undefined) {
339
+ if (segment.sourceStart <= lastCommittedEnd) {
340
+ // Overlapping range on already committed prefix - ignore rather than crashing
341
+ continue;
342
+ }
343
+ sawPending = true;
344
+ pending.push(segment);
345
+ continue;
346
+ }
347
+
348
+ sawPending = true;
349
+ pending.push(segment);
350
+ }
351
+
352
+ return pending;
353
+ }
354
+
355
+ private committedIndex(segment: ChatGptMarkdownSegment): number | undefined {
356
+ const exact = this.committed.findIndex(committed => (
357
+ segment.sourceStart !== undefined && committed.sourceStart !== undefined
358
+ ? segment.sourceStart === committed.sourceStart && segment.tag === committed.tag
359
+ : segment.key === committed.key
360
+ ));
361
+ if (exact >= 0) return exact;
362
+
363
+ if (segment.sourceStart !== undefined) return undefined;
364
+ if (!segment.tag) return undefined;
365
+ const semanticMatches = this.committed
366
+ .map((committed, index) => ({ committed, index }))
367
+ .filter(({ committed }) => committed.tag === segment.tag && committed.text === segment.text);
368
+ return semanticMatches.length === 1 ? semanticMatches[0]!.index : undefined;
369
+ }
370
+
371
+ private matchesLatestPending(segment: ChatGptMarkdownSegment): boolean {
372
+ const exact = this.latest.filter(candidate => (
373
+ segment.sourceStart !== undefined && candidate.sourceStart !== undefined
374
+ ? segment.sourceStart === candidate.sourceStart && segment.tag === candidate.tag
375
+ : segment.key === candidate.key
376
+ ));
377
+ if (exact.length === 1) return true;
378
+ if (segment.sourceStart !== undefined) return false;
379
+ if (!segment.tag) return false;
380
+ return this.latest.filter(candidate => (
381
+ candidate.tag === segment.tag && candidate.text === segment.text
382
+ )).length === 1;
383
+ }
384
+
385
+ private candidateId(segment: ChatGptMarkdownSegment): string {
386
+ return segment.sourceStart !== undefined
387
+ ? `source:${segment.sourceStart}:${segment.tag ?? ""}`
388
+ : `key:${segment.key}`;
389
+ }
390
+
391
+ private committedSegment(segment: ChatGptMarkdownSegment): CommittedChatGptMarkdownSegment {
392
+ return {
393
+ key: segment.key,
394
+ ...(segment.tag ? { tag: segment.tag } : {}),
395
+ text: segment.text,
396
+ ...(segment.sourceStart !== undefined ? { sourceStart: segment.sourceStart } : {}),
397
+ ...(segment.sourceEnd !== undefined ? { sourceEnd: segment.sourceEnd } : {}),
398
+ };
399
+ }
400
+
401
+ private changedCommittedBlockError(): ChatGptMarkdownConsistencyError {
402
+ return new ChatGptMarkdownConsistencyError(
403
+ "ChatGPT changed a completed text block that was already streamed to Codex",
404
+ );
405
+ }
406
+
407
+ private commit(segment: ChatGptMarkdownSegment): string {
408
+ const block = this.transform(chatGptHtmlToMarkdown(segment.html));
409
+ if (!block) return "";
410
+ const separator = this.markdown
411
+ ? segment.group !== undefined && segment.group === this.lastGroup ? "\n" : "\n\n"
412
+ : "";
413
+ const delta = `${separator}${block}`;
414
+ this.markdown += delta;
415
+ this.lastGroup = segment.group;
416
+ return delta;
417
+ }
418
+ }
@@ -0,0 +1,25 @@
1
+ import { defaultBrokerEndpoint, resolveBrokerEndpoint } from "../../config";
2
+ import { runChatGptMcpServer, type ChatGptMcpContract } from "./mcp-server";
3
+
4
+ function option(args: string[], name: string, fallback: string): string {
5
+ const index = args.indexOf(name);
6
+ if (index < 0) return fallback;
7
+ const value = args[index + 1]?.trim();
8
+ if (!value) throw new Error(`${name} requires a value`);
9
+ args.splice(index, 2);
10
+ return value;
11
+ }
12
+
13
+ export async function runChatGptMcpMain(args: string[]): Promise<void> {
14
+ const remaining = [...args];
15
+ const brokerSocketPath = resolveBrokerEndpoint(option(remaining, "--broker-socket", defaultBrokerEndpoint()));
16
+ const requestedContract = option(remaining, "--contract", "native");
17
+ if (requestedContract !== "native" && requestedContract !== "safe") {
18
+ throw new Error(`--contract must be native or safe, received ${requestedContract}`);
19
+ }
20
+ if (remaining.length > 0) throw new Error(`Unknown MCP arguments: ${remaining.join(" ")}`);
21
+ await runChatGptMcpServer({
22
+ brokerSocketPath,
23
+ contract: requestedContract as ChatGptMcpContract,
24
+ });
25
+ }