pi-oracle 0.7.18 → 0.7.20

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 CHANGED
@@ -1,6 +1,18 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.7.20 - 2026-07-28
4
+
5
+ ### Fixed
6
+ - accepted undifferentiated ChatGPT Pro compact menus (bare Pro / Instant 5.5) so pro presets can configure without Standard/Extended rows
7
+ - replaced `\S` tool-schema non-blank patterns with a GBNF-safe character class so llama.cpp-backed OpenAI-compatible servers can compile oracle tool schemas
8
+
9
+ ### Validation
10
+ - ran the full `npm run release:check` local gate before release
11
+
12
+ ## 0.7.19 - 2026-07-16
13
+
14
+ ### Changed
15
+ - refreshed the local Pi/pi-ai development lock, sanity contracts, safe-loader version check, and compatibility guidance for Pi 0.80.9; Oracle does not use the removed SDK auth/session options
4
16
 
5
17
  ## 0.7.18 - 2026-07-14
6
18
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `pi-oracle` lets a `pi` agent send hard, long-running work to ChatGPT.com or Grok through the web app, with repo archives, background execution, saved results, and a best-effort wake-up back into `pi` when the answer is ready.
4
4
 
5
- > Status: experimental public beta. Current local validation baseline is pi `0.80.7`; the platform-smoke harness covers macOS, Linux, and Windows native with Chromium-family browsers. Pi `0.80.7+` is the suggested tested floor for project-trust-aware package/runtime validation, but pi-bundled runtime packages remain optional wildcard peers so npm peer ranges do not block users from trying newer pi releases. Normal oracle jobs run in an isolated browser profile, not your active browser window.
5
+ > Status: experimental public beta. Current local validation baseline is pi `0.80.9`; the platform-smoke harness covers macOS, Linux, and Windows native with Chromium-family browsers. Pi `0.80.9+` is the suggested tested floor for project-trust-aware package/runtime validation, but pi-bundled runtime packages remain optional wildcard peers so npm peer ranges do not block users from trying newer pi releases. Normal oracle jobs run in an isolated browser profile, not your active browser window.
6
6
 
7
7
  ## What a successful run looks like
8
8
 
@@ -79,7 +79,7 @@ You need:
79
79
 
80
80
  - macOS, Linux, or Windows native
81
81
  - Node.js 22.19.0 or newer for package install/use; platform smoke/release validation currently expects Node 24+ per `platform-smoke.config.mjs`
82
- - Suggested tested floor: `pi` 0.80.7 or newer; older pi versions are not blocked by package metadata but are outside the current validation baseline
82
+ - Suggested tested floor: `pi` 0.80.9 or newer; older pi versions are not blocked by package metadata but are outside the current validation baseline
83
83
  - Google Chrome/Chromium or another Chromium-family browser
84
84
  - ChatGPT or Grok already signed in to the configured local browser profile for the provider you plan to use
85
85
  - `agent-browser` and `tar` available on the machine; `zstd` is also required when submitting ChatGPT `.tar.zst` archives
@@ -7,7 +7,7 @@ Companion doc:
7
7
  - `docs/ORACLE_RECOVERY_DRILL.md` — safe expired-auth recovery validation drill
8
8
 
9
9
  Compatibility target:
10
- - `pi` 0.80.7+ is the suggested tested floor for current project-trust-aware package/runtime validation
10
+ - `pi` 0.80.9+ is the suggested tested floor for current project-trust-aware package/runtime validation
11
11
  - package metadata keeps pi runtime packages as optional wildcard peers, so this suggested floor is not enforced as a hard npm install requirement
12
12
  - current extension lifecycle only; no backward-compatibility shims for removed `session_switch` / `session_fork` events
13
13
 
@@ -662,6 +662,6 @@ Recent proof points:
662
662
  - expired-auth drill post-repair success: `fa26a2a7-0057-4a21-b3e0-71c1d020facf`
663
663
  - successful multi-artifact completion: `b6b3599c-6b91-4315-adfa-8a83aa5eda9b`
664
664
  - repo-owned sanity harness: `npm run sanity:oracle`
665
- - real installed-extension smoke source of truth: `scripts/oracle-real-smoke.mjs`; required release proof runs packed-install mode (`npm run smoke:real:packed`), asserts Pi 0.80.7, and executes `/oracle-status` through Pi's installed-package loader without provider credentials or an external oracle job; optional slower model-agent submission debugging remains behind `PI_ORACLE_REAL_TEST_MODEL_AGENT=1`; source mode (`npm run smoke:real:source`) is inner-loop/debug only
665
+ - real installed-extension smoke source of truth: `scripts/oracle-real-smoke.mjs`; required release proof runs packed-install mode (`npm run smoke:real:packed`), asserts Pi 0.80.9, and executes `/oracle-status` through Pi's installed-package loader without provider credentials or an external oracle job; optional slower model-agent submission debugging remains behind `PI_ORACLE_REAL_TEST_MODEL_AGENT=1`; source mode (`npm run smoke:real:source`) is inner-loop/debug only
666
666
  - macOS, Ubuntu, and Windows native package/build/runtime smoke source of truth: `docs/platform-smoke.md`; use `npm run verify:oracle` for everyday local iteration, `npm run smoke:platform:doctor` plus a focused target/suite run for platform-sensitive changes, `npm run smoke:platform:all` for doctor-first platform matrix evidence, and `npm run release:check` for the full local-plus-platform release gate
667
667
  - release gate: `npm run release:check`, also used by `prepublishOnly`, combines static verification, fresh loaded-extension ChatGPT preset proof via `npm run release:proof:chatgpt-presets`, and all required Crabbox platform smokes
@@ -81,7 +81,7 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
81
81
  files: Type.Array(Type.String({
82
82
  description: "Project-relative file or directory path to include in the archive.",
83
83
  minLength: 1,
84
- pattern: "^.*\\S.*$",
84
+ pattern: "^.*[^ \\t\\r\\n].*$",
85
85
  }), {
86
86
  description: "Exact project-relative files/directories to include in the oracle archive.",
87
87
  minItems: 1,
@@ -99,7 +99,7 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
99
99
  chatGptConversationId: Type.Optional(Type.String({
100
100
  description: "Existing ChatGPT conversation id, or full https://chatgpt.com/c/... URL, to continue. Omit for default behavior: starting a fresh oracle thread. Do not combine with followUpJobId.",
101
101
  minLength: 1,
102
- pattern: "^.*\\S.*$",
102
+ pattern: "^.*[^ \\t\\r\\n].*$",
103
103
  })),
104
104
  }, { additionalProperties: false });
105
105
 
@@ -109,7 +109,7 @@ const ORACLE_PREFLIGHT_PARAMS = Type.Object({
109
109
  chatGptConversationId: Type.Optional(Type.String({
110
110
  description: "Existing ChatGPT conversation id, or full https://chatgpt.com/c/... URL, whose provider/thread readiness should be checked. Do not combine with followUpJobId.",
111
111
  minLength: 1,
112
- pattern: "^.*\\S.*$",
112
+ pattern: "^.*[^ \\t\\r\\n].*$",
113
113
  })),
114
114
  }, { additionalProperties: false });
115
115
 
@@ -29,9 +29,10 @@ const AUTO_SWITCH_LABEL = "Auto-switch to Thinking";
29
29
  const THINKING_EFFORT_COMBOBOX_LABEL = "Thinking effort";
30
30
  const PRO_THINKING_EFFORT_COMBOBOX_LABEL = "Pro thinking effort";
31
31
  const EFFORT_LABELS = new Set(["Light", "Standard", "Extended", "Heavy"]);
32
- const COMPACT_INTELLIGENCE_MENU_PATTERN = /(?:Intelligence.*Instant.*Medium.*High.*Pro|^(?:Instant|Medium|High|Extra High|Pro Standard|Pro Extended)$)/i;
33
- const COMPACT_INTELLIGENCE_CONTROL_PATTERN = /^(?:Instant(?:\s+5s)?|Medium(?:\s+5\s*[–-]\s*30s)?|High(?:\s+15\s*[–-]\s*60s)?|Extra High|Pro\s+5\+\s*min|Pro Standard|Pro Extended)$/i;
34
- const COMPACT_INTELLIGENCE_OPENER_PATTERN = /^(?:Instant|Medium|High|Extra High|Pro|Pro Standard|Pro Extended)$/i;
32
+ const COMPACT_INTELLIGENCE_MENU_PATTERN = /(?:Intelligence.*Instant.*Medium.*High.*Pro|^(?:Instant|Medium|High|Extra High|Pro(?: Standard| Extended)?)$)/i;
33
+ // Bare Instant is the legacy top-level family radio; compact Instant rows are versioned (Instant 5s / Instant 5.5).
34
+ const COMPACT_INTELLIGENCE_CONTROL_PATTERN = /^(?:Instant\s+[\d.]+s?|Medium(?:\s+5\s*[–-]\s*30s)?|High(?:\s+15\s*[–-]\s*60s)?|Extra High|Pro(?:\s+5\+\s*min|\s+Standard|\s+Extended)?)$/i;
35
+ const COMPACT_INTELLIGENCE_OPENER_PATTERN = /^(?:Instant(?:\s+[\d.]+s?)?|Medium|High|Extra High|Pro(?: Standard| Extended)?)$/i;
35
36
  const BARE_EFFORT_PATTERN = /^(light|standard|extended|heavy)(?:, click to remove)?$/i;
36
37
  const INSTANT_CHIP_PATTERN = /^instant(?:, click to remove)?$/i;
37
38
  const THINKING_CHIP_PATTERN = /^(?:(light|standard|extended|heavy)\s+)?thinking(?:, click to remove)?$/i;
@@ -184,7 +185,7 @@ function parseCompactIntelligenceSelection(label) {
184
185
  const normalized = normalizeChipLabel(label);
185
186
  if (!COMPACT_INTELLIGENCE_CONTROL_PATTERN.test(normalized)) return undefined;
186
187
 
187
- if (/^Instant(?:\s+5s)?$/i.test(normalized)) {
188
+ if (/^Instant\s+[\d.]+s?$/i.test(normalized)) {
188
189
  return {
189
190
  modelFamily: /** @type {OracleUiModelFamily} */ ("instant"),
190
191
  compactTier: "instant",
@@ -219,6 +220,8 @@ function parseCompactIntelligenceSelection(label) {
219
220
  compactTier: "pro",
220
221
  };
221
222
  }
223
+ // "Pro 5+ min" is always the compact Pro tier. Bare "Pro" is ambiguous with the
224
+ // legacy top-level family radio and is handled with sibling context below.
222
225
  if (/^Pro\s+5\+\s*min$/i.test(normalized)) {
223
226
  return {
224
227
  modelFamily: /** @type {OracleUiModelFamily} */ ("pro"),
@@ -229,6 +232,24 @@ function parseCompactIntelligenceSelection(label) {
229
232
  return undefined;
230
233
  }
231
234
 
235
+ function parseBareProCompactSelection(label) {
236
+ if (/click to remove/i.test(String(label || ""))) return undefined;
237
+ if (!/^Pro$/i.test(normalizeChipLabel(label))) return undefined;
238
+ return {
239
+ modelFamily: /** @type {OracleUiModelFamily} */ ("pro"),
240
+ compactTier: "pro",
241
+ };
242
+ }
243
+
244
+ function snapshotHasCompactTierSiblings(entries, exceptLabel) {
245
+ const except = normalizeChipLabel(exceptLabel).toLowerCase();
246
+ return entries.some((entry) => {
247
+ if (entry.disabled || entry.kind !== "menuitemradio") return false;
248
+ if (normalizeChipLabel(entry.label).toLowerCase() === except) return false;
249
+ return Boolean(parseCompactIntelligenceSelection(entry.label));
250
+ });
251
+ }
252
+
232
253
  function hasRemovableComposerModelChip(entries) {
233
254
  return entries.some(
234
255
  (entry) => entry.kind === "button" && /click to remove/i.test(String(entry.label || "")) && parseComposerChipSelection(entry.label),
@@ -237,7 +258,7 @@ function hasRemovableComposerModelChip(entries) {
237
258
 
238
259
  function hasCompactIntelligenceMenuContext(entries) {
239
260
  return entries.some((entry) => !entry.disabled && entry.kind === "menu" && COMPACT_INTELLIGENCE_MENU_PATTERN.test(normalizeText(entry.label)))
240
- || entries.some((entry) => !entry.disabled && entry.kind === "menuitemradio" && checkedState(entry) === true && parseCompactIntelligenceSelection(entry.label));
261
+ || entries.some((entry) => !entry.disabled && entry.kind === "menuitemradio" && checkedState(entry) === true && compactSelectionFromEntry(entry, entries));
241
262
  }
242
263
 
243
264
  function hasLegacyEffortCombobox(entries) {
@@ -248,18 +269,36 @@ function hasLegacyEffortCombobox(entries) {
248
269
  });
249
270
  }
250
271
 
251
- function compactSelectionFromEntry(entry, _entries, options = {}) {
272
+ function compactSelectionFromEntry(entry, entries = [], options = {}) {
252
273
  if (entry.disabled) return undefined;
253
274
  const kind = entry.kind || "";
254
- if (COMPACT_INTELLIGENCE_CONTROL_KINDS.has(kind)) return parseCompactIntelligenceSelection(entry.label);
275
+ if (COMPACT_INTELLIGENCE_CONTROL_KINDS.has(kind)) {
276
+ const parsed = parseCompactIntelligenceSelection(entry.label);
277
+ if (parsed) return parsed;
278
+ // Bare "Pro" is compact only when versioned Instant / Medium / High / Extra High siblings exist.
279
+ if (snapshotHasCompactTierSiblings(entries, entry.label)) {
280
+ return parseBareProCompactSelection(entry.label);
281
+ }
282
+ return undefined;
283
+ }
255
284
  if (options.allowClosedButtons && kind === "button" && !/\bexpanded=true\b/.test(String(entry.line || ""))) {
256
- return parseCompactIntelligenceSelection(entry.label);
285
+ const parsed = parseCompactIntelligenceSelection(entry.label);
286
+ if (parsed) return parsed;
287
+ const barePro = parseBareProCompactSelection(entry.label);
288
+ if (barePro) return barePro;
289
+ // Closed composer pills keep bare Instant after the compact menu closes.
290
+ if (/^Instant$/i.test(normalizeChipLabel(entry.label))) {
291
+ return {
292
+ modelFamily: /** @type {OracleUiModelFamily} */ ("instant"),
293
+ compactTier: "instant",
294
+ };
295
+ }
257
296
  }
258
297
  return undefined;
259
298
  }
260
299
 
261
300
  export function matchesCompactIntelligenceControlLabel(label) {
262
- return Boolean(parseCompactIntelligenceSelection(label));
301
+ return Boolean(parseCompactIntelligenceSelection(label) || parseBareProCompactSelection(label));
263
302
  }
264
303
 
265
304
  export function snapshotHasClosedCompactSelection(snapshot, selection) {
@@ -330,7 +369,7 @@ function detectCompactIntelligenceSelection(entries) {
330
369
  }
331
370
 
332
371
  export function matchesRequestedModelControlLabel(label, selection) {
333
- const compactSelection = parseCompactIntelligenceSelection(label);
372
+ const compactSelection = parseCompactIntelligenceSelection(label) || parseBareProCompactSelection(label);
334
373
  if (compactSelection) return compactSelectionMatchesRequested(selection, compactSelection);
335
374
  return matchesModelFamilyLabel(label, selection.modelFamily);
336
375
  }
@@ -455,7 +494,7 @@ export function snapshotHasModelConfigurationUi(snapshot) {
455
494
  ),
456
495
  );
457
496
  const visibleCompactControls = entries.filter(
458
- (entry) => !entry.disabled && entry.kind === "menuitemradio" && parseCompactIntelligenceSelection(entry.label),
497
+ (entry) => !entry.disabled && entry.kind === "menuitemradio" && compactSelectionFromEntry(entry, entries),
459
498
  );
460
499
  const hasCompactIntelligenceMenu = entries.some(
461
500
  (entry) => !entry.disabled && entry.kind === "menu" && COMPACT_INTELLIGENCE_MENU_PATTERN.test(normalizeText(entry.label)),
@@ -895,8 +895,8 @@ function snapshotHasCompactIntelligenceMenuControls(snapshot) {
895
895
  return Boolean(findEntry(snapshot, (candidate) => {
896
896
  if (candidate.disabled) return false;
897
897
  const label = normalizeSnapshotLabel(candidate.label);
898
- return (candidate.kind === "menu" && /(?:Intelligence.*Instant.*Medium.*High.*Pro|^(?:Instant|Medium|High|Extra High|Pro Extended)$)/i.test(label))
899
- || (candidate.kind === "menuitemradio" && /^(?:Instant(?:\s+5s)?|Medium(?:\s+5\s*[–-]\s*30s)?|High(?:\s+15\s*[–-]\s*60s)?|Extra High|Pro\s+5\+\s*min|Pro Standard|Pro Extended)$/i.test(label));
898
+ return (candidate.kind === "menu" && /(?:Intelligence.*Instant.*Medium.*High.*Pro|^(?:Instant|Medium|High|Extra High|Pro(?: Standard| Extended)?)$)/i.test(label))
899
+ || (candidate.kind === "menuitemradio" && /^(?:Instant\s+[\d.]+s?|Medium(?:\s+5\s*[–-]\s*30s)?|High(?:\s+15\s*[–-]\s*60s)?|Extra High|Pro(?:\s+5\+\s*min|\s+Standard|\s+Extended)?)$/i.test(label));
900
900
  }));
901
901
  }
902
902
 
@@ -904,7 +904,7 @@ function matchesRequestedModelControl(candidate, selection, options = {}) {
904
904
  if (!["button", "radio", "menuitemradio"].includes(candidate.kind || "") || typeof candidate.label !== "string" || candidate.disabled) return false;
905
905
  if (candidate.kind === "button") {
906
906
  if (/\bexpanded=true\b/.test(String(candidate.line || ""))) return false;
907
- if (options.ignoreCompactTierButtons && /^(?:Instant|Medium|High|Extra High|Pro|Pro Extended)$/i.test(candidate.label)) return false;
907
+ if (options.ignoreCompactTierButtons && /^(?:Instant(?:\s+[\d.]+s?)?|Medium|High|Extra High|Pro(?: Standard| Extended)?)$/i.test(candidate.label)) return false;
908
908
  if (options.ignoreCompactOnlyButtons && /^(?:Medium|High|Extra High)$/i.test(candidate.label)) return false;
909
909
  }
910
910
  if (selection.modelFamily === "pro" && /^Pro(?:\s+Extended)?$/i.test(candidate.label)) return true;
@@ -1582,25 +1582,34 @@ async function configureModel(job) {
1582
1582
  if (effortLabel && !effortSelectionVisible(familySnapshot, effortLabel)) {
1583
1583
  const opened = await openEffortDropdown(job);
1584
1584
  if (!opened) {
1585
- throw new Error(`Could not open effort dropdown for requested effort: ${effortLabel}`);
1586
- }
1587
- await agentBrowser(job, "wait", "300");
1588
- if (job.selection.modelFamily === "pro" && await maybeClickLabeledEntry(job, `Pro ${effortLabel}`, { kind: "menuitemradio" })) {
1589
- // Current ChatGPT exposes Pro effort choices as nested menu radio items.
1585
+ // Current ChatGPT Pro menus sometimes expose only undifferentiated "Pro" with no Standard/Extended rows.
1586
+ const afterOpenAttempt = await snapshotText(job);
1587
+ if (job.selection.modelFamily === "pro" && snapshotStronglyMatchesRequestedModel(afterOpenAttempt, job.selection)) {
1588
+ await log(`Pro effort dropdown unavailable for ${effortLabel}; accepting undifferentiated Pro selection`);
1589
+ verificationSnapshot = afterOpenAttempt;
1590
+ familySnapshot = afterOpenAttempt;
1591
+ } else {
1592
+ throw new Error(`Could not open effort dropdown for requested effort: ${effortLabel}`);
1593
+ }
1590
1594
  } else {
1591
- await clickLabeledEntry(job, effortLabel, { kind: "option" });
1592
- }
1593
- await agentBrowser(job, "wait", "400");
1594
- const effortSnapshot = await snapshotText(job);
1595
- verificationSnapshot = effortSnapshot;
1596
- const selectedEffort = findEntry(
1597
- effortSnapshot,
1598
- (candidate) => candidate.kind === "combobox" && candidate.value === effortLabel && !candidate.disabled,
1599
- );
1600
- if (!selectedEffort && !effortSelectionVisible(effortSnapshot, effortLabel)) {
1601
- throw new Error(`Requested effort did not remain selected: ${effortLabel}`);
1595
+ await agentBrowser(job, "wait", "300");
1596
+ if (job.selection.modelFamily === "pro" && await maybeClickLabeledEntry(job, `Pro ${effortLabel}`, { kind: "menuitemradio" })) {
1597
+ // Current ChatGPT exposes Pro effort choices as nested menu radio items.
1598
+ } else {
1599
+ await clickLabeledEntry(job, effortLabel, { kind: "option" });
1600
+ }
1601
+ await agentBrowser(job, "wait", "400");
1602
+ const effortSnapshot = await snapshotText(job);
1603
+ verificationSnapshot = effortSnapshot;
1604
+ const selectedEffort = findEntry(
1605
+ effortSnapshot,
1606
+ (candidate) => candidate.kind === "combobox" && candidate.value === effortLabel && !candidate.disabled,
1607
+ );
1608
+ if (!selectedEffort && !effortSelectionVisible(effortSnapshot, effortLabel)) {
1609
+ throw new Error(`Requested effort did not remain selected: ${effortLabel}`);
1610
+ }
1611
+ familySnapshot = effortSnapshot;
1602
1612
  }
1603
- familySnapshot = effortSnapshot;
1604
1613
  }
1605
1614
  }
1606
1615
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.7.18",
3
+ "version": "0.7.20",
4
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",
@@ -87,8 +87,8 @@
87
87
  "protobufjs": "7.6.1"
88
88
  },
89
89
  "devDependencies": {
90
- "@earendil-works/pi-ai": "^0.80.7",
91
- "@earendil-works/pi-coding-agent": "^0.80.7",
90
+ "@earendil-works/pi-ai": "^0.80.9",
91
+ "@earendil-works/pi-coding-agent": "^0.80.9",
92
92
  "@types/node": "^22.19.19",
93
93
  "esbuild": "^0.28.0",
94
94
  "tsx": "^4.22.3",
@@ -11,7 +11,7 @@ import { basename, dirname, join, resolve } from "node:path";
11
11
  const DEFAULT_PROVIDER = "zai";
12
12
  const DEFAULT_MODEL = "glm-5.2";
13
13
  const DEFAULT_TIMEOUT_MS = 180_000;
14
- const EXPECTED_PI_VERSION = "0.80.7";
14
+ const EXPECTED_PI_VERSION = "0.80.9";
15
15
  const PACKAGE_NAME = "pi-oracle";
16
16
 
17
17
  function usage() {