@steipete/oracle 0.12.1 → 0.14.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 +72 -61
- package/dist/bin/oracle-cli.js +77 -68
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +400 -0
- package/dist/docs-site/browser-mode.html +593 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +452 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +387 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +388 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +323 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/src/browser/actions/assistantResponse.js +91 -9
- package/dist/src/browser/actions/deepResearch.js +180 -52
- package/dist/src/browser/actions/modelSelection.js +111 -24
- package/dist/src/browser/actions/navigation.js +358 -67
- package/dist/src/browser/actions/promptComposer.js +241 -46
- package/dist/src/browser/actions/thinkingTime.js +308 -89
- package/dist/src/browser/artifacts.js +19 -0
- package/dist/src/browser/chatgptFiles.js +797 -0
- package/dist/src/browser/chatgptImages.js +130 -3
- package/dist/src/browser/chromeLifecycle.js +4 -0
- package/dist/src/browser/config.js +4 -0
- package/dist/src/browser/index.js +353 -41
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/policies.js +2 -6
- package/dist/src/browser/projectSourcesRunner.js +14 -2
- package/dist/src/browser/prompt.js +11 -15
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattach.js +6 -2
- package/dist/src/browser/reattachability.js +22 -10
- package/dist/src/browser/recoverConversation.js +73 -0
- package/dist/src/browser/sessionRunner.js +4 -1
- package/dist/src/cli/bridge/doctor.js +7 -2
- package/dist/src/cli/browserConfig.js +13 -2
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +129 -54
- package/dist/src/cli/engine.js +6 -2
- package/dist/src/cli/followup.js +72 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +20 -0
- package/dist/src/cli/reattachGuidance.js +8 -0
- package/dist/src/cli/runOptions.js +10 -31
- package/dist/src/cli/sessionCommand.js +4 -0
- package/dist/src/cli/sessionDisplay.js +17 -3
- package/dist/src/cli/sessionLineage.js +7 -4
- package/dist/src/cli/sessionRunner.js +16 -1
- package/dist/src/config.js +164 -9
- package/dist/src/gemini-web/client.js +5 -10
- package/dist/src/gemini-web/executor.js +1 -24
- package/dist/src/gemini-web/models.js +83 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/chatgptImage.js +127 -0
- package/dist/src/mcp/tools/consult.js +261 -167
- package/dist/src/mcp/types.js +2 -0
- package/dist/src/mcp/utils.js +87 -1
- package/dist/src/oracle/config.js +27 -1
- package/dist/src/oracle/files.js +4 -6
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/markdown.js +36 -3
- package/dist/src/oracle/modelResolver.js +21 -13
- package/dist/src/oracle/promptAssembly.js +2 -4
- package/dist/src/oracle/request.js +3 -5
- package/dist/src/oracle/tokenStats.js +5 -1
- package/dist/src/oracle.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +20 -29
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CHATGPT_URL } from "../browser/constants.js";
|
|
2
|
+
import { buildConversationUrl } from "../browser/reattachHelpers.js";
|
|
3
|
+
import { resolveRecoveryUrl } from "../browser/recoverConversation.js";
|
|
4
|
+
import { isRecoverableChatGptConversationUrl } from "../browser/reattachability.js";
|
|
5
|
+
import { DEFAULT_MODEL } from "../oracle/config.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the ChatGPT conversation URL to reopen for a browser follow-up.
|
|
8
|
+
*
|
|
9
|
+
* Reuses the same recoverable-URL gate as conversation recovery
|
|
10
|
+
* (`resolveRecoveryUrl`): prefer the post-harvest URL, fall back to the
|
|
11
|
+
* runtime tab URL, and reject home / project-shell / external URLs via
|
|
12
|
+
* `isRecoverableChatGptConversationUrl`. Only when neither candidate is a
|
|
13
|
+
* recoverable `chatgpt.com/c/<id>` URL do we rebuild from a stored
|
|
14
|
+
* `conversationId` against the session's ChatGPT base — and that rebuilt URL is
|
|
15
|
+
* gated too. This prevents a stale or attacker-controlled URL in session
|
|
16
|
+
* metadata from navigating the signed-in browser profile somewhere unintended.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveBrowserResumeConversationUrl(metadata, fallbackBaseUrl = CHATGPT_URL) {
|
|
19
|
+
const gatedUrl = resolveRecoveryUrl(metadata);
|
|
20
|
+
if (gatedUrl) {
|
|
21
|
+
return gatedUrl;
|
|
22
|
+
}
|
|
23
|
+
const conversationId = metadata.browser?.runtime?.conversationId?.trim();
|
|
24
|
+
if (!conversationId) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const baseUrl = metadata.browser?.config?.url ?? fallbackBaseUrl;
|
|
28
|
+
const built = buildConversationUrl({ conversationId }, baseUrl);
|
|
29
|
+
if (built && isRecoverableChatGptConversationUrl(built)) {
|
|
30
|
+
return built;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export async function resolveBrowserFollowupReference(value, store) {
|
|
35
|
+
const trimmed = value.trim();
|
|
36
|
+
if (!trimmed || trimmed.startsWith("resp_")) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const metadata = await store.readSession(trimmed);
|
|
40
|
+
if (!metadata) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const mode = metadata.mode ?? metadata.options?.mode;
|
|
44
|
+
const hasBrowserMetadata = Boolean(metadata.browser?.runtime || metadata.browser?.config || metadata.options?.browserConfig);
|
|
45
|
+
if (mode !== "browser" && !hasBrowserMetadata) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const resumeConversationUrl = resolveBrowserResumeConversationUrl(metadata);
|
|
49
|
+
if (!resumeConversationUrl) {
|
|
50
|
+
throw new Error(`Session ${trimmed} is a browser session but does not contain a ChatGPT conversation URL. Run "oracle status --hours 72 --limit 20" to list recent sessions.`);
|
|
51
|
+
}
|
|
52
|
+
const parentBrowserConfig = metadata.options?.browserConfig ?? metadata.browser?.config;
|
|
53
|
+
if (!parentBrowserConfig) {
|
|
54
|
+
throw new Error(`Session ${trimmed} is missing its stored browser configuration.`);
|
|
55
|
+
}
|
|
56
|
+
const storedModel = metadata.options?.model ?? metadata.model;
|
|
57
|
+
const model = typeof storedModel === "string" && storedModel.startsWith("gpt-")
|
|
58
|
+
? storedModel
|
|
59
|
+
: DEFAULT_MODEL;
|
|
60
|
+
return {
|
|
61
|
+
sessionId: metadata.id,
|
|
62
|
+
resumeConversationUrl,
|
|
63
|
+
model,
|
|
64
|
+
browserConfig: {
|
|
65
|
+
...parentBrowserConfig,
|
|
66
|
+
browserTabRef: null,
|
|
67
|
+
resumeConversationUrl,
|
|
68
|
+
researchMode: "off",
|
|
69
|
+
archiveConversations: "never",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/src/cli/help.js
CHANGED
|
@@ -49,7 +49,7 @@ function renderHelpFooter(program, colors) {
|
|
|
49
49
|
`${colors.bullet("•")} Spell out the project + platform + version requirements (repo name, target OS/toolchain versions, API dependencies) so Oracle doesn’t guess defaults.`,
|
|
50
50
|
`${colors.bullet("•")} When comparing multiple repos/files, spell out each repo + path + role (e.g., “Project A SettingsView → apps/project-a/Sources/SettingsView.swift; Project B SettingsView → ../project-b/mac/...”) so the model knows exactly which file is which.`,
|
|
51
51
|
`${colors.bullet("•")} Best results: 6–30 sentences plus key source files; very short prompts often yield generic answers.`,
|
|
52
|
-
`${colors.bullet("•")} Oracle is one-shot by default. API
|
|
52
|
+
`${colors.bullet("•")} Oracle is one-shot by default. Continue saved API or ChatGPT browser sessions with ${colors.accent("--followup <sessionId|responseId>")}; use repeated ${colors.accent("--browser-follow-up")} for planned same-run ChatGPT turns.`,
|
|
53
53
|
`${colors.bullet("•")} Run ${colors.accent("--files-report")} to inspect token spend before hitting the API.`,
|
|
54
54
|
`${colors.bullet("•")} Non-preview runs spawn detached sessions (especially gpt-5.5-pro API). If the CLI times out, do not re-run — reattach with ${colors.accent("oracle session <slug>")} to resume/inspect the existing run.`,
|
|
55
55
|
`${colors.bullet("•")} Set a memorable 3–5 word slug via ${colors.accent('--slug "<words>"')} to keep session IDs tidy.`,
|
package/dist/src/cli/options.js
CHANGED
|
@@ -228,6 +228,14 @@ export function resolveApiModel(modelValue) {
|
|
|
228
228
|
throw new InvalidArgumentError("Gemini Deep Think is browser-only today. Use --engine browser --model gemini-3-deep-think.");
|
|
229
229
|
}
|
|
230
230
|
if (normalized.includes("gemini")) {
|
|
231
|
+
if (normalized.includes("3.5") && normalized.includes("flash")) {
|
|
232
|
+
return "gemini-3.5-flash";
|
|
233
|
+
}
|
|
234
|
+
if ((normalized.includes("3.1") || normalized.includes("3_1")) &&
|
|
235
|
+
normalized.includes("flash") &&
|
|
236
|
+
normalized.includes("lite")) {
|
|
237
|
+
return "gemini-3.1-flash-lite";
|
|
238
|
+
}
|
|
231
239
|
if (normalized.includes("3.1") || normalized.includes("3_1")) {
|
|
232
240
|
return "gemini-3.1-pro";
|
|
233
241
|
}
|
|
@@ -266,6 +274,14 @@ export function inferModelFromLabel(modelValue) {
|
|
|
266
274
|
return "gemini-3-pro-deep-think";
|
|
267
275
|
}
|
|
268
276
|
if (normalized.includes("gemini")) {
|
|
277
|
+
if (normalized.includes("3.5") && normalized.includes("flash")) {
|
|
278
|
+
return "gemini-3.5-flash";
|
|
279
|
+
}
|
|
280
|
+
if ((normalized.includes("3.1") || normalized.includes("3_1")) &&
|
|
281
|
+
normalized.includes("flash") &&
|
|
282
|
+
normalized.includes("lite")) {
|
|
283
|
+
return "gemini-3.1-flash-lite";
|
|
284
|
+
}
|
|
269
285
|
if (normalized.includes("3.1") || normalized.includes("3_1")) {
|
|
270
286
|
return "gemini-3.1-pro";
|
|
271
287
|
}
|
|
@@ -280,6 +296,10 @@ export function inferModelFromLabel(modelValue) {
|
|
|
280
296
|
if ((normalized.includes("5.5") || normalized.includes("5_5")) && normalized.includes("pro")) {
|
|
281
297
|
return "gpt-5.5-pro";
|
|
282
298
|
}
|
|
299
|
+
if ((normalized.includes("5.5") || normalized.includes("5_5")) &&
|
|
300
|
+
(normalized.includes("instant") || normalized.includes("fast"))) {
|
|
301
|
+
return "gpt-5.5-instant";
|
|
302
|
+
}
|
|
283
303
|
if (normalized.includes("5.5") || normalized.includes("5_5")) {
|
|
284
304
|
return "gpt-5.5";
|
|
285
305
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function formatBrowserReattachGuidance(sessionId) {
|
|
2
|
+
return [
|
|
3
|
+
"This run did not return cleanly, but it may still be alive. Reattach:",
|
|
4
|
+
` oracle session ${sessionId} --render # final markdown when complete`,
|
|
5
|
+
` oracle session ${sessionId} --live # tail until done`,
|
|
6
|
+
` oracle session ${sessionId} --harvest # snapshot the current answer now`,
|
|
7
|
+
].join("\n");
|
|
8
|
+
}
|
|
@@ -7,14 +7,17 @@ import { normalizeChatGptModelForBrowser } from "./browserConfig.js";
|
|
|
7
7
|
import { resolveConfiguredMaxFileSizeBytes } from "./fileSize.js";
|
|
8
8
|
import { isAzureOpenAICandidateModel } from "../oracle/providerRouting.js";
|
|
9
9
|
export function resolveRunOptionsFromConfig({ prompt, files = [], model, models, engine, userConfig, env = process.env, }) {
|
|
10
|
-
const resolvedEngine =
|
|
10
|
+
const resolvedEngine = resolveEngine({
|
|
11
11
|
engine,
|
|
12
12
|
configEngine: userConfig?.engine,
|
|
13
13
|
env,
|
|
14
14
|
});
|
|
15
|
+
const envEnginePreference = (env.ORACLE_ENGINE ?? "").trim().toLowerCase();
|
|
15
16
|
const browserRequested = engine === "browser";
|
|
16
|
-
const
|
|
17
|
-
const
|
|
17
|
+
const explicitApiEngineRequested = engine === "api" || (!engine && envEnginePreference === "api");
|
|
18
|
+
const browserConfigured = userConfig?.engine === "browser" && !explicitApiEngineRequested;
|
|
19
|
+
const envBrowserConfigured = !engine && envEnginePreference === "browser";
|
|
20
|
+
const browserEngineRequested = browserRequested || browserConfigured || envBrowserConfigured;
|
|
18
21
|
const requestedModelList = Array.isArray(models) ? models : [];
|
|
19
22
|
const normalizedRequestedModels = requestedModelList
|
|
20
23
|
.map((entry) => normalizeModelOption(entry))
|
|
@@ -25,35 +28,22 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
25
28
|
const isCodex = apiModel.startsWith("gpt-5.1-codex");
|
|
26
29
|
const isClaude = apiModel.startsWith("claude");
|
|
27
30
|
const isGrok = apiModel.startsWith("grok");
|
|
28
|
-
const isGeminiApiOnly = apiModel === "gemini-3.1-pro";
|
|
29
31
|
const engineWasBrowser = resolvedEngine === "browser";
|
|
30
32
|
const allModels = normalizedRequestedModels.length > 0
|
|
31
33
|
? Array.from(new Set(normalizedRequestedModels.map((entry) => resolveApiModel(entry))))
|
|
32
34
|
: [apiModel];
|
|
33
35
|
const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel];
|
|
34
|
-
const includesGeminiApiOnly = allModels.some((m) => m === "gemini-3.1-pro");
|
|
35
|
-
if ((browserRequested || browserConfigured) && includesGeminiApiOnly) {
|
|
36
|
-
throw new PromptValidationError("gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.", { engine: "browser", models: allModels });
|
|
37
|
-
}
|
|
38
36
|
const isBrowserCompatible = (m) => m.startsWith("gpt-") || m.startsWith("gemini");
|
|
39
|
-
const hasNonBrowserCompatibleTarget = (
|
|
40
|
-
browserCompatibilityModels.some((m) => !isBrowserCompatible(m));
|
|
37
|
+
const hasNonBrowserCompatibleTarget = browserEngineRequested && browserCompatibilityModels.some((m) => !isBrowserCompatible(m));
|
|
41
38
|
if (hasNonBrowserCompatibleTarget) {
|
|
42
39
|
throw new PromptValidationError("Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.", { engine: "browser", models: allModels });
|
|
43
40
|
}
|
|
44
41
|
const azure = resolveAzureOptions(userConfig, env);
|
|
45
42
|
const azureAutoApi = Boolean(azure?.endpoint) &&
|
|
46
|
-
!
|
|
47
|
-
!browserConfigured &&
|
|
48
|
-
!envBrowserConfigured &&
|
|
43
|
+
!browserEngineRequested &&
|
|
49
44
|
allModels.some(isAzureOpenAICandidateModel);
|
|
50
|
-
const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok ||
|
|
51
|
-
const fixedEngine = isCodex ||
|
|
52
|
-
isClaude ||
|
|
53
|
-
isGrok ||
|
|
54
|
-
isGeminiApiOnly ||
|
|
55
|
-
azureAutoApi ||
|
|
56
|
-
normalizedRequestedModels.length > 0
|
|
45
|
+
const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok || azureAutoApi);
|
|
46
|
+
const fixedEngine = isCodex || isClaude || isGrok || azureAutoApi || normalizedRequestedModels.length > 0
|
|
57
47
|
? "api"
|
|
58
48
|
: resolvedEngine;
|
|
59
49
|
// Browser runs use ChatGPT picker labels/aliases; API runs must keep API model ids intact.
|
|
@@ -89,17 +79,6 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
89
79
|
};
|
|
90
80
|
return { runOptions, resolvedEngine: fixedEngine, engineCoercedToApi };
|
|
91
81
|
}
|
|
92
|
-
function resolveEngineWithConfig({ engine, configEngine, apiProviderRequested, env, }) {
|
|
93
|
-
if (engine)
|
|
94
|
-
return engine;
|
|
95
|
-
const envOverride = (env.ORACLE_ENGINE ?? "").trim().toLowerCase();
|
|
96
|
-
if (envOverride === "api" || envOverride === "browser") {
|
|
97
|
-
return envOverride;
|
|
98
|
-
}
|
|
99
|
-
if (configEngine)
|
|
100
|
-
return configEngine;
|
|
101
|
-
return resolveEngine({ engine: undefined, apiProviderRequested, env });
|
|
102
|
-
}
|
|
103
82
|
function resolveAzureOptions(userConfig, env) {
|
|
104
83
|
const endpoint = env.AZURE_OPENAI_ENDPOINT ?? userConfig?.azure?.endpoint;
|
|
105
84
|
if (!endpoint?.trim()) {
|
|
@@ -109,16 +109,20 @@ export async function handleSessionCommand(sessionId, command, deps = defaultDep
|
|
|
109
109
|
process.exitCode = 1;
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
|
+
// Commander sets `recover: false` when --no-recover is passed; default is `true`.
|
|
113
|
+
const recoverIfMissing = sessionOptions.recover !== false;
|
|
112
114
|
if (harvestRequested) {
|
|
113
115
|
await deps.harvestSessionBrowserOutput(sessionId, {
|
|
114
116
|
writeOutputPath,
|
|
115
117
|
browserTabRef,
|
|
118
|
+
recoverIfMissing,
|
|
116
119
|
});
|
|
117
120
|
return;
|
|
118
121
|
}
|
|
119
122
|
await deps.liveTailSessionBrowserOutput(sessionId, {
|
|
120
123
|
writeOutputPath,
|
|
121
124
|
browserTabRef,
|
|
125
|
+
recoverIfMissing,
|
|
122
126
|
});
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
@@ -628,6 +628,14 @@ function matchesModel(entry, filter) {
|
|
|
628
628
|
(entry.model ? [entry.model.toLowerCase()] : []);
|
|
629
629
|
return models.includes(normalized);
|
|
630
630
|
}
|
|
631
|
+
function formatLineageParentLabel(lineage) {
|
|
632
|
+
if (!lineage?.parentSessionId) {
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
return lineage.parentResponseId
|
|
636
|
+
? `${lineage.parentSessionId} (${abbreviateResponseId(lineage.parentResponseId)})`
|
|
637
|
+
: lineage.parentSessionId;
|
|
638
|
+
}
|
|
631
639
|
function buildStatusTreeRows(entries, responseOwners) {
|
|
632
640
|
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
633
641
|
const orderIndex = new Map(entries.map((entry, index) => [entry.id, index]));
|
|
@@ -668,7 +676,7 @@ function buildStatusTreeRows(entries, responseOwners) {
|
|
|
668
676
|
visited.add(entry.id);
|
|
669
677
|
const lineage = lineageById.get(entry.id);
|
|
670
678
|
const hiddenParent = lineage?.parentSessionId && !entryById.has(lineage.parentSessionId)
|
|
671
|
-
?
|
|
679
|
+
? formatLineageParentLabel(lineage)
|
|
672
680
|
: undefined;
|
|
673
681
|
const children = childMap.get(entry.id) ?? [];
|
|
674
682
|
rows.push({ entry, displaySlug: entry.id, detachedParentLabel: hiddenParent });
|
|
@@ -694,13 +702,19 @@ async function buildSessionChainLine(metadata) {
|
|
|
694
702
|
return `root -> ${metadata.id}`;
|
|
695
703
|
}
|
|
696
704
|
if (lineageWithoutLookup.parentSessionId) {
|
|
697
|
-
return `${
|
|
705
|
+
return `${formatLineageParentLabel(lineageWithoutLookup)} -> ${metadata.id}`;
|
|
706
|
+
}
|
|
707
|
+
if (!lineageWithoutLookup.parentResponseId) {
|
|
708
|
+
return `root -> ${metadata.id}`;
|
|
698
709
|
}
|
|
699
710
|
const sessions = await sessionStore.listSessions().catch(() => []);
|
|
700
711
|
const responseOwners = buildResponseOwnerIndex(sessions);
|
|
701
712
|
const lineage = resolveSessionLineage(metadata, responseOwners) ?? lineageWithoutLookup;
|
|
702
713
|
if (lineage.parentSessionId) {
|
|
703
|
-
return `${
|
|
714
|
+
return `${formatLineageParentLabel(lineage)} -> ${metadata.id}`;
|
|
715
|
+
}
|
|
716
|
+
if (!lineage.parentResponseId) {
|
|
717
|
+
return `root -> ${metadata.id}`;
|
|
704
718
|
}
|
|
705
719
|
return `${abbreviateResponseId(lineage.parentResponseId)} -> ${metadata.id}`;
|
|
706
720
|
}
|
|
@@ -39,17 +39,20 @@ export function buildResponseOwnerIndex(sessions) {
|
|
|
39
39
|
}
|
|
40
40
|
export function resolveSessionLineage(meta, responseOwners) {
|
|
41
41
|
const previous = meta.options?.previousResponseId?.trim();
|
|
42
|
-
|
|
42
|
+
let parentSessionId = meta.options?.followupSessionId?.trim();
|
|
43
|
+
if (!previous && !parentSessionId) {
|
|
43
44
|
return null;
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
if (!parentSessionId && responseOwners) {
|
|
46
|
+
if (!parentSessionId && previous && responseOwners) {
|
|
47
47
|
parentSessionId = responseOwners.get(previous);
|
|
48
48
|
}
|
|
49
49
|
if (parentSessionId === meta.id) {
|
|
50
50
|
parentSessionId = undefined;
|
|
51
51
|
}
|
|
52
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
parentResponseId: previous || undefined,
|
|
54
|
+
parentSessionId,
|
|
55
|
+
};
|
|
53
56
|
}
|
|
54
57
|
export function abbreviateResponseId(responseId, max = 18) {
|
|
55
58
|
if (responseId.length <= max) {
|
|
@@ -24,6 +24,7 @@ import { resumeBrowserSession } from "../browser/reattach.js";
|
|
|
24
24
|
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
|
|
25
25
|
import { estimateTokenCount } from "../browser/utils.js";
|
|
26
26
|
import { formatElapsed } from "../oracle/format.js";
|
|
27
|
+
import { formatBrowserReattachGuidance } from "./reattachGuidance.js";
|
|
27
28
|
const isTty = process.stdout.isTTY;
|
|
28
29
|
const dim = (text) => (isTty ? kleur.dim(text) : text);
|
|
29
30
|
export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, }) {
|
|
@@ -388,6 +389,16 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
388
389
|
userError.details?.stage === "assistant-timeout";
|
|
389
390
|
const cloudflareChallenge = userError?.category === "browser-automation" &&
|
|
390
391
|
userError.details?.stage === "cloudflare-challenge";
|
|
392
|
+
let reattachGuidanceLogged = false;
|
|
393
|
+
const logBrowserReattachGuidance = (runtime) => {
|
|
394
|
+
if (reattachGuidanceLogged || mode !== "browser")
|
|
395
|
+
return;
|
|
396
|
+
if (!hasRecoverableChatGptConversation(runtime) && runtime?.promptSubmitted !== true) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
reattachGuidanceLogged = true;
|
|
400
|
+
log(formatBrowserReattachGuidance(sessionMeta.id));
|
|
401
|
+
};
|
|
391
402
|
if (connectionLost && mode === "browser") {
|
|
392
403
|
const runtime = userError.details
|
|
393
404
|
?.runtime;
|
|
@@ -442,6 +453,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
442
453
|
},
|
|
443
454
|
response: { status: "running", incompleteReason: "chrome-disconnected" },
|
|
444
455
|
});
|
|
456
|
+
logBrowserReattachGuidance(recoverableRuntime);
|
|
445
457
|
return;
|
|
446
458
|
}
|
|
447
459
|
if (assistantTimeout && mode === "browser") {
|
|
@@ -492,7 +504,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
492
504
|
return;
|
|
493
505
|
}
|
|
494
506
|
}
|
|
495
|
-
|
|
507
|
+
logBrowserReattachGuidance(runtime ?? sessionMeta.browser?.runtime);
|
|
496
508
|
return;
|
|
497
509
|
}
|
|
498
510
|
if (cloudflareChallenge && mode === "browser") {
|
|
@@ -518,6 +530,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
518
530
|
const browserRuntime = mode === "browser"
|
|
519
531
|
? userError?.details?.runtime
|
|
520
532
|
: undefined;
|
|
533
|
+
if (!cloudflareChallenge) {
|
|
534
|
+
logBrowserReattachGuidance(browserRuntime ?? sessionMeta.browser?.runtime);
|
|
535
|
+
}
|
|
521
536
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
522
537
|
status: "error",
|
|
523
538
|
completedAt: new Date().toISOString(),
|
package/dist/src/config.js
CHANGED
|
@@ -1,26 +1,181 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import JSON5 from "json5";
|
|
4
5
|
import { getOracleHomeDir } from "./oracleHome.js";
|
|
5
|
-
|
|
6
|
+
export const PROJECT_CONFIG_RELATIVE_PATH = path.join(".oracle", "config.json");
|
|
7
|
+
function resolveUserConfigPath() {
|
|
6
8
|
return path.join(getOracleHomeDir(), "config.json");
|
|
7
9
|
}
|
|
8
|
-
export async function loadUserConfig() {
|
|
9
|
-
const
|
|
10
|
+
export async function loadUserConfig(options = {}) {
|
|
11
|
+
const userConfigPath = resolveUserConfigPath();
|
|
12
|
+
const userConfig = await readConfigFile(userConfigPath);
|
|
13
|
+
const projectConfigPaths = options.includeProject === false
|
|
14
|
+
? []
|
|
15
|
+
: await discoverProjectConfigPaths({
|
|
16
|
+
cwd: options.cwd ?? process.cwd(),
|
|
17
|
+
userConfigPath,
|
|
18
|
+
});
|
|
19
|
+
const loadedConfigs = [];
|
|
20
|
+
if (userConfig.loaded) {
|
|
21
|
+
loadedConfigs.push(userConfig);
|
|
22
|
+
}
|
|
23
|
+
let merged = userConfig.loaded ? userConfig.config : {};
|
|
24
|
+
for (const projectConfigPath of projectConfigPaths) {
|
|
25
|
+
const projectConfig = await readConfigFile(projectConfigPath);
|
|
26
|
+
if (!projectConfig.loaded)
|
|
27
|
+
continue;
|
|
28
|
+
loadedConfigs.push(projectConfig);
|
|
29
|
+
merged = mergeUserConfig(merged, sanitizeProjectConfig(projectConfig.config));
|
|
30
|
+
}
|
|
31
|
+
const loadedPaths = loadedConfigs.map((entry) => entry.path);
|
|
32
|
+
return {
|
|
33
|
+
config: merged,
|
|
34
|
+
path: userConfigPath,
|
|
35
|
+
paths: loadedPaths,
|
|
36
|
+
loaded: userConfig.loaded,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function readConfigFile(configPath) {
|
|
10
40
|
try {
|
|
11
|
-
const raw = await fs.readFile(
|
|
41
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
12
42
|
const parsed = JSON5.parse(raw);
|
|
13
|
-
return { config: parsed ?? {}, path:
|
|
43
|
+
return { config: parsed ?? {}, path: configPath, loaded: true };
|
|
14
44
|
}
|
|
15
45
|
catch (error) {
|
|
16
46
|
const code = error.code;
|
|
17
47
|
if (code === "ENOENT") {
|
|
18
|
-
return { config: {}, path:
|
|
48
|
+
return { config: {}, path: configPath, loaded: false };
|
|
19
49
|
}
|
|
20
|
-
console.warn(`Failed to read ${
|
|
21
|
-
return { config: {}, path:
|
|
50
|
+
console.warn(`Failed to read ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
return { config: {}, path: configPath, loaded: false };
|
|
22
52
|
}
|
|
23
53
|
}
|
|
24
54
|
export function configPath() {
|
|
25
|
-
return
|
|
55
|
+
return resolveUserConfigPath();
|
|
56
|
+
}
|
|
57
|
+
async function discoverProjectConfigPaths({ cwd, userConfigPath, }) {
|
|
58
|
+
const start = path.resolve(cwd);
|
|
59
|
+
const home = os.homedir();
|
|
60
|
+
const candidates = [];
|
|
61
|
+
const seen = new Set([path.resolve(userConfigPath)]);
|
|
62
|
+
let current = start;
|
|
63
|
+
while (true) {
|
|
64
|
+
if (current === home) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
const candidate = path.join(current, PROJECT_CONFIG_RELATIVE_PATH);
|
|
68
|
+
const resolved = path.resolve(candidate);
|
|
69
|
+
if (!seen.has(resolved)) {
|
|
70
|
+
try {
|
|
71
|
+
const stat = await fs.stat(resolved);
|
|
72
|
+
if (stat.isFile()) {
|
|
73
|
+
candidates.unshift(resolved);
|
|
74
|
+
seen.add(resolved);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== "ENOENT") {
|
|
79
|
+
console.warn(`Failed to inspect ${resolved}: ${error instanceof Error ? error.message : String(error)}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const parent = path.dirname(current);
|
|
84
|
+
if (parent === current) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
current = parent;
|
|
88
|
+
}
|
|
89
|
+
return candidates;
|
|
90
|
+
}
|
|
91
|
+
function mergeUserConfig(base, override) {
|
|
92
|
+
return deepMerge(base, override);
|
|
93
|
+
}
|
|
94
|
+
function isRecord(value) {
|
|
95
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
96
|
+
}
|
|
97
|
+
function deepMerge(base, override) {
|
|
98
|
+
if (!isRecord(base) || !isRecord(override)) {
|
|
99
|
+
return override;
|
|
100
|
+
}
|
|
101
|
+
const result = { ...base };
|
|
102
|
+
for (const [key, value] of Object.entries(override)) {
|
|
103
|
+
const existing = result[key];
|
|
104
|
+
result[key] = isRecord(existing) && isRecord(value) ? deepMerge(existing, value) : value;
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
function sanitizeProjectConfig(config) {
|
|
109
|
+
const sanitized = {};
|
|
110
|
+
if (config.engine !== undefined)
|
|
111
|
+
sanitized.engine = config.engine;
|
|
112
|
+
if (config.model !== undefined)
|
|
113
|
+
sanitized.model = config.model;
|
|
114
|
+
if (config.search !== undefined)
|
|
115
|
+
sanitized.search = config.search;
|
|
116
|
+
if (config.maxFileSizeBytes !== undefined)
|
|
117
|
+
sanitized.maxFileSizeBytes = config.maxFileSizeBytes;
|
|
118
|
+
if (config.notify !== undefined)
|
|
119
|
+
sanitized.notify = config.notify;
|
|
120
|
+
if (config.heartbeatSeconds !== undefined)
|
|
121
|
+
sanitized.heartbeatSeconds = config.heartbeatSeconds;
|
|
122
|
+
if (config.filesReport !== undefined)
|
|
123
|
+
sanitized.filesReport = config.filesReport;
|
|
124
|
+
if (config.background !== undefined)
|
|
125
|
+
sanitized.background = config.background;
|
|
126
|
+
if (config.promptSuffix !== undefined)
|
|
127
|
+
sanitized.promptSuffix = config.promptSuffix;
|
|
128
|
+
if (config.browser) {
|
|
129
|
+
sanitized.browser = {};
|
|
130
|
+
const browser = config.browser;
|
|
131
|
+
const allowedBrowserKeys = [
|
|
132
|
+
"attachRunning",
|
|
133
|
+
"timeoutMs",
|
|
134
|
+
"inputTimeoutMs",
|
|
135
|
+
"attachmentTimeoutMs",
|
|
136
|
+
"assistantRecheckDelayMs",
|
|
137
|
+
"assistantRecheckTimeoutMs",
|
|
138
|
+
"reuseChromeWaitMs",
|
|
139
|
+
"profileLockTimeoutMs",
|
|
140
|
+
"maxConcurrentTabs",
|
|
141
|
+
"autoReattachDelayMs",
|
|
142
|
+
"autoReattachIntervalMs",
|
|
143
|
+
"autoReattachTimeoutMs",
|
|
144
|
+
"cookieSyncWaitMs",
|
|
145
|
+
"hideWindow",
|
|
146
|
+
"keepBrowser",
|
|
147
|
+
"modelStrategy",
|
|
148
|
+
"thinkingTime",
|
|
149
|
+
"researchMode",
|
|
150
|
+
"archiveConversations",
|
|
151
|
+
"manualLogin",
|
|
152
|
+
];
|
|
153
|
+
for (const key of allowedBrowserKeys) {
|
|
154
|
+
if (browser[key] !== undefined) {
|
|
155
|
+
sanitized.browser[key] = browser[key];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const chatgptUrl = browser.chatgptUrl ?? browser.url;
|
|
159
|
+
if (chatgptUrl === null ||
|
|
160
|
+
(chatgptUrl !== undefined && isTrustedProjectChatgptUrl(chatgptUrl))) {
|
|
161
|
+
sanitized.browser.chatgptUrl = chatgptUrl;
|
|
162
|
+
sanitized.browser.url = chatgptUrl;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return sanitized;
|
|
166
|
+
}
|
|
167
|
+
function isTrustedProjectChatgptUrl(rawUrl) {
|
|
168
|
+
if (!rawUrl) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const parsed = new URL(rawUrl);
|
|
173
|
+
if (parsed.protocol !== "https:") {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return parsed.hostname === "chatgpt.com" || parsed.hostname === "chat.openai.com";
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
26
181
|
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { buildGeminiWebModelHeader, FALLBACK_GEMINI_WEB_MODEL, } from "./models.js";
|
|
3
4
|
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
4
5
|
const MODEL_HEADER_NAME = "x-goog-ext-525001261-jspb";
|
|
5
|
-
const MODEL_HEADERS = {
|
|
6
|
-
"gemini-3-pro": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]',
|
|
7
|
-
"gemini-3-pro-deep-think": '[1,null,null,null,"e6fa609c3fa255c0",null,null,0,[4],null,null,3]',
|
|
8
|
-
"gemini-2.5-pro": '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]',
|
|
9
|
-
"gemini-2.5-flash": '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]',
|
|
10
|
-
};
|
|
11
6
|
const GEMINI_APP_URL = "https://gemini.google.com/app";
|
|
12
7
|
const GEMINI_STREAM_GENERATE_URL = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate";
|
|
13
8
|
const GEMINI_UPLOAD_URL = "https://content-push.googleapis.com/upload";
|
|
@@ -276,7 +271,7 @@ export async function runGeminiWebOnce(input) {
|
|
|
276
271
|
"x-same-domain": "1",
|
|
277
272
|
"user-agent": USER_AGENT,
|
|
278
273
|
cookie: cookieHeader,
|
|
279
|
-
[MODEL_HEADER_NAME]:
|
|
274
|
+
[MODEL_HEADER_NAME]: buildGeminiWebModelHeader(input.model),
|
|
280
275
|
},
|
|
281
276
|
body: params.toString(),
|
|
282
277
|
});
|
|
@@ -324,9 +319,9 @@ export async function runGeminiWebOnce(input) {
|
|
|
324
319
|
}
|
|
325
320
|
export async function runGeminiWebWithFallback(input) {
|
|
326
321
|
const attempt = await runGeminiWebOnce(input);
|
|
327
|
-
if (isGeminiModelUnavailable(attempt.errorCode) && input.model !==
|
|
328
|
-
const fallback = await runGeminiWebOnce({ ...input, model:
|
|
329
|
-
return { ...fallback, effectiveModel:
|
|
322
|
+
if (isGeminiModelUnavailable(attempt.errorCode) && input.model !== FALLBACK_GEMINI_WEB_MODEL) {
|
|
323
|
+
const fallback = await runGeminiWebOnce({ ...input, model: FALLBACK_GEMINI_WEB_MODEL });
|
|
324
|
+
return { ...fallback, effectiveModel: FALLBACK_GEMINI_WEB_MODEL };
|
|
330
325
|
}
|
|
331
326
|
return { ...attempt, effectiveModel: input.model };
|
|
332
327
|
}
|
|
@@ -4,6 +4,7 @@ import { runProviderDomFlow } from "../browser/providerDomFlow.js";
|
|
|
4
4
|
import { delay } from "../browser/utils.js";
|
|
5
5
|
import { runGeminiWebWithFallback, saveFirstGeminiImageFromOutput } from "./client.js";
|
|
6
6
|
import { geminiDeepThinkDomProvider } from "../browser/providers/index.js";
|
|
7
|
+
import { resolveGeminiWebModel } from "./models.js";
|
|
7
8
|
import { openGeminiBrowserSession } from "./browserSessionManager.js";
|
|
8
9
|
import { selectGeminiExecutionMode } from "./executionMode.js";
|
|
9
10
|
const GEMINI_COOKIE_NAMES = [
|
|
@@ -38,30 +39,6 @@ function resolveInvocationPath(value) {
|
|
|
38
39
|
return undefined;
|
|
39
40
|
return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
|
|
40
41
|
}
|
|
41
|
-
function resolveGeminiWebModel(desiredModel, log) {
|
|
42
|
-
const desired = typeof desiredModel === "string" ? desiredModel.trim() : "";
|
|
43
|
-
if (!desired)
|
|
44
|
-
return "gemini-3-pro";
|
|
45
|
-
const normalized = desired.toLowerCase().replace(/[_\s]+/g, "-");
|
|
46
|
-
switch (normalized) {
|
|
47
|
-
case "gemini-3-pro":
|
|
48
|
-
case "gemini-3.0-pro":
|
|
49
|
-
return "gemini-3-pro";
|
|
50
|
-
case "gemini-3-deep-think":
|
|
51
|
-
case "gemini-3-pro-deep-think":
|
|
52
|
-
case "gemini-3-pro-deepthink":
|
|
53
|
-
return "gemini-3-pro-deep-think";
|
|
54
|
-
case "gemini-2.5-pro":
|
|
55
|
-
return "gemini-2.5-pro";
|
|
56
|
-
case "gemini-2.5-flash":
|
|
57
|
-
return "gemini-2.5-flash";
|
|
58
|
-
default:
|
|
59
|
-
if (normalized.startsWith("gemini-") || normalized.includes("gemini")) {
|
|
60
|
-
log?.(`[gemini-web] Unsupported Gemini web model "${desired}". Falling back to gemini-3-pro.`);
|
|
61
|
-
}
|
|
62
|
-
return "gemini-3-pro";
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
42
|
function resolveCookieDomain(cookie) {
|
|
66
43
|
const rawDomain = cookie.domain?.trim();
|
|
67
44
|
if (rawDomain) {
|