@steipete/oracle 0.10.0 → 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.
Files changed (52) hide show
  1. package/README.md +56 -11
  2. package/dist/bin/oracle-cli.js +104 -16
  3. package/dist/src/browser/actions/archiveConversation.js +236 -0
  4. package/dist/src/browser/actions/assistantResponse.js +26 -0
  5. package/dist/src/browser/actions/deepResearch.js +662 -0
  6. package/dist/src/browser/actions/modelSelection.js +86 -16
  7. package/dist/src/browser/actions/navigation.js +22 -0
  8. package/dist/src/browser/actions/projectSources.js +491 -0
  9. package/dist/src/browser/actions/promptComposer.js +52 -27
  10. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  11. package/dist/src/browser/artifacts.js +150 -0
  12. package/dist/src/browser/attachRunning.js +31 -0
  13. package/dist/src/browser/chatgptImages.js +315 -0
  14. package/dist/src/browser/chromeLifecycle.js +214 -3
  15. package/dist/src/browser/config.js +27 -9
  16. package/dist/src/browser/constants.js +8 -0
  17. package/dist/src/browser/controlPlan.js +81 -0
  18. package/dist/src/browser/detect.js +206 -33
  19. package/dist/src/browser/domDebug.js +49 -0
  20. package/dist/src/browser/index.js +1234 -479
  21. package/dist/src/browser/liveTabs.js +434 -0
  22. package/dist/src/browser/profileState.js +83 -3
  23. package/dist/src/browser/projectSourcesRunner.js +366 -0
  24. package/dist/src/browser/reattach.js +117 -45
  25. package/dist/src/browser/reattachHelpers.js +1 -1
  26. package/dist/src/browser/sessionRunner.js +53 -1
  27. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  28. package/dist/src/cli/bridge/claudeConfig.js +12 -8
  29. package/dist/src/cli/bridge/codexConfig.js +2 -2
  30. package/dist/src/cli/browserConfig.js +41 -8
  31. package/dist/src/cli/browserDefaults.js +31 -7
  32. package/dist/src/cli/browserTabs.js +228 -0
  33. package/dist/src/cli/dryRun.js +33 -1
  34. package/dist/src/cli/duplicatePromptGuard.js +10 -2
  35. package/dist/src/cli/help.js +1 -1
  36. package/dist/src/cli/options.js +4 -0
  37. package/dist/src/cli/projectSources.js +116 -0
  38. package/dist/src/cli/sessionCommand.js +51 -0
  39. package/dist/src/cli/sessionDisplay.js +121 -9
  40. package/dist/src/cli/sessionRunner.js +51 -7
  41. package/dist/src/mcp/consultPresets.js +19 -0
  42. package/dist/src/mcp/server.js +2 -0
  43. package/dist/src/mcp/tools/consult.js +201 -26
  44. package/dist/src/mcp/tools/projectSources.js +123 -0
  45. package/dist/src/mcp/types.js +11 -2
  46. package/dist/src/mcp/utils.js +6 -1
  47. package/dist/src/oracle/run.js +4 -1
  48. package/dist/src/projectSources/plan.js +27 -0
  49. package/dist/src/projectSources/types.js +1 -0
  50. package/dist/src/projectSources/url.js +23 -0
  51. package/dist/src/sessionManager.js +1 -0
  52. package/package.json +7 -6
@@ -9,11 +9,13 @@ export const COOKIE_URLS = [
9
9
  export const INPUT_SELECTORS = [
10
10
  'textarea[data-id="prompt-textarea"]',
11
11
  'textarea[placeholder*="Send a message"]',
12
+ 'textarea[aria-label="Chat with ChatGPT"]',
12
13
  'textarea[aria-label="Message ChatGPT"]',
13
14
  "textarea:not([disabled])",
14
15
  'textarea[name="prompt-textarea"]',
15
16
  "#prompt-textarea",
16
17
  ".ProseMirror",
18
+ '[contenteditable="true"][role="textbox"]',
17
19
  '[contenteditable="true"][data-virtualkeyboard="true"]',
18
20
  ];
19
21
  export const ANSWER_SELECTORS = [
@@ -73,4 +75,10 @@ export const MODEL_BUTTON_SELECTOR = '[data-testid="model-switcher-dropdown-butt
73
75
  export const COMPOSER_MODEL_SIGNAL_SELECTOR = '[data-testid="composer-footer-actions"]';
74
76
  export const COPY_BUTTON_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
75
77
  // Action buttons that only appear once a turn has finished rendering.
78
+ export const DEEP_RESEARCH_PLUS_BUTTON = '[data-testid="composer-plus-btn"]';
79
+ export const DEEP_RESEARCH_DROPDOWN_ITEM_TEXT = "Deep research";
80
+ export const DEEP_RESEARCH_PILL_LABEL = "Deep research";
81
+ export const DEEP_RESEARCH_POLL_INTERVAL_MS = 5_000;
82
+ export const DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS = 70_000;
83
+ export const DEEP_RESEARCH_DEFAULT_TIMEOUT_MS = 2_400_000;
76
84
  export const FINISHED_ACTIONS_SELECTOR = 'button[data-testid="copy-turn-action-button"], button[data-testid="good-response-turn-action-button"], button[data-testid="bad-response-turn-action-button"], button[aria-label="Share"]';
@@ -0,0 +1,81 @@
1
+ export function describeBrowserControlPlan(config = {}) {
2
+ const guidance = [];
3
+ const tabRef = String(config.browserTabRef ?? "").trim();
4
+ const reusesExistingTab = tabRef.length > 0;
5
+ if (config.attachRunning) {
6
+ guidance.push(reusesExistingTab
7
+ ? `Oracle reuses the matching ChatGPT tab (${tabRef}) and leaves the existing browser process alone.`
8
+ : "Oracle opens a dedicated tab and leaves the existing browser process alone.");
9
+ if (config.keepBrowser) {
10
+ guidance.push("The browser stays open because Oracle did not launch it.");
11
+ }
12
+ return {
13
+ mode: "attach-running",
14
+ launchesChrome: false,
15
+ mayFocusWindow: true,
16
+ summary: reusesExistingTab
17
+ ? "attach to an already-running local Chrome tab"
18
+ : "attach to an already-running local Chrome session",
19
+ guidance,
20
+ };
21
+ }
22
+ if (config.remoteChrome) {
23
+ guidance.push(reusesExistingTab
24
+ ? `Oracle reuses the matching ChatGPT tab (${tabRef}) in the configured remote Chrome session.`
25
+ : "Oracle opens a dedicated tab in the configured remote Chrome session.");
26
+ guidance.push("Local Chrome launch, cookie copy, and window hiding flags are skipped.");
27
+ return {
28
+ mode: "remote-chrome",
29
+ launchesChrome: false,
30
+ mayFocusWindow: false,
31
+ summary: reusesExistingTab
32
+ ? "reuse an existing remote Chrome tab"
33
+ : "reuse an existing remote Chrome session",
34
+ guidance,
35
+ };
36
+ }
37
+ if (config.headless) {
38
+ guidance.push("Headless mode avoids visible UI but may be blocked by ChatGPT or Cloudflare.");
39
+ return {
40
+ mode: "headless",
41
+ launchesChrome: true,
42
+ mayFocusWindow: false,
43
+ summary: "launch headless Chrome",
44
+ guidance,
45
+ };
46
+ }
47
+ if (config.hideWindow) {
48
+ guidance.push("Chrome may briefly focus while launching before Oracle hides it.");
49
+ guidance.push("For the calmest shared-desktop flow, prefer --browser-attach-running or --remote-chrome.");
50
+ return {
51
+ mode: "hidden-window",
52
+ launchesChrome: true,
53
+ mayFocusWindow: true,
54
+ summary: "launch Chrome and hide the window after startup",
55
+ guidance,
56
+ };
57
+ }
58
+ guidance.push(config.manualLogin
59
+ ? "Manual-login mode may show the persistent Oracle Chrome profile for sign-in or automation."
60
+ : "A visible automation Chrome window may take focus while Oracle controls ChatGPT.");
61
+ guidance.push("Use --browser-hide-window, --browser-attach-running, or --remote-chrome to reduce desktop disruption.");
62
+ if (config.keepBrowser) {
63
+ guidance.push("Chrome will remain open after the run because --browser-keep-browser is enabled.");
64
+ }
65
+ return {
66
+ mode: "visible-window",
67
+ launchesChrome: true,
68
+ mayFocusWindow: true,
69
+ summary: "launch visible Chrome",
70
+ guidance,
71
+ };
72
+ }
73
+ export function formatBrowserControlPlan(plan, label = "browser") {
74
+ const risk = plan.mayFocusWindow
75
+ ? "may focus/control the browser UI"
76
+ : "does not use a visible local browser window";
77
+ return [
78
+ `[${label}] Browser control: ${plan.summary}; ${risk}.`,
79
+ ...plan.guidance.map((entry) => `[${label}] Browser guidance: ${entry}`),
80
+ ];
81
+ }
@@ -14,7 +14,7 @@ export async function detectChromeBinary() {
14
14
  if (launcherDetected) {
15
15
  return { path: launcherDetected };
16
16
  }
17
- const candidates = platformChromeCandidates();
17
+ const candidates = platformChromeCandidates(process.platform, os.homedir());
18
18
  for (const candidate of candidates.absolutePaths) {
19
19
  if (await isExecutable(candidate)) {
20
20
  return { path: candidate };
@@ -31,9 +31,9 @@ export async function detectChromeCookieDb({ profile, }) {
31
31
  if (process.platform === "win32") {
32
32
  return null;
33
33
  }
34
- const roots = platformProfileRoots();
34
+ const roots = resolveAttachRunningProfileRoots();
35
35
  for (const root of roots) {
36
- const dir = path.join(root, profileName);
36
+ const dir = path.join(root.root, profileName);
37
37
  const direct = path.join(dir, "Cookies");
38
38
  if (await isFile(direct))
39
39
  return direct;
@@ -43,8 +43,155 @@ export async function detectChromeCookieDb({ profile, }) {
43
43
  }
44
44
  return null;
45
45
  }
46
- function platformChromeCandidates() {
47
- if (process.platform === "linux") {
46
+ export function resolveAttachRunningProfileRoots(platform = process.platform, homeDir = os.homedir()) {
47
+ if (platform === "darwin") {
48
+ return [
49
+ {
50
+ family: "chrome",
51
+ root: path.join(homeDir, "Library", "Application Support", "Google", "Chrome"),
52
+ },
53
+ {
54
+ family: "chromium",
55
+ root: path.join(homeDir, "Library", "Application Support", "Chromium"),
56
+ },
57
+ {
58
+ family: "edge",
59
+ root: path.join(homeDir, "Library", "Application Support", "Microsoft Edge"),
60
+ },
61
+ {
62
+ family: "brave",
63
+ root: path.join(homeDir, "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
64
+ },
65
+ ];
66
+ }
67
+ if (platform === "linux") {
68
+ return [
69
+ { family: "chrome", root: path.join(homeDir, ".config", "google-chrome") },
70
+ { family: "chromium", root: path.join(homeDir, ".config", "chromium") },
71
+ { family: "edge", root: path.join(homeDir, ".config", "microsoft-edge") },
72
+ {
73
+ family: "brave",
74
+ root: path.join(homeDir, ".config", "BraveSoftware", "Brave-Browser"),
75
+ },
76
+ { family: "chromium", root: path.join(homeDir, "snap", "chromium", "common", "chromium") },
77
+ { family: "chromium", root: path.join(homeDir, "snap", "chromium", "current", "chromium") },
78
+ ];
79
+ }
80
+ if (platform === "win32") {
81
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local");
82
+ return [
83
+ {
84
+ family: "chrome",
85
+ root: path.join(localAppData, "Google", "Chrome", "User Data"),
86
+ },
87
+ {
88
+ family: "chromium",
89
+ root: path.join(localAppData, "Chromium", "User Data"),
90
+ },
91
+ {
92
+ family: "edge",
93
+ root: path.join(localAppData, "Microsoft", "Edge", "User Data"),
94
+ },
95
+ {
96
+ family: "brave",
97
+ root: path.join(localAppData, "BraveSoftware", "Brave-Browser", "User Data"),
98
+ },
99
+ ];
100
+ }
101
+ return [];
102
+ }
103
+ export function resolveDevToolsActivePortDiscoveryRoots(platform = process.platform, homeDir = os.homedir()) {
104
+ if (platform === "darwin") {
105
+ return [path.join(homeDir, "Library", "Application Support")];
106
+ }
107
+ if (platform === "linux") {
108
+ return [path.join(homeDir, ".config"), path.join(homeDir, "snap")];
109
+ }
110
+ if (platform === "win32") {
111
+ return [process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local")];
112
+ }
113
+ return [];
114
+ }
115
+ export function inferAttachRunningBrowserFamily(chromePath) {
116
+ const normalized = chromePath?.trim().toLowerCase();
117
+ if (!normalized) {
118
+ return null;
119
+ }
120
+ if (normalized.includes("microsoft edge") || normalized.includes("msedge")) {
121
+ return "edge";
122
+ }
123
+ if (normalized.includes("brave")) {
124
+ return "brave";
125
+ }
126
+ if (normalized.includes("chromium")) {
127
+ return "chromium";
128
+ }
129
+ if (normalized.includes("chrome")) {
130
+ return "chrome";
131
+ }
132
+ return null;
133
+ }
134
+ export function parseDevToolsActivePort(raw, options = {}) {
135
+ const host = formatWebSocketHost(options.host ?? "127.0.0.1");
136
+ const [rawPort, rawBrowserPath] = raw.split(/\r?\n/u);
137
+ const port = Number.parseInt(rawPort?.trim() ?? "", 10);
138
+ if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
139
+ throw new Error("DevToolsActivePort did not contain a valid port.");
140
+ }
141
+ const browserPath = rawBrowserPath?.trim() || "/devtools/browser";
142
+ const normalizedPath = browserPath.startsWith("/") ? browserPath : `/${browserPath}`;
143
+ return {
144
+ port,
145
+ browserWSEndpoint: `ws://${host}:${port}${normalizedPath}`,
146
+ };
147
+ }
148
+ export async function readDevToolsActivePortInfo(profileRoot, options = {}) {
149
+ const candidates = [
150
+ path.join(profileRoot, "DevToolsActivePort"),
151
+ path.join(profileRoot, "Default", "DevToolsActivePort"),
152
+ ];
153
+ for (const candidate of candidates) {
154
+ try {
155
+ const raw = await fs.readFile(candidate, "utf8");
156
+ const parsed = parseDevToolsActivePort(raw, options);
157
+ return { ...parsed, path: candidate };
158
+ }
159
+ catch {
160
+ // ignore missing/unreadable candidates
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+ export async function discoverDevToolsActivePortCandidates(options = {}) {
166
+ const { host, platform = process.platform, homeDir = os.homedir(), maxDepth = 6 } = options;
167
+ const roots = resolveDevToolsActivePortDiscoveryRoots(platform, homeDir);
168
+ const candidates = [];
169
+ const seenPaths = new Set();
170
+ for (const root of roots) {
171
+ await walkForDevToolsActivePort(root, maxDepth, async (candidatePath, stat) => {
172
+ if (seenPaths.has(candidatePath)) {
173
+ return;
174
+ }
175
+ seenPaths.add(candidatePath);
176
+ try {
177
+ const raw = await fs.readFile(candidatePath, "utf8");
178
+ const parsed = parseDevToolsActivePort(raw, { host });
179
+ candidates.push({
180
+ ...parsed,
181
+ path: candidatePath,
182
+ profileRoot: deriveDevToolsProfileRoot(candidatePath),
183
+ mtimeMs: Number(stat.mtimeMs),
184
+ });
185
+ }
186
+ catch {
187
+ // ignore unreadable or malformed DevToolsActivePort files
188
+ }
189
+ });
190
+ }
191
+ return candidates;
192
+ }
193
+ function platformChromeCandidates(platform = process.platform, homeDir = os.homedir()) {
194
+ if (platform === "linux") {
48
195
  return {
49
196
  binaryNames: [
50
197
  "google-chrome",
@@ -73,7 +220,7 @@ function platformChromeCandidates() {
73
220
  ],
74
221
  };
75
222
  }
76
- if (process.platform === "darwin") {
223
+ if (platform === "darwin") {
77
224
  return {
78
225
  binaryNames: [],
79
226
  absolutePaths: [
@@ -84,10 +231,10 @@ function platformChromeCandidates() {
84
231
  ],
85
232
  };
86
233
  }
87
- if (process.platform === "win32") {
234
+ if (platform === "win32") {
88
235
  const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
89
236
  const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
90
- const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
237
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(homeDir, "AppData", "Local");
91
238
  return {
92
239
  binaryNames: [],
93
240
  absolutePaths: [
@@ -101,31 +248,6 @@ function platformChromeCandidates() {
101
248
  }
102
249
  return { binaryNames: [], absolutePaths: [] };
103
250
  }
104
- function platformProfileRoots() {
105
- const home = os.homedir();
106
- if (process.platform === "darwin") {
107
- return [
108
- path.join(home, "Library", "Application Support", "Google", "Chrome"),
109
- path.join(home, "Library", "Application Support", "Chromium"),
110
- path.join(home, "Library", "Application Support", "Microsoft Edge"),
111
- path.join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser"),
112
- ];
113
- }
114
- if (process.platform === "linux") {
115
- return [
116
- path.join(home, ".config", "google-chrome"),
117
- path.join(home, ".config", "google-chrome-beta"),
118
- path.join(home, ".config", "google-chrome-unstable"),
119
- path.join(home, ".config", "chromium"),
120
- path.join(home, ".config", "microsoft-edge"),
121
- path.join(home, ".config", "BraveSoftware", "Brave-Browser"),
122
- // Snap Chromium profiles
123
- path.join(home, "snap", "chromium", "common", "chromium"),
124
- path.join(home, "snap", "chromium", "current", "chromium"),
125
- ];
126
- }
127
- return [];
128
- }
129
251
  async function isExecutable(candidate) {
130
252
  try {
131
253
  const stat = await fs.stat(candidate);
@@ -162,3 +284,54 @@ async function findOnPath(names) {
162
284
  }
163
285
  return null;
164
286
  }
287
+ function deriveDevToolsProfileRoot(activePortPath) {
288
+ const parentDir = path.dirname(activePortPath);
289
+ if (path.basename(parentDir).toLowerCase() === "default") {
290
+ return path.dirname(parentDir);
291
+ }
292
+ return parentDir;
293
+ }
294
+ function formatWebSocketHost(host) {
295
+ if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) {
296
+ return `[${host}]`;
297
+ }
298
+ return host;
299
+ }
300
+ async function walkForDevToolsActivePort(root, maxDepth, onFile) {
301
+ const stack = [{ dir: root, depth: 0 }];
302
+ while (stack.length > 0) {
303
+ const current = stack.pop();
304
+ if (!current) {
305
+ continue;
306
+ }
307
+ let entries;
308
+ try {
309
+ entries = await fs.readdir(current.dir, { withFileTypes: true });
310
+ }
311
+ catch {
312
+ continue;
313
+ }
314
+ for (const entry of entries) {
315
+ const candidatePath = path.join(current.dir, entry.name);
316
+ if (entry.isSymbolicLink()) {
317
+ continue;
318
+ }
319
+ if (entry.isFile()) {
320
+ if (entry.name !== "DevToolsActivePort") {
321
+ continue;
322
+ }
323
+ try {
324
+ const stat = await fs.stat(candidatePath);
325
+ await onFile(candidatePath, stat);
326
+ }
327
+ catch {
328
+ // ignore unreadable candidates
329
+ }
330
+ continue;
331
+ }
332
+ if (entry.isDirectory() && current.depth < maxDepth) {
333
+ stack.push({ dir: candidatePath, depth: current.depth + 1 });
334
+ }
335
+ }
336
+ }
337
+ }
@@ -1,4 +1,7 @@
1
1
  import { CONVERSATION_TURN_SELECTOR } from "./constants.js";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { resolveSessionArtifactsDir } from "./artifacts.js";
2
5
  export function buildConversationDebugExpression() {
3
6
  return `(() => {
4
7
  const CONVERSATION_SELECTOR = ${JSON.stringify(CONVERSATION_TURN_SELECTOR)};
@@ -34,3 +37,49 @@ export async function logDomFailure(Runtime, logger, context) {
34
37
  // ignore snapshot failures
35
38
  }
36
39
  }
40
+ export async function captureBrowserDiagnostics(Runtime, logger, context, options = {}) {
41
+ if (!options.sessionId) {
42
+ await logConversationSnapshot(Runtime, logger).catch(() => undefined);
43
+ return {};
44
+ }
45
+ const dir = resolveSessionArtifactsDir(options.sessionId);
46
+ await fs.mkdir(dir, { recursive: true });
47
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
48
+ const baseName = `${context}-${timestamp}`;
49
+ const domPath = path.join(dir, `${baseName}.dom.json`);
50
+ const expression = `(() => {
51
+ const CONVERSATION_SELECTOR = ${JSON.stringify(CONVERSATION_TURN_SELECTOR)};
52
+ const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR)).slice(-6).map((node) => ({
53
+ role: node.getAttribute('data-message-author-role') || node.getAttribute('data-turn'),
54
+ text: (node.innerText || node.textContent || '').slice(0, 2000),
55
+ testid: node.getAttribute('data-testid'),
56
+ }));
57
+ return {
58
+ url: location.href,
59
+ title: document.title,
60
+ turns,
61
+ bodyText: (document.body?.innerText || '').slice(0, 5000),
62
+ };
63
+ })()`;
64
+ const { result } = await Runtime.evaluate({ expression, returnByValue: true });
65
+ await fs.writeFile(domPath, `${JSON.stringify(result?.value ?? null, null, 2)}\n`, "utf8");
66
+ logger(`[browser] Saved DOM diagnostic snapshot to ${domPath}`);
67
+ let screenshotPath;
68
+ if (options.Page?.captureScreenshot) {
69
+ try {
70
+ const screenshot = await options.Page.captureScreenshot({
71
+ format: "png",
72
+ captureBeyondViewport: true,
73
+ });
74
+ if (screenshot?.data) {
75
+ screenshotPath = path.join(dir, `${baseName}.png`);
76
+ await fs.writeFile(screenshotPath, Buffer.from(screenshot.data, "base64"));
77
+ logger(`[browser] Saved screenshot diagnostic snapshot to ${screenshotPath}`);
78
+ }
79
+ }
80
+ catch {
81
+ // Screenshots are best-effort; the DOM snapshot above is the primary diagnostic.
82
+ }
83
+ }
84
+ return { domPath, screenshotPath };
85
+ }