@steipete/oracle 0.7.5 → 0.8.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 +4 -2
- package/dist/bin/oracle-cli.js +12 -2
- package/dist/src/browser/actions/assistantResponse.js +437 -84
- package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
- package/dist/src/browser/actions/attachments.js +1358 -152
- package/dist/src/browser/actions/modelSelection.js +9 -4
- package/dist/src/browser/actions/navigation.js +160 -5
- package/dist/src/browser/actions/promptComposer.js +54 -11
- package/dist/src/browser/actions/remoteFileTransfer.js +5 -156
- package/dist/src/browser/chromeLifecycle.js +7 -1
- package/dist/src/browser/config.js +9 -3
- package/dist/src/browser/constants.js +4 -1
- package/dist/src/browser/cookies.js +55 -21
- package/dist/src/browser/index.js +321 -65
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/reattach.js +42 -97
- package/dist/src/browser/reattachHelpers.js +382 -0
- package/dist/src/browser/sessionRunner.js +16 -6
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/browserConfig.js +10 -4
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/options.js +27 -0
- package/dist/src/cli/sessionDisplay.js +25 -6
- package/dist/src/cli/sessionRunner.js +14 -4
- package/dist/src/gemini-web/executor.js +107 -46
- package/dist/src/oracle/finishLine.js +32 -0
- package/dist/src/oracle/run.js +58 -64
- package/dist/src/remote/server.js +30 -15
- package/dist/src/sessionManager.js +16 -0
- package/package.json +8 -17
package/dist/src/cli/options.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { InvalidArgumentError } from 'commander';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
2
4
|
import { DEFAULT_MODEL, MODEL_CONFIGS } from '../oracle.js';
|
|
3
5
|
export function collectPaths(value, previous = []) {
|
|
4
6
|
if (!value) {
|
|
@@ -17,6 +19,31 @@ export function mergePathLikeOptions(file, include, filesAlias, pathAlias, paths
|
|
|
17
19
|
const withPathAlias = collectPaths(pathAlias, withFilesAlias);
|
|
18
20
|
return collectPaths(pathsAlias, withPathAlias);
|
|
19
21
|
}
|
|
22
|
+
export function dedupePathInputs(inputs, { cwd = process.cwd() } = {}) {
|
|
23
|
+
const deduped = [];
|
|
24
|
+
const duplicates = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const entry of inputs ?? []) {
|
|
27
|
+
const raw = entry?.trim();
|
|
28
|
+
if (!raw)
|
|
29
|
+
continue;
|
|
30
|
+
let key = raw;
|
|
31
|
+
if (!raw.startsWith('!') && !fg.isDynamicPattern(raw)) {
|
|
32
|
+
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
33
|
+
key = `path:${path.normalize(absolute)}`;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
key = `pattern:${raw}`;
|
|
37
|
+
}
|
|
38
|
+
if (seen.has(key)) {
|
|
39
|
+
duplicates.push(raw);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
seen.add(key);
|
|
43
|
+
deduped.push(raw);
|
|
44
|
+
}
|
|
45
|
+
return { deduped, duplicates };
|
|
46
|
+
}
|
|
20
47
|
export function collectModelList(value, previous = []) {
|
|
21
48
|
if (!value) {
|
|
22
49
|
return previous;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import kleur from 'kleur';
|
|
3
3
|
import { renderMarkdownAnsi } from './markdownRenderer.js';
|
|
4
|
-
import {
|
|
4
|
+
import { formatFinishLine } from '../oracle/finishLine.js';
|
|
5
5
|
import { sessionStore, wait } from '../sessionStore.js';
|
|
6
6
|
import { formatTokenCount, formatTokenValue } from '../oracle/runUtils.js';
|
|
7
7
|
import { resumeBrowserSession } from '../browser/reattach.js';
|
|
@@ -61,6 +61,13 @@ export async function attachSession(sessionId, options) {
|
|
|
61
61
|
process.exitCode = 1;
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
+
if (metadata.mode === 'browser' && metadata.status === 'running' && !metadata.browser?.runtime) {
|
|
65
|
+
await wait(250);
|
|
66
|
+
const refreshed = await sessionStore.readSession(sessionId);
|
|
67
|
+
if (refreshed) {
|
|
68
|
+
metadata = refreshed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
64
71
|
const normalizedModelFilter = options?.model?.trim().toLowerCase();
|
|
65
72
|
if (normalizedModelFilter) {
|
|
66
73
|
const availableModels = metadata.models?.map((model) => model.model.toLowerCase()) ??
|
|
@@ -92,7 +99,7 @@ export async function attachSession(sessionId, options) {
|
|
|
92
99
|
if (message) {
|
|
93
100
|
console.log(dim(message));
|
|
94
101
|
}
|
|
95
|
-
}), { verbose: true }));
|
|
102
|
+
}), { verbose: true }), { promptPreview: metadata.promptPreview });
|
|
96
103
|
const outputTokens = estimateTokenCount(result.answerMarkdown);
|
|
97
104
|
const logWriter = sessionStore.createLogWriter(sessionId);
|
|
98
105
|
logWriter.logLine('[reattach] captured assistant response from existing Chrome tab');
|
|
@@ -516,7 +523,6 @@ export function formatCompletionSummary(metadata, options = {}) {
|
|
|
516
523
|
const modeLabel = metadata.mode === 'browser' ? `${metadata.model ?? 'n/a'}[browser]` : metadata.model ?? 'n/a';
|
|
517
524
|
const usage = metadata.usage;
|
|
518
525
|
const cost = resolveSessionCost(metadata);
|
|
519
|
-
const costPart = cost != null ? ` | ${formatUSD(cost)}` : '';
|
|
520
526
|
const tokensDisplay = [
|
|
521
527
|
usage.inputTokens ?? 0,
|
|
522
528
|
usage.outputTokens ?? 0,
|
|
@@ -530,10 +536,23 @@ export function formatCompletionSummary(metadata, options = {}) {
|
|
|
530
536
|
total_tokens: usage.totalTokens,
|
|
531
537
|
}, index))
|
|
532
538
|
.join('/');
|
|
539
|
+
const tokensPart = (() => {
|
|
540
|
+
const parts = tokensDisplay.split('/');
|
|
541
|
+
if (parts.length !== 4)
|
|
542
|
+
return tokensDisplay;
|
|
543
|
+
return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
|
|
544
|
+
})();
|
|
533
545
|
const filesCount = metadata.options?.file?.length ?? 0;
|
|
534
|
-
const filesPart = filesCount > 0 ? `
|
|
535
|
-
const slugPart = options.includeSlug ? `
|
|
536
|
-
|
|
546
|
+
const filesPart = filesCount > 0 ? `files=${filesCount}` : null;
|
|
547
|
+
const slugPart = options.includeSlug ? `slug=${metadata.id}` : null;
|
|
548
|
+
const { line1, line2 } = formatFinishLine({
|
|
549
|
+
elapsedMs: metadata.elapsedMs,
|
|
550
|
+
model: modeLabel,
|
|
551
|
+
costUsd: cost ?? null,
|
|
552
|
+
tokensPart,
|
|
553
|
+
detailParts: [filesPart, slugPart],
|
|
554
|
+
});
|
|
555
|
+
return line2 ? `${line1} | ${line2}` : line1;
|
|
537
556
|
}
|
|
538
557
|
async function readStoredPrompt(sessionId) {
|
|
539
558
|
const request = await sessionStore.readRequest(sessionId);
|
|
@@ -15,10 +15,9 @@ import { resolveModelConfig } from '../oracle/modelResolver.js';
|
|
|
15
15
|
import { buildPrompt, buildRequestBody } from '../oracle/request.js';
|
|
16
16
|
import { estimateRequestTokens } from '../oracle/tokenEstimate.js';
|
|
17
17
|
import { formatTokenEstimate, formatTokenValue } from '../oracle/runUtils.js';
|
|
18
|
-
import {
|
|
18
|
+
import { formatFinishLine } from '../oracle/finishLine.js';
|
|
19
19
|
import { sanitizeOscProgress } from './oscUtils.js';
|
|
20
20
|
import { readFiles } from '../oracle/files.js';
|
|
21
|
-
import { formatUSD } from '../oracle/format.js';
|
|
22
21
|
import { cwd as getCwd } from 'node:process';
|
|
23
22
|
const isTty = process.stdout.isTTY;
|
|
24
23
|
const dim = (text) => (isTty ? kleur.dim(text) : text);
|
|
@@ -211,10 +210,21 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
211
210
|
total_tokens: aggregateUsage.totalTokens,
|
|
212
211
|
}, idx))
|
|
213
212
|
.join('/');
|
|
214
|
-
const
|
|
213
|
+
const tokensPart = (() => {
|
|
214
|
+
const parts = tokensDisplay.split('/');
|
|
215
|
+
if (parts.length !== 4)
|
|
216
|
+
return tokensDisplay;
|
|
217
|
+
return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
|
|
218
|
+
})();
|
|
215
219
|
const statusColor = summary.rejected.length === 0 ? kleur.green : summary.fulfilled.length > 0 ? kleur.yellow : kleur.red;
|
|
216
220
|
const overallText = `${summary.fulfilled.length}/${multiModels.length} models`;
|
|
217
|
-
|
|
221
|
+
const { line1 } = formatFinishLine({
|
|
222
|
+
elapsedMs: summary.elapsedMs,
|
|
223
|
+
model: overallText,
|
|
224
|
+
costUsd: aggregateUsage.cost ?? null,
|
|
225
|
+
tokensPart,
|
|
226
|
+
});
|
|
227
|
+
log(statusColor(line1));
|
|
218
228
|
const hasFailure = summary.rejected.length > 0;
|
|
219
229
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
220
230
|
status: hasFailure ? 'error' : 'completed',
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { getCookies } from '@steipete/sweet-cookie';
|
|
2
3
|
import { runGeminiWebWithFallback, saveFirstGeminiImageFromOutput } from './client.js';
|
|
4
|
+
const GEMINI_COOKIE_NAMES = [
|
|
5
|
+
'__Secure-1PSID',
|
|
6
|
+
'__Secure-1PSIDTS',
|
|
7
|
+
'__Secure-1PSIDCC',
|
|
8
|
+
'__Secure-1PAPISID',
|
|
9
|
+
'NID',
|
|
10
|
+
'AEC',
|
|
11
|
+
'SOCS',
|
|
12
|
+
'__Secure-BUCKET',
|
|
13
|
+
'__Secure-ENID',
|
|
14
|
+
'SID',
|
|
15
|
+
'HSID',
|
|
16
|
+
'SSID',
|
|
17
|
+
'APISID',
|
|
18
|
+
'SAPISID',
|
|
19
|
+
'__Secure-3PSID',
|
|
20
|
+
'__Secure-3PSIDTS',
|
|
21
|
+
'__Secure-3PAPISID',
|
|
22
|
+
'SIDCC',
|
|
23
|
+
];
|
|
24
|
+
const GEMINI_REQUIRED_COOKIES = ['__Secure-1PSID', '__Secure-1PSIDTS'];
|
|
3
25
|
function estimateTokenCount(text) {
|
|
4
26
|
return Math.ceil(text.length / 4);
|
|
5
27
|
}
|
|
@@ -30,59 +52,84 @@ function resolveGeminiWebModel(desiredModel, log) {
|
|
|
30
52
|
return 'gemini-3-pro';
|
|
31
53
|
}
|
|
32
54
|
}
|
|
55
|
+
function resolveCookieDomain(cookie) {
|
|
56
|
+
const rawDomain = cookie.domain?.trim();
|
|
57
|
+
if (rawDomain) {
|
|
58
|
+
return rawDomain.startsWith('.') ? rawDomain.slice(1) : rawDomain;
|
|
59
|
+
}
|
|
60
|
+
const rawUrl = cookie.url?.trim();
|
|
61
|
+
if (rawUrl) {
|
|
62
|
+
try {
|
|
63
|
+
return new URL(rawUrl).hostname;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function pickCookieValue(cookies, name) {
|
|
72
|
+
const matches = cookies.filter((cookie) => cookie.name === name && typeof cookie.value === 'string');
|
|
73
|
+
if (matches.length === 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
const preferredDomain = matches.find((cookie) => {
|
|
76
|
+
const domain = resolveCookieDomain(cookie);
|
|
77
|
+
return domain === 'google.com' && (cookie.path ?? '/') === '/';
|
|
78
|
+
});
|
|
79
|
+
const googleDomain = matches.find((cookie) => (resolveCookieDomain(cookie) ?? '').endsWith('google.com'));
|
|
80
|
+
return (preferredDomain ?? googleDomain ?? matches[0])?.value;
|
|
81
|
+
}
|
|
82
|
+
function buildGeminiCookieMap(cookies) {
|
|
83
|
+
const cookieMap = {};
|
|
84
|
+
for (const name of GEMINI_COOKIE_NAMES) {
|
|
85
|
+
const value = pickCookieValue(cookies, name);
|
|
86
|
+
if (value)
|
|
87
|
+
cookieMap[name] = value;
|
|
88
|
+
}
|
|
89
|
+
return cookieMap;
|
|
90
|
+
}
|
|
91
|
+
function hasRequiredGeminiCookies(cookieMap) {
|
|
92
|
+
return GEMINI_REQUIRED_COOKIES.every((name) => Boolean(cookieMap[name]));
|
|
93
|
+
}
|
|
94
|
+
async function loadGeminiCookiesFromInline(browserConfig, log) {
|
|
95
|
+
const inline = browserConfig?.inlineCookies;
|
|
96
|
+
if (!inline || inline.length === 0)
|
|
97
|
+
return {};
|
|
98
|
+
const cookieMap = buildGeminiCookieMap(inline.filter((cookie) => Boolean(cookie?.name && typeof cookie.value === 'string')));
|
|
99
|
+
if (Object.keys(cookieMap).length > 0) {
|
|
100
|
+
const source = browserConfig?.inlineCookiesSource ?? 'inline';
|
|
101
|
+
log?.(`[gemini-web] Loaded Gemini cookies from inline payload (${source}): ${Object.keys(cookieMap).length} cookie(s).`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
log?.('[gemini-web] Inline cookie payload provided but no Gemini cookies matched.');
|
|
105
|
+
}
|
|
106
|
+
return cookieMap;
|
|
107
|
+
}
|
|
33
108
|
async function loadGeminiCookiesFromChrome(browserConfig, log) {
|
|
34
109
|
try {
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
browserConfig.chromeProfile.trim().length > 0
|
|
40
|
-
? browserConfig.chromeProfile.trim()
|
|
110
|
+
// Learned: Gemini web relies on Google auth cookies in the *browser* profile, not API keys.
|
|
111
|
+
const profileCandidate = browserConfig?.chromeCookiePath ?? browserConfig?.chromeProfile ?? undefined;
|
|
112
|
+
const profile = typeof profileCandidate === 'string' && profileCandidate.trim().length > 0
|
|
113
|
+
? profileCandidate.trim()
|
|
41
114
|
: undefined;
|
|
42
115
|
const sources = [
|
|
43
116
|
'https://gemini.google.com',
|
|
44
117
|
'https://accounts.google.com',
|
|
45
118
|
'https://www.google.com',
|
|
46
119
|
];
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
'HSID',
|
|
59
|
-
'SSID',
|
|
60
|
-
'APISID',
|
|
61
|
-
'SAPISID',
|
|
62
|
-
'__Secure-3PSID',
|
|
63
|
-
'__Secure-3PSIDTS',
|
|
64
|
-
'__Secure-3PAPISID',
|
|
65
|
-
'SIDCC',
|
|
66
|
-
];
|
|
67
|
-
const cookieMap = {};
|
|
68
|
-
for (const url of sources) {
|
|
69
|
-
const cookies = (await chromeCookies.getCookiesPromised(url, 'puppeteer', profile));
|
|
70
|
-
for (const name of wantNames) {
|
|
71
|
-
if (cookieMap[name])
|
|
72
|
-
continue;
|
|
73
|
-
const matches = cookies.filter((cookie) => cookie.name === name);
|
|
74
|
-
if (matches.length === 0)
|
|
75
|
-
continue;
|
|
76
|
-
const preferredDomain = matches.find((cookie) => cookie.domain === '.google.com' && (cookie.path ?? '/') === '/');
|
|
77
|
-
const googleDomain = matches.find((cookie) => (cookie.domain ?? '').endsWith('google.com'));
|
|
78
|
-
const value = (preferredDomain ?? googleDomain ?? matches[0])?.value;
|
|
79
|
-
if (value)
|
|
80
|
-
cookieMap[name] = value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (!cookieMap['__Secure-1PSID'] || !cookieMap['__Secure-1PSIDTS']) {
|
|
84
|
-
return {};
|
|
120
|
+
const { cookies, warnings } = await getCookies({
|
|
121
|
+
url: sources[0],
|
|
122
|
+
origins: sources,
|
|
123
|
+
names: [...GEMINI_COOKIE_NAMES],
|
|
124
|
+
browsers: ['chrome'],
|
|
125
|
+
mode: 'merge',
|
|
126
|
+
chromeProfile: profile,
|
|
127
|
+
timeoutMs: 5_000,
|
|
128
|
+
});
|
|
129
|
+
if (warnings.length && log?.verbose) {
|
|
130
|
+
log(`[gemini-web] Cookie warnings:\n- ${warnings.join('\n- ')}`);
|
|
85
131
|
}
|
|
132
|
+
const cookieMap = buildGeminiCookieMap(cookies);
|
|
86
133
|
log?.(`[gemini-web] Loaded Gemini cookies from Chrome (node): ${Object.keys(cookieMap).length} cookie(s).`);
|
|
87
134
|
return cookieMap;
|
|
88
135
|
}
|
|
@@ -91,13 +138,27 @@ async function loadGeminiCookiesFromChrome(browserConfig, log) {
|
|
|
91
138
|
return {};
|
|
92
139
|
}
|
|
93
140
|
}
|
|
141
|
+
async function loadGeminiCookies(browserConfig, log) {
|
|
142
|
+
const inlineMap = await loadGeminiCookiesFromInline(browserConfig, log);
|
|
143
|
+
const hasInlineRequired = hasRequiredGeminiCookies(inlineMap);
|
|
144
|
+
if (hasInlineRequired && browserConfig?.cookieSync === false) {
|
|
145
|
+
return inlineMap;
|
|
146
|
+
}
|
|
147
|
+
if (browserConfig?.cookieSync === false && !hasInlineRequired) {
|
|
148
|
+
log?.('[gemini-web] Cookie sync disabled and inline cookies missing Gemini auth tokens.');
|
|
149
|
+
return inlineMap;
|
|
150
|
+
}
|
|
151
|
+
const chromeMap = await loadGeminiCookiesFromChrome(browserConfig, log);
|
|
152
|
+
const merged = { ...chromeMap, ...inlineMap };
|
|
153
|
+
return merged;
|
|
154
|
+
}
|
|
94
155
|
export function createGeminiWebExecutor(geminiOptions) {
|
|
95
156
|
return async (runOptions) => {
|
|
96
157
|
const startTime = Date.now();
|
|
97
158
|
const log = runOptions.log;
|
|
98
159
|
log?.('[gemini-web] Starting Gemini web executor (TypeScript)');
|
|
99
|
-
const cookieMap = await
|
|
100
|
-
if (!cookieMap
|
|
160
|
+
const cookieMap = await loadGeminiCookies(runOptions.config, log);
|
|
161
|
+
if (!hasRequiredGeminiCookies(cookieMap)) {
|
|
101
162
|
throw new Error('Gemini browser mode requires Chrome cookies for google.com (missing __Secure-1PSID/__Secure-1PSIDTS).');
|
|
102
163
|
}
|
|
103
164
|
const configTimeout = typeof runOptions.config?.timeoutMs === 'number' && Number.isFinite(runOptions.config.timeoutMs)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { formatUSD } from './format.js';
|
|
2
|
+
export function formatElapsedCompact(ms) {
|
|
3
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
4
|
+
return 'unknown';
|
|
5
|
+
}
|
|
6
|
+
if (ms < 60_000) {
|
|
7
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
8
|
+
}
|
|
9
|
+
if (ms < 60 * 60_000) {
|
|
10
|
+
const minutes = Math.floor(ms / 60_000);
|
|
11
|
+
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
12
|
+
return `${minutes}m${seconds.toString().padStart(2, '0')}s`;
|
|
13
|
+
}
|
|
14
|
+
const hours = Math.floor(ms / (60 * 60_000));
|
|
15
|
+
const minutes = Math.floor((ms % (60 * 60_000)) / 60_000);
|
|
16
|
+
return `${hours}h${minutes.toString().padStart(2, '0')}m`;
|
|
17
|
+
}
|
|
18
|
+
export function formatFinishLine({ elapsedMs, model, costUsd, tokensPart, summaryExtraParts, detailParts, }) {
|
|
19
|
+
const line1Parts = [
|
|
20
|
+
formatElapsedCompact(elapsedMs),
|
|
21
|
+
typeof costUsd === 'number' ? formatUSD(costUsd) : null,
|
|
22
|
+
model,
|
|
23
|
+
tokensPart,
|
|
24
|
+
...(summaryExtraParts ?? []),
|
|
25
|
+
];
|
|
26
|
+
const line1 = line1Parts.filter((part) => typeof part === 'string' && part.length > 0).join(' · ');
|
|
27
|
+
const line2Parts = (detailParts ?? []).filter((part) => typeof part === 'string' && part.length > 0);
|
|
28
|
+
if (line2Parts.length === 0) {
|
|
29
|
+
return { line1 };
|
|
30
|
+
}
|
|
31
|
+
return { line1, line2: line2Parts.join(' | ') };
|
|
32
|
+
}
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -8,7 +8,8 @@ import { DEFAULT_SYSTEM_PROMPT, MODEL_CONFIGS, TOKENIZER_OPTIONS } from './confi
|
|
|
8
8
|
import { readFiles } from './files.js';
|
|
9
9
|
import { buildPrompt, buildRequestBody } from './request.js';
|
|
10
10
|
import { estimateRequestTokens } from './tokenEstimate.js';
|
|
11
|
-
import { formatElapsed
|
|
11
|
+
import { formatElapsed } from './format.js';
|
|
12
|
+
import { formatFinishLine } from './finishLine.js';
|
|
12
13
|
import { getFileTokenStats, printFileTokenStats } from './tokenStats.js';
|
|
13
14
|
import { OracleResponseError, OracleTransportError, PromptValidationError, describeTransportError, toTransportError, } from './errors.js';
|
|
14
15
|
import { createDefaultClientFactory } from './client.js';
|
|
@@ -19,7 +20,7 @@ import { createFsAdapter } from './fsAdapter.js';
|
|
|
19
20
|
import { resolveGeminiModelId } from './gemini.js';
|
|
20
21
|
import { resolveClaudeModelId } from './claude.js';
|
|
21
22
|
import { renderMarkdownAnsi } from '../cli/markdownRenderer.js';
|
|
22
|
-
import {
|
|
23
|
+
import { createMarkdownStreamer } from 'markdansi';
|
|
23
24
|
import { executeBackgroundResponse } from './background.js';
|
|
24
25
|
import { formatTokenEstimate, formatTokenValue, resolvePreviewMode } from './runUtils.js';
|
|
25
26
|
import { estimateUsdCost } from 'tokentally';
|
|
@@ -311,8 +312,6 @@ export async function runOracle(options, deps = {}) {
|
|
|
311
312
|
let response = null;
|
|
312
313
|
let elapsedMs = 0;
|
|
313
314
|
let sawTextDelta = false;
|
|
314
|
-
let streamedText = '';
|
|
315
|
-
let lastLiveFrameAtMs = 0;
|
|
316
315
|
let answerHeaderPrinted = false;
|
|
317
316
|
const allowAnswerHeader = options.suppressAnswerHeader !== true;
|
|
318
317
|
const timeoutExceeded = () => now() - runStart >= timeoutMs;
|
|
@@ -380,14 +379,23 @@ export async function runOracle(options, deps = {}) {
|
|
|
380
379
|
},
|
|
381
380
|
});
|
|
382
381
|
}
|
|
383
|
-
let
|
|
382
|
+
let markdownStreamer = null;
|
|
383
|
+
const flushMarkdownStreamer = () => {
|
|
384
|
+
if (!markdownStreamer)
|
|
385
|
+
return;
|
|
386
|
+
const rendered = markdownStreamer.finish();
|
|
387
|
+
markdownStreamer = null;
|
|
388
|
+
if (rendered) {
|
|
389
|
+
stdoutWrite(rendered);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
384
392
|
try {
|
|
385
|
-
|
|
393
|
+
markdownStreamer =
|
|
386
394
|
isTty && !renderPlain
|
|
387
|
-
?
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
395
|
+
? createMarkdownStreamer({
|
|
396
|
+
render: renderMarkdownAnsi,
|
|
397
|
+
spacing: 'single',
|
|
398
|
+
mode: 'hybrid',
|
|
391
399
|
})
|
|
392
400
|
: null;
|
|
393
401
|
for await (const event of stream) {
|
|
@@ -408,38 +416,27 @@ export async function runOracle(options, deps = {}) {
|
|
|
408
416
|
stdoutWrite(event.delta);
|
|
409
417
|
continue;
|
|
410
418
|
}
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const hasNewline = event.delta.includes('\n');
|
|
416
|
-
if (hasNewline || due) {
|
|
417
|
-
liveRenderer.render(streamedText);
|
|
418
|
-
lastLiveFrameAtMs = currentMs;
|
|
419
|
+
if (markdownStreamer) {
|
|
420
|
+
const rendered = markdownStreamer.push(event.delta);
|
|
421
|
+
if (rendered) {
|
|
422
|
+
stdoutWrite(rendered);
|
|
419
423
|
}
|
|
420
424
|
continue;
|
|
421
425
|
}
|
|
422
426
|
// Non-TTY streams should still surface output; fall back to raw stdout.
|
|
423
427
|
stdoutWrite(event.delta);
|
|
424
428
|
}
|
|
425
|
-
|
|
426
|
-
streamedText = streamedText.trim();
|
|
427
|
-
if (streamedText.length > 0) {
|
|
428
|
-
liveRenderer.render(streamedText);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
429
|
+
flushMarkdownStreamer();
|
|
431
430
|
throwIfTimedOut();
|
|
432
431
|
}
|
|
433
432
|
catch (streamError) {
|
|
434
433
|
// stream.abort() is not available on the interface
|
|
434
|
+
flushMarkdownStreamer();
|
|
435
435
|
stopHeartbeatNow();
|
|
436
436
|
const transportError = toTransportError(streamError, requestBody.model);
|
|
437
437
|
log(chalk.yellow(describeTransportError(transportError, timeoutMs)));
|
|
438
438
|
throw transportError;
|
|
439
439
|
}
|
|
440
|
-
finally {
|
|
441
|
-
liveRenderer?.finish();
|
|
442
|
-
}
|
|
443
440
|
response = await stream.finalResponse();
|
|
444
441
|
throwIfTimedOut();
|
|
445
442
|
stopHeartbeatNow();
|
|
@@ -454,17 +451,12 @@ export async function runOracle(options, deps = {}) {
|
|
|
454
451
|
}
|
|
455
452
|
// We only add spacing when streamed text was printed.
|
|
456
453
|
if (sawTextDelta && !options.silent) {
|
|
457
|
-
const shouldRenderAfterStream = isTty && !renderPlain && streamedText.length > 0;
|
|
458
454
|
if (renderPlain) {
|
|
459
455
|
// Plain streaming already wrote chunks; ensure clean separation.
|
|
460
456
|
stdoutWrite('\n');
|
|
461
457
|
}
|
|
462
|
-
else if (!shouldRenderAfterStream) {
|
|
463
|
-
// Non-TTY streams should still surface output; ensure separation.
|
|
464
|
-
log('');
|
|
465
|
-
}
|
|
466
458
|
else {
|
|
467
|
-
//
|
|
459
|
+
// Separate streamed output from logs.
|
|
468
460
|
log('');
|
|
469
461
|
}
|
|
470
462
|
}
|
|
@@ -530,46 +522,48 @@ export async function runOracle(options, deps = {}) {
|
|
|
530
522
|
pricing: { inputUsdPerToken: pricing.inputPerToken, outputUsdPerToken: pricing.outputPerToken },
|
|
531
523
|
})?.totalUsd
|
|
532
524
|
: undefined;
|
|
533
|
-
const elapsedDisplay = formatElapsed(elapsedMs);
|
|
534
|
-
const statsParts = [];
|
|
535
525
|
const effortLabel = modelConfig.reasoning?.effort;
|
|
536
526
|
const modelLabel = effortLabel ? `${modelConfig.model}[${effortLabel}]` : modelConfig.model;
|
|
537
527
|
const sessionIdContainsModel = typeof options.sessionId === 'string' && options.sessionId.toLowerCase().includes(modelConfig.model.toLowerCase());
|
|
538
|
-
// Avoid duplicating the model name in the prefix (session id) and the stats bundle; keep a single source of truth.
|
|
539
|
-
if (!sessionIdContainsModel) {
|
|
540
|
-
statsParts.push(modelLabel);
|
|
541
|
-
}
|
|
542
|
-
if (cost != null) {
|
|
543
|
-
statsParts.push(formatUSD(cost));
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
statsParts.push('cost=N/A');
|
|
547
|
-
}
|
|
548
528
|
const tokensDisplay = [inputTokens, outputTokens, reasoningTokens, totalTokens]
|
|
549
529
|
.map((value, index) => formatTokenValue(value, usage, index))
|
|
550
530
|
.join('/');
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
const
|
|
531
|
+
const tokensPart = (() => {
|
|
532
|
+
const parts = tokensDisplay.split('/');
|
|
533
|
+
if (parts.length !== 4)
|
|
534
|
+
return tokensDisplay;
|
|
535
|
+
return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
|
|
536
|
+
})();
|
|
537
|
+
const modelPart = sessionIdContainsModel ? null : modelLabel;
|
|
538
|
+
const actualInput = usage.input_tokens;
|
|
539
|
+
const estActualPart = (() => {
|
|
540
|
+
if (!options.verbose)
|
|
541
|
+
return null;
|
|
542
|
+
if (actualInput === undefined)
|
|
543
|
+
return null;
|
|
544
|
+
const delta = actualInput - estimatedInputTokens;
|
|
545
|
+
const deltaText = delta === 0 ? '' : delta > 0 ? ` (+${delta.toLocaleString()})` : ` (${delta.toLocaleString()})`;
|
|
546
|
+
return `est→actual=${estimatedInputTokens.toLocaleString()}→${actualInput.toLocaleString()}${deltaText}`;
|
|
547
|
+
})();
|
|
548
|
+
const { line1, line2 } = formatFinishLine({
|
|
549
|
+
elapsedMs,
|
|
550
|
+
model: modelPart,
|
|
551
|
+
costUsd: cost ?? null,
|
|
552
|
+
tokensPart,
|
|
553
|
+
summaryExtraParts: options.sessionId ? [`sid=${options.sessionId}`] : null,
|
|
554
|
+
detailParts: [
|
|
555
|
+
estActualPart,
|
|
556
|
+
!searchEnabled ? 'search=off' : null,
|
|
557
|
+
files.length > 0 ? `files=${files.length}` : null,
|
|
558
|
+
],
|
|
559
|
+
});
|
|
569
560
|
if (!options.silent) {
|
|
570
561
|
log('');
|
|
571
562
|
}
|
|
572
|
-
log(chalk.blue(
|
|
563
|
+
log(chalk.blue(line1));
|
|
564
|
+
if (line2) {
|
|
565
|
+
log(dim(line2));
|
|
566
|
+
}
|
|
573
567
|
return {
|
|
574
568
|
mode: 'live',
|
|
575
569
|
response,
|
|
@@ -7,7 +7,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
7
7
|
import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { runBrowserMode } from '../browserMode.js';
|
|
10
|
-
import {
|
|
10
|
+
import { getCookies } from '@steipete/sweet-cookie';
|
|
11
11
|
import { CHATGPT_URL } from '../browser/constants.js';
|
|
12
12
|
import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from '../browser/profileState.js';
|
|
13
13
|
import { normalizeChatgptUrl } from '../browser/utils.js';
|
|
@@ -331,13 +331,17 @@ function formatReachableAddresses(bindAddress, port) {
|
|
|
331
331
|
async function loadLocalChatgptCookies(logger, targetUrl) {
|
|
332
332
|
try {
|
|
333
333
|
logger('Loading ChatGPT cookies from this host\'s Chrome profile...');
|
|
334
|
-
const cookies = await
|
|
335
|
-
targetUrl,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
334
|
+
const { cookies: rawCookies, warnings } = await getCookies({
|
|
335
|
+
url: targetUrl,
|
|
336
|
+
browsers: ['chrome'],
|
|
337
|
+
mode: 'merge',
|
|
338
|
+
chromeProfile: 'Default',
|
|
339
|
+
timeoutMs: 5_000,
|
|
340
340
|
});
|
|
341
|
+
if (warnings.length) {
|
|
342
|
+
logger(`Cookie warnings:\n- ${warnings.join('\n- ')}`);
|
|
343
|
+
}
|
|
344
|
+
const cookies = rawCookies.map(toCdpCookie).filter((c) => Boolean(c));
|
|
341
345
|
if (!cookies || cookies.length === 0) {
|
|
342
346
|
logger('No local ChatGPT cookies found on this host. Please log in once; opening ChatGPT...');
|
|
343
347
|
const opened = triggerLocalLoginPrompt(logger, targetUrl);
|
|
@@ -348,14 +352,7 @@ async function loadLocalChatgptCookies(logger, targetUrl) {
|
|
|
348
352
|
}
|
|
349
353
|
catch (error) {
|
|
350
354
|
const message = error instanceof Error ? error.message : String(error);
|
|
351
|
-
|
|
352
|
-
if (missingDbMatch) {
|
|
353
|
-
const lookedPath = missingDbMatch[1];
|
|
354
|
-
logger(`Chrome cookies not found at ${lookedPath}. Set --browser-cookie-path to your Chrome profile or log in manually.`);
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
357
|
-
logger(`Unable to load local ChatGPT cookies on this host: ${message}`);
|
|
358
|
-
}
|
|
355
|
+
logger(`Unable to load local ChatGPT cookies on this host: ${message}`);
|
|
359
356
|
if (process.platform === 'linux' && isWsl()) {
|
|
360
357
|
logger('WSL hint: Chrome lives under /mnt/c/Users/<you>/AppData/Local/Google/Chrome/User Data/Default; pass --browser-cookie-path to that directory if auto-detect fails.');
|
|
361
358
|
}
|
|
@@ -363,6 +360,24 @@ async function loadLocalChatgptCookies(logger, targetUrl) {
|
|
|
363
360
|
return { cookies: null, opened };
|
|
364
361
|
}
|
|
365
362
|
}
|
|
363
|
+
function toCdpCookie(cookie) {
|
|
364
|
+
if (!cookie?.name)
|
|
365
|
+
return null;
|
|
366
|
+
const out = {
|
|
367
|
+
name: cookie.name,
|
|
368
|
+
value: cookie.value,
|
|
369
|
+
domain: cookie.domain,
|
|
370
|
+
path: cookie.path ?? '/',
|
|
371
|
+
secure: cookie.secure ?? true,
|
|
372
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
373
|
+
};
|
|
374
|
+
if (typeof cookie.expires === 'number')
|
|
375
|
+
out.expires = cookie.expires;
|
|
376
|
+
if (cookie.sameSite === 'Lax' || cookie.sameSite === 'Strict' || cookie.sameSite === 'None') {
|
|
377
|
+
out.sameSite = cookie.sameSite;
|
|
378
|
+
}
|
|
379
|
+
return out;
|
|
380
|
+
}
|
|
366
381
|
function triggerLocalLoginPrompt(logger, url) {
|
|
367
382
|
const verbose = process.argv.includes('--verbose') || process.env.ORACLE_SERVE_VERBOSE === '1';
|
|
368
383
|
const openers = [];
|
|
@@ -445,6 +445,22 @@ async function markZombie(meta, { persist }) {
|
|
|
445
445
|
if (!isZombie(meta)) {
|
|
446
446
|
return meta;
|
|
447
447
|
}
|
|
448
|
+
if (meta.mode === 'browser') {
|
|
449
|
+
const runtime = meta.browser?.runtime;
|
|
450
|
+
if (runtime) {
|
|
451
|
+
const signals = [];
|
|
452
|
+
if (runtime.chromePid) {
|
|
453
|
+
signals.push(isProcessAlive(runtime.chromePid));
|
|
454
|
+
}
|
|
455
|
+
if (runtime.chromePort) {
|
|
456
|
+
const host = runtime.chromeHost ?? '127.0.0.1';
|
|
457
|
+
signals.push(await isPortOpen(host, runtime.chromePort));
|
|
458
|
+
}
|
|
459
|
+
if (signals.some(Boolean)) {
|
|
460
|
+
return meta;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
448
464
|
const updated = {
|
|
449
465
|
...meta,
|
|
450
466
|
status: 'error',
|