@youdie006/prodex 0.36.2 → 0.36.4
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 +85 -25
- package/dist/store.js +38 -9
- package/package.json +1 -1
- package/scripts/release-check.mjs +20 -3
package/dist/chatgpt-browser.js
CHANGED
|
@@ -654,23 +654,38 @@ export function proModeNotAppliedWarning(proMode, sliderEffort) {
|
|
|
654
654
|
"Drop --pro-mode here, or clear a saved default with `prodex setup --clear-pro-mode`.");
|
|
655
655
|
}
|
|
656
656
|
/**
|
|
657
|
-
* Which
|
|
657
|
+
* Which control each request belongs to.
|
|
658
658
|
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
659
|
+
* The picker holds two of them: a `role="slider"` for effort (Instant..Pro) and
|
|
660
|
+
* `menuitemradio` rows for the models. Selection used to send both through the
|
|
661
|
+
* slider (`effort ?? model`), so a model name walked the slider looking for a
|
|
662
|
+
* step it could never be - and the refusal quoted the slider's TEXT, which
|
|
663
|
+
* contains the model names, so the message read as "it is right there in the
|
|
664
|
+
* list". `--model Pro` only ever worked when the slider happened to sit on its
|
|
665
|
+
* top step; once it moved to Instant the same command failed.
|
|
666
|
+
*
|
|
667
|
+
* Pro stopped being a model and became that top step, but it is the pinned
|
|
668
|
+
* default in every repo configured before the change, so it is honoured where
|
|
669
|
+
* it now lives rather than failing every one of them - out loud.
|
|
665
670
|
*/
|
|
666
|
-
export function
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
671
|
+
export function pickerSelectionPlan(input) {
|
|
672
|
+
const plan = {};
|
|
673
|
+
const modelIsNowAnEffort = input.model !== undefined && /^\s*pro\s*$/i.test(input.model);
|
|
674
|
+
if (input.effort !== undefined)
|
|
675
|
+
plan.sliderLabel = input.effort;
|
|
676
|
+
else if (modelIsNowAnEffort)
|
|
677
|
+
plan.sliderLabel = input.model;
|
|
678
|
+
if (input.model !== undefined && !modelIsNowAnEffort)
|
|
679
|
+
plan.modelLabel = input.model;
|
|
680
|
+
if (modelIsNowAnEffort) {
|
|
681
|
+
plan.warning =
|
|
682
|
+
`model_is_now_an_effort: ChatGPT's picker lists Pro as an effort step, not a model, so --model ${input.model} was ` +
|
|
683
|
+
(input.effort === undefined
|
|
684
|
+
? "applied to the effort slider instead. Say --effort Pro to be explicit"
|
|
685
|
+
: `ignored in favour of --effort ${input.effort}`) +
|
|
686
|
+
", or clear a saved default with `prodex setup --model \"\"`.";
|
|
687
|
+
}
|
|
688
|
+
return plan;
|
|
674
689
|
}
|
|
675
690
|
export function chatGptBusyBlocker(state) {
|
|
676
691
|
// Waiting for a choice is not generating. Conflating them turned a click
|
|
@@ -1544,6 +1559,17 @@ const POWER_SLIDER_APPEAR_ATTEMPTS = 5;
|
|
|
1544
1559
|
* menu must already be open. Returns the quota line so the caller can warn
|
|
1545
1560
|
* when Pro runs are nearly spent.
|
|
1546
1561
|
*/
|
|
1562
|
+
// Click a model row. They are ordinary `menuitemradio` entries in the same
|
|
1563
|
+
// menu as the effort slider, so the existing by-label lookup reaches them; what
|
|
1564
|
+
// was missing is anything routing a model here instead of into the slider.
|
|
1565
|
+
async function selectPickerModel(cdp, requested) {
|
|
1566
|
+
const hit = await cdp.evaluate(menuItemRectExpression(requested));
|
|
1567
|
+
if (!hit.ok || hit.x === undefined || hit.y === undefined) {
|
|
1568
|
+
const available = hit.available?.length ? ` The picker offers: ${hit.available.join(", ")}.` : "";
|
|
1569
|
+
throw new Error(`ChatGPT's model picker has no "${requested}" model.${available}`);
|
|
1570
|
+
}
|
|
1571
|
+
await verifiedClickWithRetry(cdp, () => cdp.evaluate(menuItemRectExpression(requested)), requested);
|
|
1572
|
+
}
|
|
1547
1573
|
async function selectPowerStep(cdp, requested) {
|
|
1548
1574
|
// The menu paints a moment after it opens, and on a page that has just been
|
|
1549
1575
|
// built - a project created seconds ago - that moment is longer. Failing the
|
|
@@ -1650,18 +1676,26 @@ async function selectModelReasoning(cdp, options, selectionWarnings = []) {
|
|
|
1650
1676
|
// it is not, so both UI generations work.
|
|
1651
1677
|
const sliderState = await cdp.evaluate(powerSliderStateExpression());
|
|
1652
1678
|
if (sliderState?.ok) {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1679
|
+
// The slider is the EFFORT control and the models are radios beside it.
|
|
1680
|
+
// Sending a model name into the slider made it walk every step looking
|
|
1681
|
+
// for one it could never be.
|
|
1682
|
+
const plan = pickerSelectionPlan({
|
|
1683
|
+
...(options.model !== undefined ? { model: options.model } : {}),
|
|
1684
|
+
...(options.effort !== undefined ? { effort: options.effort } : {})
|
|
1685
|
+
});
|
|
1686
|
+
if (plan.warning)
|
|
1687
|
+
selectionWarnings.push(plan.warning);
|
|
1688
|
+
if (plan.sliderLabel) {
|
|
1689
|
+
await selectPowerStep(cdp, plan.sliderLabel);
|
|
1690
|
+
}
|
|
1691
|
+
if (plan.modelLabel) {
|
|
1692
|
+
await selectPickerModel(cdp, plan.modelLabel);
|
|
1656
1693
|
}
|
|
1657
1694
|
// This branch cannot honour a sub-mode, and used to return without
|
|
1658
1695
|
// saying so - the caller got a clean receipt for a 확장 it never got.
|
|
1659
1696
|
const proModeWarning = proModeNotAppliedWarning(options.proMode, sliderState.effort ?? undefined);
|
|
1660
1697
|
if (proModeWarning)
|
|
1661
1698
|
selectionWarnings.push(proModeWarning);
|
|
1662
|
-
const modelWarning = modelIgnoredForEffortWarning(options.model, options.effort);
|
|
1663
|
-
if (modelWarning)
|
|
1664
|
-
selectionWarnings.push(modelWarning);
|
|
1665
1699
|
await dispatchEscapeKey(cdp);
|
|
1666
1700
|
return;
|
|
1667
1701
|
}
|
|
@@ -3954,6 +3988,30 @@ export function transcriptMatchesSentPrompt(userText, sentPrompt) {
|
|
|
3954
3988
|
// separates, U+E201 closes) and keeps the real sources in content_references.
|
|
3955
3989
|
const CITATION_DELIMITER_PATTERN = /[\uE200-\uE206]/;
|
|
3956
3990
|
const CITATION_MARKER_PATTERN = /\uE200[^\uE200-\uE206]*(?:[\uE202\uE204-\uE206][^\uE200-\uE206]*)*[\uE201\uE203]/g;
|
|
3991
|
+
/**
|
|
3992
|
+
* The words a citation marker was wrapping, if any.
|
|
3993
|
+
*
|
|
3994
|
+
* The markers come in two shapes. `<E200>cite<E202>turn0search0<E201>` is a
|
|
3995
|
+
* bare anchor - kind then reference id, nothing a reader would miss. But
|
|
3996
|
+
* `<E200>url<E202>Timeanddate.com — World Clock<E202>turn0search0<E201>` puts a
|
|
3997
|
+
* visible title in the middle, and that title is part of the sentence. Caught by
|
|
3998
|
+
* diffing a saved answer against its thread: a web-search reply ended at a
|
|
3999
|
+
* dangling "Source:" because the title had been deleted along with the marker.
|
|
4000
|
+
*/
|
|
4001
|
+
export function citationMarkerText(marker) {
|
|
4002
|
+
// The first segment is the kind and the rest are a mix of reference tokens and,
|
|
4003
|
+
// for some kinds, prose. Sorting them by SHAPE rather than by kind keeps a
|
|
4004
|
+
// title from a kind not seen yet: `cite` can carry several ids
|
|
4005
|
+
// (`cite<E202>turn0search19<E202>turn0search5`), and dropping every
|
|
4006
|
+
// id-shaped segment leaves nothing there, which is right.
|
|
4007
|
+
const referenceToken = /^turn\d+[a-z]+\d+$/i;
|
|
4008
|
+
const segments = marker.replace(/^[\uE200-\uE206]|[\uE200-\uE206]$/g, "").split(/[\uE200-\uE206]/);
|
|
4009
|
+
return segments
|
|
4010
|
+
.slice(1)
|
|
4011
|
+
.filter((segment) => segment.length > 0 && !referenceToken.test(segment))
|
|
4012
|
+
.join(" ")
|
|
4013
|
+
.trim();
|
|
4014
|
+
}
|
|
3957
4015
|
/**
|
|
3958
4016
|
* Turn those markers into ordinary markdown links, so a saved answer keeps the
|
|
3959
4017
|
* sources instead of the private-use noise (or, as in the rendered DOM, nothing
|
|
@@ -3984,11 +4042,13 @@ export function resolveTranscriptCitations(text, references = []) {
|
|
|
3984
4042
|
};
|
|
3985
4043
|
let resolved = text;
|
|
3986
4044
|
for (const [marker, reference] of byMarker) {
|
|
3987
|
-
|
|
4045
|
+
// No items to link is not permission to delete the sentence: fall back to
|
|
4046
|
+
// whatever the marker was wrapping.
|
|
4047
|
+
resolved = resolved.split(marker).join(linksFor(reference) || citationMarkerText(marker));
|
|
3988
4048
|
}
|
|
3989
|
-
// Anything still delimited had no reference to restore
|
|
3990
|
-
//
|
|
3991
|
-
return resolved.replace(CITATION_MARKER_PATTERN,
|
|
4049
|
+
// Anything still delimited had no reference to restore. The delimiters must not
|
|
4050
|
+
// reach a receipt, but the words between them still belong to the answer.
|
|
4051
|
+
return resolved.replace(CITATION_MARKER_PATTERN, (marker) => citationMarkerText(marker));
|
|
3992
4052
|
}
|
|
3993
4053
|
export function deepResearchReportExpression(conversationId) {
|
|
3994
4054
|
return `(async () => {
|
package/dist/store.js
CHANGED
|
@@ -55,6 +55,10 @@ export async function assertUsableBridgeRoot(root) {
|
|
|
55
55
|
return;
|
|
56
56
|
throw new Error(`Bridge root is not a usable repo directory: ${root}${looksLikeDevicePath ? " (that is a file-descriptor/device path, not a repo)" : ""}. prodex uses the process working directory when no --cwd is given, so a server started from a pipe or a deleted directory lands here. Pass --cwd /absolute/path/to/repo, or set PRODEX_CWD=/absolute/path/to/repo (works for the MCP server, which takes no flags).`);
|
|
57
57
|
}
|
|
58
|
+
// How many record files to read at once. High enough to hide per-file latency
|
|
59
|
+
// on a slow mount, low enough that a big store cannot run the process out of
|
|
60
|
+
// file descriptors.
|
|
61
|
+
const READ_ALL_CONCURRENCY = 32;
|
|
58
62
|
export class BridgeStore {
|
|
59
63
|
root;
|
|
60
64
|
bridgeDir;
|
|
@@ -878,15 +882,40 @@ export class BridgeStore {
|
|
|
878
882
|
async readAll(kind, parseRecord, options = {}) {
|
|
879
883
|
const dir = this.dir(kind);
|
|
880
884
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
885
|
+
const ids = entries
|
|
886
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
887
|
+
.map((entry) => entry.name.replace(/\.json$/, ""))
|
|
888
|
+
.filter((id) => isBridgeRecordId(kind, id));
|
|
889
|
+
// Read them together. The records are independent, but this loop used to
|
|
890
|
+
// await each one, so the cost was the file count times the filesystem's
|
|
891
|
+
// latency: measured on a Windows mount at ~24ms a file, 505 receipts took
|
|
892
|
+
// 12s, and `pro latest` paid it on every call because verifying ONE result
|
|
893
|
+
// reads every receipt. Bounded so a large store cannot exhaust the fd table.
|
|
894
|
+
const items = new Array(ids.length);
|
|
895
|
+
// The sequential loop threw on the FIRST bad record and never reached the
|
|
896
|
+
// rest, so that is the error callers were told about. Keep reporting the
|
|
897
|
+
// earliest one rather than whichever read happens to lose the race.
|
|
898
|
+
let failure;
|
|
899
|
+
let next = 0;
|
|
900
|
+
const readOne = async () => {
|
|
901
|
+
for (;;) {
|
|
902
|
+
const index = next;
|
|
903
|
+
next += 1;
|
|
904
|
+
if (index >= ids.length)
|
|
905
|
+
return;
|
|
906
|
+
const id = ids[index];
|
|
907
|
+
try {
|
|
908
|
+
items[index] = this.parseRecord(kind, id, await this.readRecordJson(kind, id, options), parseRecord);
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
if (!failure || index < failure.index)
|
|
912
|
+
failure = { index, error };
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
await Promise.all(Array.from({ length: Math.min(READ_ALL_CONCURRENCY, ids.length) }, readOne));
|
|
917
|
+
if (failure)
|
|
918
|
+
throw failure.error;
|
|
890
919
|
return items;
|
|
891
920
|
}
|
|
892
921
|
parseRecord(kind, id, value, parseRecord) {
|
package/package.json
CHANGED
|
@@ -133,7 +133,23 @@ async function readRequiredPackageJson(packageJsonPath) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// Budgets sized from what each check actually does, not from one number for all
|
|
137
|
+
// of them. Measured on a developer machine whose repo sits on a 9p mount:
|
|
138
|
+
// smoke:package takes 244s because it packs, installs and drives the package
|
|
139
|
+
// end to end, and the suite takes ~50s. A single 180s cap killed smoke:package
|
|
140
|
+
// mid-run and reported "release verification failed" with only npm's banner as
|
|
141
|
+
// evidence, because the process died before it printed anything - while running
|
|
142
|
+
// the same command directly passed every check. CI runs this same path before
|
|
143
|
+
// publishing, so the cap was a thin margin in the release path too.
|
|
136
144
|
async function runFullReleaseVerification(rootDir) {
|
|
145
|
+
// Declared here, not at module scope: this file runs its entry point during
|
|
146
|
+
// module evaluation, so a top-level const is still in its temporal dead zone
|
|
147
|
+
// by the time this is called.
|
|
148
|
+
const CHECK_TIMEOUT_MS = {
|
|
149
|
+
default: 300_000,
|
|
150
|
+
"npm test": 900_000,
|
|
151
|
+
"npm run smoke:package": 1_800_000
|
|
152
|
+
};
|
|
137
153
|
const checks = [
|
|
138
154
|
["npm", ["test"]],
|
|
139
155
|
["npm", ["run", "typecheck"]],
|
|
@@ -142,7 +158,8 @@ async function runFullReleaseVerification(rootDir) {
|
|
|
142
158
|
["node", ["dist/cli.js", "doctor"]]
|
|
143
159
|
];
|
|
144
160
|
for (const [command, commandArgs] of checks) {
|
|
145
|
-
|
|
161
|
+
const key = [command, ...commandArgs].join(" ");
|
|
162
|
+
await run(commandForPlatform(command), commandArgs, rootDir, CHECK_TIMEOUT_MS[key] ?? CHECK_TIMEOUT_MS.default);
|
|
146
163
|
}
|
|
147
164
|
}
|
|
148
165
|
|
|
@@ -328,13 +345,13 @@ function formatPathList(paths) {
|
|
|
328
345
|
return paths.length > 8 ? `${shown}, ... (${paths.length} files)` : shown;
|
|
329
346
|
}
|
|
330
347
|
|
|
331
|
-
async function run(command, commandArgs, cwd) {
|
|
348
|
+
async function run(command, commandArgs, cwd, timeoutMs = 300_000) {
|
|
332
349
|
const commandLine = [command, ...commandArgs].join(" ");
|
|
333
350
|
console.log(`release_check: ${commandLine}`);
|
|
334
351
|
try {
|
|
335
352
|
await execFileAsync(command, commandArgs, {
|
|
336
353
|
cwd,
|
|
337
|
-
timeout:
|
|
354
|
+
timeout: timeoutMs,
|
|
338
355
|
maxBuffer: 20 * 1024 * 1024
|
|
339
356
|
});
|
|
340
357
|
} catch (error) {
|