@youdie006/prodex 0.27.3 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chatgpt-browser.js +115 -7
- package/dist/cli-help.js +4 -4
- package/dist/cli-pro.js +52 -4
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -1915,6 +1915,23 @@ async function readTranscriptAnswer(page, conversationId, sentPrompt) {
|
|
|
1915
1915
|
? { classification: "answer", answer: { answer, modelSlug: transcript.modelSlug } }
|
|
1916
1916
|
: { classification: "pending" };
|
|
1917
1917
|
}
|
|
1918
|
+
// A page that has not reported the prompt posting within this long is worth
|
|
1919
|
+
// double-checking against the transcript; the probe is a couple of small fetches.
|
|
1920
|
+
const ACCEPTANCE_TRANSCRIPT_PROBE_AFTER_MS = 20_000;
|
|
1921
|
+
const ACCEPTANCE_TRANSCRIPT_PROBE_EVERY_MS = 10_000;
|
|
1922
|
+
/** Which conversation, if any, already holds the prompt this send posted. */
|
|
1923
|
+
async function findLandedConversation(page, prompt) {
|
|
1924
|
+
try {
|
|
1925
|
+
const candidates = await evaluateOnPage(page, recentConversationsExpression(4), {
|
|
1926
|
+
timeoutMs: 30_000
|
|
1927
|
+
});
|
|
1928
|
+
return pickLandedConversation(candidates ?? [], prompt);
|
|
1929
|
+
}
|
|
1930
|
+
catch {
|
|
1931
|
+
// Transcript unavailable: the caller falls back to the page.
|
|
1932
|
+
return undefined;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1918
1935
|
export async function sendChatGptPrompt(options) {
|
|
1919
1936
|
const port = resolveCdpPort(options.port);
|
|
1920
1937
|
const timeoutMs = options.timeoutMs ?? 90_000;
|
|
@@ -2135,8 +2152,27 @@ export async function sendChatGptPrompt(options) {
|
|
|
2135
2152
|
const acceptDeadline = computePromptAcceptanceDeadline(timeoutMs, started);
|
|
2136
2153
|
let accepted = false;
|
|
2137
2154
|
let finalState;
|
|
2155
|
+
// Seeded either from the url once ChatGPT rewrites it, or - when the page
|
|
2156
|
+
// never showed the prompt post - from the transcript that proves it did.
|
|
2157
|
+
let transcriptConversationId;
|
|
2158
|
+
// Ask the transcript early rather than only at the deadline. Acceptance runs
|
|
2159
|
+
// on the full send budget, so a page that stops reporting the prompt posting
|
|
2160
|
+
// used to burn all twenty minutes before saying anything - while the prompt
|
|
2161
|
+
// sat in a conversation the whole time.
|
|
2162
|
+
let nextTranscriptProbeAt = started + ACCEPTANCE_TRANSCRIPT_PROBE_AFTER_MS;
|
|
2138
2163
|
while (Date.now() < acceptDeadline) {
|
|
2139
2164
|
await sleep(500);
|
|
2165
|
+
if (Date.now() >= nextTranscriptProbeAt) {
|
|
2166
|
+
nextTranscriptProbeAt = Date.now() + ACCEPTANCE_TRANSCRIPT_PROBE_EVERY_MS;
|
|
2167
|
+
const landed = await findLandedConversation(page, options.prompt);
|
|
2168
|
+
if (landed) {
|
|
2169
|
+
transcriptConversationId = landed;
|
|
2170
|
+
accepted = true;
|
|
2171
|
+
sendWarnings.push("prompt_acceptance_unreadable: the page never showed the prompt posting, but the transcript has it - continuing on the conversation the transcript names.");
|
|
2172
|
+
dbgSend(`acceptance recovered from transcript conversation=${landed}`);
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2140
2176
|
try {
|
|
2141
2177
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
2142
2178
|
}
|
|
@@ -2186,7 +2222,19 @@ export async function sendChatGptPrompt(options) {
|
|
|
2186
2222
|
catch {
|
|
2187
2223
|
// best effort: fall back to submit-button signal only
|
|
2188
2224
|
}
|
|
2189
|
-
|
|
2225
|
+
// Before calling this a failed send: did the prompt actually land? Reading
|
|
2226
|
+
// acceptance off the page means a changed DOM reports "never posted" for a
|
|
2227
|
+
// prompt that posted fine, and the caller's retry asks ChatGPT the same
|
|
2228
|
+
// question twice. The transcript is the ground truth.
|
|
2229
|
+
const landed = await findLandedConversation(page, options.prompt);
|
|
2230
|
+
if (landed) {
|
|
2231
|
+
transcriptConversationId = landed;
|
|
2232
|
+
accepted = true;
|
|
2233
|
+
sendWarnings.push("prompt_acceptance_unreadable: the page never showed the prompt posting, but the transcript has it - continuing on the conversation the transcript names.");
|
|
2234
|
+
dbgSend(`acceptance recovered from transcript conversation=${landed}`);
|
|
2235
|
+
}
|
|
2236
|
+
if (!accepted)
|
|
2237
|
+
throw acceptanceTimeoutError({ timeoutMs, composerStillHasText, submitButtonFound });
|
|
2190
2238
|
}
|
|
2191
2239
|
// Pin the conversation the prompt actually landed in. The browser is shared
|
|
2192
2240
|
// (other agents, the user, tooling), and a tab that moves mid-wait made
|
|
@@ -2199,7 +2247,9 @@ export async function sendChatGptPrompt(options) {
|
|
|
2199
2247
|
// a finished answer was lost: the tab returned to the project page, the url
|
|
2200
2248
|
// still matched the pin taken before ChatGPT rewrote it, and the DOM reader
|
|
2201
2249
|
// sat on zero assistant messages until the budget ran out.
|
|
2202
|
-
|
|
2250
|
+
// Acceptance may already have adopted one from the transcript; otherwise take
|
|
2251
|
+
// it from the url ChatGPT rewrote to.
|
|
2252
|
+
transcriptConversationId ??= pinnedThreadUrl ? conversationIdFromThreadUrl(pinnedThreadUrl) : undefined;
|
|
2203
2253
|
const transcriptResult = (transcript) => {
|
|
2204
2254
|
emitProgress("answered", `transcript (${transcript.answer.length} chars)`);
|
|
2205
2255
|
return {
|
|
@@ -3218,12 +3268,70 @@ export function deepResearchUnreadableBlocker(threadUrl) {
|
|
|
3218
3268
|
};
|
|
3219
3269
|
}
|
|
3220
3270
|
/**
|
|
3221
|
-
* The
|
|
3222
|
-
*
|
|
3223
|
-
*
|
|
3224
|
-
*
|
|
3225
|
-
*
|
|
3271
|
+
* The most recently updated conversations, each with the prompt it opens with.
|
|
3272
|
+
*
|
|
3273
|
+
* Acceptance is otherwise read off the page: if the DOM changes shape, a prompt
|
|
3274
|
+
* that DID post looks like one that never left, the send fails, and the
|
|
3275
|
+
* caller's retry asks ChatGPT the same question twice. The transcript settles
|
|
3276
|
+
* it - the conversation either holds our prompt or it does not.
|
|
3277
|
+
*
|
|
3278
|
+
* Only the head of each prompt is returned: a research transcript runs to
|
|
3279
|
+
* hundreds of KB and none of that is needed to recognise it.
|
|
3226
3280
|
*/
|
|
3281
|
+
export function recentConversationsExpression(limit = 4) {
|
|
3282
|
+
return `(async () => {
|
|
3283
|
+
const out = [];
|
|
3284
|
+
let token = "";
|
|
3285
|
+
try {
|
|
3286
|
+
const session = await fetch("/api/auth/session", { credentials: "include" });
|
|
3287
|
+
if (!session.ok) return out;
|
|
3288
|
+
const parsed = await session.json();
|
|
3289
|
+
token = (parsed && parsed.accessToken) || "";
|
|
3290
|
+
} catch (error) {
|
|
3291
|
+
return out;
|
|
3292
|
+
}
|
|
3293
|
+
const headers = token ? { Authorization: "Bearer " + token } : {};
|
|
3294
|
+
let items = [];
|
|
3295
|
+
try {
|
|
3296
|
+
const response = await fetch("/backend-api/conversations?offset=0&limit=${limit}&order=updated", {
|
|
3297
|
+
credentials: "include",
|
|
3298
|
+
headers
|
|
3299
|
+
});
|
|
3300
|
+
if (!response.ok) return out;
|
|
3301
|
+
const listed = await response.json();
|
|
3302
|
+
items = (listed && listed.items) || [];
|
|
3303
|
+
} catch (error) {
|
|
3304
|
+
return out;
|
|
3305
|
+
}
|
|
3306
|
+
for (const item of items) {
|
|
3307
|
+
if (!item || !item.id) continue;
|
|
3308
|
+
try {
|
|
3309
|
+
const response = await fetch("/backend-api/conversation/" + item.id, { credentials: "include", headers });
|
|
3310
|
+
if (!response.ok) continue;
|
|
3311
|
+
const conversation = await response.json();
|
|
3312
|
+
const mapping = (conversation && conversation.mapping) || {};
|
|
3313
|
+
const chain = [];
|
|
3314
|
+
let nodeId = conversation && conversation.current_node;
|
|
3315
|
+
let guard = 0;
|
|
3316
|
+
while (nodeId && mapping[nodeId] && guard < 2000) {
|
|
3317
|
+
guard += 1;
|
|
3318
|
+
if (mapping[nodeId].message) chain.push(mapping[nodeId].message);
|
|
3319
|
+
nodeId = mapping[nodeId].parent;
|
|
3320
|
+
}
|
|
3321
|
+
const user = chain.find((entry) => entry && entry.author && entry.author.role === "user" && entry.content);
|
|
3322
|
+
const text = user ? (user.content.parts || []).filter((part) => typeof part === "string").join("") : "";
|
|
3323
|
+
out.push({ id: item.id, userText: text.slice(0, 600) });
|
|
3324
|
+
} catch (error) {
|
|
3325
|
+
// A conversation we cannot read is simply not a match.
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
return out;
|
|
3329
|
+
})()`;
|
|
3330
|
+
}
|
|
3331
|
+
/** Which of those conversations is the one this send posted into, if any. */
|
|
3332
|
+
export function pickLandedConversation(candidates, sentPrompt) {
|
|
3333
|
+
return candidates.find((candidate) => transcriptMatchesSentPrompt(candidate.userText, sentPrompt))?.id;
|
|
3334
|
+
}
|
|
3227
3335
|
export function transcriptAnswerExpression(conversationId) {
|
|
3228
3336
|
return `(async () => {
|
|
3229
3337
|
const fail = (reason, extra) => Object.assign({ ok: false, reason, status: "", endTurn: false, isComplete: false, text: "", modelSlug: "", references: [], userText: "" }, extra || {});
|
package/dist/cli-help.js
CHANGED
|
@@ -27,9 +27,9 @@ Ask / consult commands:
|
|
|
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
28
|
prodex pro browser recover [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] --target-url <thread-url> [--timeout-ms 60000] # recover a finished answer from a thread whose send timed out
|
|
29
29
|
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
30
|
-
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
30
|
+
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
31
31
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
32
|
-
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
32
|
+
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
33
33
|
|
|
34
34
|
Bridge ledger (durable tasks/results/receipts/sessions under .bridge/):
|
|
35
35
|
prodex init [--cwd /absolute/path/to/repo]
|
|
@@ -168,9 +168,9 @@ Commands:
|
|
|
168
168
|
prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
169
169
|
prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js]
|
|
170
170
|
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt"
|
|
171
|
-
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
171
|
+
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
172
172
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
173
|
-
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
173
|
+
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
174
174
|
|
|
175
175
|
Use \`prodex pro ask\` for dry-run/manual previews.
|
|
176
176
|
Use \`prodex pro browser ask\` only when you want an explicit visible-browser send.
|
package/dist/cli-pro.js
CHANGED
|
@@ -575,7 +575,7 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
575
575
|
if (subcommand === "latest") {
|
|
576
576
|
if (printHelpIfRequested(proArgs, "pro latest", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
577
577
|
return 0;
|
|
578
|
-
assertOnlyOptions(proArgs, "pro latest", ["--cwd", "--source-cli"]);
|
|
578
|
+
assertOnlyOptions(proArgs, "pro latest", ["--cwd", "--source-cli"], ["--json"]);
|
|
579
579
|
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
580
580
|
const targetStore = new BridgeStore(targetCwd);
|
|
581
581
|
const sourceCli = resolveOptionalFileFlag(io.cwd, proArgs, "--source-cli");
|
|
@@ -589,13 +589,19 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
589
589
|
}
|
|
590
590
|
if (!consult)
|
|
591
591
|
throw new Error("No GPT Pro answers found");
|
|
592
|
-
|
|
592
|
+
if (proArgs.includes("--json")) {
|
|
593
|
+
const modelUsed = await recordedModelUsed(targetStore, consult.task.id);
|
|
594
|
+
io.stdout(formatProAnswerJson(consult, sourceCli, answerOptions, modelUsed ? { modelUsed } : {}));
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
io.stdout(formatProAnswer(consult, sourceCli, answerOptions));
|
|
598
|
+
}
|
|
593
599
|
return 0;
|
|
594
600
|
}
|
|
595
601
|
if (subcommand === "show") {
|
|
596
602
|
if (printHelpIfRequested(proArgs, "pro show", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"], maxPositionals: 1 }))
|
|
597
603
|
return 0;
|
|
598
|
-
const [taskId] = readPositionalsWithOptions(proArgs, "pro show", 1, ["--cwd", "--source-cli"]);
|
|
604
|
+
const [taskId] = readPositionalsWithOptions(proArgs, "pro show", 1, ["--cwd", "--source-cli"], ["--json"]);
|
|
599
605
|
if (!taskId)
|
|
600
606
|
throw new Error("pro show requires <task-id|latest>");
|
|
601
607
|
const targetCwd = resolveCwdFlag(io.cwd, proArgs);
|
|
@@ -611,7 +617,13 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
611
617
|
}
|
|
612
618
|
if (!consult)
|
|
613
619
|
throw new Error(taskId === "latest" ? "No GPT Pro answers found" : `GPT Pro answer not found: ${taskId}`);
|
|
614
|
-
|
|
620
|
+
if (proArgs.includes("--json")) {
|
|
621
|
+
const modelUsed = await recordedModelUsed(targetStore, consult.task.id);
|
|
622
|
+
io.stdout(formatProAnswerJson(consult, sourceCli, answerOptions, modelUsed ? { modelUsed } : {}));
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
io.stdout(formatProAnswer(consult, sourceCli, answerOptions));
|
|
626
|
+
}
|
|
615
627
|
return 0;
|
|
616
628
|
}
|
|
617
629
|
if (subcommand === "debate-prompt") {
|
|
@@ -1432,6 +1444,42 @@ export function browserSendBlockerFromError(error) {
|
|
|
1432
1444
|
next_step: "Resolve the visible browser issue manually, then rerun the consult if needed."
|
|
1433
1445
|
};
|
|
1434
1446
|
}
|
|
1447
|
+
/**
|
|
1448
|
+
* The same answer as `formatProAnswer`, shaped for a program.
|
|
1449
|
+
*
|
|
1450
|
+
* Every other read path is JSON; these two printed only the human rendering,
|
|
1451
|
+
* so an agent after the thread or the model that produced an answer had to
|
|
1452
|
+
* scrape prose.
|
|
1453
|
+
*/
|
|
1454
|
+
export function formatProAnswerJson(consult, sourceCli, options = {}, extras = {}) {
|
|
1455
|
+
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
1456
|
+
return JSON.stringify({
|
|
1457
|
+
task_id: consult.task.id,
|
|
1458
|
+
status: consult.result.status,
|
|
1459
|
+
thread: consult.task.provenance.thread ?? null,
|
|
1460
|
+
created_at: consult.result.created_at,
|
|
1461
|
+
answer: consult.result.summary,
|
|
1462
|
+
...(extras.modelUsed ? { model_used: extras.modelUsed } : {}),
|
|
1463
|
+
warnings: consult.result.warnings ?? [],
|
|
1464
|
+
...(blocker ? { blocker } : {})
|
|
1465
|
+
}, null, 2);
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Which model actually answered, from the receipt that recorded the answer.
|
|
1469
|
+
* It lives there rather than on the task because it is what ChatGPT tagged the
|
|
1470
|
+
* message with, not what prodex asked for.
|
|
1471
|
+
*/
|
|
1472
|
+
async function recordedModelUsed(store, taskId) {
|
|
1473
|
+
try {
|
|
1474
|
+
const receipts = await store.listReceiptsReadOnly({ kind: "consult_answer_saved", task_id: taskId });
|
|
1475
|
+
const value = receipts[0]?.metadata?.model_used;
|
|
1476
|
+
return typeof value === "string" ? value : undefined;
|
|
1477
|
+
}
|
|
1478
|
+
catch {
|
|
1479
|
+
// A missing or unreadable receipt just means the field is unknown.
|
|
1480
|
+
return undefined;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1435
1483
|
export function formatProAnswer(consult, sourceCli, options = {}) {
|
|
1436
1484
|
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
1437
1485
|
const summary = sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker);
|