@andrewting19/oracle 0.10.2 → 0.11.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.
@@ -470,8 +470,11 @@ function resolveHeartbeatIntervalMs(seconds) {
470
470
  return Math.round(seconds * 1000);
471
471
  }
472
472
  function assertFollowupSupported({ engine, model, baseUrl, azureEndpoint, }) {
473
+ // Browser engine: follow-up navigates to an existing ChatGPT conversation URL.
474
+ if (engine === "browser")
475
+ return;
473
476
  if (engine !== "api") {
474
- throw new Error("--followup requires --engine api.");
477
+ throw new Error("--followup requires --engine api or --engine browser.");
475
478
  }
476
479
  if (model.startsWith("gemini") || model.startsWith("claude")) {
477
480
  throw new Error(`--followup is only supported for OpenAI Responses API runs. Model ${model} uses a provider client without previous_response_id support.`);
@@ -538,7 +541,7 @@ async function suggestFollowupSessionIds(input, limit = 3) {
538
541
  .slice(0, limit);
539
542
  return ranked.map((entry) => entry.id);
540
543
  }
541
- async function resolveFollowupReference(value, followupModel) {
544
+ async function resolveFollowupReference(value, followupModel, engine) {
542
545
  const trimmed = value.trim();
543
546
  if (trimmed.length === 0) {
544
547
  throw new Error("--followup requires a session id or response id.");
@@ -555,6 +558,14 @@ async function resolveFollowupReference(value, followupModel) {
555
558
  : "";
556
559
  throw new Error(`No session found with ID ${trimmed}.${suggestionText} Run "oracle status --hours 72 --limit 20" to list recent sessions.`);
557
560
  }
561
+ // Browser follow-up: extract conversation URL from the parent session.
562
+ if (engine === "browser") {
563
+ const conversationUrl = meta.browser?.runtime?.tabUrl;
564
+ if (!conversationUrl || !conversationUrl.includes("/c/")) {
565
+ throw new Error(`Session "${trimmed}" has no ChatGPT conversation URL stored. Cannot follow up in browser mode.`);
566
+ }
567
+ return { sessionId: meta.id, conversationUrl };
568
+ }
558
569
  const fromMetadata = extractResponseIdFromSession(meta, followupModel);
559
570
  if (fromMetadata) {
560
571
  return { responseId: fromMetadata, sessionId: meta.id };
@@ -917,7 +928,7 @@ async function runRootCommand(options) {
917
928
  if (normalizedMultiModels.length > 0) {
918
929
  throw new Error("--followup cannot be combined with --models.");
919
930
  }
920
- const followup = await resolveFollowupReference(options.followup, options.followupModel);
931
+ const followup = await resolveFollowupReference(options.followup, options.followupModel, engine);
921
932
  resolvedOptions.previousResponseId = followup.responseId;
922
933
  resolvedOptions.followupSessionId = followup.sessionId;
923
934
  resolvedOptions.followupModel = options.followupModel;
@@ -964,6 +975,7 @@ async function runRootCommand(options) {
964
975
  options.prompt = `${options.prompt.trim()}\n${userConfig.promptSuffix}`;
965
976
  }
966
977
  resolvedOptions.prompt = options.prompt;
978
+ let browserFollowupConversationUrl;
967
979
  if (options.followup) {
968
980
  assertFollowupSupported({
969
981
  engine,
@@ -974,10 +986,11 @@ async function runRootCommand(options) {
974
986
  if (normalizedMultiModels.length > 0) {
975
987
  throw new Error("--followup cannot be combined with --models.");
976
988
  }
977
- const followup = await resolveFollowupReference(options.followup, options.followupModel);
989
+ const followup = await resolveFollowupReference(options.followup, options.followupModel, engine);
978
990
  resolvedOptions.previousResponseId = followup.responseId;
979
991
  resolvedOptions.followupSessionId = followup.sessionId;
980
992
  resolvedOptions.followupModel = options.followupModel;
993
+ browserFollowupConversationUrl = followup.conversationUrl;
981
994
  }
982
995
  const duplicateBlocked = await shouldBlockDuplicatePrompt({
983
996
  prompt: resolvedOptions.prompt,
@@ -1018,6 +1031,13 @@ async function runRootCommand(options) {
1018
1031
  browserModelLabel: browserModelLabelOverride,
1019
1032
  })
1020
1033
  : undefined;
1034
+ // For browser follow-ups, navigate to the existing conversation and skip model selection.
1035
+ if (browserConfig && browserFollowupConversationUrl) {
1036
+ browserConfig.url = browserFollowupConversationUrl;
1037
+ browserConfig.chatgptUrl = browserFollowupConversationUrl;
1038
+ browserConfig.modelStrategy = "ignore";
1039
+ console.log(chalk.dim(`Following up on conversation: ${browserFollowupConversationUrl}`));
1040
+ }
1021
1041
  let browserDeps;
1022
1042
  if (browserConfig && remoteHost) {
1023
1043
  browserDeps = {
@@ -158,14 +158,24 @@ async function connectToNewTarget(host, port, url, logger, messages, rootSession
158
158
  try {
159
159
  let target;
160
160
  if (rootSession) {
161
- // Hidden target: not in tab strip, no window, no focus steal.
161
+ // Try hidden target (no tab strip, no window), fall back to background+focus:false.
162
162
  // Lifetime tied to rootSession (kept alive by the serve).
163
- const result = await rootSession.Target.createTarget({
164
- url,
165
- hidden: true,
166
- background: true,
167
- });
168
- target = { id: result.targetId };
163
+ try {
164
+ const result = await rootSession.Target.createTarget({
165
+ url,
166
+ hidden: true,
167
+ background: true,
168
+ });
169
+ target = { id: result.targetId };
170
+ }
171
+ catch {
172
+ const result = await rootSession.Target.createTarget({
173
+ url,
174
+ background: true,
175
+ focus: false,
176
+ });
177
+ target = { id: result.targetId };
178
+ }
169
179
  }
170
180
  else {
171
181
  // No root session: try background + focus:false, fallback to CDP.New()
@@ -124,6 +124,8 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
124
124
  chromePort: browserResult.chromePort,
125
125
  chromeHost: browserResult.chromeHost,
126
126
  userDataDir: browserResult.userDataDir,
127
+ chromeTargetId: browserResult.chromeTargetId,
128
+ tabUrl: browserResult.tabUrl,
127
129
  controllerPid: browserResult.controllerPid ?? process.pid,
128
130
  },
129
131
  answerText,
@@ -12,6 +12,7 @@ import { CHATGPT_URL } from "../browser/constants.js";
12
12
  import { getCliVersion } from "../version.js";
13
13
  import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "../browser/profileState.js";
14
14
  import { normalizeChatgptUrl } from "../browser/utils.js";
15
+ import { hideChromeWindow } from "../browser/chromeLifecycle.js";
15
16
  async function findAvailablePort() {
16
17
  return await new Promise((resolve, reject) => {
17
18
  const srv = net.createServer();
@@ -290,6 +291,7 @@ export async function serveRemote(options = {}) {
290
291
  "--disable-hang-monitor",
291
292
  "--disable-popup-blocking",
292
293
  "--disable-features=TranslateUI,AutomationControlled",
294
+ "--remote-allow-origins=*",
293
295
  "--mute-audio",
294
296
  "--window-size=1280,720",
295
297
  "--password-store=basic",
@@ -313,15 +315,30 @@ export async function serveRemote(options = {}) {
313
315
  console.log(`Root CDP session connecting to ${browserWsUrl}`);
314
316
  const rootSession = await CDP({ target: browserWsUrl });
315
317
  sharedRootSession = rootSession;
316
- // Sync ChatGPT cookies into the shared Chrome via a hidden target.
318
+ // Sync ChatGPT cookies into the shared Chrome via a background target.
319
+ // Try hidden first (no tab strip / no window), fall back to background+focus:false.
317
320
  try {
318
321
  const { syncCookies } = await import("../browser/cookies.js");
319
- const { targetId } = await rootSession.Target.createTarget({
320
- url: "about:blank",
321
- hidden: true,
322
- background: true,
323
- });
324
- const syncClient = await CDP({ host: "127.0.0.1", port: chrome.port, target: targetId });
322
+ let syncTargetId;
323
+ try {
324
+ const result = await rootSession.Target.createTarget({
325
+ url: "about:blank",
326
+ hidden: true,
327
+ background: true,
328
+ });
329
+ syncTargetId = result.targetId;
330
+ console.log("Cookie sync: created hidden target.");
331
+ }
332
+ catch {
333
+ const result = await rootSession.Target.createTarget({
334
+ url: "about:blank",
335
+ background: true,
336
+ focus: false,
337
+ });
338
+ syncTargetId = result.targetId;
339
+ console.log("Cookie sync: hidden targets unavailable, using background+focus:false.");
340
+ }
341
+ const syncClient = await CDP({ host: "127.0.0.1", port: chrome.port, target: syncTargetId });
325
342
  const { Network } = syncClient;
326
343
  await Network.enable({});
327
344
  const syncLogger = ((msg) => { if (msg)
@@ -332,12 +349,20 @@ export async function serveRemote(options = {}) {
332
349
  });
333
350
  console.log(`Synced ${count} ChatGPT cookies into shared Chrome.`);
334
351
  await syncClient.close();
335
- await rootSession.Target.closeTarget({ targetId });
352
+ await rootSession.Target.closeTarget({ targetId: syncTargetId });
336
353
  }
337
354
  catch (cookieErr) {
338
355
  const msg = cookieErr instanceof Error ? cookieErr.message : String(cookieErr);
339
356
  console.log(`Warning: failed to sync cookies into shared Chrome (${msg}). Runs may need to authenticate.`);
340
357
  }
358
+ // Hide the serve Chrome from the macOS Dock (set visible = false).
359
+ // This is safe because we interact via CDP, not GUI.
360
+ try {
361
+ await hideChromeWindow(chrome, console.log);
362
+ }
363
+ catch {
364
+ // Non-fatal: hideChromeWindow logs its own errors.
365
+ }
341
366
  sharedChrome = { host: "127.0.0.1", port: chrome.port, rootSession };
342
367
  }
343
368
  catch (error) {
@@ -398,6 +423,7 @@ function sanitizeResult(result) {
398
423
  tookMs: result.tookMs,
399
424
  answerTokens: result.answerTokens,
400
425
  answerChars: result.answerChars,
426
+ tabUrl: result.tabUrl,
401
427
  chromePid: undefined,
402
428
  chromePort: undefined,
403
429
  userDataDir: undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrewting19/oracle",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.4 Pro, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/steipete/oracle#readme",