@steipete/oracle 0.19.0 → 0.20.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/dist/bin/oracle-cli.js +11 -13
- package/dist/src/browser/actions/deepResearch.js +168 -44
- package/dist/src/browser/actions/modelSelection.js +78 -1
- package/dist/src/browser/actions/promptComposer.js +3 -0
- package/dist/src/browser/actions/thinkingTime.js +38 -1
- package/dist/src/browser/actions/webSearch.js +96 -0
- package/dist/src/browser/chromeLifecycle.js +5 -4
- package/dist/src/browser/config.js +1 -1
- package/dist/src/browser/index.js +111 -43
- package/dist/src/browser/liveTabs.js +3 -4
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattach.js +21 -1
- package/dist/src/browser/recoveryTarget.js +131 -0
- package/dist/src/browser/sessionRunner.js +3 -1
- package/dist/src/browser/tabLeaseRegistry.js +20 -0
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/cli/browserConfig.js +33 -3
- package/dist/src/cli/browserTabs.js +3 -0
- package/dist/src/cli/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/sessionDisplay.js +4 -5
- package/dist/src/cli/sessionRunner.js +4 -5
- package/dist/src/mcp/tools/consult.js +2 -2
- package/dist/src/mcp/types.js +1 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +3 -0
- package/dist/src/remote/server.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 +3 -3
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { sessionStore } from "../sessionStore.js";
|
|
3
|
+
import { connectToRemoteChromeTarget } from "./chromeLifecycle.js";
|
|
4
|
+
import { resolveBrowserApprovalWait } from "./config.js";
|
|
5
|
+
import { extractConversationIdFromUrl } from "./reattachHelpers.js";
|
|
6
|
+
import { STOP_BUTTON_SELECTORS } from "./constants.js";
|
|
7
|
+
import { hasActiveBrowserTargetLease } from "./tabLeaseRegistry.js";
|
|
8
|
+
import { buildTargetRetirementExpression, buildTargetRetirementRollbackExpression, normalizeChromeHost, } from "./targetClaim.js";
|
|
9
|
+
function processMayBeAlive(pid) {
|
|
10
|
+
try {
|
|
11
|
+
process.kill(pid, 0);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return error.code !== "ESRCH";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function hasOtherLiveBrowserController(metadata) {
|
|
19
|
+
for (const pid of [metadata.browser?.runtime?.controllerPid, metadata.lifecycle?.workerPid]) {
|
|
20
|
+
if (!pid || pid === process.pid)
|
|
21
|
+
continue;
|
|
22
|
+
if (processMayBeAlive(pid))
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
async function targetHasActiveController(metadata, capture) {
|
|
28
|
+
for (const other of await sessionStore.listSessions()) {
|
|
29
|
+
if (other.id === metadata.id)
|
|
30
|
+
continue;
|
|
31
|
+
const runtime = other.browser?.runtime;
|
|
32
|
+
if (runtime?.chromeTargetId !== capture.targetId ||
|
|
33
|
+
runtime.chromePort !== capture.port ||
|
|
34
|
+
normalizeChromeHost(runtime.chromeHost ?? "127.0.0.1") !== normalizeChromeHost(capture.host))
|
|
35
|
+
continue;
|
|
36
|
+
if ([runtime.controllerPid, other.lifecycle?.workerPid].some((pid) => pid && processMayBeAlive(pid)))
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const runtime = metadata.browser?.runtime;
|
|
40
|
+
const profileDirs = new Set([
|
|
41
|
+
runtime?.userDataDir,
|
|
42
|
+
runtime?.chromeProfileRoot,
|
|
43
|
+
metadata.browser?.config?.manualLoginProfileDir,
|
|
44
|
+
].filter((dir) => Boolean(dir)));
|
|
45
|
+
for (const dir of profileDirs) {
|
|
46
|
+
if (await hasActiveBrowserTargetLease(dir, capture))
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
export function matchesOwnedRecoveryTarget(metadata, capture) {
|
|
52
|
+
const runtime = metadata.browser?.runtime;
|
|
53
|
+
const owned = runtime?.ownedRecoveryTarget;
|
|
54
|
+
const conversationId = runtime?.conversationId ?? extractConversationIdFromUrl(runtime?.tabUrl ?? "");
|
|
55
|
+
return Boolean(owned &&
|
|
56
|
+
capture &&
|
|
57
|
+
conversationId &&
|
|
58
|
+
capture.conversationId === conversationId &&
|
|
59
|
+
owned.targetId === capture.targetId &&
|
|
60
|
+
normalizeChromeHost(owned.host) === normalizeChromeHost(capture.host) &&
|
|
61
|
+
owned.port === capture.port &&
|
|
62
|
+
owned.browserWSEndpoint === capture.browserWSEndpoint);
|
|
63
|
+
}
|
|
64
|
+
/** Call only after the full recovered answer and completed session have been saved. */
|
|
65
|
+
export async function retireRecoveredBrowserTarget(sessionId, capture, logger, deps = {}) {
|
|
66
|
+
if (!capture)
|
|
67
|
+
return;
|
|
68
|
+
let connection;
|
|
69
|
+
let lockedClaim;
|
|
70
|
+
const reservationId = randomUUID();
|
|
71
|
+
let retired = false;
|
|
72
|
+
try {
|
|
73
|
+
const metadata = await sessionStore.readSession(sessionId);
|
|
74
|
+
if (!metadata ||
|
|
75
|
+
metadata.status !== "completed" ||
|
|
76
|
+
hasOtherLiveBrowserController(metadata) ||
|
|
77
|
+
!matchesOwnedRecoveryTarget(metadata, capture))
|
|
78
|
+
return;
|
|
79
|
+
const claimId = metadata.browser?.runtime?.ownedRecoveryTarget?.claimId;
|
|
80
|
+
if (!claimId)
|
|
81
|
+
return;
|
|
82
|
+
const connect = deps.connect ?? connectToRemoteChromeTarget;
|
|
83
|
+
connection = await connect(capture.host, capture.port, logger, {
|
|
84
|
+
targetId: capture.targetId,
|
|
85
|
+
browserWSEndpoint: capture.browserWSEndpoint,
|
|
86
|
+
closeTargetOnDispose: false,
|
|
87
|
+
approvalWaitMs: resolveBrowserApprovalWait(metadata.browser?.config?.approvalWaitMs),
|
|
88
|
+
});
|
|
89
|
+
const { Runtime, Target } = connection.client;
|
|
90
|
+
const state = await Runtime.evaluate({
|
|
91
|
+
expression: `(() => ({ url: location.href, generating: Array.from(document.querySelectorAll(${JSON.stringify(STOP_BUTTON_SELECTORS.join(","))})).some(node => node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0) }))()`,
|
|
92
|
+
returnByValue: true,
|
|
93
|
+
});
|
|
94
|
+
const value = state.result?.value;
|
|
95
|
+
if (!value ||
|
|
96
|
+
value.generating !== false ||
|
|
97
|
+
extractConversationIdFromUrl(value.url ?? "") !== capture.conversationId)
|
|
98
|
+
return;
|
|
99
|
+
const { targetInfos } = await Target.getTargets();
|
|
100
|
+
if (!targetInfos.some((target) => target.type === "page" && target.targetId !== capture.targetId)) {
|
|
101
|
+
const replacement = await Target.createTarget({ url: "about:blank" });
|
|
102
|
+
if (!replacement.targetId)
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (await targetHasActiveController(metadata, capture))
|
|
106
|
+
return;
|
|
107
|
+
lockedClaim = claimId;
|
|
108
|
+
const locked = await Runtime.evaluate({
|
|
109
|
+
expression: buildTargetRetirementExpression(claimId, capture.conversationId, reservationId),
|
|
110
|
+
returnByValue: true,
|
|
111
|
+
});
|
|
112
|
+
if (locked.exceptionDetails || locked.result?.value !== true)
|
|
113
|
+
return;
|
|
114
|
+
const result = await Target.closeTarget({ targetId: capture.targetId });
|
|
115
|
+
if (!result.success)
|
|
116
|
+
throw new Error("Chrome refused target retirement");
|
|
117
|
+
retired = true;
|
|
118
|
+
logger("Retired Oracle-owned browser tab after saving the recovered answer.");
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
logger(`Recovered answer is saved; browser tab cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
if (connection && lockedClaim && !retired) {
|
|
125
|
+
await connection.client.Runtime.evaluate({
|
|
126
|
+
expression: buildTargetRetirementRollbackExpression(lockedClaim, reservationId),
|
|
127
|
+
}).catch(() => undefined);
|
|
128
|
+
}
|
|
129
|
+
await connection?.close().catch(() => undefined);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -135,7 +135,7 @@ async function executeAssembledBrowserSession({ runOptions, browserConfig, log,
|
|
|
135
135
|
if (typeof message !== "string")
|
|
136
136
|
return;
|
|
137
137
|
const shouldAlwaysPrint = message.startsWith("[browser] ") &&
|
|
138
|
-
/archive|fallback|follow-up|retry|thinking|waiting for chatgpt|remote debugging approval|browser slot|browser control|browser guidance|model selection|model picker/i.test(message);
|
|
138
|
+
/archive|fallback|follow-up|retry|thinking|research|waiting for chatgpt|remote debugging approval|browser slot|browser control|browser guidance|model selection|model picker/i.test(message);
|
|
139
139
|
if (!runOptions.verbose && !shouldAlwaysPrint)
|
|
140
140
|
return;
|
|
141
141
|
log(message);
|
|
@@ -278,9 +278,11 @@ async function executeAssembledBrowserSession({ runOptions, browserConfig, log,
|
|
|
278
278
|
chromeProfileRoot: browserResult.chromeProfileRoot,
|
|
279
279
|
userDataDir: browserResult.userDataDir,
|
|
280
280
|
chromeTargetId: browserResult.chromeTargetId,
|
|
281
|
+
ownedRecoveryTarget: browserResult.ownedRecoveryTarget,
|
|
281
282
|
tabUrl: browserResult.tabUrl,
|
|
282
283
|
conversationId: browserResult.conversationId,
|
|
283
284
|
promptSubmitted: browserResult.promptSubmitted,
|
|
285
|
+
researchPlan: browserResult.researchPlan,
|
|
284
286
|
controllerPid: browserResult.controllerPid ?? process.pid,
|
|
285
287
|
},
|
|
286
288
|
archive: browserResult.archive,
|
|
@@ -4,6 +4,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4
4
|
import { lstat, mkdir, readFile, realpath, rename, rm, rmdir, stat, writeFile, } from "node:fs/promises";
|
|
5
5
|
import { isProcessAlive, readProcessStartTimeMs } from "./profileState.js";
|
|
6
6
|
import { delay } from "./utils.js";
|
|
7
|
+
import { normalizeChromeHost } from "./targetClaim.js";
|
|
7
8
|
export const DEFAULT_MAX_CONCURRENT_CHATGPT_TABS = 3;
|
|
8
9
|
const REGISTRY_FILENAME = "oracle-tab-leases.json";
|
|
9
10
|
const REGISTRY_LOCK_DIRNAME = "oracle-tab-leases.lock";
|
|
@@ -187,6 +188,25 @@ export async function hasOtherActiveBrowserTabLeases(profileDir, leaseId, option
|
|
|
187
188
|
return active.some((lease) => lease.id !== leaseId);
|
|
188
189
|
});
|
|
189
190
|
}
|
|
191
|
+
export async function hasActiveBrowserTargetLease(profileDir, target) {
|
|
192
|
+
let raw;
|
|
193
|
+
try {
|
|
194
|
+
raw = await readFile(path.join(profileDir, REGISTRY_FILENAME), "utf8");
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.code === "ENOENT")
|
|
198
|
+
return false;
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
const registry = JSON.parse(raw);
|
|
202
|
+
if (registry.version !== 1 || !Array.isArray(registry.leases))
|
|
203
|
+
throw new Error("Unknown browser lease registry");
|
|
204
|
+
return registry.leases.some((lease) => lease.chromeTargetId === target.targetId &&
|
|
205
|
+
(!lease.chromeHost ||
|
|
206
|
+
normalizeChromeHost(lease.chromeHost) === normalizeChromeHost(target.host)) &&
|
|
207
|
+
(!lease.chromePort || lease.chromePort === target.port) &&
|
|
208
|
+
isProcessAlive(lease.pid));
|
|
209
|
+
}
|
|
190
210
|
async function withRegistryLock(profileDir, callback, signal) {
|
|
191
211
|
const lockDir = path.join(profileDir, REGISTRY_LOCK_DIRNAME);
|
|
192
212
|
const lockId = randomUUID();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { STOP_BUTTON_SELECTORS } from "./constants.js";
|
|
3
|
+
export function normalizeChromeHost(host) {
|
|
4
|
+
const value = host
|
|
5
|
+
.trim()
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/^\[|\]$/g, "");
|
|
8
|
+
return value === "localhost" || value === "::1" || (isIP(value) === 4 && value.startsWith("127."))
|
|
9
|
+
? "loopback"
|
|
10
|
+
: value;
|
|
11
|
+
}
|
|
12
|
+
export function buildTargetClaimExpression(claimId) {
|
|
13
|
+
return `(() => {
|
|
14
|
+
if (window !== window.top) return true;
|
|
15
|
+
try {
|
|
16
|
+
const claim = JSON.parse(sessionStorage.getItem('oracle:target-claim') || 'null');
|
|
17
|
+
if (claim?.retiring) return false;
|
|
18
|
+
sessionStorage.setItem('oracle:target-claim', JSON.stringify({ id: ${JSON.stringify(claimId)}, retiring: false }));
|
|
19
|
+
return true;
|
|
20
|
+
} catch { return false; }
|
|
21
|
+
})()`;
|
|
22
|
+
}
|
|
23
|
+
export function buildTargetRetirementExpression(claimId, conversationId, reservationId) {
|
|
24
|
+
return `(() => {
|
|
25
|
+
let claim;
|
|
26
|
+
try { claim = JSON.parse(sessionStorage.getItem('oracle:target-claim') || 'null'); } catch { return false; }
|
|
27
|
+
if (!claim || claim.id !== ${JSON.stringify(claimId)} || claim.retiring) return false;
|
|
28
|
+
const id = location.pathname.match(/\\/c\\/([^/]+)/)?.[1];
|
|
29
|
+
if (id !== ${JSON.stringify(conversationId)}) return false;
|
|
30
|
+
const generating = Array.from(document.querySelectorAll(${JSON.stringify(STOP_BUTTON_SELECTORS.join(","))})).some(node => node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0);
|
|
31
|
+
if (generating) return false;
|
|
32
|
+
claim.retiring = true;
|
|
33
|
+
claim.retirementId = ${JSON.stringify(reservationId)};
|
|
34
|
+
try { sessionStorage.setItem('oracle:target-claim', JSON.stringify(claim)); } catch { return false; }
|
|
35
|
+
return true;
|
|
36
|
+
})()`;
|
|
37
|
+
}
|
|
38
|
+
export function buildTargetRetirementRollbackExpression(claimId, reservationId) {
|
|
39
|
+
return `(() => { try {
|
|
40
|
+
const claim = JSON.parse(sessionStorage.getItem('oracle:target-claim') || 'null');
|
|
41
|
+
if (claim?.id === ${JSON.stringify(claimId)} && claim.retirementId === ${JSON.stringify(reservationId)}) {
|
|
42
|
+
claim.retiring = false;
|
|
43
|
+
delete claim.retirementId;
|
|
44
|
+
sessionStorage.setItem('oracle:target-claim', JSON.stringify(claim));
|
|
45
|
+
}
|
|
46
|
+
} catch {} })()`;
|
|
47
|
+
}
|
|
48
|
+
/** Per-tab storage survives document replacement and serializes acquisition with retirement. */
|
|
49
|
+
export async function claimBrowserTarget(runtime, claimId) {
|
|
50
|
+
const source = buildTargetClaimExpression(claimId);
|
|
51
|
+
const claim = await runtime.evaluate({ expression: source, returnByValue: true });
|
|
52
|
+
if (claim.exceptionDetails || claim.result?.value !== true)
|
|
53
|
+
throw new Error("This Oracle tab is being retired after recovery; choose another browser tab.");
|
|
54
|
+
}
|
|
@@ -24,6 +24,10 @@ const CURRENT_CHATGPT_PRO_ALIASES = new Set([
|
|
|
24
24
|
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
|
|
25
25
|
const BROWSER_MODEL_LABELS = [
|
|
26
26
|
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
27
|
+
// GPT-6 (Astra) has no entry of its own in the ChatGPT picker: it is the "Latest" radio of the
|
|
28
|
+
// advanced view, and "GPT-6 Pro" is that radio with the power slider at Pro (composer pill "6 Pro").
|
|
29
|
+
["gpt-6-pro", "Latest"],
|
|
30
|
+
["gpt-6-astra", "Latest"],
|
|
27
31
|
["gpt-5.6-sol", "GPT-5.6 Sol"],
|
|
28
32
|
["gpt-5.6", "GPT-5.6 Sol"],
|
|
29
33
|
["gpt-5.5-pro", "GPT-5.5"],
|
|
@@ -47,6 +51,13 @@ const BROWSER_MODEL_LABELS = [
|
|
|
47
51
|
];
|
|
48
52
|
export function normalizeChatGptModelForBrowser(model) {
|
|
49
53
|
const normalized = model.toLowerCase();
|
|
54
|
+
// Browser-only alias: gpt-6-pro keeps its name so the Pro tier default survives (label "Latest").
|
|
55
|
+
if (isGpt6ProAlias(normalized)) {
|
|
56
|
+
return "gpt-6-pro";
|
|
57
|
+
}
|
|
58
|
+
if (isGpt6Alias(normalized)) {
|
|
59
|
+
return "gpt-6-astra";
|
|
60
|
+
}
|
|
50
61
|
if (!normalized.startsWith("gpt-") || normalized.includes("codex")) {
|
|
51
62
|
return model;
|
|
52
63
|
}
|
|
@@ -72,6 +83,18 @@ export function normalizeChatGptModelForBrowser(model) {
|
|
|
72
83
|
}
|
|
73
84
|
return model;
|
|
74
85
|
}
|
|
86
|
+
// Documented spellings only: gpt-6, gpt-6-astra, gpt-6-pro (plus their label forms such as
|
|
87
|
+
// "GPT-6 Pro") and "latest" map to ChatGPT's "Latest" model. Any other gpt-6-* id (gpt-6-codex,
|
|
88
|
+
// gpt-6-custom, ...) is not an alias and must pass through unchanged for custom/OpenRouter use.
|
|
89
|
+
const GPT6_ALIAS_PATTERN = /^gpt[-_ ]?6(?:[-_ ](?:astra|pro))?$/;
|
|
90
|
+
const GPT6_PRO_ALIAS_PATTERN = /^gpt[-_ ]?6[-_ ]pro$/;
|
|
91
|
+
export function isGpt6Alias(model) {
|
|
92
|
+
const normalized = model?.trim().toLowerCase() ?? "";
|
|
93
|
+
return normalized === "latest" || GPT6_ALIAS_PATTERN.test(normalized);
|
|
94
|
+
}
|
|
95
|
+
export function isGpt6ProAlias(model) {
|
|
96
|
+
return GPT6_PRO_ALIAS_PATTERN.test(model?.trim().toLowerCase() ?? "");
|
|
97
|
+
}
|
|
75
98
|
export function isCurrentChatGptProAlias(model) {
|
|
76
99
|
return CURRENT_CHATGPT_PRO_ALIASES.has(model?.trim().toLowerCase() ?? "");
|
|
77
100
|
}
|
|
@@ -80,7 +103,10 @@ export function resolveDefaultBrowserThinkingTime({ model, requestedModel, model
|
|
|
80
103
|
if (strategy !== "select")
|
|
81
104
|
return undefined;
|
|
82
105
|
const normalizedModel = normalizeChatGptModelForBrowser(model);
|
|
83
|
-
return isCurrentChatGptProAlias(requestedModel ?? model) ||
|
|
106
|
+
return isCurrentChatGptProAlias(requestedModel ?? model) ||
|
|
107
|
+
isGpt6ProAlias(requestedModel ?? model) ||
|
|
108
|
+
normalizedModel === "gpt-6-pro" ||
|
|
109
|
+
normalizedModel === "gpt-5.5-pro"
|
|
84
110
|
? "pro"
|
|
85
111
|
: undefined;
|
|
86
112
|
}
|
|
@@ -208,7 +234,9 @@ export async function buildBrowserConfig(options) {
|
|
|
208
234
|
remoteChrome,
|
|
209
235
|
browserTabRef: options.browserTab ?? undefined,
|
|
210
236
|
thinkingTime,
|
|
211
|
-
researchMode: options.browserResearch === "deep"
|
|
237
|
+
researchMode: options.browserResearch === "deep" || options.browserResearch === "search"
|
|
238
|
+
? options.browserResearch
|
|
239
|
+
: "off",
|
|
212
240
|
archiveConversations: options.browserArchive,
|
|
213
241
|
};
|
|
214
242
|
}
|
|
@@ -288,7 +316,9 @@ export function resolveBrowserModelLabel(input, model) {
|
|
|
288
316
|
return mapModelToBrowserLabel(model);
|
|
289
317
|
}
|
|
290
318
|
const normalizedInput = trimmed.toLowerCase();
|
|
291
|
-
if (normalizedInput === model.toLowerCase()
|
|
319
|
+
if (normalizedInput === model.toLowerCase() ||
|
|
320
|
+
(isGpt6Alias(normalizedInput) && (model === "gpt-6-astra" || model === "gpt-6-pro")) ||
|
|
321
|
+
(isGpt6ProAlias(normalizedInput) && model === "gpt-6-pro")) {
|
|
292
322
|
return mapModelToBrowserLabel(model);
|
|
293
323
|
}
|
|
294
324
|
return trimmed;
|
|
@@ -6,6 +6,7 @@ import { collectChatGptTabs, DEFAULT_REMOTE_CHROME_HOST, DEFAULT_REMOTE_CHROME_P
|
|
|
6
6
|
import { isRecoveredConversationHarvestReady, recoverConversationTab, } from "../browser/recoverConversation.js";
|
|
7
7
|
import { resolveOutputPath } from "./writeOutputPath.js";
|
|
8
8
|
import { persistBrowserHarvest } from "./harvestIntegrity.js";
|
|
9
|
+
import { completeOwnedBrowserHarvest } from "./recoveredBrowserHarvest.js";
|
|
9
10
|
const LIVE_POLL_MS = 2000;
|
|
10
11
|
const DEFAULT_STALL_THRESHOLD_MS = 60_000;
|
|
11
12
|
function isRecoverableMissingTabError(message) {
|
|
@@ -199,6 +200,7 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
|
|
|
199
200
|
if (!options.quietOutput && output) {
|
|
200
201
|
process.stdout.write(`${output}${output.endsWith("\n") ? "" : "\n"}`);
|
|
201
202
|
}
|
|
203
|
+
await completeOwnedBrowserHarvest(sessionId, harvested, integrity, (line) => console.log(line));
|
|
202
204
|
return harvested;
|
|
203
205
|
}
|
|
204
206
|
finally {
|
|
@@ -296,6 +298,7 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
|
|
|
296
298
|
if (output) {
|
|
297
299
|
process.stdout.write(`${output}${output.endsWith("\n") ? "" : "\n"}`);
|
|
298
300
|
}
|
|
301
|
+
await completeOwnedBrowserHarvest(sessionId, finalHarvest, integrity, (line) => console.log(line));
|
|
299
302
|
return finalHarvest;
|
|
300
303
|
}
|
|
301
304
|
await new Promise((resolve) => setTimeout(resolve, LIVE_POLL_MS));
|
package/dist/src/cli/options.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isGpt6Alias, isGpt6ProAlias } from "./browserConfig.js";
|
|
1
2
|
import { InvalidArgumentError } from "commander";
|
|
2
3
|
import { parseDuration } from "../duration.js";
|
|
3
4
|
import path from "node:path";
|
|
@@ -187,6 +188,12 @@ export function resolveApiModel(modelValue) {
|
|
|
187
188
|
if (normalized.includes("/")) {
|
|
188
189
|
return normalized;
|
|
189
190
|
}
|
|
191
|
+
if (isGpt6ProAlias(normalized)) {
|
|
192
|
+
throw new InvalidArgumentError("GPT-6 Pro is an API reasoning mode, not a model slug. Use --model gpt-6-astra --reasoning-mode pro (or --engine browser --model gpt-6-pro).");
|
|
193
|
+
}
|
|
194
|
+
if (isGpt6Alias(normalized)) {
|
|
195
|
+
return "gpt-6-astra";
|
|
196
|
+
}
|
|
190
197
|
const gpt56Label = parseBrowserGpt56Label(normalized);
|
|
191
198
|
if (gpt56Label?.variant.split(" ").includes("pro")) {
|
|
192
199
|
throw new InvalidArgumentError("GPT-5.6 Pro is an API reasoning mode, not a model slug. Use --model gpt-5.6-sol --reasoning-mode pro.");
|
|
@@ -284,6 +291,14 @@ export function inferModelFromLabel(modelValue) {
|
|
|
284
291
|
if (normalized.includes("/")) {
|
|
285
292
|
return normalized;
|
|
286
293
|
}
|
|
294
|
+
// gpt-6 / gpt-6-pro / latest: ChatGPT's "Latest" model (GPT-6 Astra). The browser-only -pro alias
|
|
295
|
+
// is passed through so its Pro tier default survives (resolveDefaultBrowserThinkingTime).
|
|
296
|
+
if (isGpt6ProAlias(normalized)) {
|
|
297
|
+
return "gpt-6-pro";
|
|
298
|
+
}
|
|
299
|
+
if (isGpt6Alias(normalized)) {
|
|
300
|
+
return "gpt-6-astra";
|
|
301
|
+
}
|
|
287
302
|
if (normalized.includes("grok")) {
|
|
288
303
|
return "grok-4.1";
|
|
289
304
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { sessionStore } from "../sessionStore.js";
|
|
3
|
+
import { isRecoveredConversationHarvestReady } from "../browser/recoverConversation.js";
|
|
4
|
+
import { hasOtherLiveBrowserController, matchesOwnedRecoveryTarget, retireRecoveredBrowserTarget, } from "../browser/recoveryTarget.js";
|
|
5
|
+
import { extractConversationIdFromUrl } from "../browser/reattachHelpers.js";
|
|
6
|
+
import { estimateTokenCount } from "../browser/utils.js";
|
|
7
|
+
export async function completeOwnedBrowserHarvest(sessionId, harvested, integrity, logger) {
|
|
8
|
+
if (harvested.state !== "completed" ||
|
|
9
|
+
harvested.stopExists ||
|
|
10
|
+
integrity.status !== "matched" ||
|
|
11
|
+
integrity.explicitTarget)
|
|
12
|
+
return;
|
|
13
|
+
const metadata = await sessionStore.readSession(sessionId);
|
|
14
|
+
if (!metadata?.browser?.runtime?.ownedRecoveryTarget ||
|
|
15
|
+
!isRecoveredConversationHarvestReady(harvested))
|
|
16
|
+
return;
|
|
17
|
+
const runtime = metadata.browser?.runtime;
|
|
18
|
+
const capture = {
|
|
19
|
+
host: harvested.host ?? "",
|
|
20
|
+
port: harvested.port ?? 0,
|
|
21
|
+
targetId: harvested.targetId,
|
|
22
|
+
browserWSEndpoint: runtime?.chromeBrowserWSEndpoint,
|
|
23
|
+
conversationId: harvested.conversationId ?? extractConversationIdFromUrl(harvested.url),
|
|
24
|
+
};
|
|
25
|
+
if (hasOtherLiveBrowserController(metadata) || !matchesOwnedRecoveryTarget(metadata, capture))
|
|
26
|
+
return;
|
|
27
|
+
// Harvest previously persisted only a snippet/hash. Save the full answer before retirement.
|
|
28
|
+
const answer = harvested.lastAssistantMarkdown || harvested.lastAssistantText;
|
|
29
|
+
const paths = await sessionStore.getPaths(sessionId);
|
|
30
|
+
await fs.appendFile(paths.log, `[reattach] harvested assistant response from existing Chrome tab\nAnswer:\n${answer}\n`, "utf8");
|
|
31
|
+
const outputTokens = estimateTokenCount(answer);
|
|
32
|
+
const usage = { inputTokens: 0, outputTokens, reasoningTokens: 0, totalTokens: outputTokens };
|
|
33
|
+
if (metadata.model) {
|
|
34
|
+
await sessionStore.updateModelRun(sessionId, metadata.model, {
|
|
35
|
+
status: "completed",
|
|
36
|
+
completedAt: new Date().toISOString(),
|
|
37
|
+
usage,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
await sessionStore.updateSession(sessionId, {
|
|
41
|
+
status: "completed",
|
|
42
|
+
completedAt: new Date().toISOString(),
|
|
43
|
+
usage,
|
|
44
|
+
errorMessage: undefined,
|
|
45
|
+
error: undefined,
|
|
46
|
+
transport: undefined,
|
|
47
|
+
response: { status: "completed" },
|
|
48
|
+
});
|
|
49
|
+
await retireRecoveredBrowserTarget(sessionId, capture, logger);
|
|
50
|
+
}
|
|
@@ -4,7 +4,7 @@ import { normalizeModelOption, inferModelFromLabel, resolveApiModel, normalizeBa
|
|
|
4
4
|
import { resolveGeminiModelId } from "../oracle/gemini.js";
|
|
5
5
|
import { resolveOverriddenApiModel } from "../oracle/modelResolver.js";
|
|
6
6
|
import { PromptValidationError } from "../oracle/errors.js";
|
|
7
|
-
import { normalizeChatGptModelForBrowser } from "./browserConfig.js";
|
|
7
|
+
import { normalizeChatGptModelForBrowser, isGpt6ProAlias } from "./browserConfig.js";
|
|
8
8
|
import { resolveConfiguredMaxFileSizeBytes } from "./fileSize.js";
|
|
9
9
|
import { isAzureOpenAICandidateModel } from "../oracle/providerRouting.js";
|
|
10
10
|
export function resolveRunOptionsFromConfig({ prompt, files = [], model, models, engine, userConfig, env = process.env, }) {
|
|
@@ -24,7 +24,10 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
24
24
|
.map((entry) => normalizeModelOption(entry))
|
|
25
25
|
.filter(Boolean);
|
|
26
26
|
const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || DEFAULT_MODEL;
|
|
27
|
-
const
|
|
27
|
+
const isGpt6Pro = isGpt6ProAlias(cliModelArg);
|
|
28
|
+
const apiModel = isGpt6Pro && (resolvedEngine === "browser" || browserEngineRequested)
|
|
29
|
+
? "gpt-6-pro"
|
|
30
|
+
: resolveApiModel(cliModelArg);
|
|
28
31
|
// Browser label inference is intentionally engine-scoped: API model ids such as
|
|
29
32
|
// gpt-5.6-luna must remain provider values even though browser mode rejects
|
|
30
33
|
// unrecognized GPT-5.6 picker variants.
|
|
@@ -36,9 +39,11 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
36
39
|
const isGrok = apiModel.startsWith("grok");
|
|
37
40
|
const engineWasBrowser = resolvedEngine === "browser";
|
|
38
41
|
const allModels = normalizedRequestedModels.length > 0
|
|
39
|
-
? Array.from(new Set(normalizedRequestedModels.map((entry) =>
|
|
42
|
+
? Array.from(new Set(normalizedRequestedModels.map((entry) => isGpt6ProAlias(entry) && (resolvedEngine === "browser" || browserEngineRequested)
|
|
43
|
+
? "gpt-6-pro"
|
|
44
|
+
: resolveApiModel(entry))))
|
|
40
45
|
: [apiModel];
|
|
41
|
-
const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel];
|
|
46
|
+
const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel ?? apiModel];
|
|
42
47
|
const isBrowserCompatible = (m) => m.startsWith("gpt-") || m.startsWith("gemini");
|
|
43
48
|
const hasNonBrowserCompatibleTarget = browserEngineRequested && browserCompatibilityModels.some((m) => !isBrowserCompatible(m));
|
|
44
49
|
if (hasNonBrowserCompatibleTarget) {
|
|
@@ -52,6 +57,16 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
52
57
|
const fixedEngine = isCodex || isClaude || isGrok || azureAutoApi || normalizedRequestedModels.length > 0
|
|
53
58
|
? "api"
|
|
54
59
|
: resolvedEngine;
|
|
60
|
+
if (fixedEngine === "api") {
|
|
61
|
+
if (isGpt6ProAlias(cliModelArg)) {
|
|
62
|
+
resolveApiModel(cliModelArg);
|
|
63
|
+
}
|
|
64
|
+
for (const entry of normalizedRequestedModels) {
|
|
65
|
+
if (isGpt6ProAlias(entry)) {
|
|
66
|
+
resolveApiModel(entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
55
70
|
// Browser runs use ChatGPT picker labels/aliases; API runs must keep API model ids intact.
|
|
56
71
|
const resolvedModel = fixedEngine === "browser" ? browserModel : apiModel;
|
|
57
72
|
const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
|
|
@@ -6,6 +6,7 @@ import { formatFinishLine } from "../oracle/finishLine.js";
|
|
|
6
6
|
import { sessionStore, wait } from "../sessionStore.js";
|
|
7
7
|
import { formatTokenCount, formatTokenValue } from "../oracle/runUtils.js";
|
|
8
8
|
import { resumeBrowserSession } from "../browser/reattach.js";
|
|
9
|
+
import { retireRecoveredBrowserTarget } from "../browser/recoveryTarget.js";
|
|
9
10
|
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
|
|
10
11
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "../browser/artifacts.js";
|
|
11
12
|
import { estimateTokenCount } from "../browser/utils.js";
|
|
@@ -88,11 +89,8 @@ async function writeReattachAnswer(sessionId, result, replaceExistingLog) {
|
|
|
88
89
|
await fs.writeFile(paths.log, `[reattach] replaced incomplete Deep Research capture from existing Chrome tab\nAnswer:\n${body}\n`, "utf8");
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
logWriter.logLine("Answer:");
|
|
94
|
-
logWriter.logLine(body);
|
|
95
|
-
logWriter.stream.end();
|
|
92
|
+
const paths = await sessionStore.getPaths(sessionId);
|
|
93
|
+
await fs.appendFile(paths.log, `[reattach] captured assistant response from existing Chrome tab\nAnswer:\n${body}\n`, "utf8");
|
|
96
94
|
}
|
|
97
95
|
async function saveReattachBrowserArtifacts(sessionId, metadata, result) {
|
|
98
96
|
const body = result.answerMarkdown || result.answerText;
|
|
@@ -264,6 +262,7 @@ export async function attachSession(sessionId, options) {
|
|
|
264
262
|
transport: undefined,
|
|
265
263
|
});
|
|
266
264
|
console.log(chalk.green("Reattach succeeded; session marked completed."));
|
|
265
|
+
await retireRecoveredBrowserTarget(sessionId, result.captureTarget, (line) => console.log(dim(line)));
|
|
267
266
|
metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
|
|
268
267
|
}
|
|
269
268
|
catch (error) {
|
|
@@ -22,6 +22,7 @@ import { sanitizeOscProgress } from "./oscUtils.js";
|
|
|
22
22
|
import { readFiles } from "../oracle/files.js";
|
|
23
23
|
import { cwd as getCwd } from "node:process";
|
|
24
24
|
import { resumeBrowserSession } from "../browser/reattach.js";
|
|
25
|
+
import { retireRecoveredBrowserTarget } from "../browser/recoveryTarget.js";
|
|
25
26
|
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
|
|
26
27
|
import { estimateTokenCount } from "../browser/utils.js";
|
|
27
28
|
import { computeFileSha256, sanitizeArtifactFilename } from "../browser/artifacts.js";
|
|
@@ -1039,11 +1040,8 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1039
1040
|
existingArtifacts: sessionMeta.artifacts,
|
|
1040
1041
|
logger,
|
|
1041
1042
|
});
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
logWriter.logLine("Answer:");
|
|
1045
|
-
logWriter.logLine(answerText);
|
|
1046
|
-
logWriter.stream.end();
|
|
1043
|
+
const paths = await sessionStore.getPaths(sessionMeta.id);
|
|
1044
|
+
await fs.appendFile(paths.log, `[auto-reattach] captured assistant response on attempt ${attempt}\nAnswer:\n${answerText}\n`, "utf8");
|
|
1047
1045
|
if (modelForStatus) {
|
|
1048
1046
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
1049
1047
|
status: "completed",
|
|
@@ -1089,6 +1087,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1089
1087
|
transport: undefined,
|
|
1090
1088
|
});
|
|
1091
1089
|
log(kleur.green("Auto-reattach succeeded; session marked completed."));
|
|
1090
|
+
await retireRecoveredBrowserTarget(sessionMeta.id, result.captureTarget, logger);
|
|
1092
1091
|
return true;
|
|
1093
1092
|
}
|
|
1094
1093
|
catch (error) {
|
|
@@ -74,9 +74,9 @@ const consultInputShape = {
|
|
|
74
74
|
.optional()
|
|
75
75
|
.describe("Browser-only: model picker strategy. Mirrors the CLI --browser-model-strategy flag."),
|
|
76
76
|
browserResearchMode: z
|
|
77
|
-
.enum(["deep"])
|
|
77
|
+
.enum(["search", "deep"])
|
|
78
78
|
.optional()
|
|
79
|
-
.describe("Browser-only:
|
|
79
|
+
.describe("Browser-only: explicitly select ChatGPT Web Search or Deep Research."),
|
|
80
80
|
browserArchive: z
|
|
81
81
|
.enum(["auto", "always", "never"])
|
|
82
82
|
.optional()
|
package/dist/src/mcp/types.js
CHANGED
|
@@ -23,7 +23,7 @@ export const consultInputSchema = z
|
|
|
23
23
|
browserBundleFormat: z.enum(["auto", "text", "zip"]).optional(),
|
|
24
24
|
browserThinkingTime: browserThinkingTimeInputSchema.optional(),
|
|
25
25
|
browserModelStrategy: z.enum(["select", "current", "ignore"]).optional(),
|
|
26
|
-
browserResearchMode: z.enum(["deep"]).optional(),
|
|
26
|
+
browserResearchMode: z.enum(["search", "deep"]).optional(),
|
|
27
27
|
browserArchive: z.enum(["auto", "always", "never"]).optional(),
|
|
28
28
|
browserFollowUps: z.array(z.string()).optional(),
|
|
29
29
|
browserKeepBrowser: z.boolean().optional(),
|
|
@@ -30,6 +30,20 @@ const countTokensAnthropic = (input) => {
|
|
|
30
30
|
// supported limit at the base-rate boundary until cost estimation supports tiers.
|
|
31
31
|
const GPT_5_6_BASE_RATE_INPUT_LIMIT = 272_000;
|
|
32
32
|
export const MODEL_CONFIGS = {
|
|
33
|
+
"gpt-6-astra": {
|
|
34
|
+
model: "gpt-6-astra",
|
|
35
|
+
provider: "openai",
|
|
36
|
+
tokenizer: countTokensGpt5,
|
|
37
|
+
// Conservative base-rate budget, not Astra's maximum context window.
|
|
38
|
+
// https://developers.openai.com/api/docs/models/gpt-6-astra
|
|
39
|
+
inputLimit: 272_000,
|
|
40
|
+
pricing: {
|
|
41
|
+
inputPerToken: 10 / 1_000_000,
|
|
42
|
+
outputPerToken: 50 / 1_000_000,
|
|
43
|
+
},
|
|
44
|
+
reasoning: { effort: "xhigh" },
|
|
45
|
+
searchToolType: "web_search",
|
|
46
|
+
},
|
|
33
47
|
"gpt-5.6": {
|
|
34
48
|
model: "gpt-5.6",
|
|
35
49
|
provider: "openai",
|