@steipete/oracle 0.10.0 → 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.
Files changed (52) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +104 -16
  3. package/dist/src/browser/actions/archiveConversation.js +224 -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 +78 -13
  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 +26 -2
  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 +1257 -485
  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 +40 -0
  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 +7 -0
  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 +2 -1
@@ -18,13 +18,19 @@ async function readSessionLogTail(sessionId, maxBytes) {
18
18
  }
19
19
  }
20
20
  import { performSessionRun } from "../../cli/sessionRunner.js";
21
+ import { runDryRunSummary } from "../../cli/dryRun.js";
21
22
  import { CHATGPT_URL } from "../../browser/constants.js";
22
- import { consultInputSchema } from "../types.js";
23
+ import { CONSULT_PRESETS, consultInputSchema } from "../types.js";
24
+ import { applyConsultPreset } from "../consultPresets.js";
23
25
  import { loadUserConfig } from "../../config.js";
24
26
  import { resolveNotificationSettings } from "../../cli/notifier.js";
25
27
  import { mapModelToBrowserLabel, resolveBrowserModelLabel } from "../../cli/browserConfig.js";
26
28
  // Use raw shapes so the MCP SDK (with its bundled Zod) wraps them and emits valid JSON Schema.
27
29
  const consultInputShape = {
30
+ preset: z
31
+ .enum(CONSULT_PRESETS)
32
+ .optional()
33
+ .describe('Optional MCP convenience preset. "chatgpt-pro-heavy" selects ChatGPT browser mode, the current Pro model alias, and Pro Extended thinking unless overridden.'),
28
34
  prompt: z.string().min(1, "Prompt is required.").describe("User prompt to run."),
29
35
  files: z
30
36
  .array(z.string())
@@ -33,7 +39,7 @@ const consultInputShape = {
33
39
  model: z
34
40
  .string()
35
41
  .optional()
36
- .describe("Single model name/label. Prefer setting `engine` explicitly to avoid default surprises."),
42
+ .describe("Single model name/label. If `engine` is omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then `api` when OPENAI_API_KEY is set, otherwise `browser`. Prefer setting `engine` explicitly to avoid default surprises."),
37
43
  models: z
38
44
  .array(z.string())
39
45
  .optional()
@@ -41,7 +47,7 @@ const consultInputShape = {
41
47
  engine: z
42
48
  .enum(["api", "browser"])
43
49
  .optional()
44
- .describe("Execution engine. `api` uses OpenAI/other providers. `browser` automates the ChatGPT web UI (supports attachments and ChatGPT-only model labels)."),
50
+ .describe("Execution engine. `api` uses OpenAI/other providers. `browser` automates the ChatGPT web UI (supports attachments and ChatGPT-only model labels). When omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then `api` when OPENAI_API_KEY is set, otherwise `browser`."),
45
51
  browserModelLabel: z
46
52
  .string()
47
53
  .optional()
@@ -58,10 +64,30 @@ const consultInputShape = {
58
64
  .enum(["light", "standard", "extended", "heavy"])
59
65
  .optional()
60
66
  .describe("Browser-only: set ChatGPT thinking time when supported by the chosen model."),
67
+ browserModelStrategy: z
68
+ .enum(["select", "current", "ignore"])
69
+ .optional()
70
+ .describe("Browser-only: model picker strategy. Mirrors the CLI --browser-model-strategy flag."),
71
+ browserResearchMode: z
72
+ .enum(["deep"])
73
+ .optional()
74
+ .describe("Browser-only: activate ChatGPT Deep Research mode for broad web research."),
75
+ browserArchive: z
76
+ .enum(["auto", "always", "never"])
77
+ .optional()
78
+ .describe('Browser-only: archive completed ChatGPT conversations after local artifacts are saved. "auto" archives successful non-project one-shots only.'),
79
+ browserFollowUps: z
80
+ .array(z.string())
81
+ .optional()
82
+ .describe("Browser-only: additional prompts to submit sequentially in the same ChatGPT conversation after the initial answer."),
61
83
  browserKeepBrowser: z
62
84
  .boolean()
63
85
  .optional()
64
86
  .describe("Browser-only: keep Chrome running after completion (useful for debugging)."),
87
+ dryRun: z
88
+ .boolean()
89
+ .optional()
90
+ .describe("Preview the resolved Oracle run without creating a session or touching the browser."),
65
91
  search: z
66
92
  .boolean()
67
93
  .optional()
@@ -100,10 +126,34 @@ const consultModelSummaryShape = z.object({
100
126
  .optional(),
101
127
  logPath: z.string().optional(),
102
128
  });
129
+ const consultDryRunResolvedShape = z.object({
130
+ resolvedEngine: z.enum(["api", "browser"]),
131
+ model: z.string(),
132
+ models: z.array(z.string()).optional(),
133
+ files: z.array(z.string()),
134
+ followUpCount: z.number(),
135
+ browser: z
136
+ .object({
137
+ desiredModel: z.string().nullable().optional(),
138
+ thinkingTime: z.string().nullable().optional(),
139
+ modelStrategy: z.string().nullable().optional(),
140
+ researchMode: z.string().nullable().optional(),
141
+ attachments: z.string().optional(),
142
+ bundleFiles: z.boolean().optional(),
143
+ keepBrowser: z.boolean().optional(),
144
+ manualLogin: z.boolean().optional(),
145
+ profileDir: z.string().nullable().optional(),
146
+ chatgptUrl: z.string().nullable().optional(),
147
+ })
148
+ .optional(),
149
+ guidance: z.array(z.string()),
150
+ });
103
151
  const consultOutputShape = {
104
- sessionId: z.string(),
152
+ sessionId: z.string().optional(),
105
153
  status: z.string(),
106
154
  output: z.string(),
155
+ dryRun: z.boolean().optional(),
156
+ resolved: consultDryRunResolvedShape.optional(),
107
157
  models: z.array(consultModelSummaryShape).optional(),
108
158
  };
109
159
  export function summarizeModelRunsForConsult(runs) {
@@ -136,7 +186,7 @@ export function summarizeModelRunsForConsult(runs) {
136
186
  };
137
187
  });
138
188
  }
139
- export function buildConsultBrowserConfig({ userConfig, env, runModel, inputModel, browserModelLabel, browserThinkingTime, browserKeepBrowser, }) {
189
+ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputModel, browserModelLabel, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserKeepBrowser, }) {
140
190
  const configuredBrowser = userConfig.browser ?? {};
141
191
  const envProfileDir = (env.ORACLE_BROWSER_PROFILE_DIR ?? "").trim();
142
192
  const hasProfileDir = envProfileDir.length > 0;
@@ -160,19 +210,108 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
160
210
  ? ((envProfileDir || configuredBrowser.manualLoginProfileDir) ?? null)
161
211
  : null,
162
212
  thinkingTime: browserThinkingTime ?? configuredBrowser.thinkingTime,
213
+ modelStrategy: browserModelStrategy ?? configuredBrowser.modelStrategy,
214
+ researchMode: browserResearchMode ?? configuredBrowser.researchMode,
215
+ archiveConversations: browserArchive ?? configuredBrowser.archiveConversations,
163
216
  desiredModel: desiredModelLabel || mapModelToBrowserLabel(runModel),
164
217
  };
165
218
  }
219
+ export function buildConsultDryRunResolved({ resolvedEngine, runOptions, browserConfig, }) {
220
+ const guidance = [];
221
+ const followUpCount = runOptions.browserFollowUps?.filter((entry) => entry.trim()).length ?? 0;
222
+ if (resolvedEngine === "api") {
223
+ guidance.push('API engine requires provider credentials. If the operator has ChatGPT Pro but no API key, retry with engine:"browser" or preset:"chatgpt-pro-heavy".');
224
+ }
225
+ if (resolvedEngine === "browser") {
226
+ guidance.push("Browser engine uses the signed-in ChatGPT profile; run dryRun:true before live use.");
227
+ }
228
+ const desiredModel = browserConfig?.desiredModel ?? null;
229
+ const thinkingTime = browserConfig?.thinkingTime ?? null;
230
+ if (runOptions.model === "gpt-5.5-pro" && thinkingTime === "heavy") {
231
+ guidance.push('gpt-5.5-pro should normally use Pro Extended. Use model:"gpt-5.5" with browserThinkingTime:"heavy" only when you explicitly want Thinking Heavy.');
232
+ }
233
+ const chatgptUrl = browserConfig?.chatgptUrl ?? browserConfig?.url ?? null;
234
+ if (chatgptUrl?.includes("/project")) {
235
+ guidance.push("This ChatGPT project URL is persistent. Project Sources should be mutated only by the project_sources tool with confirmMutation:true.");
236
+ }
237
+ if (followUpCount > 0) {
238
+ guidance.push("This is a multi-turn browser consult; all follow-ups stay in one ChatGPT conversation.");
239
+ }
240
+ return {
241
+ resolvedEngine,
242
+ model: runOptions.model,
243
+ models: runOptions.models,
244
+ files: runOptions.file ?? [],
245
+ followUpCount,
246
+ browser: resolvedEngine === "browser"
247
+ ? {
248
+ desiredModel,
249
+ thinkingTime,
250
+ modelStrategy: browserConfig?.modelStrategy ?? null,
251
+ researchMode: browserConfig?.researchMode ?? null,
252
+ attachments: runOptions.browserAttachments,
253
+ bundleFiles: runOptions.browserBundleFiles,
254
+ keepBrowser: browserConfig?.keepBrowser,
255
+ manualLogin: browserConfig?.manualLogin,
256
+ profileDir: browserConfig?.manualLoginProfileDir ?? null,
257
+ chatgptUrl,
258
+ }
259
+ : undefined,
260
+ guidance,
261
+ };
262
+ }
263
+ export function formatConsultDryRunResolved(details) {
264
+ const lines = [
265
+ "[dry-run] MCP resolved request:",
266
+ ` engine: ${details.resolvedEngine}`,
267
+ ` model: ${details.model}`,
268
+ ];
269
+ if (details.models && details.models.length > 0) {
270
+ lines.push(` models: ${details.models.join(", ")}`);
271
+ }
272
+ lines.push(` files: ${details.files.length}`);
273
+ if (details.browser) {
274
+ lines.push(` browser desired model: ${details.browser.desiredModel ?? "(default)"}`);
275
+ lines.push(` browser thinking time: ${details.browser.thinkingTime ?? "(default)"}`);
276
+ lines.push(` browser model strategy: ${details.browser.modelStrategy ?? "(default)"}`);
277
+ lines.push(` browser research mode: ${details.browser.researchMode ?? "off"}`);
278
+ lines.push(` browser attachments: ${details.browser.attachments ?? "auto"}`);
279
+ lines.push(` browser bundle files: ${details.browser.bundleFiles ? "yes" : "no"}`);
280
+ lines.push(` browser keep browser: ${details.browser.keepBrowser ? "yes" : "no"}`);
281
+ lines.push(` browser manual login: ${details.browser.manualLogin ? "yes" : "no"}`);
282
+ if (details.browser.profileDir) {
283
+ lines.push(` browser profile: ${details.browser.profileDir}`);
284
+ }
285
+ if (details.browser.chatgptUrl) {
286
+ lines.push(` ChatGPT URL: ${details.browser.chatgptUrl}`);
287
+ }
288
+ }
289
+ lines.push(` follow-ups: ${details.followUpCount}`);
290
+ for (const guidance of details.guidance) {
291
+ lines.push(` guidance: ${guidance}`);
292
+ }
293
+ return lines;
294
+ }
166
295
  export function registerConsultTool(server) {
167
296
  server.registerTool("consult", {
168
297
  title: "Run an oracle session",
169
- description: 'Run a one-shot Oracle session (API or ChatGPT browser automation). Use `files` to attach project context. For browser-based image/file uploads, set `browserAttachments:"always"`. Sessions are stored under `ORACLE_HOME_DIR` (shared with the CLI).',
298
+ description: 'Run an Oracle session (API or ChatGPT browser automation). Use `files` to attach project context. If `engine` is omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then API when OPENAI_API_KEY is set, otherwise browser. Browser GPT-5.5 Pro consults can take many minutes; use `dryRun:true` first when configuring an agent and inspect `sessions`/`oracle status` before retrying. For browser-based image/file uploads, set `browserAttachments:"always"`. Browser consults can include `browserFollowUps` for a multi-turn ChatGPT review in one conversation. Sessions are stored under `ORACLE_HOME_DIR` (shared with the CLI).',
170
299
  // Cast to any to satisfy SDK typings across differing Zod versions.
171
300
  inputSchema: consultInputShape,
172
301
  outputSchema: consultOutputShape,
173
302
  }, async (input) => {
174
303
  const textContent = (text) => [{ type: "text", text }];
175
- const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserThinkingTime, browserKeepBrowser, slug, } = consultInputSchema.parse(input);
304
+ let parsedInput;
305
+ try {
306
+ parsedInput = applyConsultPreset(consultInputSchema.parse(input));
307
+ }
308
+ catch (error) {
309
+ return {
310
+ isError: true,
311
+ content: textContent(error instanceof Error ? error.message : String(error)),
312
+ };
313
+ }
314
+ const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserFollowUps, browserKeepBrowser, dryRun, slug, } = parsedInput;
176
315
  const { config: userConfig } = await loadUserConfig();
177
316
  const { runOptions, resolvedEngine } = mapConsultToRunOptions({
178
317
  prompt,
@@ -183,11 +322,66 @@ export function registerConsultTool(server) {
183
322
  search,
184
323
  browserAttachments,
185
324
  browserBundleFiles,
325
+ browserFollowUps,
186
326
  userConfig,
187
327
  env: process.env,
188
328
  });
189
329
  const cwd = process.cwd();
330
+ const sendLog = (text, level = "info") => server.server
331
+ .sendLoggingMessage(LoggingMessageNotificationParamsSchema.parse({
332
+ level,
333
+ data: { text, bytes: Buffer.byteLength(text, "utf8") },
334
+ }))
335
+ .catch(() => { });
190
336
  const resolvedRemote = resolveRemoteServiceConfig({ userConfig, env: process.env });
337
+ let browserConfig;
338
+ if (resolvedEngine === "browser") {
339
+ browserConfig = buildConsultBrowserConfig({
340
+ userConfig,
341
+ env: process.env,
342
+ runModel: runOptions.model,
343
+ inputModel: model,
344
+ browserModelLabel,
345
+ browserThinkingTime,
346
+ browserModelStrategy,
347
+ browserResearchMode,
348
+ browserArchive,
349
+ browserKeepBrowser,
350
+ });
351
+ }
352
+ if (dryRun) {
353
+ const lines = [];
354
+ const log = (line) => {
355
+ lines.push(line);
356
+ sendLog(line);
357
+ };
358
+ const resolved = buildConsultDryRunResolved({
359
+ resolvedEngine,
360
+ runOptions,
361
+ browserConfig,
362
+ });
363
+ await runDryRunSummary({
364
+ engine: resolvedEngine,
365
+ runOptions,
366
+ cwd,
367
+ version: getCliVersion(),
368
+ log,
369
+ browserConfig,
370
+ });
371
+ for (const line of formatConsultDryRunResolved(resolved)) {
372
+ log(line);
373
+ }
374
+ const output = lines.join("\n").trim();
375
+ return {
376
+ content: textContent(output),
377
+ structuredContent: {
378
+ status: "dry-run",
379
+ output,
380
+ dryRun: true,
381
+ resolved,
382
+ },
383
+ };
384
+ }
191
385
  const browserGuard = ensureBrowserAvailable(resolvedEngine, {
192
386
  remoteHost: resolvedRemote.host,
193
387
  });
@@ -212,18 +406,6 @@ export function registerConsultTool(server) {
212
406
  }),
213
407
  };
214
408
  }
215
- let browserConfig;
216
- if (resolvedEngine === "browser") {
217
- browserConfig = buildConsultBrowserConfig({
218
- userConfig,
219
- env: process.env,
220
- runModel: runOptions.model,
221
- inputModel: model,
222
- browserModelLabel,
223
- browserThinkingTime,
224
- browserKeepBrowser,
225
- });
226
- }
227
409
  const notifications = resolveNotificationSettings({
228
410
  cliNotify: undefined,
229
411
  cliNotifySound: undefined,
@@ -238,13 +420,6 @@ export function registerConsultTool(server) {
238
420
  waitPreference: true,
239
421
  }, cwd, notifications);
240
422
  const logWriter = sessionStore.createLogWriter(sessionMeta.id);
241
- // Best-effort: emit MCP logging notifications for live chunks but never block the run.
242
- const sendLog = (text, level = "info") => server.server
243
- .sendLoggingMessage(LoggingMessageNotificationParamsSchema.parse({
244
- level,
245
- data: { text, bytes: Buffer.byteLength(text, "utf8") },
246
- }))
247
- .catch(() => { });
248
423
  // Stream logs to both the session log and MCP logging notifications, but avoid buffering in memory
249
424
  const log = (line) => {
250
425
  logWriter.logLine(line);
@@ -0,0 +1,123 @@
1
+ import { z } from "zod";
2
+ import { loadUserConfig } from "../../config.js";
3
+ import { resolveRemoteServiceConfig } from "../../remote/remoteServiceConfig.js";
4
+ import { runBrowserProjectSources } from "../../browser/projectSourcesRunner.js";
5
+ import { normalizeProjectSourcesUrl } from "../../projectSources/url.js";
6
+ import { buildProjectSourcesBrowserConfig, resolveProjectSourceFiles, } from "../../cli/projectSources.js";
7
+ import { resolveConfiguredMaxFileSizeBytes } from "../../cli/fileSize.js";
8
+ const projectSourceEntryShape = z.object({
9
+ name: z.string(),
10
+ index: z.number(),
11
+ status: z.enum(["ready", "processing", "unknown"]).optional(),
12
+ });
13
+ const projectSourceUploadPlanShape = z.object({
14
+ path: z.string(),
15
+ displayPath: z.string(),
16
+ name: z.string(),
17
+ sizeBytes: z.number().optional(),
18
+ batch: z.number(),
19
+ });
20
+ const projectSourcesInputShape = {
21
+ operation: z
22
+ .enum(["list", "add"])
23
+ .describe("Project Sources operation. v1 intentionally supports only non-destructive list/add."),
24
+ chatgptUrl: z
25
+ .string()
26
+ .optional()
27
+ .describe("ChatGPT project URL ending in /project. Falls back to browser.chatgptUrl config."),
28
+ files: z
29
+ .array(z.string())
30
+ .default([])
31
+ .describe("Local file paths or globs to add as persistent ChatGPT Project Sources."),
32
+ dryRun: z
33
+ .boolean()
34
+ .optional()
35
+ .describe("Validate files and return an upload plan without touching the browser."),
36
+ confirmMutation: z
37
+ .boolean()
38
+ .optional()
39
+ .describe("Required for mutating add operations so agents do not modify project state accidentally."),
40
+ browserKeepBrowser: z.boolean().optional().describe("Keep Chrome running after completion."),
41
+ };
42
+ const projectSourcesOutputShape = {
43
+ status: z.enum(["ok", "dry-run"]),
44
+ operation: z.enum(["list", "add"]),
45
+ projectUrl: z.string(),
46
+ dryRun: z.boolean(),
47
+ sourcesBefore: z.array(projectSourceEntryShape).optional(),
48
+ sourcesAfter: z.array(projectSourceEntryShape).optional(),
49
+ plannedUploads: z.array(projectSourceUploadPlanShape).optional(),
50
+ added: z.array(projectSourceEntryShape).optional(),
51
+ warnings: z.array(z.string()),
52
+ tookMs: z.number(),
53
+ };
54
+ const projectSourcesInputSchema = z.object(projectSourcesInputShape);
55
+ export function registerProjectSourcesTool(server) {
56
+ server.registerTool("project_sources", {
57
+ title: "Manage ChatGPT Project Sources",
58
+ description: "List or append files to a ChatGPT Project's persistent Sources tab. This is useful for Developer Mode workflows where chats do not share memory, but explicit project sources provide shared context. Destructive delete/replace/sync operations are intentionally not included in v1.",
59
+ inputSchema: projectSourcesInputShape,
60
+ outputSchema: projectSourcesOutputShape,
61
+ }, async (input) => {
62
+ const textContent = (text) => [{ type: "text", text }];
63
+ let parsed;
64
+ try {
65
+ parsed = projectSourcesInputSchema.parse(input);
66
+ }
67
+ catch (error) {
68
+ return {
69
+ isError: true,
70
+ content: textContent(error instanceof Error ? error.message : String(error)),
71
+ };
72
+ }
73
+ const { config: userConfig } = await loadUserConfig();
74
+ const resolvedRemote = resolveRemoteServiceConfig({ userConfig, env: process.env });
75
+ if (resolvedRemote.host) {
76
+ return {
77
+ isError: true,
78
+ content: textContent("project_sources v1 must run on the signed-in browser host; remote oracle serve support is not enabled yet."),
79
+ };
80
+ }
81
+ const projectUrl = normalizeProjectSourcesUrl(parsed.chatgptUrl ?? userConfig.browser?.chatgptUrl ?? userConfig.browser?.url ?? "");
82
+ if (parsed.operation === "add" && !parsed.dryRun && parsed.confirmMutation !== true) {
83
+ return {
84
+ isError: true,
85
+ content: textContent("project_sources add modifies persistent ChatGPT Project Sources. Retry with `confirmMutation: true` or use `dryRun: true` first."),
86
+ };
87
+ }
88
+ const maxFileSizeBytes = resolveConfiguredMaxFileSizeBytes(userConfig, process.env);
89
+ const files = parsed.operation === "add"
90
+ ? await resolveProjectSourceFiles(parsed.files ?? [], {
91
+ cwd: process.cwd(),
92
+ maxFileSizeBytes,
93
+ })
94
+ : [];
95
+ const browserConfig = await buildProjectSourcesBrowserConfig({
96
+ options: {
97
+ chatgptUrl: projectUrl,
98
+ browserKeepBrowser: parsed.browserKeepBrowser,
99
+ },
100
+ projectUrl,
101
+ configuredBrowser: userConfig.browser ?? {},
102
+ });
103
+ const result = await runBrowserProjectSources({
104
+ operation: parsed.operation,
105
+ chatgptUrl: projectUrl,
106
+ files,
107
+ dryRun: parsed.dryRun,
108
+ config: browserConfig,
109
+ log: (message) => {
110
+ server.server
111
+ .sendLoggingMessage({ level: "info", data: { text: message } })
112
+ .catch(() => undefined);
113
+ },
114
+ });
115
+ const output = result.status === "dry-run"
116
+ ? `Project Sources ${result.operation} dry run: ${result.plannedUploads?.length ?? 0} planned upload(s).`
117
+ : `Project Sources ${result.operation} completed: ${result.sourcesAfter?.length ?? 0} source(s).`;
118
+ return {
119
+ content: textContent(output),
120
+ structuredContent: result,
121
+ };
122
+ });
123
+ }
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
+ export const CONSULT_PRESETS = ["chatgpt-pro-heavy"];
2
3
  export const consultInputSchema = z.object({
4
+ preset: z.enum(CONSULT_PRESETS).optional(),
3
5
  prompt: z.string().min(1, "Prompt is required."),
4
6
  files: z.array(z.string()).default([]),
5
7
  model: z.string().optional(),
@@ -9,7 +11,12 @@ export const consultInputSchema = z.object({
9
11
  browserAttachments: z.enum(["auto", "never", "always"]).optional(),
10
12
  browserBundleFiles: z.boolean().optional(),
11
13
  browserThinkingTime: z.enum(["light", "standard", "extended", "heavy"]).optional(),
14
+ browserModelStrategy: z.enum(["select", "current", "ignore"]).optional(),
15
+ browserResearchMode: z.enum(["deep"]).optional(),
16
+ browserArchive: z.enum(["auto", "always", "never"]).optional(),
17
+ browserFollowUps: z.array(z.string()).optional(),
12
18
  browserKeepBrowser: z.boolean().optional(),
19
+ dryRun: z.boolean().optional(),
13
20
  search: z.boolean().optional(),
14
21
  slug: z.string().optional(),
15
22
  });
@@ -1,6 +1,6 @@
1
1
  import { resolveRunOptionsFromConfig } from "../cli/runOptions.js";
2
2
  import { Launcher } from "chrome-launcher";
3
- export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, browserAttachments, browserBundleFiles, userConfig, env = process.env, }) {
3
+ export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, browserAttachments, browserBundleFiles, browserFollowUps, userConfig, env = process.env, }) {
4
4
  // Normalize CLI-style inputs through the shared resolver so config/env defaults apply,
5
5
  // then overlay MCP-only overrides such as explicit search toggles.
6
6
  const mergedModels = Array.isArray(models) && models.length > 0
@@ -24,6 +24,11 @@ export function mapConsultToRunOptions({ prompt, files, model, models, engine, s
24
24
  if (typeof browserBundleFiles === "boolean") {
25
25
  result.runOptions.browserBundleFiles = browserBundleFiles;
26
26
  }
27
+ if (Array.isArray(browserFollowUps)) {
28
+ result.runOptions.browserFollowUps = browserFollowUps
29
+ .map((entry) => entry.trim())
30
+ .filter(Boolean);
31
+ }
27
32
  return result;
28
33
  }
29
34
  export function ensureBrowserAvailable(engine, options) {
@@ -127,7 +127,10 @@ export async function runOracle(options, deps = {}) {
127
127
  : options.model.startsWith("grok")
128
128
  ? "XAI_API_KEY"
129
129
  : "OPENROUTER_API_KEY";
130
- throw new PromptValidationError(`Missing ${envVar}. Set it via the environment or a .env file.`, {
130
+ const browserModeHint = options.model.startsWith("gpt")
131
+ ? ' If you have a ChatGPT Pro subscription, retry with --engine browser (or MCP engine:"browser" / preset:"chatgpt-pro-heavy"); browser mode uses your signed-in ChatGPT session instead of an API key.'
132
+ : "";
133
+ throw new PromptValidationError(`Missing ${envVar}. Set it via the environment or a .env file.${browserModeHint}`, {
131
134
  env: envVar,
132
135
  });
133
136
  }
@@ -0,0 +1,27 @@
1
+ import path from "node:path";
2
+ export const PROJECT_SOURCES_MAX_UPLOAD_BATCH = 10;
3
+ export function buildProjectSourcesUploadPlan(files) {
4
+ return files.map((file, index) => ({
5
+ path: file.path,
6
+ displayPath: file.displayPath,
7
+ name: path.basename(file.path),
8
+ sizeBytes: file.sizeBytes,
9
+ batch: Math.floor(index / PROJECT_SOURCES_MAX_UPLOAD_BATCH) + 1,
10
+ }));
11
+ }
12
+ export function diffAddedProjectSources(before, after) {
13
+ const remainingBefore = new Map();
14
+ for (const source of before) {
15
+ remainingBefore.set(source.name, (remainingBefore.get(source.name) ?? 0) + 1);
16
+ }
17
+ const added = [];
18
+ for (const source of after) {
19
+ const count = remainingBefore.get(source.name) ?? 0;
20
+ if (count > 0) {
21
+ remainingBefore.set(source.name, count - 1);
22
+ continue;
23
+ }
24
+ added.push(source);
25
+ }
26
+ return added;
27
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ export function normalizeProjectSourcesUrl(rawUrl) {
2
+ let url;
3
+ try {
4
+ url = new URL(rawUrl);
5
+ }
6
+ catch (error) {
7
+ throw new Error(`Invalid ChatGPT project URL: ${rawUrl} (${error instanceof Error ? error.message : String(error)})`);
8
+ }
9
+ const hostname = url.hostname.toLowerCase();
10
+ if (hostname !== "chatgpt.com" && hostname !== "chat.openai.com") {
11
+ throw new Error(`Project Sources require a ChatGPT URL, received: ${rawUrl}`);
12
+ }
13
+ if (!/\/project\/?$/u.test(url.pathname)) {
14
+ throw new Error(`Project Sources require a ChatGPT project URL ending in /project, received: ${rawUrl}`);
15
+ }
16
+ const existingParams = Array.from(url.searchParams.entries()).filter(([key]) => key !== "tab");
17
+ url.search = "";
18
+ url.searchParams.set("tab", "sources");
19
+ for (const [key, value] of existingParams) {
20
+ url.searchParams.append(key, value);
21
+ }
22
+ return url.toString();
23
+ }
@@ -213,6 +213,7 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
213
213
  generateImage: options.generateImage,
214
214
  editImage: options.editImage,
215
215
  outputPath: options.outputPath,
216
+ browserFollowUps: options.browserFollowUps,
216
217
  aspectRatio: options.aspectRatio,
217
218
  geminiShowThoughts: options.geminiShowThoughts,
218
219
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.5 Pro, GPT-5.5, 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",
@@ -30,6 +30,7 @@
30
30
  "main": "dist/bin/oracle-cli.js",
31
31
  "scripts": {
32
32
  "docs:list": "tsx scripts/docs-list.ts",
33
+ "docs:site": "node scripts/build-docs-site.mjs",
33
34
  "build": "tsgo -p tsconfig.build.json && pnpm run build:vendor",
34
35
  "build:vendor": "node -e \"const fs=require('fs'); const path=require('path'); const vendorRoot=path.join('dist','vendor'); fs.rmSync(vendorRoot,{recursive:true,force:true}); const vendors=[['oracle-notifier']]; vendors.forEach(([name])=>{const src=path.join('vendor',name); const dest=path.join(vendorRoot,name); fs.mkdirSync(dest,{recursive:true}); if(fs.existsSync(src)){fs.cpSync(src,dest,{recursive:true,force:true});}});\"",
35
36
  "start": "pnpm run build && node ./dist/scripts/run-cli.js",