pi-oracle 0.6.16 → 0.7.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/CHANGELOG.md +30 -0
- package/README.md +197 -127
- package/docs/ORACLE_DESIGN.md +29 -26
- package/extensions/oracle/lib/auth.ts +4 -3
- package/extensions/oracle/lib/commands.ts +15 -4
- package/extensions/oracle/lib/config.ts +50 -2
- package/extensions/oracle/lib/jobs.ts +1 -1
- package/extensions/oracle/lib/runtime.ts +10 -0
- package/extensions/oracle/lib/tools.ts +123 -34
- package/extensions/oracle/shared/job-observability-helpers.d.mts +8 -0
- package/extensions/oracle/shared/job-observability-helpers.mjs +19 -0
- package/extensions/oracle/worker/auth-bootstrap.mjs +63 -15
- package/extensions/oracle/worker/auth-cookie-policy.mjs +18 -4
- package/extensions/oracle/worker/auth-flow-helpers.mjs +20 -5
- package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +2 -2
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +17 -1
- package/extensions/oracle/worker/chromium-cookie-source.mjs +2 -2
- package/extensions/oracle/worker/run-job.mjs +260 -40
- package/package.json +3 -2
- package/prompts/oracle-followup.md +21 -19
- package/prompts/oracle.md +11 -9
|
@@ -54,6 +54,13 @@ const CHATGPT_LABELS = {
|
|
|
54
54
|
autoSwitchToThinking: "Auto-switch to Thinking",
|
|
55
55
|
configure: "Configure...",
|
|
56
56
|
};
|
|
57
|
+
const GROK_LABELS = {
|
|
58
|
+
composer: "Ask Grok anything",
|
|
59
|
+
addFiles: "Attach",
|
|
60
|
+
send: "Submit",
|
|
61
|
+
modelSelect: "Model select",
|
|
62
|
+
stop: "Stop model response",
|
|
63
|
+
};
|
|
57
64
|
const WORKER_SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
58
65
|
const DEFAULT_ORACLE_STATE_DIR = "/tmp/pi-oracle-state";
|
|
59
66
|
const ORACLE_STATE_DIR = process.env.PI_ORACLE_STATE_DIR?.trim() || DEFAULT_ORACLE_STATE_DIR;
|
|
@@ -81,6 +88,18 @@ let cleaningUpRuntime = false;
|
|
|
81
88
|
let shuttingDown = false;
|
|
82
89
|
let lastHeartbeatMs = 0;
|
|
83
90
|
|
|
91
|
+
function providerForJob(job) {
|
|
92
|
+
return job?.selection?.provider === "grok" ? "grok" : "chatgpt";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isGrokJob(job) {
|
|
96
|
+
return providerForJob(job) === "grok";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function labelsForJob(job) {
|
|
100
|
+
return isGrokJob(job) ? GROK_LABELS : CHATGPT_LABELS;
|
|
101
|
+
}
|
|
102
|
+
|
|
84
103
|
async function ensurePrivateDir(path) {
|
|
85
104
|
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
86
105
|
await chmod(path, 0o700).catch(() => undefined);
|
|
@@ -239,13 +258,22 @@ function parseConversationId(chatUrl) {
|
|
|
239
258
|
if (!chatUrl) return undefined;
|
|
240
259
|
try {
|
|
241
260
|
const parsed = new URL(chatUrl);
|
|
242
|
-
const match = parsed.pathname.match(/\/c\/([^/?#]+)/i);
|
|
261
|
+
const match = parsed.pathname.match(/\/(?:c|chat)\/([^/?#]+)/i);
|
|
243
262
|
return match?.[1];
|
|
244
263
|
} catch {
|
|
245
264
|
return undefined;
|
|
246
265
|
}
|
|
247
266
|
}
|
|
248
267
|
|
|
268
|
+
async function removeChromiumProcessSingletonArtifacts(profileDir) {
|
|
269
|
+
await Promise.all([
|
|
270
|
+
rm(join(profileDir, "SingletonLock"), { force: true }),
|
|
271
|
+
rm(join(profileDir, "SingletonSocket"), { force: true }),
|
|
272
|
+
rm(join(profileDir, "SingletonCookie"), { force: true }),
|
|
273
|
+
rm(join(profileDir, "DevToolsActivePort"), { force: true }),
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
|
|
249
277
|
async function cloneSeedProfileToRuntime(job) {
|
|
250
278
|
const seedDir = job.config.browser.authSeedProfileDir;
|
|
251
279
|
if (!existsSync(seedDir)) {
|
|
@@ -270,6 +298,7 @@ async function cloneSeedProfileToRuntime(job) {
|
|
|
270
298
|
} else {
|
|
271
299
|
await spawnCommand("/bin/cp", ["-R", seedDir, job.runtimeProfileDir], { timeoutMs: PROFILE_CLONE_TIMEOUT_MS });
|
|
272
300
|
}
|
|
301
|
+
await removeChromiumProcessSingletonArtifacts(job.runtimeProfileDir);
|
|
273
302
|
}, 10 * 60 * 1000);
|
|
274
303
|
|
|
275
304
|
return seedGeneration;
|
|
@@ -705,13 +734,22 @@ function matchesModelConfigurationOpener(candidate) {
|
|
|
705
734
|
|| /^(?:(?:Light|Standard|Extended|Heavy) )?Pro(?:, click to remove)?$/i.test(label);
|
|
706
735
|
}
|
|
707
736
|
|
|
708
|
-
function
|
|
737
|
+
function canUseOpenModelMenuForSelection(snapshot, selection) {
|
|
738
|
+
if (selection.modelFamily !== "instant" || selection.autoSwitchToThinking === true) return false;
|
|
739
|
+
return Boolean(findEntry(
|
|
740
|
+
snapshot,
|
|
741
|
+
(candidate) => candidate.kind === "menuitemradio" && matchesModelFamilyControl(candidate, selection.modelFamily),
|
|
742
|
+
));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function composerControlsVisible(snapshot, job = currentJob) {
|
|
746
|
+
const labels = labelsForJob(job);
|
|
709
747
|
const entries = parseSnapshotEntries(snapshot);
|
|
710
|
-
const hasComposer =
|
|
711
|
-
(entry) => entry.kind === "textbox" && entry.label ===
|
|
712
|
-
|
|
748
|
+
const hasComposer = isGrokJob(job)
|
|
749
|
+
? entries.some((entry) => !entry.disabled && ((entry.kind === "textbox" && entry.label === labels.composer) || /editable/.test(String(entry.line || ""))))
|
|
750
|
+
: entries.some((entry) => entry.kind === "textbox" && entry.label === labels.composer && !entry.disabled);
|
|
713
751
|
const hasAddFiles = entries.some(
|
|
714
|
-
(entry) => entry.kind === "button" && entry.label ===
|
|
752
|
+
(entry) => entry.kind === "button" && entry.label === labels.addFiles && !entry.disabled,
|
|
715
753
|
);
|
|
716
754
|
return hasComposer && hasAddFiles;
|
|
717
755
|
}
|
|
@@ -766,13 +804,28 @@ async function openEffortDropdown(job) {
|
|
|
766
804
|
}
|
|
767
805
|
|
|
768
806
|
async function setComposerText(job, text) {
|
|
807
|
+
if (isGrokJob(job)) {
|
|
808
|
+
const result = await evalPage(job, toJsonScript(`
|
|
809
|
+
const el = document.querySelector('[contenteditable="true"], [contenteditable=true]');
|
|
810
|
+
if (!el) return { ok: false };
|
|
811
|
+
el.focus();
|
|
812
|
+
document.execCommand('selectAll', false, null);
|
|
813
|
+
document.execCommand('insertText', false, ${JSON.stringify(text)});
|
|
814
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: ${JSON.stringify(text)} }));
|
|
815
|
+
return { ok: true };
|
|
816
|
+
`));
|
|
817
|
+
if (!result?.ok) throw new Error("Could not find Grok composer textbox");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
769
820
|
const snapshot = await snapshotText(job);
|
|
770
|
-
const
|
|
821
|
+
const labels = labelsForJob(job);
|
|
822
|
+
const entry = findEntry(snapshot, (candidate) => candidate.kind === "textbox" && candidate.label === labels.composer);
|
|
771
823
|
if (!entry) throw new Error("Could not find ChatGPT composer textbox");
|
|
772
824
|
await agentBrowser(job, "fill", entry.ref, text);
|
|
773
825
|
}
|
|
774
826
|
|
|
775
827
|
function classifyChatPage({ job, url, snapshot, body, probe }) {
|
|
828
|
+
if (isGrokJob(job)) return classifyGrokPage({ url, snapshot, body });
|
|
776
829
|
const text = `${snapshot}\n${body}`;
|
|
777
830
|
const challengePatterns = [
|
|
778
831
|
/just a moment/i,
|
|
@@ -783,6 +836,9 @@ function classifyChatPage({ job, url, snapshot, body, probe }) {
|
|
|
783
836
|
/we detect suspicious activity/i,
|
|
784
837
|
];
|
|
785
838
|
if (challengePatterns.some((pattern) => pattern.test(text))) {
|
|
839
|
+
if (/verification successful|waiting for chatgpt\.com to respond/i.test(text)) {
|
|
840
|
+
return { state: "unknown", message: "ChatGPT verification is still settling." };
|
|
841
|
+
}
|
|
786
842
|
return { state: "challenge_blocking", message: "ChatGPT is showing a challenge/verification page" };
|
|
787
843
|
}
|
|
788
844
|
|
|
@@ -802,25 +858,35 @@ function classifyChatPage({ job, url, snapshot, body, probe }) {
|
|
|
802
858
|
const onAuthPath = typeof url === "string" && url.includes("/auth/");
|
|
803
859
|
const hasUsableComposer = snapshotHasUsableComposerControls(snapshot);
|
|
804
860
|
|
|
805
|
-
|
|
861
|
+
const probeHasAccountIdentity = probe?.bodyHasId === true || probe?.bodyHasEmail === true;
|
|
862
|
+
|
|
863
|
+
if (probe?.status === 401 || (probe?.status === 403 && (!onAllowedOrigin || !hasUsableComposer))) {
|
|
806
864
|
return { state: "login_required", message: "ChatGPT login is required. Run /oracle-auth." };
|
|
807
865
|
}
|
|
808
866
|
|
|
809
867
|
if (onAuthPath || probe?.onAuthPage) {
|
|
810
|
-
if (
|
|
868
|
+
if (probeHasAccountIdentity) {
|
|
811
869
|
return {
|
|
812
870
|
state: "auth_transitioning",
|
|
813
|
-
message: "ChatGPT is on an auth page even though the backend
|
|
871
|
+
message: "ChatGPT is on an auth page even though the backend probe returned account-like fields. Rerun /oracle-auth.",
|
|
814
872
|
};
|
|
815
873
|
}
|
|
816
874
|
return { state: "login_required", message: "ChatGPT login is required. Run /oracle-auth." };
|
|
817
875
|
}
|
|
818
876
|
|
|
877
|
+
if (onAllowedOrigin && hasUsableComposer && probe?.domLoginCta && !probeHasAccountIdentity) {
|
|
878
|
+
return {
|
|
879
|
+
state: "login_required",
|
|
880
|
+
message: "ChatGPT login is required: the chat shell still shows public Log in/Sign up controls. Run /oracle-auth.",
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
819
884
|
if (onAllowedOrigin && (probe?.status === 200 || probe?.status === 403) && hasUsableComposer) {
|
|
820
|
-
if (probe?.domLoginCta
|
|
885
|
+
if (probe?.domLoginCta) {
|
|
886
|
+
// The public logged-out composer case returned above, so a remaining visible login CTA here still has account-like probe data.
|
|
821
887
|
return {
|
|
822
888
|
state: "auth_transitioning",
|
|
823
|
-
message: "ChatGPT backend
|
|
889
|
+
message: "ChatGPT backend probe returned account-like fields, but the web shell still shows public login controls. Rerun /oracle-auth.",
|
|
824
890
|
};
|
|
825
891
|
}
|
|
826
892
|
return { state: "authenticated_and_ready", message: "ChatGPT is authenticated and ready." };
|
|
@@ -833,6 +899,33 @@ function classifyChatPage({ job, url, snapshot, body, probe }) {
|
|
|
833
899
|
return { state: "unknown", message: "ChatGPT page is not ready yet." };
|
|
834
900
|
}
|
|
835
901
|
|
|
902
|
+
function hasGrokLoginCta(text) {
|
|
903
|
+
const lines = String(text || "").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
904
|
+
return lines.some((line) => {
|
|
905
|
+
const accessibleControl = line.match(/^-\s*(?:button|link|menuitem)\s+"([^"]+)"/i)?.[1]?.trim();
|
|
906
|
+
const label = accessibleControl || line;
|
|
907
|
+
return /^(?:sign in|log in|continue with x|continue with google|create account)$/i.test(label);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function classifyGrokPage({ url, snapshot, body }) {
|
|
912
|
+
const text = `${snapshot}\n${body}`;
|
|
913
|
+
if (/captcha|cloudflare|verify you are human|unusual activity|suspicious activity/i.test(text)) {
|
|
914
|
+
return { state: "challenge_blocking", message: "Grok is showing a challenge/verification page" };
|
|
915
|
+
}
|
|
916
|
+
if (/something went wrong|network error|try again later|rate limit/i.test(text)) {
|
|
917
|
+
return { state: "transient_outage_error", message: "Grok is showing a transient outage/error page" };
|
|
918
|
+
}
|
|
919
|
+
const onGrokOrigin = typeof url === "string" && url.startsWith("https://grok.com");
|
|
920
|
+
if (onGrokOrigin && hasGrokLoginCta(text)) {
|
|
921
|
+
return { state: "login_required", message: "Grok login is required. Sign in to Grok in the configured browser profile and rerun /oracle-auth grok." };
|
|
922
|
+
}
|
|
923
|
+
const hasComposer = snapshot.includes(`button "${GROK_LABELS.addFiles}"`) && (snapshot.includes(`textbox "${GROK_LABELS.composer}"`) || snapshot.includes("contenteditable"));
|
|
924
|
+
if (onGrokOrigin && hasComposer) return { state: "authenticated_and_ready", message: "Grok is ready." };
|
|
925
|
+
if (url && !onGrokOrigin) return { state: "login_required", message: "Grok redirected away from grok.com. Sign in to Grok in the configured browser profile and rerun /oracle-auth grok if needed." };
|
|
926
|
+
return { state: "unknown", message: "Grok page is not ready yet." };
|
|
927
|
+
}
|
|
928
|
+
|
|
836
929
|
async function captureDiagnostics(job, reason) {
|
|
837
930
|
if (!browserStarted) return;
|
|
838
931
|
try {
|
|
@@ -852,7 +945,7 @@ async function captureDiagnostics(job, reason) {
|
|
|
852
945
|
|
|
853
946
|
async function waitForOracleReady(job) {
|
|
854
947
|
const startedAt = Date.now();
|
|
855
|
-
const timeoutAt = startedAt + 30_000;
|
|
948
|
+
const timeoutAt = startedAt + (isGrokJob(job) ? 30_000 : Math.min(job.config.auth.bootstrapTimeoutMs || 120_000, 120_000));
|
|
856
949
|
let retriedOutage = false;
|
|
857
950
|
let retriedAuthTransition = false;
|
|
858
951
|
|
|
@@ -875,7 +968,7 @@ async function waitForOracleReady(job) {
|
|
|
875
968
|
}
|
|
876
969
|
if (elapsedMs >= 15_000) {
|
|
877
970
|
await captureDiagnostics(job, "preflight-auth-transition");
|
|
878
|
-
throw new Error("ChatGPT
|
|
971
|
+
throw new Error(classification.message || "ChatGPT auth did not settle into a ready chat shell. Rerun /oracle-auth.");
|
|
879
972
|
}
|
|
880
973
|
await sleep(1000);
|
|
881
974
|
continue;
|
|
@@ -919,11 +1012,12 @@ function detectResponseFailureText(text) {
|
|
|
919
1012
|
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
920
1013
|
}
|
|
921
1014
|
|
|
922
|
-
function composerSnapshotSlice(snapshot) {
|
|
1015
|
+
function composerSnapshotSlice(snapshot, job = currentJob) {
|
|
923
1016
|
const lines = snapshot.split("\n");
|
|
1017
|
+
const labels = labelsForJob(job);
|
|
924
1018
|
let composerIndex = -1;
|
|
925
1019
|
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
926
|
-
if (lines[index].includes(`textbox "${
|
|
1020
|
+
if (lines[index].includes(`textbox "${labels.composer}"`) || (isGrokJob(job) && lines[index].includes("contenteditable"))) {
|
|
927
1021
|
composerIndex = index;
|
|
928
1022
|
break;
|
|
929
1023
|
}
|
|
@@ -934,8 +1028,8 @@ function composerSnapshotSlice(snapshot) {
|
|
|
934
1028
|
return lines.slice(startIndex, endIndex).join("\n");
|
|
935
1029
|
}
|
|
936
1030
|
|
|
937
|
-
function composerFileEntryCount(snapshot, fileLabel) {
|
|
938
|
-
const composerSlice = composerSnapshotSlice(snapshot);
|
|
1031
|
+
function composerFileEntryCount(snapshot, fileLabel, job = currentJob) {
|
|
1032
|
+
const composerSlice = composerSnapshotSlice(snapshot, job);
|
|
939
1033
|
return parseSnapshotEntries(composerSlice).filter((candidate) => candidate.label === fileLabel).length;
|
|
940
1034
|
}
|
|
941
1035
|
|
|
@@ -952,13 +1046,16 @@ async function waitForUploadConfirmed(job, fileLabel, baselineCount) {
|
|
|
952
1046
|
throw new Error(`Upload error detected: ${errorText}`);
|
|
953
1047
|
}
|
|
954
1048
|
|
|
1049
|
+
const labels = labelsForJob(job);
|
|
955
1050
|
const sendEntry = findEntry(
|
|
956
1051
|
snapshot,
|
|
957
|
-
(candidate) => candidate.kind === "button" && candidate.label ===
|
|
1052
|
+
(candidate) => candidate.kind === "button" && candidate.label === labels.send && !candidate.disabled,
|
|
958
1053
|
);
|
|
959
|
-
const fileCount =
|
|
1054
|
+
const fileCount = isGrokJob(job) && snapshot.includes(fileLabel)
|
|
1055
|
+
? baselineCount + 1
|
|
1056
|
+
: composerFileEntryCount(snapshot, fileLabel, job);
|
|
960
1057
|
|
|
961
|
-
if (sendEntry && fileCount > baselineCount) {
|
|
1058
|
+
if ((sendEntry || isGrokJob(job)) && fileCount > baselineCount) {
|
|
962
1059
|
stableCount += 1;
|
|
963
1060
|
if (stableCount >= 2) return sendEntry;
|
|
964
1061
|
} else {
|
|
@@ -982,18 +1079,28 @@ async function waitForSendReady(job) {
|
|
|
982
1079
|
throw new Error(`Upload error detected: ${errorText}`);
|
|
983
1080
|
}
|
|
984
1081
|
|
|
1082
|
+
const labels = labelsForJob(job);
|
|
985
1083
|
const entry = findEntry(
|
|
986
1084
|
snapshot,
|
|
987
|
-
(candidate) => candidate.kind === "button" && candidate.label ===
|
|
1085
|
+
(candidate) => candidate.kind === "button" && candidate.label === labels.send && !candidate.disabled,
|
|
988
1086
|
);
|
|
989
1087
|
if (entry) return entry;
|
|
990
1088
|
await sleep(1000);
|
|
991
1089
|
}
|
|
992
|
-
throw new Error(`Timed out waiting for ${
|
|
1090
|
+
throw new Error(`Timed out waiting for ${labelsForJob(job).send} to become enabled`);
|
|
993
1091
|
}
|
|
994
1092
|
|
|
995
1093
|
async function clickSend(job) {
|
|
996
1094
|
const entry = await waitForSendReady(job);
|
|
1095
|
+
if (isGrokJob(job)) {
|
|
1096
|
+
const result = await evalPage(job, toJsonScript(`
|
|
1097
|
+
const button = document.querySelector('button[aria-label="Submit"]');
|
|
1098
|
+
if (!button || button.disabled) return { ok: false };
|
|
1099
|
+
button.click();
|
|
1100
|
+
return { ok: true };
|
|
1101
|
+
`));
|
|
1102
|
+
if (result?.ok) return;
|
|
1103
|
+
}
|
|
997
1104
|
await clickRef(job, entry.ref);
|
|
998
1105
|
}
|
|
999
1106
|
|
|
@@ -1009,6 +1116,7 @@ async function openModelConfiguration(job) {
|
|
|
1009
1116
|
await agentBrowser(job, "wait", "800");
|
|
1010
1117
|
const after = await snapshotText(job);
|
|
1011
1118
|
if (snapshotHasModelConfigurationUi(after)) return after;
|
|
1119
|
+
if (canUseOpenModelMenuForSelection(after, job.selection)) return after;
|
|
1012
1120
|
|
|
1013
1121
|
const configureEntry = findEntry(
|
|
1014
1122
|
after,
|
|
@@ -1020,6 +1128,7 @@ async function openModelConfiguration(job) {
|
|
|
1020
1128
|
await agentBrowser(job, "wait", "1200");
|
|
1021
1129
|
const postConfigure = await snapshotText(job);
|
|
1022
1130
|
if (snapshotHasModelConfigurationUi(postConfigure)) return postConfigure;
|
|
1131
|
+
if (canUseOpenModelMenuForSelection(postConfigure, job.selection)) return postConfigure;
|
|
1023
1132
|
}
|
|
1024
1133
|
}
|
|
1025
1134
|
|
|
@@ -1075,6 +1184,7 @@ async function waitForModelConfigurationToSettle(job, options = {}) {
|
|
|
1075
1184
|
}
|
|
1076
1185
|
|
|
1077
1186
|
async function configureModel(job) {
|
|
1187
|
+
if (isGrokJob(job)) return configureGrokModel(job);
|
|
1078
1188
|
const initialSnapshot = await snapshotText(job);
|
|
1079
1189
|
if (snapshotCanSafelySkipModelConfiguration(initialSnapshot, job.selection)) {
|
|
1080
1190
|
await log(`Model already appears configured for family=${job.selection.modelFamily} effort=${job.selection?.effort || "(none)"}; skipping reconfiguration`);
|
|
@@ -1149,6 +1259,30 @@ async function configureModel(job) {
|
|
|
1149
1259
|
await waitForModelConfigurationToSettle(job, { stronglyVerified });
|
|
1150
1260
|
}
|
|
1151
1261
|
|
|
1262
|
+
async function configureGrokModel(job) {
|
|
1263
|
+
const snapshot = await snapshotText(job);
|
|
1264
|
+
if (/\bHeavy\b/.test(snapshot) && !snapshot.includes(`button "${GROK_LABELS.modelSelect}"`)) {
|
|
1265
|
+
await log("Grok model already appears configured for Heavy; skipping reconfiguration");
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const modelButton = findEntry(snapshot, (candidate) => candidate.kind === "button" && candidate.label === GROK_LABELS.modelSelect && !candidate.disabled);
|
|
1269
|
+
if (!modelButton) throw new Error("Could not find Grok model selector");
|
|
1270
|
+
await clickRef(job, modelButton.ref);
|
|
1271
|
+
await agentBrowser(job, "wait", "500");
|
|
1272
|
+
const menuSnapshot = await snapshotText(job);
|
|
1273
|
+
const heavy = findEntry(menuSnapshot, (candidate) => ["menuitem", "menuitemradio", "option", "button"].includes(candidate.kind || "") && /^Heavy\b/i.test(String(candidate.label || "")) && !candidate.disabled);
|
|
1274
|
+
if (!heavy) throw new Error("Could not find Grok Heavy model option");
|
|
1275
|
+
await clickRef(job, heavy.ref);
|
|
1276
|
+
await agentBrowser(job, "wait", "800");
|
|
1277
|
+
const after = await snapshotText(job);
|
|
1278
|
+
if (!/\bHeavy\b/i.test(after)) {
|
|
1279
|
+
if (after.includes('link "Sign in"') || after.includes('button "Sign in"')) {
|
|
1280
|
+
throw new Error("Grok Heavy requires a signed-in Grok session. Set defaults.provider='grok', run /oracle-auth, and retry.");
|
|
1281
|
+
}
|
|
1282
|
+
throw new Error("Could not verify Grok Heavy selection after model configuration");
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1152
1286
|
async function uploadArchive(job) {
|
|
1153
1287
|
if (!existsSync(job.archivePath)) {
|
|
1154
1288
|
throw new Error(`Archive missing: ${job.archivePath}`);
|
|
@@ -1156,26 +1290,50 @@ async function uploadArchive(job) {
|
|
|
1156
1290
|
|
|
1157
1291
|
const fileLabel = basename(job.archivePath);
|
|
1158
1292
|
const addFilesSnapshot = await snapshotText(job);
|
|
1159
|
-
const baselineComposerFileCount = composerFileEntryCount(addFilesSnapshot, fileLabel);
|
|
1293
|
+
const baselineComposerFileCount = composerFileEntryCount(addFilesSnapshot, fileLabel, job);
|
|
1294
|
+
const labels = labelsForJob(job);
|
|
1160
1295
|
const addFilesEntry = findEntry(
|
|
1161
1296
|
addFilesSnapshot,
|
|
1162
|
-
(candidate) => candidate.label ===
|
|
1297
|
+
(candidate) => candidate.label === labels.addFiles && candidate.kind === "button",
|
|
1163
1298
|
);
|
|
1164
1299
|
if (!addFilesEntry) {
|
|
1165
|
-
throw new Error(`Could not find "${
|
|
1300
|
+
throw new Error(`Could not find "${labels.addFiles}" button`);
|
|
1166
1301
|
}
|
|
1167
1302
|
|
|
1168
1303
|
await clickRef(job, addFilesEntry.ref);
|
|
1169
1304
|
await agentBrowser(job, "wait", "500");
|
|
1170
1305
|
await agentBrowser(job, "upload", "input[type=file]", job.archivePath);
|
|
1171
1306
|
await log(`Selected archive for upload: ${job.archivePath}`);
|
|
1172
|
-
|
|
1307
|
+
if (isGrokJob(job)) {
|
|
1308
|
+
const deadline = Date.now() + 5 * 60 * 1000;
|
|
1309
|
+
let stablePolls = 0;
|
|
1310
|
+
while (Date.now() < deadline) {
|
|
1311
|
+
await heartbeat();
|
|
1312
|
+
const [snapshot, body] = await Promise.all([snapshotText(job), pageText(job).catch(() => "")]);
|
|
1313
|
+
const errorText = detectUploadErrorText(`${snapshot}\n${body}`);
|
|
1314
|
+
if (errorText) {
|
|
1315
|
+
throw new Error(`Upload error detected: ${errorText}`);
|
|
1316
|
+
}
|
|
1317
|
+
if (`${snapshot}\n${body}`.includes(fileLabel)) {
|
|
1318
|
+
stablePolls += 1;
|
|
1319
|
+
if (stablePolls >= 2) break;
|
|
1320
|
+
} else {
|
|
1321
|
+
stablePolls = 0;
|
|
1322
|
+
}
|
|
1323
|
+
await sleep(1000);
|
|
1324
|
+
}
|
|
1325
|
+
if (stablePolls < 2) throw new Error(`Timed out waiting for Grok upload confirmation for ${fileLabel}`);
|
|
1326
|
+
} else {
|
|
1327
|
+
await waitForUploadConfirmed(job, fileLabel, baselineComposerFileCount);
|
|
1328
|
+
}
|
|
1173
1329
|
await log(`Upload confirmed for: ${fileLabel}`);
|
|
1330
|
+
if (isGrokJob(job)) await agentBrowser(job, "press", "Escape").catch(() => undefined);
|
|
1174
1331
|
await rm(job.archivePath, { force: true });
|
|
1175
1332
|
await mutateJob((current) => ({ ...current, archiveDeletedAfterUpload: true }));
|
|
1176
1333
|
}
|
|
1177
1334
|
|
|
1178
1335
|
async function assistantMessages(job) {
|
|
1336
|
+
if (isGrokJob(job)) return grokAssistantMessages(job);
|
|
1179
1337
|
const result = await evalPage(
|
|
1180
1338
|
job,
|
|
1181
1339
|
toJsonScript(`
|
|
@@ -1206,8 +1364,11 @@ async function assistantMessages(job) {
|
|
|
1206
1364
|
.trim();
|
|
1207
1365
|
return text;
|
|
1208
1366
|
};
|
|
1367
|
+
const headingMessages = headings.map((heading) => ({ text: renderText(heading.nextElementSibling) }));
|
|
1368
|
+
const messageNodes = Array.from(document.querySelectorAll('[data-testid="assistant-message"], [data-message-author-role="assistant"]'));
|
|
1369
|
+
const nodeMessages = messageNodes.map((node) => ({ text: renderText(node) }));
|
|
1209
1370
|
return {
|
|
1210
|
-
messages:
|
|
1371
|
+
messages: headingMessages.some((message) => message.text) ? headingMessages : nodeMessages,
|
|
1211
1372
|
};
|
|
1212
1373
|
`),
|
|
1213
1374
|
);
|
|
@@ -1216,6 +1377,41 @@ async function assistantMessages(job) {
|
|
|
1216
1377
|
return result.messages.map((message) => ({ text: typeof message?.text === "string" ? message.text : "" }));
|
|
1217
1378
|
}
|
|
1218
1379
|
|
|
1380
|
+
async function grokAssistantMessages(job) {
|
|
1381
|
+
const result = await evalPage(
|
|
1382
|
+
job,
|
|
1383
|
+
toJsonScript(`
|
|
1384
|
+
const normalize = (value) => String(value || '').split('\\n\\n\\n').join('\\n\\n').trim();
|
|
1385
|
+
const renderText = (node) => {
|
|
1386
|
+
if (!node) return '';
|
|
1387
|
+
const clone = node.cloneNode(true);
|
|
1388
|
+
clone.querySelectorAll('button,[aria-label="Copy"],[aria-label="Like"],[aria-label="Dislike"],[aria-label="Regenerate"],[aria-label="More actions"],.thinking-container').forEach((el) => el.remove());
|
|
1389
|
+
const text = normalize(clone.innerText || clone.textContent || '');
|
|
1390
|
+
const lines = text.split('\\n');
|
|
1391
|
+
if (/^Thought for /i.test(lines[0] || '')) return lines.slice(1).join('\\n').trim();
|
|
1392
|
+
return text;
|
|
1393
|
+
};
|
|
1394
|
+
const bubbles = Array.from(document.querySelectorAll('.message-bubble'));
|
|
1395
|
+
const sourceNodes = bubbles.length > 0
|
|
1396
|
+
? bubbles
|
|
1397
|
+
: Array.from(document.querySelectorAll('div')).filter((node) => {
|
|
1398
|
+
const classText = String(node.className || '');
|
|
1399
|
+
return classText.includes('group') && classText.includes('flex') && classText.includes('flex-col') && classText.includes('justify-center');
|
|
1400
|
+
});
|
|
1401
|
+
const messages = sourceNodes
|
|
1402
|
+
.map((node) => node.closest('[data-message-author-role], [data-testid*="message"], .group') || node)
|
|
1403
|
+
.filter((node, index, all) => all.indexOf(node) === index)
|
|
1404
|
+
.filter((node) => node.getAttribute('data-testid') !== 'user-message' && node.getAttribute('data-message-author-role') !== 'user')
|
|
1405
|
+
.filter((node) => !node.querySelector('button[aria-label="Edit"]'))
|
|
1406
|
+
.map((node) => ({ text: renderText(node.querySelector('.message-bubble') || node) }))
|
|
1407
|
+
.filter((message) => message.text && !message.text.toLowerCase().startsWith('executed code'));
|
|
1408
|
+
return { messages };
|
|
1409
|
+
`),
|
|
1410
|
+
);
|
|
1411
|
+
if (!Array.isArray(result?.messages)) return [];
|
|
1412
|
+
return result.messages.map((message) => ({ text: typeof message?.text === "string" ? message.text : "" }));
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1219
1415
|
async function waitForStableChatUrl(job, previousChatUrl) {
|
|
1220
1416
|
const timeoutAt = Date.now() + 60_000;
|
|
1221
1417
|
/** @type {import("./chatgpt-flow-helpers.d.mts").OracleStableValueState | undefined} */
|
|
@@ -1244,9 +1440,9 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1244
1440
|
while (Date.now() < timeoutAt) {
|
|
1245
1441
|
await heartbeat();
|
|
1246
1442
|
const [snapshot, body] = await Promise.all([snapshotText(job), pageText(job).catch(() => "")]);
|
|
1247
|
-
const hasStopStreaming = snapshot.includes("Stop streaming");
|
|
1443
|
+
const hasStopStreaming = isGrokJob(job) ? snapshot.includes(GROK_LABELS.stop) : snapshot.includes("Stop streaming");
|
|
1248
1444
|
const hasRetryButton = snapshot.includes('button "Retry"');
|
|
1249
|
-
const copyResponseCount = (snapshot.match(/Copy response/g) || []).length;
|
|
1445
|
+
const copyResponseCount = isGrokJob(job) ? (snapshot.match(/button "Copy"/g) || []).length : (snapshot.match(/Copy response/g) || []).length;
|
|
1250
1446
|
const responseFailureText = detectResponseFailureText(`${snapshot}\n${body}`);
|
|
1251
1447
|
const messages = await assistantMessages(job);
|
|
1252
1448
|
const targetMessage = messages[baselineAssistantCount];
|
|
@@ -1269,17 +1465,17 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1269
1465
|
continue;
|
|
1270
1466
|
}
|
|
1271
1467
|
}
|
|
1272
|
-
throw new Error(
|
|
1468
|
+
throw new Error(`${isGrokJob(job) ? "Grok" : "ChatGPT"} response failed: ${responseFailureText}`);
|
|
1273
1469
|
}
|
|
1274
1470
|
|
|
1275
1471
|
let completionSignature;
|
|
1276
|
-
if (!hasStopStreaming &&
|
|
1472
|
+
if (!hasStopStreaming && targetText && (hasTargetCopyResponse || isGrokJob(job))) {
|
|
1277
1473
|
completionSignature = deriveAssistantCompletionSignature({
|
|
1278
1474
|
hasStopStreaming,
|
|
1279
|
-
hasTargetCopyResponse,
|
|
1475
|
+
hasTargetCopyResponse: hasTargetCopyResponse || isGrokJob(job),
|
|
1280
1476
|
responseText: targetText,
|
|
1281
1477
|
});
|
|
1282
|
-
} else if (!hasStopStreaming && !targetText) {
|
|
1478
|
+
} else if (!hasStopStreaming && hasTargetCopyResponse && !targetText) {
|
|
1283
1479
|
const artifactSignals = await collectArtifactCandidates(job, baselineAssistantCount, targetText).catch(() => ({ candidates: [], suspiciousLabels: [] }));
|
|
1284
1480
|
completionSignature = deriveAssistantCompletionSignature({
|
|
1285
1481
|
hasStopStreaming,
|
|
@@ -1305,7 +1501,7 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1305
1501
|
await sleep(job.config.worker.pollMs);
|
|
1306
1502
|
}
|
|
1307
1503
|
|
|
1308
|
-
throw new Error(
|
|
1504
|
+
throw new Error(`Timed out waiting for ${isGrokJob(job) ? "Grok" : "ChatGPT"} response completion`);
|
|
1309
1505
|
}
|
|
1310
1506
|
|
|
1311
1507
|
async function sha256(path) {
|
|
@@ -1637,6 +1833,11 @@ async function flushArtifactsState(artifacts) {
|
|
|
1637
1833
|
}
|
|
1638
1834
|
|
|
1639
1835
|
async function downloadArtifacts(job, responseIndex, responseText = "") {
|
|
1836
|
+
if (isGrokJob(job)) {
|
|
1837
|
+
await secureWriteText(`${jobDir}/artifacts.json`, "[]\n");
|
|
1838
|
+
await mutateJob((current) => ({ ...current, artifactPaths: [] }));
|
|
1839
|
+
return [];
|
|
1840
|
+
}
|
|
1640
1841
|
if (!job.config.artifacts.capture) {
|
|
1641
1842
|
await secureWriteText(`${jobDir}/artifacts.json`, "[]\n");
|
|
1642
1843
|
await mutateJob((current) => ({ ...current, artifactPaths: [] }));
|
|
@@ -1775,14 +1976,14 @@ async function run() {
|
|
|
1775
1976
|
currentJob = await mutateJob((job) => transitionOracleJobPhase(job, "verifying_auth", {
|
|
1776
1977
|
at: new Date().toISOString(),
|
|
1777
1978
|
source: "oracle:worker",
|
|
1778
|
-
message:
|
|
1979
|
+
message: `Verifying the imported ${isGrokJob(currentJob) ? "Grok" : "ChatGPT"} browser session.`,
|
|
1779
1980
|
patch: { heartbeatAt: new Date().toISOString() },
|
|
1780
1981
|
}));
|
|
1781
1982
|
await waitForOracleReady(currentJob);
|
|
1782
1983
|
currentJob = await mutateJob((job) => transitionOracleJobPhase(job, "configuring_model", {
|
|
1783
1984
|
at: new Date().toISOString(),
|
|
1784
1985
|
source: "oracle:worker",
|
|
1785
|
-
message:
|
|
1986
|
+
message: `Configuring the requested ${isGrokJob(currentJob) ? "Grok" : "ChatGPT"} model selection.`,
|
|
1786
1987
|
patch: { heartbeatAt: new Date().toISOString() },
|
|
1787
1988
|
}));
|
|
1788
1989
|
await configureModel(currentJob);
|
|
@@ -1800,16 +2001,35 @@ async function run() {
|
|
|
1800
2001
|
await log(`Waiting ${POST_SEND_SETTLE_MS}ms after send to avoid streaming interruption`);
|
|
1801
2002
|
await sleep(POST_SEND_SETTLE_MS);
|
|
1802
2003
|
|
|
1803
|
-
const
|
|
1804
|
-
|
|
2004
|
+
const observedChatUrl = isGrokJob(currentJob)
|
|
2005
|
+
? stripQuery(await currentUrl(currentJob))
|
|
2006
|
+
: await waitForStableChatUrl(currentJob, currentJob.chatUrl);
|
|
2007
|
+
const observedConversationId = parseConversationId(observedChatUrl) || currentJob.conversationId;
|
|
2008
|
+
const awaitingResponsePatch = {
|
|
2009
|
+
heartbeatAt: new Date().toISOString(),
|
|
2010
|
+
...(observedConversationId ? { chatUrl: observedChatUrl, conversationId: observedConversationId } : {}),
|
|
2011
|
+
};
|
|
1805
2012
|
currentJob = await mutateJob((job) => transitionOracleJobPhase(job, "awaiting_response", {
|
|
1806
2013
|
at: new Date().toISOString(),
|
|
1807
2014
|
source: "oracle:worker",
|
|
1808
2015
|
message: "Waiting for the assistant response to finish streaming.",
|
|
1809
|
-
patch:
|
|
2016
|
+
patch: awaitingResponsePatch,
|
|
1810
2017
|
}));
|
|
1811
2018
|
|
|
1812
2019
|
const completion = await waitForChatCompletion(currentJob, baselineAssistantCount);
|
|
2020
|
+
if (isGrokJob(currentJob) && !currentJob.conversationId) {
|
|
2021
|
+
const stableGrokChatUrl = await waitForStableChatUrl(currentJob, undefined);
|
|
2022
|
+
const stableGrokConversationId = parseConversationId(stableGrokChatUrl);
|
|
2023
|
+
if (!stableGrokConversationId) {
|
|
2024
|
+
throw new Error(`Grok response completed but the conversation URL did not stabilize; current URL: ${stableGrokChatUrl || "(unknown)"}`);
|
|
2025
|
+
}
|
|
2026
|
+
currentJob = await mutateJob((job) => ({
|
|
2027
|
+
...job,
|
|
2028
|
+
chatUrl: stableGrokChatUrl,
|
|
2029
|
+
conversationId: stableGrokConversationId,
|
|
2030
|
+
heartbeatAt: new Date().toISOString(),
|
|
2031
|
+
}));
|
|
2032
|
+
}
|
|
1813
2033
|
currentJob = await mutateJob((job) => transitionOracleJobPhase(job, "extracting_response", {
|
|
1814
2034
|
at: new Date().toISOString(),
|
|
1815
2035
|
source: "oracle:worker",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-oracle",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ChatGPT web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "ChatGPT and Grok web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"pi-extension",
|
|
13
13
|
"extension",
|
|
14
14
|
"chatgpt",
|
|
15
|
+
"grok",
|
|
15
16
|
"oracle"
|
|
16
17
|
],
|
|
17
18
|
"repository": {
|