@steipete/oracle 1.0.8 → 1.2.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 +32 -4
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +178 -21
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/markdansi/types/index.js +4 -0
- package/dist/oracle/bin/oracle-cli.js +472 -0
- package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
- package/dist/oracle/src/browser/actions/attachments.js +82 -0
- package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
- package/dist/oracle/src/browser/actions/navigation.js +75 -0
- package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
- package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
- package/dist/oracle/src/browser/config.js +33 -0
- package/dist/oracle/src/browser/constants.js +40 -0
- package/dist/oracle/src/browser/cookies.js +210 -0
- package/dist/oracle/src/browser/domDebug.js +36 -0
- package/dist/oracle/src/browser/index.js +331 -0
- package/dist/oracle/src/browser/pageActions.js +5 -0
- package/dist/oracle/src/browser/prompt.js +88 -0
- package/dist/oracle/src/browser/promptSummary.js +20 -0
- package/dist/oracle/src/browser/sessionRunner.js +80 -0
- package/dist/oracle/src/browser/types.js +1 -0
- package/dist/oracle/src/browser/utils.js +62 -0
- package/dist/oracle/src/browserMode.js +1 -0
- package/dist/oracle/src/cli/browserConfig.js +44 -0
- package/dist/oracle/src/cli/dryRun.js +59 -0
- package/dist/oracle/src/cli/engine.js +17 -0
- package/dist/oracle/src/cli/errorUtils.js +9 -0
- package/dist/oracle/src/cli/help.js +70 -0
- package/dist/oracle/src/cli/markdownRenderer.js +15 -0
- package/dist/oracle/src/cli/options.js +103 -0
- package/dist/oracle/src/cli/promptRequirement.js +14 -0
- package/dist/oracle/src/cli/rootAlias.js +30 -0
- package/dist/oracle/src/cli/sessionCommand.js +77 -0
- package/dist/oracle/src/cli/sessionDisplay.js +270 -0
- package/dist/oracle/src/cli/sessionRunner.js +94 -0
- package/dist/oracle/src/heartbeat.js +43 -0
- package/dist/oracle/src/oracle/client.js +48 -0
- package/dist/oracle/src/oracle/config.js +29 -0
- package/dist/oracle/src/oracle/errors.js +101 -0
- package/dist/oracle/src/oracle/files.js +220 -0
- package/dist/oracle/src/oracle/format.js +33 -0
- package/dist/oracle/src/oracle/fsAdapter.js +7 -0
- package/dist/oracle/src/oracle/oscProgress.js +60 -0
- package/dist/oracle/src/oracle/request.js +48 -0
- package/dist/oracle/src/oracle/run.js +444 -0
- package/dist/oracle/src/oracle/tokenStats.js +39 -0
- package/dist/oracle/src/oracle/types.js +1 -0
- package/dist/oracle/src/oracle.js +9 -0
- package/dist/oracle/src/sessionManager.js +205 -0
- package/dist/oracle/src/version.js +39 -0
- package/dist/src/browser/actions/modelSelection.js +117 -29
- package/dist/src/browser/cookies.js +1 -1
- package/dist/src/browser/index.js +2 -1
- package/dist/src/browser/prompt.js +6 -5
- package/dist/src/browser/sessionRunner.js +4 -2
- package/dist/src/cli/dryRun.js +41 -5
- package/dist/src/cli/engine.js +7 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/hiddenAliases.js +17 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +223 -0
- package/dist/src/cli/promptRequirement.js +3 -0
- package/dist/src/cli/rootAlias.js +14 -0
- package/dist/src/cli/runOptions.js +29 -0
- package/dist/src/cli/sessionCommand.js +60 -2
- package/dist/src/cli/sessionDisplay.js +222 -10
- package/dist/src/cli/sessionRunner.js +21 -2
- package/dist/src/cli/tui/index.js +436 -0
- package/dist/src/config.js +27 -0
- package/dist/src/mcp/server.js +36 -0
- package/dist/src/mcp/tools/consult.js +158 -0
- package/dist/src/mcp/tools/sessionResources.js +64 -0
- package/dist/src/mcp/tools/sessions.js +106 -0
- package/dist/src/mcp/types.js +17 -0
- package/dist/src/mcp/utils.js +24 -0
- package/dist/src/oracle/files.js +143 -6
- package/dist/src/oracle/oscProgress.js +60 -0
- package/dist/src/oracle/run.js +104 -71
- package/dist/src/oracle/tokenEstimate.js +34 -0
- package/dist/src/sessionManager.js +65 -3
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- 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 +128 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- 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 +20 -0
- 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 +128 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +27 -9
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- 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 +128 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { createWriteStream } from 'node:fs';
|
|
5
|
+
const ORACLE_HOME = process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), '.oracle');
|
|
6
|
+
const SESSIONS_DIR = path.join(ORACLE_HOME, 'sessions');
|
|
7
|
+
const MAX_STATUS_LIMIT = 1000;
|
|
8
|
+
const DEFAULT_SLUG = 'session';
|
|
9
|
+
const MAX_SLUG_WORDS = 5;
|
|
10
|
+
const MIN_CUSTOM_SLUG_WORDS = 3;
|
|
11
|
+
async function ensureDir(dirPath) {
|
|
12
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
export async function ensureSessionStorage() {
|
|
15
|
+
await ensureDir(SESSIONS_DIR);
|
|
16
|
+
}
|
|
17
|
+
function slugify(text, maxWords = MAX_SLUG_WORDS) {
|
|
18
|
+
const normalized = text?.toLowerCase() ?? '';
|
|
19
|
+
const words = normalized.match(/[a-z0-9]+/g) ?? [];
|
|
20
|
+
const trimmed = words.slice(0, maxWords);
|
|
21
|
+
return trimmed.length > 0 ? trimmed.join('-') : DEFAULT_SLUG;
|
|
22
|
+
}
|
|
23
|
+
function countSlugWords(slug) {
|
|
24
|
+
return slug.split('-').filter(Boolean).length;
|
|
25
|
+
}
|
|
26
|
+
function normalizeCustomSlug(candidate) {
|
|
27
|
+
const slug = slugify(candidate, MAX_SLUG_WORDS);
|
|
28
|
+
const wordCount = countSlugWords(slug);
|
|
29
|
+
if (wordCount < MIN_CUSTOM_SLUG_WORDS || wordCount > MAX_SLUG_WORDS) {
|
|
30
|
+
throw new Error(`Custom slug must include between ${MIN_CUSTOM_SLUG_WORDS} and ${MAX_SLUG_WORDS} words.`);
|
|
31
|
+
}
|
|
32
|
+
return slug;
|
|
33
|
+
}
|
|
34
|
+
export function createSessionId(prompt, customSlug) {
|
|
35
|
+
if (customSlug) {
|
|
36
|
+
return normalizeCustomSlug(customSlug);
|
|
37
|
+
}
|
|
38
|
+
return slugify(prompt);
|
|
39
|
+
}
|
|
40
|
+
function sessionDir(id) {
|
|
41
|
+
return path.join(SESSIONS_DIR, id);
|
|
42
|
+
}
|
|
43
|
+
function metaPath(id) {
|
|
44
|
+
return path.join(sessionDir(id), 'session.json');
|
|
45
|
+
}
|
|
46
|
+
function logPath(id) {
|
|
47
|
+
return path.join(sessionDir(id), 'output.log');
|
|
48
|
+
}
|
|
49
|
+
function requestPath(id) {
|
|
50
|
+
return path.join(sessionDir(id), 'request.json');
|
|
51
|
+
}
|
|
52
|
+
async function fileExists(targetPath) {
|
|
53
|
+
try {
|
|
54
|
+
await fs.access(targetPath);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function ensureUniqueSessionId(baseSlug) {
|
|
62
|
+
let candidate = baseSlug;
|
|
63
|
+
let suffix = 2;
|
|
64
|
+
while (await fileExists(sessionDir(candidate))) {
|
|
65
|
+
candidate = `${baseSlug}-${suffix}`;
|
|
66
|
+
suffix += 1;
|
|
67
|
+
}
|
|
68
|
+
return candidate;
|
|
69
|
+
}
|
|
70
|
+
export async function initializeSession(options, cwd) {
|
|
71
|
+
await ensureSessionStorage();
|
|
72
|
+
const baseSlug = createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
|
|
73
|
+
const sessionId = await ensureUniqueSessionId(baseSlug);
|
|
74
|
+
const dir = sessionDir(sessionId);
|
|
75
|
+
await ensureDir(dir);
|
|
76
|
+
const mode = options.mode ?? 'api';
|
|
77
|
+
const browserConfig = options.browserConfig;
|
|
78
|
+
const metadata = {
|
|
79
|
+
id: sessionId,
|
|
80
|
+
createdAt: new Date().toISOString(),
|
|
81
|
+
status: 'pending',
|
|
82
|
+
promptPreview: (options.prompt || '').slice(0, 160),
|
|
83
|
+
model: options.model,
|
|
84
|
+
cwd,
|
|
85
|
+
mode,
|
|
86
|
+
browser: browserConfig ? { config: browserConfig } : undefined,
|
|
87
|
+
options: {
|
|
88
|
+
prompt: options.prompt,
|
|
89
|
+
file: options.file ?? [],
|
|
90
|
+
model: options.model,
|
|
91
|
+
maxInput: options.maxInput,
|
|
92
|
+
system: options.system,
|
|
93
|
+
maxOutput: options.maxOutput,
|
|
94
|
+
silent: options.silent,
|
|
95
|
+
filesReport: options.filesReport,
|
|
96
|
+
slug: sessionId,
|
|
97
|
+
mode,
|
|
98
|
+
browserConfig,
|
|
99
|
+
verbose: options.verbose,
|
|
100
|
+
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
101
|
+
browserInlineFiles: options.browserInlineFiles,
|
|
102
|
+
background: options.background,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
await fs.writeFile(metaPath(sessionId), JSON.stringify(metadata, null, 2), 'utf8');
|
|
106
|
+
await fs.writeFile(requestPath(sessionId), JSON.stringify(metadata.options, null, 2), 'utf8');
|
|
107
|
+
await fs.writeFile(logPath(sessionId), '', 'utf8');
|
|
108
|
+
return metadata;
|
|
109
|
+
}
|
|
110
|
+
export async function readSessionMetadata(sessionId) {
|
|
111
|
+
try {
|
|
112
|
+
const raw = await fs.readFile(metaPath(sessionId), 'utf8');
|
|
113
|
+
return JSON.parse(raw);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export async function updateSessionMetadata(sessionId, updates) {
|
|
120
|
+
const existing = (await readSessionMetadata(sessionId)) ?? { id: sessionId };
|
|
121
|
+
const next = { ...existing, ...updates };
|
|
122
|
+
await fs.writeFile(metaPath(sessionId), JSON.stringify(next, null, 2), 'utf8');
|
|
123
|
+
return next;
|
|
124
|
+
}
|
|
125
|
+
export function createSessionLogWriter(sessionId) {
|
|
126
|
+
const stream = createWriteStream(logPath(sessionId), { flags: 'a' });
|
|
127
|
+
const logLine = (line = '') => {
|
|
128
|
+
stream.write(`${line}\n`);
|
|
129
|
+
};
|
|
130
|
+
const writeChunk = (chunk) => {
|
|
131
|
+
stream.write(chunk);
|
|
132
|
+
return true;
|
|
133
|
+
};
|
|
134
|
+
return { stream, logLine, writeChunk, logPath: logPath(sessionId) };
|
|
135
|
+
}
|
|
136
|
+
export async function listSessionsMetadata() {
|
|
137
|
+
await ensureSessionStorage();
|
|
138
|
+
const entries = await fs.readdir(SESSIONS_DIR).catch(() => []);
|
|
139
|
+
const metas = [];
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const meta = await readSessionMetadata(entry);
|
|
142
|
+
if (meta) {
|
|
143
|
+
metas.push(meta);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return metas.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
147
|
+
}
|
|
148
|
+
export function filterSessionsByRange(metas, { hours = 24, includeAll = false, limit = 100 }) {
|
|
149
|
+
const maxLimit = Math.min(limit, MAX_STATUS_LIMIT);
|
|
150
|
+
let filtered = metas;
|
|
151
|
+
if (!includeAll) {
|
|
152
|
+
const cutoff = Date.now() - hours * 60 * 60 * 1000;
|
|
153
|
+
filtered = metas.filter((meta) => new Date(meta.createdAt).getTime() >= cutoff);
|
|
154
|
+
}
|
|
155
|
+
const limited = filtered.slice(0, maxLimit);
|
|
156
|
+
const truncated = filtered.length > maxLimit;
|
|
157
|
+
return { entries: limited, truncated, total: filtered.length };
|
|
158
|
+
}
|
|
159
|
+
export async function readSessionLog(sessionId) {
|
|
160
|
+
try {
|
|
161
|
+
return await fs.readFile(logPath(sessionId), 'utf8');
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export async function deleteSessionsOlderThan({ hours = 24, includeAll = false, } = {}) {
|
|
168
|
+
await ensureSessionStorage();
|
|
169
|
+
const entries = await fs.readdir(SESSIONS_DIR).catch(() => []);
|
|
170
|
+
if (!entries.length) {
|
|
171
|
+
return { deleted: 0, remaining: 0 };
|
|
172
|
+
}
|
|
173
|
+
const cutoff = includeAll ? Number.NEGATIVE_INFINITY : Date.now() - hours * 60 * 60 * 1000;
|
|
174
|
+
let deleted = 0;
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const dir = sessionDir(entry);
|
|
177
|
+
let createdMs;
|
|
178
|
+
const meta = await readSessionMetadata(entry);
|
|
179
|
+
if (meta?.createdAt) {
|
|
180
|
+
const parsed = Date.parse(meta.createdAt);
|
|
181
|
+
if (!Number.isNaN(parsed)) {
|
|
182
|
+
createdMs = parsed;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (createdMs == null) {
|
|
186
|
+
try {
|
|
187
|
+
const stats = await fs.stat(dir);
|
|
188
|
+
createdMs = stats.birthtimeMs || stats.mtimeMs;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (includeAll || (createdMs != null && createdMs < cutoff)) {
|
|
195
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
196
|
+
deleted += 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const remaining = Math.max(entries.length - deleted, 0);
|
|
200
|
+
return { deleted, remaining };
|
|
201
|
+
}
|
|
202
|
+
export async function wait(ms) {
|
|
203
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
204
|
+
}
|
|
205
|
+
export { ORACLE_HOME, SESSIONS_DIR, MAX_STATUS_LIMIT };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
let cachedVersion = null;
|
|
5
|
+
export function getCliVersion() {
|
|
6
|
+
if (cachedVersion) {
|
|
7
|
+
return cachedVersion;
|
|
8
|
+
}
|
|
9
|
+
cachedVersion = readVersionFromPackage();
|
|
10
|
+
return cachedVersion;
|
|
11
|
+
}
|
|
12
|
+
function readVersionFromPackage() {
|
|
13
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
14
|
+
let currentDir = path.dirname(modulePath);
|
|
15
|
+
const filesystemRoot = path.parse(currentDir).root;
|
|
16
|
+
// biome-ignore lint/nursery/noUnnecessaryConditions: deliberate sentinel loop to walk up directories
|
|
17
|
+
while (true) {
|
|
18
|
+
const candidate = path.join(currentDir, 'package.json');
|
|
19
|
+
try {
|
|
20
|
+
const raw = readFileSync(candidate, 'utf8');
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
const version = typeof parsed.version === 'string' && parsed.version.trim().length > 0
|
|
23
|
+
? parsed.version.trim()
|
|
24
|
+
: '0.0.0';
|
|
25
|
+
return version;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const code = error instanceof Error && 'code' in error ? error.code : undefined;
|
|
29
|
+
if (code && code !== 'ENOENT') {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (currentDir === filesystemRoot) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
currentDir = path.dirname(currentDir);
|
|
37
|
+
}
|
|
38
|
+
return '0.0.0';
|
|
39
|
+
}
|
|
@@ -9,7 +9,8 @@ export async function ensureModelSelection(Runtime, desiredModel, logger) {
|
|
|
9
9
|
const result = outcome.result?.value;
|
|
10
10
|
switch (result?.status) {
|
|
11
11
|
case 'already-selected':
|
|
12
|
-
case 'switched':
|
|
12
|
+
case 'switched':
|
|
13
|
+
case 'switched-best-effort': {
|
|
13
14
|
const label = result.label ?? desiredModel;
|
|
14
15
|
logger(`Model picker: ${label}`);
|
|
15
16
|
return;
|
|
@@ -24,18 +25,26 @@ export async function ensureModelSelection(Runtime, desiredModel, logger) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Builds the DOM expression that runs inside the ChatGPT tab to select a model.
|
|
30
|
+
* The string is evaluated inside Chrome, so keep it self-contained and well-commented.
|
|
31
|
+
*/
|
|
27
32
|
function buildModelSelectionExpression(targetModel) {
|
|
28
33
|
const matchers = buildModelMatchersLiteral(targetModel);
|
|
29
34
|
const labelLiteral = JSON.stringify(matchers.labelTokens);
|
|
30
35
|
const idLiteral = JSON.stringify(matchers.testIdTokens);
|
|
36
|
+
const primaryLabelLiteral = JSON.stringify(targetModel);
|
|
31
37
|
const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
|
|
32
38
|
const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
|
|
33
39
|
return `(() => {
|
|
40
|
+
// Capture the selectors and matcher literals up front so the browser expression stays pure.
|
|
34
41
|
const BUTTON_SELECTOR = '${MODEL_BUTTON_SELECTOR}';
|
|
35
42
|
const LABEL_TOKENS = ${labelLiteral};
|
|
36
43
|
const TEST_IDS = ${idLiteral};
|
|
37
|
-
const
|
|
38
|
-
const
|
|
44
|
+
const PRIMARY_LABEL = ${primaryLabelLiteral};
|
|
45
|
+
const INITIAL_WAIT_MS = 150;
|
|
46
|
+
const REOPEN_INTERVAL_MS = 400;
|
|
47
|
+
const MAX_WAIT_MS = 20000;
|
|
39
48
|
const normalizeText = (value) => {
|
|
40
49
|
if (!value) {
|
|
41
50
|
return '';
|
|
@@ -46,6 +55,12 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
46
55
|
.replace(/\\s+/g, ' ')
|
|
47
56
|
.trim();
|
|
48
57
|
};
|
|
58
|
+
// Normalize every candidate token to keep fuzzy matching deterministic.
|
|
59
|
+
const normalizedTarget = normalizeText(PRIMARY_LABEL);
|
|
60
|
+
const normalizedTokens = Array.from(new Set([normalizedTarget, ...LABEL_TOKENS]))
|
|
61
|
+
.map((token) => normalizeText(token))
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
const targetWords = normalizedTarget.split(' ').filter(Boolean);
|
|
49
64
|
|
|
50
65
|
const button = document.querySelector(BUTTON_SELECTOR);
|
|
51
66
|
if (!button) {
|
|
@@ -54,6 +69,7 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
54
69
|
|
|
55
70
|
let lastPointerClick = 0;
|
|
56
71
|
const pointerClick = () => {
|
|
72
|
+
// Some menus ignore synthetic click events.
|
|
57
73
|
const down = new PointerEvent('pointerdown', { bubbles: true, pointerId: 1, pointerType: 'mouse' });
|
|
58
74
|
const up = new PointerEvent('pointerup', { bubbles: true, pointerId: 1, pointerType: 'mouse' });
|
|
59
75
|
const click = new MouseEvent('click', { bubbles: true });
|
|
@@ -86,64 +102,109 @@ function buildModelSelectionExpression(targetModel) {
|
|
|
86
102
|
return false;
|
|
87
103
|
};
|
|
88
104
|
|
|
89
|
-
const
|
|
105
|
+
const scoreOption = (normalizedText, testid) => {
|
|
106
|
+
// Assign a score to every node so we can pick the most likely match without brittle equality checks.
|
|
107
|
+
if (!normalizedText && !testid) {
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
let score = 0;
|
|
111
|
+
const normalizedTestId = (testid ?? '').toLowerCase();
|
|
112
|
+
if (normalizedTestId && TEST_IDS.some((id) => normalizedTestId.includes(id))) {
|
|
113
|
+
score += 1000;
|
|
114
|
+
}
|
|
115
|
+
if (normalizedText && normalizedTarget) {
|
|
116
|
+
if (normalizedText === normalizedTarget) {
|
|
117
|
+
score += 500;
|
|
118
|
+
} else if (normalizedText.startsWith(normalizedTarget)) {
|
|
119
|
+
score += 420;
|
|
120
|
+
} else if (normalizedText.includes(normalizedTarget)) {
|
|
121
|
+
score += 380;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const token of normalizedTokens) {
|
|
125
|
+
// Reward partial matches to the expanded label/token set.
|
|
126
|
+
if (token && normalizedText.includes(token)) {
|
|
127
|
+
const tokenWeight = Math.min(120, Math.max(10, token.length * 4));
|
|
128
|
+
score += tokenWeight;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (targetWords.length > 1) {
|
|
132
|
+
let missing = 0;
|
|
133
|
+
for (const word of targetWords) {
|
|
134
|
+
if (!normalizedText.includes(word)) {
|
|
135
|
+
missing += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
score -= missing * 12;
|
|
139
|
+
}
|
|
140
|
+
return Math.max(score, 0);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const findBestOption = () => {
|
|
144
|
+
// Walk through every menu item and keep whichever earns the highest score.
|
|
145
|
+
let bestMatch = null;
|
|
90
146
|
const menus = Array.from(document.querySelectorAll(${menuContainerLiteral}));
|
|
91
147
|
for (const menu of menus) {
|
|
92
148
|
const buttons = Array.from(menu.querySelectorAll(${menuItemLiteral}));
|
|
93
149
|
for (const option of buttons) {
|
|
94
|
-
const testid = (option.getAttribute('data-testid') ?? '').toLowerCase();
|
|
95
150
|
const text = option.textContent ?? '';
|
|
96
151
|
const normalizedText = normalizeText(text);
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (matchesTestId || matchesText) {
|
|
106
|
-
return option;
|
|
152
|
+
const testid = option.getAttribute('data-testid') ?? '';
|
|
153
|
+
const score = scoreOption(normalizedText, testid);
|
|
154
|
+
if (score <= 0) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const label = getOptionLabel(option);
|
|
158
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
159
|
+
bestMatch = { node: option, label, score };
|
|
107
160
|
}
|
|
108
161
|
}
|
|
109
162
|
}
|
|
110
|
-
return
|
|
163
|
+
return bestMatch;
|
|
111
164
|
};
|
|
112
165
|
|
|
113
|
-
pointerClick();
|
|
114
166
|
return new Promise((resolve) => {
|
|
115
167
|
const start = performance.now();
|
|
116
168
|
const ensureMenuOpen = () => {
|
|
117
169
|
const menuOpen = document.querySelector('[role="menu"], [data-radix-collection-root]');
|
|
118
|
-
if (!menuOpen && performance.now() - lastPointerClick >
|
|
170
|
+
if (!menuOpen && performance.now() - lastPointerClick > REOPEN_INTERVAL_MS) {
|
|
119
171
|
pointerClick();
|
|
120
172
|
}
|
|
121
173
|
};
|
|
122
|
-
|
|
174
|
+
|
|
175
|
+
// Open once and wait a tick before first scan.
|
|
176
|
+
pointerClick();
|
|
177
|
+
const openDelay = () => new Promise((r) => setTimeout(r, INITIAL_WAIT_MS));
|
|
178
|
+
let initialized = false;
|
|
179
|
+
const attempt = async () => {
|
|
180
|
+
if (!initialized) {
|
|
181
|
+
initialized = true;
|
|
182
|
+
await openDelay();
|
|
183
|
+
}
|
|
123
184
|
ensureMenuOpen();
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
if (optionIsSelected(
|
|
127
|
-
resolve({ status: 'already-selected', label:
|
|
185
|
+
const match = findBestOption();
|
|
186
|
+
if (match) {
|
|
187
|
+
if (optionIsSelected(match.node)) {
|
|
188
|
+
resolve({ status: 'already-selected', label: match.label });
|
|
128
189
|
return;
|
|
129
190
|
}
|
|
130
|
-
|
|
131
|
-
resolve({ status: 'switched', label:
|
|
191
|
+
match.node.click();
|
|
192
|
+
resolve({ status: 'switched', label: match.label });
|
|
132
193
|
return;
|
|
133
194
|
}
|
|
134
195
|
if (performance.now() - start > MAX_WAIT_MS) {
|
|
135
196
|
resolve({ status: 'option-not-found' });
|
|
136
197
|
return;
|
|
137
198
|
}
|
|
138
|
-
|
|
139
|
-
pointerClick();
|
|
140
|
-
}
|
|
141
|
-
setTimeout(attempt, CLICK_INTERVAL_MS);
|
|
199
|
+
setTimeout(attempt, REOPEN_INTERVAL_MS / 2);
|
|
142
200
|
};
|
|
143
201
|
attempt();
|
|
144
202
|
});
|
|
145
203
|
})()`;
|
|
146
204
|
}
|
|
205
|
+
export function buildModelMatchersLiteralForTest(targetModel) {
|
|
206
|
+
return buildModelMatchersLiteral(targetModel);
|
|
207
|
+
}
|
|
147
208
|
function buildModelMatchersLiteral(targetModel) {
|
|
148
209
|
const base = targetModel.trim().toLowerCase();
|
|
149
210
|
const labelTokens = new Set();
|
|
@@ -164,6 +225,28 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
164
225
|
push(`chatgpt ${dotless}`, labelTokens);
|
|
165
226
|
push(`gpt ${base}`, labelTokens);
|
|
166
227
|
push(`gpt ${dotless}`, labelTokens);
|
|
228
|
+
// Numeric variations (5.1 ↔ 51 ↔ gpt-5-1)
|
|
229
|
+
if (base.includes('5.1') || base.includes('5-1') || base.includes('51')) {
|
|
230
|
+
push('5.1', labelTokens);
|
|
231
|
+
push('gpt-5.1', labelTokens);
|
|
232
|
+
push('gpt5.1', labelTokens);
|
|
233
|
+
push('gpt-5-1', labelTokens);
|
|
234
|
+
push('gpt5-1', labelTokens);
|
|
235
|
+
push('gpt51', labelTokens);
|
|
236
|
+
push('chatgpt 5.1', labelTokens);
|
|
237
|
+
testIdTokens.add('gpt-5-1');
|
|
238
|
+
testIdTokens.add('gpt5-1');
|
|
239
|
+
testIdTokens.add('gpt51');
|
|
240
|
+
}
|
|
241
|
+
// Pro / research variants
|
|
242
|
+
if (base.includes('pro')) {
|
|
243
|
+
push('proresearch', labelTokens);
|
|
244
|
+
push('research grade', labelTokens);
|
|
245
|
+
push('advanced reasoning', labelTokens);
|
|
246
|
+
testIdTokens.add('gpt-5-pro');
|
|
247
|
+
testIdTokens.add('pro');
|
|
248
|
+
testIdTokens.add('proresearch');
|
|
249
|
+
}
|
|
167
250
|
base
|
|
168
251
|
.split(/\s+/)
|
|
169
252
|
.map((token) => token.trim())
|
|
@@ -175,8 +258,10 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
175
258
|
push(hyphenated, testIdTokens);
|
|
176
259
|
push(collapsed, testIdTokens);
|
|
177
260
|
push(dotless, testIdTokens);
|
|
261
|
+
// data-testid values observed in the ChatGPT picker (e.g., model-switcher-gpt-5-pro)
|
|
178
262
|
push(`model-switcher-${hyphenated}`, testIdTokens);
|
|
179
263
|
push(`model-switcher-${collapsed}`, testIdTokens);
|
|
264
|
+
push(`model-switcher-${dotless}`, testIdTokens);
|
|
180
265
|
if (!labelTokens.size) {
|
|
181
266
|
labelTokens.add(base);
|
|
182
267
|
}
|
|
@@ -188,3 +273,6 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
188
273
|
testIdTokens: Array.from(testIdTokens).filter(Boolean),
|
|
189
274
|
};
|
|
190
275
|
}
|
|
276
|
+
export function buildModelSelectionExpressionForTest(targetModel) {
|
|
277
|
+
return buildModelSelectionExpression(targetModel);
|
|
278
|
+
}
|
|
@@ -177,7 +177,7 @@ async function attemptSqliteRebuild() {
|
|
|
177
177
|
}
|
|
178
178
|
attemptedSqliteRebuild = true;
|
|
179
179
|
if (process.env.ORACLE_ALLOW_SQLITE_REBUILD !== '1') {
|
|
180
|
-
console.warn('[oracle] sqlite3 bindings missing. Set ORACLE_ALLOW_SQLITE_REBUILD=1 if you want
|
|
180
|
+
console.warn('[oracle] sqlite3 bindings missing. Set ORACLE_ALLOW_SQLITE_REBUILD=1 if you want oracle to attempt an automatic rebuild, or run `pnpm rebuild chrome-cookies-secure sqlite3 keytar --workspace-root` manually.');
|
|
181
181
|
return false;
|
|
182
182
|
}
|
|
183
183
|
const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
|
|
@@ -64,6 +64,7 @@ export async function runBrowserMode(options) {
|
|
|
64
64
|
await Promise.all(domainEnablers);
|
|
65
65
|
await Network.clearBrowserCookies();
|
|
66
66
|
if (config.cookieSync) {
|
|
67
|
+
logger('Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; approve it to stay signed in or rerun with --browser-no-cookie-sync / --browser-allow-cookie-errors.');
|
|
67
68
|
const cookieCount = await syncCookies(Network, config.url, config.chromeProfile, logger, config.allowCookieErrors ?? false);
|
|
68
69
|
logger(cookieCount > 0
|
|
69
70
|
? `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? 'Default'}`
|
|
@@ -155,7 +156,7 @@ export async function runBrowserMode(options) {
|
|
|
155
156
|
logger(`Chrome window closed before completion: ${normalizedError.message}`);
|
|
156
157
|
logger(normalizedError.stack);
|
|
157
158
|
}
|
|
158
|
-
throw new Error('Chrome window closed before
|
|
159
|
+
throw new Error('Chrome window closed before oracle finished. Please keep it open until completion.', {
|
|
159
160
|
cause: normalizedError,
|
|
160
161
|
});
|
|
161
162
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { readFiles, createFileSections,
|
|
4
|
+
import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS } from '../oracle.js';
|
|
5
5
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
6
6
|
const cwd = deps.cwd ?? process.cwd();
|
|
7
7
|
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
8
8
|
const files = await readFilesFn(runOptions.file ?? [], { cwd });
|
|
9
9
|
const basePrompt = (runOptions.prompt ?? '').trim();
|
|
10
10
|
const userPrompt = basePrompt;
|
|
11
|
-
const systemPrompt = runOptions.system?.trim() ||
|
|
11
|
+
const systemPrompt = runOptions.system?.trim() || '';
|
|
12
12
|
const sections = createFileSections(files, cwd);
|
|
13
13
|
const lines = ['[SYSTEM]', systemPrompt, '', '[USER]', userPrompt, ''];
|
|
14
14
|
sections.forEach((section) => {
|
|
@@ -43,7 +43,8 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
43
43
|
sizeBytes: Buffer.byteLength(section.content, 'utf8'),
|
|
44
44
|
}));
|
|
45
45
|
const MAX_BROWSER_ATTACHMENTS = 10;
|
|
46
|
-
|
|
46
|
+
const shouldBundle = !inlineFiles && (runOptions.browserBundleFiles || attachments.length > MAX_BROWSER_ATTACHMENTS);
|
|
47
|
+
if (shouldBundle) {
|
|
47
48
|
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
48
49
|
const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
|
|
49
50
|
const bundleLines = [];
|
|
@@ -57,7 +58,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
57
58
|
attachments.length = 0;
|
|
58
59
|
attachments.push({
|
|
59
60
|
path: bundlePath,
|
|
60
|
-
displayPath:
|
|
61
|
+
displayPath: bundlePath,
|
|
61
62
|
sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
|
|
62
63
|
});
|
|
63
64
|
}
|
|
@@ -81,7 +82,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
81
82
|
attachments,
|
|
82
83
|
inlineFileCount,
|
|
83
84
|
tokenEstimateIncludesInlineFiles,
|
|
84
|
-
bundled:
|
|
85
|
+
bundled: shouldBundle && attachments.length === 1 && attachments[0]?.displayPath
|
|
85
86
|
? { originalCount: sections.length, bundlePath: attachments[0].displayPath }
|
|
86
87
|
: null,
|
|
87
88
|
};
|
|
@@ -16,14 +16,14 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
16
16
|
const attachmentList = promptArtifacts.attachments.map((attachment) => attachment.displayPath).join(', ');
|
|
17
17
|
log(chalk.dim(`[verbose] Browser attachments: ${attachmentList}`));
|
|
18
18
|
if (promptArtifacts.bundled) {
|
|
19
|
-
log(chalk.yellow(`[browser]
|
|
19
|
+
log(chalk.yellow(`[browser] Bundled ${promptArtifacts.bundled.originalCount} files into ${promptArtifacts.bundled.bundlePath}.`));
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
else if (runOptions.file && runOptions.file.length > 0 && runOptions.browserInlineFiles) {
|
|
23
23
|
log(chalk.dim('[verbose] Browser inline file fallback enabled (pasting file contents).'));
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
const headerLine = `
|
|
26
|
+
const headerLine = `oracle (${cliVersion}) launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens`;
|
|
27
27
|
const automationLogger = ((message) => {
|
|
28
28
|
if (typeof message === 'string') {
|
|
29
29
|
log(message);
|
|
@@ -56,6 +56,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
56
56
|
log(browserResult.answerMarkdown || browserResult.answerText || chalk.dim('(no text output)'));
|
|
57
57
|
log('');
|
|
58
58
|
}
|
|
59
|
+
const answerText = browserResult.answerMarkdown || browserResult.answerText || '';
|
|
59
60
|
const usage = {
|
|
60
61
|
inputTokens: promptArtifacts.estimatedInputTokens,
|
|
61
62
|
outputTokens: browserResult.answerTokens,
|
|
@@ -76,5 +77,6 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
76
77
|
chromePort: browserResult.chromePort,
|
|
77
78
|
userDataDir: browserResult.userDataDir,
|
|
78
79
|
},
|
|
80
|
+
answerText,
|
|
79
81
|
};
|
|
80
82
|
}
|
package/dist/src/cli/dryRun.js
CHANGED
|
@@ -40,20 +40,56 @@ async function runBrowserDryRun({ runOptions, cwd, version, log, }, deps) {
|
|
|
40
40
|
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
41
41
|
const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
42
42
|
log(chalk.cyan(headerLine));
|
|
43
|
-
logBrowserFileSummary(artifacts, log);
|
|
43
|
+
logBrowserFileSummary(artifacts, log, 'dry-run');
|
|
44
44
|
}
|
|
45
|
-
function logBrowserFileSummary(artifacts, log) {
|
|
45
|
+
function logBrowserFileSummary(artifacts, log, label) {
|
|
46
46
|
if (artifacts.attachments.length > 0) {
|
|
47
|
-
|
|
47
|
+
const prefix = artifacts.bundled ? `[${label}] Bundled upload:` : `[${label}] Attachments to upload:`;
|
|
48
|
+
log(chalk.bold(prefix));
|
|
48
49
|
artifacts.attachments.forEach((attachment) => {
|
|
49
50
|
log(` • ${formatAttachmentLabel(attachment)}`);
|
|
50
51
|
});
|
|
52
|
+
if (artifacts.bundled) {
|
|
53
|
+
log(chalk.dim(` (bundled ${artifacts.bundled.originalCount} files into ${artifacts.bundled.bundlePath})`));
|
|
54
|
+
}
|
|
51
55
|
return;
|
|
52
56
|
}
|
|
53
57
|
if (artifacts.inlineFileCount > 0) {
|
|
54
|
-
log(chalk.bold(
|
|
58
|
+
log(chalk.bold(`[${label}] Inline file content:`));
|
|
55
59
|
log(` • ${artifacts.inlineFileCount} file${artifacts.inlineFileCount === 1 ? '' : 's'} pasted directly into the composer.`);
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
|
-
log(chalk.dim(
|
|
62
|
+
log(chalk.dim(`[${label}] No files attached.`));
|
|
63
|
+
}
|
|
64
|
+
export async function runBrowserPreview({ runOptions, cwd, version, previewMode, log, }, deps = {}) {
|
|
65
|
+
const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
|
|
66
|
+
const artifacts = await assemblePromptImpl(runOptions, { cwd });
|
|
67
|
+
const suffix = buildTokenEstimateSuffix(artifacts);
|
|
68
|
+
const headerLine = `[preview] Oracle (${version}) browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
|
|
69
|
+
log(chalk.cyan(headerLine));
|
|
70
|
+
logBrowserFileSummary(artifacts, log, 'preview');
|
|
71
|
+
if (previewMode === 'json' || previewMode === 'full') {
|
|
72
|
+
const attachmentSummary = artifacts.attachments.map((attachment) => ({
|
|
73
|
+
path: attachment.path,
|
|
74
|
+
displayPath: attachment.displayPath,
|
|
75
|
+
sizeBytes: attachment.sizeBytes,
|
|
76
|
+
}));
|
|
77
|
+
const previewPayload = {
|
|
78
|
+
model: runOptions.model,
|
|
79
|
+
engine: 'browser',
|
|
80
|
+
composerText: artifacts.composerText,
|
|
81
|
+
attachments: attachmentSummary,
|
|
82
|
+
inlineFileCount: artifacts.inlineFileCount,
|
|
83
|
+
bundled: artifacts.bundled,
|
|
84
|
+
tokenEstimate: artifacts.estimatedInputTokens,
|
|
85
|
+
};
|
|
86
|
+
log('');
|
|
87
|
+
log(chalk.bold('Preview JSON'));
|
|
88
|
+
log(JSON.stringify(previewPayload, null, 2));
|
|
89
|
+
}
|
|
90
|
+
if (previewMode === 'full') {
|
|
91
|
+
log('');
|
|
92
|
+
log(chalk.bold('Composer Text'));
|
|
93
|
+
log(artifacts.composerText || chalk.dim('(empty prompt)'));
|
|
94
|
+
}
|
|
59
95
|
}
|
package/dist/src/cli/engine.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export function defaultWaitPreference(model, engine) {
|
|
2
|
+
// gpt-5-pro (API) can take up to 10 minutes; default to non-blocking
|
|
3
|
+
if (engine === 'api' && model === 'gpt-5-pro') {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
return true; // browser or gpt-5.1 are fast enough to block by default
|
|
7
|
+
}
|
|
1
8
|
/**
|
|
2
9
|
* Determine which engine to use based on CLI flags and the environment.
|
|
3
10
|
*
|