pi-oracle 0.7.4 → 0.7.6

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +53 -18
  3. package/docs/ORACLE_DESIGN.md +16 -8
  4. package/docs/platform-smoke.md +156 -0
  5. package/extensions/oracle/index.ts +10 -4
  6. package/extensions/oracle/lib/config.ts +53 -27
  7. package/extensions/oracle/lib/jobs.ts +9 -5
  8. package/extensions/oracle/lib/poller.ts +1 -0
  9. package/extensions/oracle/lib/runtime.ts +107 -32
  10. package/extensions/oracle/lib/tools.ts +138 -12
  11. package/extensions/oracle/shared/browser-profile-helpers.d.mts +59 -0
  12. package/extensions/oracle/shared/browser-profile-helpers.mjs +395 -0
  13. package/extensions/oracle/shared/process-helpers.mjs +12 -1
  14. package/extensions/oracle/shared/state-coordination-helpers.mjs +8 -2
  15. package/extensions/oracle/worker/auth-bootstrap.mjs +39 -10
  16. package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
  17. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +157 -1
  18. package/extensions/oracle/worker/chromium-cookie-source.mjs +2 -1
  19. package/extensions/oracle/worker/run-job.mjs +107 -25
  20. package/package.json +30 -9
  21. package/platform-smoke.config.mjs +66 -0
  22. package/scripts/oracle-real-smoke.mjs +500 -0
  23. package/scripts/platform-smoke/Dockerfile.ubuntu +8 -0
  24. package/scripts/platform-smoke/artifacts.mjs +87 -0
  25. package/scripts/platform-smoke/assertions.mjs +34 -0
  26. package/scripts/platform-smoke/crabbox-runner.mjs +135 -0
  27. package/scripts/platform-smoke/doctor.mjs +239 -0
  28. package/scripts/platform-smoke/invariants.mjs +124 -0
  29. package/scripts/platform-smoke/platform-build-windows.ps1 +168 -0
  30. package/scripts/platform-smoke/targets.mjs +434 -0
  31. package/scripts/platform-smoke.mjs +152 -0
@@ -5,7 +5,7 @@
5
5
  // Invariants/Assumptions: Job state is persisted under worker-held locks, browser/session artifacts live under the configured oracle directories, and cleanup preserves durable recovery semantics.
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { existsSync, readdirSync, readFileSync } from "node:fs";
8
- import { appendFile, chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
8
+ import { appendFile, chmod, cp as copyDirectory, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
9
9
  import { basename, dirname, join } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { spawn } from "node:child_process";
@@ -23,7 +23,9 @@ import { extractArtifactLabels, FILE_LABEL_PATTERN_SOURCE, GENERIC_ARTIFACT_LABE
23
23
  import {
24
24
  buildAllowedChatGptOrigins,
25
25
  deriveAssistantCompletionSignature,
26
+ matchesCompactIntelligenceOpenerLabel,
26
27
  matchesModelFamilyLabel,
28
+ matchesRequestedModelControlLabel,
27
29
  requestedEffortLabel,
28
30
  effortSelectionVisible,
29
31
  snapshotCanSafelySkipModelConfiguration,
@@ -34,6 +36,7 @@ import {
34
36
  autoSwitchToThinkingSelectionVisible,
35
37
  } from "./chatgpt-ui-helpers.mjs";
36
38
  import { assistantSnapshotSlice, nextStableValueState, resolveStableConversationUrlCandidate, stripUrlQueryAndHash } from "./chatgpt-flow-helpers.mjs";
39
+ import { assertNotKnownBrowserUserDataPath, scrubSweetCookieSafeStoragePasswordEnv, sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
37
40
  import { createLease, listLeaseMetadata, readLeaseMetadata, releaseLease, withLock } from "./state-locks.mjs";
38
41
 
39
42
  const jobId = process.argv[2];
@@ -80,6 +83,8 @@ const POST_SEND_SETTLE_MS = 15_000;
80
83
  const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
81
84
  (candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
82
85
  ) || "agent-browser";
86
+ const CP_BIN = process.env.PI_ORACLE_CP_PATH?.trim() || "cp";
87
+ scrubSweetCookieSafeStoragePasswordEnv();
83
88
 
84
89
  let currentJob;
85
90
  let browserStarted = false;
@@ -209,12 +214,30 @@ function sleep(ms) {
209
214
  return new Promise((resolve) => setTimeout(resolve, ms));
210
215
  }
211
216
 
217
+ function killProcessTree(child) {
218
+ if (process.platform === "win32" && child.pid) {
219
+ spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore", windowsHide: true }).on("error", () => undefined);
220
+ return;
221
+ }
222
+ child.kill("SIGTERM");
223
+ }
224
+
225
+ function killProcess(child) {
226
+ if (process.platform === "win32" && child.pid) {
227
+ spawn("taskkill", ["/pid", String(child.pid), "/f"], { stdio: "ignore", windowsHide: true }).on("error", () => undefined);
228
+ return;
229
+ }
230
+ child.kill("SIGKILL");
231
+ }
232
+
212
233
  function spawnCommand(command, args, options = {}) {
213
234
  return new Promise((resolve, reject) => {
214
235
  const { timeoutMs, ...spawnOptions } = options;
215
236
  const child = spawn(command, args, {
216
237
  stdio: ["pipe", "pipe", "pipe"],
217
238
  ...spawnOptions,
239
+ env: sweetCookieSafeStoragePasswordScrubbedEnv(spawnOptions.env),
240
+ shell: spawnOptions.shell ?? process.platform === "win32",
218
241
  });
219
242
  let stdout = "";
220
243
  let stderr = "";
@@ -223,8 +246,8 @@ function spawnCommand(command, args, options = {}) {
223
246
  if (typeof timeoutMs === "number" && timeoutMs > 0) {
224
247
  killTimer = setTimeout(() => {
225
248
  timedOut = true;
226
- child.kill("SIGTERM");
227
- setTimeout(() => child.kill("SIGKILL"), 2_000).unref?.();
249
+ killProcessTree(child);
250
+ setTimeout(() => killProcess(child), 2_000).unref?.();
228
251
  }, timeoutMs);
229
252
  killTimer.unref?.();
230
253
  }
@@ -274,8 +297,20 @@ async function removeChromiumProcessSingletonArtifacts(profileDir) {
274
297
  ]);
275
298
  }
276
299
 
300
+ function assertSafeRuntimeProfilePath(path, label, config = undefined) {
301
+ try {
302
+ assertNotKnownBrowserUserDataPath(path, label, {
303
+ cookieSources: config ? { chromeProfile: config.auth.chromeProfile, chromeCookiePath: config.auth.chromeCookiePath } : undefined,
304
+ });
305
+ } catch (error) {
306
+ throw new Error(`Oracle ${label} path is unsafe: ${path}. ${error instanceof Error ? error.message : String(error)}`);
307
+ }
308
+ }
309
+
277
310
  async function cloneSeedProfileToRuntime(job) {
278
311
  const seedDir = job.config.browser.authSeedProfileDir;
312
+ assertSafeRuntimeProfilePath(seedDir, "auth seed profile", job.config);
313
+ assertSafeRuntimeProfilePath(job.runtimeProfileDir, "runtime profile", job.config);
279
314
  if (!existsSync(seedDir)) {
280
315
  throw new Error(`Oracle auth seed profile not found: ${seedDir}. Run /oracle-auth first.`);
281
316
  }
@@ -286,17 +321,17 @@ async function cloneSeedProfileToRuntime(job) {
286
321
  await withLock(ORACLE_STATE_DIR, "auth", "global", { jobId: job.id, processPid: process.pid, action: "cloneSeedProfile" }, async () => {
287
322
  await rm(job.runtimeProfileDir, { recursive: true, force: true }).catch(() => undefined);
288
323
  await ensurePrivateDir(dirname(job.runtimeProfileDir));
289
- if (job.config.browser.cloneStrategy === "apfs-clone") {
324
+ if (job.config.browser.cloneStrategy === "apfs-clone" && process.platform === "darwin") {
290
325
  try {
291
- await spawnCommand("/bin/cp", ["-cR", seedDir, job.runtimeProfileDir], { timeoutMs: PROFILE_CLONE_TIMEOUT_MS });
326
+ await spawnCommand(CP_BIN, ["-cR", seedDir, job.runtimeProfileDir], { timeoutMs: PROFILE_CLONE_TIMEOUT_MS });
292
327
  } catch (error) {
293
328
  const message = error instanceof Error ? error.message : String(error);
294
329
  await log(`APFS clone copy failed; falling back to recursive copy: ${message}`);
295
330
  await rm(job.runtimeProfileDir, { recursive: true, force: true }).catch(() => undefined);
296
- await spawnCommand("/bin/cp", ["-R", seedDir, job.runtimeProfileDir], { timeoutMs: PROFILE_CLONE_TIMEOUT_MS });
331
+ await spawnCommand(CP_BIN, ["-R", seedDir, job.runtimeProfileDir], { timeoutMs: PROFILE_CLONE_TIMEOUT_MS });
297
332
  }
298
333
  } else {
299
- await spawnCommand("/bin/cp", ["-R", seedDir, job.runtimeProfileDir], { timeoutMs: PROFILE_CLONE_TIMEOUT_MS });
334
+ await copyDirectory(seedDir, job.runtimeProfileDir, { recursive: true, force: true, verbatimSymlinks: true });
300
335
  }
301
336
  await removeChromiumProcessSingletonArtifacts(job.runtimeProfileDir);
302
337
  }, 10 * 60 * 1000);
@@ -314,27 +349,28 @@ async function cleanupRuntime(job) {
314
349
  warnings.push(message);
315
350
  await log(message).catch(() => undefined);
316
351
  });
317
- await rm(job.runtimeProfileDir, { recursive: true, force: true }).catch(async (error) => {
352
+ try {
353
+ assertSafeRuntimeProfilePath(job.runtimeProfileDir, "runtime profile", job.config);
354
+ await rm(job.runtimeProfileDir, { recursive: true, force: true });
355
+ } catch (error) {
318
356
  const message = `Runtime profile cleanup warning: ${error instanceof Error ? error.message : String(error)}`;
319
357
  warnings.push(message);
320
358
  await log(message).catch(() => undefined);
321
- });
322
- if (warnings.length === 0) {
323
- await releaseLease(ORACLE_STATE_DIR, "conversation", job.conversationId).catch(async (error) => {
324
- const message = `Conversation lease cleanup warning: ${error instanceof Error ? error.message : String(error)}`;
325
- warnings.push(message);
326
- await log(message).catch(() => undefined);
327
- });
328
- await releaseLease(ORACLE_STATE_DIR, "runtime", job.runtimeId).catch(async (error) => {
329
- const message = `Runtime lease cleanup warning: ${error instanceof Error ? error.message : String(error)}`;
330
- warnings.push(message);
331
- await log(message).catch(() => undefined);
332
- });
333
359
  }
360
+ await releaseLease(ORACLE_STATE_DIR, "conversation", job.conversationId).catch(async (error) => {
361
+ const message = `Conversation lease cleanup warning: ${error instanceof Error ? error.message : String(error)}`;
362
+ warnings.push(message);
363
+ await log(message).catch(() => undefined);
364
+ });
365
+ await releaseLease(ORACLE_STATE_DIR, "runtime", job.runtimeId).catch(async (error) => {
366
+ const message = `Runtime lease cleanup warning: ${error instanceof Error ? error.message : String(error)}`;
367
+ warnings.push(message);
368
+ await log(message).catch(() => undefined);
369
+ });
334
370
  if (warnings.length === 0) {
335
371
  await log(`Cleanup summary: runtime ${job.runtimeId} released with no warnings`).catch(() => undefined);
336
372
  } else {
337
- await log(`Cleanup summary: runtime ${job.runtimeId} released with ${warnings.length} warning(s)`).catch(() => undefined);
373
+ await log(`Cleanup summary: runtime ${job.runtimeId} released after ${warnings.length} warning(s)`).catch(() => undefined);
338
374
  }
339
375
  return warnings;
340
376
  } finally {
@@ -723,11 +759,42 @@ function matchesModelFamilyControl(candidate, family) {
723
759
  return ["button", "radio", "menuitemradio"].includes(candidate.kind || "") && typeof candidate.label === "string" && matchesModelFamilyLabel(candidate.label, family) && !candidate.disabled;
724
760
  }
725
761
 
762
+ function normalizeSnapshotLabel(value) {
763
+ return String(value || "").replace(/\s+/g, " ").trim();
764
+ }
765
+
766
+ function snapshotHasLegacyEffortCombobox(snapshot) {
767
+ return Boolean(findEntry(snapshot, (candidate) => {
768
+ if (candidate.kind !== "combobox" || candidate.disabled) return false;
769
+ return /^(?:Thinking effort|Pro thinking effort)$/i.test(normalizeSnapshotLabel(candidate.label));
770
+ }));
771
+ }
772
+
773
+ function snapshotHasCompactIntelligenceMenuControls(snapshot) {
774
+ return Boolean(findEntry(snapshot, (candidate) => {
775
+ if (candidate.disabled) return false;
776
+ const label = normalizeSnapshotLabel(candidate.label);
777
+ return (candidate.kind === "menu" && /Intelligence.*Instant.*Medium.*High.*Pro/i.test(label))
778
+ || (candidate.kind === "menuitemradio" && /^(?:Instant\s+5s|Medium\s+5\s*[–-]\s*30s|High\s+15\s*[–-]\s*60s|Pro\s+5\+\s*min)$/i.test(label));
779
+ }));
780
+ }
781
+
782
+ function matchesRequestedModelControl(candidate, selection, options = {}) {
783
+ if (!["button", "radio", "menuitemradio"].includes(candidate.kind || "") || typeof candidate.label !== "string" || candidate.disabled) return false;
784
+ if (candidate.kind === "button") {
785
+ if (/\bexpanded=true\b/.test(String(candidate.line || ""))) return false;
786
+ if (options.ignoreCompactTierButtons && /^(?:Instant|Medium|High|Pro)$/i.test(candidate.label)) return false;
787
+ if (options.ignoreCompactOnlyButtons && /^(?:Medium|High)$/i.test(candidate.label)) return false;
788
+ }
789
+ return matchesRequestedModelControlLabel(candidate.label, selection);
790
+ }
791
+
726
792
  function matchesModelConfigurationOpener(candidate) {
727
793
  if (candidate.kind !== "button" || typeof candidate.label !== "string" || candidate.disabled) return false;
728
794
  const label = String(candidate.label || "");
729
795
  return candidate.label === "Model"
730
796
  || candidate.label === "Model selector"
797
+ || matchesCompactIntelligenceOpenerLabel(label)
731
798
  || /^(?:Light|Standard|Extended|Heavy)(?:, click to remove)?$/i.test(label)
732
799
  || ["instant", "thinking", "pro"].some((family) => matchesModelFamilyLabel(label, /** @type {import("./chatgpt-ui-helpers.d.mts").OracleUiModelFamily} */ (family)))
733
800
  || /^(?:(?:Light|Standard|Extended|Heavy) )?Thinking(?:, click to remove)?$/i.test(label)
@@ -1196,19 +1263,33 @@ async function configureModel(job) {
1196
1263
  let verificationSnapshot = familySnapshot;
1197
1264
 
1198
1265
  const alreadyConfiguredInUi = snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection);
1199
- let familyEntry = findEntry(familySnapshot, (candidate) => matchesModelFamilyControl(candidate, job.selection.modelFamily));
1266
+ const legacyEffortComboboxVisible = snapshotHasLegacyEffortCombobox(familySnapshot);
1267
+ const familyAlreadySelectedInUi = !alreadyConfiguredInUi && legacyEffortComboboxVisible && snapshotWeaklyMatchesRequestedModel(familySnapshot, job.selection);
1268
+ const controlOptions = {
1269
+ ignoreCompactTierButtons: snapshotHasCompactIntelligenceMenuControls(familySnapshot),
1270
+ ignoreCompactOnlyButtons: legacyEffortComboboxVisible,
1271
+ };
1272
+ let familyEntry = alreadyConfiguredInUi || familyAlreadySelectedInUi
1273
+ ? undefined
1274
+ : findEntry(familySnapshot, (candidate) => matchesRequestedModelControl(candidate, job.selection, controlOptions));
1200
1275
  if (alreadyConfiguredInUi) {
1201
1276
  await log("Model configuration UI opened with requested settings already selected");
1277
+ } else if (familyAlreadySelectedInUi) {
1278
+ await log("Model family already appears selected; verifying effort-specific settings");
1202
1279
  } else if (!familyEntry) {
1203
1280
  throw new Error(`Could not find model family control for ${job.selection.modelFamily}`);
1204
1281
  }
1205
1282
 
1206
- if (!alreadyConfiguredInUi && familyEntry) {
1283
+ if (!alreadyConfiguredInUi && !familyAlreadySelectedInUi && familyEntry) {
1207
1284
  await clickRef(job, familyEntry.ref);
1208
1285
  await agentBrowser(job, "wait", "800");
1209
1286
  familySnapshot = await snapshotText(job);
1210
1287
  verificationSnapshot = familySnapshot;
1211
- familyEntry = findEntry(familySnapshot, (candidate) => matchesModelFamilyControl(candidate, job.selection.modelFamily));
1288
+ const postClickControlOptions = {
1289
+ ignoreCompactTierButtons: snapshotHasCompactIntelligenceMenuControls(familySnapshot),
1290
+ ignoreCompactOnlyButtons: snapshotHasLegacyEffortCombobox(familySnapshot),
1291
+ };
1292
+ familyEntry = findEntry(familySnapshot, (candidate) => matchesRequestedModelControl(candidate, job.selection, postClickControlOptions));
1212
1293
  if (!familyEntry && !snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection)) {
1213
1294
  throw new Error(`Requested model family did not remain selected: ${job.selection.modelFamily}`);
1214
1295
  }
@@ -1240,7 +1321,8 @@ async function configureModel(job) {
1240
1321
  if (job.selection.modelFamily === "instant") {
1241
1322
  const desiredAutoSwitchState = job.selection.autoSwitchToThinking === true;
1242
1323
  const currentAutoSwitchState = autoSwitchToThinkingSelectionVisible(familySnapshot);
1243
- if (currentAutoSwitchState !== desiredAutoSwitchState && (desiredAutoSwitchState || currentAutoSwitchState === true)) {
1324
+ const compactInstantAlreadyVerified = desiredAutoSwitchState && currentAutoSwitchState === undefined && snapshotStronglyMatchesRequestedModel(familySnapshot, job.selection);
1325
+ if (!compactInstantAlreadyVerified && currentAutoSwitchState !== desiredAutoSwitchState && (desiredAutoSwitchState || currentAutoSwitchState === true)) {
1244
1326
  await clickAutoSwitchToThinkingControl(job);
1245
1327
  await agentBrowser(job, "wait", "400");
1246
1328
  verificationSnapshot = await snapshotText(job);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
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",
@@ -32,7 +32,11 @@
32
32
  "docs",
33
33
  "README.md",
34
34
  "CHANGELOG.md",
35
- "LICENSE"
35
+ "LICENSE",
36
+ "platform-smoke.config.mjs",
37
+ "scripts/platform-smoke.mjs",
38
+ "scripts/platform-smoke",
39
+ "scripts/oracle-real-smoke.mjs"
36
40
  ],
37
41
  "pi": {
38
42
  "extensions": [
@@ -43,14 +47,29 @@
43
47
  ]
44
48
  },
45
49
  "scripts": {
46
- "check:oracle-extension": "node --check extensions/oracle/shared/process-helpers.mjs && node --check extensions/oracle/shared/state-coordination-helpers.mjs && node --check extensions/oracle/shared/job-coordination-helpers.mjs && node --check extensions/oracle/shared/job-lifecycle-helpers.mjs && node --check extensions/oracle/shared/job-observability-helpers.mjs && 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/chatgpt-flow-helpers.mjs && node --check extensions/oracle/worker/auth-flow-helpers.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/chromium-cookie-source.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@earendil-works/pi-coding-agent --external:@earendil-works/pi-ai --external:typebox --outfile=/tmp/pi-oracle-extension-check.js",
50
+ "check:oracle-extension": "node --check extensions/oracle/shared/browser-profile-helpers.mjs && node --check extensions/oracle/shared/process-helpers.mjs && node --check extensions/oracle/shared/state-coordination-helpers.mjs && node --check extensions/oracle/shared/job-coordination-helpers.mjs && node --check extensions/oracle/shared/job-lifecycle-helpers.mjs && node --check extensions/oracle/shared/job-observability-helpers.mjs && 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/chatgpt-flow-helpers.mjs && node --check extensions/oracle/worker/auth-flow-helpers.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/chromium-cookie-source.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@earendil-works/pi-coding-agent --external:typebox --outfile=/tmp/pi-oracle-extension-check.js",
47
51
  "typecheck": "tsc --noEmit -p tsconfig.json",
48
52
  "typecheck:worker-helpers": "tsc --noEmit -p tsconfig.worker-helpers.json",
49
53
  "sanity:oracle": "node scripts/oracle-sanity-runner.mjs",
50
54
  "pack:check": "npm pack --dry-run",
51
- "verify:oracle": "npm run check:oracle-extension && npm run typecheck && npm run typecheck:worker-helpers && npm run sanity:oracle && npm run pack:check",
55
+ "verify:oracle": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run typecheck && npm run typecheck:worker-helpers && npm run sanity:oracle && npm run pack:check",
52
56
  "test": "npm run verify:oracle",
53
- "prepublishOnly": "npm run verify:oracle"
57
+ "prepublishOnly": "npm run release:check",
58
+ "check:platform-smoke": "node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/assertions.mjs && node --check scripts/platform-smoke/artifacts.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/targets.mjs && node scripts/platform-smoke/invariants.mjs",
59
+ "smoke:platform": "node scripts/platform-smoke.mjs",
60
+ "smoke:platform:doctor": "node scripts/platform-smoke.mjs doctor",
61
+ "smoke:platform:ubuntu": "node scripts/platform-smoke.mjs run --target ubuntu",
62
+ "smoke:platform:all": "npm run smoke:platform:doctor && node scripts/platform-smoke.mjs run --target macos,ubuntu,windows-native",
63
+ "smoke:platform:macos": "node scripts/platform-smoke.mjs run --target macos",
64
+ "smoke:platform:windows-native": "node scripts/platform-smoke.mjs run --target windows-native",
65
+ "smoke:real": "npm run smoke:real:packed",
66
+ "smoke:real:doctor": "node scripts/oracle-real-smoke.mjs doctor",
67
+ "release:check": "npm run verify:oracle && npm run smoke:platform:all",
68
+ "check:oracle-real-smoke": "node --check scripts/oracle-real-smoke.mjs",
69
+ "smoke:real:packed": "node scripts/oracle-real-smoke.mjs run --mode packed",
70
+ "smoke:real:source": "node scripts/oracle-real-smoke.mjs run --mode source",
71
+ "sanity:oracle:platform": "node scripts/oracle-sanity-runner.mjs --mode platform",
72
+ "verify:oracle:platform": "npm run check:oracle-extension && npm run check:platform-smoke && npm run check:oracle-real-smoke && npm run sanity:oracle:platform && npm run pack:check"
54
73
  },
55
74
  "dependencies": {
56
75
  "@steipete/sweet-cookie": "^0.3.0"
@@ -64,9 +83,9 @@
64
83
  "protobufjs": "7.6.1"
65
84
  },
66
85
  "devDependencies": {
67
- "@earendil-works/pi-ai": "^0.77.0",
68
- "@earendil-works/pi-coding-agent": "^0.77.0",
69
- "@types/node": "^25.9.1",
86
+ "@earendil-works/pi-ai": "^0.78.1",
87
+ "@earendil-works/pi-coding-agent": "^0.78.1",
88
+ "@types/node": "^22.19.19",
70
89
  "esbuild": "^0.28.0",
71
90
  "tsx": "^4.22.3",
72
91
  "typebox": "^1.1.39",
@@ -76,7 +95,9 @@
76
95
  "node": ">=22.19.0"
77
96
  },
78
97
  "os": [
79
- "darwin"
98
+ "darwin",
99
+ "linux",
100
+ "win32"
80
101
  ],
81
102
  "packageManager": "npm@11.16.0",
82
103
  "peerDependenciesMeta": {
@@ -0,0 +1,66 @@
1
+ // Platform smoke configuration for pi-oracle.
2
+ // Crabbox is used as the local cross-platform release/readiness gate.
3
+
4
+ export default {
5
+ packageName: "pi-oracle",
6
+ artifactRoot: ".artifacts/platform-smoke",
7
+ requiredTargets: ["macos", "ubuntu", "windows-native"],
8
+ requiredSuites: ["platform-build", "real-extension"],
9
+ workflows: {
10
+ everyday: {
11
+ description: "Fast local validation for normal iteration.",
12
+ commands: ["npm run verify:oracle"],
13
+ },
14
+ platformSensitive: {
15
+ description: "Doctor plus focused platform target/suite runs for platform-sensitive changes.",
16
+ commands: [
17
+ "npm run smoke:platform:doctor",
18
+ "node scripts/platform-smoke.mjs run --target <target> --suite <suite>",
19
+ ],
20
+ },
21
+ platformMatrix: {
22
+ description: "Doctor-first packed-install macOS/Ubuntu/Windows platform proof.",
23
+ commands: ["npm run smoke:platform:all"],
24
+ },
25
+ release: {
26
+ description: "Full release gate: local verification plus the doctor-first platform matrix.",
27
+ commands: ["npm run release:check"],
28
+ },
29
+ },
30
+ requiredCrabbox: {
31
+ source: "https://github.com/openclaw/crabbox",
32
+ minVersion: "0.26.0",
33
+ },
34
+ ubuntuContainerImage: "pi-oracle-platform-smoke:node24",
35
+ ubuntuContainerBaseImage: "cimg/node:24.16",
36
+ windowsParallels: {
37
+ sourceVm: "pi-extension-windows-template",
38
+ snapshot: "crabbox-ready",
39
+ },
40
+ nodeValidationMajor: 24,
41
+ realSmoke: {
42
+ defaultProvider: "zai",
43
+ defaultModel: "glm-5.1",
44
+ authEnvByProvider: {
45
+ zai: ["ZAI_API_KEY"],
46
+ openai: ["OPENAI_API_KEY"],
47
+ anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"],
48
+ google: ["GEMINI_API_KEY"],
49
+ xai: ["XAI_API_KEY"],
50
+ groq: ["GROQ_API_KEY"],
51
+ deepseek: ["DEEPSEEK_API_KEY"],
52
+ cerebras: ["CEREBRAS_API_KEY"],
53
+ fireworks: ["FIREWORKS_API_KEY"],
54
+ together: ["TOGETHER_API_KEY"],
55
+ openrouter: ["OPENROUTER_API_KEY"],
56
+ ai_gateway: ["AI_GATEWAY_API_KEY"],
57
+ mistral: ["MISTRAL_API_KEY"],
58
+ minimax: ["MINIMAX_API_KEY"],
59
+ "minimax-cn": ["MINIMAX_CN_API_KEY"],
60
+ "ant-ling": ["ANT_LING_API_KEY"],
61
+ nvidia: ["NVIDIA_API_KEY"],
62
+ moonshot: ["MOONSHOT_API_KEY"],
63
+ kimi: ["KIMI_API_KEY"],
64
+ },
65
+ },
66
+ };