@youdie006/prodex 0.24.0 → 0.25.0
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/bundle.js +6 -1
- package/dist/chatgpt-browser.js +102 -28
- package/dist/cli-pro.js +4 -3
- package/package.json +1 -1
package/dist/bundle.js
CHANGED
|
@@ -12,10 +12,14 @@ export async function buildDryRunBundle(root, input) {
|
|
|
12
12
|
input.prompt.trim()
|
|
13
13
|
];
|
|
14
14
|
const files = [];
|
|
15
|
+
// The prompt leads so the instruction is never buried under file dumps.
|
|
16
|
+
const sendSections = [input.prompt.trim()];
|
|
15
17
|
for (const file of input.files) {
|
|
16
18
|
const content = await readRepoFile(root, file, { maxLines: 500 });
|
|
17
19
|
files.push({ path: file, role: "context", bytes: Buffer.byteLength(content.content, "utf8") });
|
|
18
|
-
|
|
20
|
+
const fileSection = ["", `## File: ${file}`, "", "```text", content.content, "```"];
|
|
21
|
+
sections.push(...fileSection);
|
|
22
|
+
sendSections.push(...fileSection);
|
|
19
23
|
}
|
|
20
24
|
return {
|
|
21
25
|
schema_version: SCHEMA_VERSION,
|
|
@@ -24,6 +28,7 @@ export async function buildDryRunBundle(root, input) {
|
|
|
24
28
|
prompt: input.prompt,
|
|
25
29
|
files,
|
|
26
30
|
text: sections.join("\n"),
|
|
31
|
+
sendText: sendSections.join("\n"),
|
|
27
32
|
created_at: nowIso()
|
|
28
33
|
};
|
|
29
34
|
}
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -402,8 +402,43 @@ export function isUsableChatGptAnswer(answer) {
|
|
|
402
402
|
if (headerOnly)
|
|
403
403
|
return false;
|
|
404
404
|
}
|
|
405
|
+
// A composer tool renders its own progress panel into the assistant turn
|
|
406
|
+
// before any text exists. Measured on a --tool web-search send: "Searching
|
|
407
|
+
// the web" over an "Answer now" button, which the page reports as a settled
|
|
408
|
+
// two-line answer. Only treat it as a placeholder when it is the WHOLE
|
|
409
|
+
// content, so an answer that discusses searching still counts.
|
|
410
|
+
if (lineCount <= 2) {
|
|
411
|
+
const toolPanelOnly = normalized
|
|
412
|
+
.split(/\r?\n/)
|
|
413
|
+
.map((line) => line.trim())
|
|
414
|
+
.filter(Boolean)
|
|
415
|
+
.every((line) => /^(searching the web|웹\s*검색\s*중|answer now|지금\s*답변|검색\s*중)$/i.test(line));
|
|
416
|
+
if (toolPanelOnly)
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
405
419
|
return true;
|
|
406
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Who decides the answer is finished: the transcript, when it can be read.
|
|
423
|
+
*
|
|
424
|
+
* The page only guesses - a tool's progress panel looks exactly like a settled
|
|
425
|
+
* two-line answer, and a consult once returned "Searching the web / Answer now"
|
|
426
|
+
* as its result. When the transcript is reachable AND belongs to this consult,
|
|
427
|
+
* its state is authoritative: "pending" keeps the wait alive even if the page
|
|
428
|
+
* looks done. "unavailable" (unreachable, or a different conversation) is the
|
|
429
|
+
* only case that falls back to reading the page.
|
|
430
|
+
*/
|
|
431
|
+
export function classifyTranscriptRead(state, sentPrompt) {
|
|
432
|
+
if (!state)
|
|
433
|
+
return "unavailable";
|
|
434
|
+
if (!transcriptMatchesSentPrompt(state.userText, sentPrompt))
|
|
435
|
+
return "unavailable";
|
|
436
|
+
if (state.ok)
|
|
437
|
+
return "answer";
|
|
438
|
+
return state.reason === "answer_not_finished" || state.reason === "no_assistant_message" || state.reason === "answer_empty"
|
|
439
|
+
? "pending"
|
|
440
|
+
: "unavailable";
|
|
441
|
+
}
|
|
407
442
|
export function hasFreshChatGptAnswer(previousAssistantMessageCount, state) {
|
|
408
443
|
return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer) && !state.generating;
|
|
409
444
|
}
|
|
@@ -1750,25 +1785,23 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
1750
1785
|
warnings: []
|
|
1751
1786
|
};
|
|
1752
1787
|
}
|
|
1753
|
-
|
|
1754
|
-
* Read the answer from the conversation transcript, or undefined when it is not
|
|
1755
|
-
* there yet. The transcript trails the rendered stream by a beat, so callers
|
|
1756
|
-
* either poll it or fall back to the DOM text.
|
|
1757
|
-
*/
|
|
1758
|
-
async function readTranscriptAnswer(page, conversationId) {
|
|
1788
|
+
async function readTranscriptAnswer(page, conversationId, sentPrompt) {
|
|
1759
1789
|
let transcript;
|
|
1760
1790
|
try {
|
|
1761
1791
|
transcript = await evaluateOnPage(page, transcriptAnswerExpression(conversationId), { timeoutMs: 30_000 });
|
|
1762
1792
|
}
|
|
1763
1793
|
catch {
|
|
1764
|
-
// Transcript
|
|
1794
|
+
// Transcript unreachable (endpoint changed, transient failure): the DOM
|
|
1765
1795
|
// reader still runs, so this never blocks a send.
|
|
1766
|
-
return
|
|
1796
|
+
return { classification: "unavailable" };
|
|
1767
1797
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1798
|
+
const classification = classifyTranscriptRead(transcript, sentPrompt);
|
|
1799
|
+
if (classification !== "answer")
|
|
1800
|
+
return { classification };
|
|
1770
1801
|
const answer = resolveTranscriptCitations(transcript.text, transcript.references).trim();
|
|
1771
|
-
return answer.length > 0
|
|
1802
|
+
return answer.length > 0
|
|
1803
|
+
? { classification: "answer", answer: { answer, modelSlug: transcript.modelSlug } }
|
|
1804
|
+
: { classification: "pending" };
|
|
1772
1805
|
}
|
|
1773
1806
|
export async function sendChatGptPrompt(options) {
|
|
1774
1807
|
const port = resolveCdpPort(options.port);
|
|
@@ -2084,7 +2117,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
2084
2117
|
await sleep(5_000);
|
|
2085
2118
|
continue;
|
|
2086
2119
|
}
|
|
2087
|
-
if (lastState.ok && lastState.report.trim().length > 0) {
|
|
2120
|
+
if (lastState.ok && lastState.report.trim().length > 0 && transcriptMatchesSentPrompt(lastState.userText, options.prompt)) {
|
|
2088
2121
|
const report = resolveTranscriptCitations(lastState.report, lastState.references).trim();
|
|
2089
2122
|
emitProgress("answered", `deep research report (${report.length} chars)`);
|
|
2090
2123
|
return {
|
|
@@ -2115,15 +2148,19 @@ export async function sendChatGptPrompt(options) {
|
|
|
2115
2148
|
await sleep(1000);
|
|
2116
2149
|
try {
|
|
2117
2150
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
2151
|
+
// First conversation id wins. Re-deriving it every poll would let a tab
|
|
2152
|
+
// that wandered to another thread redirect the read to a stranger's
|
|
2153
|
+
// conversation - and the prompt check below is the second line of defence,
|
|
2154
|
+
// not the first.
|
|
2118
2155
|
if (!transcriptConversationId && finalState?.url)
|
|
2119
2156
|
transcriptConversationId = conversationIdFromThreadUrl(finalState.url);
|
|
2120
2157
|
// The transcript is the same data the page renders, minus the rendering:
|
|
2121
2158
|
// markdown instead of flattened innerText, an explicit finish state
|
|
2122
2159
|
// instead of caret heuristics, and the model that actually answered.
|
|
2123
2160
|
if (transcriptConversationId && !finalState.generating) {
|
|
2124
|
-
const transcript = await readTranscriptAnswer(page, transcriptConversationId);
|
|
2125
|
-
if (transcript)
|
|
2126
|
-
return transcriptResult(transcript);
|
|
2161
|
+
const transcript = await readTranscriptAnswer(page, transcriptConversationId, options.prompt);
|
|
2162
|
+
if (transcript.answer)
|
|
2163
|
+
return transcriptResult(transcript.answer);
|
|
2127
2164
|
}
|
|
2128
2165
|
// Only the DOM reader depends on which thread the tab is showing; once the
|
|
2129
2166
|
// conversation id is known, a wandering tab is harmless.
|
|
@@ -2167,12 +2204,15 @@ export async function sendChatGptPrompt(options) {
|
|
|
2167
2204
|
// Give the transcript that beat: it carries markdown (tables and fenced
|
|
2168
2205
|
// code that innerText flattens) and the model that actually answered.
|
|
2169
2206
|
if (transcriptConversationId) {
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2207
|
+
const transcript = await readTranscriptAnswer(page, transcriptConversationId, options.prompt);
|
|
2208
|
+
if (transcript.answer)
|
|
2209
|
+
return transcriptResult(transcript.answer);
|
|
2210
|
+
// The transcript can read this conversation and says it is not done:
|
|
2211
|
+
// believe it over a page that merely looks settled. A tool's progress
|
|
2212
|
+
// panel renders exactly like a two-line answer, and that is how a
|
|
2213
|
+
// consult once returned "Searching the web / Answer now" as its result.
|
|
2214
|
+
if (transcript.classification === "pending")
|
|
2215
|
+
continue;
|
|
2176
2216
|
}
|
|
2177
2217
|
break;
|
|
2178
2218
|
}
|
|
@@ -3030,7 +3070,7 @@ export function deepResearchUnreadableBlocker(threadUrl) {
|
|
|
3030
3070
|
*/
|
|
3031
3071
|
export function transcriptAnswerExpression(conversationId) {
|
|
3032
3072
|
return `(async () => {
|
|
3033
|
-
const fail = (reason, extra) => Object.assign({ ok: false, reason, status: "", endTurn: false, isComplete: false, text: "", modelSlug: "", references: [] }, extra || {});
|
|
3073
|
+
const fail = (reason, extra) => Object.assign({ ok: false, reason, status: "", endTurn: false, isComplete: false, text: "", modelSlug: "", references: [], userText: "" }, extra || {});
|
|
3034
3074
|
let token = "";
|
|
3035
3075
|
try {
|
|
3036
3076
|
const session = await fetch("/api/auth/session", { credentials: "include" });
|
|
@@ -3063,7 +3103,9 @@ export function transcriptAnswerExpression(conversationId) {
|
|
|
3063
3103
|
const message = chain.find(
|
|
3064
3104
|
(entry) => entry && entry.author && entry.author.role === "assistant" && entry.content && entry.content.content_type === "text"
|
|
3065
3105
|
);
|
|
3066
|
-
|
|
3106
|
+
const userMessage = chain.find((entry) => entry && entry.author && entry.author.role === "user" && entry.content);
|
|
3107
|
+
const userText = userMessage ? (userMessage.content.parts || []).filter((part) => typeof part === "string").join("") : "";
|
|
3108
|
+
if (!message) return fail("no_assistant_message", { userText });
|
|
3067
3109
|
const parts = (message.content.parts || []).filter((part) => typeof part === "string");
|
|
3068
3110
|
const text = parts.join("");
|
|
3069
3111
|
const metadata = message.metadata || {};
|
|
@@ -3073,13 +3115,34 @@ export function transcriptAnswerExpression(conversationId) {
|
|
|
3073
3115
|
isComplete: metadata.is_complete === true,
|
|
3074
3116
|
text,
|
|
3075
3117
|
modelSlug: metadata.model_slug || "",
|
|
3076
|
-
references: Array.isArray(metadata.content_references) ? metadata.content_references : []
|
|
3118
|
+
references: Array.isArray(metadata.content_references) ? metadata.content_references : [],
|
|
3119
|
+
userText
|
|
3077
3120
|
};
|
|
3078
3121
|
if (state.status !== "finished_successfully" || !state.endTurn) return fail("answer_not_finished", state);
|
|
3079
3122
|
if (!text) return fail("answer_empty", state);
|
|
3080
3123
|
return Object.assign({ ok: true, reason: "" }, state);
|
|
3081
3124
|
})()`;
|
|
3082
3125
|
}
|
|
3126
|
+
const NORMALIZED_PROMPT_MATCH_CHARS = 120;
|
|
3127
|
+
/**
|
|
3128
|
+
* Does this transcript belong to the consult that is waiting on it?
|
|
3129
|
+
*
|
|
3130
|
+
* The browser is shared with the user and other agents. Reading "whatever
|
|
3131
|
+
* conversation the tab shows" once cost a consult its answer - prodex saved a
|
|
3132
|
+
* different conversation as the result. Identity is checked against the prompt
|
|
3133
|
+
* that was actually sent, normalized because ChatGPT re-wraps whitespace, and
|
|
3134
|
+
* as a containment test because the transcript wraps the prompt: a composer
|
|
3135
|
+
* tool prefixes it ("@Deep research ...") and attachments append to it.
|
|
3136
|
+
*/
|
|
3137
|
+
export function transcriptMatchesSentPrompt(userText, sentPrompt) {
|
|
3138
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
3139
|
+
const seen = normalize(userText);
|
|
3140
|
+
const sent = normalize(sentPrompt);
|
|
3141
|
+
if (seen.length === 0 || sent.length === 0)
|
|
3142
|
+
return false;
|
|
3143
|
+
const expected = sent.slice(0, NORMALIZED_PROMPT_MATCH_CHARS);
|
|
3144
|
+
return seen.includes(expected);
|
|
3145
|
+
}
|
|
3083
3146
|
// ChatGPT marks citations with private-use delimiters (U+E200 opens, U+E202
|
|
3084
3147
|
// separates, U+E201 closes) and keeps the real sources in content_references.
|
|
3085
3148
|
const CITATION_DELIMITER_PATTERN = /[\uE200-\uE206]/;
|
|
@@ -3122,7 +3185,7 @@ export function resolveTranscriptCitations(text, references = []) {
|
|
|
3122
3185
|
}
|
|
3123
3186
|
export function deepResearchReportExpression(conversationId) {
|
|
3124
3187
|
return `(async () => {
|
|
3125
|
-
const fail = (reason, status) => ({ ok: false, reason, status: status || "", report: "", chars: 0, references: [] });
|
|
3188
|
+
const fail = (reason, status, userText) => ({ ok: false, reason, status: status || "", report: "", chars: 0, references: [], userText: userText || "" });
|
|
3126
3189
|
let token = "";
|
|
3127
3190
|
try {
|
|
3128
3191
|
const session = await fetch("/api/auth/session", { credentials: "include" });
|
|
@@ -3143,7 +3206,8 @@ export function deepResearchReportExpression(conversationId) {
|
|
|
3143
3206
|
} catch (error) {
|
|
3144
3207
|
return fail("conversation_error");
|
|
3145
3208
|
}
|
|
3146
|
-
const
|
|
3209
|
+
const mapping = (conversation && conversation.mapping) || {};
|
|
3210
|
+
const nodes = Object.keys(mapping).map((key) => mapping[key]);
|
|
3147
3211
|
const widgetNode = nodes.find(
|
|
3148
3212
|
(node) => node && node.message && node.message.metadata && node.message.metadata.chatgpt_sdk && node.message.metadata.chatgpt_sdk.widget_state
|
|
3149
3213
|
);
|
|
@@ -3155,12 +3219,22 @@ export function deepResearchReportExpression(conversationId) {
|
|
|
3155
3219
|
return fail("widget_state_unparsable");
|
|
3156
3220
|
}
|
|
3157
3221
|
const status = (state && state.status) || "";
|
|
3222
|
+
const chain = [];
|
|
3223
|
+
let walkId = conversation && conversation.current_node;
|
|
3224
|
+
let walkGuard = 0;
|
|
3225
|
+
while (walkId && mapping[walkId] && walkGuard < 2000) {
|
|
3226
|
+
walkGuard += 1;
|
|
3227
|
+
if (mapping[walkId].message) chain.push(mapping[walkId].message);
|
|
3228
|
+
walkId = mapping[walkId].parent;
|
|
3229
|
+
}
|
|
3230
|
+
const userNode = chain.find((entry) => entry && entry.author && entry.author.role === "user" && entry.content);
|
|
3231
|
+
const userText = userNode ? (userNode.content.parts || []).filter((part) => typeof part === "string").join("") : "";
|
|
3158
3232
|
const message = (state && state.report_message) || null;
|
|
3159
3233
|
const parts = message && message.content && message.content.parts;
|
|
3160
3234
|
const report = Array.isArray(parts) ? parts.filter((part) => typeof part === "string").join("") : "";
|
|
3161
3235
|
const references = message && message.metadata && Array.isArray(message.metadata.content_references) ? message.metadata.content_references : [];
|
|
3162
|
-
if (!report) return fail("report_not_ready", status);
|
|
3163
|
-
return { ok: true, reason: "", status, report, chars: report.length, references };
|
|
3236
|
+
if (!report) return fail("report_not_ready", status, userText);
|
|
3237
|
+
return { ok: true, reason: "", status, report, chars: report.length, references, userText };
|
|
3164
3238
|
})()`;
|
|
3165
3239
|
}
|
|
3166
3240
|
/**
|
package/dist/cli-pro.js
CHANGED
|
@@ -52,7 +52,7 @@ export async function runChatgptCommand(rest, io) {
|
|
|
52
52
|
const task = await targetStore.createTask({
|
|
53
53
|
source: "codex",
|
|
54
54
|
title: "GPT Pro smoke",
|
|
55
|
-
prompt: bundle.
|
|
55
|
+
prompt: bundle.sendText,
|
|
56
56
|
repo_id: "default",
|
|
57
57
|
provenance: {
|
|
58
58
|
adapter: "chatgpt-control",
|
|
@@ -852,7 +852,8 @@ export async function runAskProCommand(rest, io) {
|
|
|
852
852
|
const task = await targetStore.createTask({
|
|
853
853
|
source: "codex",
|
|
854
854
|
title: "GPT Pro consult",
|
|
855
|
-
|
|
855
|
+
// Record what ChatGPT actually received, not the preview rendering.
|
|
856
|
+
prompt: bundle.sendText,
|
|
856
857
|
repo_id: "default",
|
|
857
858
|
files: files.map((file) => ({ path: file, role: "context" })),
|
|
858
859
|
provenance: {
|
|
@@ -901,7 +902,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
901
902
|
// fast. A dead holder is still reaped immediately.
|
|
902
903
|
const sendOnce = () => withBrowserSendLock(busyWaitMs ?? browserTimeoutMs ?? 0, (detail) => io.stderr(`progress: ${detail}`), () => sendChatGptPrompt({
|
|
903
904
|
port: browserPort,
|
|
904
|
-
prompt: bundle.
|
|
905
|
+
prompt: bundle.sendText,
|
|
905
906
|
targetUrl: normalizedTargetUrl,
|
|
906
907
|
timeoutMs: browserTimeoutMs,
|
|
907
908
|
...(attachments.length > 0 ? { attachments } : {}),
|