@youdie006/prodex 0.6.0 → 0.6.1

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.
@@ -0,0 +1,1007 @@
1
+ import { buildDryRunBundle } from "./bundle.js";
2
+ import { chatGptVisibilityBlocker, defaultChatGptProfileDir, getChatGptBrowserStatus, listChatGptModelOptions, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, sendChatGptPrompt } from "./chatgpt-browser.js";
3
+ import { ASK_PRO_BOOLEAN_FLAGS, ASK_PRO_PREVIEW_VALUE_FLAGS, ASK_PRO_VALUE_FLAGS, assertHelpRequestArgs, assertNoExtraArgs, assertOnlyOptions, findHelpFlagIndexBeforePromptDelimiter, hasAskProDryRunMode, hasAskProMode, hasAskProSendMode, isHelpSubcommand, parseAskProArgs, printHelpIfRequested, readFlag, readPortFlag, readPositionalsWithOptions, readPositiveNumberFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
4
+ import { printProBrowserHelp, printProHelp } from "./cli-help.js";
5
+ import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
6
+ import { redactServerUrl } from "./cli-server.js";
7
+ import { errorMessage, firstLine, formatBlockedConsultRecordedMessage, formatBrowserCheckCommand, formatBrowserLoginCommand, formatBrowserSmokeCommand, formatBrowserTargetAskCommand, formatInitCommand, formatSetupCommand, isMissingFileError, isUntrustedResultError, sourceAwareBrowserBlocker, sourceAwareBrowserNextStep, sourceAwareResultError, sourceAwareResultMessage, sourceAwareSetupMessage } from "./cli-shared.js";
8
+ import { getTokenExpiryStatus, loadBrowserDefaults, loadLocalConfig } from "./config.js";
9
+ import { BridgeStore, MAX_FETCHABLE_RESULT_ARTIFACT_BYTES } from "./store.js";
10
+ export async function runChatgptCommand(rest, io) {
11
+ const [subcommand, ...chatgptArgs] = rest;
12
+ if (!subcommand || isHelpSubcommand(subcommand)) {
13
+ throw legacyChatGptNamespaceError();
14
+ }
15
+ if (subcommand === "open") {
16
+ assertOnlyOptions(chatgptArgs, "chatgpt open", ["--port", "--profile-dir", "--url"]);
17
+ const opened = openChatGptBrowser({
18
+ port: readPortFlag(chatgptArgs, "--port") ?? 9333,
19
+ profileDir: readFlag(chatgptArgs, "--profile-dir"),
20
+ url: readChatGptBrowserUrlFlag(chatgptArgs)
21
+ });
22
+ await assertBrowserLaunchStayedAlive(opened);
23
+ io.stdout(`Opened ChatGPT browser via ${opened.command}.`);
24
+ io.stdout(`Profile: ${opened.profileDir}`);
25
+ io.stdout(`Debug: http://127.0.0.1:${opened.port}`);
26
+ return 0;
27
+ }
28
+ if (subcommand === "status") {
29
+ assertOnlyOptions(chatgptArgs, "chatgpt status", ["--port"]);
30
+ const status = await getChatGptBrowserStatus({ port: readPortFlag(chatgptArgs, "--port") ?? 9333 });
31
+ io.stdout(JSON.stringify(status, null, 2));
32
+ return 0;
33
+ }
34
+ if (subcommand === "smoke") {
35
+ assertOnlyOptions(chatgptArgs, "chatgpt smoke", ["--cwd", "--port", "--timeout-ms", "--source-cli"]);
36
+ const targetCwd = resolveCwdFlag(io.cwd, chatgptArgs);
37
+ const targetStore = new BridgeStore(targetCwd);
38
+ const sourceCli = resolveOptionalFileFlag(io.cwd, chatgptArgs, "--source-cli");
39
+ const port = readPortFlag(chatgptArgs, "--port") ?? 9333;
40
+ const timeoutMs = readPositiveNumberFlag(chatgptArgs, "--timeout-ms") ?? 90000;
41
+ const commandOptions = {
42
+ ...(readFlag(chatgptArgs, "--cwd") ? { cwd: targetCwd } : {}),
43
+ ...(readFlag(chatgptArgs, "--port") ? { port } : {})
44
+ };
45
+ const smokePrompt = `This is a one-time prodex smoke test. Reply exactly: ${PRO_BROWSER_SMOKE_TOKEN}`;
46
+ const recordBlockedSmoke = async (summary, blocker, thread) => {
47
+ const bundle = await buildDryRunBundle(targetCwd, { prompt: smokePrompt, files: [] });
48
+ const task = await targetStore.createTask({
49
+ source: "codex",
50
+ title: "GPT Pro smoke",
51
+ prompt: bundle.text,
52
+ repo_id: "default",
53
+ provenance: {
54
+ adapter: "chatgpt-control",
55
+ session_id: bundle.id,
56
+ thread,
57
+ warnings: []
58
+ }
59
+ });
60
+ await targetStore.claimTask(task.id, "chatgpt-pro");
61
+ await targetStore.completeTask(task.id, {
62
+ status: "blocked",
63
+ summary,
64
+ commands: ["visible ChatGPT browser smoke"],
65
+ blocker
66
+ });
67
+ await writeSessionBestEffort(targetStore, {
68
+ id: bundle.id,
69
+ direction: "codex_to_chatgpt",
70
+ backend: "chatgpt-control",
71
+ task_id: task.id,
72
+ thread,
73
+ status: "blocked",
74
+ blocker,
75
+ warnings: []
76
+ }, io);
77
+ return task.id;
78
+ };
79
+ let result;
80
+ try {
81
+ result = await sendChatGptPrompt({
82
+ port,
83
+ prompt: smokePrompt,
84
+ timeoutMs
85
+ });
86
+ }
87
+ catch (error) {
88
+ const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, commandOptions);
89
+ const message = blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error);
90
+ let taskId;
91
+ try {
92
+ taskId = await recordBlockedSmoke(message, blocker);
93
+ }
94
+ catch (recordError) {
95
+ throw new Error(`${message} (also failed to record blocked smoke: ${errorMessage(recordError)})`);
96
+ }
97
+ throw new Error(formatBlockedConsultRecordedMessage(message, taskId, sourceCli, { cwd: targetCwd }));
98
+ }
99
+ if (result.answer.trim() !== PRO_BROWSER_SMOKE_TOKEN) {
100
+ const message = `Pro browser smoke returned an unexpected answer. Expected exactly ${PRO_BROWSER_SMOKE_TOKEN}. Actual: ${firstLine(result.answer)}`;
101
+ const blocker = {
102
+ code: "smoke_token_mismatch",
103
+ message,
104
+ retryable: true,
105
+ next_step: `Retry \`${formatBrowserSmokeCommand(sourceCli, commandOptions)}\` after selecting the intended Pro model, or inspect the visible ChatGPT answer.`
106
+ };
107
+ let taskId;
108
+ try {
109
+ taskId = await recordBlockedSmoke(message, blocker, result.url);
110
+ }
111
+ catch (recordError) {
112
+ throw new Error(`${message} (also failed to record blocked smoke: ${errorMessage(recordError)})`);
113
+ }
114
+ throw new Error(formatBlockedConsultRecordedMessage(message, taskId, sourceCli, { cwd: targetCwd }));
115
+ }
116
+ io.stdout(JSON.stringify(result, null, 2));
117
+ return 0;
118
+ }
119
+ throw legacyChatGptNamespaceError(subcommand);
120
+ }
121
+ export async function runProCommand(rest, io, runCliFn) {
122
+ const [subcommand, ...proArgs] = rest;
123
+ if (!subcommand || isHelpSubcommand(subcommand)) {
124
+ assertNoExtraArgs(proArgs, "pro help", 0);
125
+ printProHelp(io.stdout);
126
+ return 0;
127
+ }
128
+ if (subcommand === "ask") {
129
+ if (printHelpIfRequested(proArgs, "pro ask", io.stdout, printProHelp, {
130
+ valueFlags: [...ASK_PRO_PREVIEW_VALUE_FLAGS],
131
+ booleanFlags: [...ASK_PRO_BOOLEAN_FLAGS]
132
+ })) {
133
+ return 0;
134
+ }
135
+ parseAskProArgs(proArgs, ASK_PRO_PREVIEW_VALUE_FLAGS);
136
+ if (hasAskProSendMode(proArgs)) {
137
+ throw new Error("prodex pro ask is a dry-run preview. Use `prodex pro browser ask` for visible-browser sends.");
138
+ }
139
+ const hasDryRun = hasAskProDryRunMode(proArgs);
140
+ return runCliFn(["ask-pro", ...(hasDryRun ? [] : ["--dry-run"]), ...proArgs], io);
141
+ }
142
+ if (subcommand === "browser") {
143
+ const [browserSubcommand, ...browserArgs] = proArgs;
144
+ if (!browserSubcommand || isHelpSubcommand(browserSubcommand)) {
145
+ assertOnlyOptions(browserArgs, "pro browser help", ["--source-cli"]);
146
+ const sourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
147
+ printProBrowserHelp(io.stdout, sourceCli);
148
+ return 0;
149
+ }
150
+ if (browserSubcommand === "login") {
151
+ if (printProBrowserHelpIfRequested(browserArgs, "pro browser login", io, {
152
+ valueFlags: ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms"],
153
+ booleanFlags: ["--dry-run"]
154
+ })) {
155
+ return 0;
156
+ }
157
+ assertOnlyOptions(browserArgs, "pro browser login", ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms"], ["--dry-run"]);
158
+ const loginUrl = readChatGptBrowserUrlFlag(browserArgs);
159
+ const sourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
160
+ const targetCwd = readFlag(browserArgs, "--cwd") ? resolveCwdFlag(io.cwd, browserArgs) : undefined;
161
+ const profileDir = readFlag(browserArgs, "--profile-dir");
162
+ const port = readPortFlag(browserArgs, "--port") ?? 9333;
163
+ const launchTimeoutMs = readPositiveNumberFlag(browserArgs, "--launch-timeout-ms");
164
+ const commandOptions = {
165
+ ...(targetCwd ? { cwd: targetCwd } : {}),
166
+ ...(profileDir ? { profileDir } : {}),
167
+ ...(port !== 9333 ? { port } : {}),
168
+ ...(readFlag(browserArgs, "--url") ? { url: loginUrl } : {}),
169
+ ...(launchTimeoutMs !== undefined ? { launchTimeoutMs } : {})
170
+ };
171
+ if (browserArgs.includes("--dry-run")) {
172
+ printBrowserLoginGuide(io.stdout, {
173
+ opened: false,
174
+ loginUrl,
175
+ profileDir: profileDir ?? defaultChatGptProfileDir(),
176
+ port,
177
+ sourceCli,
178
+ commandOptions
179
+ });
180
+ return 0;
181
+ }
182
+ const opened = openChatGptBrowser({
183
+ port,
184
+ profileDir,
185
+ url: loginUrl
186
+ });
187
+ await assertBrowserLaunchStayedAlive(opened, launchTimeoutMs);
188
+ printBrowserLoginGuide(io.stdout, {
189
+ opened: true,
190
+ loginUrl,
191
+ profileDir: opened.profileDir,
192
+ port: opened.port,
193
+ sourceCli,
194
+ commandOptions
195
+ });
196
+ return 0;
197
+ }
198
+ if (browserSubcommand === "ask") {
199
+ if (printProBrowserHelpIfRequested(browserArgs, "pro browser ask", io, {
200
+ valueFlags: [...ASK_PRO_VALUE_FLAGS],
201
+ booleanFlags: [...ASK_PRO_BOOLEAN_FLAGS]
202
+ })) {
203
+ return 0;
204
+ }
205
+ if (hasAskProDryRunMode(browserArgs) && hasAskProSendMode(browserArgs)) {
206
+ throw new Error("ask-pro cannot combine --dry-run and --send");
207
+ }
208
+ if (hasAskProDryRunMode(browserArgs)) {
209
+ throw new Error("prodex pro browser ask is an explicit visible-browser send. Use `prodex pro ask` for dry-run previews.");
210
+ }
211
+ const hasMode = hasAskProMode(browserArgs);
212
+ return runCliFn(["ask-pro", ...(hasMode ? [] : ["--send"]), ...browserArgs], { ...io, allowAskProBrowserSend: true });
213
+ }
214
+ if (browserSubcommand === "open" || browserSubcommand === "status" || browserSubcommand === "doctor") {
215
+ const replacement = browserSubcommand === "open" ? "login" : "check";
216
+ throw new Error(`Use \`prodex pro browser ${replacement}\` for explicit browser automation.`);
217
+ }
218
+ if (browserSubcommand === "smoke") {
219
+ if (printProBrowserHelpIfRequested(browserArgs, "pro browser smoke", io, { valueFlags: ["--cwd", "--port", "--timeout-ms", "--source-cli"] }))
220
+ return 0;
221
+ return runCliFn(["chatgpt", browserSubcommand, ...browserArgs], io);
222
+ }
223
+ if (browserSubcommand === "check") {
224
+ if (printProBrowserHelpIfRequested(browserArgs, "pro browser check", io, { valueFlags: ["--cwd", "--port", "--timeout-ms", "--source-cli"] }))
225
+ return 0;
226
+ assertOnlyOptions(browserArgs, "pro browser check", ["--cwd", "--port", "--timeout-ms", "--source-cli"]);
227
+ const targetCwd = resolveCwdFlag(io.cwd, browserArgs);
228
+ readPortFlag(browserArgs, "--port");
229
+ readPositiveNumberFlag(browserArgs, "--timeout-ms");
230
+ const healthy = await printProductCheck(new BridgeStore(targetCwd), io, browserArgs, targetCwd);
231
+ return healthy ? 0 : 1;
232
+ }
233
+ if (browserSubcommand === "models") {
234
+ if (printProBrowserHelpIfRequested(browserArgs, "pro browser models", io, { valueFlags: ["--port", "--timeout-ms", "--source-cli"] }))
235
+ return 0;
236
+ assertOnlyOptions(browserArgs, "pro browser models", ["--port", "--timeout-ms", "--source-cli"]);
237
+ const listed = await listChatGptModelOptions({
238
+ port: readPortFlag(browserArgs, "--port"),
239
+ timeoutMs: readPositiveNumberFlag(browserArgs, "--timeout-ms")
240
+ });
241
+ io.stdout("Model menu options in the visible ChatGPT tab (read-only; nothing was selected):");
242
+ for (const option of listed.options) {
243
+ const marker = option.checked ? "*" : " ";
244
+ const suffix = option.kind === "submenu" ? " (has sub-variants; not selectable via --model yet)" : "";
245
+ io.stdout(`${marker} ${option.label}${suffix}`);
246
+ }
247
+ io.stdout("Use radio entries with `pro browser ask --model/--effort`; Pro sub-modes via --pro-mode 기본|확장.");
248
+ return 0;
249
+ }
250
+ throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check", "models"]);
251
+ }
252
+ if (subcommand === "open" || subcommand === "status" || subcommand === "smoke" || subcommand === "check" || subcommand === "doctor") {
253
+ throw new Error(`Use \`prodex pro browser ${subcommand === "doctor" ? "check" : subcommand}\` for explicit browser automation.`);
254
+ }
255
+ if (subcommand === "list") {
256
+ if (printHelpIfRequested(proArgs, "pro list", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
257
+ return 0;
258
+ assertOnlyOptions(proArgs, "pro list", ["--cwd", "--source-cli"]);
259
+ const targetCwd = resolveCwdFlag(io.cwd, proArgs);
260
+ const targetStore = new BridgeStore(targetCwd);
261
+ const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
262
+ const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
263
+ const consults = await listConsultListEntries(targetStore);
264
+ for (const entry of consults) {
265
+ if (entry.kind === "untrusted") {
266
+ io.stdout(`${entry.task.id}\tuntrusted\t${sourceAwareResultMessage(errorMessage(entry.error), sourceCli, answerOptions)}`);
267
+ }
268
+ else {
269
+ io.stdout(`${entry.consult.task.id}\t${entry.consult.result.status}\t${formatProListSummary(entry.consult, sourceCli, answerOptions)}`);
270
+ }
271
+ }
272
+ return 0;
273
+ }
274
+ if (subcommand === "latest") {
275
+ if (printHelpIfRequested(proArgs, "pro latest", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
276
+ return 0;
277
+ assertOnlyOptions(proArgs, "pro latest", ["--cwd", "--source-cli"]);
278
+ const targetCwd = resolveCwdFlag(io.cwd, proArgs);
279
+ const targetStore = new BridgeStore(targetCwd);
280
+ const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
281
+ const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
282
+ let consult;
283
+ try {
284
+ consult = await latestTrustedConsult(targetStore);
285
+ }
286
+ catch (error) {
287
+ throw sourceAwareResultError(error, sourceCli, answerOptions);
288
+ }
289
+ if (!consult)
290
+ throw new Error("No GPT Pro answers found");
291
+ io.stdout(formatProAnswer(consult, sourceCli, answerOptions));
292
+ return 0;
293
+ }
294
+ if (subcommand === "show") {
295
+ if (printHelpIfRequested(proArgs, "pro show", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"], maxPositionals: 1 }))
296
+ return 0;
297
+ const [taskId] = readPositionalsWithOptions(proArgs, "pro show", 1, ["--cwd", "--source-cli"]);
298
+ if (!taskId)
299
+ throw new Error("pro show requires <task-id|latest>");
300
+ const targetCwd = resolveCwdFlag(io.cwd, proArgs);
301
+ const targetStore = new BridgeStore(targetCwd);
302
+ const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
303
+ const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
304
+ let consult;
305
+ try {
306
+ consult = taskId === "latest" ? await latestTrustedConsult(targetStore) : await getConsult(targetStore, taskId, { readOnly: true });
307
+ }
308
+ catch (error) {
309
+ throw sourceAwareResultError(error, sourceCli, answerOptions);
310
+ }
311
+ if (!consult)
312
+ throw new Error(taskId === "latest" ? "No GPT Pro answers found" : `GPT Pro answer not found: ${taskId}`);
313
+ io.stdout(formatProAnswer(consult, sourceCli, answerOptions));
314
+ return 0;
315
+ }
316
+ throw unknownSubcommandError("pro", subcommand, ["ask", "browser", "list", "latest", "show"]);
317
+ }
318
+ export async function runConsultsCommand(rest, io) {
319
+ throw new Error("The legacy `consults` alias is retired. Use `prodex pro list`, `prodex pro latest`, or `prodex pro show <task-id|latest>`.");
320
+ }
321
+ export async function runAskProCommand(rest, io) {
322
+ const parsedAskPro = parseAskProArgs(rest);
323
+ const hasDryRunMode = parsedAskPro.optionArgs.includes("--dry-run");
324
+ const hasSendMode = parsedAskPro.optionArgs.includes("--send");
325
+ if (!hasDryRunMode && !hasSendMode) {
326
+ throw new Error("ask-pro requires --dry-run or --send");
327
+ }
328
+ if (hasDryRunMode && hasSendMode) {
329
+ throw new Error("ask-pro cannot combine --dry-run and --send");
330
+ }
331
+ if (hasSendMode && !io.allowAskProBrowserSend) {
332
+ throw new Error("Direct ask-pro --send is disabled. Use `prodex pro browser ask` for explicit visible-browser sends.");
333
+ }
334
+ const targetCwd = resolveCwdFlag(io.cwd, parsedAskPro.optionArgs);
335
+ const targetStore = new BridgeStore(targetCwd);
336
+ const files = readRepeatedFlag(parsedAskPro.optionArgs, "--file");
337
+ const targetUrl = readFlag(parsedAskPro.optionArgs, "--target-url");
338
+ const normalizedTargetUrl = targetUrl ? normalizeChatGptTargetUrl(targetUrl) : undefined;
339
+ if (!normalizedTargetUrl && parsedAskPro.optionArgs.includes("--confirm-target")) {
340
+ throw new Error("--confirm-target requires --target-url so the visible browser target is explicit.");
341
+ }
342
+ if (normalizedTargetUrl && hasSendMode && !parsedAskPro.optionArgs.includes("--confirm-target")) {
343
+ throw new Error("--target-url requires --confirm-target after you manually verify the visible ChatGPT tab is the intended Project/thread.");
344
+ }
345
+ const prompt = parsedAskPro.promptParts.join(" ").trim();
346
+ if (!prompt)
347
+ throw new Error("ask-pro requires a prompt");
348
+ const browserDefaults = await loadBrowserDefaults(targetCwd);
349
+ const explicitProject = readFlag(parsedAskPro.optionArgs, "--project");
350
+ const explicitProjectNew = readFlag(parsedAskPro.optionArgs, "--project-new");
351
+ if (explicitProject !== undefined && explicitProjectNew !== undefined) {
352
+ throw new Error("ask-pro cannot combine --project and --project-new; pick an existing project or create one.");
353
+ }
354
+ if (normalizedTargetUrl && (explicitProject !== undefined || explicitProjectNew !== undefined)) {
355
+ throw new Error("ask-pro cannot combine --target-url with --project/--project-new: --target-url pins the confirmed tab while the project step navigates the sidebar away from it. Open the project thread in the browser and pass its URL as --target-url instead.");
356
+ }
357
+ const explicitModel = readFlag(parsedAskPro.optionArgs, "--model");
358
+ const explicitProModeRaw = readFlag(parsedAskPro.optionArgs, "--pro-mode");
359
+ const explicitEffortRaw = readFlag(parsedAskPro.optionArgs, "--effort");
360
+ if (explicitProModeRaw !== undefined && explicitEffortRaw !== undefined) {
361
+ throw new Error("ask-pro cannot combine --pro-mode and --effort; Pro sub-modes and reasoning effort are different model axes.");
362
+ }
363
+ const explicitProMode = explicitProModeRaw === undefined ? undefined : parseProMode(explicitProModeRaw);
364
+ const explicitEffort = explicitEffortRaw === undefined ? undefined : parseReasoningEffort(explicitEffortRaw);
365
+ // Explicit per-ask flags override persisted defaults. Choosing either
366
+ // reasoning axis explicitly suppresses the default for the other axis, and
367
+ // pinning --target-url suppresses a default project (it would navigate away
368
+ // from the confirmed tab).
369
+ const selectionModel = explicitModel ?? browserDefaults?.model;
370
+ const selectionProjectNew = explicitProjectNew;
371
+ const selectionProject = explicitProject ?? (normalizedTargetUrl || selectionProjectNew !== undefined ? undefined : browserDefaults?.project);
372
+ const reasoningAxisChosen = explicitProMode !== undefined || explicitEffort !== undefined;
373
+ const selectionProMode = explicitProMode ?? (reasoningAxisChosen ? undefined : browserDefaults?.pro_mode);
374
+ const selectionEffort = explicitEffort ?? (reasoningAxisChosen ? undefined : browserDefaults?.effort);
375
+ const selectionMetadata = {
376
+ ...(selectionProject ? { project: selectionProject } : {}),
377
+ ...(selectionProjectNew ? { project_new: selectionProjectNew } : {}),
378
+ ...(selectionModel ? { model: selectionModel } : {}),
379
+ ...(selectionProMode ? { pro_mode: selectionProMode } : {}),
380
+ ...(selectionEffort ? { effort: selectionEffort } : {})
381
+ };
382
+ const browserPort = hasSendMode ? (readPortFlag(parsedAskPro.optionArgs, "--port") ?? 9333) : undefined;
383
+ // Pro extended can legitimately think for minutes, so its default timeout is
384
+ // higher; an explicit --timeout-ms always wins.
385
+ const defaultBrowserTimeoutMs = selectionProMode === "확장" ? 300_000 : 90_000;
386
+ const browserTimeoutMs = hasSendMode
387
+ ? (readPositiveNumberFlag(parsedAskPro.optionArgs, "--timeout-ms") ?? defaultBrowserTimeoutMs)
388
+ : undefined;
389
+ const sourceCli = resolveOptionalFileFlag(io.cwd, parsedAskPro.optionArgs, "--source-cli");
390
+ const bundle = await buildDryRunBundle(targetCwd, { prompt, files });
391
+ if (hasSendMode) {
392
+ const browserCommandOptions = {
393
+ cwd: targetCwd,
394
+ port: parsedAskPro.optionArgs.includes("--port") ? browserPort : undefined
395
+ };
396
+ const task = await targetStore.createTask({
397
+ source: "codex",
398
+ title: "GPT Pro consult",
399
+ prompt: bundle.text,
400
+ repo_id: "default",
401
+ files: files.map((file) => ({ path: file, role: "context" })),
402
+ provenance: {
403
+ adapter: "chatgpt-control",
404
+ session_id: bundle.id,
405
+ thread: normalizedTargetUrl,
406
+ warnings: []
407
+ }
408
+ });
409
+ await targetStore.claimTask(task.id, "chatgpt-pro");
410
+ try {
411
+ await writeSessionBeforeBrowserSend(targetStore, {
412
+ id: bundle.id,
413
+ direction: "codex_to_chatgpt",
414
+ backend: "chatgpt-control",
415
+ task_id: task.id,
416
+ thread: normalizedTargetUrl,
417
+ status: "running",
418
+ warnings: []
419
+ });
420
+ }
421
+ catch (error) {
422
+ const blocker = {
423
+ code: "session_record_failed",
424
+ message: `Could not record ChatGPT browser session before send: ${errorMessage(error)}`,
425
+ retryable: true,
426
+ next_step: "Fix local .bridge write permissions, then rerun the consult."
427
+ };
428
+ try {
429
+ await targetStore.completeTask(task.id, {
430
+ status: "blocked",
431
+ summary: blocker.message,
432
+ commands: ["visible ChatGPT browser consult"],
433
+ blocker
434
+ });
435
+ }
436
+ catch (recordError) {
437
+ throw new Error(`${blocker.message} (also failed to record blocked consult: ${errorMessage(recordError)})`);
438
+ }
439
+ throw new Error(formatBlockedConsultRecordedMessage(blocker.message, task.id, sourceCli, { cwd: targetCwd }));
440
+ }
441
+ let consult;
442
+ try {
443
+ consult = await sendChatGptPrompt({
444
+ port: browserPort,
445
+ prompt: bundle.text,
446
+ targetUrl: normalizedTargetUrl,
447
+ timeoutMs: browserTimeoutMs,
448
+ project: selectionProject,
449
+ projectNew: selectionProjectNew,
450
+ model: selectionModel,
451
+ proMode: selectionProMode,
452
+ effort: selectionEffort
453
+ });
454
+ }
455
+ catch (error) {
456
+ const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, browserCommandOptions);
457
+ const message = blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error);
458
+ try {
459
+ await targetStore.completeTask(task.id, {
460
+ status: "blocked",
461
+ summary: message,
462
+ commands: ["visible ChatGPT browser consult"],
463
+ blocker
464
+ });
465
+ await writeSessionBestEffort(targetStore, {
466
+ id: bundle.id,
467
+ direction: "codex_to_chatgpt",
468
+ backend: "chatgpt-control",
469
+ task_id: task.id,
470
+ thread: normalizedTargetUrl,
471
+ status: "blocked",
472
+ blocker,
473
+ warnings: []
474
+ }, io);
475
+ }
476
+ catch (recordError) {
477
+ throw new Error(`${message} (also failed to record blocked consult: ${errorMessage(recordError)})`);
478
+ }
479
+ throw new Error(formatBlockedConsultRecordedMessage(message, task.id, sourceCli, { cwd: targetCwd }));
480
+ }
481
+ const answerArtifactText = formatProConsultArtifact(consult);
482
+ const persistenceWarnings = [...consult.warnings];
483
+ let answerArtifactPath;
484
+ const answerArtifactBytes = Buffer.byteLength(answerArtifactText, "utf8");
485
+ if (answerArtifactBytes > MAX_FETCHABLE_RESULT_ARTIFACT_BYTES) {
486
+ const warning = `answer_artifact_warning: answer artifact is too large for bridge_fetch_result_artifact (${answerArtifactBytes} bytes > ${MAX_FETCHABLE_RESULT_ARTIFACT_BYTES} bytes); saved answer in result summary only`;
487
+ persistenceWarnings.push(warning);
488
+ io.stderr(warning);
489
+ }
490
+ else {
491
+ try {
492
+ answerArtifactPath = await targetStore.writeArtifactText(`.bridge/artifacts/pro-consults/${task.id}.md`, answerArtifactText);
493
+ }
494
+ catch (error) {
495
+ const warning = `answer_artifact_warning: ${errorMessage(error)}`;
496
+ persistenceWarnings.push(warning);
497
+ io.stderr(warning);
498
+ }
499
+ }
500
+ try {
501
+ await targetStore.writeReceipt({
502
+ kind: "consult_answer_saved",
503
+ task_id: task.id,
504
+ session_id: bundle.id,
505
+ summary: `Recorded ChatGPT answer for ${task.id}`,
506
+ metadata: {
507
+ ...(answerArtifactPath ? { artifact_path: answerArtifactPath } : {}),
508
+ thread: consult.url,
509
+ ...(Object.keys(selectionMetadata).length > 0 ? { selection: selectionMetadata } : {}),
510
+ warnings: persistenceWarnings
511
+ }
512
+ });
513
+ }
514
+ catch (error) {
515
+ const warning = `receipt_record_warning: ${errorMessage(error)}`;
516
+ persistenceWarnings.push(warning);
517
+ io.stderr(warning);
518
+ }
519
+ let result;
520
+ try {
521
+ result = await targetStore.completeTask(task.id, {
522
+ status: "done",
523
+ summary: consult.answer,
524
+ artifacts: answerArtifactPath ? [{ path: answerArtifactPath, role: "result", bytes: Buffer.byteLength(answerArtifactText, "utf8") }] : [],
525
+ commands: ["visible ChatGPT browser consult"],
526
+ warnings: persistenceWarnings,
527
+ provenance: {
528
+ thread: consult.url,
529
+ warnings: persistenceWarnings
530
+ }
531
+ });
532
+ }
533
+ catch (error) {
534
+ io.stdout(`consult_answer_received_but_not_saved: ${task.id} ${consult.url}`);
535
+ io.stdout("");
536
+ io.stdout(consult.answer);
537
+ throw new Error(`ChatGPT answer was received but local persistence failed: ${errorMessage(error)}`);
538
+ }
539
+ await writeSessionBestEffort(targetStore, {
540
+ id: bundle.id,
541
+ direction: "codex_to_chatgpt",
542
+ backend: "chatgpt-control",
543
+ task_id: task.id,
544
+ thread: consult.url,
545
+ status: "done",
546
+ warnings: persistenceWarnings
547
+ }, io);
548
+ io.stdout(`${result.task_id}\t${result.status}\t${consult.url}`);
549
+ io.stdout("");
550
+ io.stdout(result.summary);
551
+ }
552
+ else {
553
+ await writeSessionBestEffort(targetStore, {
554
+ id: bundle.id,
555
+ direction: "codex_to_chatgpt",
556
+ backend: "manual",
557
+ status: "preview",
558
+ warnings: []
559
+ }, io);
560
+ await targetStore.writeReceipt({
561
+ kind: "consult_preview",
562
+ session_id: bundle.id,
563
+ summary: `Created dry-run consult preview ${bundle.id}`
564
+ });
565
+ io.stdout(`DRY RUN ${bundle.id}`);
566
+ io.stdout(bundle.text);
567
+ }
568
+ return 0;
569
+ }
570
+ export const PRO_BROWSER_SMOKE_TOKEN = "PRODEX_PRO_SMOKE_OK";
571
+ export async function assertBrowserLaunchStayedAlive(opened, timeoutMs) {
572
+ const outcome = await waitForBrowserLaunchReady(opened, timeoutMs);
573
+ if (outcome.reachable)
574
+ return;
575
+ if (outcome.earlyExit) {
576
+ const detail = formatBrowserEarlyExit(outcome.earlyExit);
577
+ throw new Error(`Chrome/Chromium exited before DevTools became reachable (${detail}). Check the visible browser environment, profile lock, display access, or PRODEX_CHROME, then retry.`);
578
+ }
579
+ throw new Error(`Chrome/Chromium did not expose a reachable DevTools endpoint after launch. Check the visible browser environment, profile lock, display access, or PRODEX_CHROME, then retry.`);
580
+ }
581
+ export function browserSendBlockerFromError(error) {
582
+ const blocker = typeof error === "object" && error !== null && "blocker" in error ? error.blocker : undefined;
583
+ if (typeof blocker === "object" &&
584
+ blocker !== null &&
585
+ "code" in blocker &&
586
+ "message" in blocker &&
587
+ "retryable" in blocker &&
588
+ typeof blocker.code === "string" &&
589
+ typeof blocker.message === "string" &&
590
+ typeof blocker.retryable === "boolean") {
591
+ return {
592
+ code: blocker.code,
593
+ message: blocker.message,
594
+ retryable: blocker.retryable,
595
+ ...("next_step" in blocker && typeof blocker.next_step === "string" ? { next_step: blocker.next_step } : {})
596
+ };
597
+ }
598
+ const message = errorMessage(error);
599
+ return {
600
+ code: "browser_send_failed",
601
+ message,
602
+ retryable: true,
603
+ next_step: "Resolve the visible browser issue manually, then rerun the consult if needed."
604
+ };
605
+ }
606
+ export function formatProAnswer(consult, sourceCli, options = {}) {
607
+ const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
608
+ const summary = sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker);
609
+ const lines = [
610
+ `task_id: ${consult.task.id}`,
611
+ `status: ${consult.result.status}`,
612
+ consult.task.provenance.thread ? `thread: ${consult.task.provenance.thread}` : undefined,
613
+ `created_at: ${consult.result.created_at}`,
614
+ "",
615
+ summary
616
+ ].filter((line) => line !== undefined);
617
+ if (blocker) {
618
+ lines.push("", "blocker:", `- code: ${blocker.code}`, `- retryable: ${blocker.retryable}`);
619
+ if (blocker.next_step)
620
+ lines.push(`- next_step: ${blocker.next_step}`);
621
+ }
622
+ if (consult.result.warnings.length > 0) {
623
+ lines.push("", "warnings:");
624
+ for (const warning of consult.result.warnings)
625
+ lines.push(`- ${warning}`);
626
+ }
627
+ return lines.join("\n");
628
+ }
629
+ export function formatProConsultArtifact(consult) {
630
+ const lines = [`# ChatGPT Pro Consult`, "", `Thread: ${consult.url}`, `Title: ${consult.title}`, ""];
631
+ if (consult.modelHints.length > 0) {
632
+ lines.push("Model hints:", ...consult.modelHints.map((hint) => `- ${hint}`), "");
633
+ }
634
+ if (consult.warnings.length > 0) {
635
+ lines.push("Warnings:", ...consult.warnings.map((warning) => `- ${warning}`), "");
636
+ }
637
+ lines.push("## Answer", "", consult.answer.trim(), "");
638
+ return lines.join("\n");
639
+ }
640
+ export function formatProListSummary(consult, sourceCli, options = {}) {
641
+ const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
642
+ return firstLine(sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker));
643
+ }
644
+ export async function getConsult(store, taskId, options = {}) {
645
+ let task;
646
+ try {
647
+ task = options.readOnly ? await store.getTaskReadOnly(taskId) : await store.getTask(taskId);
648
+ }
649
+ catch (error) {
650
+ if (isMissingFileError(error))
651
+ return undefined;
652
+ throw error;
653
+ }
654
+ if (!isConsultTask(task))
655
+ return undefined;
656
+ let result;
657
+ try {
658
+ result = await store.getFinalizedResultReadOnly(taskId);
659
+ }
660
+ catch (error) {
661
+ if (isMissingFileError(error)) {
662
+ if (isTerminalTask(task) && isConsultTask(task))
663
+ throw missingConsultResultError(task);
664
+ return undefined;
665
+ }
666
+ throw error;
667
+ }
668
+ if (!task || !result)
669
+ return undefined;
670
+ const record = { task, result };
671
+ return isConsultRecord(record) ? record : undefined;
672
+ }
673
+ export async function latestTrustedConsult(store, options = { readOnly: true }) {
674
+ const entries = await listConsultListEntries(store, options);
675
+ const trusted = entries.find((entry) => entry.kind === "trusted");
676
+ if (trusted)
677
+ return trusted.consult;
678
+ const untrusted = entries.find((entry) => entry.kind === "untrusted");
679
+ if (untrusted)
680
+ throw untrusted.error;
681
+ return undefined;
682
+ }
683
+ export function legacyChatGptNamespaceError(subcommand) {
684
+ const prefix = subcommand ? `Unknown legacy chatgpt subcommand: ${subcommand}.` : "The legacy `chatgpt` namespace is hidden.";
685
+ return new Error(`${prefix} Use \`prodex pro browser help\` for visible-browser commands.`);
686
+ }
687
+ export async function listConsultListEntries(store, options = { readOnly: true }) {
688
+ const [tasks, results] = options.readOnly === false
689
+ ? await Promise.all([store.listTasks(), store.listResults()])
690
+ : await Promise.all([listTasksForInspection(store), listRawResultsForInspection(store)]);
691
+ const tasksById = new Map(tasks.map((task) => [task.id, task]));
692
+ assertNoMissingTerminalConsultResults(tasks, results);
693
+ assertNoOrphanConsultResults(tasksById, results);
694
+ const records = results
695
+ .map((result) => {
696
+ const task = tasksById.get(result.task_id);
697
+ return task ? { task, result } : undefined;
698
+ })
699
+ .filter((record) => Boolean(record && isConsultRecord(record)))
700
+ .sort((a, b) => b.result.created_at.localeCompare(a.result.created_at));
701
+ const entries = [];
702
+ for (const record of records) {
703
+ try {
704
+ entries.push({ kind: "trusted", consult: { ...record, result: await store.getFinalizedResultReadOnly(record.result.task_id) } });
705
+ }
706
+ catch (error) {
707
+ if (isUntrustedResultError(error)) {
708
+ entries.push({ kind: "untrusted", task: record.task, result: record.result, error });
709
+ continue;
710
+ }
711
+ throw error;
712
+ }
713
+ }
714
+ return entries;
715
+ }
716
+ export function printBrowserLoginGuide(stdout, input) {
717
+ const loginCommand = formatBrowserLoginCommand(input.sourceCli, input.commandOptions);
718
+ const runtimeCommandOptions = {
719
+ ...(input.commandOptions?.cwd ? { cwd: input.commandOptions.cwd } : {}),
720
+ ...(input.commandOptions?.port ? { port: input.commandOptions.port } : {})
721
+ };
722
+ const checkCommand = formatBrowserCheckCommand(input.sourceCli, runtimeCommandOptions);
723
+ const smokeCommand = formatBrowserSmokeCommand(input.sourceCli, runtimeCommandOptions);
724
+ stdout("ChatGPT Pro browser login");
725
+ stdout(input.opened ? "Opened the dedicated Chrome window for ChatGPT." : "Dry run: no browser was opened.");
726
+ stdout("");
727
+ stdout("Steps:");
728
+ if (input.opened) {
729
+ stdout(`1. Log in manually at ${input.loginUrl} in the dedicated Chrome window.`);
730
+ stdout("2. If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, complete it in the browser.");
731
+ stdout("3. For usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.");
732
+ stdout("4. Open a normal ChatGPT chat or the intended Project/thread so the prompt composer is visible.");
733
+ stdout("5. Select the Pro/Thinking model you want in the ChatGPT UI.");
734
+ stdout(`6. Run \`${checkCommand}\` to confirm the session is reachable.`);
735
+ stdout(`7. Run \`${smokeCommand}\` to verify a real Pro response path.`);
736
+ }
737
+ else {
738
+ stdout(`1. Run \`${loginCommand}\` without \`--dry-run\` to open the dedicated Chrome window.`);
739
+ stdout(`2. Log in manually at ${input.loginUrl} in that Chrome window.`);
740
+ stdout("3. If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, complete it in the browser.");
741
+ stdout("4. For usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.");
742
+ stdout("5. Open a normal ChatGPT chat or the intended Project/thread so the prompt composer is visible.");
743
+ stdout("6. Select the Pro/Thinking model you want in the ChatGPT UI.");
744
+ stdout(`7. Run \`${checkCommand}\` to confirm the session is reachable.`);
745
+ stdout(`8. Run \`${smokeCommand}\` to verify a real Pro response path.`);
746
+ }
747
+ stdout("");
748
+ stdout(`Profile: ${input.profileDir}`);
749
+ stdout(`Debug: http://127.0.0.1:${input.port}`);
750
+ if (input.opened) {
751
+ stdout("You can close this Chrome window after check/smoke or when you are done. The dedicated profile is reused next time.");
752
+ }
753
+ else {
754
+ stdout("The dedicated profile path above will be reused by the real login command.");
755
+ }
756
+ }
757
+ export function printProBrowserHelpIfRequested(args, command, io, options) {
758
+ const helpIndex = findHelpFlagIndexBeforePromptDelimiter(args);
759
+ if (helpIndex === -1)
760
+ return false;
761
+ assertHelpRequestArgs(args, command, options);
762
+ printProBrowserHelp(io.stdout, resolveOptionalFileFlag(io.cwd, args, "--source-cli"));
763
+ return true;
764
+ }
765
+ export async function printProductCheck(store, io, args, configCwd = io.cwd) {
766
+ const sourceCli = resolveOptionalFileFlag(io.cwd, args, "--source-cli");
767
+ const setupHintCwd = readFlag(args, "--cwd") ? configCwd : undefined;
768
+ io.stdout("prodex product check");
769
+ let bridgeReady = false;
770
+ try {
771
+ bridgeReady = await store.hasReadyBridgeStorageReadOnly();
772
+ io.stdout(bridgeReady
773
+ ? "bridge: ok (.bridge)"
774
+ : `bridge: missing (.bridge) - run \`${formatInitCommand(sourceCli, { cwd: setupHintCwd })}\` when you need local task/result storage`);
775
+ }
776
+ catch (error) {
777
+ io.stdout(`bridge: blocked - ${errorMessage(error)}`);
778
+ }
779
+ let configReady = false;
780
+ try {
781
+ const config = await loadLocalConfig(configCwd);
782
+ const tokenStatus = getTokenExpiryStatus(config);
783
+ if (tokenStatus.status === "expired") {
784
+ io.stdout(`config: expired - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\``);
785
+ }
786
+ else {
787
+ io.stdout(`config: ok ${redactServerUrl(config.server_url)} token_status=${tokenStatus.status}`);
788
+ const warningLine = formatConfigWarningLine(tokenStatus, sourceCli, setupHintCwd);
789
+ if (warningLine)
790
+ io.stdout(warningLine);
791
+ configReady = true;
792
+ }
793
+ }
794
+ catch (error) {
795
+ if (isMissingFileError(error)) {
796
+ io.stdout(`config: missing - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\``);
797
+ }
798
+ else {
799
+ io.stdout(`config: failed ${sourceAwareSetupMessage(errorMessage(error), sourceCli, { cwd: setupHintCwd })}`);
800
+ }
801
+ }
802
+ const browserStatus = await getChatGptBrowserStatus({
803
+ port: readPortFlag(args, "--port") ?? 9333,
804
+ timeoutMs: readPositiveNumberFlag(args, "--timeout-ms") ?? 1500
805
+ });
806
+ const browserCommandOptions = {
807
+ cwd: setupHintCwd,
808
+ port: readPortFlag(args, "--port") ?? undefined
809
+ };
810
+ let chatgptReady = false;
811
+ const visibilityBlocker = chatGptVisibilityBlocker(browserStatus.visibilityState, browserStatus.url);
812
+ if (!browserStatus.reachable) {
813
+ io.stdout(`chatgpt: ${browserStatus.blocker?.code ?? "unreachable"} - ${browserStatus.blocker?.message ?? "browser is not reachable"}`);
814
+ const nextStep = productCheckBrowserNextStep(browserStatus.blocker?.next_step, sourceCli, browserCommandOptions);
815
+ if (nextStep)
816
+ io.stdout(`next: ${nextStep}`);
817
+ }
818
+ else if (browserStatus.blocker) {
819
+ const visibilityText = browserStatus.blocker.code === "tab_not_visible" ? ` visibility=${browserStatus.visibilityState ?? "unknown"}` : "";
820
+ io.stdout(`chatgpt: blocked ${browserStatus.blocker.code}${visibilityText} - ${browserStatus.blocker.message}`);
821
+ const nextStep = productCheckBrowserNextStep(browserStatus.blocker.next_step, sourceCli, browserCommandOptions);
822
+ if (nextStep)
823
+ io.stdout(`next: ${nextStep}`);
824
+ }
825
+ else if (visibilityBlocker) {
826
+ io.stdout(`chatgpt: blocked ${visibilityBlocker.code} visibility=${browserStatus.visibilityState ?? "unknown"} - ${visibilityBlocker.message}`);
827
+ const nextStep = productCheckBrowserNextStep(visibilityBlocker.next_step, sourceCli, browserCommandOptions);
828
+ if (nextStep)
829
+ io.stdout(`next: ${nextStep}`);
830
+ }
831
+ else if (browserStatus.loggedInLikely && browserStatus.hasComposer) {
832
+ io.stdout(`chatgpt: ok logged_in=true composer=true${browserStatus.url ? ` url=${browserStatus.url}` : ""}`);
833
+ chatgptReady = true;
834
+ }
835
+ else {
836
+ io.stdout(`chatgpt: blocked logged_in=${browserStatus.loggedInLikely} composer=${browserStatus.hasComposer}`);
837
+ const nextStep = productCheckBrowserNextStep(browserReadinessNextStep(browserStatus), sourceCli, browserCommandOptions);
838
+ io.stdout(`next: ${nextStep}`);
839
+ }
840
+ const modelHints = formatBrowserModelHints(browserStatus.modelHints);
841
+ if (modelHints)
842
+ io.stdout(`model_hints: ${modelHints}`);
843
+ if (bridgeReady) {
844
+ try {
845
+ const latest = await latestTrustedConsult(store, { readOnly: false });
846
+ if (latest) {
847
+ for (const line of formatProductCheckLatestProLines(latest, sourceCli, browserCommandOptions))
848
+ io.stdout(line);
849
+ }
850
+ else {
851
+ io.stdout("latest_pro: missing");
852
+ }
853
+ }
854
+ catch (error) {
855
+ if (isUntrustedResultError(error)) {
856
+ io.stdout(`latest_pro: untrusted ${error.taskId} ${sourceAwareResultMessage(errorMessage(error), sourceCli, browserCommandOptions)}`);
857
+ }
858
+ else {
859
+ io.stdout(`latest_pro: unavailable ${firstLine(errorMessage(error))}`);
860
+ }
861
+ }
862
+ }
863
+ else {
864
+ io.stdout("latest_pro: missing");
865
+ }
866
+ return bridgeReady && configReady && chatgptReady;
867
+ }
868
+ export function readChatGptBrowserUrlFlag(args) {
869
+ return normalizeChatGptTargetUrl(readFlag(args, "--url") ?? "https://chatgpt.com/");
870
+ }
871
+ export async function writeSessionBeforeBrowserSend(store, input) {
872
+ try {
873
+ await store.writeSession(input);
874
+ }
875
+ catch (error) {
876
+ throw new Error(`failed to record running consult session before browser send: ${errorMessage(error)}`);
877
+ }
878
+ }
879
+ export async function writeSessionBestEffort(store, input, io) {
880
+ try {
881
+ await store.writeSession(input);
882
+ }
883
+ catch (error) {
884
+ io.stderr(`session_record_warning: ${errorMessage(error)}`);
885
+ }
886
+ }
887
+ export function assertNoMissingTerminalConsultResults(tasks, results) {
888
+ const resultTaskIds = new Set(results.map((result) => result.task_id));
889
+ const missing = tasks
890
+ .filter((task) => isTerminalTask(task) && isConsultTask(task) && !resultTaskIds.has(task.id))
891
+ .sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.id.localeCompare(a.id))[0];
892
+ if (missing)
893
+ throw missingConsultResultError(missing);
894
+ }
895
+ export function assertNoOrphanConsultResults(tasksById, results) {
896
+ const orphan = results
897
+ .filter((result) => !tasksById.has(result.task_id) && isConsultResult(result))
898
+ .sort((a, b) => b.created_at.localeCompare(a.created_at) || b.task_id.localeCompare(a.task_id))[0];
899
+ if (orphan)
900
+ throw orphanConsultResultError(orphan.task_id);
901
+ }
902
+ export function browserReadinessNextStep(input) {
903
+ if (!input.loggedInLikely) {
904
+ return "Log in manually in the visible ChatGPT browser, then retry.";
905
+ }
906
+ if (!input.hasComposer) {
907
+ return "Open a normal ChatGPT chat or Project thread, select the Pro/Thinking model, and retry.";
908
+ }
909
+ return "Review the visible ChatGPT browser state, then retry.";
910
+ }
911
+ export function formatBrowserEarlyExit(exit) {
912
+ if (!exit)
913
+ return "no exit details";
914
+ return exit.error ?? `exit code ${exit.code ?? "null"}${exit.signal ? ` signal ${exit.signal}` : ""}`;
915
+ }
916
+ export function formatBrowserModelHints(modelHints) {
917
+ const modelish = /\b(?:ChatGPT|GPT(?:-[\w.]+)?|Pro|Plus|Team|Enterprise|Thinking|Extra High|Auto)\b/i;
918
+ const hints = [...new Set(modelHints.map((hint) => hint.trim()).filter((hint) => modelish.test(hint)))]
919
+ .map((hint) => (hint.length > 80 ? `${hint.slice(0, 77)}...` : hint))
920
+ .slice(0, 6);
921
+ return hints.length > 0 ? hints.join(" | ") : undefined;
922
+ }
923
+ export function formatConfigWarningLine(tokenStatus, sourceCli, setupHintCwd) {
924
+ return tokenStatus.warning ? `config_warning: ${sourceAwareSetupMessage(tokenStatus.warning, sourceCli, { cwd: setupHintCwd })}` : undefined;
925
+ }
926
+ export function formatProductCheckLatestProLines(consult, sourceCli, options = {}) {
927
+ if (consult.result.status === "blocked") {
928
+ const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
929
+ const code = blocker?.code ?? "unknown";
930
+ const retryable = blocker?.retryable ?? false;
931
+ const lines = [`latest_pro: blocked ${consult.task.id} code=${code} retryable=${retryable} ${consult.result.created_at}`];
932
+ if (blocker?.next_step)
933
+ lines.push(`latest_pro_next: ${blocker.next_step}`);
934
+ return lines;
935
+ }
936
+ return [`latest_pro: ok ${consult.task.id} ${consult.result.status} ${consult.result.created_at}`];
937
+ }
938
+ export function isConsultRecord(record) {
939
+ return isConsultTask(record.task);
940
+ }
941
+ export function isConsultTask(task) {
942
+ return task.provenance.adapter === "chatgpt-control";
943
+ }
944
+ export function isTerminalTask(task) {
945
+ return task.status === "done" || task.status === "blocked";
946
+ }
947
+ export function missingConsultResultError(task) {
948
+ return new Error(`GPT Pro answer is corrupt: task ${task.id} is ${task.status} but .bridge/results/${task.id}.json is missing. Restore the result file, retry the completion path, or move the task record aside, then retry.`);
949
+ }
950
+ export function productCheckBrowserNextStep(nextStep, sourceCli, options = {}) {
951
+ const sourceAware = sourceAwareBrowserNextStep(nextStep, sourceCli, options);
952
+ if (!sourceAware)
953
+ return sourceAware;
954
+ if (sourceAware.includes("`"))
955
+ return sourceAware;
956
+ if (sourceAware.includes("pass --target-url with --confirm-target")) {
957
+ return sourceAware.replace("pass --target-url with --confirm-target", `run \`${formatBrowserTargetAskCommand(sourceCli, options)}\``);
958
+ }
959
+ return sourceAware.replace(/(?:and|then) retry\.$/, `then run \`${formatBrowserSmokeCommand(sourceCli, options)}\`.`);
960
+ }
961
+ export function sourceAwareProAnswerBlocker(consult, sourceCli, options = {}) {
962
+ if (!consult.result.blocker)
963
+ return undefined;
964
+ const browserAware = sourceAwareBrowserBlocker(consult.result.blocker, sourceCli, options);
965
+ if ((!sourceCli && !options.cwd && !options.port) || !isSmokeConsultRecord(consult) || !browserAware.next_step)
966
+ return browserAware;
967
+ const nextStep = productCheckBrowserNextStep(browserAware.next_step, sourceCli, options);
968
+ return nextStep === browserAware.next_step ? browserAware : { ...browserAware, next_step: nextStep };
969
+ }
970
+ export function sourceAwareProAnswerSummary(summary, originalBlocker, displayedBlocker) {
971
+ const originalNextStep = originalBlocker?.next_step;
972
+ const displayedNextStep = displayedBlocker?.next_step;
973
+ if (!originalNextStep || !displayedNextStep || originalNextStep === displayedNextStep)
974
+ return summary;
975
+ return summary.replaceAll(originalNextStep, displayedNextStep);
976
+ }
977
+ export async function waitForBrowserLaunchReady(opened, timeoutMs = 5_000) {
978
+ const deadline = Date.now() + timeoutMs;
979
+ let earlyExit;
980
+ while (Date.now() <= deadline) {
981
+ const remainingMs = Math.max(1, deadline - Date.now());
982
+ const status = await getChatGptBrowserStatus({ port: opened.port, timeoutMs: Math.min(250, remainingMs) });
983
+ if (status.reachable)
984
+ return { reachable: true };
985
+ earlyExit ??= await opened.waitForEarlyExit(1);
986
+ if (earlyExit && (earlyExit.code !== 0 || earlyExit.signal || earlyExit.error)) {
987
+ return { reachable: false, earlyExit };
988
+ }
989
+ if (Date.now() >= deadline)
990
+ break;
991
+ await sleep(Math.min(100, Math.max(1, deadline - Date.now())));
992
+ }
993
+ return { reachable: false, ...(earlyExit ? { earlyExit } : {}) };
994
+ }
995
+ export function isConsultResult(result) {
996
+ return result.commands.some((command) => /chatgpt|gpt pro|visible ChatGPT/i.test(command));
997
+ }
998
+ export function isSmokeConsultRecord(consult) {
999
+ return consult.task.title === "GPT Pro smoke" || consult.result.commands.includes("visible ChatGPT browser smoke");
1000
+ }
1001
+ export function orphanConsultResultError(taskId) {
1002
+ return new Error(`GPT Pro answer is corrupt: result .bridge/results/${taskId}.json exists but .bridge/tasks/${taskId}.json is missing. Restore the task file or move the orphan result record aside, then retry.`);
1003
+ }
1004
+ export function sleep(ms) {
1005
+ return new Promise((resolve) => setTimeout(resolve, ms));
1006
+ }
1007
+ //# sourceMappingURL=cli-pro.js.map