@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.
- package/dist/cli-args.d.ts +1 -0
- package/dist/cli-args.js +8 -0
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-ledger.d.ts +22 -0
- package/dist/cli-ledger.js +343 -0
- package/dist/cli-ledger.js.map +1 -0
- package/dist/cli-pro.d.ts +89 -0
- package/dist/cli-pro.js +1007 -0
- package/dist/cli-pro.js.map +1 -0
- package/dist/cli-server.d.ts +42 -0
- package/dist/cli-server.js +396 -0
- package/dist/cli-server.js.map +1 -0
- package/dist/cli-shared.d.ts +65 -0
- package/dist/cli-shared.js +185 -0
- package/dist/cli-shared.js.map +1 -0
- package/dist/cli.js +35 -1919
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
|
-
import { lstat,
|
|
4
|
+
import { lstat, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -9,21 +9,20 @@ import { promisify } from "node:util";
|
|
|
9
9
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
10
10
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
11
11
|
import { renderBanner, shouldColorize } from "./banner.js";
|
|
12
|
-
import {
|
|
13
|
-
import { chatGptVisibilityBlocker, defaultChatGptProfileDir, getChatGptBrowserStatus, normalizeChatGptTargetUrl, listChatGptModelOptions, openChatGptBrowser, parseProMode, parseReasoningEffort, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
14
|
-
import { getTokenExpiryStatus, loadBrowserDefaults, loadLocalConfig, writeLocalConfig } from "./config.js";
|
|
12
|
+
import { getTokenExpiryStatus, loadLocalConfig } from "./config.js";
|
|
15
13
|
import { startHttpMcpServer } from "./http-mcp.js";
|
|
16
14
|
import { createMcpToolHandlers } from "./mcp-tools.js";
|
|
17
15
|
import { runMcpServer } from "./mcp.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
16
|
+
import { BridgeStore } from "./store.js";
|
|
17
|
+
import { printHelpIfRequested, assertNoExtraArgs, assertOnlyOptions, formatCliCommand, formatSourceCliOption, isHelpSubcommand, readFlag, resolveCwdFlag, resolveExistingPathFlag, resolveOptionalFileFlag, shellQuote, unknownSubcommandError, unknownTopLevelCommandError } from "./cli-args.js";
|
|
18
|
+
import { listRawResultsForInspection, listTasksForInspection, runReceiptsCommand, runResultsCommand, runSessionsCommand, runTasksCommand } from "./cli-ledger.js";
|
|
19
|
+
import { isMissingFileError, errorMessage, formatInitCommand, formatReleaseStatusCommand, formatSetupCommand, sourceAwareReleaseMessage, sourceAwareSetupMessage } from "./cli-shared.js";
|
|
20
|
+
import { TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING, redactServerUrl, runInitCommand, runSetupCommand, runStartCommand, runStatusCommand, runTunnelCommand } from "./cli-server.js";
|
|
21
|
+
import { assertNoMissingTerminalConsultResults, assertNoOrphanConsultResults, formatConfigWarningLine, isConsultRecord, runAskProCommand, runChatgptCommand, runConsultsCommand, runProCommand } from "./cli-pro.js";
|
|
22
|
+
import { CLI_VERSION, printClaudeHelp, printDoctorHelp, printHelp, printMcpHelp, printOnboardHelp, printProjectHelp, printReleaseHelp } from "./cli-help.js";
|
|
22
23
|
const execFileAsync = promisify(execFile);
|
|
23
24
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
24
25
|
const RESERVED_PACKAGE_NAMES = new Set(["node_modules", "favicon.ico"]);
|
|
25
|
-
const PRO_BROWSER_SMOKE_TOKEN = "PRODEX_PRO_SMOKE_OK";
|
|
26
|
-
const TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING = "Token-bearing MCP URL authorizes all enabled bridge tools, including repo_read_file, repo_search, repo_write_file_dry_run, repo_write_file_apply, and repo_stage_reviewed_paths. Paste it only into your own trusted private MCP client.";
|
|
27
26
|
const DOCTOR_REQUIRED_MCP_TOOLS = [
|
|
28
27
|
"bridge_create_task",
|
|
29
28
|
"bridge_list_tasks",
|
|
@@ -57,179 +56,16 @@ export async function runCli(args, io = defaultIo()) {
|
|
|
57
56
|
printHelp(io.stdout);
|
|
58
57
|
return 0;
|
|
59
58
|
}
|
|
60
|
-
if (command === "init")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
if (command === "setup") {
|
|
72
|
-
const setupValueFlags = ["--cwd", "--host", "--port", "--token", "--token-ttl-hours", ...ASK_PRO_SELECTION_DEFAULT_FLAGS];
|
|
73
|
-
const setupBooleanFlags = [...ASK_PRO_SELECTION_CLEAR_FLAGS, "--interactive"];
|
|
74
|
-
if (printHelpIfRequested(rest, "setup", io.stdout, printSetupHelp, { valueFlags: setupValueFlags, booleanFlags: setupBooleanFlags }))
|
|
75
|
-
return 0;
|
|
76
|
-
assertOnlyOptions(rest, "setup", setupValueFlags, setupBooleanFlags);
|
|
77
|
-
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
78
|
-
const interactive = rest.includes("--interactive");
|
|
79
|
-
if (interactive) {
|
|
80
|
-
const conflicting = [...ASK_PRO_SELECTION_DEFAULT_FLAGS, ...ASK_PRO_SELECTION_CLEAR_FLAGS].filter((flag) => rest.includes(flag));
|
|
81
|
-
if (conflicting.length > 0) {
|
|
82
|
-
throw new Error(`setup --interactive collects the browser defaults itself; drop ${conflicting.join(", ")} or run without --interactive.`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const browserDefaults = interactive
|
|
86
|
-
? await runBrowserDefaultsWizard(resolvePromptUser(io), io.stdout)
|
|
87
|
-
: parseBrowserDefaultFlags(rest);
|
|
88
|
-
const config = await writeLocalConfig(targetCwd, {
|
|
89
|
-
host: readFlag(rest, "--host") ?? "127.0.0.1",
|
|
90
|
-
port: readPortFlag(rest, "--port") ?? 8787,
|
|
91
|
-
token: readFlag(rest, "--token"),
|
|
92
|
-
tokenTtlHours: readPositiveNumberFlag(rest, "--token-ttl-hours"),
|
|
93
|
-
browserDefaults
|
|
94
|
-
});
|
|
95
|
-
io.stdout("Saved local ChatGPT Developer Mode MCP profile.");
|
|
96
|
-
io.stdout(`Server URL: ${redactServerUrl(config.server_url)}`);
|
|
97
|
-
io.stdout(formatTokenExpiryLine(config));
|
|
98
|
-
io.stdout("Full URL is stored in .bridge/config.local.json.");
|
|
99
|
-
if (config.browser_defaults) {
|
|
100
|
-
io.stdout(`Browser send defaults: ${formatBrowserDefaults(config.browser_defaults)}`);
|
|
101
|
-
}
|
|
102
|
-
return 0;
|
|
103
|
-
}
|
|
104
|
-
if (command === "start") {
|
|
105
|
-
if (printHelpIfRequested(rest, "start", io.stdout, printStartHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
106
|
-
return 0;
|
|
107
|
-
assertOnlyOptions(rest, "start", ["--cwd", "--source-cli"]);
|
|
108
|
-
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
109
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, rest, "--source-cli");
|
|
110
|
-
const setupHintCwd = readFlag(rest, "--cwd") ? targetCwd : undefined;
|
|
111
|
-
const config = await loadLocalConfigForCommand(targetCwd, "start", sourceCli, setupHintCwd);
|
|
112
|
-
assertTokenNotExpiredForCommand(config, sourceCli, setupHintCwd);
|
|
113
|
-
const running = await startHttpMcpServer({
|
|
114
|
-
cwd: targetCwd,
|
|
115
|
-
host: config.host,
|
|
116
|
-
port: config.port,
|
|
117
|
-
token: config.token,
|
|
118
|
-
tokenExpiresAt: config.token_expires_at
|
|
119
|
-
});
|
|
120
|
-
io.stdout(`prodex HTTP MCP listening on ${redactServerUrl(running.mcp_url)}`);
|
|
121
|
-
io.stdout(formatTokenExpiryLine(config));
|
|
122
|
-
await waitForShutdown(async () => running.close());
|
|
123
|
-
return 0;
|
|
124
|
-
}
|
|
125
|
-
if (command === "status") {
|
|
126
|
-
if (printHelpIfRequested(rest, "status", io.stdout, printStatusHelp, {
|
|
127
|
-
valueFlags: ["--cwd", "--source-cli"],
|
|
128
|
-
booleanFlags: ["--show-token", "--url-only", "--unsafe-show-non-expiring-token"]
|
|
129
|
-
})) {
|
|
130
|
-
return 0;
|
|
131
|
-
}
|
|
132
|
-
assertOnlyOptions(rest, "status", ["--cwd", "--source-cli"], ["--show-token", "--url-only", "--unsafe-show-non-expiring-token"]);
|
|
133
|
-
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
134
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, rest, "--source-cli");
|
|
135
|
-
const setupHintCwd = readFlag(rest, "--cwd") ? targetCwd : undefined;
|
|
136
|
-
const config = await loadLocalConfigForCommand(targetCwd, "status", sourceCli, setupHintCwd);
|
|
137
|
-
const showToken = rest.includes("--show-token");
|
|
138
|
-
const allowNonExpiringTokenReveal = rest.includes("--unsafe-show-non-expiring-token");
|
|
139
|
-
const tokenStatus = getTokenExpiryStatus(config);
|
|
140
|
-
if (showToken && tokenStatus.status === "non_expiring" && !allowNonExpiringTokenReveal) {
|
|
141
|
-
throw new Error(sourceAwareSetupMessage("status --show-token requires a token with expiry. Run `prodex setup --token-ttl-hours <hours>` first, or pass --unsafe-show-non-expiring-token for local-only debugging.", sourceCli, { cwd: setupHintCwd }));
|
|
142
|
-
}
|
|
143
|
-
if (showToken && tokenStatus.status === "expired") {
|
|
144
|
-
throw new Error(sourceAwareSetupMessage(`token expired at ${tokenStatus.token_expires_at}. Run \`prodex setup --token-ttl-hours <hours>\`.`, sourceCli, {
|
|
145
|
-
cwd: setupHintCwd
|
|
146
|
-
}));
|
|
147
|
-
}
|
|
148
|
-
const nonExpiringRevealWarning = showToken && allowNonExpiringTokenReveal && tokenStatus.status === "non_expiring"
|
|
149
|
-
? sourceAwareSetupMessage("Showing a non-expiring token. Keep this local-only and rotate it with `prodex setup --token-ttl-hours <hours>` before any tunnel or ChatGPT Project use.", sourceCli, { cwd: setupHintCwd })
|
|
150
|
-
: undefined;
|
|
151
|
-
const serverUrl = formatServerUrlForOutput(config.server_url, { showToken });
|
|
152
|
-
if (rest.includes("--url-only")) {
|
|
153
|
-
if (showToken)
|
|
154
|
-
io.stderr(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
155
|
-
if (nonExpiringRevealWarning)
|
|
156
|
-
io.stderr(nonExpiringRevealWarning);
|
|
157
|
-
io.stdout(serverUrl);
|
|
158
|
-
return 0;
|
|
159
|
-
}
|
|
160
|
-
const warnings = tokenStatus.warning ? [sourceAwareSetupMessage(tokenStatus.warning, sourceCli, { cwd: setupHintCwd })] : [];
|
|
161
|
-
if (showToken)
|
|
162
|
-
warnings.push(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
163
|
-
if (nonExpiringRevealWarning)
|
|
164
|
-
warnings.push(nonExpiringRevealWarning);
|
|
165
|
-
io.stdout(JSON.stringify({
|
|
166
|
-
server_url: serverUrl,
|
|
167
|
-
config_path: ".bridge/config.local.json",
|
|
168
|
-
token_status: tokenStatus.status,
|
|
169
|
-
token_expires_at: tokenStatus.token_expires_at ?? null,
|
|
170
|
-
browser_defaults: config.browser_defaults ?? null,
|
|
171
|
-
warnings
|
|
172
|
-
}, null, 2));
|
|
173
|
-
return 0;
|
|
174
|
-
}
|
|
175
|
-
if (command === "tunnel") {
|
|
176
|
-
const [subcommand, ...tunnelArgs] = rest;
|
|
177
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
178
|
-
assertNoExtraArgs(tunnelArgs, "tunnel help", 0);
|
|
179
|
-
printTunnelHelp(io.stdout);
|
|
180
|
-
return 0;
|
|
181
|
-
}
|
|
182
|
-
if (subcommand !== "url")
|
|
183
|
-
throw unknownSubcommandError("tunnel", subcommand, ["url"]);
|
|
184
|
-
if (printHelpIfRequested(tunnelArgs, "tunnel url", io.stdout, printTunnelUrlHelp, {
|
|
185
|
-
valueFlags: ["--cwd", "--public-url", "--source-cli"],
|
|
186
|
-
booleanFlags: ["--show-token", "--url-only"]
|
|
187
|
-
})) {
|
|
188
|
-
return 0;
|
|
189
|
-
}
|
|
190
|
-
assertOnlyOptions(tunnelArgs, "tunnel url", ["--cwd", "--public-url", "--source-cli"], ["--show-token", "--url-only"]);
|
|
191
|
-
const targetCwd = resolveCwdFlag(io.cwd, tunnelArgs);
|
|
192
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, tunnelArgs, "--source-cli");
|
|
193
|
-
const setupHintCwd = readFlag(tunnelArgs, "--cwd") ? targetCwd : undefined;
|
|
194
|
-
const publicUrl = readFlag(tunnelArgs, "--public-url");
|
|
195
|
-
if (!publicUrl)
|
|
196
|
-
throw new Error("tunnel url requires --public-url <https-url>");
|
|
197
|
-
parseTunnelPublicUrl(publicUrl);
|
|
198
|
-
const config = await loadLocalConfigForCommand(targetCwd, "tunnel url", sourceCli, setupHintCwd);
|
|
199
|
-
const tokenStatus = getTokenExpiryStatus(config);
|
|
200
|
-
if (tokenStatus.status === "non_expiring") {
|
|
201
|
-
throw new Error(sourceAwareSetupMessage("tunnel url requires a short-lived token. Run `prodex setup --token-ttl-hours <hours>` first.", sourceCli, {
|
|
202
|
-
cwd: setupHintCwd
|
|
203
|
-
}));
|
|
204
|
-
}
|
|
205
|
-
if (tokenStatus.status === "expired") {
|
|
206
|
-
throw new Error(sourceAwareSetupMessage(`token expired at ${tokenStatus.token_expires_at}. Run \`prodex setup --token-ttl-hours <hours>\`.`, sourceCli, {
|
|
207
|
-
cwd: setupHintCwd
|
|
208
|
-
}));
|
|
209
|
-
}
|
|
210
|
-
const mcpUrl = makeTunnelMcpUrl(publicUrl, config.token);
|
|
211
|
-
const showToken = tunnelArgs.includes("--show-token");
|
|
212
|
-
const outputUrl = showToken ? mcpUrl : redactServerUrl(mcpUrl);
|
|
213
|
-
if (tunnelArgs.includes("--url-only")) {
|
|
214
|
-
if (showToken)
|
|
215
|
-
io.stderr(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
216
|
-
io.stdout(outputUrl);
|
|
217
|
-
return 0;
|
|
218
|
-
}
|
|
219
|
-
const warnings = [
|
|
220
|
-
"This command does not create a tunnel. Keep `prodex start` running behind your own tunnel.",
|
|
221
|
-
"Only paste the token-bearing URL into a trusted private MCP client."
|
|
222
|
-
];
|
|
223
|
-
if (showToken)
|
|
224
|
-
warnings.push(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
225
|
-
io.stdout(JSON.stringify({
|
|
226
|
-
mcp_url: outputUrl,
|
|
227
|
-
token_status: tokenStatus.status,
|
|
228
|
-
token_expires_at: tokenStatus.token_expires_at,
|
|
229
|
-
warnings
|
|
230
|
-
}, null, 2));
|
|
231
|
-
return 0;
|
|
232
|
-
}
|
|
59
|
+
if (command === "init")
|
|
60
|
+
return runInitCommand(rest, io);
|
|
61
|
+
if (command === "setup")
|
|
62
|
+
return runSetupCommand(rest, io);
|
|
63
|
+
if (command === "start")
|
|
64
|
+
return runStartCommand(rest, io);
|
|
65
|
+
if (command === "status")
|
|
66
|
+
return runStatusCommand(rest, io);
|
|
67
|
+
if (command === "tunnel")
|
|
68
|
+
return runTunnelCommand(rest, io);
|
|
233
69
|
if (command === "doctor") {
|
|
234
70
|
if (printHelpIfRequested(rest, "doctor", io.stdout, printDoctorHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
235
71
|
return 0;
|
|
@@ -329,830 +165,22 @@ export async function runCli(args, io = defaultIo()) {
|
|
|
329
165
|
}
|
|
330
166
|
throw unknownSubcommandError("claude", subcommand, ["prompt", "config"]);
|
|
331
167
|
}
|
|
332
|
-
if (command === "chatgpt")
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return 0;
|
|
349
|
-
}
|
|
350
|
-
if (subcommand === "status") {
|
|
351
|
-
assertOnlyOptions(chatgptArgs, "chatgpt status", ["--port"]);
|
|
352
|
-
const status = await getChatGptBrowserStatus({ port: readPortFlag(chatgptArgs, "--port") ?? 9333 });
|
|
353
|
-
io.stdout(JSON.stringify(status, null, 2));
|
|
354
|
-
return 0;
|
|
355
|
-
}
|
|
356
|
-
if (subcommand === "smoke") {
|
|
357
|
-
assertOnlyOptions(chatgptArgs, "chatgpt smoke", ["--cwd", "--port", "--timeout-ms", "--source-cli"]);
|
|
358
|
-
const targetCwd = resolveCwdFlag(io.cwd, chatgptArgs);
|
|
359
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
360
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, chatgptArgs, "--source-cli");
|
|
361
|
-
const port = readPortFlag(chatgptArgs, "--port") ?? 9333;
|
|
362
|
-
const timeoutMs = readPositiveNumberFlag(chatgptArgs, "--timeout-ms") ?? 90000;
|
|
363
|
-
const commandOptions = {
|
|
364
|
-
...(readFlag(chatgptArgs, "--cwd") ? { cwd: targetCwd } : {}),
|
|
365
|
-
...(readFlag(chatgptArgs, "--port") ? { port } : {})
|
|
366
|
-
};
|
|
367
|
-
const smokePrompt = `This is a one-time prodex smoke test. Reply exactly: ${PRO_BROWSER_SMOKE_TOKEN}`;
|
|
368
|
-
const recordBlockedSmoke = async (summary, blocker, thread) => {
|
|
369
|
-
const bundle = await buildDryRunBundle(targetCwd, { prompt: smokePrompt, files: [] });
|
|
370
|
-
const task = await targetStore.createTask({
|
|
371
|
-
source: "codex",
|
|
372
|
-
title: "GPT Pro smoke",
|
|
373
|
-
prompt: bundle.text,
|
|
374
|
-
repo_id: "default",
|
|
375
|
-
provenance: {
|
|
376
|
-
adapter: "chatgpt-control",
|
|
377
|
-
session_id: bundle.id,
|
|
378
|
-
thread,
|
|
379
|
-
warnings: []
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
await targetStore.claimTask(task.id, "chatgpt-pro");
|
|
383
|
-
await targetStore.completeTask(task.id, {
|
|
384
|
-
status: "blocked",
|
|
385
|
-
summary,
|
|
386
|
-
commands: ["visible ChatGPT browser smoke"],
|
|
387
|
-
blocker
|
|
388
|
-
});
|
|
389
|
-
await writeSessionBestEffort(targetStore, {
|
|
390
|
-
id: bundle.id,
|
|
391
|
-
direction: "codex_to_chatgpt",
|
|
392
|
-
backend: "chatgpt-control",
|
|
393
|
-
task_id: task.id,
|
|
394
|
-
thread,
|
|
395
|
-
status: "blocked",
|
|
396
|
-
blocker,
|
|
397
|
-
warnings: []
|
|
398
|
-
}, io);
|
|
399
|
-
return task.id;
|
|
400
|
-
};
|
|
401
|
-
let result;
|
|
402
|
-
try {
|
|
403
|
-
result = await sendChatGptPrompt({
|
|
404
|
-
port,
|
|
405
|
-
prompt: smokePrompt,
|
|
406
|
-
timeoutMs
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
catch (error) {
|
|
410
|
-
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, commandOptions);
|
|
411
|
-
const message = blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error);
|
|
412
|
-
let taskId;
|
|
413
|
-
try {
|
|
414
|
-
taskId = await recordBlockedSmoke(message, blocker);
|
|
415
|
-
}
|
|
416
|
-
catch (recordError) {
|
|
417
|
-
throw new Error(`${message} (also failed to record blocked smoke: ${errorMessage(recordError)})`);
|
|
418
|
-
}
|
|
419
|
-
throw new Error(formatBlockedConsultRecordedMessage(message, taskId, sourceCli, { cwd: targetCwd }));
|
|
420
|
-
}
|
|
421
|
-
if (result.answer.trim() !== PRO_BROWSER_SMOKE_TOKEN) {
|
|
422
|
-
const message = `Pro browser smoke returned an unexpected answer. Expected exactly ${PRO_BROWSER_SMOKE_TOKEN}. Actual: ${firstLine(result.answer)}`;
|
|
423
|
-
const blocker = {
|
|
424
|
-
code: "smoke_token_mismatch",
|
|
425
|
-
message,
|
|
426
|
-
retryable: true,
|
|
427
|
-
next_step: `Retry \`${formatBrowserSmokeCommand(sourceCli, commandOptions)}\` after selecting the intended Pro model, or inspect the visible ChatGPT answer.`
|
|
428
|
-
};
|
|
429
|
-
let taskId;
|
|
430
|
-
try {
|
|
431
|
-
taskId = await recordBlockedSmoke(message, blocker, result.url);
|
|
432
|
-
}
|
|
433
|
-
catch (recordError) {
|
|
434
|
-
throw new Error(`${message} (also failed to record blocked smoke: ${errorMessage(recordError)})`);
|
|
435
|
-
}
|
|
436
|
-
throw new Error(formatBlockedConsultRecordedMessage(message, taskId, sourceCli, { cwd: targetCwd }));
|
|
437
|
-
}
|
|
438
|
-
io.stdout(JSON.stringify(result, null, 2));
|
|
439
|
-
return 0;
|
|
440
|
-
}
|
|
441
|
-
throw legacyChatGptNamespaceError(subcommand);
|
|
442
|
-
}
|
|
443
|
-
if (command === "tasks") {
|
|
444
|
-
const [subcommand, ...taskArgs] = rest;
|
|
445
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
446
|
-
assertNoExtraArgs(taskArgs, "tasks help", 0);
|
|
447
|
-
printTasksHelp(io.stdout);
|
|
448
|
-
return 0;
|
|
449
|
-
}
|
|
450
|
-
if (subcommand === "create") {
|
|
451
|
-
if (printHelpIfRequested(taskArgs, "tasks create", io.stdout, printTasksHelp, { valueFlags: ["--cwd", "--title", "--prompt", "--repo-id", "--file"] }))
|
|
452
|
-
return 0;
|
|
453
|
-
assertOnlyOptions(taskArgs, "tasks create", ["--cwd", "--title", "--prompt", "--repo-id", "--file"]);
|
|
454
|
-
const title = readFlag(taskArgs, "--title");
|
|
455
|
-
const prompt = readFlag(taskArgs, "--prompt");
|
|
456
|
-
if (!title || !prompt)
|
|
457
|
-
throw new Error("tasks create requires --title and --prompt");
|
|
458
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
459
|
-
const task = await targetStore.createTask({
|
|
460
|
-
source: "codex",
|
|
461
|
-
title,
|
|
462
|
-
prompt,
|
|
463
|
-
repo_id: readFlag(taskArgs, "--repo-id") ?? "default",
|
|
464
|
-
files: readRepeatedFlag(taskArgs, "--file").map((file) => ({ path: file, role: "context" })),
|
|
465
|
-
provenance: { adapter: "cli", warnings: [] }
|
|
466
|
-
});
|
|
467
|
-
io.stdout(`${task.id}\t${task.status}\t${task.title}`);
|
|
468
|
-
return 0;
|
|
469
|
-
}
|
|
470
|
-
if (subcommand === "list") {
|
|
471
|
-
if (printHelpIfRequested(taskArgs, "tasks list", io.stdout, printTasksHelp, { valueFlags: ["--cwd", "--status"] }))
|
|
472
|
-
return 0;
|
|
473
|
-
assertOnlyOptions(taskArgs, "tasks list", ["--cwd", "--status"]);
|
|
474
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
475
|
-
const status = readTaskStatusFlag(taskArgs);
|
|
476
|
-
const tasks = await listTasksForInspection(targetStore, status);
|
|
477
|
-
for (const task of tasks) {
|
|
478
|
-
io.stdout(`${task.id}\t${task.status}\t${task.title}`);
|
|
479
|
-
}
|
|
480
|
-
return 0;
|
|
481
|
-
}
|
|
482
|
-
if (subcommand === "show") {
|
|
483
|
-
if (printHelpIfRequested(taskArgs, "tasks show", io.stdout, printTasksHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
484
|
-
return 0;
|
|
485
|
-
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks show", 1, ["--cwd"]);
|
|
486
|
-
if (!taskId)
|
|
487
|
-
throw new Error("tasks show requires <task-id|latest>");
|
|
488
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
489
|
-
const task = taskId === "latest" ? await latestTask(targetStore, { readOnly: true }) : await targetStore.getTaskReadOnly(taskId);
|
|
490
|
-
if (!task)
|
|
491
|
-
throw new Error(taskId === "latest" ? "No tasks found" : `Task not found: ${taskId}`);
|
|
492
|
-
io.stdout(JSON.stringify(task, null, 2));
|
|
493
|
-
return 0;
|
|
494
|
-
}
|
|
495
|
-
if (subcommand === "claim") {
|
|
496
|
-
if (printHelpIfRequested(taskArgs, "tasks claim", io.stdout, printTasksHelp, { valueFlags: ["--cwd", "--by"], maxPositionals: 1 }))
|
|
497
|
-
return 0;
|
|
498
|
-
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks claim", 1, ["--cwd", "--by"]);
|
|
499
|
-
if (!taskId)
|
|
500
|
-
throw new Error("tasks claim requires <task-id>");
|
|
501
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
502
|
-
const task = await targetStore.claimTask(taskId, readFlag(taskArgs, "--by") ?? "codex");
|
|
503
|
-
io.stdout(`${task.id}\t${task.status}\t${task.claimed_by ?? ""}`);
|
|
504
|
-
return 0;
|
|
505
|
-
}
|
|
506
|
-
if (subcommand === "complete") {
|
|
507
|
-
if (printHelpIfRequested(taskArgs, "tasks complete", io.stdout, printTasksHelp, {
|
|
508
|
-
valueFlags: ["--cwd", "--summary", "--command", "--artifact"],
|
|
509
|
-
maxPositionals: 1
|
|
510
|
-
})) {
|
|
511
|
-
return 0;
|
|
512
|
-
}
|
|
513
|
-
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks complete", 1, ["--cwd", "--summary", "--command", "--artifact"]);
|
|
514
|
-
if (!taskId)
|
|
515
|
-
throw new Error("tasks complete requires <task-id> --summary");
|
|
516
|
-
const summary = readFlag(taskArgs, "--summary");
|
|
517
|
-
if (!summary)
|
|
518
|
-
throw new Error("tasks complete requires <task-id> --summary");
|
|
519
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
520
|
-
const result = await targetStore.completeTask(taskId, {
|
|
521
|
-
status: "done",
|
|
522
|
-
summary,
|
|
523
|
-
commands: readRepeatedFlag(taskArgs, "--command"),
|
|
524
|
-
artifacts: await writeTaskCompleteArtifacts(targetStore, readRepeatedFlag(taskArgs, "--artifact"))
|
|
525
|
-
});
|
|
526
|
-
io.stdout(`${result.task_id}\t${result.status}\t${result.summary}`);
|
|
527
|
-
return 0;
|
|
528
|
-
}
|
|
529
|
-
if (subcommand === "block") {
|
|
530
|
-
if (printHelpIfRequested(taskArgs, "tasks block", io.stdout, printTasksHelp, {
|
|
531
|
-
valueFlags: ["--cwd", "--summary", "--code", "--next-step", "--command"],
|
|
532
|
-
booleanFlags: ["--retryable"],
|
|
533
|
-
maxPositionals: 1
|
|
534
|
-
})) {
|
|
535
|
-
return 0;
|
|
536
|
-
}
|
|
537
|
-
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks block", 1, ["--cwd", "--summary", "--code", "--next-step", "--command"], ["--retryable"]);
|
|
538
|
-
if (!taskId)
|
|
539
|
-
throw new Error("tasks block requires <task-id> --summary");
|
|
540
|
-
const summary = readFlag(taskArgs, "--summary");
|
|
541
|
-
if (!summary)
|
|
542
|
-
throw new Error("tasks block requires <task-id> --summary");
|
|
543
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
544
|
-
const result = await targetStore.completeTask(taskId, {
|
|
545
|
-
status: "blocked",
|
|
546
|
-
summary,
|
|
547
|
-
blocker: {
|
|
548
|
-
code: readFlag(taskArgs, "--code") ?? "manual_blocker",
|
|
549
|
-
message: summary,
|
|
550
|
-
retryable: taskArgs.includes("--retryable"),
|
|
551
|
-
next_step: readFlag(taskArgs, "--next-step")
|
|
552
|
-
},
|
|
553
|
-
commands: readRepeatedFlag(taskArgs, "--command")
|
|
554
|
-
});
|
|
555
|
-
io.stdout(`${result.task_id}\t${result.status}\t${result.summary}`);
|
|
556
|
-
return 0;
|
|
557
|
-
}
|
|
558
|
-
throw unknownSubcommandError("tasks", subcommand, ["create", "list", "show", "claim", "complete", "block"]);
|
|
559
|
-
}
|
|
560
|
-
if (command === "results") {
|
|
561
|
-
const [subcommand, ...resultArgs] = rest;
|
|
562
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
563
|
-
assertNoExtraArgs(resultArgs, "results help", 0);
|
|
564
|
-
printResultsHelp(io.stdout);
|
|
565
|
-
return 0;
|
|
566
|
-
}
|
|
567
|
-
if (subcommand === "show") {
|
|
568
|
-
if (printHelpIfRequested(resultArgs, "results show", io.stdout, printResultsHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
569
|
-
return 0;
|
|
570
|
-
const [taskId] = readPositionalsWithOptions(resultArgs, "results show", 1, ["--cwd"]);
|
|
571
|
-
if (!taskId)
|
|
572
|
-
throw new Error("results show requires <task-id|latest>");
|
|
573
|
-
const targetCwd = resolveCwdFlag(io.cwd, resultArgs);
|
|
574
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
575
|
-
const resultOptions = { cwd: readFlag(resultArgs, "--cwd") ? targetCwd : undefined };
|
|
576
|
-
try {
|
|
577
|
-
const resolvedTaskId = taskId === "latest" ? await latestResultTaskId(targetStore, { readOnly: true }) : taskId;
|
|
578
|
-
io.stdout(JSON.stringify(await targetStore.getFinalizedResultReadOnly(resolvedTaskId), null, 2));
|
|
579
|
-
}
|
|
580
|
-
catch (error) {
|
|
581
|
-
throw sourceAwareResultError(error, undefined, resultOptions);
|
|
582
|
-
}
|
|
583
|
-
return 0;
|
|
584
|
-
}
|
|
585
|
-
if (subcommand === "artifact") {
|
|
586
|
-
if (printHelpIfRequested(resultArgs, "results artifact", io.stdout, printResultsHelp, { valueFlags: ["--cwd"], maxPositionals: 2 }))
|
|
587
|
-
return 0;
|
|
588
|
-
const [taskId, artifactPath] = readPositionalsWithOptions(resultArgs, "results artifact", 2, ["--cwd"]);
|
|
589
|
-
if (!taskId)
|
|
590
|
-
throw new Error("results artifact requires <task-id> [artifact-path]");
|
|
591
|
-
const targetCwd = resolveCwdFlag(io.cwd, resultArgs);
|
|
592
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
593
|
-
const resultOptions = { cwd: readFlag(resultArgs, "--cwd") ? targetCwd : undefined };
|
|
594
|
-
try {
|
|
595
|
-
const resolvedTaskId = taskId === "latest" ? await latestResultTaskId(targetStore, { readOnly: true }) : taskId;
|
|
596
|
-
const artifact = await targetStore.readFinalizedResultArtifactText(resolvedTaskId, artifactPath);
|
|
597
|
-
io.stdout(artifact.content);
|
|
598
|
-
}
|
|
599
|
-
catch (error) {
|
|
600
|
-
throw sourceAwareResultError(error, undefined, resultOptions);
|
|
601
|
-
}
|
|
602
|
-
return 0;
|
|
603
|
-
}
|
|
604
|
-
if (subcommand === "reseal") {
|
|
605
|
-
if (printHelpIfRequested(resultArgs, "results reseal", io.stdout, printResultsHelp, {
|
|
606
|
-
valueFlags: ["--cwd"],
|
|
607
|
-
booleanFlags: ["--confirm-current-result"],
|
|
608
|
-
maxPositionals: 1
|
|
609
|
-
})) {
|
|
610
|
-
return 0;
|
|
611
|
-
}
|
|
612
|
-
const [taskId] = readPositionalsWithOptions(resultArgs, "results reseal", 1, ["--cwd"], ["--confirm-current-result"]);
|
|
613
|
-
if (!taskId)
|
|
614
|
-
throw new Error("results reseal requires <task-id|latest> --confirm-current-result");
|
|
615
|
-
if (!resultArgs.includes("--confirm-current-result")) {
|
|
616
|
-
throw new Error("results reseal requires --confirm-current-result after you review the current .bridge/results/<task-id>.json payload locally.");
|
|
617
|
-
}
|
|
618
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, resultArgs));
|
|
619
|
-
const resolvedTaskId = taskId === "latest" ? await latestRawResultTaskId(targetStore) : taskId;
|
|
620
|
-
const resealed = await targetStore.resealResult(resolvedTaskId);
|
|
621
|
-
io.stdout(`${resealed.result.task_id}\tresealed\t${resealed.receipt.id}\tresult_sha256=${resealed.receipt.metadata.result_sha256}`);
|
|
622
|
-
return 0;
|
|
623
|
-
}
|
|
624
|
-
throw unknownSubcommandError("results", subcommand, ["show", "artifact", "reseal"]);
|
|
625
|
-
}
|
|
626
|
-
if (command === "receipts") {
|
|
627
|
-
const [subcommand, ...receiptArgs] = rest;
|
|
628
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
629
|
-
assertNoExtraArgs(receiptArgs, "receipts help", 0);
|
|
630
|
-
printReceiptsHelp(io.stdout);
|
|
631
|
-
return 0;
|
|
632
|
-
}
|
|
633
|
-
if (subcommand === "list") {
|
|
634
|
-
if (printHelpIfRequested(receiptArgs, "receipts list", io.stdout, printReceiptsHelp, { valueFlags: ["--cwd", "--kind", "--task-id"] }))
|
|
635
|
-
return 0;
|
|
636
|
-
assertOnlyOptions(receiptArgs, "receipts list", ["--cwd", "--kind", "--task-id"]);
|
|
637
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, receiptArgs));
|
|
638
|
-
const receipts = await listReceiptsForInspection(targetStore, {
|
|
639
|
-
kind: readReceiptKindFlag(receiptArgs),
|
|
640
|
-
task_id: readFlag(receiptArgs, "--task-id")
|
|
641
|
-
});
|
|
642
|
-
for (const receipt of receipts) {
|
|
643
|
-
io.stdout(`${receipt.id}\t${receipt.kind}\t${receipt.summary}${receiptInspectionListSuffix(receipt)}`);
|
|
644
|
-
}
|
|
645
|
-
return 0;
|
|
646
|
-
}
|
|
647
|
-
if (subcommand === "show") {
|
|
648
|
-
if (printHelpIfRequested(receiptArgs, "receipts show", io.stdout, printReceiptsHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
649
|
-
return 0;
|
|
650
|
-
const [receiptId] = readPositionalsWithOptions(receiptArgs, "receipts show", 1, ["--cwd"]);
|
|
651
|
-
if (!receiptId)
|
|
652
|
-
throw new Error("receipts show requires <receipt-id|latest>");
|
|
653
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, receiptArgs));
|
|
654
|
-
const receipt = receiptId === "latest" ? (await listReceiptsForInspection(targetStore))[0] : await targetStore.getReceiptForDisplayReadOnly(receiptId);
|
|
655
|
-
if (!receipt)
|
|
656
|
-
throw new Error(receiptId === "latest" ? "No receipts found" : `Receipt not found: ${receiptId}`);
|
|
657
|
-
io.stdout(JSON.stringify(receipt, null, 2));
|
|
658
|
-
return 0;
|
|
659
|
-
}
|
|
660
|
-
if (subcommand === "rotate-key") {
|
|
661
|
-
if (printHelpIfRequested(receiptArgs, "receipts rotate-key", io.stdout, printReceiptsHelp, { valueFlags: ["--cwd"] }))
|
|
662
|
-
return 0;
|
|
663
|
-
assertOnlyOptions(receiptArgs, "receipts rotate-key", ["--cwd"]);
|
|
664
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, receiptArgs));
|
|
665
|
-
await targetStore.ensure();
|
|
666
|
-
const rotated = await targetStore.rotateReceiptIntegrityKey();
|
|
667
|
-
io.stdout(`Rotated the local receipt integrity key: ${rotated.keys} key(s) in .bridge/receipt-key.local.`);
|
|
668
|
-
io.stdout("New receipts are signed with the new key; receipts signed before the rotation still verify via the retained legacy keys.");
|
|
669
|
-
return 0;
|
|
670
|
-
}
|
|
671
|
-
throw unknownSubcommandError("receipts", subcommand, ["list", "show", "rotate-key"]);
|
|
672
|
-
}
|
|
673
|
-
if (command === "sessions") {
|
|
674
|
-
const [subcommand, ...sessionArgs] = rest;
|
|
675
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
676
|
-
assertNoExtraArgs(sessionArgs, "sessions help", 0);
|
|
677
|
-
printSessionsHelp(io.stdout);
|
|
678
|
-
return 0;
|
|
679
|
-
}
|
|
680
|
-
if (subcommand === "list") {
|
|
681
|
-
if (printHelpIfRequested(sessionArgs, "sessions list", io.stdout, printSessionsHelp, { valueFlags: ["--cwd", "--status"] }))
|
|
682
|
-
return 0;
|
|
683
|
-
assertOnlyOptions(sessionArgs, "sessions list", ["--cwd", "--status"]);
|
|
684
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, sessionArgs));
|
|
685
|
-
const status = readSessionStatusFlag(sessionArgs);
|
|
686
|
-
const sessions = await listSessionsForInspection(targetStore, status);
|
|
687
|
-
for (const session of sessions) {
|
|
688
|
-
io.stdout(`${session.id}\t${session.status}\t${session.backend}\t${session.direction}`);
|
|
689
|
-
}
|
|
690
|
-
return 0;
|
|
691
|
-
}
|
|
692
|
-
if (subcommand === "show") {
|
|
693
|
-
if (printHelpIfRequested(sessionArgs, "sessions show", io.stdout, printSessionsHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
694
|
-
return 0;
|
|
695
|
-
const [sessionId] = readPositionalsWithOptions(sessionArgs, "sessions show", 1, ["--cwd"]);
|
|
696
|
-
if (!sessionId)
|
|
697
|
-
throw new Error("sessions show requires <session-id|latest>");
|
|
698
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, sessionArgs));
|
|
699
|
-
const session = sessionId === "latest" ? (await listSessionsForInspection(targetStore))[0] : await targetStore.getSessionReadOnly(sessionId);
|
|
700
|
-
if (!session)
|
|
701
|
-
throw new Error(sessionId === "latest" ? "No sessions found" : `Session not found: ${sessionId}`);
|
|
702
|
-
io.stdout(formatSession(session));
|
|
703
|
-
return 0;
|
|
704
|
-
}
|
|
705
|
-
throw unknownSubcommandError("sessions", subcommand, ["list", "show"]);
|
|
706
|
-
}
|
|
707
|
-
if (command === "pro") {
|
|
708
|
-
const [subcommand, ...proArgs] = rest;
|
|
709
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
710
|
-
assertNoExtraArgs(proArgs, "pro help", 0);
|
|
711
|
-
printProHelp(io.stdout);
|
|
712
|
-
return 0;
|
|
713
|
-
}
|
|
714
|
-
if (subcommand === "ask") {
|
|
715
|
-
if (printHelpIfRequested(proArgs, "pro ask", io.stdout, printProHelp, {
|
|
716
|
-
valueFlags: [...ASK_PRO_PREVIEW_VALUE_FLAGS],
|
|
717
|
-
booleanFlags: [...ASK_PRO_BOOLEAN_FLAGS]
|
|
718
|
-
})) {
|
|
719
|
-
return 0;
|
|
720
|
-
}
|
|
721
|
-
parseAskProArgs(proArgs, ASK_PRO_PREVIEW_VALUE_FLAGS);
|
|
722
|
-
if (hasAskProSendMode(proArgs)) {
|
|
723
|
-
throw new Error("prodex pro ask is a dry-run preview. Use `prodex pro browser ask` for visible-browser sends.");
|
|
724
|
-
}
|
|
725
|
-
const hasDryRun = hasAskProDryRunMode(proArgs);
|
|
726
|
-
return runCli(["ask-pro", ...(hasDryRun ? [] : ["--dry-run"]), ...proArgs], io);
|
|
727
|
-
}
|
|
728
|
-
if (subcommand === "browser") {
|
|
729
|
-
const [browserSubcommand, ...browserArgs] = proArgs;
|
|
730
|
-
if (!browserSubcommand || isHelpSubcommand(browserSubcommand)) {
|
|
731
|
-
assertOnlyOptions(browserArgs, "pro browser help", ["--source-cli"]);
|
|
732
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
|
|
733
|
-
printProBrowserHelp(io.stdout, sourceCli);
|
|
734
|
-
return 0;
|
|
735
|
-
}
|
|
736
|
-
if (browserSubcommand === "login") {
|
|
737
|
-
if (printProBrowserHelpIfRequested(browserArgs, "pro browser login", io, {
|
|
738
|
-
valueFlags: ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms"],
|
|
739
|
-
booleanFlags: ["--dry-run"]
|
|
740
|
-
})) {
|
|
741
|
-
return 0;
|
|
742
|
-
}
|
|
743
|
-
assertOnlyOptions(browserArgs, "pro browser login", ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms"], ["--dry-run"]);
|
|
744
|
-
const loginUrl = readChatGptBrowserUrlFlag(browserArgs);
|
|
745
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
|
|
746
|
-
const targetCwd = readFlag(browserArgs, "--cwd") ? resolveCwdFlag(io.cwd, browserArgs) : undefined;
|
|
747
|
-
const profileDir = readFlag(browserArgs, "--profile-dir");
|
|
748
|
-
const port = readPortFlag(browserArgs, "--port") ?? 9333;
|
|
749
|
-
const launchTimeoutMs = readPositiveNumberFlag(browserArgs, "--launch-timeout-ms");
|
|
750
|
-
const commandOptions = {
|
|
751
|
-
...(targetCwd ? { cwd: targetCwd } : {}),
|
|
752
|
-
...(profileDir ? { profileDir } : {}),
|
|
753
|
-
...(port !== 9333 ? { port } : {}),
|
|
754
|
-
...(readFlag(browserArgs, "--url") ? { url: loginUrl } : {}),
|
|
755
|
-
...(launchTimeoutMs !== undefined ? { launchTimeoutMs } : {})
|
|
756
|
-
};
|
|
757
|
-
if (browserArgs.includes("--dry-run")) {
|
|
758
|
-
printBrowserLoginGuide(io.stdout, {
|
|
759
|
-
opened: false,
|
|
760
|
-
loginUrl,
|
|
761
|
-
profileDir: profileDir ?? defaultChatGptProfileDir(),
|
|
762
|
-
port,
|
|
763
|
-
sourceCli,
|
|
764
|
-
commandOptions
|
|
765
|
-
});
|
|
766
|
-
return 0;
|
|
767
|
-
}
|
|
768
|
-
const opened = openChatGptBrowser({
|
|
769
|
-
port,
|
|
770
|
-
profileDir,
|
|
771
|
-
url: loginUrl
|
|
772
|
-
});
|
|
773
|
-
await assertBrowserLaunchStayedAlive(opened, launchTimeoutMs);
|
|
774
|
-
printBrowserLoginGuide(io.stdout, {
|
|
775
|
-
opened: true,
|
|
776
|
-
loginUrl,
|
|
777
|
-
profileDir: opened.profileDir,
|
|
778
|
-
port: opened.port,
|
|
779
|
-
sourceCli,
|
|
780
|
-
commandOptions
|
|
781
|
-
});
|
|
782
|
-
return 0;
|
|
783
|
-
}
|
|
784
|
-
if (browserSubcommand === "ask") {
|
|
785
|
-
if (printProBrowserHelpIfRequested(browserArgs, "pro browser ask", io, {
|
|
786
|
-
valueFlags: [...ASK_PRO_VALUE_FLAGS],
|
|
787
|
-
booleanFlags: [...ASK_PRO_BOOLEAN_FLAGS]
|
|
788
|
-
})) {
|
|
789
|
-
return 0;
|
|
790
|
-
}
|
|
791
|
-
if (hasAskProDryRunMode(browserArgs) && hasAskProSendMode(browserArgs)) {
|
|
792
|
-
throw new Error("ask-pro cannot combine --dry-run and --send");
|
|
793
|
-
}
|
|
794
|
-
if (hasAskProDryRunMode(browserArgs)) {
|
|
795
|
-
throw new Error("prodex pro browser ask is an explicit visible-browser send. Use `prodex pro ask` for dry-run previews.");
|
|
796
|
-
}
|
|
797
|
-
const hasMode = hasAskProMode(browserArgs);
|
|
798
|
-
return runCli(["ask-pro", ...(hasMode ? [] : ["--send"]), ...browserArgs], { ...io, allowAskProBrowserSend: true });
|
|
799
|
-
}
|
|
800
|
-
if (browserSubcommand === "open" || browserSubcommand === "status" || browserSubcommand === "doctor") {
|
|
801
|
-
const replacement = browserSubcommand === "open" ? "login" : "check";
|
|
802
|
-
throw new Error(`Use \`prodex pro browser ${replacement}\` for explicit browser automation.`);
|
|
803
|
-
}
|
|
804
|
-
if (browserSubcommand === "smoke") {
|
|
805
|
-
if (printProBrowserHelpIfRequested(browserArgs, "pro browser smoke", io, { valueFlags: ["--cwd", "--port", "--timeout-ms", "--source-cli"] }))
|
|
806
|
-
return 0;
|
|
807
|
-
return runCli(["chatgpt", browserSubcommand, ...browserArgs], io);
|
|
808
|
-
}
|
|
809
|
-
if (browserSubcommand === "check") {
|
|
810
|
-
if (printProBrowserHelpIfRequested(browserArgs, "pro browser check", io, { valueFlags: ["--cwd", "--port", "--timeout-ms", "--source-cli"] }))
|
|
811
|
-
return 0;
|
|
812
|
-
assertOnlyOptions(browserArgs, "pro browser check", ["--cwd", "--port", "--timeout-ms", "--source-cli"]);
|
|
813
|
-
const targetCwd = resolveCwdFlag(io.cwd, browserArgs);
|
|
814
|
-
readPortFlag(browserArgs, "--port");
|
|
815
|
-
readPositiveNumberFlag(browserArgs, "--timeout-ms");
|
|
816
|
-
const healthy = await printProductCheck(new BridgeStore(targetCwd), io, browserArgs, targetCwd);
|
|
817
|
-
return healthy ? 0 : 1;
|
|
818
|
-
}
|
|
819
|
-
if (browserSubcommand === "models") {
|
|
820
|
-
if (printProBrowserHelpIfRequested(browserArgs, "pro browser models", io, { valueFlags: ["--port", "--timeout-ms", "--source-cli"] }))
|
|
821
|
-
return 0;
|
|
822
|
-
assertOnlyOptions(browserArgs, "pro browser models", ["--port", "--timeout-ms", "--source-cli"]);
|
|
823
|
-
const listed = await listChatGptModelOptions({
|
|
824
|
-
port: readPortFlag(browserArgs, "--port"),
|
|
825
|
-
timeoutMs: readPositiveNumberFlag(browserArgs, "--timeout-ms")
|
|
826
|
-
});
|
|
827
|
-
io.stdout("Model menu options in the visible ChatGPT tab (read-only; nothing was selected):");
|
|
828
|
-
for (const option of listed.options) {
|
|
829
|
-
const marker = option.checked ? "*" : " ";
|
|
830
|
-
const suffix = option.kind === "submenu" ? " (has sub-variants; not selectable via --model yet)" : "";
|
|
831
|
-
io.stdout(`${marker} ${option.label}${suffix}`);
|
|
832
|
-
}
|
|
833
|
-
io.stdout("Use radio entries with `pro browser ask --model/--effort`; Pro sub-modes via --pro-mode 기본|확장.");
|
|
834
|
-
return 0;
|
|
835
|
-
}
|
|
836
|
-
throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check", "models"]);
|
|
837
|
-
}
|
|
838
|
-
if (subcommand === "open" || subcommand === "status" || subcommand === "smoke" || subcommand === "check" || subcommand === "doctor") {
|
|
839
|
-
throw new Error(`Use \`prodex pro browser ${subcommand === "doctor" ? "check" : subcommand}\` for explicit browser automation.`);
|
|
840
|
-
}
|
|
841
|
-
if (subcommand === "list") {
|
|
842
|
-
if (printHelpIfRequested(proArgs, "pro list", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
843
|
-
return 0;
|
|
844
|
-
assertOnlyOptions(proArgs, "pro list", ["--cwd", "--source-cli"]);
|
|
845
|
-
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
846
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
847
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
|
|
848
|
-
const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
|
|
849
|
-
const consults = await listConsultListEntries(targetStore);
|
|
850
|
-
for (const entry of consults) {
|
|
851
|
-
if (entry.kind === "untrusted") {
|
|
852
|
-
io.stdout(`${entry.task.id}\tuntrusted\t${sourceAwareResultMessage(errorMessage(entry.error), sourceCli, answerOptions)}`);
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
io.stdout(`${entry.consult.task.id}\t${entry.consult.result.status}\t${formatProListSummary(entry.consult, sourceCli, answerOptions)}`);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
return 0;
|
|
859
|
-
}
|
|
860
|
-
if (subcommand === "latest") {
|
|
861
|
-
if (printHelpIfRequested(proArgs, "pro latest", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
862
|
-
return 0;
|
|
863
|
-
assertOnlyOptions(proArgs, "pro latest", ["--cwd", "--source-cli"]);
|
|
864
|
-
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
865
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
866
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
|
|
867
|
-
const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
|
|
868
|
-
let consult;
|
|
869
|
-
try {
|
|
870
|
-
consult = await latestTrustedConsult(targetStore);
|
|
871
|
-
}
|
|
872
|
-
catch (error) {
|
|
873
|
-
throw sourceAwareResultError(error, sourceCli, answerOptions);
|
|
874
|
-
}
|
|
875
|
-
if (!consult)
|
|
876
|
-
throw new Error("No GPT Pro answers found");
|
|
877
|
-
io.stdout(formatProAnswer(consult, sourceCli, answerOptions));
|
|
878
|
-
return 0;
|
|
879
|
-
}
|
|
880
|
-
if (subcommand === "show") {
|
|
881
|
-
if (printHelpIfRequested(proArgs, "pro show", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"], maxPositionals: 1 }))
|
|
882
|
-
return 0;
|
|
883
|
-
const [taskId] = readPositionalsWithOptions(proArgs, "pro show", 1, ["--cwd", "--source-cli"]);
|
|
884
|
-
if (!taskId)
|
|
885
|
-
throw new Error("pro show requires <task-id|latest>");
|
|
886
|
-
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
887
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
888
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
|
|
889
|
-
const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
|
|
890
|
-
let consult;
|
|
891
|
-
try {
|
|
892
|
-
consult = taskId === "latest" ? await latestTrustedConsult(targetStore) : await getConsult(targetStore, taskId, { readOnly: true });
|
|
893
|
-
}
|
|
894
|
-
catch (error) {
|
|
895
|
-
throw sourceAwareResultError(error, sourceCli, answerOptions);
|
|
896
|
-
}
|
|
897
|
-
if (!consult)
|
|
898
|
-
throw new Error(taskId === "latest" ? "No GPT Pro answers found" : `GPT Pro answer not found: ${taskId}`);
|
|
899
|
-
io.stdout(formatProAnswer(consult, sourceCli, answerOptions));
|
|
900
|
-
return 0;
|
|
901
|
-
}
|
|
902
|
-
throw unknownSubcommandError("pro", subcommand, ["ask", "browser", "list", "latest", "show"]);
|
|
903
|
-
}
|
|
904
|
-
if (command === "consults") {
|
|
905
|
-
throw new Error("The legacy `consults` alias is retired. Use `prodex pro list`, `prodex pro latest`, or `prodex pro show <task-id|latest>`.");
|
|
906
|
-
}
|
|
907
|
-
if (command === "ask-pro") {
|
|
908
|
-
const parsedAskPro = parseAskProArgs(rest);
|
|
909
|
-
const hasDryRunMode = parsedAskPro.optionArgs.includes("--dry-run");
|
|
910
|
-
const hasSendMode = parsedAskPro.optionArgs.includes("--send");
|
|
911
|
-
if (!hasDryRunMode && !hasSendMode) {
|
|
912
|
-
throw new Error("ask-pro requires --dry-run or --send");
|
|
913
|
-
}
|
|
914
|
-
if (hasDryRunMode && hasSendMode) {
|
|
915
|
-
throw new Error("ask-pro cannot combine --dry-run and --send");
|
|
916
|
-
}
|
|
917
|
-
if (hasSendMode && !io.allowAskProBrowserSend) {
|
|
918
|
-
throw new Error("Direct ask-pro --send is disabled. Use `prodex pro browser ask` for explicit visible-browser sends.");
|
|
919
|
-
}
|
|
920
|
-
const targetCwd = resolveCwdFlag(io.cwd, parsedAskPro.optionArgs);
|
|
921
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
922
|
-
const files = readRepeatedFlag(parsedAskPro.optionArgs, "--file");
|
|
923
|
-
const targetUrl = readFlag(parsedAskPro.optionArgs, "--target-url");
|
|
924
|
-
const normalizedTargetUrl = targetUrl ? normalizeChatGptTargetUrl(targetUrl) : undefined;
|
|
925
|
-
if (!normalizedTargetUrl && parsedAskPro.optionArgs.includes("--confirm-target")) {
|
|
926
|
-
throw new Error("--confirm-target requires --target-url so the visible browser target is explicit.");
|
|
927
|
-
}
|
|
928
|
-
if (normalizedTargetUrl && hasSendMode && !parsedAskPro.optionArgs.includes("--confirm-target")) {
|
|
929
|
-
throw new Error("--target-url requires --confirm-target after you manually verify the visible ChatGPT tab is the intended Project/thread.");
|
|
930
|
-
}
|
|
931
|
-
const prompt = parsedAskPro.promptParts.join(" ").trim();
|
|
932
|
-
if (!prompt)
|
|
933
|
-
throw new Error("ask-pro requires a prompt");
|
|
934
|
-
const browserDefaults = await loadBrowserDefaults(targetCwd);
|
|
935
|
-
const explicitProject = readFlag(parsedAskPro.optionArgs, "--project");
|
|
936
|
-
const explicitProjectNew = readFlag(parsedAskPro.optionArgs, "--project-new");
|
|
937
|
-
if (explicitProject !== undefined && explicitProjectNew !== undefined) {
|
|
938
|
-
throw new Error("ask-pro cannot combine --project and --project-new; pick an existing project or create one.");
|
|
939
|
-
}
|
|
940
|
-
if (normalizedTargetUrl && (explicitProject !== undefined || explicitProjectNew !== undefined)) {
|
|
941
|
-
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.");
|
|
942
|
-
}
|
|
943
|
-
const explicitModel = readFlag(parsedAskPro.optionArgs, "--model");
|
|
944
|
-
const explicitProModeRaw = readFlag(parsedAskPro.optionArgs, "--pro-mode");
|
|
945
|
-
const explicitEffortRaw = readFlag(parsedAskPro.optionArgs, "--effort");
|
|
946
|
-
if (explicitProModeRaw !== undefined && explicitEffortRaw !== undefined) {
|
|
947
|
-
throw new Error("ask-pro cannot combine --pro-mode and --effort; Pro sub-modes and reasoning effort are different model axes.");
|
|
948
|
-
}
|
|
949
|
-
const explicitProMode = explicitProModeRaw === undefined ? undefined : parseProMode(explicitProModeRaw);
|
|
950
|
-
const explicitEffort = explicitEffortRaw === undefined ? undefined : parseReasoningEffort(explicitEffortRaw);
|
|
951
|
-
// Explicit per-ask flags override persisted defaults. Choosing either
|
|
952
|
-
// reasoning axis explicitly suppresses the default for the other axis, and
|
|
953
|
-
// pinning --target-url suppresses a default project (it would navigate away
|
|
954
|
-
// from the confirmed tab).
|
|
955
|
-
const selectionModel = explicitModel ?? browserDefaults?.model;
|
|
956
|
-
const selectionProjectNew = explicitProjectNew;
|
|
957
|
-
const selectionProject = explicitProject ?? (normalizedTargetUrl || selectionProjectNew !== undefined ? undefined : browserDefaults?.project);
|
|
958
|
-
const reasoningAxisChosen = explicitProMode !== undefined || explicitEffort !== undefined;
|
|
959
|
-
const selectionProMode = explicitProMode ?? (reasoningAxisChosen ? undefined : browserDefaults?.pro_mode);
|
|
960
|
-
const selectionEffort = explicitEffort ?? (reasoningAxisChosen ? undefined : browserDefaults?.effort);
|
|
961
|
-
const selectionMetadata = {
|
|
962
|
-
...(selectionProject ? { project: selectionProject } : {}),
|
|
963
|
-
...(selectionProjectNew ? { project_new: selectionProjectNew } : {}),
|
|
964
|
-
...(selectionModel ? { model: selectionModel } : {}),
|
|
965
|
-
...(selectionProMode ? { pro_mode: selectionProMode } : {}),
|
|
966
|
-
...(selectionEffort ? { effort: selectionEffort } : {})
|
|
967
|
-
};
|
|
968
|
-
const browserPort = hasSendMode ? (readPortFlag(parsedAskPro.optionArgs, "--port") ?? 9333) : undefined;
|
|
969
|
-
// Pro extended can legitimately think for minutes, so its default timeout is
|
|
970
|
-
// higher; an explicit --timeout-ms always wins.
|
|
971
|
-
const defaultBrowserTimeoutMs = selectionProMode === "확장" ? 300_000 : 90_000;
|
|
972
|
-
const browserTimeoutMs = hasSendMode
|
|
973
|
-
? (readPositiveNumberFlag(parsedAskPro.optionArgs, "--timeout-ms") ?? defaultBrowserTimeoutMs)
|
|
974
|
-
: undefined;
|
|
975
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, parsedAskPro.optionArgs, "--source-cli");
|
|
976
|
-
const bundle = await buildDryRunBundle(targetCwd, { prompt, files });
|
|
977
|
-
if (hasSendMode) {
|
|
978
|
-
const browserCommandOptions = {
|
|
979
|
-
cwd: targetCwd,
|
|
980
|
-
port: parsedAskPro.optionArgs.includes("--port") ? browserPort : undefined
|
|
981
|
-
};
|
|
982
|
-
const task = await targetStore.createTask({
|
|
983
|
-
source: "codex",
|
|
984
|
-
title: "GPT Pro consult",
|
|
985
|
-
prompt: bundle.text,
|
|
986
|
-
repo_id: "default",
|
|
987
|
-
files: files.map((file) => ({ path: file, role: "context" })),
|
|
988
|
-
provenance: {
|
|
989
|
-
adapter: "chatgpt-control",
|
|
990
|
-
session_id: bundle.id,
|
|
991
|
-
thread: normalizedTargetUrl,
|
|
992
|
-
warnings: []
|
|
993
|
-
}
|
|
994
|
-
});
|
|
995
|
-
await targetStore.claimTask(task.id, "chatgpt-pro");
|
|
996
|
-
try {
|
|
997
|
-
await writeSessionBeforeBrowserSend(targetStore, {
|
|
998
|
-
id: bundle.id,
|
|
999
|
-
direction: "codex_to_chatgpt",
|
|
1000
|
-
backend: "chatgpt-control",
|
|
1001
|
-
task_id: task.id,
|
|
1002
|
-
thread: normalizedTargetUrl,
|
|
1003
|
-
status: "running",
|
|
1004
|
-
warnings: []
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
|
-
catch (error) {
|
|
1008
|
-
const blocker = {
|
|
1009
|
-
code: "session_record_failed",
|
|
1010
|
-
message: `Could not record ChatGPT browser session before send: ${errorMessage(error)}`,
|
|
1011
|
-
retryable: true,
|
|
1012
|
-
next_step: "Fix local .bridge write permissions, then rerun the consult."
|
|
1013
|
-
};
|
|
1014
|
-
try {
|
|
1015
|
-
await targetStore.completeTask(task.id, {
|
|
1016
|
-
status: "blocked",
|
|
1017
|
-
summary: blocker.message,
|
|
1018
|
-
commands: ["visible ChatGPT browser consult"],
|
|
1019
|
-
blocker
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
catch (recordError) {
|
|
1023
|
-
throw new Error(`${blocker.message} (also failed to record blocked consult: ${errorMessage(recordError)})`);
|
|
1024
|
-
}
|
|
1025
|
-
throw new Error(formatBlockedConsultRecordedMessage(blocker.message, task.id, sourceCli, { cwd: targetCwd }));
|
|
1026
|
-
}
|
|
1027
|
-
let consult;
|
|
1028
|
-
try {
|
|
1029
|
-
consult = await sendChatGptPrompt({
|
|
1030
|
-
port: browserPort,
|
|
1031
|
-
prompt: bundle.text,
|
|
1032
|
-
targetUrl: normalizedTargetUrl,
|
|
1033
|
-
timeoutMs: browserTimeoutMs,
|
|
1034
|
-
project: selectionProject,
|
|
1035
|
-
projectNew: selectionProjectNew,
|
|
1036
|
-
model: selectionModel,
|
|
1037
|
-
proMode: selectionProMode,
|
|
1038
|
-
effort: selectionEffort
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
catch (error) {
|
|
1042
|
-
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, browserCommandOptions);
|
|
1043
|
-
const message = blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error);
|
|
1044
|
-
try {
|
|
1045
|
-
await targetStore.completeTask(task.id, {
|
|
1046
|
-
status: "blocked",
|
|
1047
|
-
summary: message,
|
|
1048
|
-
commands: ["visible ChatGPT browser consult"],
|
|
1049
|
-
blocker
|
|
1050
|
-
});
|
|
1051
|
-
await writeSessionBestEffort(targetStore, {
|
|
1052
|
-
id: bundle.id,
|
|
1053
|
-
direction: "codex_to_chatgpt",
|
|
1054
|
-
backend: "chatgpt-control",
|
|
1055
|
-
task_id: task.id,
|
|
1056
|
-
thread: normalizedTargetUrl,
|
|
1057
|
-
status: "blocked",
|
|
1058
|
-
blocker,
|
|
1059
|
-
warnings: []
|
|
1060
|
-
}, io);
|
|
1061
|
-
}
|
|
1062
|
-
catch (recordError) {
|
|
1063
|
-
throw new Error(`${message} (also failed to record blocked consult: ${errorMessage(recordError)})`);
|
|
1064
|
-
}
|
|
1065
|
-
throw new Error(formatBlockedConsultRecordedMessage(message, task.id, sourceCli, { cwd: targetCwd }));
|
|
1066
|
-
}
|
|
1067
|
-
const answerArtifactText = formatProConsultArtifact(consult);
|
|
1068
|
-
const persistenceWarnings = [...consult.warnings];
|
|
1069
|
-
let answerArtifactPath;
|
|
1070
|
-
const answerArtifactBytes = Buffer.byteLength(answerArtifactText, "utf8");
|
|
1071
|
-
if (answerArtifactBytes > MAX_FETCHABLE_RESULT_ARTIFACT_BYTES) {
|
|
1072
|
-
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`;
|
|
1073
|
-
persistenceWarnings.push(warning);
|
|
1074
|
-
io.stderr(warning);
|
|
1075
|
-
}
|
|
1076
|
-
else {
|
|
1077
|
-
try {
|
|
1078
|
-
answerArtifactPath = await targetStore.writeArtifactText(`.bridge/artifacts/pro-consults/${task.id}.md`, answerArtifactText);
|
|
1079
|
-
}
|
|
1080
|
-
catch (error) {
|
|
1081
|
-
const warning = `answer_artifact_warning: ${errorMessage(error)}`;
|
|
1082
|
-
persistenceWarnings.push(warning);
|
|
1083
|
-
io.stderr(warning);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
try {
|
|
1087
|
-
await targetStore.writeReceipt({
|
|
1088
|
-
kind: "consult_answer_saved",
|
|
1089
|
-
task_id: task.id,
|
|
1090
|
-
session_id: bundle.id,
|
|
1091
|
-
summary: `Recorded ChatGPT answer for ${task.id}`,
|
|
1092
|
-
metadata: {
|
|
1093
|
-
...(answerArtifactPath ? { artifact_path: answerArtifactPath } : {}),
|
|
1094
|
-
thread: consult.url,
|
|
1095
|
-
...(Object.keys(selectionMetadata).length > 0 ? { selection: selectionMetadata } : {}),
|
|
1096
|
-
warnings: persistenceWarnings
|
|
1097
|
-
}
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
catch (error) {
|
|
1101
|
-
const warning = `receipt_record_warning: ${errorMessage(error)}`;
|
|
1102
|
-
persistenceWarnings.push(warning);
|
|
1103
|
-
io.stderr(warning);
|
|
1104
|
-
}
|
|
1105
|
-
let result;
|
|
1106
|
-
try {
|
|
1107
|
-
result = await targetStore.completeTask(task.id, {
|
|
1108
|
-
status: "done",
|
|
1109
|
-
summary: consult.answer,
|
|
1110
|
-
artifacts: answerArtifactPath ? [{ path: answerArtifactPath, role: "result", bytes: Buffer.byteLength(answerArtifactText, "utf8") }] : [],
|
|
1111
|
-
commands: ["visible ChatGPT browser consult"],
|
|
1112
|
-
warnings: persistenceWarnings,
|
|
1113
|
-
provenance: {
|
|
1114
|
-
thread: consult.url,
|
|
1115
|
-
warnings: persistenceWarnings
|
|
1116
|
-
}
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
catch (error) {
|
|
1120
|
-
io.stdout(`consult_answer_received_but_not_saved: ${task.id} ${consult.url}`);
|
|
1121
|
-
io.stdout("");
|
|
1122
|
-
io.stdout(consult.answer);
|
|
1123
|
-
throw new Error(`ChatGPT answer was received but local persistence failed: ${errorMessage(error)}`);
|
|
1124
|
-
}
|
|
1125
|
-
await writeSessionBestEffort(targetStore, {
|
|
1126
|
-
id: bundle.id,
|
|
1127
|
-
direction: "codex_to_chatgpt",
|
|
1128
|
-
backend: "chatgpt-control",
|
|
1129
|
-
task_id: task.id,
|
|
1130
|
-
thread: consult.url,
|
|
1131
|
-
status: "done",
|
|
1132
|
-
warnings: persistenceWarnings
|
|
1133
|
-
}, io);
|
|
1134
|
-
io.stdout(`${result.task_id}\t${result.status}\t${consult.url}`);
|
|
1135
|
-
io.stdout("");
|
|
1136
|
-
io.stdout(result.summary);
|
|
1137
|
-
}
|
|
1138
|
-
else {
|
|
1139
|
-
await writeSessionBestEffort(targetStore, {
|
|
1140
|
-
id: bundle.id,
|
|
1141
|
-
direction: "codex_to_chatgpt",
|
|
1142
|
-
backend: "manual",
|
|
1143
|
-
status: "preview",
|
|
1144
|
-
warnings: []
|
|
1145
|
-
}, io);
|
|
1146
|
-
await targetStore.writeReceipt({
|
|
1147
|
-
kind: "consult_preview",
|
|
1148
|
-
session_id: bundle.id,
|
|
1149
|
-
summary: `Created dry-run consult preview ${bundle.id}`
|
|
1150
|
-
});
|
|
1151
|
-
io.stdout(`DRY RUN ${bundle.id}`);
|
|
1152
|
-
io.stdout(bundle.text);
|
|
1153
|
-
}
|
|
1154
|
-
return 0;
|
|
1155
|
-
}
|
|
168
|
+
if (command === "chatgpt")
|
|
169
|
+
return runChatgptCommand(rest, io);
|
|
170
|
+
if (command === "tasks")
|
|
171
|
+
return runTasksCommand(rest, io);
|
|
172
|
+
if (command === "results")
|
|
173
|
+
return runResultsCommand(rest, io);
|
|
174
|
+
if (command === "receipts")
|
|
175
|
+
return runReceiptsCommand(rest, io);
|
|
176
|
+
if (command === "sessions")
|
|
177
|
+
return runSessionsCommand(rest, io);
|
|
178
|
+
if (command === "pro")
|
|
179
|
+
return runProCommand(rest, io, runCli);
|
|
180
|
+
if (command === "consults")
|
|
181
|
+
return runConsultsCommand(rest, io);
|
|
182
|
+
if (command === "ask-pro")
|
|
183
|
+
return runAskProCommand(rest, io);
|
|
1156
184
|
if (command === "mcp") {
|
|
1157
185
|
if (printHelpIfRequested(rest, "mcp", io.stdout, printMcpHelp, { valueFlags: ["--cwd"] }))
|
|
1158
186
|
return 0;
|
|
@@ -1172,26 +200,6 @@ function defaultIo() {
|
|
|
1172
200
|
function isHelpArgs(args) {
|
|
1173
201
|
return args.length > 0 && isHelpSubcommand(args[0]);
|
|
1174
202
|
}
|
|
1175
|
-
function printHelpIfRequested(args, command, stdout, printHelp, options = {}) {
|
|
1176
|
-
const helpIndex = findHelpFlagIndexBeforePromptDelimiter(args);
|
|
1177
|
-
if (helpIndex === -1)
|
|
1178
|
-
return false;
|
|
1179
|
-
assertHelpRequestArgs(args, command, options);
|
|
1180
|
-
printHelp(stdout);
|
|
1181
|
-
return true;
|
|
1182
|
-
}
|
|
1183
|
-
function printProBrowserHelpIfRequested(args, command, io, options) {
|
|
1184
|
-
const helpIndex = findHelpFlagIndexBeforePromptDelimiter(args);
|
|
1185
|
-
if (helpIndex === -1)
|
|
1186
|
-
return false;
|
|
1187
|
-
assertHelpRequestArgs(args, command, options);
|
|
1188
|
-
printProBrowserHelp(io.stdout, resolveOptionalFileFlag(io.cwd, args, "--source-cli"));
|
|
1189
|
-
return true;
|
|
1190
|
-
}
|
|
1191
|
-
function legacyChatGptNamespaceError(subcommand) {
|
|
1192
|
-
const prefix = subcommand ? `Unknown legacy chatgpt subcommand: ${subcommand}.` : "The legacy `chatgpt` namespace is hidden.";
|
|
1193
|
-
return new Error(`${prefix} Use \`prodex pro browser help\` for visible-browser commands.`);
|
|
1194
|
-
}
|
|
1195
203
|
function formatProjectVerificationPrompt(cwd, sourceCli) {
|
|
1196
204
|
const cli = formatCliCommand(sourceCli);
|
|
1197
205
|
const quotedCwd = shellQuote(cwd);
|
|
@@ -1365,183 +373,9 @@ function formatClaudeConfig(cwd, sourceCli) {
|
|
|
1365
373
|
}
|
|
1366
374
|
}, null, 2);
|
|
1367
375
|
}
|
|
1368
|
-
function formatInitCommand(sourceCli, options = {}) {
|
|
1369
|
-
return [`${formatCliCommand(sourceCli)} init`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined].filter(Boolean).join(" ");
|
|
1370
|
-
}
|
|
1371
|
-
function formatSetupCommand(sourceCli, options = {}) {
|
|
1372
|
-
return [`${formatCliCommand(sourceCli)} setup`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined].filter(Boolean).join(" ");
|
|
1373
|
-
}
|
|
1374
|
-
function formatBrowserLoginCommand(sourceCli, options = {}) {
|
|
1375
|
-
return formatCommandInCwd(formatBrowserLoginCommandBody(sourceCli, options), options.cwd);
|
|
1376
|
-
}
|
|
1377
|
-
function formatBrowserLoginCommandBody(sourceCli, options = {}) {
|
|
1378
|
-
const command = [
|
|
1379
|
-
`${formatCliCommand(sourceCli)} pro browser login${formatSourceCliOption(sourceCli)}`,
|
|
1380
|
-
options.profileDir ? `--profile-dir ${shellQuote(options.profileDir)}` : undefined,
|
|
1381
|
-
options.port ? `--port ${options.port}` : undefined,
|
|
1382
|
-
options.url ? `--url ${shellQuote(options.url)}` : undefined,
|
|
1383
|
-
options.launchTimeoutMs ? `--launch-timeout-ms ${options.launchTimeoutMs}` : undefined
|
|
1384
|
-
]
|
|
1385
|
-
.filter(Boolean)
|
|
1386
|
-
.join(" ");
|
|
1387
|
-
return command;
|
|
1388
|
-
}
|
|
1389
|
-
function formatBrowserSmokeCommand(sourceCli, options = {}) {
|
|
1390
|
-
return formatCommandInCwd(formatBrowserSmokeCommandBody(sourceCli, options), options.cwd);
|
|
1391
|
-
}
|
|
1392
|
-
function formatBrowserSmokeCommandBody(sourceCli, options = {}) {
|
|
1393
|
-
const command = [`${formatCliCommand(sourceCli)} pro browser smoke${formatSourceCliOption(sourceCli)}`, options.port ? `--port ${options.port}` : undefined]
|
|
1394
|
-
.filter(Boolean)
|
|
1395
|
-
.join(" ");
|
|
1396
|
-
return command;
|
|
1397
|
-
}
|
|
1398
|
-
// ask retry commands carry their own --target-url/--confirm-target/"prompt" tail, so callers
|
|
1399
|
-
// preserve the stored argument tail verbatim and only re-point this bare base at the source CLI.
|
|
1400
|
-
function formatBrowserAskCommandBody(sourceCli) {
|
|
1401
|
-
return `${formatCliCommand(sourceCli)} pro browser ask${formatSourceCliOption(sourceCli)}`;
|
|
1402
|
-
}
|
|
1403
|
-
function formatBrowserCheckCommand(sourceCli, options = {}) {
|
|
1404
|
-
const command = [`${formatCliCommand(sourceCli)} pro browser check${formatSourceCliOption(sourceCli)}`, options.port ? `--port ${options.port}` : undefined]
|
|
1405
|
-
.filter(Boolean)
|
|
1406
|
-
.join(" ");
|
|
1407
|
-
return formatCommandInCwd(command, options.cwd);
|
|
1408
|
-
}
|
|
1409
|
-
function formatBrowserTargetAskCommand(sourceCli, options = {}) {
|
|
1410
|
-
const command = [
|
|
1411
|
-
`${formatCliCommand(sourceCli)} pro browser ask${formatSourceCliOption(sourceCli)}`,
|
|
1412
|
-
options.port ? `--port ${options.port}` : undefined,
|
|
1413
|
-
`--target-url ${options.targetUrl ? shellQuote(options.targetUrl) : "<chatgpt-url>"} --confirm-target "prompt"`
|
|
1414
|
-
]
|
|
1415
|
-
.filter(Boolean)
|
|
1416
|
-
.join(" ");
|
|
1417
|
-
return formatCommandInCwd(command, options.cwd);
|
|
1418
|
-
}
|
|
1419
|
-
function formatCommandInCwd(command, cwd) {
|
|
1420
|
-
return cwd ? `cd ${shellQuote(cwd)} && ${command}` : command;
|
|
1421
|
-
}
|
|
1422
|
-
function formatProShowCommand(taskId, sourceCli, options = {}) {
|
|
1423
|
-
return [`${formatCliCommand(sourceCli)} pro show ${shellQuote(taskId)}${formatSourceCliOption(sourceCli)}`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined]
|
|
1424
|
-
.filter(Boolean)
|
|
1425
|
-
.join(" ");
|
|
1426
|
-
}
|
|
1427
|
-
function formatProLatestCommand(sourceCli, options = {}) {
|
|
1428
|
-
return [`${formatCliCommand(sourceCli)} pro latest${formatSourceCliOption(sourceCli)}`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined]
|
|
1429
|
-
.filter(Boolean)
|
|
1430
|
-
.join(" ");
|
|
1431
|
-
}
|
|
1432
|
-
function formatResultResealCommand(taskId, sourceCli, options = {}) {
|
|
1433
|
-
return [
|
|
1434
|
-
`${formatCliCommand(sourceCli)} results reseal ${shellQuote(taskId)} --confirm-current-result`,
|
|
1435
|
-
options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined
|
|
1436
|
-
]
|
|
1437
|
-
.filter(Boolean)
|
|
1438
|
-
.join(" ");
|
|
1439
|
-
}
|
|
1440
|
-
function sourceAwareResultMessage(message, sourceCli, options = {}) {
|
|
1441
|
-
if (!sourceCli && !options.cwd)
|
|
1442
|
-
return message;
|
|
1443
|
-
return message.replace(/`prodex results reseal ([^`\s]+) --confirm-current-result`/g, (_match, taskId) => `\`${formatResultResealCommand(taskId, sourceCli, options)}\``);
|
|
1444
|
-
}
|
|
1445
|
-
function sourceAwareResultError(error, sourceCli, options = {}) {
|
|
1446
|
-
if (!isUntrustedResultError(error))
|
|
1447
|
-
return error;
|
|
1448
|
-
return new Error(sourceAwareResultMessage(errorMessage(error), sourceCli, options), { cause: error });
|
|
1449
|
-
}
|
|
1450
|
-
function formatBlockedConsultRecordedMessage(message, taskId, sourceCli, options = {}) {
|
|
1451
|
-
return `${message}\nblocked consult recorded: ${taskId}; inspect with \`${formatProShowCommand(taskId, sourceCli, options)}\` or \`${formatProLatestCommand(sourceCli, options)}\`.`;
|
|
1452
|
-
}
|
|
1453
|
-
function formatReleaseStatusCommand(sourceCli, options = {}) {
|
|
1454
|
-
return [`${formatCliCommand(sourceCli)} release status${formatSourceCliOption(sourceCli)}`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined]
|
|
1455
|
-
.filter(Boolean)
|
|
1456
|
-
.join(" ");
|
|
1457
|
-
}
|
|
1458
|
-
function formatReleasePackCommand(sourceCli, options = {}) {
|
|
1459
|
-
return [
|
|
1460
|
-
`${formatCliCommand(sourceCli)} release pack${formatSourceCliOption(sourceCli)}`,
|
|
1461
|
-
options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined,
|
|
1462
|
-
"--pack-destination <dir>"
|
|
1463
|
-
]
|
|
1464
|
-
.filter(Boolean)
|
|
1465
|
-
.join(" ");
|
|
1466
|
-
}
|
|
1467
376
|
function formatGitPushUpstreamCommand(branch) {
|
|
1468
377
|
return `git push -u origin ${shellQuote(branch)}`;
|
|
1469
378
|
}
|
|
1470
|
-
function sourceAwareBrowserNextStep(nextStep, sourceCli, options = {}) {
|
|
1471
|
-
if (!nextStep)
|
|
1472
|
-
return nextStep;
|
|
1473
|
-
const targetRetry = nextStep.match(/^Open (https:\/\/chatgpt\.com\/\S+) in the (visible|dedicated) browser and retry(\. Current: .+|\.)$/);
|
|
1474
|
-
if (targetRetry) {
|
|
1475
|
-
const [, targetUrl, location, suffix] = targetRetry;
|
|
1476
|
-
return `Open ${targetUrl} in the ${location} browser and run \`${formatBrowserTargetAskCommand(sourceCli, {
|
|
1477
|
-
...options,
|
|
1478
|
-
targetUrl
|
|
1479
|
-
})}\`${suffix}`;
|
|
1480
|
-
}
|
|
1481
|
-
if (!sourceCli && !options.port && !options.cwd)
|
|
1482
|
-
return nextStep;
|
|
1483
|
-
return nextStep
|
|
1484
|
-
.replace(/`cd (.+?) && prodex pro browser login([^`]*)?`/g, (_match, cwdPrefix, storedArgs) => {
|
|
1485
|
-
return `\`cd ${cwdPrefix} && ${formatBrowserLoginCommandBody(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1486
|
-
})
|
|
1487
|
-
.replace(/`cd (.+?) && prodex pro browser smoke([^`]*)?`/g, (_match, cwdPrefix, storedArgs) => {
|
|
1488
|
-
return `\`cd ${cwdPrefix} && ${formatBrowserSmokeCommandBody(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1489
|
-
})
|
|
1490
|
-
.replace(/`cd (.+?) && prodex pro browser ask([^`]*)?`/g, (_match, cwdPrefix, storedArgs) => {
|
|
1491
|
-
return `\`cd ${cwdPrefix} && ${formatBrowserAskCommandBody(sourceCli)}${storedArgs ?? ""}\``;
|
|
1492
|
-
})
|
|
1493
|
-
.replace(/`prodex pro browser login([^`]*)?`/g, (_match, storedArgs) => {
|
|
1494
|
-
return `\`${formatBrowserLoginCommand(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1495
|
-
})
|
|
1496
|
-
.replace(/`prodex pro browser smoke([^`]*)?`/g, (_match, storedArgs) => {
|
|
1497
|
-
return `\`${formatBrowserSmokeCommand(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1498
|
-
})
|
|
1499
|
-
.replace(/`prodex pro browser ask([^`]*)?`/g, (_match, storedArgs) => {
|
|
1500
|
-
return `\`${formatBrowserAskCommandBody(sourceCli)}${storedArgs ?? ""}\``;
|
|
1501
|
-
})
|
|
1502
|
-
.replaceAll("pass --target-url with --confirm-target", `run \`${formatBrowserTargetAskCommand(sourceCli, options)}\``);
|
|
1503
|
-
}
|
|
1504
|
-
function browserOptionsWithStoredPort(options, storedArgs) {
|
|
1505
|
-
if (options.port || !storedArgs)
|
|
1506
|
-
return options;
|
|
1507
|
-
const match = storedArgs.match(/(?:^|\s)--port\s+(\d{1,5})(?:\s|$)/);
|
|
1508
|
-
if (!match)
|
|
1509
|
-
return options;
|
|
1510
|
-
const port = Number(match[1]);
|
|
1511
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
1512
|
-
return options;
|
|
1513
|
-
return { ...options, port };
|
|
1514
|
-
}
|
|
1515
|
-
function productCheckBrowserNextStep(nextStep, sourceCli, options = {}) {
|
|
1516
|
-
const sourceAware = sourceAwareBrowserNextStep(nextStep, sourceCli, options);
|
|
1517
|
-
if (!sourceAware)
|
|
1518
|
-
return sourceAware;
|
|
1519
|
-
if (sourceAware.includes("`"))
|
|
1520
|
-
return sourceAware;
|
|
1521
|
-
if (sourceAware.includes("pass --target-url with --confirm-target")) {
|
|
1522
|
-
return sourceAware.replace("pass --target-url with --confirm-target", `run \`${formatBrowserTargetAskCommand(sourceCli, options)}\``);
|
|
1523
|
-
}
|
|
1524
|
-
return sourceAware.replace(/(?:and|then) retry\.$/, `then run \`${formatBrowserSmokeCommand(sourceCli, options)}\`.`);
|
|
1525
|
-
}
|
|
1526
|
-
function sourceAwareBrowserBlocker(blocker, sourceCli, options = {}) {
|
|
1527
|
-
const nextStep = sourceAwareBrowserNextStep(blocker.next_step, sourceCli, options);
|
|
1528
|
-
return nextStep === blocker.next_step ? blocker : { ...blocker, next_step: nextStep };
|
|
1529
|
-
}
|
|
1530
|
-
function sourceAwareSetupMessage(message, sourceCli, options = {}) {
|
|
1531
|
-
if (!sourceCli && !options.cwd)
|
|
1532
|
-
return message;
|
|
1533
|
-
const setupCommand = formatSetupCommand(sourceCli, options);
|
|
1534
|
-
return message
|
|
1535
|
-
.replaceAll("`prodex setup --token-ttl-hours <hours>`", `\`${setupCommand} --token-ttl-hours <hours>\``)
|
|
1536
|
-
.replaceAll("`prodex setup`", `\`${setupCommand}\``);
|
|
1537
|
-
}
|
|
1538
|
-
function sourceAwareReleaseMessage(message, sourceCli, options = {}) {
|
|
1539
|
-
if (!sourceCli && !options.cwd)
|
|
1540
|
-
return message;
|
|
1541
|
-
return message
|
|
1542
|
-
.replaceAll("`prodex release pack --pack-destination <dir>`", `\`${formatReleasePackCommand(sourceCli, options)}\``)
|
|
1543
|
-
.replaceAll("`prodex release status`", `\`${formatReleaseStatusCommand(sourceCli, options)}\``);
|
|
1544
|
-
}
|
|
1545
379
|
async function formatReleaseStatus(cwd, sourceCli, releaseHintCwd) {
|
|
1546
380
|
const packageJsonPath = path.join(cwd, "package.json");
|
|
1547
381
|
const raw = await readReleasePackageJson(packageJsonPath).catch(async (error) => {
|
|
@@ -2398,217 +1232,6 @@ async function runMcpWriteSmoke() {
|
|
|
2398
1232
|
}
|
|
2399
1233
|
}
|
|
2400
1234
|
}
|
|
2401
|
-
function printBrowserLoginGuide(stdout, input) {
|
|
2402
|
-
const loginCommand = formatBrowserLoginCommand(input.sourceCli, input.commandOptions);
|
|
2403
|
-
const runtimeCommandOptions = {
|
|
2404
|
-
...(input.commandOptions?.cwd ? { cwd: input.commandOptions.cwd } : {}),
|
|
2405
|
-
...(input.commandOptions?.port ? { port: input.commandOptions.port } : {})
|
|
2406
|
-
};
|
|
2407
|
-
const checkCommand = formatBrowserCheckCommand(input.sourceCli, runtimeCommandOptions);
|
|
2408
|
-
const smokeCommand = formatBrowserSmokeCommand(input.sourceCli, runtimeCommandOptions);
|
|
2409
|
-
stdout("ChatGPT Pro browser login");
|
|
2410
|
-
stdout(input.opened ? "Opened the dedicated Chrome window for ChatGPT." : "Dry run: no browser was opened.");
|
|
2411
|
-
stdout("");
|
|
2412
|
-
stdout("Steps:");
|
|
2413
|
-
if (input.opened) {
|
|
2414
|
-
stdout(`1. Log in manually at ${input.loginUrl} in the dedicated Chrome window.`);
|
|
2415
|
-
stdout("2. If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, complete it in the browser.");
|
|
2416
|
-
stdout("3. For usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.");
|
|
2417
|
-
stdout("4. Open a normal ChatGPT chat or the intended Project/thread so the prompt composer is visible.");
|
|
2418
|
-
stdout("5. Select the Pro/Thinking model you want in the ChatGPT UI.");
|
|
2419
|
-
stdout(`6. Run \`${checkCommand}\` to confirm the session is reachable.`);
|
|
2420
|
-
stdout(`7. Run \`${smokeCommand}\` to verify a real Pro response path.`);
|
|
2421
|
-
}
|
|
2422
|
-
else {
|
|
2423
|
-
stdout(`1. Run \`${loginCommand}\` without \`--dry-run\` to open the dedicated Chrome window.`);
|
|
2424
|
-
stdout(`2. Log in manually at ${input.loginUrl} in that Chrome window.`);
|
|
2425
|
-
stdout("3. If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, complete it in the browser.");
|
|
2426
|
-
stdout("4. For usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.");
|
|
2427
|
-
stdout("5. Open a normal ChatGPT chat or the intended Project/thread so the prompt composer is visible.");
|
|
2428
|
-
stdout("6. Select the Pro/Thinking model you want in the ChatGPT UI.");
|
|
2429
|
-
stdout(`7. Run \`${checkCommand}\` to confirm the session is reachable.`);
|
|
2430
|
-
stdout(`8. Run \`${smokeCommand}\` to verify a real Pro response path.`);
|
|
2431
|
-
}
|
|
2432
|
-
stdout("");
|
|
2433
|
-
stdout(`Profile: ${input.profileDir}`);
|
|
2434
|
-
stdout(`Debug: http://127.0.0.1:${input.port}`);
|
|
2435
|
-
if (input.opened) {
|
|
2436
|
-
stdout("You can close this Chrome window after check/smoke or when you are done. The dedicated profile is reused next time.");
|
|
2437
|
-
}
|
|
2438
|
-
else {
|
|
2439
|
-
stdout("The dedicated profile path above will be reused by the real login command.");
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
async function assertBrowserLaunchStayedAlive(opened, timeoutMs) {
|
|
2443
|
-
const outcome = await waitForBrowserLaunchReady(opened, timeoutMs);
|
|
2444
|
-
if (outcome.reachable)
|
|
2445
|
-
return;
|
|
2446
|
-
if (outcome.earlyExit) {
|
|
2447
|
-
const detail = formatBrowserEarlyExit(outcome.earlyExit);
|
|
2448
|
-
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.`);
|
|
2449
|
-
}
|
|
2450
|
-
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.`);
|
|
2451
|
-
}
|
|
2452
|
-
async function waitForBrowserLaunchReady(opened, timeoutMs = 5_000) {
|
|
2453
|
-
const deadline = Date.now() + timeoutMs;
|
|
2454
|
-
let earlyExit;
|
|
2455
|
-
while (Date.now() <= deadline) {
|
|
2456
|
-
const remainingMs = Math.max(1, deadline - Date.now());
|
|
2457
|
-
const status = await getChatGptBrowserStatus({ port: opened.port, timeoutMs: Math.min(250, remainingMs) });
|
|
2458
|
-
if (status.reachable)
|
|
2459
|
-
return { reachable: true };
|
|
2460
|
-
earlyExit ??= await opened.waitForEarlyExit(1);
|
|
2461
|
-
if (earlyExit && (earlyExit.code !== 0 || earlyExit.signal || earlyExit.error)) {
|
|
2462
|
-
return { reachable: false, earlyExit };
|
|
2463
|
-
}
|
|
2464
|
-
if (Date.now() >= deadline)
|
|
2465
|
-
break;
|
|
2466
|
-
await sleep(Math.min(100, Math.max(1, deadline - Date.now())));
|
|
2467
|
-
}
|
|
2468
|
-
return { reachable: false, ...(earlyExit ? { earlyExit } : {}) };
|
|
2469
|
-
}
|
|
2470
|
-
function formatBrowserEarlyExit(exit) {
|
|
2471
|
-
if (!exit)
|
|
2472
|
-
return "no exit details";
|
|
2473
|
-
return exit.error ?? `exit code ${exit.code ?? "null"}${exit.signal ? ` signal ${exit.signal}` : ""}`;
|
|
2474
|
-
}
|
|
2475
|
-
function sleep(ms) {
|
|
2476
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2477
|
-
}
|
|
2478
|
-
function formatBrowserModelHints(modelHints) {
|
|
2479
|
-
const modelish = /\b(?:ChatGPT|GPT(?:-[\w.]+)?|Pro|Plus|Team|Enterprise|Thinking|Extra High|Auto)\b/i;
|
|
2480
|
-
const hints = [...new Set(modelHints.map((hint) => hint.trim()).filter((hint) => modelish.test(hint)))]
|
|
2481
|
-
.map((hint) => (hint.length > 80 ? `${hint.slice(0, 77)}...` : hint))
|
|
2482
|
-
.slice(0, 6);
|
|
2483
|
-
return hints.length > 0 ? hints.join(" | ") : undefined;
|
|
2484
|
-
}
|
|
2485
|
-
function browserReadinessNextStep(input) {
|
|
2486
|
-
if (!input.loggedInLikely) {
|
|
2487
|
-
return "Log in manually in the visible ChatGPT browser, then retry.";
|
|
2488
|
-
}
|
|
2489
|
-
if (!input.hasComposer) {
|
|
2490
|
-
return "Open a normal ChatGPT chat or Project thread, select the Pro/Thinking model, and retry.";
|
|
2491
|
-
}
|
|
2492
|
-
return "Review the visible ChatGPT browser state, then retry.";
|
|
2493
|
-
}
|
|
2494
|
-
async function printProductCheck(store, io, args, configCwd = io.cwd) {
|
|
2495
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, args, "--source-cli");
|
|
2496
|
-
const setupHintCwd = readFlag(args, "--cwd") ? configCwd : undefined;
|
|
2497
|
-
io.stdout("prodex product check");
|
|
2498
|
-
let bridgeReady = false;
|
|
2499
|
-
try {
|
|
2500
|
-
bridgeReady = await store.hasReadyBridgeStorageReadOnly();
|
|
2501
|
-
io.stdout(bridgeReady
|
|
2502
|
-
? "bridge: ok (.bridge)"
|
|
2503
|
-
: `bridge: missing (.bridge) - run \`${formatInitCommand(sourceCli, { cwd: setupHintCwd })}\` when you need local task/result storage`);
|
|
2504
|
-
}
|
|
2505
|
-
catch (error) {
|
|
2506
|
-
io.stdout(`bridge: blocked - ${errorMessage(error)}`);
|
|
2507
|
-
}
|
|
2508
|
-
let configReady = false;
|
|
2509
|
-
try {
|
|
2510
|
-
const config = await loadLocalConfig(configCwd);
|
|
2511
|
-
const tokenStatus = getTokenExpiryStatus(config);
|
|
2512
|
-
if (tokenStatus.status === "expired") {
|
|
2513
|
-
io.stdout(`config: expired - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\``);
|
|
2514
|
-
}
|
|
2515
|
-
else {
|
|
2516
|
-
io.stdout(`config: ok ${redactServerUrl(config.server_url)} token_status=${tokenStatus.status}`);
|
|
2517
|
-
const warningLine = formatConfigWarningLine(tokenStatus, sourceCli, setupHintCwd);
|
|
2518
|
-
if (warningLine)
|
|
2519
|
-
io.stdout(warningLine);
|
|
2520
|
-
configReady = true;
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
catch (error) {
|
|
2524
|
-
if (isMissingFileError(error)) {
|
|
2525
|
-
io.stdout(`config: missing - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\``);
|
|
2526
|
-
}
|
|
2527
|
-
else {
|
|
2528
|
-
io.stdout(`config: failed ${sourceAwareSetupMessage(errorMessage(error), sourceCli, { cwd: setupHintCwd })}`);
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
const browserStatus = await getChatGptBrowserStatus({
|
|
2532
|
-
port: readPortFlag(args, "--port") ?? 9333,
|
|
2533
|
-
timeoutMs: readPositiveNumberFlag(args, "--timeout-ms") ?? 1500
|
|
2534
|
-
});
|
|
2535
|
-
const browserCommandOptions = {
|
|
2536
|
-
cwd: setupHintCwd,
|
|
2537
|
-
port: readPortFlag(args, "--port") ?? undefined
|
|
2538
|
-
};
|
|
2539
|
-
let chatgptReady = false;
|
|
2540
|
-
const visibilityBlocker = chatGptVisibilityBlocker(browserStatus.visibilityState, browserStatus.url);
|
|
2541
|
-
if (!browserStatus.reachable) {
|
|
2542
|
-
io.stdout(`chatgpt: ${browserStatus.blocker?.code ?? "unreachable"} - ${browserStatus.blocker?.message ?? "browser is not reachable"}`);
|
|
2543
|
-
const nextStep = productCheckBrowserNextStep(browserStatus.blocker?.next_step, sourceCli, browserCommandOptions);
|
|
2544
|
-
if (nextStep)
|
|
2545
|
-
io.stdout(`next: ${nextStep}`);
|
|
2546
|
-
}
|
|
2547
|
-
else if (browserStatus.blocker) {
|
|
2548
|
-
const visibilityText = browserStatus.blocker.code === "tab_not_visible" ? ` visibility=${browserStatus.visibilityState ?? "unknown"}` : "";
|
|
2549
|
-
io.stdout(`chatgpt: blocked ${browserStatus.blocker.code}${visibilityText} - ${browserStatus.blocker.message}`);
|
|
2550
|
-
const nextStep = productCheckBrowserNextStep(browserStatus.blocker.next_step, sourceCli, browserCommandOptions);
|
|
2551
|
-
if (nextStep)
|
|
2552
|
-
io.stdout(`next: ${nextStep}`);
|
|
2553
|
-
}
|
|
2554
|
-
else if (visibilityBlocker) {
|
|
2555
|
-
io.stdout(`chatgpt: blocked ${visibilityBlocker.code} visibility=${browserStatus.visibilityState ?? "unknown"} - ${visibilityBlocker.message}`);
|
|
2556
|
-
const nextStep = productCheckBrowserNextStep(visibilityBlocker.next_step, sourceCli, browserCommandOptions);
|
|
2557
|
-
if (nextStep)
|
|
2558
|
-
io.stdout(`next: ${nextStep}`);
|
|
2559
|
-
}
|
|
2560
|
-
else if (browserStatus.loggedInLikely && browserStatus.hasComposer) {
|
|
2561
|
-
io.stdout(`chatgpt: ok logged_in=true composer=true${browserStatus.url ? ` url=${browserStatus.url}` : ""}`);
|
|
2562
|
-
chatgptReady = true;
|
|
2563
|
-
}
|
|
2564
|
-
else {
|
|
2565
|
-
io.stdout(`chatgpt: blocked logged_in=${browserStatus.loggedInLikely} composer=${browserStatus.hasComposer}`);
|
|
2566
|
-
const nextStep = productCheckBrowserNextStep(browserReadinessNextStep(browserStatus), sourceCli, browserCommandOptions);
|
|
2567
|
-
io.stdout(`next: ${nextStep}`);
|
|
2568
|
-
}
|
|
2569
|
-
const modelHints = formatBrowserModelHints(browserStatus.modelHints);
|
|
2570
|
-
if (modelHints)
|
|
2571
|
-
io.stdout(`model_hints: ${modelHints}`);
|
|
2572
|
-
if (bridgeReady) {
|
|
2573
|
-
try {
|
|
2574
|
-
const latest = await latestTrustedConsult(store, { readOnly: false });
|
|
2575
|
-
if (latest) {
|
|
2576
|
-
for (const line of formatProductCheckLatestProLines(latest, sourceCli, browserCommandOptions))
|
|
2577
|
-
io.stdout(line);
|
|
2578
|
-
}
|
|
2579
|
-
else {
|
|
2580
|
-
io.stdout("latest_pro: missing");
|
|
2581
|
-
}
|
|
2582
|
-
}
|
|
2583
|
-
catch (error) {
|
|
2584
|
-
if (isUntrustedResultError(error)) {
|
|
2585
|
-
io.stdout(`latest_pro: untrusted ${error.taskId} ${sourceAwareResultMessage(errorMessage(error), sourceCli, browserCommandOptions)}`);
|
|
2586
|
-
}
|
|
2587
|
-
else {
|
|
2588
|
-
io.stdout(`latest_pro: unavailable ${firstLine(errorMessage(error))}`);
|
|
2589
|
-
}
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
else {
|
|
2593
|
-
io.stdout("latest_pro: missing");
|
|
2594
|
-
}
|
|
2595
|
-
return bridgeReady && configReady && chatgptReady;
|
|
2596
|
-
}
|
|
2597
|
-
async function listTasksForInspection(store, status) {
|
|
2598
|
-
return store.listTasksReadOnly(status);
|
|
2599
|
-
}
|
|
2600
|
-
async function listResultsForInspection(store) {
|
|
2601
|
-
return store.listFinalizedResultsReadOnly();
|
|
2602
|
-
}
|
|
2603
|
-
async function listRawResultsForInspection(store) {
|
|
2604
|
-
return store.listResultsReadOnly();
|
|
2605
|
-
}
|
|
2606
|
-
async function listReceiptsForInspection(store, input = {}) {
|
|
2607
|
-
return store.listReceiptsReadOnly(input);
|
|
2608
|
-
}
|
|
2609
|
-
async function listSessionsForInspection(store, status) {
|
|
2610
|
-
return store.listSessionsReadOnly(status);
|
|
2611
|
-
}
|
|
2612
1235
|
async function listConsults(store, options = {}) {
|
|
2613
1236
|
const [tasks, results] = options.readOnly
|
|
2614
1237
|
? await Promise.all([listTasksForInspection(store), listRawResultsForInspection(store)])
|
|
@@ -2629,356 +1252,6 @@ async function listConsults(store, options = {}) {
|
|
|
2629
1252
|
}
|
|
2630
1253
|
return finalized;
|
|
2631
1254
|
}
|
|
2632
|
-
async function listConsultListEntries(store, options = { readOnly: true }) {
|
|
2633
|
-
const [tasks, results] = options.readOnly === false
|
|
2634
|
-
? await Promise.all([store.listTasks(), store.listResults()])
|
|
2635
|
-
: await Promise.all([listTasksForInspection(store), listRawResultsForInspection(store)]);
|
|
2636
|
-
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
2637
|
-
assertNoMissingTerminalConsultResults(tasks, results);
|
|
2638
|
-
assertNoOrphanConsultResults(tasksById, results);
|
|
2639
|
-
const records = results
|
|
2640
|
-
.map((result) => {
|
|
2641
|
-
const task = tasksById.get(result.task_id);
|
|
2642
|
-
return task ? { task, result } : undefined;
|
|
2643
|
-
})
|
|
2644
|
-
.filter((record) => Boolean(record && isConsultRecord(record)))
|
|
2645
|
-
.sort((a, b) => b.result.created_at.localeCompare(a.result.created_at));
|
|
2646
|
-
const entries = [];
|
|
2647
|
-
for (const record of records) {
|
|
2648
|
-
try {
|
|
2649
|
-
entries.push({ kind: "trusted", consult: { ...record, result: await store.getFinalizedResultReadOnly(record.result.task_id) } });
|
|
2650
|
-
}
|
|
2651
|
-
catch (error) {
|
|
2652
|
-
if (isUntrustedResultError(error)) {
|
|
2653
|
-
entries.push({ kind: "untrusted", task: record.task, result: record.result, error });
|
|
2654
|
-
continue;
|
|
2655
|
-
}
|
|
2656
|
-
throw error;
|
|
2657
|
-
}
|
|
2658
|
-
}
|
|
2659
|
-
return entries;
|
|
2660
|
-
}
|
|
2661
|
-
async function latestTrustedConsult(store, options = { readOnly: true }) {
|
|
2662
|
-
const entries = await listConsultListEntries(store, options);
|
|
2663
|
-
const trusted = entries.find((entry) => entry.kind === "trusted");
|
|
2664
|
-
if (trusted)
|
|
2665
|
-
return trusted.consult;
|
|
2666
|
-
const untrusted = entries.find((entry) => entry.kind === "untrusted");
|
|
2667
|
-
if (untrusted)
|
|
2668
|
-
throw untrusted.error;
|
|
2669
|
-
return undefined;
|
|
2670
|
-
}
|
|
2671
|
-
function assertNoOrphanConsultResults(tasksById, results) {
|
|
2672
|
-
const orphan = results
|
|
2673
|
-
.filter((result) => !tasksById.has(result.task_id) && isConsultResult(result))
|
|
2674
|
-
.sort((a, b) => b.created_at.localeCompare(a.created_at) || b.task_id.localeCompare(a.task_id))[0];
|
|
2675
|
-
if (orphan)
|
|
2676
|
-
throw orphanConsultResultError(orphan.task_id);
|
|
2677
|
-
}
|
|
2678
|
-
async function latestResultTaskId(store, options = {}) {
|
|
2679
|
-
const results = options.readOnly ? await listResultsForInspection(store) : await store.listResults();
|
|
2680
|
-
const result = results.at(-1);
|
|
2681
|
-
if (!result)
|
|
2682
|
-
throw new Error("No results found");
|
|
2683
|
-
return result.task_id;
|
|
2684
|
-
}
|
|
2685
|
-
async function latestRawResultTaskId(store) {
|
|
2686
|
-
const result = (await store.listResults()).at(-1);
|
|
2687
|
-
if (!result)
|
|
2688
|
-
throw new Error("No results found");
|
|
2689
|
-
return result.task_id;
|
|
2690
|
-
}
|
|
2691
|
-
async function latestTask(store, options = {}) {
|
|
2692
|
-
const tasks = options.readOnly ? await listTasksForInspection(store) : await store.listTasks();
|
|
2693
|
-
return tasks.sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id))[0];
|
|
2694
|
-
}
|
|
2695
|
-
async function writeTaskCompleteArtifacts(store, values) {
|
|
2696
|
-
const artifacts = [];
|
|
2697
|
-
for (const value of values) {
|
|
2698
|
-
const separator = value.indexOf("=");
|
|
2699
|
-
if (separator <= 0) {
|
|
2700
|
-
throw new Error("tasks complete --artifact requires path=text");
|
|
2701
|
-
}
|
|
2702
|
-
const artifactPath = value.slice(0, separator);
|
|
2703
|
-
const content = value.slice(separator + 1);
|
|
2704
|
-
if (!artifactPath.trim()) {
|
|
2705
|
-
throw new Error("tasks complete --artifact requires path=text");
|
|
2706
|
-
}
|
|
2707
|
-
const storedPath = await store.writeArtifactText(artifactPath, content);
|
|
2708
|
-
artifacts.push({ path: storedPath, role: "result" });
|
|
2709
|
-
}
|
|
2710
|
-
return artifacts;
|
|
2711
|
-
}
|
|
2712
|
-
async function getConsult(store, taskId, options = {}) {
|
|
2713
|
-
let task;
|
|
2714
|
-
try {
|
|
2715
|
-
task = options.readOnly ? await store.getTaskReadOnly(taskId) : await store.getTask(taskId);
|
|
2716
|
-
}
|
|
2717
|
-
catch (error) {
|
|
2718
|
-
if (isMissingFileError(error))
|
|
2719
|
-
return undefined;
|
|
2720
|
-
throw error;
|
|
2721
|
-
}
|
|
2722
|
-
if (!isConsultTask(task))
|
|
2723
|
-
return undefined;
|
|
2724
|
-
let result;
|
|
2725
|
-
try {
|
|
2726
|
-
result = await store.getFinalizedResultReadOnly(taskId);
|
|
2727
|
-
}
|
|
2728
|
-
catch (error) {
|
|
2729
|
-
if (isMissingFileError(error)) {
|
|
2730
|
-
if (isTerminalTask(task) && isConsultTask(task))
|
|
2731
|
-
throw missingConsultResultError(task);
|
|
2732
|
-
return undefined;
|
|
2733
|
-
}
|
|
2734
|
-
throw error;
|
|
2735
|
-
}
|
|
2736
|
-
if (!task || !result)
|
|
2737
|
-
return undefined;
|
|
2738
|
-
const record = { task, result };
|
|
2739
|
-
return isConsultRecord(record) ? record : undefined;
|
|
2740
|
-
}
|
|
2741
|
-
function assertNoMissingTerminalConsultResults(tasks, results) {
|
|
2742
|
-
const resultTaskIds = new Set(results.map((result) => result.task_id));
|
|
2743
|
-
const missing = tasks
|
|
2744
|
-
.filter((task) => isTerminalTask(task) && isConsultTask(task) && !resultTaskIds.has(task.id))
|
|
2745
|
-
.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.id.localeCompare(a.id))[0];
|
|
2746
|
-
if (missing)
|
|
2747
|
-
throw missingConsultResultError(missing);
|
|
2748
|
-
}
|
|
2749
|
-
function isTerminalTask(task) {
|
|
2750
|
-
return task.status === "done" || task.status === "blocked";
|
|
2751
|
-
}
|
|
2752
|
-
function isConsultTask(task) {
|
|
2753
|
-
return task.provenance.adapter === "chatgpt-control";
|
|
2754
|
-
}
|
|
2755
|
-
function isConsultResult(result) {
|
|
2756
|
-
return result.commands.some((command) => /chatgpt|gpt pro|visible ChatGPT/i.test(command));
|
|
2757
|
-
}
|
|
2758
|
-
function missingConsultResultError(task) {
|
|
2759
|
-
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.`);
|
|
2760
|
-
}
|
|
2761
|
-
function orphanConsultResultError(taskId) {
|
|
2762
|
-
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.`);
|
|
2763
|
-
}
|
|
2764
|
-
function isConsultRecord(record) {
|
|
2765
|
-
return isConsultTask(record.task);
|
|
2766
|
-
}
|
|
2767
|
-
function formatProAnswer(consult, sourceCli, options = {}) {
|
|
2768
|
-
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
2769
|
-
const summary = sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker);
|
|
2770
|
-
const lines = [
|
|
2771
|
-
`task_id: ${consult.task.id}`,
|
|
2772
|
-
`status: ${consult.result.status}`,
|
|
2773
|
-
consult.task.provenance.thread ? `thread: ${consult.task.provenance.thread}` : undefined,
|
|
2774
|
-
`created_at: ${consult.result.created_at}`,
|
|
2775
|
-
"",
|
|
2776
|
-
summary
|
|
2777
|
-
].filter((line) => line !== undefined);
|
|
2778
|
-
if (blocker) {
|
|
2779
|
-
lines.push("", "blocker:", `- code: ${blocker.code}`, `- retryable: ${blocker.retryable}`);
|
|
2780
|
-
if (blocker.next_step)
|
|
2781
|
-
lines.push(`- next_step: ${blocker.next_step}`);
|
|
2782
|
-
}
|
|
2783
|
-
if (consult.result.warnings.length > 0) {
|
|
2784
|
-
lines.push("", "warnings:");
|
|
2785
|
-
for (const warning of consult.result.warnings)
|
|
2786
|
-
lines.push(`- ${warning}`);
|
|
2787
|
-
}
|
|
2788
|
-
return lines.join("\n");
|
|
2789
|
-
}
|
|
2790
|
-
function formatProListSummary(consult, sourceCli, options = {}) {
|
|
2791
|
-
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
2792
|
-
return firstLine(sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker));
|
|
2793
|
-
}
|
|
2794
|
-
function formatProductCheckLatestProLines(consult, sourceCli, options = {}) {
|
|
2795
|
-
if (consult.result.status === "blocked") {
|
|
2796
|
-
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
2797
|
-
const code = blocker?.code ?? "unknown";
|
|
2798
|
-
const retryable = blocker?.retryable ?? false;
|
|
2799
|
-
const lines = [`latest_pro: blocked ${consult.task.id} code=${code} retryable=${retryable} ${consult.result.created_at}`];
|
|
2800
|
-
if (blocker?.next_step)
|
|
2801
|
-
lines.push(`latest_pro_next: ${blocker.next_step}`);
|
|
2802
|
-
return lines;
|
|
2803
|
-
}
|
|
2804
|
-
return [`latest_pro: ok ${consult.task.id} ${consult.result.status} ${consult.result.created_at}`];
|
|
2805
|
-
}
|
|
2806
|
-
function sourceAwareProAnswerBlocker(consult, sourceCli, options = {}) {
|
|
2807
|
-
if (!consult.result.blocker)
|
|
2808
|
-
return undefined;
|
|
2809
|
-
const browserAware = sourceAwareBrowserBlocker(consult.result.blocker, sourceCli, options);
|
|
2810
|
-
if ((!sourceCli && !options.cwd && !options.port) || !isSmokeConsultRecord(consult) || !browserAware.next_step)
|
|
2811
|
-
return browserAware;
|
|
2812
|
-
const nextStep = productCheckBrowserNextStep(browserAware.next_step, sourceCli, options);
|
|
2813
|
-
return nextStep === browserAware.next_step ? browserAware : { ...browserAware, next_step: nextStep };
|
|
2814
|
-
}
|
|
2815
|
-
function sourceAwareProAnswerSummary(summary, originalBlocker, displayedBlocker) {
|
|
2816
|
-
const originalNextStep = originalBlocker?.next_step;
|
|
2817
|
-
const displayedNextStep = displayedBlocker?.next_step;
|
|
2818
|
-
if (!originalNextStep || !displayedNextStep || originalNextStep === displayedNextStep)
|
|
2819
|
-
return summary;
|
|
2820
|
-
return summary.replaceAll(originalNextStep, displayedNextStep);
|
|
2821
|
-
}
|
|
2822
|
-
function isSmokeConsultRecord(consult) {
|
|
2823
|
-
return consult.task.title === "GPT Pro smoke" || consult.result.commands.includes("visible ChatGPT browser smoke");
|
|
2824
|
-
}
|
|
2825
|
-
function receiptInspectionListSuffix(receipt) {
|
|
2826
|
-
const status = receipt.metadata.integrity_status;
|
|
2827
|
-
if (typeof status === "object" &&
|
|
2828
|
-
status !== null &&
|
|
2829
|
-
"trusted" in status &&
|
|
2830
|
-
status.trusted === false) {
|
|
2831
|
-
return "\tintegrity=untrusted";
|
|
2832
|
-
}
|
|
2833
|
-
return "";
|
|
2834
|
-
}
|
|
2835
|
-
function formatSession(session) {
|
|
2836
|
-
return JSON.stringify({
|
|
2837
|
-
id: session.id,
|
|
2838
|
-
status: session.status,
|
|
2839
|
-
direction: session.direction,
|
|
2840
|
-
backend: session.backend,
|
|
2841
|
-
project: session.project,
|
|
2842
|
-
thread: session.thread,
|
|
2843
|
-
task_id: session.task_id,
|
|
2844
|
-
blocker: session.blocker,
|
|
2845
|
-
warnings: session.warnings,
|
|
2846
|
-
created_at: session.created_at,
|
|
2847
|
-
last_used_at: session.last_used_at
|
|
2848
|
-
}, null, 2);
|
|
2849
|
-
}
|
|
2850
|
-
function formatProConsultArtifact(consult) {
|
|
2851
|
-
const lines = [`# ChatGPT Pro Consult`, "", `Thread: ${consult.url}`, `Title: ${consult.title}`, ""];
|
|
2852
|
-
if (consult.modelHints.length > 0) {
|
|
2853
|
-
lines.push("Model hints:", ...consult.modelHints.map((hint) => `- ${hint}`), "");
|
|
2854
|
-
}
|
|
2855
|
-
if (consult.warnings.length > 0) {
|
|
2856
|
-
lines.push("Warnings:", ...consult.warnings.map((warning) => `- ${warning}`), "");
|
|
2857
|
-
}
|
|
2858
|
-
lines.push("## Answer", "", consult.answer.trim(), "");
|
|
2859
|
-
return lines.join("\n");
|
|
2860
|
-
}
|
|
2861
|
-
async function writeSessionBestEffort(store, input, io) {
|
|
2862
|
-
try {
|
|
2863
|
-
await store.writeSession(input);
|
|
2864
|
-
}
|
|
2865
|
-
catch (error) {
|
|
2866
|
-
io.stderr(`session_record_warning: ${errorMessage(error)}`);
|
|
2867
|
-
}
|
|
2868
|
-
}
|
|
2869
|
-
async function writeSessionBeforeBrowserSend(store, input) {
|
|
2870
|
-
try {
|
|
2871
|
-
await store.writeSession(input);
|
|
2872
|
-
}
|
|
2873
|
-
catch (error) {
|
|
2874
|
-
throw new Error(`failed to record running consult session before browser send: ${errorMessage(error)}`);
|
|
2875
|
-
}
|
|
2876
|
-
}
|
|
2877
|
-
function firstLine(value) {
|
|
2878
|
-
return value.split(/\r?\n/).find((line) => line.trim())?.trim() ?? "";
|
|
2879
|
-
}
|
|
2880
|
-
function errorMessage(error) {
|
|
2881
|
-
return error instanceof Error ? error.message : String(error);
|
|
2882
|
-
}
|
|
2883
|
-
function browserSendBlockerFromError(error) {
|
|
2884
|
-
const blocker = typeof error === "object" && error !== null && "blocker" in error ? error.blocker : undefined;
|
|
2885
|
-
if (typeof blocker === "object" &&
|
|
2886
|
-
blocker !== null &&
|
|
2887
|
-
"code" in blocker &&
|
|
2888
|
-
"message" in blocker &&
|
|
2889
|
-
"retryable" in blocker &&
|
|
2890
|
-
typeof blocker.code === "string" &&
|
|
2891
|
-
typeof blocker.message === "string" &&
|
|
2892
|
-
typeof blocker.retryable === "boolean") {
|
|
2893
|
-
return {
|
|
2894
|
-
code: blocker.code,
|
|
2895
|
-
message: blocker.message,
|
|
2896
|
-
retryable: blocker.retryable,
|
|
2897
|
-
...("next_step" in blocker && typeof blocker.next_step === "string" ? { next_step: blocker.next_step } : {})
|
|
2898
|
-
};
|
|
2899
|
-
}
|
|
2900
|
-
const message = errorMessage(error);
|
|
2901
|
-
return {
|
|
2902
|
-
code: "browser_send_failed",
|
|
2903
|
-
message,
|
|
2904
|
-
retryable: true,
|
|
2905
|
-
next_step: "Resolve the visible browser issue manually, then rerun the consult if needed."
|
|
2906
|
-
};
|
|
2907
|
-
}
|
|
2908
|
-
function isMissingFileError(error) {
|
|
2909
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2910
|
-
}
|
|
2911
|
-
function isUntrustedResultError(error) {
|
|
2912
|
-
return (typeof error === "object" &&
|
|
2913
|
-
error !== null &&
|
|
2914
|
-
"code" in error &&
|
|
2915
|
-
"taskId" in error &&
|
|
2916
|
-
error.code === "EUNTRUSTED_RESULT" &&
|
|
2917
|
-
typeof error.taskId === "string");
|
|
2918
|
-
}
|
|
2919
|
-
function assertTokenNotExpiredForCommand(config, sourceCli, setupHintCwd) {
|
|
2920
|
-
const tokenStatus = getTokenExpiryStatus(config);
|
|
2921
|
-
if (tokenStatus.status === "expired") {
|
|
2922
|
-
throw new Error(sourceAwareSetupMessage(tokenStatus.warning.toLowerCase(), sourceCli, { cwd: setupHintCwd }));
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
async function loadLocalConfigForCommand(cwd, command, sourceCli, setupHintCwd) {
|
|
2926
|
-
return loadLocalConfig(cwd).catch(async (error) => {
|
|
2927
|
-
if (isMissingFileError(error)) {
|
|
2928
|
-
throw new Error(sourceAwareSetupMessage(`${command} requires local MCP setup. Run \`prodex setup\` first. Add \`--token-ttl-hours <hours>\` before revealing token URLs, using tunnels, or connecting ChatGPT Projects.`, sourceCli, { cwd: setupHintCwd }));
|
|
2929
|
-
}
|
|
2930
|
-
throw new Error(sourceAwareSetupMessage(errorMessage(error), sourceCli, { cwd: setupHintCwd }));
|
|
2931
|
-
});
|
|
2932
|
-
}
|
|
2933
|
-
function redactServerUrl(value) {
|
|
2934
|
-
return formatServerUrlForOutput(value, { showToken: false });
|
|
2935
|
-
}
|
|
2936
|
-
function formatServerUrlForOutput(value, options) {
|
|
2937
|
-
try {
|
|
2938
|
-
const url = new URL(value);
|
|
2939
|
-
url.username = "";
|
|
2940
|
-
url.password = "";
|
|
2941
|
-
if (!options.showToken && url.searchParams.has("prodex_token"))
|
|
2942
|
-
url.searchParams.set("prodex_token", "***");
|
|
2943
|
-
return url.toString();
|
|
2944
|
-
}
|
|
2945
|
-
catch {
|
|
2946
|
-
const withoutUserinfo = value.replace(/\/\/[^/@\s]+@/g, "//");
|
|
2947
|
-
return options.showToken ? withoutUserinfo : withoutUserinfo.replace(/([?&]prodex_token=)[^&]+/g, "$1***");
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
function makeTunnelMcpUrl(publicUrl, token) {
|
|
2951
|
-
const url = parseTunnelPublicUrl(publicUrl);
|
|
2952
|
-
url.username = "";
|
|
2953
|
-
url.password = "";
|
|
2954
|
-
url.pathname = "/mcp";
|
|
2955
|
-
url.search = "";
|
|
2956
|
-
url.hash = "";
|
|
2957
|
-
url.searchParams.set("prodex_token", token);
|
|
2958
|
-
return url.toString();
|
|
2959
|
-
}
|
|
2960
|
-
function parseTunnelPublicUrl(publicUrl) {
|
|
2961
|
-
let url;
|
|
2962
|
-
try {
|
|
2963
|
-
url = new URL(publicUrl);
|
|
2964
|
-
}
|
|
2965
|
-
catch {
|
|
2966
|
-
throw new Error("--public-url must be a valid URL");
|
|
2967
|
-
}
|
|
2968
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
2969
|
-
throw new Error("--public-url must use http or https");
|
|
2970
|
-
}
|
|
2971
|
-
if (url.protocol !== "https:" && !isLoopbackHost(url.hostname)) {
|
|
2972
|
-
throw new Error("--public-url must use https for non-loopback tunnel URLs");
|
|
2973
|
-
}
|
|
2974
|
-
return url;
|
|
2975
|
-
}
|
|
2976
|
-
function isLoopbackHost(hostname) {
|
|
2977
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
2978
|
-
}
|
|
2979
|
-
function readChatGptBrowserUrlFlag(args) {
|
|
2980
|
-
return normalizeChatGptTargetUrl(readFlag(args, "--url") ?? "https://chatgpt.com/");
|
|
2981
|
-
}
|
|
2982
1255
|
function resolveOptionalPathFlag(defaultCwd, args, flag) {
|
|
2983
1256
|
const value = readFlag(args, flag);
|
|
2984
1257
|
return value ? resolveExistingPathFlag(defaultCwd, value, flag) : undefined;
|
|
@@ -2989,163 +1262,6 @@ function readRequiredLeadingArgument(args, command, placeholder) {
|
|
|
2989
1262
|
throw new Error(`${command} requires ${placeholder}`);
|
|
2990
1263
|
return value;
|
|
2991
1264
|
}
|
|
2992
|
-
function parseBrowserDefaultFlags(args) {
|
|
2993
|
-
const model = readFlag(args, "--model");
|
|
2994
|
-
const proModeRaw = readFlag(args, "--pro-mode");
|
|
2995
|
-
const effortRaw = readFlag(args, "--effort");
|
|
2996
|
-
const project = readFlag(args, "--project");
|
|
2997
|
-
if (proModeRaw !== undefined && effortRaw !== undefined) {
|
|
2998
|
-
throw new Error("setup cannot combine --pro-mode and --effort; Pro sub-modes and reasoning effort are different model axes.");
|
|
2999
|
-
}
|
|
3000
|
-
const clears = [
|
|
3001
|
-
["--clear-model", "--model", model],
|
|
3002
|
-
["--clear-pro-mode", "--pro-mode", proModeRaw],
|
|
3003
|
-
["--clear-effort", "--effort", effortRaw],
|
|
3004
|
-
["--clear-project", "--project", project]
|
|
3005
|
-
].map(([clearFlag, setFlag, setValue]) => {
|
|
3006
|
-
const wantsClear = args.includes(clearFlag);
|
|
3007
|
-
if (wantsClear && setValue !== undefined) {
|
|
3008
|
-
throw new Error(`setup cannot combine ${setFlag} and ${clearFlag}; set a new default or clear it, not both.`);
|
|
3009
|
-
}
|
|
3010
|
-
return wantsClear;
|
|
3011
|
-
});
|
|
3012
|
-
const [clearModel, clearProMode, clearEffort, clearProject] = clears;
|
|
3013
|
-
const anySet = model !== undefined || proModeRaw !== undefined || effortRaw !== undefined || project !== undefined;
|
|
3014
|
-
if (!anySet && !clears.some(Boolean))
|
|
3015
|
-
return undefined;
|
|
3016
|
-
// Cleared fields are passed as explicit undefined so the config merge deletes
|
|
3017
|
-
// them while untouched fields survive.
|
|
3018
|
-
return {
|
|
3019
|
-
...(model !== undefined ? { model } : {}),
|
|
3020
|
-
...(clearModel ? { model: undefined } : {}),
|
|
3021
|
-
...(proModeRaw !== undefined ? { proMode: parseProMode(proModeRaw) } : {}),
|
|
3022
|
-
...(clearProMode ? { proMode: undefined } : {}),
|
|
3023
|
-
...(effortRaw !== undefined ? { effort: parseReasoningEffort(effortRaw) } : {}),
|
|
3024
|
-
...(clearEffort ? { effort: undefined } : {}),
|
|
3025
|
-
...(project !== undefined ? { project } : {}),
|
|
3026
|
-
...(clearProject ? { project: undefined } : {})
|
|
3027
|
-
};
|
|
3028
|
-
}
|
|
3029
|
-
function resolvePromptUser(io) {
|
|
3030
|
-
if (io.promptUser)
|
|
3031
|
-
return io.promptUser;
|
|
3032
|
-
if (!process.stdin.isTTY) {
|
|
3033
|
-
throw new Error("setup --interactive needs a terminal (TTY). Use the --model/--pro-mode/--effort/--project flags instead.");
|
|
3034
|
-
}
|
|
3035
|
-
return async (question) => {
|
|
3036
|
-
const readline = await import("node:readline/promises");
|
|
3037
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3038
|
-
try {
|
|
3039
|
-
return (await rl.question(question)).trim();
|
|
3040
|
-
}
|
|
3041
|
-
finally {
|
|
3042
|
-
rl.close();
|
|
3043
|
-
}
|
|
3044
|
-
};
|
|
3045
|
-
}
|
|
3046
|
-
// Ask up to `attempts` times; empty input means skip (returns undefined).
|
|
3047
|
-
async function askChoice(prompt, question, choices, attempts = 3) {
|
|
3048
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
3049
|
-
const raw = (await prompt(question)).trim();
|
|
3050
|
-
if (raw === "")
|
|
3051
|
-
return undefined;
|
|
3052
|
-
const index = Number.parseInt(raw, 10);
|
|
3053
|
-
if (Number.isInteger(index) && index >= 1 && index <= choices.length)
|
|
3054
|
-
return choices[index - 1];
|
|
3055
|
-
}
|
|
3056
|
-
throw new Error(`No valid choice after ${attempts} attempts; run setup again or use flags.`);
|
|
3057
|
-
}
|
|
3058
|
-
async function runBrowserDefaultsWizard(prompt, stdout) {
|
|
3059
|
-
stdout("Browser send defaults (press Enter to skip a question; labels match the Korean ChatGPT UI):");
|
|
3060
|
-
const model = await askChoice(prompt, "Default model — 1) Pro [Enter=skip]: ", ["Pro"]);
|
|
3061
|
-
let proMode;
|
|
3062
|
-
let effort;
|
|
3063
|
-
if (model === "Pro") {
|
|
3064
|
-
proMode = (await askChoice(prompt, "Pro sub-mode — 1) 기본 2) 확장 [Enter=skip]: ", ["기본", "확장"]));
|
|
3065
|
-
}
|
|
3066
|
-
else {
|
|
3067
|
-
effort = (await askChoice(prompt, "Reasoning effort — 1) 즉시 2) 중간 3) 높음 4) 매우 높음 [Enter=skip]: ", [
|
|
3068
|
-
"즉시",
|
|
3069
|
-
"중간",
|
|
3070
|
-
"높음",
|
|
3071
|
-
"매우 높음"
|
|
3072
|
-
]));
|
|
3073
|
-
}
|
|
3074
|
-
const projectRaw = (await prompt('Default project name (existing sidebar project) [Enter=skip]: ')).trim();
|
|
3075
|
-
const project = projectRaw === "" ? undefined : projectRaw;
|
|
3076
|
-
if (model === undefined && proMode === undefined && effort === undefined && project === undefined)
|
|
3077
|
-
return undefined;
|
|
3078
|
-
return {
|
|
3079
|
-
...(model !== undefined ? { model } : {}),
|
|
3080
|
-
...(proMode !== undefined ? { proMode } : {}),
|
|
3081
|
-
...(effort !== undefined ? { effort } : {}),
|
|
3082
|
-
...(project !== undefined ? { project } : {})
|
|
3083
|
-
};
|
|
3084
|
-
}
|
|
3085
|
-
function formatBrowserDefaults(defaults) {
|
|
3086
|
-
const parts = [];
|
|
3087
|
-
if (defaults.model)
|
|
3088
|
-
parts.push(`model=${defaults.model}`);
|
|
3089
|
-
if (defaults.pro_mode)
|
|
3090
|
-
parts.push(`pro-mode=${defaults.pro_mode}`);
|
|
3091
|
-
if (defaults.effort)
|
|
3092
|
-
parts.push(`effort=${defaults.effort}`);
|
|
3093
|
-
if (defaults.project)
|
|
3094
|
-
parts.push(`project=${defaults.project}`);
|
|
3095
|
-
return parts.length > 0 ? parts.join(", ") : "(none)";
|
|
3096
|
-
}
|
|
3097
|
-
function formatTokenExpiryLine(config) {
|
|
3098
|
-
const tokenStatus = getTokenExpiryStatus(config);
|
|
3099
|
-
if (tokenStatus.status === "valid")
|
|
3100
|
-
return `Token expires: ${tokenStatus.token_expires_at}`;
|
|
3101
|
-
if (tokenStatus.status === "expired")
|
|
3102
|
-
return `Token expired: ${tokenStatus.token_expires_at}`;
|
|
3103
|
-
return "Token expires: never (local-only; use --token-ttl-hours before exposing through a tunnel).";
|
|
3104
|
-
}
|
|
3105
|
-
function formatConfigWarningLine(tokenStatus, sourceCli, setupHintCwd) {
|
|
3106
|
-
return tokenStatus.warning ? `config_warning: ${sourceAwareSetupMessage(tokenStatus.warning, sourceCli, { cwd: setupHintCwd })}` : undefined;
|
|
3107
|
-
}
|
|
3108
|
-
async function ensureBridgeGitignore(cwd) {
|
|
3109
|
-
const bridgeIgnorePath = path.join(cwd, ".bridge", ".gitignore");
|
|
3110
|
-
await mkdir(path.dirname(bridgeIgnorePath), { recursive: true });
|
|
3111
|
-
await writeVerifiedUtf8File(bridgeIgnorePath, ["tasks/*.json", "results/*.json", "sessions/*.json", "receipts/*.json", "artifacts/*", "config.local.json", "receipt-key.local", "!.gitignore", ""].join("\n"), () => assertGitignoreTargetSafe(bridgeIgnorePath), { create: true });
|
|
3112
|
-
const rootIgnorePath = path.join(cwd, ".gitignore");
|
|
3113
|
-
let current = "";
|
|
3114
|
-
try {
|
|
3115
|
-
current = await readVerifiedUtf8File(rootIgnorePath, () => assertGitignoreTargetSafe(rootIgnorePath));
|
|
3116
|
-
}
|
|
3117
|
-
catch (error) {
|
|
3118
|
-
if (!isMissingFileError(error))
|
|
3119
|
-
throw error;
|
|
3120
|
-
}
|
|
3121
|
-
const ignored = new Set(current.split(/\r?\n/).filter(Boolean));
|
|
3122
|
-
const additions = ["node_modules/", "dist/"].filter((line) => !ignored.has(line));
|
|
3123
|
-
if (additions.length > 0) {
|
|
3124
|
-
await writeVerifiedUtf8File(rootIgnorePath, `${current}${current && !current.endsWith("\n") ? "\n" : ""}${additions.join("\n")}\n`, () => assertGitignoreTargetSafe(rootIgnorePath), { create: true });
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
async function assertGitignoreTargetSafe(filePath) {
|
|
3128
|
-
try {
|
|
3129
|
-
const stat = await lstat(filePath);
|
|
3130
|
-
if (stat.isSymbolicLink())
|
|
3131
|
-
throw new Error(`${filePath} must not be a symlink`);
|
|
3132
|
-
if (!stat.isFile())
|
|
3133
|
-
throw new Error(`${filePath} must be a regular file`);
|
|
3134
|
-
}
|
|
3135
|
-
catch (error) {
|
|
3136
|
-
if (isMissingFileError(error))
|
|
3137
|
-
return;
|
|
3138
|
-
throw error;
|
|
3139
|
-
}
|
|
3140
|
-
}
|
|
3141
|
-
async function waitForShutdown(close) {
|
|
3142
|
-
await new Promise((resolve) => {
|
|
3143
|
-
const shutdown = () => resolve();
|
|
3144
|
-
process.once("SIGINT", shutdown);
|
|
3145
|
-
process.once("SIGTERM", shutdown);
|
|
3146
|
-
});
|
|
3147
|
-
await close();
|
|
3148
|
-
}
|
|
3149
1265
|
function isDirectCliInvocation() {
|
|
3150
1266
|
if (!process.argv[1])
|
|
3151
1267
|
return false;
|