@steipete/oracle 0.13.0 → 0.14.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 +22 -11
- package/dist/bin/oracle-cli.js +71 -71
- package/dist/src/browser/actions/assistantResponse.js +93 -10
- package/dist/src/browser/actions/deepResearch.js +180 -52
- package/dist/src/browser/actions/modelSelection.js +357 -40
- package/dist/src/browser/actions/navigation.js +294 -65
- package/dist/src/browser/actions/promptComposer.js +50 -4
- package/dist/src/browser/actions/thinkingTime.js +517 -105
- package/dist/src/browser/artifacts.js +19 -0
- package/dist/src/browser/chatgptFiles.js +797 -0
- package/dist/src/browser/chatgptImages.js +256 -27
- package/dist/src/browser/chromeLifecycle.js +4 -0
- package/dist/src/browser/config.js +2 -0
- package/dist/src/browser/index.js +404 -41
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/policies.js +2 -6
- package/dist/src/browser/projectSourcesRunner.js +16 -3
- package/dist/src/browser/prompt.js +161 -36
- package/dist/src/browser/reattach.js +6 -2
- package/dist/src/browser/reattachability.js +22 -10
- package/dist/src/browser/recoverConversation.js +73 -0
- package/dist/src/browser/sessionRunner.js +4 -1
- package/dist/src/cli/browserConfig.js +6 -2
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/browserTabs.js +129 -54
- package/dist/src/cli/followup.js +72 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +24 -0
- package/dist/src/cli/reattachGuidance.js +8 -0
- package/dist/src/cli/runOptions.js +2 -12
- package/dist/src/cli/sessionCommand.js +4 -0
- package/dist/src/cli/sessionDisplay.js +17 -3
- package/dist/src/cli/sessionLineage.js +7 -4
- package/dist/src/cli/sessionRunner.js +16 -1
- package/dist/src/gemini-web/client.js +5 -10
- package/dist/src/gemini-web/executor.js +1 -24
- package/dist/src/gemini-web/models.js +83 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/chatgptImage.js +132 -0
- package/dist/src/mcp/tools/consult.js +270 -175
- package/dist/src/mcp/types.js +13 -2
- package/dist/src/mcp/utils.js +87 -1
- package/dist/src/oracle/config.js +27 -1
- package/dist/src/oracle/files.js +4 -6
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/markdown.js +36 -3
- package/dist/src/oracle/modelResolver.js +21 -13
- package/dist/src/oracle/promptAssembly.js +2 -4
- package/dist/src/oracle/request.js +3 -5
- package/dist/src/oracle/thinkingTime.js +40 -0
- package/dist/src/oracle/tokenStats.js +5 -1
- package/dist/src/oracle.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +47 -58
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/dist/bin/oracle.js +0 -569
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/windowsCookies.js +0 -219
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
- package/vendor/oracle-notifier/README.md +0 -26
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
|
-
import chromeCookies from 'chrome-cookies-secure';
|
|
7
|
-
import { COOKIE_URLS } from './constants.js';
|
|
8
|
-
import { ensureCookiesDirForFallback } from './windowsCookies.js';
|
|
9
|
-
const COOKIE_READ_TIMEOUT_MS = readDuration('ORACLE_COOKIE_LOAD_TIMEOUT_MS', 5_000);
|
|
10
|
-
const KEYCHAIN_PROBE_TIMEOUT_MS = readDuration('ORACLE_KEYCHAIN_PROBE_TIMEOUT_MS', 3_000);
|
|
11
|
-
const MAC_KEYCHAIN_LABELS = loadKeychainLabels();
|
|
12
|
-
export async function loadChromeCookies({ targetUrl, profile, explicitCookiePath, filterNames, }) {
|
|
13
|
-
if (process.platform === 'win32') {
|
|
14
|
-
throw new Error('Cookie sync is disabled on Windows; use --browser-manual-login (persistent profile) or inline cookies instead.');
|
|
15
|
-
}
|
|
16
|
-
const urlsToCheck = Array.from(new Set([stripQuery(targetUrl), ...COOKIE_URLS]));
|
|
17
|
-
const merged = new Map();
|
|
18
|
-
const cookieFile = await resolveCookieFilePath({ explicitPath: explicitCookiePath, profile });
|
|
19
|
-
const cookiesPath = await materializeCookieFile(cookieFile); // returns the copied file (or source on non-Windows)
|
|
20
|
-
const fallbackDir = await ensureCookiesDirForFallback(cookiesPath);
|
|
21
|
-
if (process.env.ORACLE_DEBUG_COOKIES === '1') {
|
|
22
|
-
// eslint-disable-next-line no-console
|
|
23
|
-
console.log(`[cookies] resolved cookie file: ${cookiesPath}`);
|
|
24
|
-
console.log(`[cookies] fallback dir for chrome-cookies-secure: ${fallbackDir}`);
|
|
25
|
-
}
|
|
26
|
-
await ensureMacKeychainReadable();
|
|
27
|
-
for (const url of urlsToCheck) {
|
|
28
|
-
let raw;
|
|
29
|
-
try {
|
|
30
|
-
const pathForSecure = await adaptPathForChromeCookies(fallbackDir);
|
|
31
|
-
raw = await settleWithTimeout(chromeCookies.getCookiesPromised(url, 'puppeteer', pathForSecure), COOKIE_READ_TIMEOUT_MS, `Timed out reading Chrome cookies from ${pathForSecure} (after ${COOKIE_READ_TIMEOUT_MS} ms)`);
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
-
throw new Error(`Failed to load Chrome cookies for ${url}: ${message}`);
|
|
36
|
-
}
|
|
37
|
-
if (!Array.isArray(raw))
|
|
38
|
-
continue;
|
|
39
|
-
const fallbackHost = new URL(url).hostname;
|
|
40
|
-
for (const cookie of raw) {
|
|
41
|
-
if (filterNames && filterNames.size > 0 && !filterNames.has(cookie.name))
|
|
42
|
-
continue;
|
|
43
|
-
const normalized = normalizeCookie(cookie, fallbackHost);
|
|
44
|
-
if (!normalized)
|
|
45
|
-
continue;
|
|
46
|
-
const key = `${normalized.domain ?? fallbackHost}:${normalized.name}`;
|
|
47
|
-
if (!merged.has(key)) {
|
|
48
|
-
merged.set(key, normalized);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return Array.from(merged.values());
|
|
53
|
-
}
|
|
54
|
-
async function ensureMacKeychainReadable() {
|
|
55
|
-
if (process.platform === 'win32') {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
// chrome-cookies-secure can hang forever when the platform keyring rejects access (e.g., SSH/no GUI).
|
|
59
|
-
// Probe the keyring ourselves with a timeout so callers fail fast instead of blocking the run.
|
|
60
|
-
let keytar = null;
|
|
61
|
-
let keytarPath = null;
|
|
62
|
-
try {
|
|
63
|
-
const require = createRequire(import.meta.url);
|
|
64
|
-
keytarPath = require.resolve('keytar');
|
|
65
|
-
const keytarModule = await import('keytar');
|
|
66
|
-
keytar = (keytarModule.default ?? keytarModule);
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
const base = error instanceof Error ? error.message : String(error);
|
|
70
|
-
const rebuildHint = keytarPath
|
|
71
|
-
? ` You may need to rebuild keytar: PYTHON=/usr/bin/python3 /Users/steipete/Projects/oracle/runner npx node-gyp rebuild (run inside ${path.dirname(keytarPath)}).`
|
|
72
|
-
: '';
|
|
73
|
-
throw new Error(`Failed to load keytar for secure cookie copy (${base}). Install keyring deps (macOS Keychain / libsecret) or rerun with --render --copy to paste into ChatGPT manually.${rebuildHint}`);
|
|
74
|
-
}
|
|
75
|
-
const password = await settleWithTimeout(findKeychainPassword(keytar, MAC_KEYCHAIN_LABELS), KEYCHAIN_PROBE_TIMEOUT_MS, `Timed out reading macOS Keychain while looking up Chrome Safe Storage (after ${KEYCHAIN_PROBE_TIMEOUT_MS} ms). Unlock the login keychain or start oracle serve from a GUI session.`);
|
|
76
|
-
if (!password) {
|
|
77
|
-
throw new Error('macOS Keychain denied access to Chrome cookies. Unlock the login keychain or run oracle serve from a GUI session, then retry.');
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async function findKeychainPassword(keytar, labels) {
|
|
81
|
-
let lastError = null;
|
|
82
|
-
for (const label of labels) {
|
|
83
|
-
try {
|
|
84
|
-
const value = await keytar.getPassword(label.service, label.account);
|
|
85
|
-
if (value)
|
|
86
|
-
return value;
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (lastError) {
|
|
93
|
-
throw lastError;
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
function settleWithTimeout(promise, timeoutMs, timeoutMessage) {
|
|
98
|
-
return new Promise((resolve, reject) => {
|
|
99
|
-
const timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
100
|
-
timer.unref?.();
|
|
101
|
-
promise.then((value) => {
|
|
102
|
-
clearTimeout(timer);
|
|
103
|
-
resolve(value);
|
|
104
|
-
}, (error) => {
|
|
105
|
-
clearTimeout(timer);
|
|
106
|
-
reject(error);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
function normalizeCookie(cookie, fallbackHost) {
|
|
111
|
-
if (!cookie?.name)
|
|
112
|
-
return null;
|
|
113
|
-
const domain = cookie.domain?.startsWith('.') ? cookie.domain.slice(1) : cookie.domain ?? fallbackHost;
|
|
114
|
-
const expires = normalizeExpiration(cookie.expires);
|
|
115
|
-
const secure = typeof cookie.Secure === 'boolean' ? cookie.Secure : true;
|
|
116
|
-
const httpOnly = typeof cookie.HttpOnly === 'boolean' ? cookie.HttpOnly : false;
|
|
117
|
-
return {
|
|
118
|
-
name: cookie.name,
|
|
119
|
-
value: cleanValue(cookie.value ?? ''),
|
|
120
|
-
domain,
|
|
121
|
-
path: cookie.path ?? '/',
|
|
122
|
-
expires,
|
|
123
|
-
secure,
|
|
124
|
-
httpOnly,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function cleanValue(value) {
|
|
128
|
-
let i = 0;
|
|
129
|
-
while (i < value.length && value.charCodeAt(i) < 0x20)
|
|
130
|
-
i += 1;
|
|
131
|
-
return value.slice(i);
|
|
132
|
-
}
|
|
133
|
-
function normalizeExpiration(expires) {
|
|
134
|
-
if (!expires || Number.isNaN(expires)) {
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
const value = Number(expires);
|
|
138
|
-
if (value <= 0)
|
|
139
|
-
return undefined;
|
|
140
|
-
if (value > 1_000_000_000_000) {
|
|
141
|
-
return Math.round(value / 1_000_000 - 11644473600);
|
|
142
|
-
}
|
|
143
|
-
if (value > 1_000_000_000) {
|
|
144
|
-
return Math.round(value / 1000);
|
|
145
|
-
}
|
|
146
|
-
return Math.round(value);
|
|
147
|
-
}
|
|
148
|
-
async function resolveCookieFilePath({ explicitPath, profile, }) {
|
|
149
|
-
if (explicitPath && explicitPath.trim().length > 0) {
|
|
150
|
-
return ensureCookieFile(explicitPath);
|
|
151
|
-
}
|
|
152
|
-
if (profile && looksLikePath(profile)) {
|
|
153
|
-
return ensureCookieFile(profile);
|
|
154
|
-
}
|
|
155
|
-
const profileName = profile && profile.trim().length > 0 ? profile : 'Default';
|
|
156
|
-
const baseDir = await defaultProfileRoot();
|
|
157
|
-
return ensureCookieFile(path.join(baseDir, profileName));
|
|
158
|
-
}
|
|
159
|
-
async function adaptPathForChromeCookies(resolved) {
|
|
160
|
-
const stat = await fs.stat(resolved).catch(() => null);
|
|
161
|
-
if (stat?.isFile()) {
|
|
162
|
-
// chrome-cookies-secure appends "Cookies" when given a directory; if we already have the file, return its directory.
|
|
163
|
-
return path.dirname(resolved);
|
|
164
|
-
}
|
|
165
|
-
return resolved;
|
|
166
|
-
}
|
|
167
|
-
async function ensureCookieFile(inputPath) {
|
|
168
|
-
const expanded = expandPath(inputPath);
|
|
169
|
-
const stat = await fs.stat(expanded).catch(() => null);
|
|
170
|
-
if (!stat) {
|
|
171
|
-
throw new Error(`Unable to locate Chrome cookie DB at ${expanded}`);
|
|
172
|
-
}
|
|
173
|
-
if (stat.isDirectory()) {
|
|
174
|
-
const directFile = path.join(expanded, 'Cookies');
|
|
175
|
-
if (await fileExists(directFile))
|
|
176
|
-
return directFile;
|
|
177
|
-
const networkFile = path.join(expanded, 'Network', 'Cookies');
|
|
178
|
-
if (await fileExists(networkFile))
|
|
179
|
-
return networkFile;
|
|
180
|
-
throw new Error(`No Cookies DB found under ${expanded}`);
|
|
181
|
-
}
|
|
182
|
-
return expanded;
|
|
183
|
-
}
|
|
184
|
-
async function fileExists(candidate) {
|
|
185
|
-
try {
|
|
186
|
-
const stat = await fs.stat(candidate);
|
|
187
|
-
return stat.isFile();
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
function expandPath(input) {
|
|
194
|
-
if (input.startsWith('~/')) {
|
|
195
|
-
return path.join(os.homedir(), input.slice(2));
|
|
196
|
-
}
|
|
197
|
-
return path.isAbsolute(input) ? input : path.resolve(process.cwd(), input);
|
|
198
|
-
}
|
|
199
|
-
function looksLikePath(value) {
|
|
200
|
-
return value.includes('/') || value.includes('\\');
|
|
201
|
-
}
|
|
202
|
-
async function materializeCookieFile(sourcePath) {
|
|
203
|
-
if (process.platform !== 'win32')
|
|
204
|
-
return sourcePath;
|
|
205
|
-
// Chrome can keep the Cookies DB locked; copy to a temp file so sqlite can open it reliably.
|
|
206
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookies-'));
|
|
207
|
-
const tempPath = path.join(tempDir, 'Cookies');
|
|
208
|
-
try {
|
|
209
|
-
await fs.copyFile(sourcePath, tempPath);
|
|
210
|
-
return tempPath;
|
|
211
|
-
}
|
|
212
|
-
catch (_error) {
|
|
213
|
-
// Fall back to the original path if the copy fails; upstream error handling will surface issues.
|
|
214
|
-
return sourcePath;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
async function defaultProfileRoot() {
|
|
218
|
-
const candidates = [];
|
|
219
|
-
if (process.platform === 'darwin') {
|
|
220
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome'), path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge'), path.join(os.homedir(), 'Library', 'Application Support', 'Chromium'));
|
|
221
|
-
}
|
|
222
|
-
else if (process.platform === 'linux') {
|
|
223
|
-
if (isWsl()) {
|
|
224
|
-
const windowsHomes = [process.env.USERPROFILE, process.env.WIN_HOME, process.env.HOME?.replace('/home', '/mnt/c/Users')]
|
|
225
|
-
.filter((p) => Boolean(p))
|
|
226
|
-
.map((p) => p.replace(/\\/g, '/'));
|
|
227
|
-
const wslCandidates = [];
|
|
228
|
-
for (const home of windowsHomes) {
|
|
229
|
-
const normalized = home.startsWith('/mnt/') ? home : `/mnt/c/Users/${path.basename(home)}`;
|
|
230
|
-
wslCandidates.push(path.join(normalized, 'AppData', 'Local', 'Google', 'Chrome', 'User Data'), path.join(normalized, 'AppData', 'Local', 'Microsoft', 'Edge', 'User Data'));
|
|
231
|
-
}
|
|
232
|
-
// Ensure we don't pick a non-user Default profile ahead of real ones.
|
|
233
|
-
for (const candidate of wslCandidates) {
|
|
234
|
-
const hasProfile = existsSync(path.join(candidate, 'Default'));
|
|
235
|
-
if (hasProfile) {
|
|
236
|
-
candidates.push(candidate);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
candidates.push(path.join(os.homedir(), '.config', 'google-chrome'), path.join(os.homedir(), '.config', 'microsoft-edge'), path.join(os.homedir(), '.config', 'chromium'),
|
|
241
|
-
// Snap Chromium profiles
|
|
242
|
-
path.join(os.homedir(), 'snap', 'chromium', 'common', 'chromium'), path.join(os.homedir(), 'snap', 'chromium', 'current', 'chromium'));
|
|
243
|
-
}
|
|
244
|
-
else if (process.platform === 'win32') {
|
|
245
|
-
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
246
|
-
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data'), path.join(localAppData, 'Microsoft', 'Edge', 'User Data'), path.join(localAppData, 'Chromium', 'User Data'));
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
250
|
-
}
|
|
251
|
-
for (const candidate of candidates) {
|
|
252
|
-
if (existsSync(candidate)) {
|
|
253
|
-
return candidate;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
// fallback: first candidate even if missing; upstream will throw clearer error
|
|
257
|
-
return candidates[0];
|
|
258
|
-
}
|
|
259
|
-
function stripQuery(url) {
|
|
260
|
-
try {
|
|
261
|
-
const parsed = new URL(url);
|
|
262
|
-
parsed.hash = '';
|
|
263
|
-
parsed.search = '';
|
|
264
|
-
return parsed.toString();
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
return url;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
function readDuration(envKey, fallback) {
|
|
271
|
-
const raw = process.env[envKey];
|
|
272
|
-
if (!raw)
|
|
273
|
-
return fallback;
|
|
274
|
-
const parsed = Number.parseInt(raw, 10);
|
|
275
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
276
|
-
}
|
|
277
|
-
function isWsl() {
|
|
278
|
-
if (process.platform !== 'linux')
|
|
279
|
-
return false;
|
|
280
|
-
return Boolean(process.env.WSL_DISTRO_NAME || os.release().toLowerCase().includes('microsoft'));
|
|
281
|
-
}
|
|
282
|
-
function loadKeychainLabels() {
|
|
283
|
-
const defaults = [
|
|
284
|
-
{ service: 'Chrome Safe Storage', account: 'Chrome' },
|
|
285
|
-
{ service: 'Chromium Safe Storage', account: 'Chromium' },
|
|
286
|
-
{ service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
|
|
287
|
-
{ service: 'Brave Safe Storage', account: 'Brave' },
|
|
288
|
-
{ service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
|
|
289
|
-
];
|
|
290
|
-
const rawEnv = process.env.ORACLE_KEYCHAIN_LABELS;
|
|
291
|
-
if (!rawEnv)
|
|
292
|
-
return defaults;
|
|
293
|
-
try {
|
|
294
|
-
const parsed = JSON.parse(rawEnv);
|
|
295
|
-
if (!Array.isArray(parsed))
|
|
296
|
-
return defaults;
|
|
297
|
-
const envLabels = parsed
|
|
298
|
-
.map((entry) => (entry && typeof entry === 'object' ? entry : null))
|
|
299
|
-
.filter((entry) => Boolean(entry?.service && entry?.account));
|
|
300
|
-
return envLabels.length ? [...envLabels, ...defaults] : defaults;
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
303
|
-
return defaults;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
307
|
-
export const __test__ = {
|
|
308
|
-
normalizeExpiration,
|
|
309
|
-
cleanValue,
|
|
310
|
-
looksLikePath,
|
|
311
|
-
defaultProfileRoot,
|
|
312
|
-
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
const defaultLabels = [
|
|
2
|
-
{ service: 'Chrome Safe Storage', account: 'Chrome' },
|
|
3
|
-
{ service: 'Chromium Safe Storage', account: 'Chromium' },
|
|
4
|
-
{ service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
|
|
5
|
-
{ service: 'Brave Safe Storage', account: 'Brave' },
|
|
6
|
-
{ service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
|
|
7
|
-
];
|
|
8
|
-
function loadEnvLabels() {
|
|
9
|
-
const raw = process.env.ORACLE_KEYCHAIN_LABELS;
|
|
10
|
-
if (!raw)
|
|
11
|
-
return [];
|
|
12
|
-
try {
|
|
13
|
-
const parsed = JSON.parse(raw);
|
|
14
|
-
if (Array.isArray(parsed)) {
|
|
15
|
-
return parsed
|
|
16
|
-
.map((entry) => (entry && typeof entry === 'object' ? entry : null))
|
|
17
|
-
.filter((entry) => Boolean(entry?.service && entry?.account));
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
// ignore invalid env payload
|
|
22
|
-
}
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
const fallbackLabels = [...loadEnvLabels(), ...defaultLabels];
|
|
26
|
-
const disableKeytar = process.env.ORACLE_DISABLE_KEYTAR === '1' || process.env.CI === 'true';
|
|
27
|
-
let keytar;
|
|
28
|
-
if (disableKeytar) {
|
|
29
|
-
keytar = {
|
|
30
|
-
getPassword: async () => null,
|
|
31
|
-
setPassword: async () => undefined,
|
|
32
|
-
deletePassword: async () => false,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
const keytarModule = await import('keytar');
|
|
37
|
-
keytar = (keytarModule.default ?? keytarModule);
|
|
38
|
-
const originalGetPassword = keytar.getPassword.bind(keytar);
|
|
39
|
-
keytar.getPassword = async (service, account) => {
|
|
40
|
-
const primary = await originalGetPassword(service, account);
|
|
41
|
-
if (primary) {
|
|
42
|
-
return primary;
|
|
43
|
-
}
|
|
44
|
-
for (const label of fallbackLabels) {
|
|
45
|
-
if (label.service === service && label.account === account) {
|
|
46
|
-
continue; // already tried
|
|
47
|
-
}
|
|
48
|
-
const value = await originalGetPassword(label.service, label.account);
|
|
49
|
-
if (value) {
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
export default keytar;
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import { existsSync, promises as fsp } from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import sqlite3 from 'sqlite3';
|
|
7
|
-
import { createRequire } from 'node:module';
|
|
8
|
-
import { spawnSync } from 'node:child_process';
|
|
9
|
-
let cachedUnprotect = null;
|
|
10
|
-
function getUnprotectData() {
|
|
11
|
-
if (cachedUnprotect) {
|
|
12
|
-
return cachedUnprotect;
|
|
13
|
-
}
|
|
14
|
-
try {
|
|
15
|
-
// win-dpapi is CommonJS; require it explicitly and support both named/default shapes.
|
|
16
|
-
const dpapiModule = createRequire(import.meta.url)('win-dpapi');
|
|
17
|
-
const unprotect = dpapiModule?.Dpapi?.unprotectData ??
|
|
18
|
-
dpapiModule?.default?.unprotectData ??
|
|
19
|
-
((_) => {
|
|
20
|
-
throw new Error('win-dpapi unprotectData not available');
|
|
21
|
-
});
|
|
22
|
-
cachedUnprotect = unprotect;
|
|
23
|
-
return unprotect;
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
if (process.platform !== 'win32') {
|
|
27
|
-
// On macOS/Linux we don't need DPAPI; return a function that makes misuse obvious.
|
|
28
|
-
cachedUnprotect = () => {
|
|
29
|
-
throw new Error('win-dpapi is unavailable on non-Windows platforms');
|
|
30
|
-
};
|
|
31
|
-
return cachedUnprotect;
|
|
32
|
-
}
|
|
33
|
-
throw error;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export async function loadWindowsCookies(dbPath, filterNames) {
|
|
37
|
-
if (process.platform !== 'win32') {
|
|
38
|
-
throw new Error('loadWindowsCookies is only supported on Windows');
|
|
39
|
-
}
|
|
40
|
-
const localStatePath = await locateLocalState(dbPath);
|
|
41
|
-
const cookiesCopy = await copyLockedFile(dbPath, 'Cookies'); // name the copy exactly "Cookies"
|
|
42
|
-
const cookiesDirForFallback = await ensureCookiesDirForFallback(cookiesCopy);
|
|
43
|
-
const localStateCopy = await copyLockedFile(localStatePath, 'Local State');
|
|
44
|
-
const aesKey = await extractWindowsAesKey(localStateCopy);
|
|
45
|
-
const rows = await readChromeCookiesDb(cookiesCopy, filterNames);
|
|
46
|
-
const cookies = [];
|
|
47
|
-
for (const row of rows) {
|
|
48
|
-
const enc = row.encrypted_value && row.encrypted_value.length > 0 ? row.encrypted_value : Buffer.from(row.value ?? '', 'utf8');
|
|
49
|
-
const decrypted = enc.length > 0 ? decryptCookie(enc, aesKey) : '';
|
|
50
|
-
cookies.push({
|
|
51
|
-
name: row.name,
|
|
52
|
-
value: decrypted,
|
|
53
|
-
domain: row.host_key?.startsWith('.') ? row.host_key.slice(1) : row.host_key,
|
|
54
|
-
path: row.path ?? '/',
|
|
55
|
-
expires: normalizeExpiration(row.expires_utc),
|
|
56
|
-
secure: Boolean(row.is_secure),
|
|
57
|
-
httpOnly: Boolean(row.is_httponly),
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
if (process.env.ORACLE_DEBUG_COOKIES === '1') {
|
|
61
|
-
// eslint-disable-next-line no-console
|
|
62
|
-
console.log(`[cookies] windows decrypt decoded ${cookies.length} cookies from ${cookiesCopy} (fallback dir ${cookiesDirForFallback})`);
|
|
63
|
-
}
|
|
64
|
-
return cookies.filter((c) => c.value);
|
|
65
|
-
}
|
|
66
|
-
function decryptCookie(value, aesKey) {
|
|
67
|
-
const prefix = value.slice(0, 3).toString();
|
|
68
|
-
// Chrome prefixes AES-GCM encrypted cookies with version markers like v10/v11/v20; treat all v** the same.
|
|
69
|
-
if (/^v\d{2}$/u.test(prefix)) {
|
|
70
|
-
const iv = value.slice(3, 15);
|
|
71
|
-
const tag = value.slice(value.length - 16);
|
|
72
|
-
const data = value.slice(15, value.length - 16);
|
|
73
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, iv);
|
|
74
|
-
decipher.setAuthTag(tag);
|
|
75
|
-
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
76
|
-
return decrypted.toString('utf8');
|
|
77
|
-
}
|
|
78
|
-
const unprotectData = getUnprotectData();
|
|
79
|
-
const unprotected = unprotectData(value, undefined, 'CurrentUser');
|
|
80
|
-
return Buffer.from(unprotected).toString('utf8');
|
|
81
|
-
}
|
|
82
|
-
async function locateLocalState(dbPath) {
|
|
83
|
-
// Prefer sibling Local State near the profile path; fall back to default location.
|
|
84
|
-
const guess = path.resolve(path.join(path.dirname(dbPath), '..', 'Local State'));
|
|
85
|
-
if (existsSync(guess))
|
|
86
|
-
return guess;
|
|
87
|
-
const localState = path.join(os.homedir(), 'AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Local State');
|
|
88
|
-
if (existsSync(localState))
|
|
89
|
-
return localState;
|
|
90
|
-
throw new Error('Chrome Local State not found for AES key');
|
|
91
|
-
}
|
|
92
|
-
async function extractWindowsAesKey(localStatePath) {
|
|
93
|
-
const raw = await fs.readFile(localStatePath, 'utf8');
|
|
94
|
-
const state = JSON.parse(raw);
|
|
95
|
-
const encKeyB64 = state?.os_crypt?.encrypted_key;
|
|
96
|
-
if (!encKeyB64)
|
|
97
|
-
throw new Error('encrypted_key missing in Local State');
|
|
98
|
-
const encKey = Buffer.from(encKeyB64, 'base64');
|
|
99
|
-
const dpapiBlob = encKey.slice(5); // strip "DPAPI"
|
|
100
|
-
const unprotectData = getUnprotectData();
|
|
101
|
-
const unprotected = unprotectData(dpapiBlob, undefined, 'CurrentUser');
|
|
102
|
-
return Buffer.from(unprotected);
|
|
103
|
-
}
|
|
104
|
-
async function readChromeCookiesDb(dbPath, filterNames) {
|
|
105
|
-
const db = await openSqlite(dbPath);
|
|
106
|
-
const placeholders = filterNames && filterNames.size > 0 ? `AND name IN (${Array.from(filterNames).map(() => '?').join(',')})` : '';
|
|
107
|
-
const params = filterNames ? Array.from(filterNames) : [];
|
|
108
|
-
const sql = `SELECT name,value,encrypted_value,host_key,path,expires_utc,is_secure,is_httponly FROM cookies WHERE host_key LIKE '%chatgpt.com%' ${placeholders}`;
|
|
109
|
-
try {
|
|
110
|
-
return await allSqlite(db, sql, params);
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
db.close();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function openSqlite(dbPath) {
|
|
117
|
-
return new Promise((resolve, reject) => {
|
|
118
|
-
const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
|
|
119
|
-
if (err)
|
|
120
|
-
reject(err);
|
|
121
|
-
else
|
|
122
|
-
resolve(db);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
async function copyLockedFile(sourcePath, targetName) {
|
|
127
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookie-'));
|
|
128
|
-
const tempPath = targetName ? path.join(tempDir, targetName) : path.join(tempDir, path.basename(sourcePath));
|
|
129
|
-
const psScript = `
|
|
130
|
-
$src = '${sourcePath.replace(/'/g, "''")}'
|
|
131
|
-
$dst = '${tempPath.replace(/'/g, "''")}'
|
|
132
|
-
try {
|
|
133
|
-
$fs = [System.IO.File]::Open($src, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
|
134
|
-
$bytes = New-Object byte[] $fs.Length
|
|
135
|
-
$null = $fs.Read($bytes, 0, $fs.Length)
|
|
136
|
-
$fs.Close()
|
|
137
|
-
[System.IO.File]::WriteAllBytes($dst, $bytes)
|
|
138
|
-
exit 0
|
|
139
|
-
} catch {
|
|
140
|
-
exit 1
|
|
141
|
-
}
|
|
142
|
-
`;
|
|
143
|
-
const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
|
|
144
|
-
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], {
|
|
145
|
-
timeout: 15000,
|
|
146
|
-
encoding: 'utf8',
|
|
147
|
-
});
|
|
148
|
-
if (result.status !== 0) {
|
|
149
|
-
// Fallback: best-effort normal copy
|
|
150
|
-
await fs.copyFile(sourcePath, tempPath);
|
|
151
|
-
}
|
|
152
|
-
if (process.env.ORACLE_DEBUG_COOKIES === '1') {
|
|
153
|
-
// eslint-disable-next-line no-console
|
|
154
|
-
console.log(`[cookies] copied locked file ${sourcePath} -> ${tempPath}`);
|
|
155
|
-
}
|
|
156
|
-
return tempPath;
|
|
157
|
-
}
|
|
158
|
-
// Create a directory containing a file named "Cookies" for chrome-cookies-secure fallback
|
|
159
|
-
export async function ensureCookiesDirForFallback(cookieFile) {
|
|
160
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookies-secure-'));
|
|
161
|
-
const target = path.join(dir, 'Cookies');
|
|
162
|
-
await fs.copyFile(cookieFile, target);
|
|
163
|
-
return dir;
|
|
164
|
-
}
|
|
165
|
-
function allSqlite(db, sql, params) {
|
|
166
|
-
return new Promise((resolve, reject) => {
|
|
167
|
-
db.all(sql, params, (err, rows) => {
|
|
168
|
-
if (err)
|
|
169
|
-
reject(err);
|
|
170
|
-
else
|
|
171
|
-
resolve((rows ?? []));
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
function normalizeExpiration(expires) {
|
|
176
|
-
if (!expires || Number.isNaN(expires)) {
|
|
177
|
-
return undefined;
|
|
178
|
-
}
|
|
179
|
-
const value = Number(expires);
|
|
180
|
-
if (value <= 0)
|
|
181
|
-
return undefined;
|
|
182
|
-
if (value > 1_000_000_000_000) {
|
|
183
|
-
return Math.round(value / 1_000_000 - 11644473600);
|
|
184
|
-
}
|
|
185
|
-
if (value > 1_000_000_000) {
|
|
186
|
-
return Math.round(value / 1000);
|
|
187
|
-
}
|
|
188
|
-
return Math.round(value);
|
|
189
|
-
}
|
|
190
|
-
export async function materializeCookieFile(sourcePath) {
|
|
191
|
-
if (process.platform !== 'win32')
|
|
192
|
-
return sourcePath;
|
|
193
|
-
const resolved = await resolveDirectoryCandidate(sourcePath);
|
|
194
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookies-'));
|
|
195
|
-
const tempPath = path.join(tempDir, 'Cookies');
|
|
196
|
-
await fs.copyFile(resolved, tempPath);
|
|
197
|
-
return tempPath;
|
|
198
|
-
}
|
|
199
|
-
async function resolveDirectoryCandidate(inputPath) {
|
|
200
|
-
const stat = await fs.stat(inputPath).catch(() => null);
|
|
201
|
-
if (!stat?.isDirectory())
|
|
202
|
-
return inputPath;
|
|
203
|
-
const network = path.join(inputPath, 'Network', 'Cookies');
|
|
204
|
-
const direct = path.join(inputPath, 'Cookies');
|
|
205
|
-
if (await fileExists(network))
|
|
206
|
-
return network;
|
|
207
|
-
if (await fileExists(direct))
|
|
208
|
-
return direct;
|
|
209
|
-
return inputPath;
|
|
210
|
-
}
|
|
211
|
-
async function fileExists(candidate) {
|
|
212
|
-
try {
|
|
213
|
-
const stat = await fsp.stat(candidate);
|
|
214
|
-
return stat.isFile();
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
Binary file
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>CFBundleIdentifier</key>
|
|
6
|
-
<string>com.steipete.oracle.notifier</string>
|
|
7
|
-
<key>CFBundleName</key>
|
|
8
|
-
<string>OracleNotifier</string>
|
|
9
|
-
<key>CFBundleDisplayName</key>
|
|
10
|
-
<string>Oracle Notifier</string>
|
|
11
|
-
<key>CFBundleExecutable</key>
|
|
12
|
-
<string>OracleNotifier</string>
|
|
13
|
-
<key>CFBundleIconFile</key>
|
|
14
|
-
<string>OracleIcon</string>
|
|
15
|
-
<key>CFBundlePackageType</key>
|
|
16
|
-
<string>APPL</string>
|
|
17
|
-
<key>LSMinimumSystemVersion</key>
|
|
18
|
-
<string>13.0</string>
|
|
19
|
-
</dict>
|
|
20
|
-
</plist>
|
|
Binary file
|