@youdie006/prodex 0.5.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/README.md +1 -1
- package/dist/chatgpt-browser.d.ts +3 -3
- package/dist/chatgpt-browser.js +37 -17
- package/dist/chatgpt-browser.js.map +1 -1
- package/dist/cli-args.d.ts +53 -0
- package/dist/cli-args.js +340 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-help.d.ts +20 -0
- package/dist/cli-help.js +259 -0
- package/dist/cli-help.js.map +1 -0
- 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 +67 -2535
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { realpathSync
|
|
4
|
-
import { lstat,
|
|
5
|
-
import { createRequire } from "node:module";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { lstat, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
5
|
import { tmpdir } from "node:os";
|
|
7
6
|
import path from "node:path";
|
|
8
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,43 +9,20 @@ import { promisify } from "node:util";
|
|
|
10
9
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
11
10
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
12
11
|
import { renderBanner, shouldColorize } from "./banner.js";
|
|
13
|
-
import {
|
|
14
|
-
import { chatGptVisibilityBlocker, defaultChatGptProfileDir, getChatGptBrowserStatus, normalizeChatGptTargetUrl, listChatGptModelOptions, openChatGptBrowser, parseProMode, parseReasoningEffort, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
15
|
-
import { getTokenExpiryStatus, loadBrowserDefaults, loadLocalConfig, writeLocalConfig } from "./config.js";
|
|
12
|
+
import { getTokenExpiryStatus, loadLocalConfig } from "./config.js";
|
|
16
13
|
import { startHttpMcpServer } from "./http-mcp.js";
|
|
17
14
|
import { createMcpToolHandlers } from "./mcp-tools.js";
|
|
18
15
|
import { runMcpServer } from "./mcp.js";
|
|
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
|
-
const requirePackageJson = createRequire(import.meta.url);
|
|
24
|
-
const packageJson = requirePackageJson("../package.json");
|
|
25
24
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
|
-
const CLI_VERSION = packageJson.version ?? "0.0.0";
|
|
27
25
|
const RESERVED_PACKAGE_NAMES = new Set(["node_modules", "favicon.ico"]);
|
|
28
|
-
const PRO_BROWSER_SMOKE_TOKEN = "PRODEX_PRO_SMOKE_OK";
|
|
29
|
-
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.";
|
|
30
|
-
const TOP_LEVEL_COMMANDS = [
|
|
31
|
-
"help",
|
|
32
|
-
"version",
|
|
33
|
-
"init",
|
|
34
|
-
"setup",
|
|
35
|
-
"start",
|
|
36
|
-
"status",
|
|
37
|
-
"tunnel",
|
|
38
|
-
"doctor",
|
|
39
|
-
"onboard",
|
|
40
|
-
"project",
|
|
41
|
-
"claude",
|
|
42
|
-
"tasks",
|
|
43
|
-
"results",
|
|
44
|
-
"receipts",
|
|
45
|
-
"sessions",
|
|
46
|
-
"pro",
|
|
47
|
-
"release",
|
|
48
|
-
"mcp"
|
|
49
|
-
];
|
|
50
26
|
const DOCTOR_REQUIRED_MCP_TOOLS = [
|
|
51
27
|
"bridge_create_task",
|
|
52
28
|
"bridge_list_tasks",
|
|
@@ -80,179 +56,16 @@ export async function runCli(args, io = defaultIo()) {
|
|
|
80
56
|
printHelp(io.stdout);
|
|
81
57
|
return 0;
|
|
82
58
|
}
|
|
83
|
-
if (command === "init")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
if (command === "setup") {
|
|
95
|
-
const setupValueFlags = ["--cwd", "--host", "--port", "--token", "--token-ttl-hours", ...ASK_PRO_SELECTION_DEFAULT_FLAGS];
|
|
96
|
-
const setupBooleanFlags = [...ASK_PRO_SELECTION_CLEAR_FLAGS, "--interactive"];
|
|
97
|
-
if (printHelpIfRequested(rest, "setup", io.stdout, printSetupHelp, { valueFlags: setupValueFlags, booleanFlags: setupBooleanFlags }))
|
|
98
|
-
return 0;
|
|
99
|
-
assertOnlyOptions(rest, "setup", setupValueFlags, setupBooleanFlags);
|
|
100
|
-
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
101
|
-
const interactive = rest.includes("--interactive");
|
|
102
|
-
if (interactive) {
|
|
103
|
-
const conflicting = [...ASK_PRO_SELECTION_DEFAULT_FLAGS, ...ASK_PRO_SELECTION_CLEAR_FLAGS].filter((flag) => rest.includes(flag));
|
|
104
|
-
if (conflicting.length > 0) {
|
|
105
|
-
throw new Error(`setup --interactive collects the browser defaults itself; drop ${conflicting.join(", ")} or run without --interactive.`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const browserDefaults = interactive
|
|
109
|
-
? await runBrowserDefaultsWizard(resolvePromptUser(io), io.stdout)
|
|
110
|
-
: parseBrowserDefaultFlags(rest);
|
|
111
|
-
const config = await writeLocalConfig(targetCwd, {
|
|
112
|
-
host: readFlag(rest, "--host") ?? "127.0.0.1",
|
|
113
|
-
port: readPortFlag(rest, "--port") ?? 8787,
|
|
114
|
-
token: readFlag(rest, "--token"),
|
|
115
|
-
tokenTtlHours: readPositiveNumberFlag(rest, "--token-ttl-hours"),
|
|
116
|
-
browserDefaults
|
|
117
|
-
});
|
|
118
|
-
io.stdout("Saved local ChatGPT Developer Mode MCP profile.");
|
|
119
|
-
io.stdout(`Server URL: ${redactServerUrl(config.server_url)}`);
|
|
120
|
-
io.stdout(formatTokenExpiryLine(config));
|
|
121
|
-
io.stdout("Full URL is stored in .bridge/config.local.json.");
|
|
122
|
-
if (config.browser_defaults) {
|
|
123
|
-
io.stdout(`Browser send defaults: ${formatBrowserDefaults(config.browser_defaults)}`);
|
|
124
|
-
}
|
|
125
|
-
return 0;
|
|
126
|
-
}
|
|
127
|
-
if (command === "start") {
|
|
128
|
-
if (printHelpIfRequested(rest, "start", io.stdout, printStartHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
129
|
-
return 0;
|
|
130
|
-
assertOnlyOptions(rest, "start", ["--cwd", "--source-cli"]);
|
|
131
|
-
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
132
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, rest, "--source-cli");
|
|
133
|
-
const setupHintCwd = readFlag(rest, "--cwd") ? targetCwd : undefined;
|
|
134
|
-
const config = await loadLocalConfigForCommand(targetCwd, "start", sourceCli, setupHintCwd);
|
|
135
|
-
assertTokenNotExpiredForCommand(config, sourceCli, setupHintCwd);
|
|
136
|
-
const running = await startHttpMcpServer({
|
|
137
|
-
cwd: targetCwd,
|
|
138
|
-
host: config.host,
|
|
139
|
-
port: config.port,
|
|
140
|
-
token: config.token,
|
|
141
|
-
tokenExpiresAt: config.token_expires_at
|
|
142
|
-
});
|
|
143
|
-
io.stdout(`prodex HTTP MCP listening on ${redactServerUrl(running.mcp_url)}`);
|
|
144
|
-
io.stdout(formatTokenExpiryLine(config));
|
|
145
|
-
await waitForShutdown(async () => running.close());
|
|
146
|
-
return 0;
|
|
147
|
-
}
|
|
148
|
-
if (command === "status") {
|
|
149
|
-
if (printHelpIfRequested(rest, "status", io.stdout, printStatusHelp, {
|
|
150
|
-
valueFlags: ["--cwd", "--source-cli"],
|
|
151
|
-
booleanFlags: ["--show-token", "--url-only", "--unsafe-show-non-expiring-token"]
|
|
152
|
-
})) {
|
|
153
|
-
return 0;
|
|
154
|
-
}
|
|
155
|
-
assertOnlyOptions(rest, "status", ["--cwd", "--source-cli"], ["--show-token", "--url-only", "--unsafe-show-non-expiring-token"]);
|
|
156
|
-
const targetCwd = resolveCwdFlag(io.cwd, rest);
|
|
157
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, rest, "--source-cli");
|
|
158
|
-
const setupHintCwd = readFlag(rest, "--cwd") ? targetCwd : undefined;
|
|
159
|
-
const config = await loadLocalConfigForCommand(targetCwd, "status", sourceCli, setupHintCwd);
|
|
160
|
-
const showToken = rest.includes("--show-token");
|
|
161
|
-
const allowNonExpiringTokenReveal = rest.includes("--unsafe-show-non-expiring-token");
|
|
162
|
-
const tokenStatus = getTokenExpiryStatus(config);
|
|
163
|
-
if (showToken && tokenStatus.status === "non_expiring" && !allowNonExpiringTokenReveal) {
|
|
164
|
-
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 }));
|
|
165
|
-
}
|
|
166
|
-
if (showToken && tokenStatus.status === "expired") {
|
|
167
|
-
throw new Error(sourceAwareSetupMessage(`token expired at ${tokenStatus.token_expires_at}. Run \`prodex setup --token-ttl-hours <hours>\`.`, sourceCli, {
|
|
168
|
-
cwd: setupHintCwd
|
|
169
|
-
}));
|
|
170
|
-
}
|
|
171
|
-
const nonExpiringRevealWarning = showToken && allowNonExpiringTokenReveal && tokenStatus.status === "non_expiring"
|
|
172
|
-
? 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 })
|
|
173
|
-
: undefined;
|
|
174
|
-
const serverUrl = formatServerUrlForOutput(config.server_url, { showToken });
|
|
175
|
-
if (rest.includes("--url-only")) {
|
|
176
|
-
if (showToken)
|
|
177
|
-
io.stderr(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
178
|
-
if (nonExpiringRevealWarning)
|
|
179
|
-
io.stderr(nonExpiringRevealWarning);
|
|
180
|
-
io.stdout(serverUrl);
|
|
181
|
-
return 0;
|
|
182
|
-
}
|
|
183
|
-
const warnings = tokenStatus.warning ? [sourceAwareSetupMessage(tokenStatus.warning, sourceCli, { cwd: setupHintCwd })] : [];
|
|
184
|
-
if (showToken)
|
|
185
|
-
warnings.push(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
186
|
-
if (nonExpiringRevealWarning)
|
|
187
|
-
warnings.push(nonExpiringRevealWarning);
|
|
188
|
-
io.stdout(JSON.stringify({
|
|
189
|
-
server_url: serverUrl,
|
|
190
|
-
config_path: ".bridge/config.local.json",
|
|
191
|
-
token_status: tokenStatus.status,
|
|
192
|
-
token_expires_at: tokenStatus.token_expires_at ?? null,
|
|
193
|
-
browser_defaults: config.browser_defaults ?? null,
|
|
194
|
-
warnings
|
|
195
|
-
}, null, 2));
|
|
196
|
-
return 0;
|
|
197
|
-
}
|
|
198
|
-
if (command === "tunnel") {
|
|
199
|
-
const [subcommand, ...tunnelArgs] = rest;
|
|
200
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
201
|
-
assertNoExtraArgs(tunnelArgs, "tunnel help", 0);
|
|
202
|
-
printTunnelHelp(io.stdout);
|
|
203
|
-
return 0;
|
|
204
|
-
}
|
|
205
|
-
if (subcommand !== "url")
|
|
206
|
-
throw unknownSubcommandError("tunnel", subcommand, ["url"]);
|
|
207
|
-
if (printHelpIfRequested(tunnelArgs, "tunnel url", io.stdout, printTunnelUrlHelp, {
|
|
208
|
-
valueFlags: ["--cwd", "--public-url", "--source-cli"],
|
|
209
|
-
booleanFlags: ["--show-token", "--url-only"]
|
|
210
|
-
})) {
|
|
211
|
-
return 0;
|
|
212
|
-
}
|
|
213
|
-
assertOnlyOptions(tunnelArgs, "tunnel url", ["--cwd", "--public-url", "--source-cli"], ["--show-token", "--url-only"]);
|
|
214
|
-
const targetCwd = resolveCwdFlag(io.cwd, tunnelArgs);
|
|
215
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, tunnelArgs, "--source-cli");
|
|
216
|
-
const setupHintCwd = readFlag(tunnelArgs, "--cwd") ? targetCwd : undefined;
|
|
217
|
-
const publicUrl = readFlag(tunnelArgs, "--public-url");
|
|
218
|
-
if (!publicUrl)
|
|
219
|
-
throw new Error("tunnel url requires --public-url <https-url>");
|
|
220
|
-
parseTunnelPublicUrl(publicUrl);
|
|
221
|
-
const config = await loadLocalConfigForCommand(targetCwd, "tunnel url", sourceCli, setupHintCwd);
|
|
222
|
-
const tokenStatus = getTokenExpiryStatus(config);
|
|
223
|
-
if (tokenStatus.status === "non_expiring") {
|
|
224
|
-
throw new Error(sourceAwareSetupMessage("tunnel url requires a short-lived token. Run `prodex setup --token-ttl-hours <hours>` first.", sourceCli, {
|
|
225
|
-
cwd: setupHintCwd
|
|
226
|
-
}));
|
|
227
|
-
}
|
|
228
|
-
if (tokenStatus.status === "expired") {
|
|
229
|
-
throw new Error(sourceAwareSetupMessage(`token expired at ${tokenStatus.token_expires_at}. Run \`prodex setup --token-ttl-hours <hours>\`.`, sourceCli, {
|
|
230
|
-
cwd: setupHintCwd
|
|
231
|
-
}));
|
|
232
|
-
}
|
|
233
|
-
const mcpUrl = makeTunnelMcpUrl(publicUrl, config.token);
|
|
234
|
-
const showToken = tunnelArgs.includes("--show-token");
|
|
235
|
-
const outputUrl = showToken ? mcpUrl : redactServerUrl(mcpUrl);
|
|
236
|
-
if (tunnelArgs.includes("--url-only")) {
|
|
237
|
-
if (showToken)
|
|
238
|
-
io.stderr(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
239
|
-
io.stdout(outputUrl);
|
|
240
|
-
return 0;
|
|
241
|
-
}
|
|
242
|
-
const warnings = [
|
|
243
|
-
"This command does not create a tunnel. Keep `prodex start` running behind your own tunnel.",
|
|
244
|
-
"Only paste the token-bearing URL into a trusted private MCP client."
|
|
245
|
-
];
|
|
246
|
-
if (showToken)
|
|
247
|
-
warnings.push(TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING);
|
|
248
|
-
io.stdout(JSON.stringify({
|
|
249
|
-
mcp_url: outputUrl,
|
|
250
|
-
token_status: tokenStatus.status,
|
|
251
|
-
token_expires_at: tokenStatus.token_expires_at,
|
|
252
|
-
warnings
|
|
253
|
-
}, null, 2));
|
|
254
|
-
return 0;
|
|
255
|
-
}
|
|
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);
|
|
256
69
|
if (command === "doctor") {
|
|
257
70
|
if (printHelpIfRequested(rest, "doctor", io.stdout, printDoctorHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
258
71
|
return 0;
|
|
@@ -352,830 +165,22 @@ export async function runCli(args, io = defaultIo()) {
|
|
|
352
165
|
}
|
|
353
166
|
throw unknownSubcommandError("claude", subcommand, ["prompt", "config"]);
|
|
354
167
|
}
|
|
355
|
-
if (command === "chatgpt")
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
return 0;
|
|
372
|
-
}
|
|
373
|
-
if (subcommand === "status") {
|
|
374
|
-
assertOnlyOptions(chatgptArgs, "chatgpt status", ["--port"]);
|
|
375
|
-
const status = await getChatGptBrowserStatus({ port: readPortFlag(chatgptArgs, "--port") ?? 9333 });
|
|
376
|
-
io.stdout(JSON.stringify(status, null, 2));
|
|
377
|
-
return 0;
|
|
378
|
-
}
|
|
379
|
-
if (subcommand === "smoke") {
|
|
380
|
-
assertOnlyOptions(chatgptArgs, "chatgpt smoke", ["--cwd", "--port", "--timeout-ms", "--source-cli"]);
|
|
381
|
-
const targetCwd = resolveCwdFlag(io.cwd, chatgptArgs);
|
|
382
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
383
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, chatgptArgs, "--source-cli");
|
|
384
|
-
const port = readPortFlag(chatgptArgs, "--port") ?? 9333;
|
|
385
|
-
const timeoutMs = readPositiveNumberFlag(chatgptArgs, "--timeout-ms") ?? 90000;
|
|
386
|
-
const commandOptions = {
|
|
387
|
-
...(readFlag(chatgptArgs, "--cwd") ? { cwd: targetCwd } : {}),
|
|
388
|
-
...(readFlag(chatgptArgs, "--port") ? { port } : {})
|
|
389
|
-
};
|
|
390
|
-
const smokePrompt = `This is a one-time prodex smoke test. Reply exactly: ${PRO_BROWSER_SMOKE_TOKEN}`;
|
|
391
|
-
const recordBlockedSmoke = async (summary, blocker, thread) => {
|
|
392
|
-
const bundle = await buildDryRunBundle(targetCwd, { prompt: smokePrompt, files: [] });
|
|
393
|
-
const task = await targetStore.createTask({
|
|
394
|
-
source: "codex",
|
|
395
|
-
title: "GPT Pro smoke",
|
|
396
|
-
prompt: bundle.text,
|
|
397
|
-
repo_id: "default",
|
|
398
|
-
provenance: {
|
|
399
|
-
adapter: "chatgpt-control",
|
|
400
|
-
session_id: bundle.id,
|
|
401
|
-
thread,
|
|
402
|
-
warnings: []
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
await targetStore.claimTask(task.id, "chatgpt-pro");
|
|
406
|
-
await targetStore.completeTask(task.id, {
|
|
407
|
-
status: "blocked",
|
|
408
|
-
summary,
|
|
409
|
-
commands: ["visible ChatGPT browser smoke"],
|
|
410
|
-
blocker
|
|
411
|
-
});
|
|
412
|
-
await writeSessionBestEffort(targetStore, {
|
|
413
|
-
id: bundle.id,
|
|
414
|
-
direction: "codex_to_chatgpt",
|
|
415
|
-
backend: "chatgpt-control",
|
|
416
|
-
task_id: task.id,
|
|
417
|
-
thread,
|
|
418
|
-
status: "blocked",
|
|
419
|
-
blocker,
|
|
420
|
-
warnings: []
|
|
421
|
-
}, io);
|
|
422
|
-
return task.id;
|
|
423
|
-
};
|
|
424
|
-
let result;
|
|
425
|
-
try {
|
|
426
|
-
result = await sendChatGptPrompt({
|
|
427
|
-
port,
|
|
428
|
-
prompt: smokePrompt,
|
|
429
|
-
timeoutMs
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
catch (error) {
|
|
433
|
-
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, commandOptions);
|
|
434
|
-
const message = blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error);
|
|
435
|
-
let taskId;
|
|
436
|
-
try {
|
|
437
|
-
taskId = await recordBlockedSmoke(message, blocker);
|
|
438
|
-
}
|
|
439
|
-
catch (recordError) {
|
|
440
|
-
throw new Error(`${message} (also failed to record blocked smoke: ${errorMessage(recordError)})`);
|
|
441
|
-
}
|
|
442
|
-
throw new Error(formatBlockedConsultRecordedMessage(message, taskId, sourceCli, { cwd: targetCwd }));
|
|
443
|
-
}
|
|
444
|
-
if (result.answer.trim() !== PRO_BROWSER_SMOKE_TOKEN) {
|
|
445
|
-
const message = `Pro browser smoke returned an unexpected answer. Expected exactly ${PRO_BROWSER_SMOKE_TOKEN}. Actual: ${firstLine(result.answer)}`;
|
|
446
|
-
const blocker = {
|
|
447
|
-
code: "smoke_token_mismatch",
|
|
448
|
-
message,
|
|
449
|
-
retryable: true,
|
|
450
|
-
next_step: `Retry \`${formatBrowserSmokeCommand(sourceCli, commandOptions)}\` after selecting the intended Pro model, or inspect the visible ChatGPT answer.`
|
|
451
|
-
};
|
|
452
|
-
let taskId;
|
|
453
|
-
try {
|
|
454
|
-
taskId = await recordBlockedSmoke(message, blocker, result.url);
|
|
455
|
-
}
|
|
456
|
-
catch (recordError) {
|
|
457
|
-
throw new Error(`${message} (also failed to record blocked smoke: ${errorMessage(recordError)})`);
|
|
458
|
-
}
|
|
459
|
-
throw new Error(formatBlockedConsultRecordedMessage(message, taskId, sourceCli, { cwd: targetCwd }));
|
|
460
|
-
}
|
|
461
|
-
io.stdout(JSON.stringify(result, null, 2));
|
|
462
|
-
return 0;
|
|
463
|
-
}
|
|
464
|
-
throw legacyChatGptNamespaceError(subcommand);
|
|
465
|
-
}
|
|
466
|
-
if (command === "tasks") {
|
|
467
|
-
const [subcommand, ...taskArgs] = rest;
|
|
468
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
469
|
-
assertNoExtraArgs(taskArgs, "tasks help", 0);
|
|
470
|
-
printTasksHelp(io.stdout);
|
|
471
|
-
return 0;
|
|
472
|
-
}
|
|
473
|
-
if (subcommand === "create") {
|
|
474
|
-
if (printHelpIfRequested(taskArgs, "tasks create", io.stdout, printTasksHelp, { valueFlags: ["--cwd", "--title", "--prompt", "--repo-id", "--file"] }))
|
|
475
|
-
return 0;
|
|
476
|
-
assertOnlyOptions(taskArgs, "tasks create", ["--cwd", "--title", "--prompt", "--repo-id", "--file"]);
|
|
477
|
-
const title = readFlag(taskArgs, "--title");
|
|
478
|
-
const prompt = readFlag(taskArgs, "--prompt");
|
|
479
|
-
if (!title || !prompt)
|
|
480
|
-
throw new Error("tasks create requires --title and --prompt");
|
|
481
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
482
|
-
const task = await targetStore.createTask({
|
|
483
|
-
source: "codex",
|
|
484
|
-
title,
|
|
485
|
-
prompt,
|
|
486
|
-
repo_id: readFlag(taskArgs, "--repo-id") ?? "default",
|
|
487
|
-
files: readRepeatedFlag(taskArgs, "--file").map((file) => ({ path: file, role: "context" })),
|
|
488
|
-
provenance: { adapter: "cli", warnings: [] }
|
|
489
|
-
});
|
|
490
|
-
io.stdout(`${task.id}\t${task.status}\t${task.title}`);
|
|
491
|
-
return 0;
|
|
492
|
-
}
|
|
493
|
-
if (subcommand === "list") {
|
|
494
|
-
if (printHelpIfRequested(taskArgs, "tasks list", io.stdout, printTasksHelp, { valueFlags: ["--cwd", "--status"] }))
|
|
495
|
-
return 0;
|
|
496
|
-
assertOnlyOptions(taskArgs, "tasks list", ["--cwd", "--status"]);
|
|
497
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
498
|
-
const status = readTaskStatusFlag(taskArgs);
|
|
499
|
-
const tasks = await listTasksForInspection(targetStore, status);
|
|
500
|
-
for (const task of tasks) {
|
|
501
|
-
io.stdout(`${task.id}\t${task.status}\t${task.title}`);
|
|
502
|
-
}
|
|
503
|
-
return 0;
|
|
504
|
-
}
|
|
505
|
-
if (subcommand === "show") {
|
|
506
|
-
if (printHelpIfRequested(taskArgs, "tasks show", io.stdout, printTasksHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
507
|
-
return 0;
|
|
508
|
-
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks show", 1, ["--cwd"]);
|
|
509
|
-
if (!taskId)
|
|
510
|
-
throw new Error("tasks show requires <task-id|latest>");
|
|
511
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
512
|
-
const task = taskId === "latest" ? await latestTask(targetStore, { readOnly: true }) : await targetStore.getTaskReadOnly(taskId);
|
|
513
|
-
if (!task)
|
|
514
|
-
throw new Error(taskId === "latest" ? "No tasks found" : `Task not found: ${taskId}`);
|
|
515
|
-
io.stdout(JSON.stringify(task, null, 2));
|
|
516
|
-
return 0;
|
|
517
|
-
}
|
|
518
|
-
if (subcommand === "claim") {
|
|
519
|
-
if (printHelpIfRequested(taskArgs, "tasks claim", io.stdout, printTasksHelp, { valueFlags: ["--cwd", "--by"], maxPositionals: 1 }))
|
|
520
|
-
return 0;
|
|
521
|
-
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks claim", 1, ["--cwd", "--by"]);
|
|
522
|
-
if (!taskId)
|
|
523
|
-
throw new Error("tasks claim requires <task-id>");
|
|
524
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
525
|
-
const task = await targetStore.claimTask(taskId, readFlag(taskArgs, "--by") ?? "codex");
|
|
526
|
-
io.stdout(`${task.id}\t${task.status}\t${task.claimed_by ?? ""}`);
|
|
527
|
-
return 0;
|
|
528
|
-
}
|
|
529
|
-
if (subcommand === "complete") {
|
|
530
|
-
if (printHelpIfRequested(taskArgs, "tasks complete", io.stdout, printTasksHelp, {
|
|
531
|
-
valueFlags: ["--cwd", "--summary", "--command", "--artifact"],
|
|
532
|
-
maxPositionals: 1
|
|
533
|
-
})) {
|
|
534
|
-
return 0;
|
|
535
|
-
}
|
|
536
|
-
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks complete", 1, ["--cwd", "--summary", "--command", "--artifact"]);
|
|
537
|
-
if (!taskId)
|
|
538
|
-
throw new Error("tasks complete requires <task-id> --summary");
|
|
539
|
-
const summary = readFlag(taskArgs, "--summary");
|
|
540
|
-
if (!summary)
|
|
541
|
-
throw new Error("tasks complete requires <task-id> --summary");
|
|
542
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
543
|
-
const result = await targetStore.completeTask(taskId, {
|
|
544
|
-
status: "done",
|
|
545
|
-
summary,
|
|
546
|
-
commands: readRepeatedFlag(taskArgs, "--command"),
|
|
547
|
-
artifacts: await writeTaskCompleteArtifacts(targetStore, readRepeatedFlag(taskArgs, "--artifact"))
|
|
548
|
-
});
|
|
549
|
-
io.stdout(`${result.task_id}\t${result.status}\t${result.summary}`);
|
|
550
|
-
return 0;
|
|
551
|
-
}
|
|
552
|
-
if (subcommand === "block") {
|
|
553
|
-
if (printHelpIfRequested(taskArgs, "tasks block", io.stdout, printTasksHelp, {
|
|
554
|
-
valueFlags: ["--cwd", "--summary", "--code", "--next-step", "--command"],
|
|
555
|
-
booleanFlags: ["--retryable"],
|
|
556
|
-
maxPositionals: 1
|
|
557
|
-
})) {
|
|
558
|
-
return 0;
|
|
559
|
-
}
|
|
560
|
-
const [taskId] = readPositionalsWithOptions(taskArgs, "tasks block", 1, ["--cwd", "--summary", "--code", "--next-step", "--command"], ["--retryable"]);
|
|
561
|
-
if (!taskId)
|
|
562
|
-
throw new Error("tasks block requires <task-id> --summary");
|
|
563
|
-
const summary = readFlag(taskArgs, "--summary");
|
|
564
|
-
if (!summary)
|
|
565
|
-
throw new Error("tasks block requires <task-id> --summary");
|
|
566
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, taskArgs));
|
|
567
|
-
const result = await targetStore.completeTask(taskId, {
|
|
568
|
-
status: "blocked",
|
|
569
|
-
summary,
|
|
570
|
-
blocker: {
|
|
571
|
-
code: readFlag(taskArgs, "--code") ?? "manual_blocker",
|
|
572
|
-
message: summary,
|
|
573
|
-
retryable: taskArgs.includes("--retryable"),
|
|
574
|
-
next_step: readFlag(taskArgs, "--next-step")
|
|
575
|
-
},
|
|
576
|
-
commands: readRepeatedFlag(taskArgs, "--command")
|
|
577
|
-
});
|
|
578
|
-
io.stdout(`${result.task_id}\t${result.status}\t${result.summary}`);
|
|
579
|
-
return 0;
|
|
580
|
-
}
|
|
581
|
-
throw unknownSubcommandError("tasks", subcommand, ["create", "list", "show", "claim", "complete", "block"]);
|
|
582
|
-
}
|
|
583
|
-
if (command === "results") {
|
|
584
|
-
const [subcommand, ...resultArgs] = rest;
|
|
585
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
586
|
-
assertNoExtraArgs(resultArgs, "results help", 0);
|
|
587
|
-
printResultsHelp(io.stdout);
|
|
588
|
-
return 0;
|
|
589
|
-
}
|
|
590
|
-
if (subcommand === "show") {
|
|
591
|
-
if (printHelpIfRequested(resultArgs, "results show", io.stdout, printResultsHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
592
|
-
return 0;
|
|
593
|
-
const [taskId] = readPositionalsWithOptions(resultArgs, "results show", 1, ["--cwd"]);
|
|
594
|
-
if (!taskId)
|
|
595
|
-
throw new Error("results show requires <task-id|latest>");
|
|
596
|
-
const targetCwd = resolveCwdFlag(io.cwd, resultArgs);
|
|
597
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
598
|
-
const resultOptions = { cwd: readFlag(resultArgs, "--cwd") ? targetCwd : undefined };
|
|
599
|
-
try {
|
|
600
|
-
const resolvedTaskId = taskId === "latest" ? await latestResultTaskId(targetStore, { readOnly: true }) : taskId;
|
|
601
|
-
io.stdout(JSON.stringify(await targetStore.getFinalizedResultReadOnly(resolvedTaskId), null, 2));
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
throw sourceAwareResultError(error, undefined, resultOptions);
|
|
605
|
-
}
|
|
606
|
-
return 0;
|
|
607
|
-
}
|
|
608
|
-
if (subcommand === "artifact") {
|
|
609
|
-
if (printHelpIfRequested(resultArgs, "results artifact", io.stdout, printResultsHelp, { valueFlags: ["--cwd"], maxPositionals: 2 }))
|
|
610
|
-
return 0;
|
|
611
|
-
const [taskId, artifactPath] = readPositionalsWithOptions(resultArgs, "results artifact", 2, ["--cwd"]);
|
|
612
|
-
if (!taskId)
|
|
613
|
-
throw new Error("results artifact requires <task-id> [artifact-path]");
|
|
614
|
-
const targetCwd = resolveCwdFlag(io.cwd, resultArgs);
|
|
615
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
616
|
-
const resultOptions = { cwd: readFlag(resultArgs, "--cwd") ? targetCwd : undefined };
|
|
617
|
-
try {
|
|
618
|
-
const resolvedTaskId = taskId === "latest" ? await latestResultTaskId(targetStore, { readOnly: true }) : taskId;
|
|
619
|
-
const artifact = await targetStore.readFinalizedResultArtifactText(resolvedTaskId, artifactPath);
|
|
620
|
-
io.stdout(artifact.content);
|
|
621
|
-
}
|
|
622
|
-
catch (error) {
|
|
623
|
-
throw sourceAwareResultError(error, undefined, resultOptions);
|
|
624
|
-
}
|
|
625
|
-
return 0;
|
|
626
|
-
}
|
|
627
|
-
if (subcommand === "reseal") {
|
|
628
|
-
if (printHelpIfRequested(resultArgs, "results reseal", io.stdout, printResultsHelp, {
|
|
629
|
-
valueFlags: ["--cwd"],
|
|
630
|
-
booleanFlags: ["--confirm-current-result"],
|
|
631
|
-
maxPositionals: 1
|
|
632
|
-
})) {
|
|
633
|
-
return 0;
|
|
634
|
-
}
|
|
635
|
-
const [taskId] = readPositionalsWithOptions(resultArgs, "results reseal", 1, ["--cwd"], ["--confirm-current-result"]);
|
|
636
|
-
if (!taskId)
|
|
637
|
-
throw new Error("results reseal requires <task-id|latest> --confirm-current-result");
|
|
638
|
-
if (!resultArgs.includes("--confirm-current-result")) {
|
|
639
|
-
throw new Error("results reseal requires --confirm-current-result after you review the current .bridge/results/<task-id>.json payload locally.");
|
|
640
|
-
}
|
|
641
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, resultArgs));
|
|
642
|
-
const resolvedTaskId = taskId === "latest" ? await latestRawResultTaskId(targetStore) : taskId;
|
|
643
|
-
const resealed = await targetStore.resealResult(resolvedTaskId);
|
|
644
|
-
io.stdout(`${resealed.result.task_id}\tresealed\t${resealed.receipt.id}\tresult_sha256=${resealed.receipt.metadata.result_sha256}`);
|
|
645
|
-
return 0;
|
|
646
|
-
}
|
|
647
|
-
throw unknownSubcommandError("results", subcommand, ["show", "artifact", "reseal"]);
|
|
648
|
-
}
|
|
649
|
-
if (command === "receipts") {
|
|
650
|
-
const [subcommand, ...receiptArgs] = rest;
|
|
651
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
652
|
-
assertNoExtraArgs(receiptArgs, "receipts help", 0);
|
|
653
|
-
printReceiptsHelp(io.stdout);
|
|
654
|
-
return 0;
|
|
655
|
-
}
|
|
656
|
-
if (subcommand === "list") {
|
|
657
|
-
if (printHelpIfRequested(receiptArgs, "receipts list", io.stdout, printReceiptsHelp, { valueFlags: ["--cwd", "--kind", "--task-id"] }))
|
|
658
|
-
return 0;
|
|
659
|
-
assertOnlyOptions(receiptArgs, "receipts list", ["--cwd", "--kind", "--task-id"]);
|
|
660
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, receiptArgs));
|
|
661
|
-
const receipts = await listReceiptsForInspection(targetStore, {
|
|
662
|
-
kind: readReceiptKindFlag(receiptArgs),
|
|
663
|
-
task_id: readFlag(receiptArgs, "--task-id")
|
|
664
|
-
});
|
|
665
|
-
for (const receipt of receipts) {
|
|
666
|
-
io.stdout(`${receipt.id}\t${receipt.kind}\t${receipt.summary}${receiptInspectionListSuffix(receipt)}`);
|
|
667
|
-
}
|
|
668
|
-
return 0;
|
|
669
|
-
}
|
|
670
|
-
if (subcommand === "show") {
|
|
671
|
-
if (printHelpIfRequested(receiptArgs, "receipts show", io.stdout, printReceiptsHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
672
|
-
return 0;
|
|
673
|
-
const [receiptId] = readPositionalsWithOptions(receiptArgs, "receipts show", 1, ["--cwd"]);
|
|
674
|
-
if (!receiptId)
|
|
675
|
-
throw new Error("receipts show requires <receipt-id|latest>");
|
|
676
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, receiptArgs));
|
|
677
|
-
const receipt = receiptId === "latest" ? (await listReceiptsForInspection(targetStore))[0] : await targetStore.getReceiptForDisplayReadOnly(receiptId);
|
|
678
|
-
if (!receipt)
|
|
679
|
-
throw new Error(receiptId === "latest" ? "No receipts found" : `Receipt not found: ${receiptId}`);
|
|
680
|
-
io.stdout(JSON.stringify(receipt, null, 2));
|
|
681
|
-
return 0;
|
|
682
|
-
}
|
|
683
|
-
if (subcommand === "rotate-key") {
|
|
684
|
-
if (printHelpIfRequested(receiptArgs, "receipts rotate-key", io.stdout, printReceiptsHelp, { valueFlags: ["--cwd"] }))
|
|
685
|
-
return 0;
|
|
686
|
-
assertOnlyOptions(receiptArgs, "receipts rotate-key", ["--cwd"]);
|
|
687
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, receiptArgs));
|
|
688
|
-
await targetStore.ensure();
|
|
689
|
-
const rotated = await targetStore.rotateReceiptIntegrityKey();
|
|
690
|
-
io.stdout(`Rotated the local receipt integrity key: ${rotated.keys} key(s) in .bridge/receipt-key.local.`);
|
|
691
|
-
io.stdout("New receipts are signed with the new key; receipts signed before the rotation still verify via the retained legacy keys.");
|
|
692
|
-
return 0;
|
|
693
|
-
}
|
|
694
|
-
throw unknownSubcommandError("receipts", subcommand, ["list", "show", "rotate-key"]);
|
|
695
|
-
}
|
|
696
|
-
if (command === "sessions") {
|
|
697
|
-
const [subcommand, ...sessionArgs] = rest;
|
|
698
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
699
|
-
assertNoExtraArgs(sessionArgs, "sessions help", 0);
|
|
700
|
-
printSessionsHelp(io.stdout);
|
|
701
|
-
return 0;
|
|
702
|
-
}
|
|
703
|
-
if (subcommand === "list") {
|
|
704
|
-
if (printHelpIfRequested(sessionArgs, "sessions list", io.stdout, printSessionsHelp, { valueFlags: ["--cwd", "--status"] }))
|
|
705
|
-
return 0;
|
|
706
|
-
assertOnlyOptions(sessionArgs, "sessions list", ["--cwd", "--status"]);
|
|
707
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, sessionArgs));
|
|
708
|
-
const status = readSessionStatusFlag(sessionArgs);
|
|
709
|
-
const sessions = await listSessionsForInspection(targetStore, status);
|
|
710
|
-
for (const session of sessions) {
|
|
711
|
-
io.stdout(`${session.id}\t${session.status}\t${session.backend}\t${session.direction}`);
|
|
712
|
-
}
|
|
713
|
-
return 0;
|
|
714
|
-
}
|
|
715
|
-
if (subcommand === "show") {
|
|
716
|
-
if (printHelpIfRequested(sessionArgs, "sessions show", io.stdout, printSessionsHelp, { valueFlags: ["--cwd"], maxPositionals: 1 }))
|
|
717
|
-
return 0;
|
|
718
|
-
const [sessionId] = readPositionalsWithOptions(sessionArgs, "sessions show", 1, ["--cwd"]);
|
|
719
|
-
if (!sessionId)
|
|
720
|
-
throw new Error("sessions show requires <session-id|latest>");
|
|
721
|
-
const targetStore = new BridgeStore(resolveCwdFlag(io.cwd, sessionArgs));
|
|
722
|
-
const session = sessionId === "latest" ? (await listSessionsForInspection(targetStore))[0] : await targetStore.getSessionReadOnly(sessionId);
|
|
723
|
-
if (!session)
|
|
724
|
-
throw new Error(sessionId === "latest" ? "No sessions found" : `Session not found: ${sessionId}`);
|
|
725
|
-
io.stdout(formatSession(session));
|
|
726
|
-
return 0;
|
|
727
|
-
}
|
|
728
|
-
throw unknownSubcommandError("sessions", subcommand, ["list", "show"]);
|
|
729
|
-
}
|
|
730
|
-
if (command === "pro") {
|
|
731
|
-
const [subcommand, ...proArgs] = rest;
|
|
732
|
-
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
733
|
-
assertNoExtraArgs(proArgs, "pro help", 0);
|
|
734
|
-
printProHelp(io.stdout);
|
|
735
|
-
return 0;
|
|
736
|
-
}
|
|
737
|
-
if (subcommand === "ask") {
|
|
738
|
-
if (printHelpIfRequested(proArgs, "pro ask", io.stdout, printProHelp, {
|
|
739
|
-
valueFlags: [...ASK_PRO_PREVIEW_VALUE_FLAGS],
|
|
740
|
-
booleanFlags: [...ASK_PRO_BOOLEAN_FLAGS]
|
|
741
|
-
})) {
|
|
742
|
-
return 0;
|
|
743
|
-
}
|
|
744
|
-
parseAskProArgs(proArgs, ASK_PRO_PREVIEW_VALUE_FLAGS);
|
|
745
|
-
if (hasAskProSendMode(proArgs)) {
|
|
746
|
-
throw new Error("prodex pro ask is a dry-run preview. Use `prodex pro browser ask` for visible-browser sends.");
|
|
747
|
-
}
|
|
748
|
-
const hasDryRun = hasAskProDryRunMode(proArgs);
|
|
749
|
-
return runCli(["ask-pro", ...(hasDryRun ? [] : ["--dry-run"]), ...proArgs], io);
|
|
750
|
-
}
|
|
751
|
-
if (subcommand === "browser") {
|
|
752
|
-
const [browserSubcommand, ...browserArgs] = proArgs;
|
|
753
|
-
if (!browserSubcommand || isHelpSubcommand(browserSubcommand)) {
|
|
754
|
-
assertOnlyOptions(browserArgs, "pro browser help", ["--source-cli"]);
|
|
755
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
|
|
756
|
-
printProBrowserHelp(io.stdout, sourceCli);
|
|
757
|
-
return 0;
|
|
758
|
-
}
|
|
759
|
-
if (browserSubcommand === "login") {
|
|
760
|
-
if (printProBrowserHelpIfRequested(browserArgs, "pro browser login", io, {
|
|
761
|
-
valueFlags: ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms"],
|
|
762
|
-
booleanFlags: ["--dry-run"]
|
|
763
|
-
})) {
|
|
764
|
-
return 0;
|
|
765
|
-
}
|
|
766
|
-
assertOnlyOptions(browserArgs, "pro browser login", ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms"], ["--dry-run"]);
|
|
767
|
-
const loginUrl = readChatGptBrowserUrlFlag(browserArgs);
|
|
768
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
|
|
769
|
-
const targetCwd = readFlag(browserArgs, "--cwd") ? resolveCwdFlag(io.cwd, browserArgs) : undefined;
|
|
770
|
-
const profileDir = readFlag(browserArgs, "--profile-dir");
|
|
771
|
-
const port = readPortFlag(browserArgs, "--port") ?? 9333;
|
|
772
|
-
const launchTimeoutMs = readPositiveNumberFlag(browserArgs, "--launch-timeout-ms");
|
|
773
|
-
const commandOptions = {
|
|
774
|
-
...(targetCwd ? { cwd: targetCwd } : {}),
|
|
775
|
-
...(profileDir ? { profileDir } : {}),
|
|
776
|
-
...(port !== 9333 ? { port } : {}),
|
|
777
|
-
...(readFlag(browserArgs, "--url") ? { url: loginUrl } : {}),
|
|
778
|
-
...(launchTimeoutMs !== undefined ? { launchTimeoutMs } : {})
|
|
779
|
-
};
|
|
780
|
-
if (browserArgs.includes("--dry-run")) {
|
|
781
|
-
printBrowserLoginGuide(io.stdout, {
|
|
782
|
-
opened: false,
|
|
783
|
-
loginUrl,
|
|
784
|
-
profileDir: profileDir ?? defaultChatGptProfileDir(),
|
|
785
|
-
port,
|
|
786
|
-
sourceCli,
|
|
787
|
-
commandOptions
|
|
788
|
-
});
|
|
789
|
-
return 0;
|
|
790
|
-
}
|
|
791
|
-
const opened = openChatGptBrowser({
|
|
792
|
-
port,
|
|
793
|
-
profileDir,
|
|
794
|
-
url: loginUrl
|
|
795
|
-
});
|
|
796
|
-
await assertBrowserLaunchStayedAlive(opened, launchTimeoutMs);
|
|
797
|
-
printBrowserLoginGuide(io.stdout, {
|
|
798
|
-
opened: true,
|
|
799
|
-
loginUrl,
|
|
800
|
-
profileDir: opened.profileDir,
|
|
801
|
-
port: opened.port,
|
|
802
|
-
sourceCli,
|
|
803
|
-
commandOptions
|
|
804
|
-
});
|
|
805
|
-
return 0;
|
|
806
|
-
}
|
|
807
|
-
if (browserSubcommand === "ask") {
|
|
808
|
-
if (printProBrowserHelpIfRequested(browserArgs, "pro browser ask", io, {
|
|
809
|
-
valueFlags: [...ASK_PRO_VALUE_FLAGS],
|
|
810
|
-
booleanFlags: [...ASK_PRO_BOOLEAN_FLAGS]
|
|
811
|
-
})) {
|
|
812
|
-
return 0;
|
|
813
|
-
}
|
|
814
|
-
if (hasAskProDryRunMode(browserArgs) && hasAskProSendMode(browserArgs)) {
|
|
815
|
-
throw new Error("ask-pro cannot combine --dry-run and --send");
|
|
816
|
-
}
|
|
817
|
-
if (hasAskProDryRunMode(browserArgs)) {
|
|
818
|
-
throw new Error("prodex pro browser ask is an explicit visible-browser send. Use `prodex pro ask` for dry-run previews.");
|
|
819
|
-
}
|
|
820
|
-
const hasMode = hasAskProMode(browserArgs);
|
|
821
|
-
return runCli(["ask-pro", ...(hasMode ? [] : ["--send"]), ...browserArgs], { ...io, allowAskProBrowserSend: true });
|
|
822
|
-
}
|
|
823
|
-
if (browserSubcommand === "open" || browserSubcommand === "status" || browserSubcommand === "doctor") {
|
|
824
|
-
const replacement = browserSubcommand === "open" ? "login" : "check";
|
|
825
|
-
throw new Error(`Use \`prodex pro browser ${replacement}\` for explicit browser automation.`);
|
|
826
|
-
}
|
|
827
|
-
if (browserSubcommand === "smoke") {
|
|
828
|
-
if (printProBrowserHelpIfRequested(browserArgs, "pro browser smoke", io, { valueFlags: ["--cwd", "--port", "--timeout-ms", "--source-cli"] }))
|
|
829
|
-
return 0;
|
|
830
|
-
return runCli(["chatgpt", browserSubcommand, ...browserArgs], io);
|
|
831
|
-
}
|
|
832
|
-
if (browserSubcommand === "check") {
|
|
833
|
-
if (printProBrowserHelpIfRequested(browserArgs, "pro browser check", io, { valueFlags: ["--cwd", "--port", "--timeout-ms", "--source-cli"] }))
|
|
834
|
-
return 0;
|
|
835
|
-
assertOnlyOptions(browserArgs, "pro browser check", ["--cwd", "--port", "--timeout-ms", "--source-cli"]);
|
|
836
|
-
const targetCwd = resolveCwdFlag(io.cwd, browserArgs);
|
|
837
|
-
readPortFlag(browserArgs, "--port");
|
|
838
|
-
readPositiveNumberFlag(browserArgs, "--timeout-ms");
|
|
839
|
-
const healthy = await printProductCheck(new BridgeStore(targetCwd), io, browserArgs, targetCwd);
|
|
840
|
-
return healthy ? 0 : 1;
|
|
841
|
-
}
|
|
842
|
-
if (browserSubcommand === "models") {
|
|
843
|
-
if (printProBrowserHelpIfRequested(browserArgs, "pro browser models", io, { valueFlags: ["--port", "--timeout-ms", "--source-cli"] }))
|
|
844
|
-
return 0;
|
|
845
|
-
assertOnlyOptions(browserArgs, "pro browser models", ["--port", "--timeout-ms", "--source-cli"]);
|
|
846
|
-
const listed = await listChatGptModelOptions({
|
|
847
|
-
port: readPortFlag(browserArgs, "--port"),
|
|
848
|
-
timeoutMs: readPositiveNumberFlag(browserArgs, "--timeout-ms")
|
|
849
|
-
});
|
|
850
|
-
io.stdout("Model menu options in the visible ChatGPT tab (read-only; nothing was selected):");
|
|
851
|
-
for (const option of listed.options) {
|
|
852
|
-
const marker = option.checked ? "*" : " ";
|
|
853
|
-
const suffix = option.kind === "submenu" ? " (has sub-variants; not selectable via --model yet)" : "";
|
|
854
|
-
io.stdout(`${marker} ${option.label}${suffix}`);
|
|
855
|
-
}
|
|
856
|
-
io.stdout("Use radio entries with `pro browser ask --model/--effort`; Pro sub-modes via --pro-mode 기본|확장.");
|
|
857
|
-
return 0;
|
|
858
|
-
}
|
|
859
|
-
throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check", "models"]);
|
|
860
|
-
}
|
|
861
|
-
if (subcommand === "open" || subcommand === "status" || subcommand === "smoke" || subcommand === "check" || subcommand === "doctor") {
|
|
862
|
-
throw new Error(`Use \`prodex pro browser ${subcommand === "doctor" ? "check" : subcommand}\` for explicit browser automation.`);
|
|
863
|
-
}
|
|
864
|
-
if (subcommand === "list") {
|
|
865
|
-
if (printHelpIfRequested(proArgs, "pro list", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
866
|
-
return 0;
|
|
867
|
-
assertOnlyOptions(proArgs, "pro list", ["--cwd", "--source-cli"]);
|
|
868
|
-
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
869
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
870
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
|
|
871
|
-
const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
|
|
872
|
-
const consults = await listConsultListEntries(targetStore);
|
|
873
|
-
for (const entry of consults) {
|
|
874
|
-
if (entry.kind === "untrusted") {
|
|
875
|
-
io.stdout(`${entry.task.id}\tuntrusted\t${sourceAwareResultMessage(errorMessage(entry.error), sourceCli, answerOptions)}`);
|
|
876
|
-
}
|
|
877
|
-
else {
|
|
878
|
-
io.stdout(`${entry.consult.task.id}\t${entry.consult.result.status}\t${formatProListSummary(entry.consult, sourceCli, answerOptions)}`);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
return 0;
|
|
882
|
-
}
|
|
883
|
-
if (subcommand === "latest") {
|
|
884
|
-
if (printHelpIfRequested(proArgs, "pro latest", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
885
|
-
return 0;
|
|
886
|
-
assertOnlyOptions(proArgs, "pro latest", ["--cwd", "--source-cli"]);
|
|
887
|
-
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
888
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
889
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
|
|
890
|
-
const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
|
|
891
|
-
let consult;
|
|
892
|
-
try {
|
|
893
|
-
consult = await latestTrustedConsult(targetStore);
|
|
894
|
-
}
|
|
895
|
-
catch (error) {
|
|
896
|
-
throw sourceAwareResultError(error, sourceCli, answerOptions);
|
|
897
|
-
}
|
|
898
|
-
if (!consult)
|
|
899
|
-
throw new Error("No GPT Pro answers found");
|
|
900
|
-
io.stdout(formatProAnswer(consult, sourceCli, answerOptions));
|
|
901
|
-
return 0;
|
|
902
|
-
}
|
|
903
|
-
if (subcommand === "show") {
|
|
904
|
-
if (printHelpIfRequested(proArgs, "pro show", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"], maxPositionals: 1 }))
|
|
905
|
-
return 0;
|
|
906
|
-
const [taskId] = readPositionalsWithOptions(proArgs, "pro show", 1, ["--cwd", "--source-cli"]);
|
|
907
|
-
if (!taskId)
|
|
908
|
-
throw new Error("pro show requires <task-id|latest>");
|
|
909
|
-
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
910
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
911
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
|
|
912
|
-
const answerOptions = { cwd: readFlag(proArgs, "--cwd") ? targetCwd : undefined };
|
|
913
|
-
let consult;
|
|
914
|
-
try {
|
|
915
|
-
consult = taskId === "latest" ? await latestTrustedConsult(targetStore) : await getConsult(targetStore, taskId, { readOnly: true });
|
|
916
|
-
}
|
|
917
|
-
catch (error) {
|
|
918
|
-
throw sourceAwareResultError(error, sourceCli, answerOptions);
|
|
919
|
-
}
|
|
920
|
-
if (!consult)
|
|
921
|
-
throw new Error(taskId === "latest" ? "No GPT Pro answers found" : `GPT Pro answer not found: ${taskId}`);
|
|
922
|
-
io.stdout(formatProAnswer(consult, sourceCli, answerOptions));
|
|
923
|
-
return 0;
|
|
924
|
-
}
|
|
925
|
-
throw unknownSubcommandError("pro", subcommand, ["ask", "browser", "list", "latest", "show"]);
|
|
926
|
-
}
|
|
927
|
-
if (command === "consults") {
|
|
928
|
-
throw new Error("The legacy `consults` alias is retired. Use `prodex pro list`, `prodex pro latest`, or `prodex pro show <task-id|latest>`.");
|
|
929
|
-
}
|
|
930
|
-
if (command === "ask-pro") {
|
|
931
|
-
const parsedAskPro = parseAskProArgs(rest);
|
|
932
|
-
const hasDryRunMode = parsedAskPro.optionArgs.includes("--dry-run");
|
|
933
|
-
const hasSendMode = parsedAskPro.optionArgs.includes("--send");
|
|
934
|
-
if (!hasDryRunMode && !hasSendMode) {
|
|
935
|
-
throw new Error("ask-pro requires --dry-run or --send");
|
|
936
|
-
}
|
|
937
|
-
if (hasDryRunMode && hasSendMode) {
|
|
938
|
-
throw new Error("ask-pro cannot combine --dry-run and --send");
|
|
939
|
-
}
|
|
940
|
-
if (hasSendMode && !io.allowAskProBrowserSend) {
|
|
941
|
-
throw new Error("Direct ask-pro --send is disabled. Use `prodex pro browser ask` for explicit visible-browser sends.");
|
|
942
|
-
}
|
|
943
|
-
const targetCwd = resolveCwdFlag(io.cwd, parsedAskPro.optionArgs);
|
|
944
|
-
const targetStore = new BridgeStore(targetCwd);
|
|
945
|
-
const files = readRepeatedFlag(parsedAskPro.optionArgs, "--file");
|
|
946
|
-
const targetUrl = readFlag(parsedAskPro.optionArgs, "--target-url");
|
|
947
|
-
const normalizedTargetUrl = targetUrl ? normalizeChatGptTargetUrl(targetUrl) : undefined;
|
|
948
|
-
if (!normalizedTargetUrl && parsedAskPro.optionArgs.includes("--confirm-target")) {
|
|
949
|
-
throw new Error("--confirm-target requires --target-url so the visible browser target is explicit.");
|
|
950
|
-
}
|
|
951
|
-
if (normalizedTargetUrl && hasSendMode && !parsedAskPro.optionArgs.includes("--confirm-target")) {
|
|
952
|
-
throw new Error("--target-url requires --confirm-target after you manually verify the visible ChatGPT tab is the intended Project/thread.");
|
|
953
|
-
}
|
|
954
|
-
const prompt = parsedAskPro.promptParts.join(" ").trim();
|
|
955
|
-
if (!prompt)
|
|
956
|
-
throw new Error("ask-pro requires a prompt");
|
|
957
|
-
const browserDefaults = await loadBrowserDefaults(targetCwd);
|
|
958
|
-
const explicitProject = readFlag(parsedAskPro.optionArgs, "--project");
|
|
959
|
-
const explicitProjectNew = readFlag(parsedAskPro.optionArgs, "--project-new");
|
|
960
|
-
if (explicitProject !== undefined && explicitProjectNew !== undefined) {
|
|
961
|
-
throw new Error("ask-pro cannot combine --project and --project-new; pick an existing project or create one.");
|
|
962
|
-
}
|
|
963
|
-
if (normalizedTargetUrl && (explicitProject !== undefined || explicitProjectNew !== undefined)) {
|
|
964
|
-
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.");
|
|
965
|
-
}
|
|
966
|
-
const explicitModel = readFlag(parsedAskPro.optionArgs, "--model");
|
|
967
|
-
const explicitProModeRaw = readFlag(parsedAskPro.optionArgs, "--pro-mode");
|
|
968
|
-
const explicitEffortRaw = readFlag(parsedAskPro.optionArgs, "--effort");
|
|
969
|
-
if (explicitProModeRaw !== undefined && explicitEffortRaw !== undefined) {
|
|
970
|
-
throw new Error("ask-pro cannot combine --pro-mode and --effort; Pro sub-modes and reasoning effort are different model axes.");
|
|
971
|
-
}
|
|
972
|
-
const explicitProMode = explicitProModeRaw === undefined ? undefined : parseProMode(explicitProModeRaw);
|
|
973
|
-
const explicitEffort = explicitEffortRaw === undefined ? undefined : parseReasoningEffort(explicitEffortRaw);
|
|
974
|
-
// Explicit per-ask flags override persisted defaults. Choosing either
|
|
975
|
-
// reasoning axis explicitly suppresses the default for the other axis, and
|
|
976
|
-
// pinning --target-url suppresses a default project (it would navigate away
|
|
977
|
-
// from the confirmed tab).
|
|
978
|
-
const selectionModel = explicitModel ?? browserDefaults?.model;
|
|
979
|
-
const selectionProjectNew = explicitProjectNew;
|
|
980
|
-
const selectionProject = explicitProject ?? (normalizedTargetUrl || selectionProjectNew !== undefined ? undefined : browserDefaults?.project);
|
|
981
|
-
const reasoningAxisChosen = explicitProMode !== undefined || explicitEffort !== undefined;
|
|
982
|
-
const selectionProMode = explicitProMode ?? (reasoningAxisChosen ? undefined : browserDefaults?.pro_mode);
|
|
983
|
-
const selectionEffort = explicitEffort ?? (reasoningAxisChosen ? undefined : browserDefaults?.effort);
|
|
984
|
-
const selectionMetadata = {
|
|
985
|
-
...(selectionProject ? { project: selectionProject } : {}),
|
|
986
|
-
...(selectionProjectNew ? { project_new: selectionProjectNew } : {}),
|
|
987
|
-
...(selectionModel ? { model: selectionModel } : {}),
|
|
988
|
-
...(selectionProMode ? { pro_mode: selectionProMode } : {}),
|
|
989
|
-
...(selectionEffort ? { effort: selectionEffort } : {})
|
|
990
|
-
};
|
|
991
|
-
const browserPort = hasSendMode ? (readPortFlag(parsedAskPro.optionArgs, "--port") ?? 9333) : undefined;
|
|
992
|
-
// Pro extended can legitimately think for minutes, so its default timeout is
|
|
993
|
-
// higher; an explicit --timeout-ms always wins.
|
|
994
|
-
const defaultBrowserTimeoutMs = selectionProMode === "확장" ? 300_000 : 90_000;
|
|
995
|
-
const browserTimeoutMs = hasSendMode
|
|
996
|
-
? (readPositiveNumberFlag(parsedAskPro.optionArgs, "--timeout-ms") ?? defaultBrowserTimeoutMs)
|
|
997
|
-
: undefined;
|
|
998
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, parsedAskPro.optionArgs, "--source-cli");
|
|
999
|
-
const bundle = await buildDryRunBundle(targetCwd, { prompt, files });
|
|
1000
|
-
if (hasSendMode) {
|
|
1001
|
-
const browserCommandOptions = {
|
|
1002
|
-
cwd: targetCwd,
|
|
1003
|
-
port: parsedAskPro.optionArgs.includes("--port") ? browserPort : undefined
|
|
1004
|
-
};
|
|
1005
|
-
const task = await targetStore.createTask({
|
|
1006
|
-
source: "codex",
|
|
1007
|
-
title: "GPT Pro consult",
|
|
1008
|
-
prompt: bundle.text,
|
|
1009
|
-
repo_id: "default",
|
|
1010
|
-
files: files.map((file) => ({ path: file, role: "context" })),
|
|
1011
|
-
provenance: {
|
|
1012
|
-
adapter: "chatgpt-control",
|
|
1013
|
-
session_id: bundle.id,
|
|
1014
|
-
thread: normalizedTargetUrl,
|
|
1015
|
-
warnings: []
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1018
|
-
await targetStore.claimTask(task.id, "chatgpt-pro");
|
|
1019
|
-
try {
|
|
1020
|
-
await writeSessionBeforeBrowserSend(targetStore, {
|
|
1021
|
-
id: bundle.id,
|
|
1022
|
-
direction: "codex_to_chatgpt",
|
|
1023
|
-
backend: "chatgpt-control",
|
|
1024
|
-
task_id: task.id,
|
|
1025
|
-
thread: normalizedTargetUrl,
|
|
1026
|
-
status: "running",
|
|
1027
|
-
warnings: []
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
catch (error) {
|
|
1031
|
-
const blocker = {
|
|
1032
|
-
code: "session_record_failed",
|
|
1033
|
-
message: `Could not record ChatGPT browser session before send: ${errorMessage(error)}`,
|
|
1034
|
-
retryable: true,
|
|
1035
|
-
next_step: "Fix local .bridge write permissions, then rerun the consult."
|
|
1036
|
-
};
|
|
1037
|
-
try {
|
|
1038
|
-
await targetStore.completeTask(task.id, {
|
|
1039
|
-
status: "blocked",
|
|
1040
|
-
summary: blocker.message,
|
|
1041
|
-
commands: ["visible ChatGPT browser consult"],
|
|
1042
|
-
blocker
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
catch (recordError) {
|
|
1046
|
-
throw new Error(`${blocker.message} (also failed to record blocked consult: ${errorMessage(recordError)})`);
|
|
1047
|
-
}
|
|
1048
|
-
throw new Error(formatBlockedConsultRecordedMessage(blocker.message, task.id, sourceCli, { cwd: targetCwd }));
|
|
1049
|
-
}
|
|
1050
|
-
let consult;
|
|
1051
|
-
try {
|
|
1052
|
-
consult = await sendChatGptPrompt({
|
|
1053
|
-
port: browserPort,
|
|
1054
|
-
prompt: bundle.text,
|
|
1055
|
-
targetUrl: normalizedTargetUrl,
|
|
1056
|
-
timeoutMs: browserTimeoutMs,
|
|
1057
|
-
project: selectionProject,
|
|
1058
|
-
projectNew: selectionProjectNew,
|
|
1059
|
-
model: selectionModel,
|
|
1060
|
-
proMode: selectionProMode,
|
|
1061
|
-
effort: selectionEffort
|
|
1062
|
-
});
|
|
1063
|
-
}
|
|
1064
|
-
catch (error) {
|
|
1065
|
-
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, browserCommandOptions);
|
|
1066
|
-
const message = blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error);
|
|
1067
|
-
try {
|
|
1068
|
-
await targetStore.completeTask(task.id, {
|
|
1069
|
-
status: "blocked",
|
|
1070
|
-
summary: message,
|
|
1071
|
-
commands: ["visible ChatGPT browser consult"],
|
|
1072
|
-
blocker
|
|
1073
|
-
});
|
|
1074
|
-
await writeSessionBestEffort(targetStore, {
|
|
1075
|
-
id: bundle.id,
|
|
1076
|
-
direction: "codex_to_chatgpt",
|
|
1077
|
-
backend: "chatgpt-control",
|
|
1078
|
-
task_id: task.id,
|
|
1079
|
-
thread: normalizedTargetUrl,
|
|
1080
|
-
status: "blocked",
|
|
1081
|
-
blocker,
|
|
1082
|
-
warnings: []
|
|
1083
|
-
}, io);
|
|
1084
|
-
}
|
|
1085
|
-
catch (recordError) {
|
|
1086
|
-
throw new Error(`${message} (also failed to record blocked consult: ${errorMessage(recordError)})`);
|
|
1087
|
-
}
|
|
1088
|
-
throw new Error(formatBlockedConsultRecordedMessage(message, task.id, sourceCli, { cwd: targetCwd }));
|
|
1089
|
-
}
|
|
1090
|
-
const answerArtifactText = formatProConsultArtifact(consult);
|
|
1091
|
-
const persistenceWarnings = [...consult.warnings];
|
|
1092
|
-
let answerArtifactPath;
|
|
1093
|
-
const answerArtifactBytes = Buffer.byteLength(answerArtifactText, "utf8");
|
|
1094
|
-
if (answerArtifactBytes > MAX_FETCHABLE_RESULT_ARTIFACT_BYTES) {
|
|
1095
|
-
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`;
|
|
1096
|
-
persistenceWarnings.push(warning);
|
|
1097
|
-
io.stderr(warning);
|
|
1098
|
-
}
|
|
1099
|
-
else {
|
|
1100
|
-
try {
|
|
1101
|
-
answerArtifactPath = await targetStore.writeArtifactText(`.bridge/artifacts/pro-consults/${task.id}.md`, answerArtifactText);
|
|
1102
|
-
}
|
|
1103
|
-
catch (error) {
|
|
1104
|
-
const warning = `answer_artifact_warning: ${errorMessage(error)}`;
|
|
1105
|
-
persistenceWarnings.push(warning);
|
|
1106
|
-
io.stderr(warning);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
try {
|
|
1110
|
-
await targetStore.writeReceipt({
|
|
1111
|
-
kind: "consult_answer_saved",
|
|
1112
|
-
task_id: task.id,
|
|
1113
|
-
session_id: bundle.id,
|
|
1114
|
-
summary: `Recorded ChatGPT answer for ${task.id}`,
|
|
1115
|
-
metadata: {
|
|
1116
|
-
...(answerArtifactPath ? { artifact_path: answerArtifactPath } : {}),
|
|
1117
|
-
thread: consult.url,
|
|
1118
|
-
...(Object.keys(selectionMetadata).length > 0 ? { selection: selectionMetadata } : {}),
|
|
1119
|
-
warnings: persistenceWarnings
|
|
1120
|
-
}
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
catch (error) {
|
|
1124
|
-
const warning = `receipt_record_warning: ${errorMessage(error)}`;
|
|
1125
|
-
persistenceWarnings.push(warning);
|
|
1126
|
-
io.stderr(warning);
|
|
1127
|
-
}
|
|
1128
|
-
let result;
|
|
1129
|
-
try {
|
|
1130
|
-
result = await targetStore.completeTask(task.id, {
|
|
1131
|
-
status: "done",
|
|
1132
|
-
summary: consult.answer,
|
|
1133
|
-
artifacts: answerArtifactPath ? [{ path: answerArtifactPath, role: "result", bytes: Buffer.byteLength(answerArtifactText, "utf8") }] : [],
|
|
1134
|
-
commands: ["visible ChatGPT browser consult"],
|
|
1135
|
-
warnings: persistenceWarnings,
|
|
1136
|
-
provenance: {
|
|
1137
|
-
thread: consult.url,
|
|
1138
|
-
warnings: persistenceWarnings
|
|
1139
|
-
}
|
|
1140
|
-
});
|
|
1141
|
-
}
|
|
1142
|
-
catch (error) {
|
|
1143
|
-
io.stdout(`consult_answer_received_but_not_saved: ${task.id} ${consult.url}`);
|
|
1144
|
-
io.stdout("");
|
|
1145
|
-
io.stdout(consult.answer);
|
|
1146
|
-
throw new Error(`ChatGPT answer was received but local persistence failed: ${errorMessage(error)}`);
|
|
1147
|
-
}
|
|
1148
|
-
await writeSessionBestEffort(targetStore, {
|
|
1149
|
-
id: bundle.id,
|
|
1150
|
-
direction: "codex_to_chatgpt",
|
|
1151
|
-
backend: "chatgpt-control",
|
|
1152
|
-
task_id: task.id,
|
|
1153
|
-
thread: consult.url,
|
|
1154
|
-
status: "done",
|
|
1155
|
-
warnings: persistenceWarnings
|
|
1156
|
-
}, io);
|
|
1157
|
-
io.stdout(`${result.task_id}\t${result.status}\t${consult.url}`);
|
|
1158
|
-
io.stdout("");
|
|
1159
|
-
io.stdout(result.summary);
|
|
1160
|
-
}
|
|
1161
|
-
else {
|
|
1162
|
-
await writeSessionBestEffort(targetStore, {
|
|
1163
|
-
id: bundle.id,
|
|
1164
|
-
direction: "codex_to_chatgpt",
|
|
1165
|
-
backend: "manual",
|
|
1166
|
-
status: "preview",
|
|
1167
|
-
warnings: []
|
|
1168
|
-
}, io);
|
|
1169
|
-
await targetStore.writeReceipt({
|
|
1170
|
-
kind: "consult_preview",
|
|
1171
|
-
session_id: bundle.id,
|
|
1172
|
-
summary: `Created dry-run consult preview ${bundle.id}`
|
|
1173
|
-
});
|
|
1174
|
-
io.stdout(`DRY RUN ${bundle.id}`);
|
|
1175
|
-
io.stdout(bundle.text);
|
|
1176
|
-
}
|
|
1177
|
-
return 0;
|
|
1178
|
-
}
|
|
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);
|
|
1179
184
|
if (command === "mcp") {
|
|
1180
185
|
if (printHelpIfRequested(rest, "mcp", io.stdout, printMcpHelp, { valueFlags: ["--cwd"] }))
|
|
1181
186
|
return 0;
|
|
@@ -1192,368 +197,54 @@ function defaultIo() {
|
|
|
1192
197
|
stderr: (line) => console.error(line)
|
|
1193
198
|
};
|
|
1194
199
|
}
|
|
1195
|
-
function
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
Commands:
|
|
1199
|
-
prodex --version
|
|
1200
|
-
prodex init [--cwd /absolute/path/to/repo]
|
|
1201
|
-
prodex doctor [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1202
|
-
prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name"] [--clear-model|--clear-pro-mode|--clear-effort|--clear-project] [--interactive]
|
|
1203
|
-
prodex start [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1204
|
-
prodex status [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] [--show-token] [--url-only] [--unsafe-show-non-expiring-token]
|
|
1205
|
-
prodex tunnel url [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --public-url https://... [--show-token] [--url-only]
|
|
1206
|
-
prodex release status [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1207
|
-
prodex release pack [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --pack-destination /absolute/path [--keep-workdir]
|
|
1208
|
-
prodex onboard [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1209
|
-
prodex project prompt [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1210
|
-
prodex claude prompt [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1211
|
-
prodex claude config [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1212
|
-
prodex pro ask [--dry-run] [--cwd /absolute/path/to/repo] [--file path] "prompt" # dry-run preview
|
|
1213
|
-
prodex pro browser login [--cwd /absolute/path/to/repo] [--dry-run] [--source-cli /absolute/path/to/dist/cli.js] [--profile-dir path] [--port 9333] [--url https://chatgpt.com/...] [--launch-timeout-ms 5000] # preview/open visible browser login
|
|
1214
|
-
prodex pro browser help [--source-cli /absolute/path/to/dist/cli.js]
|
|
1215
|
-
prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 1500]
|
|
1216
|
-
prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]
|
|
1217
|
-
prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000] # read-only list of model menu options
|
|
1218
|
-
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--target-url url --confirm-target] [--file path] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
1219
|
-
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1220
|
-
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1221
|
-
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1222
|
-
prodex tasks create [--cwd /absolute/path/to/repo] --title "Title" --prompt "Prompt"
|
|
1223
|
-
prodex tasks list [--status new|claimed|done|blocked] [--cwd /absolute/path/to/repo]
|
|
1224
|
-
prodex tasks show <task-id|latest> [--cwd /absolute/path/to/repo]
|
|
1225
|
-
prodex tasks claim <task-id> [--cwd /absolute/path/to/repo] [--by codex]
|
|
1226
|
-
prodex tasks complete <task-id> [--cwd /absolute/path/to/repo] --summary "Summary" [--command "npm test"] [--artifact .bridge/artifacts/results/name.md=text]
|
|
1227
|
-
prodex tasks block <task-id> [--cwd /absolute/path/to/repo] --summary "Summary" [--code code] [--next-step "Next step"] [--retryable]
|
|
1228
|
-
prodex results show <task-id|latest> [--cwd /absolute/path/to/repo]
|
|
1229
|
-
prodex results artifact <task-id|latest> [artifact-path] [--cwd /absolute/path/to/repo]
|
|
1230
|
-
prodex results reseal <task-id|latest> --confirm-current-result [--cwd /absolute/path/to/repo]
|
|
1231
|
-
prodex receipts list [--kind kind] [--task-id task-id] [--cwd /absolute/path/to/repo]
|
|
1232
|
-
prodex receipts show <receipt-id|latest> [--cwd /absolute/path/to/repo]
|
|
1233
|
-
prodex receipts rotate-key [--cwd /absolute/path/to/repo]
|
|
1234
|
-
prodex sessions list [--status preview|running|done|blocked] [--cwd /absolute/path/to/repo]
|
|
1235
|
-
prodex sessions show <session-id|latest> [--cwd /absolute/path/to/repo]
|
|
1236
|
-
prodex mcp [--cwd /absolute/path/to/repo]`);
|
|
200
|
+
function isHelpArgs(args) {
|
|
201
|
+
return args.length > 0 && isHelpSubcommand(args[0]);
|
|
1237
202
|
}
|
|
1238
|
-
function
|
|
1239
|
-
|
|
203
|
+
function formatProjectVerificationPrompt(cwd, sourceCli) {
|
|
204
|
+
const cli = formatCliCommand(sourceCli);
|
|
205
|
+
const quotedCwd = shellQuote(cwd);
|
|
206
|
+
const sourceCliOption = formatSourceCliOption(sourceCli);
|
|
207
|
+
return `ChatGPT Project MCP verification prompt
|
|
1240
208
|
|
|
1241
|
-
|
|
1242
|
-
|
|
209
|
+
Paste this into the ChatGPT Project after adding the prodex MCP server URL.
|
|
210
|
+
${TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING}
|
|
1243
211
|
|
|
1244
|
-
|
|
1245
|
-
}
|
|
1246
|
-
function printSetupHelp(stdout) {
|
|
1247
|
-
stdout(`prodex setup
|
|
212
|
+
Please verify the prodex MCP bridge for this private project:
|
|
1248
213
|
|
|
1249
|
-
|
|
1250
|
-
prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name"] [--clear-model|--clear-pro-mode|--clear-effort|--clear-project] [--interactive]
|
|
214
|
+
1. Call the MCP tool \`bridge_create_task\` with:
|
|
1251
215
|
|
|
1252
|
-
|
|
216
|
+
{
|
|
217
|
+
"title": "prodex MCP verification",
|
|
218
|
+
"prompt": "Verify that this ChatGPT Project can create tasks through the local prodex MCP bridge.",
|
|
219
|
+
"repo_id": "default"
|
|
220
|
+
}
|
|
1253
221
|
|
|
1254
|
-
|
|
1255
|
-
--model Composer model to pick by its exact menu label (verified: Pro)
|
|
1256
|
-
--pro-mode Pro sub-mode: 기본 (standard) or 확장 (extended)
|
|
1257
|
-
--effort Reasoning effort: 즉시 / 중간 / 높음 / 매우 높음 (English aliases: instant/medium/high/max); picking one deselects Pro
|
|
1258
|
-
--project Sidebar project to enter before sending
|
|
1259
|
-
Clear a saved default with --clear-model / --clear-pro-mode / --clear-effort / --clear-project.
|
|
1260
|
-
--pro-mode and --effort are different model axes and cannot be combined. View saved defaults with \`prodex status\`.`);
|
|
1261
|
-
}
|
|
1262
|
-
function printStartHelp(stdout) {
|
|
1263
|
-
stdout(`prodex start
|
|
222
|
+
2. Call \`bridge_list_tasks\` with:
|
|
1264
223
|
|
|
1265
|
-
|
|
1266
|
-
prodex start [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
224
|
+
{ "status": "new" }
|
|
1267
225
|
|
|
1268
|
-
|
|
1269
|
-
}
|
|
1270
|
-
function printStatusHelp(stdout) {
|
|
1271
|
-
stdout(`prodex status
|
|
226
|
+
3. Call \`bridge_get_task\` with the task_id returned by \`bridge_create_task\`.
|
|
1272
227
|
|
|
1273
|
-
|
|
1274
|
-
prodex status [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] [--show-token] [--url-only] [--unsafe-show-non-expiring-token]
|
|
228
|
+
4. Reply with the task_id and whether all three MCP calls succeeded. Ask me to run the local completion command below, then wait.
|
|
1275
229
|
|
|
1276
|
-
|
|
1277
|
-
}
|
|
1278
|
-
function printTunnelHelp(stdout) {
|
|
1279
|
-
stdout(`prodex tunnel
|
|
230
|
+
5. After I reply exactly \`local completion done\`, call \`bridge_fetch_result\` with:
|
|
1280
231
|
|
|
1281
|
-
|
|
1282
|
-
prodex tunnel url [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --public-url https://... [--show-token] [--url-only]
|
|
232
|
+
{ "task_id": "<task-id>" }
|
|
1283
233
|
|
|
1284
|
-
|
|
1285
|
-
}
|
|
1286
|
-
function printTunnelUrlHelp(stdout) {
|
|
1287
|
-
stdout(`prodex tunnel url
|
|
234
|
+
6. If the fetched result lists artifacts, call \`bridge_fetch_result_artifact\` for each listed result artifact path:
|
|
1288
235
|
|
|
1289
|
-
|
|
1290
|
-
prodex tunnel url [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --public-url https://... [--show-token] [--url-only]
|
|
236
|
+
{ "task_id": "<task-id>", "path": "<artifact-path>" }
|
|
1291
237
|
|
|
1292
|
-
|
|
1293
|
-
}
|
|
1294
|
-
function printDoctorHelp(stdout) {
|
|
1295
|
-
stdout(`prodex doctor
|
|
238
|
+
7. Reply with whether \`bridge_fetch_result\` returned the verification result summary and whether every listed result artifact was readable. Do not call repo_write_file_dry_run, repo_write_file_apply, repo_stage_reviewed_paths, or any write/stage tool for this verification.
|
|
1296
239
|
|
|
1297
|
-
|
|
1298
|
-
prodex doctor [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
240
|
+
Local follow-up after ChatGPT replies:
|
|
1299
241
|
|
|
1300
|
-
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
|
|
242
|
+
cd ${quotedCwd}
|
|
243
|
+
${cli} tasks list --status new --cwd ${quotedCwd}
|
|
244
|
+
${cli} tasks show <task-id> --cwd ${quotedCwd}
|
|
245
|
+
${cli} tasks complete <task-id> --cwd ${quotedCwd} --summary "prodex MCP verification result" --artifact .bridge/artifacts/results/mcp-verification.md="prodex MCP verification artifact"
|
|
1304
246
|
|
|
1305
|
-
|
|
1306
|
-
prodex onboard [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1307
|
-
|
|
1308
|
-
Print a local-first setup guide for Codex, ChatGPT Projects, Claude, and visible-browser Pro consults.`);
|
|
1309
|
-
}
|
|
1310
|
-
function printMcpHelp(stdout) {
|
|
1311
|
-
stdout(`prodex mcp
|
|
1312
|
-
|
|
1313
|
-
Commands:
|
|
1314
|
-
prodex mcp [--cwd /absolute/path/to/repo]
|
|
1315
|
-
|
|
1316
|
-
Run the stdio MCP server for local clients such as Claude. This does not reveal HTTP MCP URL tokens.`);
|
|
1317
|
-
}
|
|
1318
|
-
function printReleaseHelp(stdout) {
|
|
1319
|
-
stdout(`prodex release
|
|
1320
|
-
|
|
1321
|
-
Commands:
|
|
1322
|
-
prodex release status [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1323
|
-
prodex release pack [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --pack-destination /absolute/path [--keep-workdir]
|
|
1324
|
-
|
|
1325
|
-
Release commands are local checks and package preparation helpers; they do not publish or push.`);
|
|
1326
|
-
}
|
|
1327
|
-
function printProHelp(stdout) {
|
|
1328
|
-
stdout(`prodex pro
|
|
1329
|
-
|
|
1330
|
-
Commands:
|
|
1331
|
-
prodex pro ask [--dry-run] [--cwd /absolute/path/to/repo] [--file path] "prompt"
|
|
1332
|
-
prodex pro browser help [--source-cli /absolute/path/to/dist/cli.js]
|
|
1333
|
-
prodex pro browser login [--cwd /absolute/path/to/repo] [--dry-run] [--source-cli /absolute/path/to/dist/cli.js] [--launch-timeout-ms 5000]
|
|
1334
|
-
prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1335
|
-
prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1336
|
-
prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js]
|
|
1337
|
-
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--file path] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt"
|
|
1338
|
-
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1339
|
-
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1340
|
-
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
1341
|
-
|
|
1342
|
-
Use \`prodex pro ask\` for dry-run/manual previews.
|
|
1343
|
-
Use \`prodex pro browser ask\` only when you want an explicit visible-browser send.
|
|
1344
|
-
Model/project selection (visible-browser send):
|
|
1345
|
-
--model "label" Pick the composer model by its exact menu label (verified: Pro). Submenu models (e.g. GPT-5.5 variants) are rejected for now.
|
|
1346
|
-
--pro-mode 기본 | 확장 Pro sub-mode (only when the model is Pro); 확장 raises the default timeout to 300000 ms
|
|
1347
|
-
--effort 즉시|중간|높음|매우 높음 Reasoning effort (aliases: instant/medium/high/max); picking one deselects Pro
|
|
1348
|
-
--project "name" Enter an existing sidebar project first (cannot combine with --target-url)
|
|
1349
|
-
Labels match the Korean ChatGPT UI; run \`prodex pro browser models\` to list what your account shows.
|
|
1350
|
-
Persist defaults with \`prodex setup --model/--pro-mode/--effort/--project\`; clear them with setup --clear-model/--clear-pro-mode/--clear-effort/--clear-project.
|
|
1351
|
-
(Creating a new project from the CLI is planned; for now create it in ChatGPT and pass --project.)`);
|
|
1352
|
-
}
|
|
1353
|
-
function printProjectHelp(stdout) {
|
|
1354
|
-
stdout(`prodex project
|
|
1355
|
-
|
|
1356
|
-
Commands:
|
|
1357
|
-
prodex project prompt [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1358
|
-
|
|
1359
|
-
Print a ChatGPT Project MCP verification prompt. The prompt asks for read/task handoff verification only.`);
|
|
1360
|
-
}
|
|
1361
|
-
function printClaudeHelp(stdout) {
|
|
1362
|
-
stdout(`prodex claude
|
|
1363
|
-
|
|
1364
|
-
Commands:
|
|
1365
|
-
prodex claude prompt [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1366
|
-
prodex claude config [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
1367
|
-
|
|
1368
|
-
Print Claude MCP setup and verification helpers. These commands do not start MCP or reveal HTTP tokens.`);
|
|
1369
|
-
}
|
|
1370
|
-
function printTasksHelp(stdout) {
|
|
1371
|
-
stdout(`prodex tasks
|
|
1372
|
-
|
|
1373
|
-
Commands:
|
|
1374
|
-
prodex tasks create [--cwd /absolute/path/to/repo] --title "Title" --prompt "Prompt"
|
|
1375
|
-
prodex tasks list [--status new|claimed|done|blocked] [--cwd /absolute/path/to/repo]
|
|
1376
|
-
prodex tasks show <task-id|latest> [--cwd /absolute/path/to/repo]
|
|
1377
|
-
prodex tasks claim <task-id> [--cwd /absolute/path/to/repo] [--by codex]
|
|
1378
|
-
prodex tasks complete <task-id> [--cwd /absolute/path/to/repo] --summary "Summary" [--command "npm test"] [--artifact .bridge/artifacts/results/name.md=text]
|
|
1379
|
-
prodex tasks block <task-id> [--cwd /absolute/path/to/repo] --summary "Summary" [--code code] [--next-step "Next step"] [--retryable]`);
|
|
1380
|
-
}
|
|
1381
|
-
function printResultsHelp(stdout) {
|
|
1382
|
-
stdout(`prodex results
|
|
1383
|
-
|
|
1384
|
-
Commands:
|
|
1385
|
-
prodex results show <task-id|latest> [--cwd /absolute/path/to/repo]
|
|
1386
|
-
prodex results artifact <task-id|latest> [artifact-path] [--cwd /absolute/path/to/repo]
|
|
1387
|
-
prodex results reseal <task-id|latest> --confirm-current-result [--cwd /absolute/path/to/repo]`);
|
|
1388
|
-
}
|
|
1389
|
-
function printReceiptsHelp(stdout) {
|
|
1390
|
-
stdout(`prodex receipts
|
|
1391
|
-
|
|
1392
|
-
Commands:
|
|
1393
|
-
prodex receipts list [--kind kind] [--task-id task-id] [--cwd /absolute/path/to/repo]
|
|
1394
|
-
prodex receipts show <receipt-id|latest> [--cwd /absolute/path/to/repo]
|
|
1395
|
-
prodex receipts rotate-key [--cwd /absolute/path/to/repo]
|
|
1396
|
-
|
|
1397
|
-
rotate-key generates a new signing key for receipt integrity seals and keeps the
|
|
1398
|
-
previous keys in .bridge/receipt-key.local so receipts signed before the
|
|
1399
|
-
rotation still verify.`);
|
|
1400
|
-
}
|
|
1401
|
-
function printSessionsHelp(stdout) {
|
|
1402
|
-
stdout(`prodex sessions
|
|
1403
|
-
|
|
1404
|
-
Commands:
|
|
1405
|
-
prodex sessions list [--status preview|running|done|blocked] [--cwd /absolute/path/to/repo]
|
|
1406
|
-
prodex sessions show <session-id|latest> [--cwd /absolute/path/to/repo]`);
|
|
1407
|
-
}
|
|
1408
|
-
function isHelpSubcommand(value) {
|
|
1409
|
-
return value === "help" || value === "--help" || value === "-h";
|
|
1410
|
-
}
|
|
1411
|
-
function isHelpArgs(args) {
|
|
1412
|
-
return args.length > 0 && isHelpSubcommand(args[0]);
|
|
1413
|
-
}
|
|
1414
|
-
function printHelpIfRequested(args, command, stdout, printHelp, options = {}) {
|
|
1415
|
-
const helpIndex = findHelpFlagIndexBeforePromptDelimiter(args);
|
|
1416
|
-
if (helpIndex === -1)
|
|
1417
|
-
return false;
|
|
1418
|
-
assertHelpRequestArgs(args, command, options);
|
|
1419
|
-
printHelp(stdout);
|
|
1420
|
-
return true;
|
|
1421
|
-
}
|
|
1422
|
-
function printProBrowserHelpIfRequested(args, command, io, options) {
|
|
1423
|
-
const helpIndex = findHelpFlagIndexBeforePromptDelimiter(args);
|
|
1424
|
-
if (helpIndex === -1)
|
|
1425
|
-
return false;
|
|
1426
|
-
assertHelpRequestArgs(args, command, options);
|
|
1427
|
-
printProBrowserHelp(io.stdout, resolveOptionalFileFlag(io.cwd, args, "--source-cli"));
|
|
1428
|
-
return true;
|
|
1429
|
-
}
|
|
1430
|
-
function findHelpFlagIndexBeforePromptDelimiter(args) {
|
|
1431
|
-
const delimiterIndex = args.indexOf("--");
|
|
1432
|
-
const limit = delimiterIndex === -1 ? args.length : delimiterIndex;
|
|
1433
|
-
return args.findIndex((arg, index) => index < limit && isHelpSubcommand(arg));
|
|
1434
|
-
}
|
|
1435
|
-
function assertHelpRequestArgs(args, command, options) {
|
|
1436
|
-
const delimiterIndex = args.indexOf("--");
|
|
1437
|
-
const commandArgs = delimiterIndex === -1 ? args : args.slice(0, delimiterIndex);
|
|
1438
|
-
const valueFlagSet = new Set(options.valueFlags ?? []);
|
|
1439
|
-
const booleanFlagSet = new Set(options.booleanFlags ?? []);
|
|
1440
|
-
const maxPositionals = options.maxPositionals ?? 0;
|
|
1441
|
-
let positionals = 0;
|
|
1442
|
-
for (let index = 0; index < commandArgs.length; index += 1) {
|
|
1443
|
-
const arg = commandArgs[index];
|
|
1444
|
-
if (isHelpSubcommand(arg))
|
|
1445
|
-
continue;
|
|
1446
|
-
if (valueFlagSet.has(arg)) {
|
|
1447
|
-
const next = commandArgs[index + 1];
|
|
1448
|
-
if (next && !isHelpSubcommand(next)) {
|
|
1449
|
-
readFlagValue(commandArgs, index, arg);
|
|
1450
|
-
index += 1;
|
|
1451
|
-
}
|
|
1452
|
-
continue;
|
|
1453
|
-
}
|
|
1454
|
-
if (booleanFlagSet.has(arg))
|
|
1455
|
-
continue;
|
|
1456
|
-
if (arg.startsWith("-")) {
|
|
1457
|
-
throw unknownOptionError(arg, command, [...valueFlagSet, ...booleanFlagSet]);
|
|
1458
|
-
}
|
|
1459
|
-
if (positionals >= maxPositionals) {
|
|
1460
|
-
throw new Error(`Unexpected argument for ${command}: ${arg}`);
|
|
1461
|
-
}
|
|
1462
|
-
positionals += 1;
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
function unknownSubcommandError(command, subcommand, expected) {
|
|
1466
|
-
const suggestion = closestSuggestion(subcommand, expected);
|
|
1467
|
-
const suggestionText = suggestion ? ` Did you mean \`prodex ${command} ${suggestion}\`?` : "";
|
|
1468
|
-
return new Error(`Unknown ${command} subcommand: ${subcommand}.${suggestionText} Expected one of: ${expected.join(", ")}. Run \`prodex ${command} --help\`.`);
|
|
1469
|
-
}
|
|
1470
|
-
function unknownTopLevelCommandError(command) {
|
|
1471
|
-
const suggestion = closestSuggestion(command, TOP_LEVEL_COMMANDS);
|
|
1472
|
-
const suggestionText = suggestion ? ` Did you mean \`prodex ${suggestion}\`?` : "";
|
|
1473
|
-
return new Error(`Unknown command: ${command}.${suggestionText} Run \`prodex help\`.`);
|
|
1474
|
-
}
|
|
1475
|
-
function unknownOptionError(option, command, candidates) {
|
|
1476
|
-
const suggestion = closestSuggestion(option, candidates);
|
|
1477
|
-
const suggestionText = suggestion ? `. Did you mean \`${suggestion}\`?` : "";
|
|
1478
|
-
const context = command ? ` for ${command}` : "";
|
|
1479
|
-
return new Error(`Unknown option${context}: ${option}${suggestionText}`);
|
|
1480
|
-
}
|
|
1481
|
-
function closestSuggestion(value, candidates) {
|
|
1482
|
-
let best;
|
|
1483
|
-
for (const candidate of candidates) {
|
|
1484
|
-
const distance = editDistance(value, candidate);
|
|
1485
|
-
const prefixMatch = isUsefulPrefixSuggestion(value, candidate);
|
|
1486
|
-
if (!best || (prefixMatch && !best.prefixMatch) || (prefixMatch === best.prefixMatch && distance < best.distance)) {
|
|
1487
|
-
best = { command: candidate, distance, prefixMatch };
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
return best && (best.prefixMatch || best.distance <= 2) ? best.command : undefined;
|
|
1491
|
-
}
|
|
1492
|
-
function isUsefulPrefixSuggestion(value, candidate) {
|
|
1493
|
-
return value.length >= 5 && candidate.startsWith(value);
|
|
1494
|
-
}
|
|
1495
|
-
function editDistance(left, right) {
|
|
1496
|
-
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
1497
|
-
const current = Array.from({ length: right.length + 1 }, () => 0);
|
|
1498
|
-
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
1499
|
-
current[0] = leftIndex;
|
|
1500
|
-
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
1501
|
-
const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
|
|
1502
|
-
current[rightIndex] = Math.min(previous[rightIndex] + 1, current[rightIndex - 1] + 1, previous[rightIndex - 1] + substitutionCost);
|
|
1503
|
-
}
|
|
1504
|
-
previous.splice(0, previous.length, ...current);
|
|
1505
|
-
}
|
|
1506
|
-
return previous[right.length];
|
|
1507
|
-
}
|
|
1508
|
-
function legacyChatGptNamespaceError(subcommand) {
|
|
1509
|
-
const prefix = subcommand ? `Unknown legacy chatgpt subcommand: ${subcommand}.` : "The legacy `chatgpt` namespace is hidden.";
|
|
1510
|
-
return new Error(`${prefix} Use \`prodex pro browser help\` for visible-browser commands.`);
|
|
1511
|
-
}
|
|
1512
|
-
function formatProjectVerificationPrompt(cwd, sourceCli) {
|
|
1513
|
-
const cli = formatCliCommand(sourceCli);
|
|
1514
|
-
const quotedCwd = shellQuote(cwd);
|
|
1515
|
-
const sourceCliOption = formatSourceCliOption(sourceCli);
|
|
1516
|
-
return `ChatGPT Project MCP verification prompt
|
|
1517
|
-
|
|
1518
|
-
Paste this into the ChatGPT Project after adding the prodex MCP server URL.
|
|
1519
|
-
${TOKEN_BEARING_MCP_URL_AUTHORITY_WARNING}
|
|
1520
|
-
|
|
1521
|
-
Please verify the prodex MCP bridge for this private project:
|
|
1522
|
-
|
|
1523
|
-
1. Call the MCP tool \`bridge_create_task\` with:
|
|
1524
|
-
|
|
1525
|
-
{
|
|
1526
|
-
"title": "prodex MCP verification",
|
|
1527
|
-
"prompt": "Verify that this ChatGPT Project can create tasks through the local prodex MCP bridge.",
|
|
1528
|
-
"repo_id": "default"
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
2. Call \`bridge_list_tasks\` with:
|
|
1532
|
-
|
|
1533
|
-
{ "status": "new" }
|
|
1534
|
-
|
|
1535
|
-
3. Call \`bridge_get_task\` with the task_id returned by \`bridge_create_task\`.
|
|
1536
|
-
|
|
1537
|
-
4. Reply with the task_id and whether all three MCP calls succeeded. Ask me to run the local completion command below, then wait.
|
|
1538
|
-
|
|
1539
|
-
5. After I reply exactly \`local completion done\`, call \`bridge_fetch_result\` with:
|
|
1540
|
-
|
|
1541
|
-
{ "task_id": "<task-id>" }
|
|
1542
|
-
|
|
1543
|
-
6. If the fetched result lists artifacts, call \`bridge_fetch_result_artifact\` for each listed result artifact path:
|
|
1544
|
-
|
|
1545
|
-
{ "task_id": "<task-id>", "path": "<artifact-path>" }
|
|
1546
|
-
|
|
1547
|
-
7. Reply with whether \`bridge_fetch_result\` returned the verification result summary and whether every listed result artifact was readable. Do not call repo_write_file_dry_run, repo_write_file_apply, repo_stage_reviewed_paths, or any write/stage tool for this verification.
|
|
1548
|
-
|
|
1549
|
-
Local follow-up after ChatGPT replies:
|
|
1550
|
-
|
|
1551
|
-
cd ${quotedCwd}
|
|
1552
|
-
${cli} tasks list --status new --cwd ${quotedCwd}
|
|
1553
|
-
${cli} tasks show <task-id> --cwd ${quotedCwd}
|
|
1554
|
-
${cli} tasks complete <task-id> --cwd ${quotedCwd} --summary "prodex MCP verification result" --artifact .bridge/artifacts/results/mcp-verification.md="prodex MCP verification artifact"
|
|
1555
|
-
|
|
1556
|
-
Then reply to ChatGPT with:
|
|
247
|
+
Then reply to ChatGPT with:
|
|
1557
248
|
|
|
1558
249
|
local completion done
|
|
1559
250
|
|
|
@@ -1682,192 +373,9 @@ function formatClaudeConfig(cwd, sourceCli) {
|
|
|
1682
373
|
}
|
|
1683
374
|
}, null, 2);
|
|
1684
375
|
}
|
|
1685
|
-
function shellQuote(value) {
|
|
1686
|
-
return /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
1687
|
-
}
|
|
1688
|
-
function formatCliCommand(sourceCli) {
|
|
1689
|
-
return sourceCli ? `node ${shellQuote(sourceCli)}` : "prodex";
|
|
1690
|
-
}
|
|
1691
|
-
function formatInitCommand(sourceCli, options = {}) {
|
|
1692
|
-
return [`${formatCliCommand(sourceCli)} init`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined].filter(Boolean).join(" ");
|
|
1693
|
-
}
|
|
1694
|
-
function formatSetupCommand(sourceCli, options = {}) {
|
|
1695
|
-
return [`${formatCliCommand(sourceCli)} setup`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined].filter(Boolean).join(" ");
|
|
1696
|
-
}
|
|
1697
|
-
function formatSourceCliOption(sourceCli) {
|
|
1698
|
-
return sourceCli ? ` --source-cli ${shellQuote(sourceCli)}` : "";
|
|
1699
|
-
}
|
|
1700
|
-
function formatBrowserLoginCommand(sourceCli, options = {}) {
|
|
1701
|
-
return formatCommandInCwd(formatBrowserLoginCommandBody(sourceCli, options), options.cwd);
|
|
1702
|
-
}
|
|
1703
|
-
function formatBrowserLoginCommandBody(sourceCli, options = {}) {
|
|
1704
|
-
const command = [
|
|
1705
|
-
`${formatCliCommand(sourceCli)} pro browser login${formatSourceCliOption(sourceCli)}`,
|
|
1706
|
-
options.profileDir ? `--profile-dir ${shellQuote(options.profileDir)}` : undefined,
|
|
1707
|
-
options.port ? `--port ${options.port}` : undefined,
|
|
1708
|
-
options.url ? `--url ${shellQuote(options.url)}` : undefined,
|
|
1709
|
-
options.launchTimeoutMs ? `--launch-timeout-ms ${options.launchTimeoutMs}` : undefined
|
|
1710
|
-
]
|
|
1711
|
-
.filter(Boolean)
|
|
1712
|
-
.join(" ");
|
|
1713
|
-
return command;
|
|
1714
|
-
}
|
|
1715
|
-
function formatBrowserSmokeCommand(sourceCli, options = {}) {
|
|
1716
|
-
return formatCommandInCwd(formatBrowserSmokeCommandBody(sourceCli, options), options.cwd);
|
|
1717
|
-
}
|
|
1718
|
-
function formatBrowserSmokeCommandBody(sourceCli, options = {}) {
|
|
1719
|
-
const command = [`${formatCliCommand(sourceCli)} pro browser smoke${formatSourceCliOption(sourceCli)}`, options.port ? `--port ${options.port}` : undefined]
|
|
1720
|
-
.filter(Boolean)
|
|
1721
|
-
.join(" ");
|
|
1722
|
-
return command;
|
|
1723
|
-
}
|
|
1724
|
-
// ask retry commands carry their own --target-url/--confirm-target/"prompt" tail, so callers
|
|
1725
|
-
// preserve the stored argument tail verbatim and only re-point this bare base at the source CLI.
|
|
1726
|
-
function formatBrowserAskCommandBody(sourceCli) {
|
|
1727
|
-
return `${formatCliCommand(sourceCli)} pro browser ask${formatSourceCliOption(sourceCli)}`;
|
|
1728
|
-
}
|
|
1729
|
-
function formatBrowserCheckCommand(sourceCli, options = {}) {
|
|
1730
|
-
const command = [`${formatCliCommand(sourceCli)} pro browser check${formatSourceCliOption(sourceCli)}`, options.port ? `--port ${options.port}` : undefined]
|
|
1731
|
-
.filter(Boolean)
|
|
1732
|
-
.join(" ");
|
|
1733
|
-
return formatCommandInCwd(command, options.cwd);
|
|
1734
|
-
}
|
|
1735
|
-
function formatBrowserTargetAskCommand(sourceCli, options = {}) {
|
|
1736
|
-
const command = [
|
|
1737
|
-
`${formatCliCommand(sourceCli)} pro browser ask${formatSourceCliOption(sourceCli)}`,
|
|
1738
|
-
options.port ? `--port ${options.port}` : undefined,
|
|
1739
|
-
`--target-url ${options.targetUrl ? shellQuote(options.targetUrl) : "<chatgpt-url>"} --confirm-target "prompt"`
|
|
1740
|
-
]
|
|
1741
|
-
.filter(Boolean)
|
|
1742
|
-
.join(" ");
|
|
1743
|
-
return formatCommandInCwd(command, options.cwd);
|
|
1744
|
-
}
|
|
1745
|
-
function formatCommandInCwd(command, cwd) {
|
|
1746
|
-
return cwd ? `cd ${shellQuote(cwd)} && ${command}` : command;
|
|
1747
|
-
}
|
|
1748
|
-
function formatProShowCommand(taskId, sourceCli, options = {}) {
|
|
1749
|
-
return [`${formatCliCommand(sourceCli)} pro show ${shellQuote(taskId)}${formatSourceCliOption(sourceCli)}`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined]
|
|
1750
|
-
.filter(Boolean)
|
|
1751
|
-
.join(" ");
|
|
1752
|
-
}
|
|
1753
|
-
function formatProLatestCommand(sourceCli, options = {}) {
|
|
1754
|
-
return [`${formatCliCommand(sourceCli)} pro latest${formatSourceCliOption(sourceCli)}`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined]
|
|
1755
|
-
.filter(Boolean)
|
|
1756
|
-
.join(" ");
|
|
1757
|
-
}
|
|
1758
|
-
function formatResultResealCommand(taskId, sourceCli, options = {}) {
|
|
1759
|
-
return [
|
|
1760
|
-
`${formatCliCommand(sourceCli)} results reseal ${shellQuote(taskId)} --confirm-current-result`,
|
|
1761
|
-
options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined
|
|
1762
|
-
]
|
|
1763
|
-
.filter(Boolean)
|
|
1764
|
-
.join(" ");
|
|
1765
|
-
}
|
|
1766
|
-
function sourceAwareResultMessage(message, sourceCli, options = {}) {
|
|
1767
|
-
if (!sourceCli && !options.cwd)
|
|
1768
|
-
return message;
|
|
1769
|
-
return message.replace(/`prodex results reseal ([^`\s]+) --confirm-current-result`/g, (_match, taskId) => `\`${formatResultResealCommand(taskId, sourceCli, options)}\``);
|
|
1770
|
-
}
|
|
1771
|
-
function sourceAwareResultError(error, sourceCli, options = {}) {
|
|
1772
|
-
if (!isUntrustedResultError(error))
|
|
1773
|
-
return error;
|
|
1774
|
-
return new Error(sourceAwareResultMessage(errorMessage(error), sourceCli, options), { cause: error });
|
|
1775
|
-
}
|
|
1776
|
-
function formatBlockedConsultRecordedMessage(message, taskId, sourceCli, options = {}) {
|
|
1777
|
-
return `${message}\nblocked consult recorded: ${taskId}; inspect with \`${formatProShowCommand(taskId, sourceCli, options)}\` or \`${formatProLatestCommand(sourceCli, options)}\`.`;
|
|
1778
|
-
}
|
|
1779
|
-
function formatReleaseStatusCommand(sourceCli, options = {}) {
|
|
1780
|
-
return [`${formatCliCommand(sourceCli)} release status${formatSourceCliOption(sourceCli)}`, options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined]
|
|
1781
|
-
.filter(Boolean)
|
|
1782
|
-
.join(" ");
|
|
1783
|
-
}
|
|
1784
|
-
function formatReleasePackCommand(sourceCli, options = {}) {
|
|
1785
|
-
return [
|
|
1786
|
-
`${formatCliCommand(sourceCli)} release pack${formatSourceCliOption(sourceCli)}`,
|
|
1787
|
-
options.cwd ? `--cwd ${shellQuote(options.cwd)}` : undefined,
|
|
1788
|
-
"--pack-destination <dir>"
|
|
1789
|
-
]
|
|
1790
|
-
.filter(Boolean)
|
|
1791
|
-
.join(" ");
|
|
1792
|
-
}
|
|
1793
376
|
function formatGitPushUpstreamCommand(branch) {
|
|
1794
377
|
return `git push -u origin ${shellQuote(branch)}`;
|
|
1795
378
|
}
|
|
1796
|
-
function sourceAwareBrowserNextStep(nextStep, sourceCli, options = {}) {
|
|
1797
|
-
if (!nextStep)
|
|
1798
|
-
return nextStep;
|
|
1799
|
-
const targetRetry = nextStep.match(/^Open (https:\/\/chatgpt\.com\/\S+) in the (visible|dedicated) browser and retry(\. Current: .+|\.)$/);
|
|
1800
|
-
if (targetRetry) {
|
|
1801
|
-
const [, targetUrl, location, suffix] = targetRetry;
|
|
1802
|
-
return `Open ${targetUrl} in the ${location} browser and run \`${formatBrowserTargetAskCommand(sourceCli, {
|
|
1803
|
-
...options,
|
|
1804
|
-
targetUrl
|
|
1805
|
-
})}\`${suffix}`;
|
|
1806
|
-
}
|
|
1807
|
-
if (!sourceCli && !options.port && !options.cwd)
|
|
1808
|
-
return nextStep;
|
|
1809
|
-
return nextStep
|
|
1810
|
-
.replace(/`cd (.+?) && prodex pro browser login([^`]*)?`/g, (_match, cwdPrefix, storedArgs) => {
|
|
1811
|
-
return `\`cd ${cwdPrefix} && ${formatBrowserLoginCommandBody(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1812
|
-
})
|
|
1813
|
-
.replace(/`cd (.+?) && prodex pro browser smoke([^`]*)?`/g, (_match, cwdPrefix, storedArgs) => {
|
|
1814
|
-
return `\`cd ${cwdPrefix} && ${formatBrowserSmokeCommandBody(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1815
|
-
})
|
|
1816
|
-
.replace(/`cd (.+?) && prodex pro browser ask([^`]*)?`/g, (_match, cwdPrefix, storedArgs) => {
|
|
1817
|
-
return `\`cd ${cwdPrefix} && ${formatBrowserAskCommandBody(sourceCli)}${storedArgs ?? ""}\``;
|
|
1818
|
-
})
|
|
1819
|
-
.replace(/`prodex pro browser login([^`]*)?`/g, (_match, storedArgs) => {
|
|
1820
|
-
return `\`${formatBrowserLoginCommand(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1821
|
-
})
|
|
1822
|
-
.replace(/`prodex pro browser smoke([^`]*)?`/g, (_match, storedArgs) => {
|
|
1823
|
-
return `\`${formatBrowserSmokeCommand(sourceCli, browserOptionsWithStoredPort(options, storedArgs))}\``;
|
|
1824
|
-
})
|
|
1825
|
-
.replace(/`prodex pro browser ask([^`]*)?`/g, (_match, storedArgs) => {
|
|
1826
|
-
return `\`${formatBrowserAskCommandBody(sourceCli)}${storedArgs ?? ""}\``;
|
|
1827
|
-
})
|
|
1828
|
-
.replaceAll("pass --target-url with --confirm-target", `run \`${formatBrowserTargetAskCommand(sourceCli, options)}\``);
|
|
1829
|
-
}
|
|
1830
|
-
function browserOptionsWithStoredPort(options, storedArgs) {
|
|
1831
|
-
if (options.port || !storedArgs)
|
|
1832
|
-
return options;
|
|
1833
|
-
const match = storedArgs.match(/(?:^|\s)--port\s+(\d{1,5})(?:\s|$)/);
|
|
1834
|
-
if (!match)
|
|
1835
|
-
return options;
|
|
1836
|
-
const port = Number(match[1]);
|
|
1837
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
1838
|
-
return options;
|
|
1839
|
-
return { ...options, port };
|
|
1840
|
-
}
|
|
1841
|
-
function productCheckBrowserNextStep(nextStep, sourceCli, options = {}) {
|
|
1842
|
-
const sourceAware = sourceAwareBrowserNextStep(nextStep, sourceCli, options);
|
|
1843
|
-
if (!sourceAware)
|
|
1844
|
-
return sourceAware;
|
|
1845
|
-
if (sourceAware.includes("`"))
|
|
1846
|
-
return sourceAware;
|
|
1847
|
-
if (sourceAware.includes("pass --target-url with --confirm-target")) {
|
|
1848
|
-
return sourceAware.replace("pass --target-url with --confirm-target", `run \`${formatBrowserTargetAskCommand(sourceCli, options)}\``);
|
|
1849
|
-
}
|
|
1850
|
-
return sourceAware.replace(/(?:and|then) retry\.$/, `then run \`${formatBrowserSmokeCommand(sourceCli, options)}\`.`);
|
|
1851
|
-
}
|
|
1852
|
-
function sourceAwareBrowserBlocker(blocker, sourceCli, options = {}) {
|
|
1853
|
-
const nextStep = sourceAwareBrowserNextStep(blocker.next_step, sourceCli, options);
|
|
1854
|
-
return nextStep === blocker.next_step ? blocker : { ...blocker, next_step: nextStep };
|
|
1855
|
-
}
|
|
1856
|
-
function sourceAwareSetupMessage(message, sourceCli, options = {}) {
|
|
1857
|
-
if (!sourceCli && !options.cwd)
|
|
1858
|
-
return message;
|
|
1859
|
-
const setupCommand = formatSetupCommand(sourceCli, options);
|
|
1860
|
-
return message
|
|
1861
|
-
.replaceAll("`prodex setup --token-ttl-hours <hours>`", `\`${setupCommand} --token-ttl-hours <hours>\``)
|
|
1862
|
-
.replaceAll("`prodex setup`", `\`${setupCommand}\``);
|
|
1863
|
-
}
|
|
1864
|
-
function sourceAwareReleaseMessage(message, sourceCli, options = {}) {
|
|
1865
|
-
if (!sourceCli && !options.cwd)
|
|
1866
|
-
return message;
|
|
1867
|
-
return message
|
|
1868
|
-
.replaceAll("`prodex release pack --pack-destination <dir>`", `\`${formatReleasePackCommand(sourceCli, options)}\``)
|
|
1869
|
-
.replaceAll("`prodex release status`", `\`${formatReleaseStatusCommand(sourceCli, options)}\``);
|
|
1870
|
-
}
|
|
1871
379
|
async function formatReleaseStatus(cwd, sourceCli, releaseHintCwd) {
|
|
1872
380
|
const packageJsonPath = path.join(cwd, "package.json");
|
|
1873
381
|
const raw = await readReleasePackageJson(packageJsonPath).catch(async (error) => {
|
|
@@ -2724,257 +1232,6 @@ async function runMcpWriteSmoke() {
|
|
|
2724
1232
|
}
|
|
2725
1233
|
}
|
|
2726
1234
|
}
|
|
2727
|
-
function printBrowserLoginGuide(stdout, input) {
|
|
2728
|
-
const loginCommand = formatBrowserLoginCommand(input.sourceCli, input.commandOptions);
|
|
2729
|
-
const runtimeCommandOptions = {
|
|
2730
|
-
...(input.commandOptions?.cwd ? { cwd: input.commandOptions.cwd } : {}),
|
|
2731
|
-
...(input.commandOptions?.port ? { port: input.commandOptions.port } : {})
|
|
2732
|
-
};
|
|
2733
|
-
const checkCommand = formatBrowserCheckCommand(input.sourceCli, runtimeCommandOptions);
|
|
2734
|
-
const smokeCommand = formatBrowserSmokeCommand(input.sourceCli, runtimeCommandOptions);
|
|
2735
|
-
stdout("ChatGPT Pro browser login");
|
|
2736
|
-
stdout(input.opened ? "Opened the dedicated Chrome window for ChatGPT." : "Dry run: no browser was opened.");
|
|
2737
|
-
stdout("");
|
|
2738
|
-
stdout("Steps:");
|
|
2739
|
-
if (input.opened) {
|
|
2740
|
-
stdout(`1. Log in manually at ${input.loginUrl} in the dedicated Chrome window.`);
|
|
2741
|
-
stdout("2. If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, complete it in the browser.");
|
|
2742
|
-
stdout("3. For usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.");
|
|
2743
|
-
stdout("4. Open a normal ChatGPT chat or the intended Project/thread so the prompt composer is visible.");
|
|
2744
|
-
stdout("5. Select the Pro/Thinking model you want in the ChatGPT UI.");
|
|
2745
|
-
stdout(`6. Run \`${checkCommand}\` to confirm the session is reachable.`);
|
|
2746
|
-
stdout(`7. Run \`${smokeCommand}\` to verify a real Pro response path.`);
|
|
2747
|
-
}
|
|
2748
|
-
else {
|
|
2749
|
-
stdout(`1. Run \`${loginCommand}\` without \`--dry-run\` to open the dedicated Chrome window.`);
|
|
2750
|
-
stdout(`2. Log in manually at ${input.loginUrl} in that Chrome window.`);
|
|
2751
|
-
stdout("3. If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, complete it in the browser.");
|
|
2752
|
-
stdout("4. For usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.");
|
|
2753
|
-
stdout("5. Open a normal ChatGPT chat or the intended Project/thread so the prompt composer is visible.");
|
|
2754
|
-
stdout("6. Select the Pro/Thinking model you want in the ChatGPT UI.");
|
|
2755
|
-
stdout(`7. Run \`${checkCommand}\` to confirm the session is reachable.`);
|
|
2756
|
-
stdout(`8. Run \`${smokeCommand}\` to verify a real Pro response path.`);
|
|
2757
|
-
}
|
|
2758
|
-
stdout("");
|
|
2759
|
-
stdout(`Profile: ${input.profileDir}`);
|
|
2760
|
-
stdout(`Debug: http://127.0.0.1:${input.port}`);
|
|
2761
|
-
if (input.opened) {
|
|
2762
|
-
stdout("You can close this Chrome window after check/smoke or when you are done. The dedicated profile is reused next time.");
|
|
2763
|
-
}
|
|
2764
|
-
else {
|
|
2765
|
-
stdout("The dedicated profile path above will be reused by the real login command.");
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
function printProBrowserHelp(stdout, sourceCli) {
|
|
2769
|
-
const cli = formatCliCommand(sourceCli);
|
|
2770
|
-
const sourceCliOption = formatSourceCliOption(sourceCli);
|
|
2771
|
-
const loginUsage = sourceCli
|
|
2772
|
-
? `${cli} pro browser login${sourceCliOption} [--cwd /absolute/path/to/repo] [--dry-run] [--profile-dir path] [--port 9333] [--url https://chatgpt.com/...] [--launch-timeout-ms 5000]`
|
|
2773
|
-
: "prodex pro browser login [--cwd /absolute/path/to/repo] [--dry-run] [--source-cli /absolute/path/to/dist/cli.js] [--profile-dir path] [--port 9333] [--url https://chatgpt.com/...] [--launch-timeout-ms 5000]";
|
|
2774
|
-
const checkUsage = sourceCli
|
|
2775
|
-
? `${cli} pro browser check${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 1500]`
|
|
2776
|
-
: "prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 1500]";
|
|
2777
|
-
const smokeUsage = sourceCli
|
|
2778
|
-
? `${cli} pro browser smoke${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]`
|
|
2779
|
-
: "prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]";
|
|
2780
|
-
const selectionUsage = '[--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"]';
|
|
2781
|
-
const askUsage = sourceCli
|
|
2782
|
-
? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--target-url url --confirm-target] [--file path] ${selectionUsage} "prompt"`
|
|
2783
|
-
: `prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--target-url url --confirm-target] [--file path] ${selectionUsage} "prompt"`;
|
|
2784
|
-
const modelsUsage = sourceCli
|
|
2785
|
-
? `${cli} pro browser models${sourceCliOption} [--port 9333] [--timeout-ms 15000]`
|
|
2786
|
-
: "prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000]";
|
|
2787
|
-
stdout(`${cli} pro browser
|
|
2788
|
-
|
|
2789
|
-
Commands:
|
|
2790
|
-
${loginUsage}
|
|
2791
|
-
${checkUsage}
|
|
2792
|
-
${smokeUsage}
|
|
2793
|
-
${modelsUsage}
|
|
2794
|
-
${askUsage}
|
|
2795
|
-
|
|
2796
|
-
Visible-browser sends require a manual browser session and stop on login, captcha, Cloudflare, permission, rate-limit, or usage-limit blockers.
|
|
2797
|
-
Model/project selection (ask):
|
|
2798
|
-
--model Composer model to pick by its exact menu label (verified: Pro). Models whose menu entry opens a submenu of variants are rejected with a clear error for now.
|
|
2799
|
-
--pro-mode Pro sub-mode: 기본 (standard) or 확장 (extended), used when the model is Pro. 확장 raises the default --timeout-ms to 300000.
|
|
2800
|
-
--effort Reasoning effort: 즉시 / 중간 / 높음 / 매우 높음 (aliases: instant/medium/high/max). Picking an effort switches the composer to the standard reasoning model, deselecting Pro.
|
|
2801
|
-
--project Enter an existing sidebar project before sending. Cannot be combined with --target-url.
|
|
2802
|
-
--pro-mode and --effort cannot be combined. These labels match the Korean ChatGPT UI; set your ChatGPT display language to Korean for selection flags.
|
|
2803
|
-
Run \`${cli} pro browser models${sourceCliOption}\` to list the labels your account currently shows.
|
|
2804
|
-
Persist defaults with \`${cli} setup${sourceCliOption}\`; per-ask flags override them.
|
|
2805
|
-
Use \`${cli} pro ask\` for dry-run/manual previews.
|
|
2806
|
-
\`${cli} pro browser ask${sourceCliOption}\` always attempts an explicit visible-browser send.`);
|
|
2807
|
-
}
|
|
2808
|
-
async function assertBrowserLaunchStayedAlive(opened, timeoutMs) {
|
|
2809
|
-
const outcome = await waitForBrowserLaunchReady(opened, timeoutMs);
|
|
2810
|
-
if (outcome.reachable)
|
|
2811
|
-
return;
|
|
2812
|
-
if (outcome.earlyExit) {
|
|
2813
|
-
const detail = formatBrowserEarlyExit(outcome.earlyExit);
|
|
2814
|
-
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.`);
|
|
2815
|
-
}
|
|
2816
|
-
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.`);
|
|
2817
|
-
}
|
|
2818
|
-
async function waitForBrowserLaunchReady(opened, timeoutMs = 5_000) {
|
|
2819
|
-
const deadline = Date.now() + timeoutMs;
|
|
2820
|
-
let earlyExit;
|
|
2821
|
-
while (Date.now() <= deadline) {
|
|
2822
|
-
const remainingMs = Math.max(1, deadline - Date.now());
|
|
2823
|
-
const status = await getChatGptBrowserStatus({ port: opened.port, timeoutMs: Math.min(250, remainingMs) });
|
|
2824
|
-
if (status.reachable)
|
|
2825
|
-
return { reachable: true };
|
|
2826
|
-
earlyExit ??= await opened.waitForEarlyExit(1);
|
|
2827
|
-
if (earlyExit && (earlyExit.code !== 0 || earlyExit.signal || earlyExit.error)) {
|
|
2828
|
-
return { reachable: false, earlyExit };
|
|
2829
|
-
}
|
|
2830
|
-
if (Date.now() >= deadline)
|
|
2831
|
-
break;
|
|
2832
|
-
await sleep(Math.min(100, Math.max(1, deadline - Date.now())));
|
|
2833
|
-
}
|
|
2834
|
-
return { reachable: false, ...(earlyExit ? { earlyExit } : {}) };
|
|
2835
|
-
}
|
|
2836
|
-
function formatBrowserEarlyExit(exit) {
|
|
2837
|
-
if (!exit)
|
|
2838
|
-
return "no exit details";
|
|
2839
|
-
return exit.error ?? `exit code ${exit.code ?? "null"}${exit.signal ? ` signal ${exit.signal}` : ""}`;
|
|
2840
|
-
}
|
|
2841
|
-
function sleep(ms) {
|
|
2842
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2843
|
-
}
|
|
2844
|
-
function formatBrowserModelHints(modelHints) {
|
|
2845
|
-
const modelish = /\b(?:ChatGPT|GPT(?:-[\w.]+)?|Pro|Plus|Team|Enterprise|Thinking|Extra High|Auto)\b/i;
|
|
2846
|
-
const hints = [...new Set(modelHints.map((hint) => hint.trim()).filter((hint) => modelish.test(hint)))]
|
|
2847
|
-
.map((hint) => (hint.length > 80 ? `${hint.slice(0, 77)}...` : hint))
|
|
2848
|
-
.slice(0, 6);
|
|
2849
|
-
return hints.length > 0 ? hints.join(" | ") : undefined;
|
|
2850
|
-
}
|
|
2851
|
-
function browserReadinessNextStep(input) {
|
|
2852
|
-
if (!input.loggedInLikely) {
|
|
2853
|
-
return "Log in manually in the visible ChatGPT browser, then retry.";
|
|
2854
|
-
}
|
|
2855
|
-
if (!input.hasComposer) {
|
|
2856
|
-
return "Open a normal ChatGPT chat or Project thread, select the Pro/Thinking model, and retry.";
|
|
2857
|
-
}
|
|
2858
|
-
return "Review the visible ChatGPT browser state, then retry.";
|
|
2859
|
-
}
|
|
2860
|
-
async function printProductCheck(store, io, args, configCwd = io.cwd) {
|
|
2861
|
-
const sourceCli = resolveOptionalFileFlag(io.cwd, args, "--source-cli");
|
|
2862
|
-
const setupHintCwd = readFlag(args, "--cwd") ? configCwd : undefined;
|
|
2863
|
-
io.stdout("prodex product check");
|
|
2864
|
-
let bridgeReady = false;
|
|
2865
|
-
try {
|
|
2866
|
-
bridgeReady = await store.hasReadyBridgeStorageReadOnly();
|
|
2867
|
-
io.stdout(bridgeReady
|
|
2868
|
-
? "bridge: ok (.bridge)"
|
|
2869
|
-
: `bridge: missing (.bridge) - run \`${formatInitCommand(sourceCli, { cwd: setupHintCwd })}\` when you need local task/result storage`);
|
|
2870
|
-
}
|
|
2871
|
-
catch (error) {
|
|
2872
|
-
io.stdout(`bridge: blocked - ${errorMessage(error)}`);
|
|
2873
|
-
}
|
|
2874
|
-
let configReady = false;
|
|
2875
|
-
try {
|
|
2876
|
-
const config = await loadLocalConfig(configCwd);
|
|
2877
|
-
const tokenStatus = getTokenExpiryStatus(config);
|
|
2878
|
-
if (tokenStatus.status === "expired") {
|
|
2879
|
-
io.stdout(`config: expired - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\``);
|
|
2880
|
-
}
|
|
2881
|
-
else {
|
|
2882
|
-
io.stdout(`config: ok ${redactServerUrl(config.server_url)} token_status=${tokenStatus.status}`);
|
|
2883
|
-
const warningLine = formatConfigWarningLine(tokenStatus, sourceCli, setupHintCwd);
|
|
2884
|
-
if (warningLine)
|
|
2885
|
-
io.stdout(warningLine);
|
|
2886
|
-
configReady = true;
|
|
2887
|
-
}
|
|
2888
|
-
}
|
|
2889
|
-
catch (error) {
|
|
2890
|
-
if (isMissingFileError(error)) {
|
|
2891
|
-
io.stdout(`config: missing - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\``);
|
|
2892
|
-
}
|
|
2893
|
-
else {
|
|
2894
|
-
io.stdout(`config: failed ${sourceAwareSetupMessage(errorMessage(error), sourceCli, { cwd: setupHintCwd })}`);
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
const browserStatus = await getChatGptBrowserStatus({
|
|
2898
|
-
port: readPortFlag(args, "--port") ?? 9333,
|
|
2899
|
-
timeoutMs: readPositiveNumberFlag(args, "--timeout-ms") ?? 1500
|
|
2900
|
-
});
|
|
2901
|
-
const browserCommandOptions = {
|
|
2902
|
-
cwd: setupHintCwd,
|
|
2903
|
-
port: readPortFlag(args, "--port") ?? undefined
|
|
2904
|
-
};
|
|
2905
|
-
let chatgptReady = false;
|
|
2906
|
-
const visibilityBlocker = chatGptVisibilityBlocker(browserStatus.visibilityState, browserStatus.url);
|
|
2907
|
-
if (!browserStatus.reachable) {
|
|
2908
|
-
io.stdout(`chatgpt: ${browserStatus.blocker?.code ?? "unreachable"} - ${browserStatus.blocker?.message ?? "browser is not reachable"}`);
|
|
2909
|
-
const nextStep = productCheckBrowserNextStep(browserStatus.blocker?.next_step, sourceCli, browserCommandOptions);
|
|
2910
|
-
if (nextStep)
|
|
2911
|
-
io.stdout(`next: ${nextStep}`);
|
|
2912
|
-
}
|
|
2913
|
-
else if (browserStatus.blocker) {
|
|
2914
|
-
const visibilityText = browserStatus.blocker.code === "tab_not_visible" ? ` visibility=${browserStatus.visibilityState ?? "unknown"}` : "";
|
|
2915
|
-
io.stdout(`chatgpt: blocked ${browserStatus.blocker.code}${visibilityText} - ${browserStatus.blocker.message}`);
|
|
2916
|
-
const nextStep = productCheckBrowserNextStep(browserStatus.blocker.next_step, sourceCli, browserCommandOptions);
|
|
2917
|
-
if (nextStep)
|
|
2918
|
-
io.stdout(`next: ${nextStep}`);
|
|
2919
|
-
}
|
|
2920
|
-
else if (visibilityBlocker) {
|
|
2921
|
-
io.stdout(`chatgpt: blocked ${visibilityBlocker.code} visibility=${browserStatus.visibilityState ?? "unknown"} - ${visibilityBlocker.message}`);
|
|
2922
|
-
const nextStep = productCheckBrowserNextStep(visibilityBlocker.next_step, sourceCli, browserCommandOptions);
|
|
2923
|
-
if (nextStep)
|
|
2924
|
-
io.stdout(`next: ${nextStep}`);
|
|
2925
|
-
}
|
|
2926
|
-
else if (browserStatus.loggedInLikely && browserStatus.hasComposer) {
|
|
2927
|
-
io.stdout(`chatgpt: ok logged_in=true composer=true${browserStatus.url ? ` url=${browserStatus.url}` : ""}`);
|
|
2928
|
-
chatgptReady = true;
|
|
2929
|
-
}
|
|
2930
|
-
else {
|
|
2931
|
-
io.stdout(`chatgpt: blocked logged_in=${browserStatus.loggedInLikely} composer=${browserStatus.hasComposer}`);
|
|
2932
|
-
const nextStep = productCheckBrowserNextStep(browserReadinessNextStep(browserStatus), sourceCli, browserCommandOptions);
|
|
2933
|
-
io.stdout(`next: ${nextStep}`);
|
|
2934
|
-
}
|
|
2935
|
-
const modelHints = formatBrowserModelHints(browserStatus.modelHints);
|
|
2936
|
-
if (modelHints)
|
|
2937
|
-
io.stdout(`model_hints: ${modelHints}`);
|
|
2938
|
-
if (bridgeReady) {
|
|
2939
|
-
try {
|
|
2940
|
-
const latest = await latestTrustedConsult(store, { readOnly: false });
|
|
2941
|
-
if (latest) {
|
|
2942
|
-
for (const line of formatProductCheckLatestProLines(latest, sourceCli, browserCommandOptions))
|
|
2943
|
-
io.stdout(line);
|
|
2944
|
-
}
|
|
2945
|
-
else {
|
|
2946
|
-
io.stdout("latest_pro: missing");
|
|
2947
|
-
}
|
|
2948
|
-
}
|
|
2949
|
-
catch (error) {
|
|
2950
|
-
if (isUntrustedResultError(error)) {
|
|
2951
|
-
io.stdout(`latest_pro: untrusted ${error.taskId} ${sourceAwareResultMessage(errorMessage(error), sourceCli, browserCommandOptions)}`);
|
|
2952
|
-
}
|
|
2953
|
-
else {
|
|
2954
|
-
io.stdout(`latest_pro: unavailable ${firstLine(errorMessage(error))}`);
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
}
|
|
2958
|
-
else {
|
|
2959
|
-
io.stdout("latest_pro: missing");
|
|
2960
|
-
}
|
|
2961
|
-
return bridgeReady && configReady && chatgptReady;
|
|
2962
|
-
}
|
|
2963
|
-
async function listTasksForInspection(store, status) {
|
|
2964
|
-
return store.listTasksReadOnly(status);
|
|
2965
|
-
}
|
|
2966
|
-
async function listResultsForInspection(store) {
|
|
2967
|
-
return store.listFinalizedResultsReadOnly();
|
|
2968
|
-
}
|
|
2969
|
-
async function listRawResultsForInspection(store) {
|
|
2970
|
-
return store.listResultsReadOnly();
|
|
2971
|
-
}
|
|
2972
|
-
async function listReceiptsForInspection(store, input = {}) {
|
|
2973
|
-
return store.listReceiptsReadOnly(input);
|
|
2974
|
-
}
|
|
2975
|
-
async function listSessionsForInspection(store, status) {
|
|
2976
|
-
return store.listSessionsReadOnly(status);
|
|
2977
|
-
}
|
|
2978
1235
|
async function listConsults(store, options = {}) {
|
|
2979
1236
|
const [tasks, results] = options.readOnly
|
|
2980
1237
|
? await Promise.all([listTasksForInspection(store), listRawResultsForInspection(store)])
|
|
@@ -2995,741 +1252,16 @@ async function listConsults(store, options = {}) {
|
|
|
2995
1252
|
}
|
|
2996
1253
|
return finalized;
|
|
2997
1254
|
}
|
|
2998
|
-
async function listConsultListEntries(store, options = { readOnly: true }) {
|
|
2999
|
-
const [tasks, results] = options.readOnly === false
|
|
3000
|
-
? await Promise.all([store.listTasks(), store.listResults()])
|
|
3001
|
-
: await Promise.all([listTasksForInspection(store), listRawResultsForInspection(store)]);
|
|
3002
|
-
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
3003
|
-
assertNoMissingTerminalConsultResults(tasks, results);
|
|
3004
|
-
assertNoOrphanConsultResults(tasksById, results);
|
|
3005
|
-
const records = results
|
|
3006
|
-
.map((result) => {
|
|
3007
|
-
const task = tasksById.get(result.task_id);
|
|
3008
|
-
return task ? { task, result } : undefined;
|
|
3009
|
-
})
|
|
3010
|
-
.filter((record) => Boolean(record && isConsultRecord(record)))
|
|
3011
|
-
.sort((a, b) => b.result.created_at.localeCompare(a.result.created_at));
|
|
3012
|
-
const entries = [];
|
|
3013
|
-
for (const record of records) {
|
|
3014
|
-
try {
|
|
3015
|
-
entries.push({ kind: "trusted", consult: { ...record, result: await store.getFinalizedResultReadOnly(record.result.task_id) } });
|
|
3016
|
-
}
|
|
3017
|
-
catch (error) {
|
|
3018
|
-
if (isUntrustedResultError(error)) {
|
|
3019
|
-
entries.push({ kind: "untrusted", task: record.task, result: record.result, error });
|
|
3020
|
-
continue;
|
|
3021
|
-
}
|
|
3022
|
-
throw error;
|
|
3023
|
-
}
|
|
3024
|
-
}
|
|
3025
|
-
return entries;
|
|
3026
|
-
}
|
|
3027
|
-
async function latestTrustedConsult(store, options = { readOnly: true }) {
|
|
3028
|
-
const entries = await listConsultListEntries(store, options);
|
|
3029
|
-
const trusted = entries.find((entry) => entry.kind === "trusted");
|
|
3030
|
-
if (trusted)
|
|
3031
|
-
return trusted.consult;
|
|
3032
|
-
const untrusted = entries.find((entry) => entry.kind === "untrusted");
|
|
3033
|
-
if (untrusted)
|
|
3034
|
-
throw untrusted.error;
|
|
3035
|
-
return undefined;
|
|
3036
|
-
}
|
|
3037
|
-
function assertNoOrphanConsultResults(tasksById, results) {
|
|
3038
|
-
const orphan = results
|
|
3039
|
-
.filter((result) => !tasksById.has(result.task_id) && isConsultResult(result))
|
|
3040
|
-
.sort((a, b) => b.created_at.localeCompare(a.created_at) || b.task_id.localeCompare(a.task_id))[0];
|
|
3041
|
-
if (orphan)
|
|
3042
|
-
throw orphanConsultResultError(orphan.task_id);
|
|
3043
|
-
}
|
|
3044
|
-
async function latestResultTaskId(store, options = {}) {
|
|
3045
|
-
const results = options.readOnly ? await listResultsForInspection(store) : await store.listResults();
|
|
3046
|
-
const result = results.at(-1);
|
|
3047
|
-
if (!result)
|
|
3048
|
-
throw new Error("No results found");
|
|
3049
|
-
return result.task_id;
|
|
3050
|
-
}
|
|
3051
|
-
async function latestRawResultTaskId(store) {
|
|
3052
|
-
const result = (await store.listResults()).at(-1);
|
|
3053
|
-
if (!result)
|
|
3054
|
-
throw new Error("No results found");
|
|
3055
|
-
return result.task_id;
|
|
3056
|
-
}
|
|
3057
|
-
async function latestTask(store, options = {}) {
|
|
3058
|
-
const tasks = options.readOnly ? await listTasksForInspection(store) : await store.listTasks();
|
|
3059
|
-
return tasks.sort((a, b) => b.created_at.localeCompare(a.created_at) || b.id.localeCompare(a.id))[0];
|
|
3060
|
-
}
|
|
3061
|
-
async function writeTaskCompleteArtifacts(store, values) {
|
|
3062
|
-
const artifacts = [];
|
|
3063
|
-
for (const value of values) {
|
|
3064
|
-
const separator = value.indexOf("=");
|
|
3065
|
-
if (separator <= 0) {
|
|
3066
|
-
throw new Error("tasks complete --artifact requires path=text");
|
|
3067
|
-
}
|
|
3068
|
-
const artifactPath = value.slice(0, separator);
|
|
3069
|
-
const content = value.slice(separator + 1);
|
|
3070
|
-
if (!artifactPath.trim()) {
|
|
3071
|
-
throw new Error("tasks complete --artifact requires path=text");
|
|
3072
|
-
}
|
|
3073
|
-
const storedPath = await store.writeArtifactText(artifactPath, content);
|
|
3074
|
-
artifacts.push({ path: storedPath, role: "result" });
|
|
3075
|
-
}
|
|
3076
|
-
return artifacts;
|
|
3077
|
-
}
|
|
3078
|
-
async function getConsult(store, taskId, options = {}) {
|
|
3079
|
-
let task;
|
|
3080
|
-
try {
|
|
3081
|
-
task = options.readOnly ? await store.getTaskReadOnly(taskId) : await store.getTask(taskId);
|
|
3082
|
-
}
|
|
3083
|
-
catch (error) {
|
|
3084
|
-
if (isMissingFileError(error))
|
|
3085
|
-
return undefined;
|
|
3086
|
-
throw error;
|
|
3087
|
-
}
|
|
3088
|
-
if (!isConsultTask(task))
|
|
3089
|
-
return undefined;
|
|
3090
|
-
let result;
|
|
3091
|
-
try {
|
|
3092
|
-
result = await store.getFinalizedResultReadOnly(taskId);
|
|
3093
|
-
}
|
|
3094
|
-
catch (error) {
|
|
3095
|
-
if (isMissingFileError(error)) {
|
|
3096
|
-
if (isTerminalTask(task) && isConsultTask(task))
|
|
3097
|
-
throw missingConsultResultError(task);
|
|
3098
|
-
return undefined;
|
|
3099
|
-
}
|
|
3100
|
-
throw error;
|
|
3101
|
-
}
|
|
3102
|
-
if (!task || !result)
|
|
3103
|
-
return undefined;
|
|
3104
|
-
const record = { task, result };
|
|
3105
|
-
return isConsultRecord(record) ? record : undefined;
|
|
3106
|
-
}
|
|
3107
|
-
function assertNoMissingTerminalConsultResults(tasks, results) {
|
|
3108
|
-
const resultTaskIds = new Set(results.map((result) => result.task_id));
|
|
3109
|
-
const missing = tasks
|
|
3110
|
-
.filter((task) => isTerminalTask(task) && isConsultTask(task) && !resultTaskIds.has(task.id))
|
|
3111
|
-
.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.id.localeCompare(a.id))[0];
|
|
3112
|
-
if (missing)
|
|
3113
|
-
throw missingConsultResultError(missing);
|
|
3114
|
-
}
|
|
3115
|
-
function isTerminalTask(task) {
|
|
3116
|
-
return task.status === "done" || task.status === "blocked";
|
|
3117
|
-
}
|
|
3118
|
-
function isConsultTask(task) {
|
|
3119
|
-
return task.provenance.adapter === "chatgpt-control";
|
|
3120
|
-
}
|
|
3121
|
-
function isConsultResult(result) {
|
|
3122
|
-
return result.commands.some((command) => /chatgpt|gpt pro|visible ChatGPT/i.test(command));
|
|
3123
|
-
}
|
|
3124
|
-
function missingConsultResultError(task) {
|
|
3125
|
-
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.`);
|
|
3126
|
-
}
|
|
3127
|
-
function orphanConsultResultError(taskId) {
|
|
3128
|
-
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.`);
|
|
3129
|
-
}
|
|
3130
|
-
function isConsultRecord(record) {
|
|
3131
|
-
return isConsultTask(record.task);
|
|
3132
|
-
}
|
|
3133
|
-
function formatProAnswer(consult, sourceCli, options = {}) {
|
|
3134
|
-
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
3135
|
-
const summary = sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker);
|
|
3136
|
-
const lines = [
|
|
3137
|
-
`task_id: ${consult.task.id}`,
|
|
3138
|
-
`status: ${consult.result.status}`,
|
|
3139
|
-
consult.task.provenance.thread ? `thread: ${consult.task.provenance.thread}` : undefined,
|
|
3140
|
-
`created_at: ${consult.result.created_at}`,
|
|
3141
|
-
"",
|
|
3142
|
-
summary
|
|
3143
|
-
].filter((line) => line !== undefined);
|
|
3144
|
-
if (blocker) {
|
|
3145
|
-
lines.push("", "blocker:", `- code: ${blocker.code}`, `- retryable: ${blocker.retryable}`);
|
|
3146
|
-
if (blocker.next_step)
|
|
3147
|
-
lines.push(`- next_step: ${blocker.next_step}`);
|
|
3148
|
-
}
|
|
3149
|
-
if (consult.result.warnings.length > 0) {
|
|
3150
|
-
lines.push("", "warnings:");
|
|
3151
|
-
for (const warning of consult.result.warnings)
|
|
3152
|
-
lines.push(`- ${warning}`);
|
|
3153
|
-
}
|
|
3154
|
-
return lines.join("\n");
|
|
3155
|
-
}
|
|
3156
|
-
function formatProListSummary(consult, sourceCli, options = {}) {
|
|
3157
|
-
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
3158
|
-
return firstLine(sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker));
|
|
3159
|
-
}
|
|
3160
|
-
function formatProductCheckLatestProLines(consult, sourceCli, options = {}) {
|
|
3161
|
-
if (consult.result.status === "blocked") {
|
|
3162
|
-
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
3163
|
-
const code = blocker?.code ?? "unknown";
|
|
3164
|
-
const retryable = blocker?.retryable ?? false;
|
|
3165
|
-
const lines = [`latest_pro: blocked ${consult.task.id} code=${code} retryable=${retryable} ${consult.result.created_at}`];
|
|
3166
|
-
if (blocker?.next_step)
|
|
3167
|
-
lines.push(`latest_pro_next: ${blocker.next_step}`);
|
|
3168
|
-
return lines;
|
|
3169
|
-
}
|
|
3170
|
-
return [`latest_pro: ok ${consult.task.id} ${consult.result.status} ${consult.result.created_at}`];
|
|
3171
|
-
}
|
|
3172
|
-
function sourceAwareProAnswerBlocker(consult, sourceCli, options = {}) {
|
|
3173
|
-
if (!consult.result.blocker)
|
|
3174
|
-
return undefined;
|
|
3175
|
-
const browserAware = sourceAwareBrowserBlocker(consult.result.blocker, sourceCli, options);
|
|
3176
|
-
if ((!sourceCli && !options.cwd && !options.port) || !isSmokeConsultRecord(consult) || !browserAware.next_step)
|
|
3177
|
-
return browserAware;
|
|
3178
|
-
const nextStep = productCheckBrowserNextStep(browserAware.next_step, sourceCli, options);
|
|
3179
|
-
return nextStep === browserAware.next_step ? browserAware : { ...browserAware, next_step: nextStep };
|
|
3180
|
-
}
|
|
3181
|
-
function sourceAwareProAnswerSummary(summary, originalBlocker, displayedBlocker) {
|
|
3182
|
-
const originalNextStep = originalBlocker?.next_step;
|
|
3183
|
-
const displayedNextStep = displayedBlocker?.next_step;
|
|
3184
|
-
if (!originalNextStep || !displayedNextStep || originalNextStep === displayedNextStep)
|
|
3185
|
-
return summary;
|
|
3186
|
-
return summary.replaceAll(originalNextStep, displayedNextStep);
|
|
3187
|
-
}
|
|
3188
|
-
function isSmokeConsultRecord(consult) {
|
|
3189
|
-
return consult.task.title === "GPT Pro smoke" || consult.result.commands.includes("visible ChatGPT browser smoke");
|
|
3190
|
-
}
|
|
3191
|
-
function receiptInspectionListSuffix(receipt) {
|
|
3192
|
-
const status = receipt.metadata.integrity_status;
|
|
3193
|
-
if (typeof status === "object" &&
|
|
3194
|
-
status !== null &&
|
|
3195
|
-
"trusted" in status &&
|
|
3196
|
-
status.trusted === false) {
|
|
3197
|
-
return "\tintegrity=untrusted";
|
|
3198
|
-
}
|
|
3199
|
-
return "";
|
|
3200
|
-
}
|
|
3201
|
-
function formatSession(session) {
|
|
3202
|
-
return JSON.stringify({
|
|
3203
|
-
id: session.id,
|
|
3204
|
-
status: session.status,
|
|
3205
|
-
direction: session.direction,
|
|
3206
|
-
backend: session.backend,
|
|
3207
|
-
project: session.project,
|
|
3208
|
-
thread: session.thread,
|
|
3209
|
-
task_id: session.task_id,
|
|
3210
|
-
blocker: session.blocker,
|
|
3211
|
-
warnings: session.warnings,
|
|
3212
|
-
created_at: session.created_at,
|
|
3213
|
-
last_used_at: session.last_used_at
|
|
3214
|
-
}, null, 2);
|
|
3215
|
-
}
|
|
3216
|
-
function formatProConsultArtifact(consult) {
|
|
3217
|
-
const lines = [`# ChatGPT Pro Consult`, "", `Thread: ${consult.url}`, `Title: ${consult.title}`, ""];
|
|
3218
|
-
if (consult.modelHints.length > 0) {
|
|
3219
|
-
lines.push("Model hints:", ...consult.modelHints.map((hint) => `- ${hint}`), "");
|
|
3220
|
-
}
|
|
3221
|
-
if (consult.warnings.length > 0) {
|
|
3222
|
-
lines.push("Warnings:", ...consult.warnings.map((warning) => `- ${warning}`), "");
|
|
3223
|
-
}
|
|
3224
|
-
lines.push("## Answer", "", consult.answer.trim(), "");
|
|
3225
|
-
return lines.join("\n");
|
|
3226
|
-
}
|
|
3227
|
-
async function writeSessionBestEffort(store, input, io) {
|
|
3228
|
-
try {
|
|
3229
|
-
await store.writeSession(input);
|
|
3230
|
-
}
|
|
3231
|
-
catch (error) {
|
|
3232
|
-
io.stderr(`session_record_warning: ${errorMessage(error)}`);
|
|
3233
|
-
}
|
|
3234
|
-
}
|
|
3235
|
-
async function writeSessionBeforeBrowserSend(store, input) {
|
|
3236
|
-
try {
|
|
3237
|
-
await store.writeSession(input);
|
|
3238
|
-
}
|
|
3239
|
-
catch (error) {
|
|
3240
|
-
throw new Error(`failed to record running consult session before browser send: ${errorMessage(error)}`);
|
|
3241
|
-
}
|
|
3242
|
-
}
|
|
3243
|
-
function firstLine(value) {
|
|
3244
|
-
return value.split(/\r?\n/).find((line) => line.trim())?.trim() ?? "";
|
|
3245
|
-
}
|
|
3246
|
-
function errorMessage(error) {
|
|
3247
|
-
return error instanceof Error ? error.message : String(error);
|
|
3248
|
-
}
|
|
3249
|
-
function browserSendBlockerFromError(error) {
|
|
3250
|
-
const blocker = typeof error === "object" && error !== null && "blocker" in error ? error.blocker : undefined;
|
|
3251
|
-
if (typeof blocker === "object" &&
|
|
3252
|
-
blocker !== null &&
|
|
3253
|
-
"code" in blocker &&
|
|
3254
|
-
"message" in blocker &&
|
|
3255
|
-
"retryable" in blocker &&
|
|
3256
|
-
typeof blocker.code === "string" &&
|
|
3257
|
-
typeof blocker.message === "string" &&
|
|
3258
|
-
typeof blocker.retryable === "boolean") {
|
|
3259
|
-
return {
|
|
3260
|
-
code: blocker.code,
|
|
3261
|
-
message: blocker.message,
|
|
3262
|
-
retryable: blocker.retryable,
|
|
3263
|
-
...("next_step" in blocker && typeof blocker.next_step === "string" ? { next_step: blocker.next_step } : {})
|
|
3264
|
-
};
|
|
3265
|
-
}
|
|
3266
|
-
const message = errorMessage(error);
|
|
3267
|
-
return {
|
|
3268
|
-
code: "browser_send_failed",
|
|
3269
|
-
message,
|
|
3270
|
-
retryable: true,
|
|
3271
|
-
next_step: "Resolve the visible browser issue manually, then rerun the consult if needed."
|
|
3272
|
-
};
|
|
3273
|
-
}
|
|
3274
|
-
function isMissingFileError(error) {
|
|
3275
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3276
|
-
}
|
|
3277
|
-
function isUntrustedResultError(error) {
|
|
3278
|
-
return (typeof error === "object" &&
|
|
3279
|
-
error !== null &&
|
|
3280
|
-
"code" in error &&
|
|
3281
|
-
"taskId" in error &&
|
|
3282
|
-
error.code === "EUNTRUSTED_RESULT" &&
|
|
3283
|
-
typeof error.taskId === "string");
|
|
3284
|
-
}
|
|
3285
|
-
function assertTokenNotExpiredForCommand(config, sourceCli, setupHintCwd) {
|
|
3286
|
-
const tokenStatus = getTokenExpiryStatus(config);
|
|
3287
|
-
if (tokenStatus.status === "expired") {
|
|
3288
|
-
throw new Error(sourceAwareSetupMessage(tokenStatus.warning.toLowerCase(), sourceCli, { cwd: setupHintCwd }));
|
|
3289
|
-
}
|
|
3290
|
-
}
|
|
3291
|
-
async function loadLocalConfigForCommand(cwd, command, sourceCli, setupHintCwd) {
|
|
3292
|
-
return loadLocalConfig(cwd).catch(async (error) => {
|
|
3293
|
-
if (isMissingFileError(error)) {
|
|
3294
|
-
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 }));
|
|
3295
|
-
}
|
|
3296
|
-
throw new Error(sourceAwareSetupMessage(errorMessage(error), sourceCli, { cwd: setupHintCwd }));
|
|
3297
|
-
});
|
|
3298
|
-
}
|
|
3299
|
-
function redactServerUrl(value) {
|
|
3300
|
-
return formatServerUrlForOutput(value, { showToken: false });
|
|
3301
|
-
}
|
|
3302
|
-
function formatServerUrlForOutput(value, options) {
|
|
3303
|
-
try {
|
|
3304
|
-
const url = new URL(value);
|
|
3305
|
-
url.username = "";
|
|
3306
|
-
url.password = "";
|
|
3307
|
-
if (!options.showToken && url.searchParams.has("prodex_token"))
|
|
3308
|
-
url.searchParams.set("prodex_token", "***");
|
|
3309
|
-
return url.toString();
|
|
3310
|
-
}
|
|
3311
|
-
catch {
|
|
3312
|
-
const withoutUserinfo = value.replace(/\/\/[^/@\s]+@/g, "//");
|
|
3313
|
-
return options.showToken ? withoutUserinfo : withoutUserinfo.replace(/([?&]prodex_token=)[^&]+/g, "$1***");
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
function makeTunnelMcpUrl(publicUrl, token) {
|
|
3317
|
-
const url = parseTunnelPublicUrl(publicUrl);
|
|
3318
|
-
url.username = "";
|
|
3319
|
-
url.password = "";
|
|
3320
|
-
url.pathname = "/mcp";
|
|
3321
|
-
url.search = "";
|
|
3322
|
-
url.hash = "";
|
|
3323
|
-
url.searchParams.set("prodex_token", token);
|
|
3324
|
-
return url.toString();
|
|
3325
|
-
}
|
|
3326
|
-
function parseTunnelPublicUrl(publicUrl) {
|
|
3327
|
-
let url;
|
|
3328
|
-
try {
|
|
3329
|
-
url = new URL(publicUrl);
|
|
3330
|
-
}
|
|
3331
|
-
catch {
|
|
3332
|
-
throw new Error("--public-url must be a valid URL");
|
|
3333
|
-
}
|
|
3334
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
3335
|
-
throw new Error("--public-url must use http or https");
|
|
3336
|
-
}
|
|
3337
|
-
if (url.protocol !== "https:" && !isLoopbackHost(url.hostname)) {
|
|
3338
|
-
throw new Error("--public-url must use https for non-loopback tunnel URLs");
|
|
3339
|
-
}
|
|
3340
|
-
return url;
|
|
3341
|
-
}
|
|
3342
|
-
function isLoopbackHost(hostname) {
|
|
3343
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
3344
|
-
}
|
|
3345
|
-
function readFlag(args, flag) {
|
|
3346
|
-
const index = args.indexOf(flag);
|
|
3347
|
-
if (index === -1)
|
|
3348
|
-
return undefined;
|
|
3349
|
-
return readFlagValue(args, index, flag);
|
|
3350
|
-
}
|
|
3351
|
-
function readNumberFlag(args, flag) {
|
|
3352
|
-
const raw = readFlag(args, flag);
|
|
3353
|
-
if (raw === undefined)
|
|
3354
|
-
return undefined;
|
|
3355
|
-
const value = Number(raw);
|
|
3356
|
-
if (!Number.isFinite(value))
|
|
3357
|
-
throw new Error(`${flag} requires a finite number`);
|
|
3358
|
-
return value;
|
|
3359
|
-
}
|
|
3360
|
-
function readPositiveNumberFlag(args, flag) {
|
|
3361
|
-
const value = readNumberFlag(args, flag);
|
|
3362
|
-
if (value === undefined)
|
|
3363
|
-
return undefined;
|
|
3364
|
-
if (value <= 0)
|
|
3365
|
-
throw new Error(`${flag} must be greater than 0`);
|
|
3366
|
-
return value;
|
|
3367
|
-
}
|
|
3368
|
-
function readPortFlag(args, flag) {
|
|
3369
|
-
const value = readNumberFlag(args, flag);
|
|
3370
|
-
if (value === undefined)
|
|
3371
|
-
return undefined;
|
|
3372
|
-
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
3373
|
-
throw new Error(`${flag} must be an integer from 1 to 65535`);
|
|
3374
|
-
}
|
|
3375
|
-
return value;
|
|
3376
|
-
}
|
|
3377
|
-
function readChatGptBrowserUrlFlag(args) {
|
|
3378
|
-
return normalizeChatGptTargetUrl(readFlag(args, "--url") ?? "https://chatgpt.com/");
|
|
3379
|
-
}
|
|
3380
|
-
function readRepeatedFlag(args, flag) {
|
|
3381
|
-
const values = [];
|
|
3382
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
3383
|
-
if (args[index] === flag) {
|
|
3384
|
-
values.push(readFlagValue(args, index, flag));
|
|
3385
|
-
index += 1;
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
return values;
|
|
3389
|
-
}
|
|
3390
|
-
function resolveCwdFlag(defaultCwd, args) {
|
|
3391
|
-
const cwd = readFlag(args, "--cwd");
|
|
3392
|
-
if (!cwd)
|
|
3393
|
-
return defaultCwd;
|
|
3394
|
-
return resolveExistingDirectoryFlag(defaultCwd, cwd, "--cwd");
|
|
3395
|
-
}
|
|
3396
1255
|
function resolveOptionalPathFlag(defaultCwd, args, flag) {
|
|
3397
1256
|
const value = readFlag(args, flag);
|
|
3398
1257
|
return value ? resolveExistingPathFlag(defaultCwd, value, flag) : undefined;
|
|
3399
1258
|
}
|
|
3400
|
-
function resolveOptionalFileFlag(defaultCwd, args, flag) {
|
|
3401
|
-
const value = readFlag(args, flag);
|
|
3402
|
-
return value ? resolveExistingFileFlag(defaultCwd, value, flag) : undefined;
|
|
3403
|
-
}
|
|
3404
|
-
function resolveExistingPathFlag(defaultCwd, value, flag) {
|
|
3405
|
-
const resolved = path.resolve(defaultCwd, value);
|
|
3406
|
-
try {
|
|
3407
|
-
return realpathSync(resolved);
|
|
3408
|
-
}
|
|
3409
|
-
catch {
|
|
3410
|
-
throw new Error(`${flag} does not exist or is not accessible: ${resolved}`);
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
function resolveExistingFileFlag(defaultCwd, value, flag) {
|
|
3414
|
-
const resolved = resolveExistingPathFlag(defaultCwd, value, flag);
|
|
3415
|
-
if (!statSync(resolved).isFile()) {
|
|
3416
|
-
throw new Error(`${flag} must be a file: ${resolved}`);
|
|
3417
|
-
}
|
|
3418
|
-
return resolved;
|
|
3419
|
-
}
|
|
3420
|
-
function resolveExistingDirectoryFlag(defaultCwd, value, flag) {
|
|
3421
|
-
const resolved = resolveExistingPathFlag(defaultCwd, value, flag);
|
|
3422
|
-
if (!statSync(resolved).isDirectory()) {
|
|
3423
|
-
throw new Error(`${flag} must be a directory: ${resolved}`);
|
|
3424
|
-
}
|
|
3425
|
-
return resolved;
|
|
3426
|
-
}
|
|
3427
|
-
function assertOnlyOptions(args, command, valueFlags, booleanFlags = []) {
|
|
3428
|
-
const valueFlagSet = new Set(valueFlags);
|
|
3429
|
-
const booleanFlagSet = new Set(booleanFlags);
|
|
3430
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
3431
|
-
const arg = args[index];
|
|
3432
|
-
if (valueFlagSet.has(arg)) {
|
|
3433
|
-
readFlagValue(args, index, arg);
|
|
3434
|
-
index += 1;
|
|
3435
|
-
continue;
|
|
3436
|
-
}
|
|
3437
|
-
if (booleanFlagSet.has(arg))
|
|
3438
|
-
continue;
|
|
3439
|
-
if (arg.startsWith("-")) {
|
|
3440
|
-
throw unknownOptionError(arg, command, [...valueFlagSet, ...booleanFlagSet]);
|
|
3441
|
-
}
|
|
3442
|
-
throw new Error(`Unexpected argument for ${command}: ${arg}`);
|
|
3443
|
-
}
|
|
3444
|
-
}
|
|
3445
|
-
function readPositionalsWithOptions(args, command, maxPositionals, valueFlags, booleanFlags = []) {
|
|
3446
|
-
const valueFlagSet = new Set(valueFlags);
|
|
3447
|
-
const booleanFlagSet = new Set(booleanFlags);
|
|
3448
|
-
const positionals = [];
|
|
3449
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
3450
|
-
const arg = args[index];
|
|
3451
|
-
if (valueFlagSet.has(arg)) {
|
|
3452
|
-
readFlagValue(args, index, arg);
|
|
3453
|
-
index += 1;
|
|
3454
|
-
continue;
|
|
3455
|
-
}
|
|
3456
|
-
if (booleanFlagSet.has(arg))
|
|
3457
|
-
continue;
|
|
3458
|
-
if (arg.startsWith("-")) {
|
|
3459
|
-
throw unknownOptionError(arg, command, [...valueFlagSet, ...booleanFlagSet]);
|
|
3460
|
-
}
|
|
3461
|
-
if (positionals.length >= maxPositionals) {
|
|
3462
|
-
throw new Error(`Unexpected argument for ${command}: ${arg}`);
|
|
3463
|
-
}
|
|
3464
|
-
positionals.push(arg);
|
|
3465
|
-
}
|
|
3466
|
-
return positionals;
|
|
3467
|
-
}
|
|
3468
|
-
function assertNoExtraArgs(args, command, maxPositionals) {
|
|
3469
|
-
for (const arg of args.slice(maxPositionals)) {
|
|
3470
|
-
if (arg.startsWith("-")) {
|
|
3471
|
-
throw new Error(`Unknown option for ${command}: ${arg}`);
|
|
3472
|
-
}
|
|
3473
|
-
throw new Error(`Unexpected argument for ${command}: ${arg}`);
|
|
3474
|
-
}
|
|
3475
|
-
}
|
|
3476
1259
|
function readRequiredLeadingArgument(args, command, placeholder) {
|
|
3477
1260
|
const value = args[0];
|
|
3478
1261
|
if (!value || value.startsWith("-"))
|
|
3479
1262
|
throw new Error(`${command} requires ${placeholder}`);
|
|
3480
1263
|
return value;
|
|
3481
1264
|
}
|
|
3482
|
-
const ASK_PRO_BOOLEAN_FLAGS = new Set(["--dry-run", "--send", "--confirm-target"]);
|
|
3483
|
-
const ASK_PRO_SELECTION_VALUE_FLAGS = ["--project", "--project-new", "--model", "--pro-mode", "--effort"];
|
|
3484
|
-
const ASK_PRO_VALUE_FLAGS = new Set([
|
|
3485
|
-
"--cwd",
|
|
3486
|
-
"--file",
|
|
3487
|
-
"--port",
|
|
3488
|
-
"--timeout-ms",
|
|
3489
|
-
"--target-url",
|
|
3490
|
-
"--source-cli",
|
|
3491
|
-
...ASK_PRO_SELECTION_VALUE_FLAGS
|
|
3492
|
-
]);
|
|
3493
|
-
const ASK_PRO_PREVIEW_VALUE_FLAGS = new Set([
|
|
3494
|
-
"--cwd",
|
|
3495
|
-
"--file",
|
|
3496
|
-
"--port",
|
|
3497
|
-
"--timeout-ms",
|
|
3498
|
-
"--target-url",
|
|
3499
|
-
...ASK_PRO_SELECTION_VALUE_FLAGS
|
|
3500
|
-
]);
|
|
3501
|
-
// Setup persists defaults for a subset of the per-ask selection flags. A new
|
|
3502
|
-
// project is created per-ask, never as a standing default, so --project-new is
|
|
3503
|
-
// intentionally excluded here.
|
|
3504
|
-
const ASK_PRO_SELECTION_DEFAULT_FLAGS = ["--model", "--pro-mode", "--effort", "--project"];
|
|
3505
|
-
const ASK_PRO_SELECTION_CLEAR_FLAGS = ["--clear-model", "--clear-pro-mode", "--clear-effort", "--clear-project"];
|
|
3506
|
-
function parseBrowserDefaultFlags(args) {
|
|
3507
|
-
const model = readFlag(args, "--model");
|
|
3508
|
-
const proModeRaw = readFlag(args, "--pro-mode");
|
|
3509
|
-
const effortRaw = readFlag(args, "--effort");
|
|
3510
|
-
const project = readFlag(args, "--project");
|
|
3511
|
-
if (proModeRaw !== undefined && effortRaw !== undefined) {
|
|
3512
|
-
throw new Error("setup cannot combine --pro-mode and --effort; Pro sub-modes and reasoning effort are different model axes.");
|
|
3513
|
-
}
|
|
3514
|
-
const clears = [
|
|
3515
|
-
["--clear-model", "--model", model],
|
|
3516
|
-
["--clear-pro-mode", "--pro-mode", proModeRaw],
|
|
3517
|
-
["--clear-effort", "--effort", effortRaw],
|
|
3518
|
-
["--clear-project", "--project", project]
|
|
3519
|
-
].map(([clearFlag, setFlag, setValue]) => {
|
|
3520
|
-
const wantsClear = args.includes(clearFlag);
|
|
3521
|
-
if (wantsClear && setValue !== undefined) {
|
|
3522
|
-
throw new Error(`setup cannot combine ${setFlag} and ${clearFlag}; set a new default or clear it, not both.`);
|
|
3523
|
-
}
|
|
3524
|
-
return wantsClear;
|
|
3525
|
-
});
|
|
3526
|
-
const [clearModel, clearProMode, clearEffort, clearProject] = clears;
|
|
3527
|
-
const anySet = model !== undefined || proModeRaw !== undefined || effortRaw !== undefined || project !== undefined;
|
|
3528
|
-
if (!anySet && !clears.some(Boolean))
|
|
3529
|
-
return undefined;
|
|
3530
|
-
// Cleared fields are passed as explicit undefined so the config merge deletes
|
|
3531
|
-
// them while untouched fields survive.
|
|
3532
|
-
return {
|
|
3533
|
-
...(model !== undefined ? { model } : {}),
|
|
3534
|
-
...(clearModel ? { model: undefined } : {}),
|
|
3535
|
-
...(proModeRaw !== undefined ? { proMode: parseProMode(proModeRaw) } : {}),
|
|
3536
|
-
...(clearProMode ? { proMode: undefined } : {}),
|
|
3537
|
-
...(effortRaw !== undefined ? { effort: parseReasoningEffort(effortRaw) } : {}),
|
|
3538
|
-
...(clearEffort ? { effort: undefined } : {}),
|
|
3539
|
-
...(project !== undefined ? { project } : {}),
|
|
3540
|
-
...(clearProject ? { project: undefined } : {})
|
|
3541
|
-
};
|
|
3542
|
-
}
|
|
3543
|
-
function resolvePromptUser(io) {
|
|
3544
|
-
if (io.promptUser)
|
|
3545
|
-
return io.promptUser;
|
|
3546
|
-
if (!process.stdin.isTTY) {
|
|
3547
|
-
throw new Error("setup --interactive needs a terminal (TTY). Use the --model/--pro-mode/--effort/--project flags instead.");
|
|
3548
|
-
}
|
|
3549
|
-
return async (question) => {
|
|
3550
|
-
const readline = await import("node:readline/promises");
|
|
3551
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3552
|
-
try {
|
|
3553
|
-
return (await rl.question(question)).trim();
|
|
3554
|
-
}
|
|
3555
|
-
finally {
|
|
3556
|
-
rl.close();
|
|
3557
|
-
}
|
|
3558
|
-
};
|
|
3559
|
-
}
|
|
3560
|
-
// Ask up to `attempts` times; empty input means skip (returns undefined).
|
|
3561
|
-
async function askChoice(prompt, question, choices, attempts = 3) {
|
|
3562
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
3563
|
-
const raw = (await prompt(question)).trim();
|
|
3564
|
-
if (raw === "")
|
|
3565
|
-
return undefined;
|
|
3566
|
-
const index = Number.parseInt(raw, 10);
|
|
3567
|
-
if (Number.isInteger(index) && index >= 1 && index <= choices.length)
|
|
3568
|
-
return choices[index - 1];
|
|
3569
|
-
}
|
|
3570
|
-
throw new Error(`No valid choice after ${attempts} attempts; run setup again or use flags.`);
|
|
3571
|
-
}
|
|
3572
|
-
async function runBrowserDefaultsWizard(prompt, stdout) {
|
|
3573
|
-
stdout("Browser send defaults (press Enter to skip a question; labels match the Korean ChatGPT UI):");
|
|
3574
|
-
const model = await askChoice(prompt, "Default model — 1) Pro [Enter=skip]: ", ["Pro"]);
|
|
3575
|
-
let proMode;
|
|
3576
|
-
let effort;
|
|
3577
|
-
if (model === "Pro") {
|
|
3578
|
-
proMode = (await askChoice(prompt, "Pro sub-mode — 1) 기본 2) 확장 [Enter=skip]: ", ["기본", "확장"]));
|
|
3579
|
-
}
|
|
3580
|
-
else {
|
|
3581
|
-
effort = (await askChoice(prompt, "Reasoning effort — 1) 즉시 2) 중간 3) 높음 4) 매우 높음 [Enter=skip]: ", [
|
|
3582
|
-
"즉시",
|
|
3583
|
-
"중간",
|
|
3584
|
-
"높음",
|
|
3585
|
-
"매우 높음"
|
|
3586
|
-
]));
|
|
3587
|
-
}
|
|
3588
|
-
const projectRaw = (await prompt('Default project name (existing sidebar project) [Enter=skip]: ')).trim();
|
|
3589
|
-
const project = projectRaw === "" ? undefined : projectRaw;
|
|
3590
|
-
if (model === undefined && proMode === undefined && effort === undefined && project === undefined)
|
|
3591
|
-
return undefined;
|
|
3592
|
-
return {
|
|
3593
|
-
...(model !== undefined ? { model } : {}),
|
|
3594
|
-
...(proMode !== undefined ? { proMode } : {}),
|
|
3595
|
-
...(effort !== undefined ? { effort } : {}),
|
|
3596
|
-
...(project !== undefined ? { project } : {})
|
|
3597
|
-
};
|
|
3598
|
-
}
|
|
3599
|
-
function formatBrowserDefaults(defaults) {
|
|
3600
|
-
const parts = [];
|
|
3601
|
-
if (defaults.model)
|
|
3602
|
-
parts.push(`model=${defaults.model}`);
|
|
3603
|
-
if (defaults.pro_mode)
|
|
3604
|
-
parts.push(`pro-mode=${defaults.pro_mode}`);
|
|
3605
|
-
if (defaults.effort)
|
|
3606
|
-
parts.push(`effort=${defaults.effort}`);
|
|
3607
|
-
if (defaults.project)
|
|
3608
|
-
parts.push(`project=${defaults.project}`);
|
|
3609
|
-
return parts.length > 0 ? parts.join(", ") : "(none)";
|
|
3610
|
-
}
|
|
3611
|
-
function parseAskProArgs(args, valueFlags = ASK_PRO_VALUE_FLAGS) {
|
|
3612
|
-
const delimiterIndex = args.indexOf("--");
|
|
3613
|
-
const optionArgs = delimiterIndex === -1 ? args : args.slice(0, delimiterIndex);
|
|
3614
|
-
const promptTail = delimiterIndex === -1 ? [] : args.slice(delimiterIndex + 1);
|
|
3615
|
-
const positionalPromptParts = [];
|
|
3616
|
-
for (let index = 0; index < optionArgs.length; index += 1) {
|
|
3617
|
-
const arg = optionArgs[index];
|
|
3618
|
-
if (!arg.startsWith("--")) {
|
|
3619
|
-
if (arg.startsWith("-"))
|
|
3620
|
-
throw unknownOptionError(arg, undefined, [...valueFlags, ...ASK_PRO_BOOLEAN_FLAGS]);
|
|
3621
|
-
positionalPromptParts.push(arg);
|
|
3622
|
-
continue;
|
|
3623
|
-
}
|
|
3624
|
-
if (ASK_PRO_BOOLEAN_FLAGS.has(arg))
|
|
3625
|
-
continue;
|
|
3626
|
-
if (valueFlags.has(arg)) {
|
|
3627
|
-
readFlagValue(optionArgs, index, arg);
|
|
3628
|
-
index += 1;
|
|
3629
|
-
continue;
|
|
3630
|
-
}
|
|
3631
|
-
throw unknownOptionError(arg, undefined, [...valueFlags, ...ASK_PRO_BOOLEAN_FLAGS]);
|
|
3632
|
-
}
|
|
3633
|
-
return { optionArgs, promptParts: [...positionalPromptParts, ...promptTail] };
|
|
3634
|
-
}
|
|
3635
|
-
function askProOptionArgs(args) {
|
|
3636
|
-
const delimiterIndex = args.indexOf("--");
|
|
3637
|
-
return delimiterIndex === -1 ? args : args.slice(0, delimiterIndex);
|
|
3638
|
-
}
|
|
3639
|
-
function hasAskProMode(args) {
|
|
3640
|
-
const optionArgs = askProOptionArgs(args);
|
|
3641
|
-
return optionArgs.includes("--send") || optionArgs.includes("--dry-run");
|
|
3642
|
-
}
|
|
3643
|
-
function hasAskProSendMode(args) {
|
|
3644
|
-
return askProOptionArgs(args).includes("--send");
|
|
3645
|
-
}
|
|
3646
|
-
function hasAskProDryRunMode(args) {
|
|
3647
|
-
return askProOptionArgs(args).includes("--dry-run");
|
|
3648
|
-
}
|
|
3649
|
-
function readFlagValue(args, index, flag) {
|
|
3650
|
-
const value = args[index + 1];
|
|
3651
|
-
if (!value || value.startsWith("--"))
|
|
3652
|
-
throw new Error(`${flag} requires a value`);
|
|
3653
|
-
return value;
|
|
3654
|
-
}
|
|
3655
|
-
function readSessionStatusFlag(args) {
|
|
3656
|
-
const value = readFlag(args, "--status");
|
|
3657
|
-
if (value === undefined)
|
|
3658
|
-
return undefined;
|
|
3659
|
-
if (value === "preview" || value === "running" || value === "done" || value === "blocked")
|
|
3660
|
-
return value;
|
|
3661
|
-
throw new Error("--status must be one of preview, running, done, blocked");
|
|
3662
|
-
}
|
|
3663
|
-
const TASK_STATUSES = TaskStatusSchema.options;
|
|
3664
|
-
function readTaskStatusFlag(args) {
|
|
3665
|
-
const value = readFlag(args, "--status");
|
|
3666
|
-
if (value === undefined)
|
|
3667
|
-
return undefined;
|
|
3668
|
-
if (TaskStatusSchema.safeParse(value).success)
|
|
3669
|
-
return value;
|
|
3670
|
-
throw new Error(`--status must be one of ${TASK_STATUSES.join(", ")}`);
|
|
3671
|
-
}
|
|
3672
|
-
const RECEIPT_KINDS = ReceiptKindSchema.options;
|
|
3673
|
-
function readReceiptKindFlag(args) {
|
|
3674
|
-
const value = readFlag(args, "--kind");
|
|
3675
|
-
if (value === undefined)
|
|
3676
|
-
return undefined;
|
|
3677
|
-
if (ReceiptKindSchema.safeParse(value).success)
|
|
3678
|
-
return value;
|
|
3679
|
-
throw new Error(`--kind must be one of ${RECEIPT_KINDS.join(", ")}`);
|
|
3680
|
-
}
|
|
3681
|
-
function formatTokenExpiryLine(config) {
|
|
3682
|
-
const tokenStatus = getTokenExpiryStatus(config);
|
|
3683
|
-
if (tokenStatus.status === "valid")
|
|
3684
|
-
return `Token expires: ${tokenStatus.token_expires_at}`;
|
|
3685
|
-
if (tokenStatus.status === "expired")
|
|
3686
|
-
return `Token expired: ${tokenStatus.token_expires_at}`;
|
|
3687
|
-
return "Token expires: never (local-only; use --token-ttl-hours before exposing through a tunnel).";
|
|
3688
|
-
}
|
|
3689
|
-
function formatConfigWarningLine(tokenStatus, sourceCli, setupHintCwd) {
|
|
3690
|
-
return tokenStatus.warning ? `config_warning: ${sourceAwareSetupMessage(tokenStatus.warning, sourceCli, { cwd: setupHintCwd })}` : undefined;
|
|
3691
|
-
}
|
|
3692
|
-
async function ensureBridgeGitignore(cwd) {
|
|
3693
|
-
const bridgeIgnorePath = path.join(cwd, ".bridge", ".gitignore");
|
|
3694
|
-
await mkdir(path.dirname(bridgeIgnorePath), { recursive: true });
|
|
3695
|
-
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 });
|
|
3696
|
-
const rootIgnorePath = path.join(cwd, ".gitignore");
|
|
3697
|
-
let current = "";
|
|
3698
|
-
try {
|
|
3699
|
-
current = await readVerifiedUtf8File(rootIgnorePath, () => assertGitignoreTargetSafe(rootIgnorePath));
|
|
3700
|
-
}
|
|
3701
|
-
catch (error) {
|
|
3702
|
-
if (!isMissingFileError(error))
|
|
3703
|
-
throw error;
|
|
3704
|
-
}
|
|
3705
|
-
const ignored = new Set(current.split(/\r?\n/).filter(Boolean));
|
|
3706
|
-
const additions = ["node_modules/", "dist/"].filter((line) => !ignored.has(line));
|
|
3707
|
-
if (additions.length > 0) {
|
|
3708
|
-
await writeVerifiedUtf8File(rootIgnorePath, `${current}${current && !current.endsWith("\n") ? "\n" : ""}${additions.join("\n")}\n`, () => assertGitignoreTargetSafe(rootIgnorePath), { create: true });
|
|
3709
|
-
}
|
|
3710
|
-
}
|
|
3711
|
-
async function assertGitignoreTargetSafe(filePath) {
|
|
3712
|
-
try {
|
|
3713
|
-
const stat = await lstat(filePath);
|
|
3714
|
-
if (stat.isSymbolicLink())
|
|
3715
|
-
throw new Error(`${filePath} must not be a symlink`);
|
|
3716
|
-
if (!stat.isFile())
|
|
3717
|
-
throw new Error(`${filePath} must be a regular file`);
|
|
3718
|
-
}
|
|
3719
|
-
catch (error) {
|
|
3720
|
-
if (isMissingFileError(error))
|
|
3721
|
-
return;
|
|
3722
|
-
throw error;
|
|
3723
|
-
}
|
|
3724
|
-
}
|
|
3725
|
-
async function waitForShutdown(close) {
|
|
3726
|
-
await new Promise((resolve) => {
|
|
3727
|
-
const shutdown = () => resolve();
|
|
3728
|
-
process.once("SIGINT", shutdown);
|
|
3729
|
-
process.once("SIGTERM", shutdown);
|
|
3730
|
-
});
|
|
3731
|
-
await close();
|
|
3732
|
-
}
|
|
3733
1265
|
function isDirectCliInvocation() {
|
|
3734
1266
|
if (!process.argv[1])
|
|
3735
1267
|
return false;
|