@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.
- package/README.md +55 -10
- package/dist/bin/oracle-cli.js +104 -16
- package/dist/src/browser/actions/archiveConversation.js +224 -0
- package/dist/src/browser/actions/assistantResponse.js +26 -0
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/modelSelection.js +78 -13
- package/dist/src/browser/actions/navigation.js +22 -0
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +52 -27
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +214 -3
- package/dist/src/browser/config.js +26 -2
- package/dist/src/browser/constants.js +8 -0
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/detect.js +206 -33
- package/dist/src/browser/domDebug.js +49 -0
- package/dist/src/browser/index.js +1257 -485
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/profileState.js +83 -3
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/reattach.js +117 -45
- package/dist/src/browser/reattachHelpers.js +1 -1
- package/dist/src/browser/sessionRunner.js +53 -1
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/cli/bridge/claudeConfig.js +12 -8
- package/dist/src/cli/bridge/codexConfig.js +2 -2
- package/dist/src/cli/browserConfig.js +40 -0
- package/dist/src/cli/browserDefaults.js +31 -7
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/dryRun.js +33 -1
- package/dist/src/cli/duplicatePromptGuard.js +10 -2
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +4 -0
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/sessionCommand.js +51 -0
- package/dist/src/cli/sessionDisplay.js +121 -9
- package/dist/src/cli/sessionRunner.js +51 -7
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/consult.js +201 -26
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/types.js +7 -0
- package/dist/src/mcp/utils.js +6 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/types.js +1 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/sessionManager.js +1 -0
- 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
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/src/mcp/types.js
CHANGED
|
@@ -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
|
});
|
package/dist/src/mcp/utils.js
CHANGED
|
@@ -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) {
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|