@steipete/oracle 1.0.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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/bin/oracle-cli.js +458 -0
- package/dist/bin/oracle.js +683 -0
- package/dist/scripts/browser-tools.js +536 -0
- package/dist/scripts/check.js +21 -0
- package/dist/scripts/chrome/browser-tools.js +295 -0
- package/dist/scripts/run-cli.js +14 -0
- package/dist/src/browser/actions/assistantResponse.js +471 -0
- package/dist/src/browser/actions/attachments.js +82 -0
- package/dist/src/browser/actions/modelSelection.js +190 -0
- package/dist/src/browser/actions/navigation.js +75 -0
- package/dist/src/browser/actions/promptComposer.js +167 -0
- package/dist/src/browser/chromeLifecycle.js +104 -0
- package/dist/src/browser/config.js +33 -0
- package/dist/src/browser/constants.js +40 -0
- package/dist/src/browser/cookies.js +210 -0
- package/dist/src/browser/domDebug.js +36 -0
- package/dist/src/browser/index.js +319 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/prompt.js +56 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/sessionRunner.js +77 -0
- package/dist/src/browser/types.js +1 -0
- package/dist/src/browser/utils.js +62 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/cli/browserConfig.js +44 -0
- package/dist/src/cli/dryRun.js +59 -0
- package/dist/src/cli/engine.js +17 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/help.js +68 -0
- package/dist/src/cli/options.js +103 -0
- package/dist/src/cli/promptRequirement.js +14 -0
- package/dist/src/cli/rootAlias.js +16 -0
- package/dist/src/cli/sessionCommand.js +48 -0
- package/dist/src/cli/sessionDisplay.js +222 -0
- package/dist/src/cli/sessionRunner.js +94 -0
- package/dist/src/heartbeat.js +43 -0
- package/dist/src/oracle/client.js +48 -0
- package/dist/src/oracle/config.js +29 -0
- package/dist/src/oracle/errors.js +101 -0
- package/dist/src/oracle/files.js +220 -0
- package/dist/src/oracle/format.js +33 -0
- package/dist/src/oracle/fsAdapter.js +7 -0
- package/dist/src/oracle/request.js +48 -0
- package/dist/src/oracle/run.js +411 -0
- package/dist/src/oracle/tokenStats.js +39 -0
- package/dist/src/oracle/types.js +1 -0
- package/dist/src/oracle.js +9 -0
- package/dist/src/sessionManager.js +205 -0
- package/dist/src/version.js +39 -0
- package/package.json +69 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { execSync, spawn } from 'node:child_process';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import puppeteer from 'puppeteer-core';
|
|
6
|
+
const DEFAULT_PORT = 9222;
|
|
7
|
+
const DEFAULT_PROFILE_DIR = path.join(os.homedir(), '.cache', 'scraping');
|
|
8
|
+
const DEFAULT_CHROME_BIN = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
9
|
+
function browserURL(port) {
|
|
10
|
+
return `http://localhost:${port}`;
|
|
11
|
+
}
|
|
12
|
+
async function connectBrowser(port) {
|
|
13
|
+
return puppeteer.connect({ browserURL: browserURL(port), defaultViewport: null });
|
|
14
|
+
}
|
|
15
|
+
async function getActivePage(port) {
|
|
16
|
+
const browser = await connectBrowser(port);
|
|
17
|
+
const pages = await browser.pages();
|
|
18
|
+
const page = pages.at(-1);
|
|
19
|
+
if (!page) {
|
|
20
|
+
await browser.disconnect();
|
|
21
|
+
throw new Error('No active tab found');
|
|
22
|
+
}
|
|
23
|
+
return { browser, page };
|
|
24
|
+
}
|
|
25
|
+
const program = new Command();
|
|
26
|
+
program
|
|
27
|
+
.name('browser-tools')
|
|
28
|
+
.description('Minimal Chrome DevTools helpers inspired by Mario Zechner’s “What if you don’t need MCP?” article.')
|
|
29
|
+
.configureHelp({
|
|
30
|
+
sortSubcommands: true,
|
|
31
|
+
})
|
|
32
|
+
.showSuggestionAfterError();
|
|
33
|
+
program
|
|
34
|
+
.command('start')
|
|
35
|
+
.description('Launch Chrome with remote debugging enabled.')
|
|
36
|
+
.option('-p, --port <number>', 'Remote debugging port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
37
|
+
.option('--profile', 'Copy your existing Chrome profile before launching.', false)
|
|
38
|
+
.option('--profile-dir <path>', 'Directory for the temporary Chrome profile.', DEFAULT_PROFILE_DIR)
|
|
39
|
+
.option('--chrome-path <path>', 'Path to the Chrome binary.', DEFAULT_CHROME_BIN)
|
|
40
|
+
.action(async (options) => {
|
|
41
|
+
const { port, profile, profileDir, chromePath } = options;
|
|
42
|
+
try {
|
|
43
|
+
execSync("killall 'Google Chrome'", { stdio: 'ignore' });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
49
|
+
execSync(`mkdir -p "${profileDir}"`);
|
|
50
|
+
if (profile) {
|
|
51
|
+
const source = path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome') + '/';
|
|
52
|
+
execSync(`rsync -a --delete "${source}" "${profileDir}/"`, { stdio: 'ignore' });
|
|
53
|
+
}
|
|
54
|
+
spawn(chromePath, ['--remote-debugging-port=' + port, `--user-data-dir=${profileDir}`, '--no-first-run', '--disable-popup-blocking'], {
|
|
55
|
+
detached: true,
|
|
56
|
+
stdio: 'ignore',
|
|
57
|
+
}).unref();
|
|
58
|
+
let connected = false;
|
|
59
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
60
|
+
try {
|
|
61
|
+
const browser = await connectBrowser(port);
|
|
62
|
+
await browser.disconnect();
|
|
63
|
+
connected = true;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!connected) {
|
|
71
|
+
console.error(`✗ Failed to start Chrome on port ${port}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
console.log(`✓ Chrome listening on http://localhost:${port}${profile ? ' (with your profile)' : ''}`);
|
|
75
|
+
});
|
|
76
|
+
program
|
|
77
|
+
.command('nav <url>')
|
|
78
|
+
.description('Navigate the current tab or open a new tab.')
|
|
79
|
+
.option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
80
|
+
.option('--new', 'Open in a new tab instead of reusing the current tab.', false)
|
|
81
|
+
.action(async (url, options) => {
|
|
82
|
+
const port = options.port;
|
|
83
|
+
const browser = await connectBrowser(port);
|
|
84
|
+
try {
|
|
85
|
+
if (options.new) {
|
|
86
|
+
const page = await browser.newPage();
|
|
87
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
88
|
+
console.log('✓ Opened in new tab:', url);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const pages = await browser.pages();
|
|
92
|
+
const page = pages.at(-1);
|
|
93
|
+
if (!page) {
|
|
94
|
+
throw new Error('No active tab found');
|
|
95
|
+
}
|
|
96
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
97
|
+
console.log('✓ Navigated current tab to:', url);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await browser.disconnect();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
program
|
|
105
|
+
.command('eval <code...>')
|
|
106
|
+
.description('Evaluate JavaScript in the active page context.')
|
|
107
|
+
.option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
108
|
+
.action(async (code, options) => {
|
|
109
|
+
const snippet = code.join(' ');
|
|
110
|
+
const port = options.port;
|
|
111
|
+
const { browser, page } = await getActivePage(port);
|
|
112
|
+
try {
|
|
113
|
+
const result = await page.evaluate((body) => {
|
|
114
|
+
const AsyncFunctionConstructor = Object.getPrototypeOf(async function () { }).constructor;
|
|
115
|
+
return new AsyncFunctionConstructor(`return (${body})`)();
|
|
116
|
+
}, snippet);
|
|
117
|
+
if (Array.isArray(result)) {
|
|
118
|
+
result.forEach((entry, index) => {
|
|
119
|
+
if (index > 0)
|
|
120
|
+
console.log('');
|
|
121
|
+
Object.entries(entry).forEach(([key, value]) => console.log(`${key}: ${value}`));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else if (typeof result === 'object' && result !== null) {
|
|
125
|
+
Object.entries(result).forEach(([key, value]) => console.log(`${key}: ${value}`));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(result);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await browser.disconnect();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
program
|
|
136
|
+
.command('screenshot')
|
|
137
|
+
.description('Capture the current viewport and print the temp PNG path.')
|
|
138
|
+
.option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
139
|
+
.action(async (options) => {
|
|
140
|
+
const port = options.port;
|
|
141
|
+
const { browser, page } = await getActivePage(port);
|
|
142
|
+
try {
|
|
143
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
144
|
+
const filePath = path.join(os.tmpdir(), `screenshot-${timestamp}.png`);
|
|
145
|
+
await page.screenshot({ path: filePath });
|
|
146
|
+
console.log(filePath);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await browser.disconnect();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
program
|
|
153
|
+
.command('pick <message...>')
|
|
154
|
+
.description('Interactive DOM picker that prints metadata for clicked elements.')
|
|
155
|
+
.option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
156
|
+
.action(async (messageParts, options) => {
|
|
157
|
+
const message = messageParts.join(' ');
|
|
158
|
+
const port = options.port;
|
|
159
|
+
const { browser, page } = await getActivePage(port);
|
|
160
|
+
try {
|
|
161
|
+
await page.evaluate(() => {
|
|
162
|
+
const scope = globalThis;
|
|
163
|
+
if (scope.pickOverlayInjected) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
scope.pickOverlayInjected = true;
|
|
167
|
+
scope.pick = async (prompt) => new Promise((resolve) => {
|
|
168
|
+
const selections = [];
|
|
169
|
+
const selectedElements = new Set();
|
|
170
|
+
const overlay = document.createElement('div');
|
|
171
|
+
overlay.style.cssText =
|
|
172
|
+
'position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none';
|
|
173
|
+
const highlight = document.createElement('div');
|
|
174
|
+
highlight.style.cssText =
|
|
175
|
+
'position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);transition:all 0.05s ease';
|
|
176
|
+
overlay.appendChild(highlight);
|
|
177
|
+
const banner = document.createElement('div');
|
|
178
|
+
banner.style.cssText =
|
|
179
|
+
'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#1f2937;color:#fff;padding:12px 24px;border-radius:8px;font:14px system-ui;box-shadow:0 4px 12px rgba(0,0,0,0.3);pointer-events:auto;z-index:2147483647';
|
|
180
|
+
const updateBanner = () => {
|
|
181
|
+
banner.textContent = `${prompt} (${selections.length} selected, Cmd/Ctrl+click to add, Enter to finish, ESC to cancel)`;
|
|
182
|
+
};
|
|
183
|
+
const cleanup = () => {
|
|
184
|
+
document.removeEventListener('mousemove', onMove, true);
|
|
185
|
+
document.removeEventListener('click', onClick, true);
|
|
186
|
+
document.removeEventListener('keydown', onKey, true);
|
|
187
|
+
overlay.remove();
|
|
188
|
+
banner.remove();
|
|
189
|
+
selectedElements.forEach((el) => (el.style.outline = ''));
|
|
190
|
+
};
|
|
191
|
+
const serialize = (el) => {
|
|
192
|
+
const parents = [];
|
|
193
|
+
let current = el.parentElement;
|
|
194
|
+
while (current && current !== document.body) {
|
|
195
|
+
const id = current.id ? `#${current.id}` : '';
|
|
196
|
+
const cls = current.className ? `.${current.className.trim().split(/\\s+/).join('.')}` : '';
|
|
197
|
+
parents.push(`${current.tagName.toLowerCase()}${id}${cls}`);
|
|
198
|
+
current = current.parentElement;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
tag: el.tagName.toLowerCase(),
|
|
202
|
+
id: el.id || null,
|
|
203
|
+
class: el.className || null,
|
|
204
|
+
text: el.textContent?.trim()?.slice(0, 200) || null,
|
|
205
|
+
html: el.outerHTML.slice(0, 500),
|
|
206
|
+
parents: parents.join(' > '),
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
const onMove = (event) => {
|
|
210
|
+
const node = document.elementFromPoint(event.clientX, event.clientY);
|
|
211
|
+
if (!node || overlay.contains(node) || banner.contains(node))
|
|
212
|
+
return;
|
|
213
|
+
const rect = node.getBoundingClientRect();
|
|
214
|
+
highlight.style.cssText = `position:absolute;border:2px solid #3b82f6;background:rgba(59,130,246,0.1);top:${rect.top}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px`;
|
|
215
|
+
};
|
|
216
|
+
const onClick = (event) => {
|
|
217
|
+
if (banner.contains(event.target))
|
|
218
|
+
return;
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
event.stopPropagation();
|
|
221
|
+
const node = document.elementFromPoint(event.clientX, event.clientY);
|
|
222
|
+
if (!node || overlay.contains(node) || banner.contains(node))
|
|
223
|
+
return;
|
|
224
|
+
if (event.metaKey || event.ctrlKey) {
|
|
225
|
+
if (!selectedElements.has(node)) {
|
|
226
|
+
selectedElements.add(node);
|
|
227
|
+
node.style.outline = '3px solid #10b981';
|
|
228
|
+
selections.push(serialize(node));
|
|
229
|
+
updateBanner();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
cleanup();
|
|
234
|
+
const info = serialize(node);
|
|
235
|
+
resolve(selections.length > 0 ? selections : info);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const onKey = (event) => {
|
|
239
|
+
if (event.key === 'Escape') {
|
|
240
|
+
cleanup();
|
|
241
|
+
resolve(null);
|
|
242
|
+
}
|
|
243
|
+
else if (event.key === 'Enter' && selections.length > 0) {
|
|
244
|
+
cleanup();
|
|
245
|
+
resolve(selections);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
document.addEventListener('mousemove', onMove, true);
|
|
249
|
+
document.addEventListener('click', onClick, true);
|
|
250
|
+
document.addEventListener('keydown', onKey, true);
|
|
251
|
+
document.body.append(overlay, banner);
|
|
252
|
+
updateBanner();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
const result = await page.evaluate((msg) => {
|
|
256
|
+
const pickFn = window.pick;
|
|
257
|
+
if (!pickFn) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return pickFn(msg);
|
|
261
|
+
}, message);
|
|
262
|
+
if (Array.isArray(result)) {
|
|
263
|
+
result.forEach((entry, index) => {
|
|
264
|
+
if (index > 0)
|
|
265
|
+
console.log('');
|
|
266
|
+
Object.entries(entry).forEach(([key, value]) => console.log(`${key}: ${value}`));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
else if (result && typeof result === 'object') {
|
|
270
|
+
Object.entries(result).forEach(([key, value]) => console.log(`${key}: ${value}`));
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
console.log(result);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
await browser.disconnect();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
program
|
|
281
|
+
.command('cookies')
|
|
282
|
+
.description('Dump cookies from the active tab as JSON.')
|
|
283
|
+
.option('--port <number>', 'Debugger port (default: 9222)', (value) => Number.parseInt(value, 10), DEFAULT_PORT)
|
|
284
|
+
.action(async (options) => {
|
|
285
|
+
const port = options.port;
|
|
286
|
+
const { browser, page } = await getActivePage(port);
|
|
287
|
+
try {
|
|
288
|
+
const cookies = await page.cookies();
|
|
289
|
+
console.log(JSON.stringify(cookies, null, 2));
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
await browser.disconnect();
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const rawArgs = process.argv.slice(2);
|
|
6
|
+
const args = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs;
|
|
7
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const cliEntry = path.join(here, '../bin/oracle-cli.js');
|
|
9
|
+
const child = spawn(process.execPath, ['--', cliEntry, ...args], {
|
|
10
|
+
stdio: 'inherit',
|
|
11
|
+
});
|
|
12
|
+
child.on('exit', (code) => {
|
|
13
|
+
process.exit(code ?? 0);
|
|
14
|
+
});
|