@youdie006/prodex 0.39.4 → 0.40.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/blocker-report.js +81 -0
- package/dist/chatgpt-browser.js +35 -9
- package/dist/cli-args.js +5 -1
- package/dist/cli-help.js +7 -4
- package/dist/cli-pro.js +162 -3
- package/dist/mcp.js +5 -1
- package/dist/registry.js +32 -0
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rank what actually blocks consults, across every bridge root on the machine.
|
|
3
|
+
*
|
|
4
|
+
* Half of every consult here ends in a blocker, and until now asking WHICH
|
|
5
|
+
* failure dominates meant writing a throwaway script: `pro list` reads a single
|
|
6
|
+
* repo, and the registry that knows where the others are had no reader. Pure
|
|
7
|
+
* on purpose - the reading lives in the command, so the ranking can be tested
|
|
8
|
+
* without a filesystem.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Codes that describe how a send died rather than why. Counting these whole
|
|
12
|
+
* reports "the catch-all is biggest" and names nothing to fix - measured, one
|
|
13
|
+
* of them held 165 of 267 blockers - so their message decides the group.
|
|
14
|
+
*/
|
|
15
|
+
const CATCH_ALL_CODES = new Set(["browser_send_failed", "consult_failed", "unknown_error"]);
|
|
16
|
+
/**
|
|
17
|
+
* The part of a message that identifies the failure, with the varying parts
|
|
18
|
+
* removed: the same picker failure is written once with "Pro" and once with a
|
|
19
|
+
* model name, and those are one cause, not two.
|
|
20
|
+
*/
|
|
21
|
+
export function blockerCause(message) {
|
|
22
|
+
const firstSentence = /^(.*?)(?:\.\s|\.$|$)/.exec(message.trim())?.[1] ?? message.trim();
|
|
23
|
+
return firstSentence
|
|
24
|
+
.replace(/"[^"]*"/g, '"..."')
|
|
25
|
+
.replace(/\d+/g, "N")
|
|
26
|
+
.replace(/\s+/g, " ")
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
function groupKey(code, message) {
|
|
30
|
+
if (!CATCH_ALL_CODES.has(code))
|
|
31
|
+
return code;
|
|
32
|
+
const cause = blockerCause(message);
|
|
33
|
+
return cause ? `${code}: ${cause}` : code;
|
|
34
|
+
}
|
|
35
|
+
export function buildBlockerReport(input) {
|
|
36
|
+
const cutoff = input.since ? Date.parse(input.since) : undefined;
|
|
37
|
+
const inWindow = input.consults.filter((c) => {
|
|
38
|
+
if (cutoff === undefined)
|
|
39
|
+
return true;
|
|
40
|
+
if (!c.createdAt)
|
|
41
|
+
return false;
|
|
42
|
+
const at = Date.parse(c.createdAt);
|
|
43
|
+
return Number.isFinite(at) && at >= cutoff;
|
|
44
|
+
});
|
|
45
|
+
const groups = new Map();
|
|
46
|
+
let blocked = 0;
|
|
47
|
+
for (const consult of inWindow) {
|
|
48
|
+
if (!consult.blocker)
|
|
49
|
+
continue;
|
|
50
|
+
blocked += 1;
|
|
51
|
+
const key = groupKey(consult.blocker.code, consult.blocker.message);
|
|
52
|
+
const at = consult.createdAt ?? "";
|
|
53
|
+
const existing = groups.get(key);
|
|
54
|
+
if (existing) {
|
|
55
|
+
existing.count += 1;
|
|
56
|
+
if (at > existing.lastSeen)
|
|
57
|
+
existing.lastSeen = at;
|
|
58
|
+
existing.repos.set(consult.repo, (existing.repos.get(consult.repo) ?? 0) + 1);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
groups.set(key, {
|
|
62
|
+
count: 1,
|
|
63
|
+
lastSeen: at,
|
|
64
|
+
example: consult.blocker.message,
|
|
65
|
+
repos: new Map([[consult.repo, 1]])
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const ranked = [...groups.entries()]
|
|
70
|
+
.sort((a, b) => b[1].count - a[1].count || a[0].localeCompare(b[0]))
|
|
71
|
+
.slice(0, input.limit ?? Number.POSITIVE_INFINITY)
|
|
72
|
+
.map(([code, g]) => ({
|
|
73
|
+
code,
|
|
74
|
+
count: g.count,
|
|
75
|
+
share: blocked > 0 ? g.count / blocked : 0,
|
|
76
|
+
lastSeen: g.lastSeen,
|
|
77
|
+
example: g.example,
|
|
78
|
+
repos: [...g.repos.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([repo]) => repo)
|
|
79
|
+
}));
|
|
80
|
+
return { totalConsults: inWindow.length, blocked, roots: input.roots, groups: ranked };
|
|
81
|
+
}
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -613,14 +613,32 @@ export function hasChatGptPromptAcceptance(previous, state) {
|
|
|
613
613
|
* they were getting Pro reasoning when they were not.
|
|
614
614
|
*/
|
|
615
615
|
export function modelSelectionWarning(requestedModel, modelSlug) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
616
|
+
return selectionMismatchWarning({
|
|
617
|
+
...(requestedModel !== undefined ? { model: requestedModel } : {}),
|
|
618
|
+
...(modelSlug !== undefined ? { modelSlug } : {})
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
/** Whether a requested label names ChatGPT's Pro step, in either locale. */
|
|
622
|
+
export function namesPro(label) {
|
|
623
|
+
if (!label)
|
|
624
|
+
return false;
|
|
625
|
+
return /\bpro\b/i.test(label) || label.includes("프로");
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Compare what was asked for against the model that actually answered.
|
|
629
|
+
*
|
|
630
|
+
* Pro can be asked for on either axis - `--model Pro` or `--effort Pro` - and
|
|
631
|
+
* this only ever looked at the model, so an effort-shaped Pro request answered
|
|
632
|
+
* by a lesser model passed without a word. Measured: a consult asking for Pro
|
|
633
|
+
* came back from gpt-5-6-thinking and was recorded as a clean success.
|
|
634
|
+
*/
|
|
635
|
+
export function selectionMismatchWarning(input) {
|
|
636
|
+
const asked = namesPro(input.model) ? input.model : namesPro(input.effort) ? input.effort : undefined;
|
|
637
|
+
if (!asked || !input.modelSlug)
|
|
620
638
|
return undefined;
|
|
621
|
-
if (/pro/i.test(modelSlug))
|
|
639
|
+
if (/pro/i.test(input.modelSlug))
|
|
622
640
|
return undefined;
|
|
623
|
-
return `model_mismatch: you asked for ${
|
|
641
|
+
return `model_mismatch: you asked for ${asked}, but the answer came from "${input.modelSlug}". Check the model picker in the browser; the selection did not take.`;
|
|
624
642
|
}
|
|
625
643
|
/**
|
|
626
644
|
* The panel ChatGPT raises to ask which of two answers you prefer.
|
|
@@ -2248,6 +2266,14 @@ async function selectModelReasoning(cdp, options, selectionWarnings = []) {
|
|
|
2248
2266
|
const noSuchStep = /has no "[^"]*" step/.test(message);
|
|
2249
2267
|
if (!noSuchStep)
|
|
2250
2268
|
throw error;
|
|
2269
|
+
// Asking for a step is a contract. Sending at whatever the slider
|
|
2270
|
+
// happened to be on is not a lesser version of that contract, it is
|
|
2271
|
+
// a different answer that the caller cannot use: measured, a Pro
|
|
2272
|
+
// request came back from gpt-5-6-thinking after minutes of waiting
|
|
2273
|
+
// and was thrown away. A caller that would rather have any answer
|
|
2274
|
+
// says so with --allow-model-fallback.
|
|
2275
|
+
if (!options.allowSelectionFallback)
|
|
2276
|
+
throw error;
|
|
2251
2277
|
const offered = /It showed: (.*)$/.exec(message)?.[1]?.split(" / ").map((part) => part.trim()) ?? [];
|
|
2252
2278
|
selectionWarnings.push(stepSelectionUnavailableWarning(plan.sliderLabel, offered));
|
|
2253
2279
|
}
|
|
@@ -3108,7 +3134,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
3108
3134
|
answer: transcript.answer,
|
|
3109
3135
|
modelHints: finalState?.modelHints ?? [],
|
|
3110
3136
|
...(transcript.modelSlug ? { modelSlug: transcript.modelSlug } : finalState?.modelSlug ? { modelSlug: finalState.modelSlug } : {}),
|
|
3111
|
-
warnings: withDialogNote([...sendWarnings,
|
|
3137
|
+
warnings: withDialogNote([...sendWarnings, selectionMismatchWarning({ ...(options.model !== undefined ? { model: options.model } : {}), ...(options.effort !== undefined ? { effort: options.effort } : {}), ...((transcript.modelSlug || finalState?.modelSlug) ? { modelSlug: (transcript.modelSlug || finalState?.modelSlug) } : {}) })]).filter((warning) => Boolean(warning))
|
|
3112
3138
|
};
|
|
3113
3139
|
};
|
|
3114
3140
|
// Deep research never reaches the DOM answer wait below: the report is
|
|
@@ -3256,7 +3282,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
3256
3282
|
answer: completed.answer.trim(),
|
|
3257
3283
|
modelHints: completed.modelHints,
|
|
3258
3284
|
...(completed.modelSlug ? { modelSlug: completed.modelSlug } : {}),
|
|
3259
|
-
warnings: withDialogNote([...sendWarnings,
|
|
3285
|
+
warnings: withDialogNote([...sendWarnings, selectionMismatchWarning({ ...(options.model !== undefined ? { model: options.model } : {}), ...(options.effort !== undefined ? { effort: options.effort } : {}), ...(completed.modelSlug !== undefined ? { modelSlug: completed.modelSlug } : {}) })]).filter((warning) => Boolean(warning))
|
|
3260
3286
|
};
|
|
3261
3287
|
}
|
|
3262
3288
|
// Timed out while the answer was still streaming: salvage the partial text
|
|
@@ -3272,7 +3298,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
3272
3298
|
...(completed.modelSlug ? { modelSlug: completed.modelSlug } : {}),
|
|
3273
3299
|
warnings: withDialogNote([
|
|
3274
3300
|
...sendWarnings,
|
|
3275
|
-
...(
|
|
3301
|
+
...(selectionMismatchWarning({ ...(options.model !== undefined ? { model: options.model } : {}), ...(options.effort !== undefined ? { effort: options.effort } : {}), ...(completed.modelSlug !== undefined ? { modelSlug: completed.modelSlug } : {}) }) ? [selectionMismatchWarning({ ...(options.model !== undefined ? { model: options.model } : {}), ...(options.effort !== undefined ? { effort: options.effort } : {}), ...(completed.modelSlug !== undefined ? { modelSlug: completed.modelSlug } : {}) })] : []),
|
|
3276
3302
|
`answer_incomplete: ChatGPT was still generating after ${formatDurationMs(timeoutMs)} (${timeoutMs}ms), so the answer below may be truncated. Raise --timeout-ms and retry for the full response.`
|
|
3277
3303
|
])
|
|
3278
3304
|
};
|
package/dist/cli-args.js
CHANGED
|
@@ -259,7 +259,11 @@ export const ASK_PRO_BOOLEAN_FLAGS = new Set([
|
|
|
259
259
|
"--auto-login",
|
|
260
260
|
"--no-auto-login",
|
|
261
261
|
// Send outside any project for once, overriding a pinned default.
|
|
262
|
-
"--no-project"
|
|
262
|
+
"--no-project",
|
|
263
|
+
// Send even when the requested model/effort could not be applied, rather
|
|
264
|
+
// than stopping. Off by default: an answer from a step nobody asked for is
|
|
265
|
+
// usually thrown away.
|
|
266
|
+
"--allow-model-fallback"
|
|
263
267
|
]);
|
|
264
268
|
export const ASK_PRO_SELECTION_VALUE_FLAGS = ["--project", "--project-new", "--model", "--pro-mode", "--effort"];
|
|
265
269
|
export const ASK_PRO_VALUE_FLAGS = new Set([
|
package/dist/cli-help.js
CHANGED
|
@@ -26,8 +26,9 @@ Ask / consult commands:
|
|
|
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
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
|
-
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] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
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] [--temporary] [--allow-model-fallback] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
30
30
|
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
31
|
+
prodex pro blockers [--cwd /absolute/path/to/repo] [--since 7d] [--limit 10] [--json] # what actually blocks consults, ranked, across every bridge root on this machine
|
|
31
32
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
32
33
|
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
33
34
|
prodex pro report-issue [--cwd /absolute/path/to/repo] [--task <task-id>] [--repo owner/name] [--confirm] # bug report from a failed consult's receipt; previews unless --confirm, never carries the prompt or the answer
|
|
@@ -88,6 +89,7 @@ Optional visible-browser send defaults (applied by \`pro browser ask\` when the
|
|
|
88
89
|
--model Composer model by its exact menu label. Only Pro applies on the current picker: it is a slider step, and the model rows cannot be clicked
|
|
89
90
|
--pro-mode Pro sub-mode: 기본 (standard) or 확장 (extended)
|
|
90
91
|
--effort Reasoning effort: 즉시 / 중간 / 높음 / 매우 높음 / Max / Ultra / Pro. Max and Ultra are rungs of ChatGPT's Work surface and only apply when the browser is already on Work (prodex does not switch to Work for them); every other value is sent on Chat, whose top step is Pro
|
|
92
|
+
--allow-model-fallback Send even when the requested model/effort could not be applied. Off by default: a send that could not reach what was asked for stops instead, because an answer from a step nobody asked for is usually unusable
|
|
91
93
|
--project Sidebar project to enter before sending
|
|
92
94
|
Clear a saved default with --clear-model / --clear-pro-mode / --clear-effort / --clear-project.
|
|
93
95
|
--pro-mode and --effort are different model axes and cannot be combined. View saved defaults with \`prodex status\`.`);
|
|
@@ -168,8 +170,9 @@ Commands:
|
|
|
168
170
|
prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
169
171
|
prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
170
172
|
prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js]
|
|
171
|
-
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"] "prompt"
|
|
173
|
+
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--temporary] [--allow-model-fallback] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"] "prompt"
|
|
172
174
|
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
175
|
+
prodex pro blockers [--cwd /absolute/path/to/repo] [--since 7d] [--limit 10] [--json] # what actually blocks consults, ranked, across every bridge root on this machine
|
|
173
176
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
174
177
|
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
175
178
|
prodex pro report-issue [--cwd /absolute/path/to/repo] [--task <task-id>] [--repo owner/name] [--confirm] # bug report from a failed consult's receipt; previews unless --confirm, never carries the prompt or the answer
|
|
@@ -255,8 +258,8 @@ export function printProBrowserHelp(stdout, sourceCli) {
|
|
|
255
258
|
: "prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]";
|
|
256
259
|
const selectionUsage = '[--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"]';
|
|
257
260
|
const askUsage = sourceCli
|
|
258
|
-
? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] ${selectionUsage} "prompt"`
|
|
259
|
-
: `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] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] ${selectionUsage} "prompt"`;
|
|
261
|
+
? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--temporary] [--allow-model-fallback] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] ${selectionUsage} "prompt"`
|
|
262
|
+
: `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] [--temporary] [--allow-model-fallback] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] ${selectionUsage} "prompt"`;
|
|
260
263
|
const modelsUsage = sourceCli
|
|
261
264
|
? `${cli} pro browser models${sourceCliOption} [--port 9333] [--timeout-ms 15000]`
|
|
262
265
|
: "prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000]";
|
package/dist/cli-pro.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildDryRunBundle } from "./bundle.js";
|
|
5
|
-
import { DEFAULT_CDP_PORT, resolveCdpPort, resolveConversationToDelete, resolveProjectToDelete, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, formatModelMenuOption, listChatGptModelOptions, deleteChatGptConversation, endWedgedBrowser, browserRecoveryPlan, findWedgedBrowser, wedgedBrowserBlocker, deleteChatGptProject, listChatGptProjectsWithIds, listRecentChatGptConversations, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveBrowserWindowMode, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt, statusMeansBrowserDead } from "./chatgpt-browser.js";
|
|
5
|
+
import { DEFAULT_CDP_PORT, resolveCdpPort, resolveConversationToDelete, resolveProjectToDelete, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, formatModelMenuOption, listChatGptModelOptions, deleteChatGptConversation, endWedgedBrowser, browserRecoveryPlan, findWedgedBrowser, wedgedBrowserBlocker, deleteChatGptProject, listChatGptProjectsWithIds, listRecentChatGptConversations, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveBrowserWindowMode, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt, statusMeansBrowserDead, namesPro } from "./chatgpt-browser.js";
|
|
6
6
|
import { ASK_PRO_BOOLEAN_FLAGS, ASK_PRO_PREVIEW_VALUE_FLAGS, ASK_PRO_VALUE_FLAGS, assertHelpRequestArgs, assertNoExtraArgs, assertOnlyOptions, findHelpFlagIndexBeforePromptDelimiter, formatCliCommand, hasAskProDryRunMode, hasAskProMode, hasAskProSendMode, isHelpSubcommand, parseAskProArgs, printHelpIfRequested, readFlag, readPortFlag, readPositionalsWithOptions, readNonNegativeIntegerFlag, readPositiveIntegerFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
|
|
7
7
|
import { printProBrowserHelp, printProHelp } from "./cli-help.js";
|
|
8
8
|
import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
|
|
@@ -10,6 +10,8 @@ import { formatBrowserDefaults, redactServerUrl } from "./cli-server.js";
|
|
|
10
10
|
import { errorMessage, firstLine, formatBlockedConsultRecordedMessage, formatProLatestCommand, formatBrowserCheckCommand, formatBrowserLoginCommand, formatBrowserSmokeCommand, formatBrowserTargetAskCommand, formatInitCommand, formatSetupCommand, isMissingFileError, computeSendPacingWaitMs, isUntrustedResultError, resolveMinSendIntervalMs, sourceAwareBrowserBlocker, sourceAwareBrowserNextStep, sourceAwareResultError, sourceAwareResultMessage, sourceAwareSetupMessage } from "./cli-shared.js";
|
|
11
11
|
import { getTokenExpiryStatus, loadBrowserDefaults, loadLocalConfig } from "./config.js";
|
|
12
12
|
import { withBrowserSendLock } from "./browser-send-lock.js";
|
|
13
|
+
import { blockerCause, buildBlockerReport } from "./blocker-report.js";
|
|
14
|
+
import { readBridgeRoots } from "./registry.js";
|
|
13
15
|
import { BridgeStore, MAX_FETCHABLE_RESULT_ARTIFACT_BYTES } from "./store.js";
|
|
14
16
|
import { CLI_VERSION } from "./cli-help.js";
|
|
15
17
|
import { PRODEX_ISSUE_REPO, buildIssueReport, fileGitHubIssue } from "./issue-report.js";
|
|
@@ -732,6 +734,50 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
732
734
|
// a two-hop dead end that made an agent abandon the diagnosis.
|
|
733
735
|
throw new Error(`Use \`prodex pro browser ${legacyBrowserSubcommandReplacement(subcommand)}\` for explicit browser automation.`);
|
|
734
736
|
}
|
|
737
|
+
if (subcommand === "blockers") {
|
|
738
|
+
if (printHelpIfRequested(proArgs, "pro blockers", io.stdout, printProHelp, { valueFlags: ["--cwd", "--since", "--limit"] }))
|
|
739
|
+
return 0;
|
|
740
|
+
assertOnlyOptions(proArgs, "pro blockers", ["--cwd", "--since", "--limit"], ["--json"]);
|
|
741
|
+
const scopedToOneRepo = readFlag(proArgs, "--cwd") !== undefined;
|
|
742
|
+
// Default to every bridge root: the failures are spread across the repos
|
|
743
|
+
// consults were run from, and `pro list` already covers just this one.
|
|
744
|
+
const roots = scopedToOneRepo ? [resolveCwdFlag(io.cwd, proArgs)] : await readBridgeRoots();
|
|
745
|
+
const since = readSinceFlag(proArgs);
|
|
746
|
+
const limit = readPositiveIntegerFlag(proArgs, "--limit") ?? 10;
|
|
747
|
+
const consults = [];
|
|
748
|
+
let readable = 0;
|
|
749
|
+
for (const root of roots) {
|
|
750
|
+
try {
|
|
751
|
+
const results = await new BridgeStore(root).listResultsReadOnly();
|
|
752
|
+
readable += 1;
|
|
753
|
+
for (const result of results) {
|
|
754
|
+
if (!result.task_id.includes("gpt-pro-consult"))
|
|
755
|
+
continue;
|
|
756
|
+
consults.push({
|
|
757
|
+
repo: path.basename(root),
|
|
758
|
+
createdAt: result.created_at,
|
|
759
|
+
...(result.blocker ? { blocker: { code: result.blocker.code, message: result.blocker.message } } : {})
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
// A root that has been deleted or is unreadable is not a failure of
|
|
765
|
+
// the report; it just has nothing to contribute.
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const report = buildBlockerReport({
|
|
769
|
+
consults,
|
|
770
|
+
roots: readable,
|
|
771
|
+
...(since ? { since } : {}),
|
|
772
|
+
limit
|
|
773
|
+
});
|
|
774
|
+
if (proArgs.includes("--json")) {
|
|
775
|
+
io.stdout(JSON.stringify(report, null, 2));
|
|
776
|
+
return 0;
|
|
777
|
+
}
|
|
778
|
+
io.stdout(formatBlockerReport(report, { since, scopedToOneRepo }));
|
|
779
|
+
return 0;
|
|
780
|
+
}
|
|
735
781
|
if (subcommand === "list") {
|
|
736
782
|
if (printHelpIfRequested(proArgs, "pro list", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
737
783
|
return 0;
|
|
@@ -879,7 +925,7 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
879
925
|
io.stdout(formatDebatePrompt({ topic: readFlag(proArgs, "--topic"), rounds, sourceCli }));
|
|
880
926
|
return 0;
|
|
881
927
|
}
|
|
882
|
-
throw unknownSubcommandError("pro", subcommand, ["ask", "browser", "debate-prompt", "list", "latest", "show"]);
|
|
928
|
+
throw unknownSubcommandError("pro", subcommand, ["ask", "blockers", "browser", "debate-prompt", "list", "latest", "show"]);
|
|
883
929
|
}
|
|
884
930
|
export async function runConsultsCommand(rest, io) {
|
|
885
931
|
throw new Error("The legacy `consults` alias is retired. Use `prodex pro list`, `prodex pro latest`, or `prodex pro show <task-id|latest>`.");
|
|
@@ -1110,13 +1156,22 @@ export async function runAskProCommand(rest, io) {
|
|
|
1110
1156
|
};
|
|
1111
1157
|
const browserPort = hasSendMode ? resolveCdpPort(readPortFlag(parsedAskPro.optionArgs, "--port")) : undefined;
|
|
1112
1158
|
const busyWaitMs = readNonNegativeIntegerFlag(parsedAskPro.optionArgs, "--busy-wait-ms");
|
|
1159
|
+
const allowSelectionFallback = parsedAskPro.optionArgs.includes("--allow-model-fallback");
|
|
1113
1160
|
// Pro extended can legitimately think for minutes, so its default timeout is
|
|
1114
1161
|
// higher; an explicit --timeout-ms always wins.
|
|
1115
1162
|
// Pro reasoning routinely runs for many minutes (a real consult measured
|
|
1116
1163
|
// ~13 minutes). The elevated default used to be keyed to the removed
|
|
1117
1164
|
// --pro-mode, so --model Pro sends fell back to 90s and chronically timed
|
|
1118
1165
|
// out. Any effective Pro selection now defaults to 15 minutes.
|
|
1119
|
-
|
|
1166
|
+
// Pro can be asked for on either axis. Keying the raised budget to the
|
|
1167
|
+
// model alone gave `--effort Pro` five minutes for reasoning this file's
|
|
1168
|
+
// own comment calls 6-20 minutes, which manufactured send_timeout
|
|
1169
|
+
// blockers - 55 of the 267 blockers measured on one machine.
|
|
1170
|
+
const effectiveProSelection = isProSelection({
|
|
1171
|
+
...(selectionModel !== undefined ? { model: selectionModel } : {}),
|
|
1172
|
+
...(selectionProMode !== undefined ? { proMode: selectionProMode } : {}),
|
|
1173
|
+
...(selectionEffort !== undefined ? { effort: selectionEffort } : {})
|
|
1174
|
+
});
|
|
1120
1175
|
// Pro reasoning routinely runs 6-20 minutes; 15 min was still cutting long
|
|
1121
1176
|
// answers off (field report), so a Pro selection defaults to 20 minutes.
|
|
1122
1177
|
// With NO model selection at all (no flag, no saved default) the UI's
|
|
@@ -1202,6 +1257,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
1202
1257
|
model: selectionModel,
|
|
1203
1258
|
proMode: selectionProMode,
|
|
1204
1259
|
effort: selectionEffort,
|
|
1260
|
+
...(allowSelectionFallback ? { allowSelectionFallback: true } : {}),
|
|
1205
1261
|
onProgress: createBrowserSendProgressPrinter(io.stderr)
|
|
1206
1262
|
}));
|
|
1207
1263
|
// One-command recovery: interactive terminals (or explicit --auto-login)
|
|
@@ -1303,6 +1359,16 @@ export async function runAskProCommand(rest, io) {
|
|
|
1303
1359
|
io.stderr(warning);
|
|
1304
1360
|
if (consult.modelSlug)
|
|
1305
1361
|
io.stderr(`model_used: ${consult.modelSlug}`);
|
|
1362
|
+
// "Can I count this as a Pro review?" - answered here rather than left
|
|
1363
|
+
// for whoever reads the receipt to work out from two other fields.
|
|
1364
|
+
const proVerified = proSelectionVerified({
|
|
1365
|
+
...(selectionModel !== undefined ? { model: selectionModel } : {}),
|
|
1366
|
+
...(selectionProMode !== undefined ? { proMode: selectionProMode } : {}),
|
|
1367
|
+
...(selectionEffort !== undefined ? { effort: selectionEffort } : {}),
|
|
1368
|
+
...(consult.modelSlug !== undefined ? { modelSlug: consult.modelSlug } : {})
|
|
1369
|
+
});
|
|
1370
|
+
if (proVerified !== undefined)
|
|
1371
|
+
io.stderr(`pro_verified: ${proVerified ? "yes" : "no"}`);
|
|
1306
1372
|
let answerArtifactPath;
|
|
1307
1373
|
const answerArtifactBytes = Buffer.byteLength(answerArtifactText, "utf8");
|
|
1308
1374
|
if (answerArtifactBytes > MAX_FETCHABLE_RESULT_ARTIFACT_BYTES) {
|
|
@@ -1333,6 +1399,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
1333
1399
|
// What actually answered, straight from ChatGPT's own tag - the
|
|
1334
1400
|
// receipt used to record only what prodex asked for.
|
|
1335
1401
|
...(consult.modelSlug ? { model_used: consult.modelSlug } : {}),
|
|
1402
|
+
...(proVerified !== undefined ? { pro_verified: proVerified } : {}),
|
|
1336
1403
|
warnings: persistenceWarnings
|
|
1337
1404
|
}
|
|
1338
1405
|
});
|
|
@@ -1498,6 +1565,7 @@ export async function performBrowserConsultForMcp(cwd, input, onProgress) {
|
|
|
1498
1565
|
...(input.attach ?? []).flatMap((file) => ["--attach", file]),
|
|
1499
1566
|
...(input.tools ?? []).flatMap((tool) => ["--tool", tool]),
|
|
1500
1567
|
...(input.new_chat ? ["--new-chat"] : []),
|
|
1568
|
+
...(input.allow_model_fallback ? ["--allow-model-fallback"] : []),
|
|
1501
1569
|
"--",
|
|
1502
1570
|
input.prompt
|
|
1503
1571
|
];
|
|
@@ -1705,6 +1773,29 @@ export async function assertBrowserLaunchStayedAlive(opened, timeoutMs) {
|
|
|
1705
1773
|
}
|
|
1706
1774
|
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.`);
|
|
1707
1775
|
}
|
|
1776
|
+
/** Whether a send asked for ChatGPT's Pro step, on either axis that can name it. */
|
|
1777
|
+
export function isProSelection(selection) {
|
|
1778
|
+
if (selection.proMode !== undefined)
|
|
1779
|
+
return true;
|
|
1780
|
+
return namesPro(selection.model) || namesPro(selection.effort);
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Whether a Pro request was actually answered by Pro, per ChatGPT's own tag on
|
|
1784
|
+
* the message. Undefined when Pro was not asked for, or when no tag came back
|
|
1785
|
+
* to check against - there is nothing to certify in either case.
|
|
1786
|
+
*
|
|
1787
|
+
* The receipt already carried the request and the answering model separately,
|
|
1788
|
+
* leaving the comparison to whoever read it later. The question a caller has
|
|
1789
|
+
* is "can I count this as a Pro review?", and a consult that asked for Pro and
|
|
1790
|
+
* was answered by gpt-5-6-thinking was recorded as a clean success.
|
|
1791
|
+
*/
|
|
1792
|
+
export function proSelectionVerified(selection) {
|
|
1793
|
+
if (!isProSelection(selection))
|
|
1794
|
+
return undefined;
|
|
1795
|
+
if (!selection.modelSlug)
|
|
1796
|
+
return undefined;
|
|
1797
|
+
return /pro/i.test(selection.modelSlug);
|
|
1798
|
+
}
|
|
1708
1799
|
export function browserSendBlockerFromError(error) {
|
|
1709
1800
|
const blocker = typeof error === "object" && error !== null && "blocker" in error ? error.blocker : undefined;
|
|
1710
1801
|
if (typeof blocker === "object" &&
|
|
@@ -1770,6 +1861,17 @@ export function browserSendBlockerFromError(error) {
|
|
|
1770
1861
|
next_step: "Another prodex send holds the browser. Wait for it to finish and retry, or pass a longer --timeout-ms, which is also the queue budget."
|
|
1771
1862
|
};
|
|
1772
1863
|
}
|
|
1864
|
+
// The picker could not provide the step that was asked for. Retrying asks
|
|
1865
|
+
// the same picker the same question, so this is not retryable; the caller
|
|
1866
|
+
// either picks a step it offers or opts into whatever the slider is on.
|
|
1867
|
+
if (/has no "[^"]*" step/.test(message)) {
|
|
1868
|
+
return {
|
|
1869
|
+
code: "selection_not_applied",
|
|
1870
|
+
message,
|
|
1871
|
+
retryable: false,
|
|
1872
|
+
next_step: "ChatGPT's picker could not provide the model or effort that was asked for, so nothing was sent. Run `prodex pro browser models` to see the steps this account offers and ask for one of those, or pass --allow-model-fallback to send at whatever the picker is currently set to."
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1773
1875
|
// The picker's slider took focus before its key handler was attached and
|
|
1774
1876
|
// ignored every press. Retrying is the cure; nothing in the browser is
|
|
1775
1877
|
// broken, so the generic "resolve it manually" pointed at nothing.
|
|
@@ -1878,6 +1980,63 @@ export function formatProConsultArtifact(consult) {
|
|
|
1878
1980
|
lines.push("## Answer", "", consult.answer.trim(), "");
|
|
1879
1981
|
return lines.join("\n");
|
|
1880
1982
|
}
|
|
1983
|
+
/**
|
|
1984
|
+
* `--since 7d`, `--since 24h`, or a plain date. A window is how you ask whether
|
|
1985
|
+
* a fix landed, so it has to be quick to type.
|
|
1986
|
+
*/
|
|
1987
|
+
export function readSinceFlag(args) {
|
|
1988
|
+
const raw = readFlag(args, "--since");
|
|
1989
|
+
if (raw === undefined)
|
|
1990
|
+
return undefined;
|
|
1991
|
+
const relative = /^(\d+)\s*([dh])$/i.exec(raw.trim());
|
|
1992
|
+
if (relative) {
|
|
1993
|
+
const amount = Number(relative[1]);
|
|
1994
|
+
const hours = relative[2]?.toLowerCase() === "d" ? amount * 24 : amount;
|
|
1995
|
+
return new Date(Date.now() - hours * 3_600_000).toISOString();
|
|
1996
|
+
}
|
|
1997
|
+
const parsed = Date.parse(raw);
|
|
1998
|
+
if (!Number.isFinite(parsed)) {
|
|
1999
|
+
throw new Error(`--since expects a date or an age like 7d or 24h, not "${raw}".`);
|
|
2000
|
+
}
|
|
2001
|
+
return new Date(parsed).toISOString();
|
|
2002
|
+
}
|
|
2003
|
+
/** The ranked report as a table, widest column first so the counts line up. */
|
|
2004
|
+
export function formatBlockerReport(report, options = {}) {
|
|
2005
|
+
const scope = options.scopedToOneRepo ? "this repo" : `${report.roots} bridge root${report.roots === 1 ? "" : "s"}`;
|
|
2006
|
+
const window = options.since ? ` since ${options.since.slice(0, 10)}` : "";
|
|
2007
|
+
if (report.totalConsults === 0) {
|
|
2008
|
+
return `No consults recorded in ${scope}${window}.`;
|
|
2009
|
+
}
|
|
2010
|
+
const share = report.totalConsults > 0 ? Math.round((report.blocked / report.totalConsults) * 100) : 0;
|
|
2011
|
+
const head = `Consult blockers: ${report.blocked} of ${report.totalConsults} consults (${share}%) across ${scope}${window}`;
|
|
2012
|
+
if (report.groups.length === 0)
|
|
2013
|
+
return `${head}\n\nNothing was blocked.`;
|
|
2014
|
+
// A split catch-all carries the first sentence in its own name, so the
|
|
2015
|
+
// example would repeat it; only a bare code needs one.
|
|
2016
|
+
const clip = (text, max) => (text.length > max ? `${text.slice(0, max - 1)}\u2026` : text);
|
|
2017
|
+
const rows = report.groups.map((group) => {
|
|
2018
|
+
const example = group.example.replace(/\s+/g, " ").trim();
|
|
2019
|
+
// Compare through the same normalisation the cause went through: the code
|
|
2020
|
+
// says `has no "..." step` where the message says `has no "Pro" step`.
|
|
2021
|
+
const causePart = group.code.includes(": ") ? group.code.split(": ").slice(1).join(": ") : undefined;
|
|
2022
|
+
const redundant = causePart !== undefined && blockerCause(example) === causePart;
|
|
2023
|
+
return {
|
|
2024
|
+
count: String(group.count),
|
|
2025
|
+
share: `${Math.round(group.share * 100)}%`,
|
|
2026
|
+
code: clip(group.code, 58),
|
|
2027
|
+
lastSeen: group.lastSeen ? group.lastSeen.slice(0, 10) : "-",
|
|
2028
|
+
example: redundant ? "" : clip(example, 58)
|
|
2029
|
+
};
|
|
2030
|
+
});
|
|
2031
|
+
const width = (pick, header) => Math.max(header.length, ...rows.map((row) => pick(row).length));
|
|
2032
|
+
const wCount = width((r) => r.count, "COUNT");
|
|
2033
|
+
const wShare = width((r) => r.share, "SHARE");
|
|
2034
|
+
const wCode = width((r) => r.code, "CAUSE");
|
|
2035
|
+
const wLast = width((r) => r.lastSeen, "LAST SEEN");
|
|
2036
|
+
const line = (count, sharePct, code, lastSeen, example) => ` ${count.padStart(wCount)} ${sharePct.padStart(wShare)} ${code.padEnd(wCode)} ${lastSeen.padEnd(wLast)} ${example}`;
|
|
2037
|
+
const body = rows.map((row) => line(row.count, row.share, row.code, row.lastSeen, row.example).trimEnd());
|
|
2038
|
+
return [head, "", line("COUNT", "SHARE", "CAUSE", "LAST SEEN", "EXAMPLE"), ...body].join("\n");
|
|
2039
|
+
}
|
|
1881
2040
|
export function formatProListSummary(consult, sourceCli, options = {}) {
|
|
1882
2041
|
const blocker = sourceAwareProAnswerBlocker(consult, sourceCli, options);
|
|
1883
2042
|
return firstLine(sourceAwareProAnswerSummary(consult.result.summary, consult.result.blocker, blocker));
|
package/dist/mcp.js
CHANGED
|
@@ -187,7 +187,11 @@ export function createServer(cwd = process.cwd(), options = {}) {
|
|
|
187
187
|
new_chat: z
|
|
188
188
|
.boolean()
|
|
189
189
|
.optional()
|
|
190
|
-
.describe("Start a fresh thread. Omit to continue the current thread (preferred for follow-ups).")
|
|
190
|
+
.describe("Start a fresh thread. Omit to continue the current thread (preferred for follow-ups)."),
|
|
191
|
+
allow_model_fallback: z
|
|
192
|
+
.boolean()
|
|
193
|
+
.optional()
|
|
194
|
+
.describe("Send even when the requested model or effort could not be applied. Off by default, because an answer from a step nobody asked for is usually unusable: a consult asking for Pro that comes back from a lesser model cannot be cited as a Pro review. Pass true only when any answer beats no answer.")
|
|
191
195
|
}
|
|
192
196
|
}, async (input, extra) => {
|
|
193
197
|
// Bridge send progress to MCP progress notifications, but only when
|
package/dist/registry.js
CHANGED
|
@@ -44,6 +44,38 @@ async function directoryExists(dir) {
|
|
|
44
44
|
return false;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Every bridge root this machine knows about. The registry has only ever been
|
|
49
|
+
* written; reading it is what lets a report span the repos where the failures
|
|
50
|
+
* actually are, instead of the one the command happens to run in.
|
|
51
|
+
*
|
|
52
|
+
* Advisory like the rest of the registry: a missing or corrupt file is an
|
|
53
|
+
* empty list, not an error, and roots that have since been deleted are
|
|
54
|
+
* dropped rather than reported.
|
|
55
|
+
*/
|
|
56
|
+
export async function readBridgeRoots() {
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(await fs.readFile(bridgesRegistryPath(), "utf8"));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const raw = parsed?.roots;
|
|
65
|
+
if (!Array.isArray(raw))
|
|
66
|
+
return [];
|
|
67
|
+
const roots = [];
|
|
68
|
+
for (const entry of raw.slice(0, MAX_REGISTRY_ROOTS)) {
|
|
69
|
+
const value = typeof entry === "string" ? entry : entry?.path;
|
|
70
|
+
if (typeof value !== "string" || value.length === 0)
|
|
71
|
+
continue;
|
|
72
|
+
if (!(await directoryExists(path.join(value, ".bridge"))))
|
|
73
|
+
continue;
|
|
74
|
+
if (!roots.includes(value))
|
|
75
|
+
roots.push(value);
|
|
76
|
+
}
|
|
77
|
+
return roots;
|
|
78
|
+
}
|
|
47
79
|
export function registerBridgeRoot(root) {
|
|
48
80
|
const next = registryQueue.then(() => registerBridgeRootInner(root));
|
|
49
81
|
// Keep the chain alive even if an inner registration rejects.
|