@steipete/oracle 0.6.1 → 0.7.1
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 +16 -8
- package/dist/bin/oracle-cli.js +37 -17
- package/dist/src/browser/actions/assistantResponse.js +81 -49
- package/dist/src/browser/actions/attachments.js +37 -3
- package/dist/src/browser/actions/modelSelection.js +94 -5
- package/dist/src/browser/actions/promptComposer.js +22 -14
- package/dist/src/browser/constants.js +6 -2
- package/dist/src/browser/index.js +78 -5
- package/dist/src/browser/prompt.js +30 -6
- package/dist/src/browser/sessionRunner.js +0 -5
- package/dist/src/cli/browserConfig.js +34 -8
- package/dist/src/cli/help.js +3 -3
- package/dist/src/cli/options.js +20 -8
- package/dist/src/cli/runOptions.js +10 -8
- package/dist/src/cli/sessionRunner.js +0 -3
- package/dist/src/gemini-web/client.js +328 -0
- package/dist/src/gemini-web/executor.js +224 -0
- package/dist/src/gemini-web/index.js +1 -0
- package/dist/src/gemini-web/types.js +1 -0
- package/dist/src/mcp/tools/consult.js +4 -1
- package/dist/src/oracle/config.js +1 -1
- package/dist/src/oracle/run.js +15 -4
- package/package.json +17 -17
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +0 -45
- package/dist/vendor/oracle-notifier/oracle-notifier/README.md +0 -24
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +0 -93
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
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
|
+
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-2.5-pro': '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]',
|
|
8
|
+
'gemini-2.5-flash': '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]',
|
|
9
|
+
};
|
|
10
|
+
const GEMINI_APP_URL = 'https://gemini.google.com/app';
|
|
11
|
+
const GEMINI_STREAM_GENERATE_URL = 'https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate';
|
|
12
|
+
const GEMINI_UPLOAD_URL = 'https://content-push.googleapis.com/upload';
|
|
13
|
+
const GEMINI_UPLOAD_PUSH_ID = 'feeds/mcudyrk2a4khkz';
|
|
14
|
+
function getNestedValue(value, pathParts, fallback) {
|
|
15
|
+
let current = value;
|
|
16
|
+
for (const part of pathParts) {
|
|
17
|
+
if (current == null)
|
|
18
|
+
return fallback;
|
|
19
|
+
if (typeof part === 'number') {
|
|
20
|
+
if (!Array.isArray(current))
|
|
21
|
+
return fallback;
|
|
22
|
+
current = current[part];
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
if (typeof current !== 'object')
|
|
26
|
+
return fallback;
|
|
27
|
+
current = current[part];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return current ?? fallback;
|
|
31
|
+
}
|
|
32
|
+
function buildCookieHeader(cookieMap) {
|
|
33
|
+
return Object.entries(cookieMap)
|
|
34
|
+
.filter(([, value]) => typeof value === 'string' && value.length > 0)
|
|
35
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
36
|
+
.join('; ');
|
|
37
|
+
}
|
|
38
|
+
export async function fetchGeminiAccessToken(cookieMap, signal) {
|
|
39
|
+
const cookieHeader = buildCookieHeader(cookieMap);
|
|
40
|
+
const res = await fetch(GEMINI_APP_URL, {
|
|
41
|
+
redirect: 'follow',
|
|
42
|
+
signal,
|
|
43
|
+
headers: {
|
|
44
|
+
cookie: cookieHeader,
|
|
45
|
+
'user-agent': USER_AGENT,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const html = await res.text();
|
|
49
|
+
const tokens = ['SNlM0e', 'thykhd'];
|
|
50
|
+
for (const key of tokens) {
|
|
51
|
+
const match = html.match(new RegExp(`"${key}":"(.*?)"`));
|
|
52
|
+
if (match?.[1])
|
|
53
|
+
return match[1];
|
|
54
|
+
}
|
|
55
|
+
throw new Error('Unable to locate Gemini access token on gemini.google.com/app (missing SNlM0e/thykhd).');
|
|
56
|
+
}
|
|
57
|
+
function trimGeminiJsonEnvelope(text) {
|
|
58
|
+
const start = text.indexOf('[');
|
|
59
|
+
const end = text.lastIndexOf(']');
|
|
60
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
61
|
+
throw new Error('Gemini response did not contain a JSON payload.');
|
|
62
|
+
}
|
|
63
|
+
return text.slice(start, end + 1);
|
|
64
|
+
}
|
|
65
|
+
function extractErrorCode(responseJson) {
|
|
66
|
+
const code = getNestedValue(responseJson, [0, 5, 2, 0, 1, 0], -1);
|
|
67
|
+
return typeof code === 'number' && code >= 0 ? code : undefined;
|
|
68
|
+
}
|
|
69
|
+
function extractGgdlUrls(rawText) {
|
|
70
|
+
const matches = rawText.match(/https:\/\/lh3\.googleusercontent\.com\/gg-dl\/[^\s"']+/g) ?? [];
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
const urls = [];
|
|
73
|
+
for (const match of matches) {
|
|
74
|
+
if (seen.has(match))
|
|
75
|
+
continue;
|
|
76
|
+
seen.add(match);
|
|
77
|
+
urls.push(match);
|
|
78
|
+
}
|
|
79
|
+
return urls;
|
|
80
|
+
}
|
|
81
|
+
function ensureFullSizeImageUrl(url) {
|
|
82
|
+
if (url.includes('=s2048'))
|
|
83
|
+
return url;
|
|
84
|
+
if (url.includes('=s'))
|
|
85
|
+
return url;
|
|
86
|
+
return `${url}=s2048`;
|
|
87
|
+
}
|
|
88
|
+
async function fetchWithCookiePreservingRedirects(url, init, signal, maxRedirects = 10) {
|
|
89
|
+
let current = url;
|
|
90
|
+
for (let i = 0; i <= maxRedirects; i += 1) {
|
|
91
|
+
const res = await fetch(current, { ...init, redirect: 'manual', signal });
|
|
92
|
+
if (res.status >= 300 && res.status < 400) {
|
|
93
|
+
const location = res.headers.get('location');
|
|
94
|
+
if (!location)
|
|
95
|
+
return res;
|
|
96
|
+
current = new URL(location, current).toString();
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
return res;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`Too many redirects while downloading image (>${maxRedirects}).`);
|
|
102
|
+
}
|
|
103
|
+
async function downloadGeminiImage(url, cookieMap, outputPath, signal) {
|
|
104
|
+
const cookieHeader = buildCookieHeader(cookieMap);
|
|
105
|
+
const res = await fetchWithCookiePreservingRedirects(ensureFullSizeImageUrl(url), {
|
|
106
|
+
headers: {
|
|
107
|
+
cookie: cookieHeader,
|
|
108
|
+
'user-agent': USER_AGENT,
|
|
109
|
+
},
|
|
110
|
+
}, signal);
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
throw new Error(`Failed to download image: ${res.status} ${res.statusText} (${res.url})`);
|
|
113
|
+
}
|
|
114
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
115
|
+
await mkdir(path.dirname(outputPath), { recursive: true });
|
|
116
|
+
await writeFile(outputPath, data);
|
|
117
|
+
}
|
|
118
|
+
async function uploadGeminiFile(filePath, signal) {
|
|
119
|
+
const absPath = path.resolve(process.cwd(), filePath);
|
|
120
|
+
const data = await readFile(absPath);
|
|
121
|
+
const fileName = path.basename(absPath);
|
|
122
|
+
const form = new FormData();
|
|
123
|
+
form.append('file', new Blob([data]), fileName);
|
|
124
|
+
const res = await fetch(GEMINI_UPLOAD_URL, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
redirect: 'follow',
|
|
127
|
+
signal,
|
|
128
|
+
headers: {
|
|
129
|
+
'push-id': GEMINI_UPLOAD_PUSH_ID,
|
|
130
|
+
'user-agent': USER_AGENT,
|
|
131
|
+
},
|
|
132
|
+
body: form,
|
|
133
|
+
});
|
|
134
|
+
const text = await res.text();
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
throw new Error(`File upload failed: ${res.status} ${res.statusText} (${text.slice(0, 200)})`);
|
|
137
|
+
}
|
|
138
|
+
return { id: text, name: fileName };
|
|
139
|
+
}
|
|
140
|
+
function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
|
|
141
|
+
const promptPayload = uploaded.length > 0
|
|
142
|
+
? [
|
|
143
|
+
prompt,
|
|
144
|
+
0,
|
|
145
|
+
null,
|
|
146
|
+
// Matches gemini-webapi payload format: [[[fileId, 1]]] for a single attachment.
|
|
147
|
+
// Keep it extensible for multiple uploads by emitting one [[id, 1]] entry per file.
|
|
148
|
+
uploaded.map((file) => [[file.id, 1]]),
|
|
149
|
+
]
|
|
150
|
+
: [prompt];
|
|
151
|
+
const innerList = [promptPayload, null, chatMetadata ?? null];
|
|
152
|
+
return JSON.stringify([null, JSON.stringify(innerList)]);
|
|
153
|
+
}
|
|
154
|
+
export function parseGeminiStreamGenerateResponse(rawText) {
|
|
155
|
+
const responseJson = JSON.parse(trimGeminiJsonEnvelope(rawText));
|
|
156
|
+
const errorCode = extractErrorCode(responseJson);
|
|
157
|
+
const parts = Array.isArray(responseJson) ? responseJson : [];
|
|
158
|
+
let bodyIndex = 0;
|
|
159
|
+
let body = null;
|
|
160
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
161
|
+
const partBody = getNestedValue(parts[i], [2], null);
|
|
162
|
+
if (!partBody)
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(partBody);
|
|
166
|
+
const candidateList = getNestedValue(parsed, [4], []);
|
|
167
|
+
if (Array.isArray(candidateList) && candidateList.length > 0) {
|
|
168
|
+
bodyIndex = i;
|
|
169
|
+
body = parsed;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// ignore
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const candidateList = getNestedValue(body, [4], []);
|
|
178
|
+
const firstCandidate = candidateList[0];
|
|
179
|
+
const textRaw = getNestedValue(firstCandidate, [1, 0], '');
|
|
180
|
+
const cardContent = /^http:\/\/googleusercontent\.com\/card_content\/\d+/.test(textRaw);
|
|
181
|
+
const text = cardContent
|
|
182
|
+
? (getNestedValue(firstCandidate, [22, 0], null) ?? textRaw)
|
|
183
|
+
: textRaw;
|
|
184
|
+
const thoughts = getNestedValue(firstCandidate, [37, 0, 0], null);
|
|
185
|
+
const metadata = getNestedValue(body, [1], []);
|
|
186
|
+
const images = [];
|
|
187
|
+
const webImages = getNestedValue(firstCandidate, [12, 1], []);
|
|
188
|
+
for (const webImage of webImages) {
|
|
189
|
+
const url = getNestedValue(webImage, [0, 0, 0], null);
|
|
190
|
+
if (!url)
|
|
191
|
+
continue;
|
|
192
|
+
images.push({
|
|
193
|
+
kind: 'web',
|
|
194
|
+
url,
|
|
195
|
+
title: getNestedValue(webImage, [7, 0], undefined),
|
|
196
|
+
alt: getNestedValue(webImage, [0, 4], undefined),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const hasGenerated = Boolean(getNestedValue(firstCandidate, [12, 7, 0], null));
|
|
200
|
+
if (hasGenerated) {
|
|
201
|
+
let imgBody = null;
|
|
202
|
+
for (let i = bodyIndex; i < parts.length; i += 1) {
|
|
203
|
+
const partBody = getNestedValue(parts[i], [2], null);
|
|
204
|
+
if (!partBody)
|
|
205
|
+
continue;
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(partBody);
|
|
208
|
+
const candidateImages = getNestedValue(parsed, [4, 0, 12, 7, 0], null);
|
|
209
|
+
if (candidateImages != null) {
|
|
210
|
+
imgBody = parsed;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// ignore
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const imgCandidate = getNestedValue(imgBody ?? body, [4, 0], null);
|
|
219
|
+
const generated = getNestedValue(imgCandidate, [12, 7, 0], []);
|
|
220
|
+
for (const genImage of generated) {
|
|
221
|
+
const url = getNestedValue(genImage, [0, 3, 3], null);
|
|
222
|
+
if (!url)
|
|
223
|
+
continue;
|
|
224
|
+
images.push({
|
|
225
|
+
kind: 'generated',
|
|
226
|
+
url,
|
|
227
|
+
title: '[Generated Image]',
|
|
228
|
+
alt: '',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { metadata, text, thoughts, images, errorCode };
|
|
233
|
+
}
|
|
234
|
+
export function isGeminiModelUnavailable(errorCode) {
|
|
235
|
+
return errorCode === 1052;
|
|
236
|
+
}
|
|
237
|
+
export async function runGeminiWebOnce(input) {
|
|
238
|
+
const cookieHeader = buildCookieHeader(input.cookieMap);
|
|
239
|
+
const at = await fetchGeminiAccessToken(input.cookieMap, input.signal);
|
|
240
|
+
const uploaded = [];
|
|
241
|
+
for (const file of input.files ?? []) {
|
|
242
|
+
if (input.signal?.aborted) {
|
|
243
|
+
throw new Error('Gemini web run aborted before upload.');
|
|
244
|
+
}
|
|
245
|
+
uploaded.push(await uploadGeminiFile(file, input.signal));
|
|
246
|
+
}
|
|
247
|
+
const fReq = buildGeminiFReqPayload(input.prompt, uploaded, input.chatMetadata ?? null);
|
|
248
|
+
const params = new URLSearchParams();
|
|
249
|
+
params.set('at', at);
|
|
250
|
+
params.set('f.req', fReq);
|
|
251
|
+
const res = await fetch(GEMINI_STREAM_GENERATE_URL, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
redirect: 'follow',
|
|
254
|
+
signal: input.signal,
|
|
255
|
+
headers: {
|
|
256
|
+
'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
|
|
257
|
+
origin: 'https://gemini.google.com',
|
|
258
|
+
referer: 'https://gemini.google.com/',
|
|
259
|
+
'x-same-domain': '1',
|
|
260
|
+
'user-agent': USER_AGENT,
|
|
261
|
+
cookie: cookieHeader,
|
|
262
|
+
[MODEL_HEADER_NAME]: MODEL_HEADERS[input.model],
|
|
263
|
+
},
|
|
264
|
+
body: params.toString(),
|
|
265
|
+
});
|
|
266
|
+
const rawResponseText = await res.text();
|
|
267
|
+
if (!res.ok) {
|
|
268
|
+
return {
|
|
269
|
+
rawResponseText,
|
|
270
|
+
text: '',
|
|
271
|
+
thoughts: null,
|
|
272
|
+
metadata: input.chatMetadata ?? null,
|
|
273
|
+
images: [],
|
|
274
|
+
errorMessage: `Gemini request failed: ${res.status} ${res.statusText}`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const parsed = parseGeminiStreamGenerateResponse(rawResponseText);
|
|
279
|
+
return {
|
|
280
|
+
rawResponseText,
|
|
281
|
+
text: parsed.text ?? '',
|
|
282
|
+
thoughts: parsed.thoughts,
|
|
283
|
+
metadata: parsed.metadata,
|
|
284
|
+
images: parsed.images,
|
|
285
|
+
errorCode: parsed.errorCode,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
let responseJson = null;
|
|
290
|
+
try {
|
|
291
|
+
responseJson = JSON.parse(trimGeminiJsonEnvelope(rawResponseText));
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
responseJson = null;
|
|
295
|
+
}
|
|
296
|
+
const errorCode = extractErrorCode(responseJson);
|
|
297
|
+
return {
|
|
298
|
+
rawResponseText,
|
|
299
|
+
text: '',
|
|
300
|
+
thoughts: null,
|
|
301
|
+
metadata: input.chatMetadata ?? null,
|
|
302
|
+
images: [],
|
|
303
|
+
errorCode: typeof errorCode === 'number' ? errorCode : undefined,
|
|
304
|
+
errorMessage: error instanceof Error ? error.message : String(error ?? ''),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
export async function runGeminiWebWithFallback(input) {
|
|
309
|
+
const attempt = await runGeminiWebOnce(input);
|
|
310
|
+
if (isGeminiModelUnavailable(attempt.errorCode) && input.model !== 'gemini-2.5-flash') {
|
|
311
|
+
const fallback = await runGeminiWebOnce({ ...input, model: 'gemini-2.5-flash' });
|
|
312
|
+
return { ...fallback, effectiveModel: 'gemini-2.5-flash' };
|
|
313
|
+
}
|
|
314
|
+
return { ...attempt, effectiveModel: input.model };
|
|
315
|
+
}
|
|
316
|
+
export async function saveFirstGeminiImageFromOutput(output, cookieMap, outputPath, signal) {
|
|
317
|
+
const generatedOrWeb = output.images.find((img) => img.kind === 'generated') ?? output.images[0];
|
|
318
|
+
if (generatedOrWeb?.url) {
|
|
319
|
+
await downloadGeminiImage(generatedOrWeb.url, cookieMap, outputPath, signal);
|
|
320
|
+
return { saved: true, imageCount: output.images.length };
|
|
321
|
+
}
|
|
322
|
+
const ggdl = extractGgdlUrls(output.rawResponseText);
|
|
323
|
+
if (ggdl[0]) {
|
|
324
|
+
await downloadGeminiImage(ggdl[0], cookieMap, outputPath, signal);
|
|
325
|
+
return { saved: true, imageCount: ggdl.length };
|
|
326
|
+
}
|
|
327
|
+
return { saved: false, imageCount: 0 };
|
|
328
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { runGeminiWebWithFallback, saveFirstGeminiImageFromOutput } from './client.js';
|
|
3
|
+
function estimateTokenCount(text) {
|
|
4
|
+
return Math.ceil(text.length / 4);
|
|
5
|
+
}
|
|
6
|
+
function resolveInvocationPath(value) {
|
|
7
|
+
if (!value)
|
|
8
|
+
return undefined;
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
if (!trimmed)
|
|
11
|
+
return undefined;
|
|
12
|
+
return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
|
|
13
|
+
}
|
|
14
|
+
function resolveGeminiWebModel(desiredModel, log) {
|
|
15
|
+
const desired = typeof desiredModel === 'string' ? desiredModel.trim() : '';
|
|
16
|
+
if (!desired)
|
|
17
|
+
return 'gemini-3-pro';
|
|
18
|
+
switch (desired) {
|
|
19
|
+
case 'gemini-3-pro':
|
|
20
|
+
case 'gemini-3.0-pro':
|
|
21
|
+
return 'gemini-3-pro';
|
|
22
|
+
case 'gemini-2.5-pro':
|
|
23
|
+
return 'gemini-2.5-pro';
|
|
24
|
+
case 'gemini-2.5-flash':
|
|
25
|
+
return 'gemini-2.5-flash';
|
|
26
|
+
default:
|
|
27
|
+
if (desired.startsWith('gemini-')) {
|
|
28
|
+
log?.(`[gemini-web] Unsupported Gemini web model "${desired}". Falling back to gemini-3-pro.`);
|
|
29
|
+
}
|
|
30
|
+
return 'gemini-3-pro';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function loadGeminiCookiesFromChrome(browserConfig, log) {
|
|
34
|
+
try {
|
|
35
|
+
const mod = (await import('chrome-cookies-secure'));
|
|
36
|
+
const chromeCookies = mod.default ??
|
|
37
|
+
mod;
|
|
38
|
+
const profile = typeof browserConfig?.chromeProfile === 'string' &&
|
|
39
|
+
browserConfig.chromeProfile.trim().length > 0
|
|
40
|
+
? browserConfig.chromeProfile.trim()
|
|
41
|
+
: undefined;
|
|
42
|
+
const sources = [
|
|
43
|
+
'https://gemini.google.com',
|
|
44
|
+
'https://accounts.google.com',
|
|
45
|
+
'https://www.google.com',
|
|
46
|
+
];
|
|
47
|
+
const wantNames = [
|
|
48
|
+
'__Secure-1PSID',
|
|
49
|
+
'__Secure-1PSIDTS',
|
|
50
|
+
'__Secure-1PSIDCC',
|
|
51
|
+
'__Secure-1PAPISID',
|
|
52
|
+
'NID',
|
|
53
|
+
'AEC',
|
|
54
|
+
'SOCS',
|
|
55
|
+
'__Secure-BUCKET',
|
|
56
|
+
'__Secure-ENID',
|
|
57
|
+
'SID',
|
|
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 {};
|
|
85
|
+
}
|
|
86
|
+
log?.(`[gemini-web] Loaded Gemini cookies from Chrome (node): ${Object.keys(cookieMap).length} cookie(s).`);
|
|
87
|
+
return cookieMap;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
log?.(`[gemini-web] Failed to load Chrome cookies via node: ${error instanceof Error ? error.message : String(error ?? '')}`);
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function createGeminiWebExecutor(geminiOptions) {
|
|
95
|
+
return async (runOptions) => {
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
const log = runOptions.log;
|
|
98
|
+
log?.('[gemini-web] Starting Gemini web executor (TypeScript)');
|
|
99
|
+
const cookieMap = await loadGeminiCookiesFromChrome(runOptions.config, log);
|
|
100
|
+
if (!cookieMap['__Secure-1PSID'] || !cookieMap['__Secure-1PSIDTS']) {
|
|
101
|
+
throw new Error('Gemini browser mode requires Chrome cookies for google.com (missing __Secure-1PSID/__Secure-1PSIDTS).');
|
|
102
|
+
}
|
|
103
|
+
const configTimeout = typeof runOptions.config?.timeoutMs === 'number' && Number.isFinite(runOptions.config.timeoutMs)
|
|
104
|
+
? Math.max(1_000, runOptions.config.timeoutMs)
|
|
105
|
+
: null;
|
|
106
|
+
const defaultTimeoutMs = geminiOptions.youtube
|
|
107
|
+
? 240_000
|
|
108
|
+
: geminiOptions.generateImage || geminiOptions.editImage
|
|
109
|
+
? 300_000
|
|
110
|
+
: 120_000;
|
|
111
|
+
const timeoutMs = Math.min(configTimeout ?? defaultTimeoutMs, 600_000);
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
114
|
+
const generateImagePath = resolveInvocationPath(geminiOptions.generateImage);
|
|
115
|
+
const editImagePath = resolveInvocationPath(geminiOptions.editImage);
|
|
116
|
+
const outputPath = resolveInvocationPath(geminiOptions.outputPath);
|
|
117
|
+
const attachmentPaths = (runOptions.attachments ?? []).map((attachment) => attachment.path);
|
|
118
|
+
let prompt = runOptions.prompt;
|
|
119
|
+
if (geminiOptions.aspectRatio && (generateImagePath || editImagePath)) {
|
|
120
|
+
prompt = `${prompt} (aspect ratio: ${geminiOptions.aspectRatio})`;
|
|
121
|
+
}
|
|
122
|
+
if (geminiOptions.youtube) {
|
|
123
|
+
prompt = `${prompt}\n\nYouTube video: ${geminiOptions.youtube}`;
|
|
124
|
+
}
|
|
125
|
+
if (generateImagePath && !editImagePath) {
|
|
126
|
+
prompt = `Generate an image: ${prompt}`;
|
|
127
|
+
}
|
|
128
|
+
const model = resolveGeminiWebModel(runOptions.config?.desiredModel, log);
|
|
129
|
+
let response;
|
|
130
|
+
try {
|
|
131
|
+
if (editImagePath) {
|
|
132
|
+
const intro = await runGeminiWebWithFallback({
|
|
133
|
+
prompt: 'Here is an image to edit',
|
|
134
|
+
files: [editImagePath],
|
|
135
|
+
model,
|
|
136
|
+
cookieMap,
|
|
137
|
+
chatMetadata: null,
|
|
138
|
+
signal: controller.signal,
|
|
139
|
+
});
|
|
140
|
+
const editPrompt = `Use image generation tool to ${prompt}`;
|
|
141
|
+
const out = await runGeminiWebWithFallback({
|
|
142
|
+
prompt: editPrompt,
|
|
143
|
+
files: attachmentPaths,
|
|
144
|
+
model,
|
|
145
|
+
cookieMap,
|
|
146
|
+
chatMetadata: intro.metadata,
|
|
147
|
+
signal: controller.signal,
|
|
148
|
+
});
|
|
149
|
+
response = {
|
|
150
|
+
text: out.text ?? null,
|
|
151
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
152
|
+
has_images: false,
|
|
153
|
+
image_count: 0,
|
|
154
|
+
};
|
|
155
|
+
const resolvedOutputPath = outputPath ?? generateImagePath ?? 'generated.png';
|
|
156
|
+
const imageSave = await saveFirstGeminiImageFromOutput(out, cookieMap, resolvedOutputPath, controller.signal);
|
|
157
|
+
response.has_images = imageSave.saved;
|
|
158
|
+
response.image_count = imageSave.imageCount;
|
|
159
|
+
if (!imageSave.saved) {
|
|
160
|
+
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (generateImagePath) {
|
|
164
|
+
const out = await runGeminiWebWithFallback({
|
|
165
|
+
prompt,
|
|
166
|
+
files: attachmentPaths,
|
|
167
|
+
model,
|
|
168
|
+
cookieMap,
|
|
169
|
+
chatMetadata: null,
|
|
170
|
+
signal: controller.signal,
|
|
171
|
+
});
|
|
172
|
+
response = {
|
|
173
|
+
text: out.text ?? null,
|
|
174
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
175
|
+
has_images: false,
|
|
176
|
+
image_count: 0,
|
|
177
|
+
};
|
|
178
|
+
const imageSave = await saveFirstGeminiImageFromOutput(out, cookieMap, generateImagePath, controller.signal);
|
|
179
|
+
response.has_images = imageSave.saved;
|
|
180
|
+
response.image_count = imageSave.imageCount;
|
|
181
|
+
if (!imageSave.saved) {
|
|
182
|
+
throw new Error(`No images generated. Response text:\n${out.text || '(empty response)'}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const out = await runGeminiWebWithFallback({
|
|
187
|
+
prompt,
|
|
188
|
+
files: attachmentPaths,
|
|
189
|
+
model,
|
|
190
|
+
cookieMap,
|
|
191
|
+
chatMetadata: null,
|
|
192
|
+
signal: controller.signal,
|
|
193
|
+
});
|
|
194
|
+
response = {
|
|
195
|
+
text: out.text ?? null,
|
|
196
|
+
thoughts: geminiOptions.showThoughts ? out.thoughts : null,
|
|
197
|
+
has_images: out.images.length > 0,
|
|
198
|
+
image_count: out.images.length,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
clearTimeout(timeout);
|
|
204
|
+
}
|
|
205
|
+
const answerText = response.text ?? '';
|
|
206
|
+
let answerMarkdown = answerText;
|
|
207
|
+
if (geminiOptions.showThoughts && response.thoughts) {
|
|
208
|
+
answerMarkdown = `## Thinking\n\n${response.thoughts}\n\n## Response\n\n${answerText}`;
|
|
209
|
+
}
|
|
210
|
+
if (response.has_images && response.image_count > 0) {
|
|
211
|
+
const imagePath = generateImagePath || outputPath || 'generated.png';
|
|
212
|
+
answerMarkdown += `\n\n*Generated ${response.image_count} image(s). Saved to: ${imagePath}*`;
|
|
213
|
+
}
|
|
214
|
+
const tookMs = Date.now() - startTime;
|
|
215
|
+
log?.(`[gemini-web] Completed in ${tookMs}ms`);
|
|
216
|
+
return {
|
|
217
|
+
answerText,
|
|
218
|
+
answerMarkdown,
|
|
219
|
+
tookMs,
|
|
220
|
+
answerTokens: estimateTokenCount(answerText),
|
|
221
|
+
answerChars: answerText.length,
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createGeminiWebExecutor } from './executor.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -129,7 +129,10 @@ export function registerConsultTool(server) {
|
|
|
129
129
|
let browserConfig;
|
|
130
130
|
if (resolvedEngine === 'browser') {
|
|
131
131
|
const preferredLabel = (browserModelLabel ?? model)?.trim();
|
|
132
|
-
const
|
|
132
|
+
const isChatGptModel = runOptions.model.startsWith('gpt-') && !runOptions.model.includes('codex');
|
|
133
|
+
const desiredModelLabel = isChatGptModel
|
|
134
|
+
? mapModelToBrowserLabel(runOptions.model)
|
|
135
|
+
: resolveBrowserModelLabel(preferredLabel, runOptions.model);
|
|
133
136
|
// Keep the browser path minimal; only forward a desired model label for the ChatGPT picker.
|
|
134
137
|
browserConfig = {
|
|
135
138
|
url: CHATGPT_URL,
|
|
@@ -2,7 +2,7 @@ import { countTokens as countTokensGpt5 } from 'gpt-tokenizer/model/gpt-5';
|
|
|
2
2
|
import { countTokens as countTokensGpt5Pro } from 'gpt-tokenizer/model/gpt-5-pro';
|
|
3
3
|
import { countTokens as countTokensAnthropicRaw } from '@anthropic-ai/tokenizer';
|
|
4
4
|
import { stringifyTokenizerInput } from './tokenStringifier.js';
|
|
5
|
-
export const DEFAULT_MODEL = 'gpt-5.
|
|
5
|
+
export const DEFAULT_MODEL = 'gpt-5.2-pro';
|
|
6
6
|
export const PRO_MODELS = new Set(['gpt-5.1-pro', 'gpt-5-pro', 'gpt-5.2-pro', 'claude-4.5-sonnet', 'claude-4.1-opus']);
|
|
7
7
|
const countTokensAnthropic = (input) => countTokensAnthropicRaw(stringifyTokenizerInput(input));
|
|
8
8
|
export const MODEL_CONFIGS = {
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -191,7 +191,6 @@ export async function runOracle(options, deps = {}) {
|
|
|
191
191
|
(options.model.startsWith('gemini')
|
|
192
192
|
? resolveGeminiModelId(options.model)
|
|
193
193
|
: (modelConfig.apiModel ?? modelConfig.model));
|
|
194
|
-
const headerModelLabel = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
|
|
195
194
|
const requestBody = buildRequestBody({
|
|
196
195
|
modelConfig,
|
|
197
196
|
systemPrompt,
|
|
@@ -205,7 +204,13 @@ export async function runOracle(options, deps = {}) {
|
|
|
205
204
|
const tokenLabel = formatTokenEstimate(estimatedInputTokens, (text) => (richTty ? chalk.green(text) : text));
|
|
206
205
|
const fileLabel = richTty ? chalk.magenta(fileCount.toString()) : fileCount.toString();
|
|
207
206
|
const filesPhrase = fileCount === 0 ? 'no files' : `${fileLabel} files`;
|
|
208
|
-
const
|
|
207
|
+
const headerModelLabelBase = richTty ? chalk.cyan(modelConfig.model) : modelConfig.model;
|
|
208
|
+
const headerModelSuffix = effectiveModelId !== modelConfig.model
|
|
209
|
+
? richTty
|
|
210
|
+
? chalk.gray(` (API: ${effectiveModelId})`)
|
|
211
|
+
: ` (API: ${effectiveModelId})`
|
|
212
|
+
: '';
|
|
213
|
+
const headerLine = `Calling ${headerModelLabelBase}${headerModelSuffix} — ${tokenLabel} tokens, ${filesPhrase}.`;
|
|
209
214
|
const shouldReportFiles = (options.filesReport || fileTokenInfo.totalTokens > inputTokenBudget) && fileTokenInfo.stats.length > 0;
|
|
210
215
|
if (!isPreview) {
|
|
211
216
|
if (!options.suppressHeader) {
|
|
@@ -213,9 +218,14 @@ export async function runOracle(options, deps = {}) {
|
|
|
213
218
|
}
|
|
214
219
|
const maskedKey = maskApiKey(apiKey);
|
|
215
220
|
if (maskedKey && options.verbose) {
|
|
216
|
-
const resolvedSuffix = effectiveModelId !== modelConfig.model ? ` (
|
|
221
|
+
const resolvedSuffix = effectiveModelId !== modelConfig.model ? ` (API: ${effectiveModelId})` : '';
|
|
217
222
|
log(dim(`Using ${envVar}=${maskedKey} for model ${modelConfig.model}${resolvedSuffix}`));
|
|
218
223
|
}
|
|
224
|
+
if (!options.suppressHeader &&
|
|
225
|
+
modelConfig.model === 'gpt-5.1-pro' &&
|
|
226
|
+
effectiveModelId === 'gpt-5.2-pro') {
|
|
227
|
+
log(dim('Note: `gpt-5.1-pro` is a stable CLI alias; OpenAI API uses `gpt-5.2-pro`.'));
|
|
228
|
+
}
|
|
219
229
|
if (baseUrl) {
|
|
220
230
|
log(dim(`Base URL: ${formatBaseUrlForLog(baseUrl)}`));
|
|
221
231
|
}
|
|
@@ -442,10 +452,11 @@ export async function runOracle(options, deps = {}) {
|
|
|
442
452
|
if (response.id && response.status === 'in_progress') {
|
|
443
453
|
const polishingStart = now();
|
|
444
454
|
const pollIntervalMs = 2_000;
|
|
445
|
-
const maxWaitMs =
|
|
455
|
+
const maxWaitMs = 180_000;
|
|
446
456
|
log(chalk.dim('Response still in_progress; polling until completion...'));
|
|
447
457
|
// Short polling loop — we don't want to hang forever, just catch late finalization.
|
|
448
458
|
while (now() - polishingStart < maxWaitMs) {
|
|
459
|
+
throwIfTimedOut();
|
|
449
460
|
await wait(pollIntervalMs);
|
|
450
461
|
const refreshed = await clientInstance.responses.retrieve(response.id);
|
|
451
462
|
if (refreshed.status === 'completed') {
|