@youdie006/prodex 0.16.12 → 0.16.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-send-lock.js +79 -0
- package/dist/chatgpt-browser.js +38 -2
- package/dist/cli-args.js +1 -0
- package/dist/cli-help.js +3 -3
- package/dist/cli-pro.js +8 -4
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// One visible-browser send at a time per machine: the dedicated Chrome is a
|
|
5
|
+
// single shared tab, and two concurrent prodex clients interleave composer
|
|
6
|
+
// input and navigation, silently cross-contaminating each other's threads
|
|
7
|
+
// (measured live: one client's token prompt landed inside the other client's
|
|
8
|
+
// consult thread, and a 15-minute consult never actually posted).
|
|
9
|
+
function lockPath() {
|
|
10
|
+
const override = process.env.PRODEX_SEND_LOCK_FILE;
|
|
11
|
+
if (override)
|
|
12
|
+
return override;
|
|
13
|
+
return path.join(os.homedir(), ".local", "share", "prodex", "browser-send.lock");
|
|
14
|
+
}
|
|
15
|
+
function holderIsAlive(pid) {
|
|
16
|
+
try {
|
|
17
|
+
process.kill(pid, 0);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function readHolderPid(file) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
27
|
+
return typeof parsed.pid === "number" ? parsed.pid : undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function tryAcquire(file) {
|
|
34
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
35
|
+
try {
|
|
36
|
+
const handle = await open(file, "wx", 0o600);
|
|
37
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() })}\n`);
|
|
38
|
+
await handle.close();
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code !== "EEXIST")
|
|
43
|
+
throw error;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Serialize visible-browser sends across processes. Waits up to waitMs for a
|
|
49
|
+
* live holder to finish (0 = fail fast); a lock whose holder process is dead
|
|
50
|
+
* is reaped immediately.
|
|
51
|
+
*/
|
|
52
|
+
export async function withBrowserSendLock(waitMs, onWait, fn) {
|
|
53
|
+
const file = lockPath();
|
|
54
|
+
const deadline = Date.now() + Math.max(0, waitMs);
|
|
55
|
+
let waited = false;
|
|
56
|
+
for (;;) {
|
|
57
|
+
if (await tryAcquire(file))
|
|
58
|
+
break;
|
|
59
|
+
const holder = await readHolderPid(file);
|
|
60
|
+
if (holder === undefined || !holderIsAlive(holder)) {
|
|
61
|
+
await rm(file, { force: true }).catch(() => undefined);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (Date.now() >= deadline) {
|
|
65
|
+
throw new Error(`Another prodex browser send is in progress (pid ${holder}). Wait for it to finish, or pass --busy-wait-ms to queue behind it.`);
|
|
66
|
+
}
|
|
67
|
+
if (!waited) {
|
|
68
|
+
waited = true;
|
|
69
|
+
onWait(`another prodex send holds the browser (pid ${holder}); waiting`);
|
|
70
|
+
}
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return await fn();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await rm(file, { force: true }).catch(() => undefined);
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -933,7 +933,26 @@ async function selectModelReasoning(cdp, options) {
|
|
|
933
933
|
throw new Error(button.reason ?? "Could not open the ChatGPT model selector");
|
|
934
934
|
}
|
|
935
935
|
try {
|
|
936
|
-
|
|
936
|
+
// The hover-verified click can be transiently refused right after a page
|
|
937
|
+
// transition (measured live: the just-closed create-project modal's
|
|
938
|
+
// overlay still covered the selector for a beat, and one refusal aborted
|
|
939
|
+
// the whole send). Retry briefly with FRESH coordinates; a persistent
|
|
940
|
+
// cover still fails with the refusal message.
|
|
941
|
+
const clickDeadline = Date.now() + 5_000;
|
|
942
|
+
for (;;) {
|
|
943
|
+
try {
|
|
944
|
+
await verifiedClickAt(cdp, button.x, button.y, "model selector");
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
catch (error) {
|
|
948
|
+
if (Date.now() >= clickDeadline || !/Refusing to click/.test(error instanceof Error ? error.message : String(error)))
|
|
949
|
+
throw error;
|
|
950
|
+
await sleep(700);
|
|
951
|
+
const fresh = await cdp.evaluate(modelButtonRectExpression());
|
|
952
|
+
if (fresh.ok && fresh.x !== undefined && fresh.y !== undefined)
|
|
953
|
+
button = fresh;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
937
956
|
const opened = await waitForExpressionTrue(cdp, menuOpenExpression(), MENU_OPEN_TIMEOUT_MS);
|
|
938
957
|
if (!opened)
|
|
939
958
|
throw new Error("ChatGPT model menu did not open after clicking the selector");
|
|
@@ -1205,7 +1224,24 @@ export async function sendChatGptPrompt(options) {
|
|
|
1205
1224
|
if (normalizedTargetUrl)
|
|
1206
1225
|
assertChatGptTargetUrlMatches(status.url, normalizedTargetUrl);
|
|
1207
1226
|
assertVisibleChatGptTab(status.visibilityState, status.url, normalizedTargetUrl);
|
|
1208
|
-
|
|
1227
|
+
let busyBlocker = chatGptBusyBlocker(status.generating);
|
|
1228
|
+
if (busyBlocker && (options.busyWaitMs ?? 0) > 0) {
|
|
1229
|
+
// Queue behind the in-flight response instead of failing: shared-tab
|
|
1230
|
+
// contention (another agent or the user mid-generation) is a when, not an
|
|
1231
|
+
// if. Bounded, and a mid-wait page blocker (usage limit etc.) still throws.
|
|
1232
|
+
const busyDeadline = Date.now() + (options.busyWaitMs ?? 0);
|
|
1233
|
+
emitProgress("waiting", "tab busy with another response; waiting");
|
|
1234
|
+
while (busyBlocker && Date.now() < busyDeadline) {
|
|
1235
|
+
await sleep(3_000);
|
|
1236
|
+
status = await evaluateOnPage(page, statusExpression());
|
|
1237
|
+
const midBlocker = detectChatGptPageBlocker(status);
|
|
1238
|
+
if (midBlocker)
|
|
1239
|
+
throw new ChatGptBrowserBlockerError(midBlocker);
|
|
1240
|
+
busyBlocker = chatGptBusyBlocker(status.generating);
|
|
1241
|
+
if (busyBlocker)
|
|
1242
|
+
emitProgress("waiting", "tab busy with another response; waiting");
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1209
1245
|
if (busyBlocker) {
|
|
1210
1246
|
throw new ChatGptBrowserBlockerError(busyBlocker);
|
|
1211
1247
|
}
|
package/dist/cli-args.js
CHANGED
package/dist/cli-help.js
CHANGED
|
@@ -25,7 +25,7 @@ Ask / consult commands:
|
|
|
25
25
|
prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]
|
|
26
26
|
prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000] # read-only list of model menu options
|
|
27
27
|
prodex pro browser projects [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000] # read-only list of sidebar project names (for --project)
|
|
28
|
-
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] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
28
|
+
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
29
29
|
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
30
30
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
31
31
|
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
@@ -251,8 +251,8 @@ export function printProBrowserHelp(stdout, sourceCli) {
|
|
|
251
251
|
: "prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]";
|
|
252
252
|
const selectionUsage = '[--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"]';
|
|
253
253
|
const askUsage = sourceCli
|
|
254
|
-
? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] ${selectionUsage} "prompt"`
|
|
255
|
-
: `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] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] ${selectionUsage} "prompt"`;
|
|
254
|
+
? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] ${selectionUsage} "prompt"`
|
|
255
|
+
: `prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] ${selectionUsage} "prompt"`;
|
|
256
256
|
const modelsUsage = sourceCli
|
|
257
257
|
? `${cli} pro browser models${sourceCliOption} [--port 9333] [--timeout-ms 15000]`
|
|
258
258
|
: "prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000]";
|
package/dist/cli-pro.js
CHANGED
|
@@ -8,6 +8,7 @@ import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledge
|
|
|
8
8
|
import { formatBrowserDefaults, redactServerUrl } from "./cli-server.js";
|
|
9
9
|
import { errorMessage, firstLine, formatBlockedConsultRecordedMessage, formatProLatestCommand, formatBrowserCheckCommand, formatBrowserLoginCommand, formatBrowserSmokeCommand, formatBrowserTargetAskCommand, formatInitCommand, formatSetupCommand, isMissingFileError, computeSendPacingWaitMs, isUntrustedResultError, resolveMinSendIntervalMs, sourceAwareBrowserBlocker, sourceAwareBrowserNextStep, sourceAwareResultError, sourceAwareResultMessage, sourceAwareSetupMessage } from "./cli-shared.js";
|
|
10
10
|
import { getTokenExpiryStatus, loadBrowserDefaults, loadLocalConfig } from "./config.js";
|
|
11
|
+
import { withBrowserSendLock } from "./browser-send-lock.js";
|
|
11
12
|
import { BridgeStore, MAX_FETCHABLE_RESULT_ARTIFACT_BYTES } from "./store.js";
|
|
12
13
|
export async function runChatgptCommand(rest, io) {
|
|
13
14
|
const [subcommand, ...chatgptArgs] = rest;
|
|
@@ -80,12 +81,12 @@ export async function runChatgptCommand(rest, io) {
|
|
|
80
81
|
};
|
|
81
82
|
let result;
|
|
82
83
|
try {
|
|
83
|
-
result = await sendChatGptPrompt({
|
|
84
|
+
result = await withBrowserSendLock(0, (detail) => io.stderr(`progress: ${detail}`), () => sendChatGptPrompt({
|
|
84
85
|
port,
|
|
85
86
|
prompt: smokePrompt,
|
|
86
87
|
timeoutMs,
|
|
87
88
|
onProgress: createBrowserSendProgressPrinter(io.stderr)
|
|
88
|
-
});
|
|
89
|
+
}));
|
|
89
90
|
}
|
|
90
91
|
catch (error) {
|
|
91
92
|
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), sourceCli, commandOptions);
|
|
@@ -145,6 +146,7 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
145
146
|
const straySendFlag = [
|
|
146
147
|
"--port",
|
|
147
148
|
"--timeout-ms",
|
|
149
|
+
"--busy-wait-ms",
|
|
148
150
|
"--target-url",
|
|
149
151
|
"--confirm-target",
|
|
150
152
|
"--project",
|
|
@@ -594,6 +596,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
594
596
|
...(selectionEffort ? { effort: selectionEffort } : {})
|
|
595
597
|
};
|
|
596
598
|
const browserPort = hasSendMode ? resolveCdpPort(readPortFlag(parsedAskPro.optionArgs, "--port")) : undefined;
|
|
599
|
+
const busyWaitMs = readPositiveIntegerFlag(parsedAskPro.optionArgs, "--busy-wait-ms");
|
|
597
600
|
// Pro extended can legitimately think for minutes, so its default timeout is
|
|
598
601
|
// higher; an explicit --timeout-ms always wins.
|
|
599
602
|
const defaultBrowserTimeoutMs = selectionProMode === "확장" ? 300_000 : 90_000;
|
|
@@ -653,19 +656,20 @@ export async function runAskProCommand(rest, io) {
|
|
|
653
656
|
}
|
|
654
657
|
throw new Error(formatBlockedConsultRecordedMessage(blocker.message, task.id, sourceCli, { cwd: targetCwd }));
|
|
655
658
|
}
|
|
656
|
-
const sendOnce = () => sendChatGptPrompt({
|
|
659
|
+
const sendOnce = () => withBrowserSendLock(busyWaitMs ?? 0, (detail) => io.stderr(`progress: ${detail}`), () => sendChatGptPrompt({
|
|
657
660
|
port: browserPort,
|
|
658
661
|
prompt: bundle.text,
|
|
659
662
|
targetUrl: normalizedTargetUrl,
|
|
660
663
|
timeoutMs: browserTimeoutMs,
|
|
661
664
|
...(newChat ? { newChat: true } : {}),
|
|
665
|
+
...(busyWaitMs !== undefined ? { busyWaitMs } : {}),
|
|
662
666
|
project: selectionProject,
|
|
663
667
|
projectNew: selectionProjectNew,
|
|
664
668
|
model: selectionModel,
|
|
665
669
|
proMode: selectionProMode,
|
|
666
670
|
effort: selectionEffort,
|
|
667
671
|
onProgress: createBrowserSendProgressPrinter(io.stderr)
|
|
668
|
-
});
|
|
672
|
+
}));
|
|
669
673
|
// One-command recovery: interactive terminals (or explicit --auto-login)
|
|
670
674
|
// launch the dedicated browser and retry once when no browser runs.
|
|
671
675
|
// Scripts and agents keep the plain blocker unless they opt in.
|