pi-oracle 0.6.17 → 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.
@@ -429,6 +429,15 @@ async function spawnCp(args: string[], options?: { timeoutMs?: number }): Promis
429
429
  });
430
430
  }
431
431
 
432
+ async function removeChromiumProcessSingletonArtifacts(profileDir: string): Promise<void> {
433
+ await Promise.all([
434
+ rm(join(profileDir, "SingletonLock"), { force: true }),
435
+ rm(join(profileDir, "SingletonSocket"), { force: true }),
436
+ rm(join(profileDir, "SingletonCookie"), { force: true }),
437
+ rm(join(profileDir, "DevToolsActivePort"), { force: true }),
438
+ ]);
439
+ }
440
+
432
441
  export async function cloneSeedProfileToRuntime(
433
442
  config: OracleConfig,
434
443
  runtimeProfileDir: string,
@@ -441,6 +450,7 @@ export async function cloneSeedProfileToRuntime(
441
450
  await rm(runtimeProfileDir, { recursive: true, force: true }).catch(() => undefined);
442
451
  await mkdir(dirname(runtimeProfileDir), { recursive: true, mode: 0o700 }).catch(() => undefined);
443
452
  await spawnCp(profileCloneArgs(config, seedDir, runtimeProfileDir), { timeoutMs: options?.cpTimeoutMs ?? PROFILE_CLONE_TIMEOUT_MS });
453
+ await removeChromiumProcessSingletonArtifacts(runtimeProfileDir);
444
454
  });
445
455
 
446
456
  return getSeedGeneration(config);
@@ -16,8 +16,12 @@ import { isLockTimeoutError, withGlobalReconcileLock, withLock } from "./locks.j
16
16
  import {
17
17
  coerceOracleSubmitPresetId,
18
18
  loadOracleConfig,
19
+ ORACLE_PROVIDERS,
19
20
  ORACLE_SUBMIT_PRESET_IDS,
21
+ resolveOracleConfigForProvider,
22
+ resolveOracleGrokMode,
20
23
  resolveOracleSubmitPreset,
24
+ type OracleProvider,
21
25
  } from "./config.js";
22
26
  import {
23
27
  appendCleanupWarnings,
@@ -59,7 +63,7 @@ import {
59
63
  } from "./runtime.js";
60
64
 
61
65
  const ORACLE_SUBMIT_PARAMS = Type.Object({
62
- prompt: Type.String({ description: "Prompt text to send to ChatGPT web." }),
66
+ prompt: Type.String({ description: "Prompt text to send to ChatGPT or Grok web." }),
63
67
  files: Type.Array(Type.String({
64
68
  description: "Project-relative file or directory path to include in the archive.",
65
69
  minLength: 1,
@@ -68,16 +72,35 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
68
72
  description: "Exact project-relative files/directories to include in the oracle archive.",
69
73
  minItems: 1,
70
74
  }),
75
+ provider: Type.Optional(
76
+ Type.String({
77
+ description: `Oracle web provider. Omit to use the configured default provider. Supported providers: ${ORACLE_PROVIDERS.join(", ")}. Use grok when the user asks to oracle to Grok. Grok archives are capped at 200 MiB.`,
78
+ }),
79
+ ),
71
80
  preset: Type.Optional(
72
81
  Type.String({
73
82
  description:
74
83
  `ChatGPT model preset. Omit to use the configured default preset. Canonical ids: ${ORACLE_SUBMIT_PRESET_IDS.join(", ")}. ` +
75
- "Matching human-readable preset labels and common hyphen/space variants are normalized automatically.",
84
+ "Matching human-readable preset labels and common hyphen/space variants are normalized automatically. Do not pass preset when provider is grok.",
85
+ }),
86
+ ),
87
+ mode: Type.Optional(
88
+ Type.String({
89
+ description: "Provider mode. For Grok, only heavy is currently supported. Omit to use the configured default mode.",
76
90
  }),
77
91
  ),
78
92
  followUpJobId: Type.Optional(Type.String({ description: "Earlier oracle job id whose chat thread should be continued." })),
79
93
  });
80
94
 
95
+ const ORACLE_PREFLIGHT_PARAMS = Type.Object({
96
+ provider: Type.Optional(Type.String({ description: `Provider readiness to check. Omit to use the configured default provider. Supported providers: ${ORACLE_PROVIDERS.join(", ")}.` })),
97
+ followUpJobId: Type.Optional(Type.String({ description: "Earlier oracle job id whose provider/thread readiness should be checked." })),
98
+ });
99
+
100
+ const ORACLE_AUTH_PARAMS = Type.Object({
101
+ provider: Type.Optional(Type.String({ description: `Provider auth seed to refresh. Omit to use the configured default provider. Supported providers: ${ORACLE_PROVIDERS.join(", ")}.` })),
102
+ });
103
+
81
104
  const ORACLE_READ_PARAMS = Type.Object({
82
105
  jobId: Type.String({ description: "Oracle job id." }),
83
106
  });
@@ -86,7 +109,9 @@ const ORACLE_CANCEL_PARAMS = Type.Object({
86
109
  jobId: Type.String({ description: "Oracle job id." }),
87
110
  });
88
111
 
89
- const MAX_ARCHIVE_BYTES = 250 * 1024 * 1024;
112
+ const CHATGPT_MAX_ARCHIVE_BYTES = 250 * 1024 * 1024;
113
+ const GROK_MAX_ARCHIVE_BYTES = 200 * 1024 * 1024;
114
+ const MAX_ARCHIVE_BYTES = CHATGPT_MAX_ARCHIVE_BYTES;
90
115
  const MAX_QUEUED_JOBS_PER_ACTIVE_RUNTIME = 1;
91
116
  const MAX_QUEUED_ARCHIVE_BYTES_PER_ACTIVE_RUNTIME = MAX_ARCHIVE_BYTES;
92
117
  const ARCHIVE_COMMAND_TIMEOUT_MS = 120_000;
@@ -347,7 +372,7 @@ function formatArchiveOversizeError(args: {
347
372
  const topLevel = summarizeTopLevelIncludedPaths(args.entrySizes);
348
373
  const adaptiveCandidates = summarizeAdaptivePruneCandidates(args.entrySizes, args.adaptivePruneMinBytes).slice(0, 7);
349
374
  return [
350
- `Oracle archive exceeds ChatGPT upload limit (${formatBytes(args.maxBytes)}) after default exclusions${args.autoPrunedPrefixes.length > 0 ? " and automatic generic generated-output-dir pruning" : ""}.`,
375
+ `Oracle archive exceeds provider upload limit (${formatBytes(args.maxBytes)}) after default exclusions${args.autoPrunedPrefixes.length > 0 ? " and automatic generic generated-output-dir pruning" : ""}.`,
351
376
  `The local archive measured ${formatBytes(args.archiveBytes)} (${args.archiveBytes} bytes), so submission stopped before dispatch.`,
352
377
  args.autoPrunedPrefixes.length > 0 ? "Automatically pruned generic generated-output paths before failing:" : undefined,
353
378
  ...args.autoPrunedPrefixes.map((entry) => `- ${formatDirectoryLabel(entry.relativePath)} — ${formatBytes(entry.bytes)}`),
@@ -497,7 +522,7 @@ export async function createArchiveForTesting(
497
522
  }
498
523
 
499
524
  const archiveBytes = await writeArchiveFile(cwd, expandedEntries, archivePath, listPath, { commandTimeoutMs: options?.commandTimeoutMs });
500
- if (archiveBytes < maxBytes) {
525
+ if (archiveBytes <= maxBytes) {
501
526
  return {
502
527
  sha256: await sha256File(archivePath),
503
528
  archiveBytes,
@@ -528,8 +553,29 @@ export async function createArchiveForTesting(
528
553
  }
529
554
  }
530
555
 
531
- async function createArchive(cwd: string, files: string[], archivePath: string): Promise<ArchiveCreationResult> {
532
- return createArchiveForTesting(cwd, files, archivePath);
556
+ async function createArchive(cwd: string, files: string[], archivePath: string, maxBytes = MAX_ARCHIVE_BYTES): Promise<ArchiveCreationResult> {
557
+ return createArchiveForTesting(cwd, files, archivePath, { maxBytes });
558
+ }
559
+
560
+ function normalizeOracleProvider(value: unknown, fallback: OracleProvider, toolName = "oracle_submit"): OracleProvider {
561
+ if (value === undefined) return fallback;
562
+ if (typeof value !== "string") throw new Error(`${toolName} provider must be a string`);
563
+ const normalized = value.trim().toLowerCase();
564
+ if (normalized === "chatgpt" || normalized === "chat-gpt" || normalized === "openai") return "chatgpt";
565
+ if (normalized === "grok" || normalized === "xai" || normalized === "x.ai") return "grok";
566
+ throw new Error(`Unknown ${toolName} provider: ${value}. Use chatgpt or grok.`);
567
+ }
568
+
569
+ function normalizeGrokMode(value: unknown, fallback: "heavy"): "heavy" {
570
+ if (value === undefined) return fallback;
571
+ if (typeof value !== "string") throw new Error("oracle_submit mode must be a string");
572
+ const normalized = value.trim().toLowerCase();
573
+ if (normalized === "heavy" || normalized === "grok heavy" || normalized === "grok-heavy") return "heavy";
574
+ throw new Error(`Unknown Grok oracle mode: ${value}. Only heavy is currently supported.`);
575
+ }
576
+
577
+ function getProviderMaxArchiveBytes(provider: "chatgpt" | "grok"): number {
578
+ return provider === "grok" ? GROK_MAX_ARCHIVE_BYTES : CHATGPT_MAX_ARCHIVE_BYTES;
533
579
  }
534
580
 
535
581
  export interface QueuedArchivePressure {
@@ -590,6 +636,7 @@ function resolveFollowUp(previousJobId: string | undefined, cwd: string): {
590
636
  followUpToJobId?: string;
591
637
  chatUrl?: string;
592
638
  conversationId?: string;
639
+ provider?: "chatgpt" | "grok";
593
640
  } {
594
641
  if (!previousJobId) return {};
595
642
  const previous = readJob(previousJobId);
@@ -609,6 +656,7 @@ function resolveFollowUp(previousJobId: string | undefined, cwd: string): {
609
656
  followUpToJobId: previous.id,
610
657
  chatUrl: previous.chatUrl,
611
658
  conversationId: previous.conversationId || parseConversationId(previous.chatUrl),
659
+ provider: previous.selection?.provider === "grok" ? "grok" : "chatgpt",
612
660
  };
613
661
  }
614
662
 
@@ -898,7 +946,7 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
898
946
  };
899
947
  }
900
948
 
901
- if (toolName === "oracle_submit" && message.startsWith("Oracle archive exceeds ChatGPT upload limit")) {
949
+ if (toolName === "oracle_submit" && (message.startsWith("Oracle archive exceeds provider upload limit") || message.startsWith("Oracle archive exceeds ChatGPT upload limit"))) {
902
950
  return {
903
951
  code: "archive_too_large",
904
952
  message,
@@ -937,6 +985,7 @@ function buildOracleToolErrorResult(
937
985
 
938
986
  type OraclePreflightDetails = {
939
987
  ready: boolean;
988
+ provider?: OracleProvider;
940
989
  session: {
941
990
  persisted: boolean;
942
991
  sessionFile?: string;
@@ -951,25 +1000,32 @@ type OraclePreflightDetails = {
951
1000
  error?: OracleToolErrorDetails;
952
1001
  };
953
1002
 
1003
+ function formatOracleProviderLabel(provider: OracleProvider | undefined): string {
1004
+ if (provider === "grok") return "Grok";
1005
+ if (provider === "chatgpt") return "ChatGPT";
1006
+ return "configured provider";
1007
+ }
1008
+
954
1009
  function formatOraclePreflightResponse(details: OraclePreflightDetails): string {
1010
+ const providerLabel = formatOracleProviderLabel(details.provider);
955
1011
  if (details.ready) {
956
1012
  return [
957
- "Oracle preflight ready.",
1013
+ `Oracle preflight ready for ${providerLabel}.`,
958
1014
  details.session.sessionFile ? `Persisted session: ${details.session.sessionFile}` : undefined,
959
1015
  details.auth.seedProfileDir ? `Auth seed profile: ${details.auth.seedProfileDir}` : undefined,
960
- "Preflight validates the persisted pi session, local oracle config, and ChatGPT auth seed created by oracle_auth.",
1016
+ `Preflight validates the persisted pi session, local oracle config, and ${providerLabel} auth seed created by oracle_auth.`,
961
1017
  "You can continue with oracle context gathering and submission.",
962
1018
  ].filter(Boolean).join("\n");
963
1019
  }
964
1020
 
965
1021
  return [
966
1022
  `Oracle preflight blocked: ${details.error?.message ?? "unknown blocker"}`,
967
- "Preflight checks the persisted pi session, local oracle config, and ChatGPT auth seed before any archive work starts.",
1023
+ `Preflight checks the persisted pi session, local oracle config, and ${providerLabel} auth seed before any archive work starts.`,
968
1024
  details.error?.suggestedNextStep ? `Suggested next step: ${details.error.suggestedNextStep}` : undefined,
969
1025
  ].filter(Boolean).join("\n");
970
1026
  }
971
1027
 
972
- async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePreflightDetails> {
1028
+ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: unknown; followUpJobId?: unknown } = {}): Promise<OraclePreflightDetails> {
973
1029
  const sessionFile = getSessionFile(ctx);
974
1030
  if (!hasPersistedSessionFile(sessionFile)) {
975
1031
  return {
@@ -986,11 +1042,23 @@ async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePrefligh
986
1042
  }
987
1043
 
988
1044
  let config;
1045
+ let provider: OracleProvider | undefined;
989
1046
  try {
990
- config = loadOracleConfig(ctx.cwd);
1047
+ const followUpJobId = params.followUpJobId;
1048
+ if (followUpJobId !== undefined && typeof followUpJobId !== "string") {
1049
+ throw new Error("oracle_preflight followUpJobId must be a string");
1050
+ }
1051
+ const baseConfig = loadOracleConfig(ctx.cwd);
1052
+ const followUp = resolveFollowUp(followUpJobId, ctx.cwd);
1053
+ provider = normalizeOracleProvider(params.provider, followUp.provider ?? baseConfig.defaults.provider, "oracle_preflight");
1054
+ if (followUp.provider && provider !== followUp.provider) {
1055
+ throw new Error(`Follow-up job ${followUpJobId} uses provider ${followUp.provider}; cannot check it with ${provider}.`);
1056
+ }
1057
+ config = resolveOracleConfigForProvider(baseConfig, provider);
991
1058
  } catch (error) {
992
1059
  return {
993
1060
  ready: false,
1061
+ provider,
994
1062
  session: { persisted: true, sessionFile },
995
1063
  config: { ready: false },
996
1064
  auth: { ready: false },
@@ -1004,6 +1072,7 @@ async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePrefligh
1004
1072
  const errorDetails = buildOracleToolErrorDetails("oracle_preflight", error, {});
1005
1073
  return {
1006
1074
  ready: false,
1075
+ provider,
1007
1076
  session: { persisted: true, sessionFile },
1008
1077
  config: { ready: true },
1009
1078
  auth: {
@@ -1016,6 +1085,7 @@ async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePrefligh
1016
1085
 
1017
1086
  return {
1018
1087
  ready: true,
1088
+ provider,
1019
1089
  session: { persisted: true, sessionFile },
1020
1090
  config: { ready: true },
1021
1091
  auth: {
@@ -1042,11 +1112,11 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1042
1112
  description: "Check whether oracle is ready in this session before spending time gathering context or preparing a submission.",
1043
1113
  promptSnippet: "Check oracle readiness before expensive /oracle preparation.",
1044
1114
  promptGuidelines: [
1045
- "Call oracle_preflight before doing expensive /oracle preparation. If ready is false, stop immediately and report the suggested next step instead of reading files or crafting archive inputs.",
1115
+ "Call oracle_preflight before doing expensive /oracle preparation. Pass provider='grok' when the user explicitly asks for Grok, or followUpJobId for same-thread follow-ups. If ready is false, stop immediately and report the suggested next step instead of reading files or crafting archive inputs.",
1046
1116
  ],
1047
- parameters: Type.Object({}),
1048
- async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1049
- const details = await runOraclePreflight(ctx);
1117
+ parameters: ORACLE_PREFLIGHT_PARAMS,
1118
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1119
+ const details = await runOraclePreflight(ctx, params);
1050
1120
  return {
1051
1121
  content: [{ type: "text" as const, text: formatOraclePreflightResponse(details) }],
1052
1122
  details,
@@ -1057,23 +1127,26 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1057
1127
  pi.registerTool({
1058
1128
  name: "oracle_auth",
1059
1129
  label: "Oracle Auth",
1060
- description: "Refresh the shared oracle auth seed profile by importing ChatGPT cookies from your configured local browser profile.",
1130
+ description: "Refresh the shared oracle auth seed profile by importing ChatGPT or Grok cookies from your configured local browser profile, based on the configured default provider.",
1061
1131
  promptSnippet: "Refresh oracle auth before retrying a login-required oracle run.",
1062
1132
  promptGuidelines: [
1063
- "Call oracle_auth when an oracle run failed because ChatGPT login is required, the worker said to rerun /oracle-auth, or stale auth appears to be blocking submission execution.",
1133
+ "Call oracle_auth when an oracle run failed because ChatGPT or Grok login is required, the worker said to rerun /oracle-auth, or stale auth appears to be blocking submission execution. Pass provider='grok' when refreshing Grok auth.",
1064
1134
  "At most once per user request, refresh auth and then retry the blocked oracle submission.",
1065
1135
  "If oracle_auth itself fails, stop and report the failure instead of looping.",
1066
1136
  ],
1067
- parameters: Type.Object({}),
1068
- async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
1137
+ parameters: ORACLE_AUTH_PARAMS,
1138
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1069
1139
  try {
1070
1140
  const projectCwd = getProjectId(ctx.cwd);
1071
- const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd);
1141
+ const baseConfig = loadOracleConfig(projectCwd);
1142
+ const provider = normalizeOracleProvider(params.provider, baseConfig.defaults.provider, "oracle_auth");
1143
+ const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd, provider);
1072
1144
  return {
1073
1145
  content: [{ type: "text" as const, text: message }],
1074
1146
  details: {
1075
1147
  refreshed: true,
1076
- authSeedProfileDir: loadOracleConfig(projectCwd).browser.authSeedProfileDir,
1148
+ provider,
1149
+ authSeedProfileDir: resolveOracleConfigForProvider(baseConfig, provider).browser.authSeedProfileDir,
1077
1150
  },
1078
1151
  };
1079
1152
  } catch (error) {
@@ -1086,13 +1159,13 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1086
1159
  name: "oracle_submit",
1087
1160
  label: "Oracle Submit",
1088
1161
  description:
1089
- "Dispatch a background ChatGPT web oracle job after gathering context. Always pass a prompt and exact project-relative archive inputs. " +
1090
- "Optional ChatGPT model: set parameter `preset`, or omit it for configured defaults; canonical preset ids are listed in the README and ORACLE_SUBMIT_PRESETS registry, and matching labels are normalized at submit time.",
1091
- promptSnippet: "Dispatch a background ChatGPT web oracle job after gathering repo context.",
1162
+ "Dispatch a background ChatGPT or Grok web oracle job after gathering context. Always pass a prompt and exact project-relative archive inputs. " +
1163
+ "Optional provider: set `provider` to `grok` when the user asks for Grok; Grok currently supports only Heavy. Optional ChatGPT model: set parameter `preset`, or omit it for configured defaults; canonical preset ids are listed in the README and ORACLE_SUBMIT_PRESETS registry, and matching labels are normalized at submit time.",
1164
+ promptSnippet: "Dispatch a background ChatGPT or Grok web oracle job after gathering repo context.",
1092
1165
  promptGuidelines: [
1093
1166
  "Gather context before calling oracle_submit.",
1094
- "If the immediately preceding oracle run failed because ChatGPT login is required or the worker explicitly said to rerun /oracle-auth, call oracle_auth once before retrying the submission. Do not loop auth refreshes.",
1095
- "Prefer context-rich archives up to the 250 MB ceiling because more relevant surrounding context is usually better than less.",
1167
+ "If the immediately preceding oracle run failed because ChatGPT or Grok login is required or the worker explicitly said to rerun /oracle-auth, call oracle_auth once before retrying the submission; pass provider='grok' for Grok retries. Do not loop auth refreshes.",
1168
+ "Prefer context-rich archives up to the provider ceiling because more relevant surrounding context is usually better than less: 250 MB for ChatGPT and 200 MiB for Grok.",
1096
1169
  "By default, archive the whole repo by passing '.' for broad or unclear requests; 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.",
1097
1170
  "For narrower asks, still include nearby tests, docs, configs, and adjacent modules when they may improve answer quality. Only narrow aggressively when the user explicitly asks, privacy/sensitivity requires it, or size pressure forces it.",
1098
1171
  "Do not default to a one-file archive for a single function, file, or stack trace if the relevant surrounding context still fits comfortably within the limit.",
@@ -1104,21 +1177,31 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1104
1177
  "For any other submit-time error, stop and report the error instead of retrying automatically.",
1105
1178
  "If oracle_submit returns a queued job instead of an immediately dispatched one, treat that as success and stop exactly the same way.",
1106
1179
  "After a successful or queued oracle_submit, stop; do not continue the task while the oracle job is running. If oracle_submit failed with retryable archive_too_large, narrow the archive and retry first.",
1107
- "Use `preset` as the only model-selection parameter on oracle_submit. " +
1180
+ "For ChatGPT, use `preset` as the only model-selection parameter on oracle_submit. " +
1108
1181
  `Canonical ids: ${ORACLE_SUBMIT_PRESET_IDS.join(", ")}. ` +
1109
- "matching human-readable preset labels are normalized automatically. Omit preset to use the configured default.",
1182
+ "matching human-readable preset labels are normalized automatically. Omit preset to use the configured default. For Grok, pass provider='grok' and omit preset; only Heavy is supported today.",
1110
1183
  ],
1111
1184
  parameters: ORACLE_SUBMIT_PARAMS,
1112
1185
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1113
1186
  try {
1114
1187
  const projectCwd = getProjectId(ctx.cwd);
1115
- const config = loadOracleConfig(projectCwd);
1188
+ const baseConfig = loadOracleConfig(projectCwd);
1116
1189
  const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
1117
1190
  const projectId = getProjectId(projectCwd);
1118
1191
  const sessionId = getSessionId(originSessionFile, projectId);
1119
- const presetId = typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : config.defaults.preset;
1120
- const selection = resolveOracleSubmitPreset(presetId);
1121
1192
  const followUp = resolveFollowUp(params.followUpJobId, projectCwd);
1193
+ const provider = normalizeOracleProvider(params.provider, followUp.provider ?? baseConfig.defaults.provider, "oracle_submit");
1194
+ if (followUp.provider && provider !== followUp.provider) {
1195
+ throw new Error(`Follow-up job ${params.followUpJobId} uses provider ${followUp.provider}; cannot continue it with ${provider}.`);
1196
+ }
1197
+ if (provider === "grok" && typeof params.preset === "string") {
1198
+ throw new Error("oracle_submit preset is only valid for ChatGPT. For Grok, use provider='grok' and mode='heavy'.");
1199
+ }
1200
+ const selection = provider === "grok"
1201
+ ? resolveOracleGrokMode(normalizeGrokMode(params.mode, baseConfig.defaults.grokMode))
1202
+ : resolveOracleSubmitPreset(typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : baseConfig.defaults.preset);
1203
+ const config = resolveOracleConfigForProvider(baseConfig, provider);
1204
+ const targetChatUrl = followUp.chatUrl;
1122
1205
  // Validate caller-specified archive paths before surfacing unrelated local setup failures such as a missing auth seed profile.
1123
1206
  resolveArchiveInputs(projectCwd, params.files);
1124
1207
  await assertOracleSubmitPrerequisites(config);
@@ -1144,7 +1227,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1144
1227
  let spawnedWorker: Awaited<ReturnType<typeof spawnWorker>> | undefined;
1145
1228
 
1146
1229
  try {
1147
- archive = await createArchive(projectCwd, params.files, tempArchivePath);
1230
+ archive = await createArchive(projectCwd, params.files, tempArchivePath, getProviderMaxArchiveBytes(selection.provider));
1148
1231
  const currentArchive = archive;
1149
1232
  await withLock("admission", "global", { jobId, processPid: process.pid }, async () => {
1150
1233
  await promoteQueuedJobsWithinAdmissionLock({ workerPath, source: "oracle_submit" });
@@ -1184,7 +1267,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1184
1267
  files: params.files,
1185
1268
  selection,
1186
1269
  followUpToJobId: followUp.followUpToJobId,
1187
- chatUrl: followUp.chatUrl,
1270
+ chatUrl: targetChatUrl,
1188
1271
  requestSource: "tool",
1189
1272
  },
1190
1273
  projectCwd,
@@ -1227,7 +1310,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1227
1310
  files: params.files,
1228
1311
  selection,
1229
1312
  followUpToJobId: followUp.followUpToJobId,
1230
- chatUrl: followUp.chatUrl,
1313
+ chatUrl: targetChatUrl,
1231
1314
  requestSource: "tool",
1232
1315
  },
1233
1316
  projectCwd,
@@ -1369,6 +1452,8 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1369
1452
  name: "oracle_read",
1370
1453
  label: "Oracle Read",
1371
1454
  description: "Read the status and outputs of a previously dispatched oracle job.",
1455
+ promptSnippet: "Read oracle job status, queue position, artifacts, and response preview by job id.",
1456
+ promptGuidelines: ["Use oracle_read when the user asks for the status, output, or artifacts of a previously submitted oracle job."],
1372
1457
  parameters: ORACLE_READ_PARAMS,
1373
1458
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1374
1459
  try {
@@ -1427,6 +1512,8 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1427
1512
  name: "oracle_cancel",
1428
1513
  label: "Oracle Cancel",
1429
1514
  description: "Cancel a queued or active oracle job.",
1515
+ promptSnippet: "Cancel a queued or active oracle background job by job id.",
1516
+ promptGuidelines: ["Use oracle_cancel only when the user explicitly asks to stop a queued or active oracle job."],
1430
1517
  parameters: ORACLE_CANCEL_PARAMS,
1431
1518
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
1432
1519
  try {
@@ -13,7 +13,9 @@ export interface OracleJobSummaryLike {
13
13
  projectId: string;
14
14
  sessionId: string;
15
15
  selection?: {
16
+ provider?: string;
16
17
  preset?: string;
18
+ mode?: string;
17
19
  modelFamily?: string;
18
20
  effort?: string;
19
21
  autoSwitchToThinking?: boolean;
@@ -51,13 +51,17 @@ function formatAutoPrunedArchiveMessage(autoPrunedPrefixes) {
51
51
  * @returns {string | undefined}
52
52
  */
53
53
  function formatOracleSelection(selection) {
54
- if (!selection?.preset || !selection.modelFamily) return undefined;
54
+ if (!selection?.modelFamily) return undefined;
55
55
  const details = [
56
+ selection.provider ? `provider=${selection.provider}` : undefined,
56
57
  `family=${selection.modelFamily}`,
58
+ selection.mode ? `mode=${selection.mode}` : undefined,
57
59
  selection.effort ? `effort=${selection.effort}` : undefined,
58
60
  selection.autoSwitchToThinking === true ? "auto-switch-to-thinking=true" : undefined,
59
61
  ].filter(Boolean).join(", ");
60
- return `Model preset: ${selection.preset}${details ? ` (${details})` : ""}`;
62
+ if (selection.preset) return `Model preset: ${selection.preset}${details ? ` (${details})` : ""}`;
63
+ if (selection.provider === "grok") return `Provider model: Grok${selection.mode ? ` ${selection.mode}` : ""}${details ? ` (${details})` : ""}`;
64
+ return `Model selection: ${details}`;
61
65
  }
62
66
 
63
67
  const ACTIVE_SUMMARY_STATUSES = new Set(["preparing", "submitted", "waiting"]);
@@ -1,4 +1,4 @@
1
- // Purpose: Bootstrap isolated oracle browser auth by importing real Chromium-family cookies and validating ChatGPT session readiness.
1
+ // Purpose: Bootstrap isolated oracle browser auth by importing real Chromium-family cookies and validating provider session readiness.
2
2
  // Responsibilities: Copy/import cookies, classify auth pages, drive lightweight account-selection flows, and persist diagnostics for auth failures.
3
3
  // Scope: Auth bootstrap worker only; long-running oracle job execution stays in run-job.mjs and shared lifecycle/state helpers stay elsewhere.
4
4
  // Usage: Spawned by /oracle-auth to prepare the shared auth seed profile used by future oracle jobs.
@@ -51,6 +51,7 @@ const CHATGPT_COOKIE_ORIGINS = [
51
51
  "https://sentinel.openai.com",
52
52
  "https://ws.chatgpt.com",
53
53
  ];
54
+ const GROK_COOKIE_ORIGINS = ["https://grok.com", "https://x.ai", "https://x.com"];
54
55
  let DIAGNOSTICS_DIR;
55
56
  let LOG_PATH = "(oracle-auth log path unavailable)";
56
57
  let URL_PATH = "(oracle-auth url path unavailable)";
@@ -75,6 +76,7 @@ function readPositiveIntEnv(name, fallback) {
75
76
  const AGENT_BROWSER_COMMAND_TIMEOUT_MS = readPositiveIntEnv("PI_ORACLE_AUTH_AGENT_BROWSER_TIMEOUT_MS", 30_000);
76
77
  const AGENT_BROWSER_CLOSE_TIMEOUT_MS = readPositiveIntEnv("PI_ORACLE_AUTH_CLOSE_TIMEOUT_MS", 10_000);
77
78
  const AGENT_BROWSER_KILL_GRACE_MS = readPositiveIntEnv("PI_ORACLE_AUTH_KILL_GRACE_MS", 2_000);
79
+ const COOKIE_READ_TIMEOUT_MS = readPositiveIntEnv("PI_ORACLE_AUTH_COOKIE_READ_TIMEOUT_MS", 30_000);
78
80
 
79
81
  let runtimeProfileDir = config.browser.authSeedProfileDir;
80
82
 
@@ -455,8 +457,21 @@ function stripQuery(url) {
455
457
  }
456
458
  }
457
459
 
460
+ function preferredProvider() {
461
+ return config?.defaults?.provider === "grok" ? "grok" : "chatgpt";
462
+ }
463
+
464
+ function providerChatUrl() {
465
+ return preferredProvider() === "grok" ? "https://grok.com/" : config.browser.chatUrl;
466
+ }
467
+
468
+ function providerName() {
469
+ return preferredProvider() === "grok" ? "Grok" : "ChatGPT";
470
+ }
471
+
458
472
  function cookieOrigins() {
459
- return Array.from(new Set([stripQuery(config.browser.chatUrl), ...CHATGPT_COOKIE_ORIGINS]));
473
+ const providerOrigins = preferredProvider() === "grok" ? GROK_COOKIE_ORIGINS : CHATGPT_COOKIE_ORIGINS;
474
+ return Array.from(new Set([stripQuery(providerChatUrl()), ...providerOrigins]));
460
475
  }
461
476
 
462
477
  function cookieSource() {
@@ -513,13 +528,13 @@ function formatAuthFailureGuidance(error) {
513
528
  if (usesConfiguredChromiumCookieSource()) {
514
529
  lines.push(
515
530
  "- the configured cookie DB is stale or from the wrong browser profile",
516
- "- ChatGPT is logged out in that browser profile",
531
+ `- ${providerName()} is logged out in that browser profile`,
517
532
  "- auth.chromiumKeychain does not match the browser safe-storage item for that cookie DB",
518
533
  "- the target browser was still running while /oracle-auth read its Cookies DB",
519
534
  "",
520
535
  "Next:",
521
536
  "1. Open the configured browser profile.",
522
- "2. Confirm ChatGPT works there.",
537
+ `2. Confirm ${providerName()} works there.`,
523
538
  "3. Quit the browser fully.",
524
539
  "4. Confirm auth.chromeCookiePath points at that profile's Cookies DB.",
525
540
  "5. Confirm auth.chromiumKeychain.services names the browser's safe-storage Keychain service.",
@@ -527,13 +542,13 @@ function formatAuthFailureGuidance(error) {
527
542
  );
528
543
  } else {
529
544
  lines.push(
530
- "- ChatGPT is logged out in the configured local browser profile",
545
+ `- ${providerName()} is logged out in the configured local browser profile`,
531
546
  "- auth.chromeProfile or auth.chromeCookiePath points at the wrong profile",
532
547
  "- the profile cookie store is stale or unreadable",
533
548
  "",
534
549
  "Next:",
535
550
  "1. Open the configured local browser profile.",
536
- "2. Confirm ChatGPT works there.",
551
+ `2. Confirm ${providerName()} works there.`,
537
552
  "3. Quit the browser fully.",
538
553
  "4. Re-run /oracle-auth.",
539
554
  );
@@ -561,29 +576,29 @@ async function readRawSourceCookies() {
561
576
  keychain: config.auth.chromiumKeychain,
562
577
  origins: cookieOrigins(),
563
578
  profile: config.auth.chromeProfile,
564
- timeoutMs: 5_000,
579
+ timeoutMs: COOKIE_READ_TIMEOUT_MS,
565
580
  });
566
581
  }
567
582
 
568
583
  return await getCookies({
569
- url: config.browser.chatUrl,
584
+ url: providerChatUrl(),
570
585
  origins: cookieOrigins(),
571
586
  browsers: ["chrome"],
572
587
  mode: "merge",
573
588
  chromeProfile: cookieSource(),
574
- timeoutMs: 5_000,
589
+ timeoutMs: COOKIE_READ_TIMEOUT_MS,
575
590
  });
576
591
  }
577
592
 
578
593
  async function readSourceCookies() {
579
- await log(`Reading ChatGPT cookies from ${cookieSourceLabel()}`);
594
+ await log(`Reading ${providerName()} cookies from ${cookieSourceLabel()}`);
580
595
  const { cookies, warnings } = await readRawSourceCookies();
581
596
 
582
597
  if (warnings.length) {
583
598
  await log(`Cookie source warnings: ${warnings.join(" | ")}`);
584
599
  }
585
600
 
586
- const filtered = filterImportableAuthCookies(cookies, config.browser.chatUrl);
601
+ const filtered = filterImportableAuthCookies(cookies, providerChatUrl());
587
602
  let normalizedCookies = filtered.cookies;
588
603
  await log(
589
604
  `Read ${normalizedCookies.length} filtered auth cookies: ${normalizedCookies.map((cookie) => `${cookie.name}@${cookie.domain}`).join(", ")}`,
@@ -599,6 +614,13 @@ async function readSourceCookies() {
599
614
  const hasAccountCookie = normalizedCookies.some((cookie) => cookie.name === "_account");
600
615
  await log(`Cookie presence: sessionToken=${hasSessionToken} account=${hasAccountCookie}`);
601
616
 
617
+ if (preferredProvider() === "grok") {
618
+ if (normalizedCookies.length === 0) {
619
+ throw new Error(`No Grok cookies were found in ${cookieSourceLabel()}. Make sure Grok is logged into that browser profile. ${authConfigRemediation()}`);
620
+ }
621
+ return normalizedCookies;
622
+ }
623
+
602
624
  if (!hasSessionToken) {
603
625
  throw new Error(
604
626
  `No ChatGPT session-token cookies were found in ${cookieSourceLabel()}. Make sure ChatGPT is logged into that browser profile. ${authConfigRemediation()}`,
@@ -626,7 +648,7 @@ function cookieSetArgs(cookie) {
626
648
  }
627
649
 
628
650
  async function seedCookiesIntoTarget(cookies) {
629
- await log("Clearing isolated browser cookies before seeding fresh ChatGPT cookies");
651
+ await log(`Clearing isolated browser cookies before seeding fresh ${providerName()} cookies`);
630
652
  await targetCommand("cookies", "clear", { logLabel: "cookies clear" });
631
653
 
632
654
  let applied = 0;
@@ -764,6 +786,7 @@ async function captureDiagnostics(reason) {
764
786
  }
765
787
 
766
788
  function classifyChatPage({ url, snapshot, body, probe }) {
789
+ if (preferredProvider() === "grok") return classifyGrokAuthPage({ url, snapshot, body });
767
790
  return classifyChatAuthPage({
768
791
  url,
769
792
  snapshot,
@@ -778,6 +801,31 @@ function classifyChatPage({ url, snapshot, body, probe }) {
778
801
  });
779
802
  }
780
803
 
804
+ function hasGrokLoginCta(text) {
805
+ const lines = String(text || "").split("\n").map((line) => line.trim()).filter(Boolean);
806
+ return lines.some((line) => {
807
+ const accessibleControl = line.match(/^-\s*(?:button|link|menuitem)\s+"([^"]+)"/i)?.[1]?.trim();
808
+ const label = accessibleControl || line;
809
+ return /^(?:sign in|log in|continue with x|continue with google|create account)$/i.test(label);
810
+ });
811
+ }
812
+
813
+ function classifyGrokAuthPage({ url, snapshot, body }) {
814
+ const text = `${snapshot}\n${body}`;
815
+ if (/captcha|cloudflare|verify you are human|unusual activity|suspicious activity/i.test(text)) {
816
+ return { state: "challenge_blocking", message: "Grok is showing a challenge/verification page" };
817
+ }
818
+ if (/something went wrong|network error|try again later|rate limit/i.test(text)) {
819
+ return { state: "transient_outage_error", message: "Grok is showing a transient outage/error page" };
820
+ }
821
+ const onGrokOrigin = typeof url === "string" && url.startsWith("https://grok.com");
822
+ const hasComposer = snapshot.includes('button "Attach"') && (snapshot.includes('textbox "Ask Grok anything"') || snapshot.includes("contenteditable"));
823
+ if (onGrokOrigin && hasGrokLoginCta(text)) return { state: "login_required", message: `Grok login is required. Sign in to Grok in ${cookieSourceLabel()}.` };
824
+ if (onGrokOrigin && hasComposer) return { state: "authenticated_and_ready", message: "Grok is authenticated and ready." };
825
+ if (url && !onGrokOrigin) return { state: "login_required", message: "Grok redirected away from grok.com." };
826
+ return { state: "unknown", message: "Grok page is not ready yet." };
827
+ }
828
+
781
829
  async function maybeSelectAccountIdentity(snapshot, probe) {
782
830
  const candidates = buildAccountChooserCandidateLabels(probe?.name);
783
831
 
@@ -827,7 +875,7 @@ async function waitForImportedAuthReady() {
827
875
  await writeFile(BODY_PATH, `${body}\n`, { mode: 0o600 }).catch(() => undefined);
828
876
  const classification = classifyChatPage({ url, snapshot, body, probe });
829
877
  await log(
830
- `poll ${iteration}: url=${JSON.stringify(url)} probe=${JSON.stringify(probe)} classification=${classification.state} hasComposer=${snapshot.includes(`textbox \"${CHATGPT_LABELS.composer}\"`)} hasAddFiles=${snapshot.includes(`button \"${CHATGPT_LABELS.addFiles}\"`)}`,
878
+ `poll ${iteration}: url=${JSON.stringify(url)} probe=${JSON.stringify(probe)} classification=${classification.state} hasComposer=${preferredProvider() === "grok" ? snapshot.includes('Ask Grok anything') || snapshot.includes('contenteditable') : snapshot.includes(`textbox \"${CHATGPT_LABELS.composer}\"`)} hasAddFiles=${preferredProvider() === "grok" ? snapshot.includes('button \"Attach\"') : snapshot.includes(`button \"${CHATGPT_LABELS.addFiles}\"`)}`,
831
879
  );
832
880
  if (classification.state === "authenticated_and_ready") return classification;
833
881
  if (classification.state === "auth_transitioning") {
@@ -880,7 +928,7 @@ async function waitForImportedAuthReady() {
880
928
  await sleep(config.auth.pollMs);
881
929
  }
882
930
  await captureDiagnostics("timeout");
883
- throw new Error(`Timed out verifying synced ChatGPT cookies in the isolated oracle profile. Logs: ${LOG_PATH}`);
931
+ throw new Error(`Timed out verifying synced ${providerName()} cookies in the isolated oracle profile. Logs: ${LOG_PATH}`);
884
932
  }
885
933
 
886
934
  async function run() {
@@ -901,7 +949,7 @@ async function run() {
901
949
  await launchTargetBrowser();
902
950
  const appliedCount = await seedCookiesIntoTarget(cookies);
903
951
  await log(`Cookie seeding complete: applied=${appliedCount}`);
904
- await openUrl(config.browser.chatUrl, config.browser.chatUrl);
952
+ await openUrl(providerChatUrl(), providerChatUrl());
905
953
  const classification = await waitForImportedAuthReady();
906
954
  await log(`Auth bootstrap success: ${classification.message}`);
907
955
  await closeTargetBrowser();