@andrewting19/oracle 0.10.3 → 0.11.1

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 = {
@@ -1402,6 +1402,19 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1402
1402
  }
1403
1403
  }
1404
1404
  stopThinkingMonitor?.();
1405
+ // Capture the final conversation URL (chatgpt.com/c/<id>) for follow-up support.
1406
+ try {
1407
+ const { result: hrefResult } = await Runtime.evaluate({
1408
+ expression: "location.href",
1409
+ returnByValue: true,
1410
+ });
1411
+ if (typeof hrefResult?.value === "string" && hrefResult.value.includes("/c/")) {
1412
+ lastUrl = hrefResult.value;
1413
+ }
1414
+ }
1415
+ catch {
1416
+ // ignore — keep whatever lastUrl we have
1417
+ }
1405
1418
  const durationMs = Date.now() - startedAt;
1406
1419
  const answerChars = answerText.length;
1407
1420
  const answerTokens = estimateTokenCount(answerMarkdown);
@@ -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();
@@ -354,6 +355,14 @@ export async function serveRemote(options = {}) {
354
355
  const msg = cookieErr instanceof Error ? cookieErr.message : String(cookieErr);
355
356
  console.log(`Warning: failed to sync cookies into shared Chrome (${msg}). Runs may need to authenticate.`);
356
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
+ }
357
366
  sharedChrome = { host: "127.0.0.1", port: chrome.port, rootSession };
358
367
  }
359
368
  catch (error) {
@@ -414,6 +423,7 @@ function sanitizeResult(result) {
414
423
  tookMs: result.tookMs,
415
424
  answerTokens: result.answerTokens,
416
425
  answerChars: result.answerChars,
426
+ tabUrl: result.tabUrl,
417
427
  chromePid: undefined,
418
428
  chromePort: undefined,
419
429
  userDataDir: undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrewting19/oracle",
3
- "version": "0.10.3",
3
+ "version": "0.11.1",
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",