pi-oracle 0.3.3 → 0.3.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/CHANGELOG.md +12 -0
- package/README.md +5 -0
- package/docs/ORACLE_DESIGN.md +1 -1
- package/docs/ORACLE_RECOVERY_DRILL.md +5 -4
- package/extensions/oracle/lib/tools.ts +4 -2
- package/extensions/oracle/worker/auth-bootstrap.mjs +25 -10
- package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +33 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +208 -0
- package/extensions/oracle/worker/run-job.mjs +69 -106
- package/package.json +13 -5
- package/prompts/oracle.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.4 - 2026-04-11
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- oracle archive defaults now exclude nested `secrets/` and `.secrets/` directories anywhere in the repo unless they are explicitly requested
|
|
7
|
+
- package metadata now reflects the current runtime floor and platform support (`node >=22`, `darwin`) and local release automation runs `verify:oracle` through `npm test` / `prepublishOnly`
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- model verification now distinguishes `thinking`, `pro`, `instant`, and `instant_auto_switch` more conservatively instead of accepting mismatched presets on partial UI evidence
|
|
11
|
+
- artifact-only assistant responses can now complete without timing out on missing plain-text bodies
|
|
12
|
+
- `/oracle-auth` diagnostics now write into a unique private temp directory per run instead of fixed `/tmp/oracle-auth.*` paths
|
|
13
|
+
- sanity coverage now exercises shared ChatGPT UI helpers directly, verifies nested secret exclusion, and guards the new auth diagnostics path handling
|
|
14
|
+
|
|
3
15
|
## 0.3.3 - 2026-04-11
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -147,6 +147,7 @@ Project config should only override safe, non-privileged settings.
|
|
|
147
147
|
## Requirements
|
|
148
148
|
|
|
149
149
|
- macOS
|
|
150
|
+
- Node.js 22 or newer
|
|
150
151
|
- Google Chrome installed
|
|
151
152
|
- ChatGPT already signed into a local Chrome profile
|
|
152
153
|
- `pi` 0.65.0 or newer
|
|
@@ -211,10 +212,14 @@ npm run check:oracle-extension
|
|
|
211
212
|
npm run typecheck
|
|
212
213
|
npm run sanity:oracle
|
|
213
214
|
npm run pack:check
|
|
215
|
+
# conventional local gate
|
|
216
|
+
npm test
|
|
214
217
|
# or all at once
|
|
215
218
|
npm run verify:oracle
|
|
216
219
|
```
|
|
217
220
|
|
|
221
|
+
`npm publish` is also guarded locally via `prepublishOnly` and will run `npm run verify:oracle` before publishing.
|
|
222
|
+
|
|
218
223
|
## Beta caveats
|
|
219
224
|
|
|
220
225
|
The highest-risk areas to monitor are:
|
package/docs/ORACLE_DESIGN.md
CHANGED
|
@@ -585,7 +585,7 @@ Remaining non-blocking hardening work:
|
|
|
585
585
|
|
|
586
586
|
Recent proof points:
|
|
587
587
|
- expired-auth drill fail path: `a2460bc1-7d89-4041-b67d-39680d310325`
|
|
588
|
-
- `/oracle-auth` repair evidence: `/tmp/oracle-auth.log`
|
|
588
|
+
- `/oracle-auth` repair evidence: the per-run `/tmp/pi-oracle-auth-*/oracle-auth.log` bundle path printed by `/oracle-auth`
|
|
589
589
|
- expired-auth drill post-repair success: `fa26a2a7-0057-4a21-b3e0-71c1d020facf`
|
|
590
590
|
- successful multi-artifact completion: `b6b3599c-6b91-4315-adfa-8a83aa5eda9b`
|
|
591
591
|
- repo-owned sanity harness: `npm run sanity:oracle`
|
|
@@ -105,10 +105,11 @@ For the failed run:
|
|
|
105
105
|
- any failure diagnostics under that job dir
|
|
106
106
|
|
|
107
107
|
For the repair:
|
|
108
|
-
- `/tmp/oracle-auth
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
108
|
+
- the per-run `/tmp/pi-oracle-auth-*/` diagnostics directory printed by `/oracle-auth`
|
|
109
|
+
- `oracle-auth.log`
|
|
110
|
+
- `oracle-auth.url.txt`
|
|
111
|
+
- `oracle-auth.snapshot.txt`
|
|
112
|
+
- `oracle-auth.body.txt`
|
|
112
113
|
|
|
113
114
|
For the successful rerun:
|
|
114
115
|
- `/tmp/oracle-<job-id>/job.json`
|
|
@@ -107,8 +107,10 @@ const DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_ANYWHERE = new Set([
|
|
|
107
107
|
".pnpm-store",
|
|
108
108
|
".serverless",
|
|
109
109
|
".aws-sam",
|
|
110
|
+
"secrets",
|
|
111
|
+
".secrets",
|
|
110
112
|
]);
|
|
111
|
-
const DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_AT_REPO_ROOT = new Set(["coverage", "htmlcov", "tmp", "temp", ".tmp", "dist", "build", "out"
|
|
113
|
+
const DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_AT_REPO_ROOT = new Set(["coverage", "htmlcov", "tmp", "temp", ".tmp", "dist", "build", "out"]);
|
|
112
114
|
const DEFAULT_ARCHIVE_EXCLUDED_FILES = new Set([
|
|
113
115
|
".coverage",
|
|
114
116
|
".DS_Store",
|
|
@@ -583,7 +585,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
583
585
|
promptSnippet: "Dispatch a background ChatGPT web oracle job after gathering repo context.",
|
|
584
586
|
promptGuidelines: [
|
|
585
587
|
"Gather context before calling oracle_submit.",
|
|
586
|
-
"By default, archive the whole repo by passing '.'; default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like .env files, key material, credential dotfiles, local database files, and
|
|
588
|
+
"By default, archive the whole repo by passing '.'; default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like .env files, key material, credential dotfiles, local database files, and nested secrets directories anywhere in the repo.",
|
|
587
589
|
"Only narrow file selection when the user explicitly asks, the task is clearly scoped smaller, or privacy/sensitivity requires it.",
|
|
588
590
|
"For very targeted asks like a single function or stack trace, a smaller archive is preferable.",
|
|
589
591
|
"When files='.' and the post-exclusion archive is still too large, submit automatically prunes the largest nested directories matching generic generated-output names like build/, dist/, out/, coverage/, and tmp/ outside obvious source roots like src/ and lib/ until the archive fits or no candidate remains; successful submissions report what was pruned.",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { withLock } from "./state-locks.mjs";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
import { appendFile, chmod, lstat, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
-
import { homedir } from "node:os";
|
|
4
|
+
import { appendFile, chmod, lstat, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import { basename, dirname, join, resolve } from "node:path";
|
|
7
7
|
import { getCookies } from "@steipete/sweet-cookie";
|
|
8
8
|
import { ensureAccountCookie, filterImportableAuthCookies } from "./auth-cookie-policy.mjs";
|
|
9
|
+
import { buildAllowedChatGptOrigins } from "./chatgpt-ui-helpers.mjs";
|
|
9
10
|
|
|
10
11
|
const rawConfig = process.argv[2];
|
|
11
12
|
if (!rawConfig) {
|
|
@@ -27,11 +28,12 @@ const CHATGPT_COOKIE_ORIGINS = [
|
|
|
27
28
|
"https://sentinel.openai.com",
|
|
28
29
|
"https://ws.chatgpt.com",
|
|
29
30
|
];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
let DIAGNOSTICS_DIR;
|
|
32
|
+
let LOG_PATH = "(oracle-auth log path unavailable)";
|
|
33
|
+
let URL_PATH = "(oracle-auth url path unavailable)";
|
|
34
|
+
let SNAPSHOT_PATH = "(oracle-auth snapshot path unavailable)";
|
|
35
|
+
let BODY_PATH = "(oracle-auth body path unavailable)";
|
|
36
|
+
let SCREENSHOT_PATH = "(oracle-auth screenshot path unavailable)";
|
|
35
37
|
const REAL_CHROME_USER_DATA_DIR = resolve(homedir(), "Library", "Application Support", "Google", "Chrome");
|
|
36
38
|
const DEFAULT_ORACLE_STATE_DIR = "/tmp/pi-oracle-state";
|
|
37
39
|
const ORACLE_STATE_DIR = process.env.PI_ORACLE_STATE_DIR?.trim() || DEFAULT_ORACLE_STATE_DIR;
|
|
@@ -50,12 +52,25 @@ function sleep(ms) {
|
|
|
50
52
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
async function initDiagnosticsBundle() {
|
|
56
|
+
if (DIAGNOSTICS_DIR) return;
|
|
57
|
+
DIAGNOSTICS_DIR = await mkdtemp(join(tmpdir(), "pi-oracle-auth-"));
|
|
58
|
+
await chmod(DIAGNOSTICS_DIR, 0o700).catch(() => undefined);
|
|
59
|
+
LOG_PATH = join(DIAGNOSTICS_DIR, "oracle-auth.log");
|
|
60
|
+
URL_PATH = join(DIAGNOSTICS_DIR, "oracle-auth.url.txt");
|
|
61
|
+
SNAPSHOT_PATH = join(DIAGNOSTICS_DIR, "oracle-auth.snapshot.txt");
|
|
62
|
+
BODY_PATH = join(DIAGNOSTICS_DIR, "oracle-auth.body.txt");
|
|
63
|
+
SCREENSHOT_PATH = join(DIAGNOSTICS_DIR, "oracle-auth.png");
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
async function initLog() {
|
|
67
|
+
await initDiagnosticsBundle();
|
|
54
68
|
await writeFile(LOG_PATH, "", { mode: 0o600 });
|
|
55
69
|
await chmod(LOG_PATH, 0o600).catch(() => undefined);
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
async function log(message) {
|
|
73
|
+
await initDiagnosticsBundle();
|
|
59
74
|
const line = `[${new Date().toISOString()}] ${message}\n`;
|
|
60
75
|
await appendFile(LOG_PATH, line, { encoding: "utf8", mode: 0o600 });
|
|
61
76
|
await chmod(LOG_PATH, 0o600).catch(() => undefined);
|
|
@@ -566,7 +581,7 @@ async function captureDiagnostics(reason) {
|
|
|
566
581
|
|
|
567
582
|
function classifyChatPage({ url, snapshot, body, probe }) {
|
|
568
583
|
const text = `${snapshot}\n${body}`;
|
|
569
|
-
const allowedOrigins =
|
|
584
|
+
const allowedOrigins = buildAllowedChatGptOrigins(config.browser.chatUrl, config.browser.authUrl);
|
|
570
585
|
|
|
571
586
|
const challengePatterns = [
|
|
572
587
|
/just a moment/i,
|
|
@@ -804,7 +819,7 @@ async function run() {
|
|
|
804
819
|
await writeFile(join(profilePlan.targetDir, ".oracle-seed-generation"), `${generation}\n`, { encoding: "utf8", mode: 0o600 });
|
|
805
820
|
committedProfile = true;
|
|
806
821
|
process.stdout.write(
|
|
807
|
-
`${classification.message} Synced ${appliedCount} cookies into ${profilePlan.targetDir}`,
|
|
822
|
+
`${classification.message} Synced ${appliedCount} cookies into ${profilePlan.targetDir}. Diagnostics: ${DIAGNOSTICS_DIR}`,
|
|
808
823
|
);
|
|
809
824
|
} catch (error) {
|
|
810
825
|
shouldPreserveBrowser = Boolean(error && typeof error === "object" && error.preserveBrowser === true);
|
|
@@ -822,7 +837,7 @@ async function run() {
|
|
|
822
837
|
|
|
823
838
|
run().catch((error) => {
|
|
824
839
|
process.stderr.write(
|
|
825
|
-
`${error instanceof Error ? error.message : String(error)}\nSee ${LOG_PATH} and diagnostics in
|
|
840
|
+
`${error instanceof Error ? error.message : String(error)}\nSee ${LOG_PATH} and diagnostics in ${DIAGNOSTICS_DIR || "(oracle-auth diagnostics dir unavailable)"}\nIf needed, ensure the configured real Chrome profile is already logged into ChatGPT and grant macOS Keychain access when prompted.`,
|
|
826
841
|
);
|
|
827
842
|
process.exit(1);
|
|
828
843
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type OracleUiModelFamily = "instant" | "thinking" | "pro";
|
|
2
|
+
export type OracleUiEffort = "light" | "standard" | "extended" | "heavy";
|
|
3
|
+
|
|
4
|
+
export interface OracleUiSelection {
|
|
5
|
+
modelFamily: OracleUiModelFamily;
|
|
6
|
+
effort?: OracleUiEffort;
|
|
7
|
+
autoSwitchToThinking?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export declare const CHATGPT_CANONICAL_APP_ORIGINS: readonly string[];
|
|
11
|
+
|
|
12
|
+
export declare function buildAllowedChatGptOrigins(chatUrl: string, authUrl?: string): string[];
|
|
13
|
+
export declare function matchesModelFamilyLabel(label: string | undefined, family: OracleUiModelFamily): boolean;
|
|
14
|
+
export declare function requestedEffortLabel(selection: OracleUiSelection): string | undefined;
|
|
15
|
+
export declare function effortSelectionVisible(snapshot: string, effortLabel: string | undefined): boolean;
|
|
16
|
+
export declare function thinkingChipVisible(snapshot: string): boolean;
|
|
17
|
+
export declare function snapshotHasModelConfigurationUi(snapshot: string): boolean;
|
|
18
|
+
export declare function autoSwitchToThinkingSelectionVisible(snapshot: string): boolean | undefined;
|
|
19
|
+
export declare function snapshotCanSafelySkipModelConfiguration(snapshot: string, selection: OracleUiSelection): boolean;
|
|
20
|
+
export declare function snapshotStronglyMatchesRequestedModel(snapshot: string, selection: OracleUiSelection): boolean;
|
|
21
|
+
export declare function snapshotWeaklyMatchesRequestedModel(snapshot: string, selection: OracleUiSelection): boolean;
|
|
22
|
+
export declare function buildAssistantCompletionSignature(args: {
|
|
23
|
+
responseText: string;
|
|
24
|
+
artifactLabels?: string[];
|
|
25
|
+
suspiciousArtifactLabels?: string[];
|
|
26
|
+
}): string | undefined;
|
|
27
|
+
export declare function deriveAssistantCompletionSignature(args: {
|
|
28
|
+
hasStopStreaming: boolean;
|
|
29
|
+
hasTargetCopyResponse: boolean;
|
|
30
|
+
responseText: string;
|
|
31
|
+
artifactLabels?: string[];
|
|
32
|
+
suspiciousArtifactLabels?: string[];
|
|
33
|
+
}): string | undefined;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { parseSnapshotEntries } from "./artifact-heuristics.mjs";
|
|
2
|
+
|
|
3
|
+
export const CHATGPT_CANONICAL_APP_ORIGINS = Object.freeze([
|
|
4
|
+
"https://chatgpt.com",
|
|
5
|
+
"https://chat.openai.com",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
const MODEL_FAMILY_PREFIX = {
|
|
9
|
+
instant: "Instant ",
|
|
10
|
+
thinking: "Thinking ",
|
|
11
|
+
pro: "Pro ",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const AUTO_SWITCH_LABEL = "Auto-switch to Thinking";
|
|
15
|
+
|
|
16
|
+
function originFromUrl(url) {
|
|
17
|
+
if (typeof url !== "string" || !url.trim()) return undefined;
|
|
18
|
+
try {
|
|
19
|
+
return new URL(url).origin;
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function uniqueStrings(values) {
|
|
26
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value))];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function titleCase(value) {
|
|
30
|
+
return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeText(value) {
|
|
34
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildAllowedChatGptOrigins(chatUrl, authUrl) {
|
|
38
|
+
return uniqueStrings([
|
|
39
|
+
...CHATGPT_CANONICAL_APP_ORIGINS,
|
|
40
|
+
originFromUrl(chatUrl),
|
|
41
|
+
originFromUrl(authUrl),
|
|
42
|
+
"https://auth.openai.com",
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function matchesModelFamilyLabel(label, family) {
|
|
47
|
+
const normalized = String(label || "");
|
|
48
|
+
const prefix = MODEL_FAMILY_PREFIX[family];
|
|
49
|
+
const exact = prefix.trim();
|
|
50
|
+
return normalized === exact || normalized.startsWith(prefix) || normalized.startsWith(`${exact},`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function requestedEffortLabel(selection) {
|
|
54
|
+
return selection?.effort ? titleCase(selection.effort) : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function effortSelectionVisible(snapshot, effortLabel) {
|
|
58
|
+
if (!effortLabel) return true;
|
|
59
|
+
const entries = parseSnapshotEntries(snapshot);
|
|
60
|
+
return entries.some((entry) => {
|
|
61
|
+
if (entry.disabled) return false;
|
|
62
|
+
if (entry.kind === "combobox" && entry.value === effortLabel) return true;
|
|
63
|
+
if (entry.kind !== "button") return false;
|
|
64
|
+
const label = String(entry.label || "").toLowerCase();
|
|
65
|
+
const normalizedEffort = effortLabel.toLowerCase();
|
|
66
|
+
return (
|
|
67
|
+
label === normalizedEffort ||
|
|
68
|
+
label === `${normalizedEffort} thinking` ||
|
|
69
|
+
label === `${normalizedEffort}, click to remove` ||
|
|
70
|
+
label === `${normalizedEffort} thinking, click to remove`
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function thinkingChipVisible(snapshot) {
|
|
76
|
+
return /button "(?:Light|Standard|Extended|Heavy)(?: thinking)?(?:, click to remove)?"/i.test(snapshot);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function snapshotHasModelConfigurationUi(snapshot) {
|
|
80
|
+
const entries = parseSnapshotEntries(snapshot);
|
|
81
|
+
const visibleFamilies = new Set(
|
|
82
|
+
entries
|
|
83
|
+
.filter((entry) => entry.kind === "button" && typeof entry.label === "string")
|
|
84
|
+
.flatMap((entry) =>
|
|
85
|
+
Object.keys(MODEL_FAMILY_PREFIX)
|
|
86
|
+
.filter((family) => matchesModelFamilyLabel(entry.label, family))
|
|
87
|
+
.map((family) => family),
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
const hasCloseButton = entries.some((entry) => entry.kind === "button" && entry.label === "Close" && !entry.disabled);
|
|
91
|
+
const hasEffortCombobox = entries.some(
|
|
92
|
+
(entry) => entry.kind === "combobox" && ["Light", "Standard", "Extended", "Heavy"].includes(entry.value || "") && !entry.disabled,
|
|
93
|
+
);
|
|
94
|
+
return visibleFamilies.size >= 2 || hasCloseButton || hasEffortCombobox;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function autoSwitchToThinkingSelectionVisible(snapshot) {
|
|
98
|
+
const entries = parseSnapshotEntries(snapshot);
|
|
99
|
+
let foundControl = false;
|
|
100
|
+
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
const controlText = normalizeText([entry.label, entry.value, entry.line].filter(Boolean).join(" "));
|
|
103
|
+
if (!controlText.toLowerCase().includes(AUTO_SWITCH_LABEL.toLowerCase())) continue;
|
|
104
|
+
foundControl = true;
|
|
105
|
+
|
|
106
|
+
if (/\b(?:checked|selected|enabled|on|active)\b/i.test(controlText)) return true;
|
|
107
|
+
if (/\b(?:unchecked|not checked|disabled|off)\b/i.test(controlText)) return false;
|
|
108
|
+
if (typeof entry.label === "string" && /click to remove/i.test(entry.label)) return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return foundControl ? false : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function snapshotCanSafelySkipModelConfiguration(snapshot, selection) {
|
|
115
|
+
if (!snapshotStronglyMatchesRequestedModel(snapshot, selection)) return false;
|
|
116
|
+
|
|
117
|
+
if (selection.modelFamily === "thinking" || selection.modelFamily === "pro") {
|
|
118
|
+
const effortLabel = requestedEffortLabel(selection);
|
|
119
|
+
if (effortLabel && !effortSelectionVisible(snapshot, effortLabel)) return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (selection.modelFamily === "instant" && selection.autoSwitchToThinking) {
|
|
123
|
+
return autoSwitchToThinkingSelectionVisible(snapshot) === true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function snapshotStronglyMatchesRequestedModel(snapshot, selection) {
|
|
130
|
+
const entries = parseSnapshotEntries(snapshot);
|
|
131
|
+
const familyMatched = entries.some((entry) => {
|
|
132
|
+
return !entry.disabled && matchesModelFamilyLabel(entry.label, selection.modelFamily);
|
|
133
|
+
});
|
|
134
|
+
if (!familyMatched) return false;
|
|
135
|
+
|
|
136
|
+
const configurationUiVisible = snapshotHasModelConfigurationUi(snapshot);
|
|
137
|
+
const effortLabel = requestedEffortLabel(selection);
|
|
138
|
+
|
|
139
|
+
if (selection.modelFamily === "thinking" || selection.modelFamily === "pro") {
|
|
140
|
+
if (!effortLabel) return true;
|
|
141
|
+
if (effortSelectionVisible(snapshot, effortLabel)) return true;
|
|
142
|
+
return !configurationUiVisible;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (selection.modelFamily === "instant") {
|
|
146
|
+
const autoSwitchState = autoSwitchToThinkingSelectionVisible(snapshot);
|
|
147
|
+
if (selection.autoSwitchToThinking) {
|
|
148
|
+
return autoSwitchState === true || (!configurationUiVisible && autoSwitchState === undefined);
|
|
149
|
+
}
|
|
150
|
+
return autoSwitchState !== true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function snapshotWeaklyMatchesRequestedModel(snapshot, selection) {
|
|
157
|
+
const entries = parseSnapshotEntries(snapshot);
|
|
158
|
+
const familyMatched = entries.some((entry) => {
|
|
159
|
+
return !entry.disabled && matchesModelFamilyLabel(entry.label, selection.modelFamily);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (selection.modelFamily === "thinking") {
|
|
163
|
+
return familyMatched || effortSelectionVisible(snapshot, requestedEffortLabel(selection));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!familyMatched) return false;
|
|
167
|
+
|
|
168
|
+
if (selection.modelFamily === "pro") {
|
|
169
|
+
return !thinkingChipVisible(snapshot);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (selection.modelFamily === "instant") {
|
|
173
|
+
const autoSwitchState = autoSwitchToThinkingSelectionVisible(snapshot);
|
|
174
|
+
return selection.autoSwitchToThinking ? autoSwitchState !== false : autoSwitchState !== true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function buildAssistantCompletionSignature({ responseText, artifactLabels = [], suspiciousArtifactLabels = [] }) {
|
|
181
|
+
const normalizedResponse = normalizeText(responseText);
|
|
182
|
+
if (normalizedResponse) return `text:${normalizedResponse}`;
|
|
183
|
+
|
|
184
|
+
const labels = uniqueStrings([...artifactLabels, ...suspiciousArtifactLabels].map((value) => normalizeText(value))).sort((left, right) => left.localeCompare(right));
|
|
185
|
+
if (labels.length > 0) return `artifacts:${labels.join("|")}`;
|
|
186
|
+
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function deriveAssistantCompletionSignature({
|
|
191
|
+
hasStopStreaming,
|
|
192
|
+
hasTargetCopyResponse,
|
|
193
|
+
responseText,
|
|
194
|
+
artifactLabels = [],
|
|
195
|
+
suspiciousArtifactLabels = [],
|
|
196
|
+
}) {
|
|
197
|
+
if (hasStopStreaming) return undefined;
|
|
198
|
+
|
|
199
|
+
if (hasTargetCopyResponse && normalizeText(responseText)) {
|
|
200
|
+
return buildAssistantCompletionSignature({ responseText });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!normalizeText(responseText)) {
|
|
204
|
+
return buildAssistantCompletionSignature({ responseText, artifactLabels, suspiciousArtifactLabels });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
@@ -5,6 +5,18 @@ import { basename, dirname, join } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { spawn, execFileSync } from "node:child_process";
|
|
7
7
|
import { extractArtifactLabels, FILE_LABEL_PATTERN_SOURCE, GENERIC_ARTIFACT_LABELS, parseSnapshotEntries, partitionStructuralArtifactCandidates } from "./artifact-heuristics.mjs";
|
|
8
|
+
import {
|
|
9
|
+
buildAllowedChatGptOrigins,
|
|
10
|
+
deriveAssistantCompletionSignature,
|
|
11
|
+
matchesModelFamilyLabel,
|
|
12
|
+
requestedEffortLabel,
|
|
13
|
+
effortSelectionVisible,
|
|
14
|
+
snapshotCanSafelySkipModelConfiguration,
|
|
15
|
+
snapshotHasModelConfigurationUi,
|
|
16
|
+
snapshotStronglyMatchesRequestedModel,
|
|
17
|
+
snapshotWeaklyMatchesRequestedModel,
|
|
18
|
+
autoSwitchToThinkingSelectionVisible,
|
|
19
|
+
} from "./chatgpt-ui-helpers.mjs";
|
|
8
20
|
import { createLease, listLeaseMetadata, readLeaseMetadata, releaseLease, withLock } from "./state-locks.mjs";
|
|
9
21
|
|
|
10
22
|
const jobId = process.argv[2];
|
|
@@ -25,12 +37,6 @@ const CHATGPT_LABELS = {
|
|
|
25
37
|
autoSwitchToThinking: "Auto-switch to Thinking",
|
|
26
38
|
configure: "Configure...",
|
|
27
39
|
};
|
|
28
|
-
const MODEL_FAMILY_PREFIX = {
|
|
29
|
-
instant: "Instant ",
|
|
30
|
-
thinking: "Thinking ",
|
|
31
|
-
pro: "Pro ",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
40
|
const WORKER_SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
35
41
|
const DEFAULT_ORACLE_STATE_DIR = "/tmp/pi-oracle-state";
|
|
36
42
|
const ORACLE_STATE_DIR = process.env.PI_ORACLE_STATE_DIR?.trim() || DEFAULT_ORACLE_STATE_DIR;
|
|
@@ -791,83 +797,10 @@ function findLastEntry(snapshot, predicate) {
|
|
|
791
797
|
return undefined;
|
|
792
798
|
}
|
|
793
799
|
|
|
794
|
-
function titleCase(value) {
|
|
795
|
-
return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function matchesModelFamilyLabel(label, family) {
|
|
799
|
-
const normalized = String(label || "");
|
|
800
|
-
const prefix = MODEL_FAMILY_PREFIX[family];
|
|
801
|
-
const exact = prefix.trim();
|
|
802
|
-
return normalized === exact || normalized.startsWith(prefix) || normalized.startsWith(`${exact},`);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
800
|
function matchesModelFamilyButton(candidate, family) {
|
|
806
801
|
return candidate.kind === "button" && typeof candidate.label === "string" && matchesModelFamilyLabel(candidate.label, family) && !candidate.disabled;
|
|
807
802
|
}
|
|
808
803
|
|
|
809
|
-
function requestedEffortLabel(job) {
|
|
810
|
-
return job.selection?.effort ? titleCase(job.selection.effort) : undefined;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function effortSelectionVisible(snapshot, effortLabel) {
|
|
814
|
-
if (!effortLabel) return true;
|
|
815
|
-
const entries = parseSnapshotEntries(snapshot);
|
|
816
|
-
return entries.some((entry) => {
|
|
817
|
-
if (entry.disabled) return false;
|
|
818
|
-
if (entry.kind === "combobox" && entry.value === effortLabel) return true;
|
|
819
|
-
if (entry.kind !== "button") return false;
|
|
820
|
-
const label = String(entry.label || "").toLowerCase();
|
|
821
|
-
const normalizedEffort = effortLabel.toLowerCase();
|
|
822
|
-
return (
|
|
823
|
-
label === normalizedEffort ||
|
|
824
|
-
label === `${normalizedEffort} thinking` ||
|
|
825
|
-
label === `${normalizedEffort}, click to remove` ||
|
|
826
|
-
label === `${normalizedEffort} thinking, click to remove`
|
|
827
|
-
);
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function thinkingChipVisible(snapshot) {
|
|
832
|
-
return /button "(?:Light|Standard|Extended|Heavy)(?: thinking)?(?:, click to remove)?"/i.test(snapshot);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function snapshotHasModelConfigurationUi(snapshot) {
|
|
836
|
-
const entries = parseSnapshotEntries(snapshot);
|
|
837
|
-
const visibleFamilies = new Set(
|
|
838
|
-
entries
|
|
839
|
-
.filter((entry) => entry.kind === "button" && typeof entry.label === "string")
|
|
840
|
-
.flatMap((entry) =>
|
|
841
|
-
Object.keys(MODEL_FAMILY_PREFIX)
|
|
842
|
-
.filter((family) => matchesModelFamilyLabel(entry.label, family))
|
|
843
|
-
.map((family) => family),
|
|
844
|
-
),
|
|
845
|
-
);
|
|
846
|
-
const hasCloseButton = entries.some((entry) => entry.kind === "button" && entry.label === CHATGPT_LABELS.close && !entry.disabled);
|
|
847
|
-
const hasEffortCombobox = entries.some(
|
|
848
|
-
(entry) => entry.kind === "combobox" && ["Light", "Standard", "Extended", "Heavy"].includes(entry.value || "") && !entry.disabled,
|
|
849
|
-
);
|
|
850
|
-
return visibleFamilies.size >= 2 || hasCloseButton || hasEffortCombobox;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
function snapshotStronglyMatchesRequestedModel(snapshot, job) {
|
|
854
|
-
const entries = parseSnapshotEntries(snapshot);
|
|
855
|
-
const familyMatched = entries.some((entry) => matchesModelFamilyButton(entry, job.selection.modelFamily));
|
|
856
|
-
const effortLabel = requestedEffortLabel(job);
|
|
857
|
-
if (job.selection.modelFamily === "thinking") {
|
|
858
|
-
return familyMatched || effortSelectionVisible(snapshot, effortLabel);
|
|
859
|
-
}
|
|
860
|
-
if (job.selection.modelFamily === "pro") {
|
|
861
|
-
return effortLabel ? familyMatched && effortSelectionVisible(snapshot, effortLabel) : familyMatched;
|
|
862
|
-
}
|
|
863
|
-
return familyMatched;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function thinkingSelectionVisible(snapshot) {
|
|
867
|
-
const entries = parseSnapshotEntries(snapshot);
|
|
868
|
-
return entries.some((entry) => !entry.disabled && entry.kind === "button" && matchesModelFamilyLabel(entry.label, "thinking"));
|
|
869
|
-
}
|
|
870
|
-
|
|
871
804
|
function composerControlsVisible(snapshot) {
|
|
872
805
|
const entries = parseSnapshotEntries(snapshot);
|
|
873
806
|
const hasComposer = entries.some(
|
|
@@ -879,17 +812,15 @@ function composerControlsVisible(snapshot) {
|
|
|
879
812
|
return hasComposer && hasAddFiles;
|
|
880
813
|
}
|
|
881
814
|
|
|
882
|
-
function
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
}
|
|
892
|
-
return false;
|
|
815
|
+
async function clickAutoSwitchToThinkingControl(job) {
|
|
816
|
+
const snapshot = await snapshotText(job);
|
|
817
|
+
const entry = findEntry(
|
|
818
|
+
snapshot,
|
|
819
|
+
(candidate) => candidate.kind === "button" && typeof candidate.label === "string" && candidate.label.startsWith(CHATGPT_LABELS.autoSwitchToThinking) && !candidate.disabled,
|
|
820
|
+
);
|
|
821
|
+
if (!entry) throw new Error(`Could not find ${CHATGPT_LABELS.autoSwitchToThinking} control`);
|
|
822
|
+
await clickRef(job, entry.ref);
|
|
823
|
+
return entry;
|
|
893
824
|
}
|
|
894
825
|
|
|
895
826
|
async function clickRef(job, ref) {
|
|
@@ -962,7 +893,7 @@ function classifyChatPage({ job, url, snapshot, body, probe }) {
|
|
|
962
893
|
return { state: "transient_outage_error", message: "ChatGPT is showing a transient outage/error page" };
|
|
963
894
|
}
|
|
964
895
|
|
|
965
|
-
const allowedOrigins =
|
|
896
|
+
const allowedOrigins = buildAllowedChatGptOrigins(job.config.browser.chatUrl, job.config.browser.authUrl);
|
|
966
897
|
const onAllowedOrigin = typeof url === "string" && allowedOrigins.some((origin) => url.startsWith(origin));
|
|
967
898
|
const onAuthPath = typeof url === "string" && url.includes("/auth/");
|
|
968
899
|
const hasComposer = snapshot.includes(`textbox "${CHATGPT_LABELS.composer}"`);
|
|
@@ -1210,7 +1141,7 @@ async function waitForModelConfigurationToSettle(job, options = {}) {
|
|
|
1210
1141
|
const configurationUiVisible = snapshotHasModelConfigurationUi(snapshot);
|
|
1211
1142
|
|
|
1212
1143
|
if (!configurationUiVisible) {
|
|
1213
|
-
if (snapshotWeaklyMatchesRequestedModel(snapshot, job)) return;
|
|
1144
|
+
if (snapshotWeaklyMatchesRequestedModel(snapshot, job.selection)) return;
|
|
1214
1145
|
if (options.stronglyVerified) {
|
|
1215
1146
|
if (!fallbackLogged) {
|
|
1216
1147
|
fallbackLogged = true;
|
|
@@ -1248,7 +1179,7 @@ async function waitForModelConfigurationToSettle(job, options = {}) {
|
|
|
1248
1179
|
|
|
1249
1180
|
async function configureModel(job) {
|
|
1250
1181
|
const initialSnapshot = await snapshotText(job);
|
|
1251
|
-
if (
|
|
1182
|
+
if (snapshotCanSafelySkipModelConfiguration(initialSnapshot, job.selection)) {
|
|
1252
1183
|
await log(`Model already appears configured for family=${job.selection.modelFamily} effort=${job.selection?.effort || "(none)"}; skipping reconfiguration`);
|
|
1253
1184
|
return;
|
|
1254
1185
|
}
|
|
@@ -1257,23 +1188,27 @@ async function configureModel(job) {
|
|
|
1257
1188
|
let familySnapshot = await openModelConfiguration(job);
|
|
1258
1189
|
let verificationSnapshot = familySnapshot;
|
|
1259
1190
|
|
|
1191
|
+
const alreadyConfiguredInUi = snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection);
|
|
1260
1192
|
let familyEntry = findEntry(familySnapshot, (candidate) => matchesModelFamilyButton(candidate, job.selection.modelFamily));
|
|
1261
|
-
if (
|
|
1193
|
+
if (alreadyConfiguredInUi) {
|
|
1262
1194
|
await log("Model configuration UI opened with requested settings already selected");
|
|
1263
|
-
}
|
|
1264
|
-
if (!familyEntry && !snapshotStronglyMatchesRequestedModel(familySnapshot, job)) {
|
|
1195
|
+
} else if (!familyEntry) {
|
|
1265
1196
|
throw new Error(`Could not find model family button for ${job.selection.modelFamily}`);
|
|
1266
1197
|
}
|
|
1267
1198
|
|
|
1268
|
-
if (familyEntry) {
|
|
1199
|
+
if (!alreadyConfiguredInUi && familyEntry) {
|
|
1269
1200
|
await clickRef(job, familyEntry.ref);
|
|
1270
1201
|
await agentBrowser(job, "wait", "800");
|
|
1271
1202
|
familySnapshot = await snapshotText(job);
|
|
1272
1203
|
verificationSnapshot = familySnapshot;
|
|
1204
|
+
familyEntry = findEntry(familySnapshot, (candidate) => matchesModelFamilyButton(candidate, job.selection.modelFamily));
|
|
1205
|
+
if (!familyEntry && !snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection)) {
|
|
1206
|
+
throw new Error(`Requested model family did not remain selected: ${job.selection.modelFamily}`);
|
|
1207
|
+
}
|
|
1273
1208
|
}
|
|
1274
1209
|
|
|
1275
1210
|
if (job.selection.modelFamily === "thinking" || job.selection.modelFamily === "pro") {
|
|
1276
|
-
const effortLabel = requestedEffortLabel(job);
|
|
1211
|
+
const effortLabel = requestedEffortLabel(job.selection);
|
|
1277
1212
|
if (effortLabel && !effortSelectionVisible(familySnapshot, effortLabel)) {
|
|
1278
1213
|
const opened = await openEffortDropdown(job);
|
|
1279
1214
|
if (!opened) {
|
|
@@ -1291,15 +1226,22 @@ async function configureModel(job) {
|
|
|
1291
1226
|
if (!selectedEffort && !effortSelectionVisible(effortSnapshot, effortLabel)) {
|
|
1292
1227
|
throw new Error(`Requested effort did not remain selected: ${effortLabel}`);
|
|
1293
1228
|
}
|
|
1229
|
+
familySnapshot = effortSnapshot;
|
|
1294
1230
|
}
|
|
1295
1231
|
}
|
|
1296
1232
|
|
|
1297
|
-
if (job.selection.modelFamily === "instant"
|
|
1298
|
-
|
|
1299
|
-
|
|
1233
|
+
if (job.selection.modelFamily === "instant") {
|
|
1234
|
+
const desiredAutoSwitchState = job.selection.autoSwitchToThinking === true;
|
|
1235
|
+
const currentAutoSwitchState = autoSwitchToThinkingSelectionVisible(familySnapshot);
|
|
1236
|
+
if (currentAutoSwitchState !== desiredAutoSwitchState && (desiredAutoSwitchState || currentAutoSwitchState === true)) {
|
|
1237
|
+
await clickAutoSwitchToThinkingControl(job);
|
|
1238
|
+
await agentBrowser(job, "wait", "400");
|
|
1239
|
+
verificationSnapshot = await snapshotText(job);
|
|
1240
|
+
familySnapshot = verificationSnapshot;
|
|
1241
|
+
}
|
|
1300
1242
|
}
|
|
1301
1243
|
|
|
1302
|
-
const stronglyVerified = snapshotStronglyMatchesRequestedModel(verificationSnapshot, job);
|
|
1244
|
+
const stronglyVerified = snapshotStronglyMatchesRequestedModel(verificationSnapshot, job.selection);
|
|
1303
1245
|
if (!stronglyVerified) {
|
|
1304
1246
|
throw new Error(`Could not verify requested model settings in configuration UI for ${job.selection.modelFamily}`);
|
|
1305
1247
|
}
|
|
@@ -1427,7 +1369,7 @@ async function waitForStableChatUrl(job, previousChatUrl) {
|
|
|
1427
1369
|
|
|
1428
1370
|
async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
1429
1371
|
const timeoutAt = Date.now() + job.config.worker.completionTimeoutMs;
|
|
1430
|
-
let
|
|
1372
|
+
let lastCompletionSignature = "";
|
|
1431
1373
|
let stableCount = 0;
|
|
1432
1374
|
let retriedAfterFailure = false;
|
|
1433
1375
|
|
|
@@ -1451,7 +1393,7 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1451
1393
|
);
|
|
1452
1394
|
if (retryEntry) {
|
|
1453
1395
|
retriedAfterFailure = true;
|
|
1454
|
-
|
|
1396
|
+
lastCompletionSignature = "";
|
|
1455
1397
|
stableCount = 0;
|
|
1456
1398
|
await log(`Response delivery failed (${responseFailureText}); clicking Retry once`);
|
|
1457
1399
|
await clickRef(job, retryEntry.ref);
|
|
@@ -1462,13 +1404,34 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1462
1404
|
throw new Error(`ChatGPT response failed: ${responseFailureText}`);
|
|
1463
1405
|
}
|
|
1464
1406
|
|
|
1407
|
+
let completionSignature;
|
|
1465
1408
|
if (!hasStopStreaming && hasTargetCopyResponse && targetText) {
|
|
1466
|
-
|
|
1409
|
+
completionSignature = deriveAssistantCompletionSignature({
|
|
1410
|
+
hasStopStreaming,
|
|
1411
|
+
hasTargetCopyResponse,
|
|
1412
|
+
responseText: targetText,
|
|
1413
|
+
});
|
|
1414
|
+
} else if (!hasStopStreaming && !targetText) {
|
|
1415
|
+
const artifactSignals = await collectArtifactCandidates(job, baselineAssistantCount, targetText).catch(() => ({ candidates: [], suspiciousLabels: [] }));
|
|
1416
|
+
completionSignature = deriveAssistantCompletionSignature({
|
|
1417
|
+
hasStopStreaming,
|
|
1418
|
+
hasTargetCopyResponse,
|
|
1419
|
+
responseText: targetText,
|
|
1420
|
+
artifactLabels: artifactSignals.candidates.map((candidate) => candidate.label),
|
|
1421
|
+
suspiciousArtifactLabels: artifactSignals.suspiciousLabels,
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
if (completionSignature) {
|
|
1426
|
+
if (completionSignature === lastCompletionSignature) stableCount += 1;
|
|
1467
1427
|
else stableCount = 1;
|
|
1468
|
-
|
|
1428
|
+
lastCompletionSignature = completionSignature;
|
|
1469
1429
|
if (stableCount >= 2) {
|
|
1470
1430
|
return { responseIndex: baselineAssistantCount, responseText: targetText };
|
|
1471
1431
|
}
|
|
1432
|
+
} else {
|
|
1433
|
+
lastCompletionSignature = "";
|
|
1434
|
+
stableCount = 0;
|
|
1472
1435
|
}
|
|
1473
1436
|
|
|
1474
1437
|
await sleep(job.config.worker.pollMs);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-oracle",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "ChatGPT web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,16 +42,21 @@
|
|
|
42
42
|
]
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
|
-
"check:oracle-extension": "node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/state-locks.mjs && node --check extensions/oracle/worker/artifact-heuristics.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-ai --external:@sinclair/typebox --outfile=/tmp/pi-oracle-extension-check.js",
|
|
45
|
+
"check:oracle-extension": "node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/state-locks.mjs && node --check extensions/oracle/worker/artifact-heuristics.mjs && node --check extensions/oracle/worker/chatgpt-ui-helpers.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-ai --external:@sinclair/typebox --outfile=/tmp/pi-oracle-extension-check.js",
|
|
46
46
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
47
47
|
"sanity:oracle": "node scripts/oracle-sanity-runner.mjs",
|
|
48
48
|
"pack:check": "npm pack --dry-run",
|
|
49
|
-
"verify:oracle": "npm run check:oracle-extension && npm run typecheck && npm run sanity:oracle && npm run pack:check"
|
|
49
|
+
"verify:oracle": "npm run check:oracle-extension && npm run typecheck && npm run sanity:oracle && npm run pack:check",
|
|
50
|
+
"test": "npm run verify:oracle",
|
|
51
|
+
"prepublishOnly": "npm run verify:oracle"
|
|
50
52
|
},
|
|
51
53
|
"dependencies": {
|
|
52
54
|
"@sinclair/typebox": "^0.34.49",
|
|
53
55
|
"@steipete/sweet-cookie": "^0.2.0"
|
|
54
56
|
},
|
|
57
|
+
"overrides": {
|
|
58
|
+
"basic-ftp": "^5.2.2"
|
|
59
|
+
},
|
|
55
60
|
"devDependencies": {
|
|
56
61
|
"@mariozechner/pi-ai": "^0.65.2",
|
|
57
62
|
"@mariozechner/pi-coding-agent": "^0.65.2",
|
|
@@ -61,6 +66,9 @@
|
|
|
61
66
|
"typescript": "^5.9.3"
|
|
62
67
|
},
|
|
63
68
|
"engines": {
|
|
64
|
-
"node": ">=
|
|
65
|
-
}
|
|
69
|
+
"node": ">=22"
|
|
70
|
+
},
|
|
71
|
+
"os": [
|
|
72
|
+
"darwin"
|
|
73
|
+
]
|
|
66
74
|
}
|
package/prompts/oracle.md
CHANGED
|
@@ -22,7 +22,7 @@ Oracle model (`oracle_submit`):
|
|
|
22
22
|
|
|
23
23
|
Rules:
|
|
24
24
|
- Always include an archive. Do not submit without context files.
|
|
25
|
-
- By default, include the whole repository by passing `.`. Default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like `.env` files, key material, credential dotfiles, local database files, and
|
|
25
|
+
- By default, include the whole repository by passing `.`. Default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like `.env` files, key material, credential dotfiles, local database files, and nested `secrets/` directories anywhere in the repo.
|
|
26
26
|
- Only limit file selection if the user explicitly requests it, if the task is clearly scoped to a smaller area, or if privacy/sensitivity requires it.
|
|
27
27
|
- For very targeted asks like reviewing one function or explaining one stack trace, a smaller archive is preferable.
|
|
28
28
|
- If the request depends on git state or pending changes (for example code review, ship readiness, or release approval), create a tracked diff bundle file inside the repo (for example under `.pi/`) containing `git status` plus `git diff` output, include that file in the archive, and tell the oracle to use it because the `.git` directory is not included in oracle exports.
|