@steipete/oracle 0.7.5 → 0.7.6
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 +1 -0
- package/dist/bin/oracle-cli.js +8 -2
- package/dist/src/browser/actions/attachments.js +69 -31
- package/dist/src/browser/actions/promptComposer.js +1 -1
- package/dist/src/browser/chromeLifecycle.js +7 -1
- package/dist/src/browser/reattach.js +119 -16
- package/dist/src/browser/sessionRunner.js +16 -6
- package/dist/src/cli/options.js +27 -0
- package/dist/src/cli/sessionDisplay.js +18 -6
- package/dist/src/cli/sessionRunner.js +14 -4
- package/dist/src/oracle/finishLine.js +32 -0
- package/dist/src/oracle/run.js +35 -32
- package/dist/src/sessionManager.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Oracle bundles your prompt and files so another AI can answer with real context.
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
18
18
|
Install globally: `npm install -g @steipete/oracle`
|
|
19
|
+
Homebrew: `brew install steipete/tap/oracle`
|
|
19
20
|
|
|
20
21
|
Use `npx -y @steipete/oracle …` (not `pnpx`)—pnpx's sandboxed cache can’t load the sqlite bindings and will throw missing `node_sqlite3.node` errors.
|
|
21
22
|
|
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { CHATGPT_URL } from '../src/browserMode.js';
|
|
|
20
20
|
import { createRemoteBrowserExecutor } from '../src/remote/client.js';
|
|
21
21
|
import { createGeminiWebExecutor } from '../src/gemini-web/index.js';
|
|
22
22
|
import { applyHelpStyling } from '../src/cli/help.js';
|
|
23
|
-
import { collectPaths, collectModelList, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, mergePathLikeOptions, } from '../src/cli/options.js';
|
|
23
|
+
import { collectPaths, collectModelList, parseFloatOption, parseIntOption, parseSearchOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, mergePathLikeOptions, dedupePathInputs, } from '../src/cli/options.js';
|
|
24
24
|
import { copyToClipboard } from '../src/cli/clipboard.js';
|
|
25
25
|
import { buildMarkdownBundle } from '../src/cli/markdownBundle.js';
|
|
26
26
|
import { shouldDetachSession } from '../src/cli/detach.js';
|
|
@@ -420,7 +420,13 @@ async function runRootCommand(options) {
|
|
|
420
420
|
const previewMode = resolvePreviewMode(options.dryRun || options.preview);
|
|
421
421
|
const mergedFileInputs = mergePathLikeOptions(options.file, options.include, options.files, options.path, options.paths);
|
|
422
422
|
if (mergedFileInputs.length > 0) {
|
|
423
|
-
|
|
423
|
+
const { deduped, duplicates } = dedupePathInputs(mergedFileInputs, { cwd: process.cwd() });
|
|
424
|
+
if (duplicates.length > 0) {
|
|
425
|
+
const preview = duplicates.slice(0, 8).join(', ');
|
|
426
|
+
const suffix = duplicates.length > 8 ? ` (+${duplicates.length - 8} more)` : '';
|
|
427
|
+
console.log(chalk.dim(`Ignoring duplicate --file inputs: ${preview}${suffix}`));
|
|
428
|
+
}
|
|
429
|
+
options.file = deduped;
|
|
424
430
|
}
|
|
425
431
|
const copyMarkdown = options.copyMarkdown || options.copy;
|
|
426
432
|
const renderMarkdown = resolveRenderFlag(options.render, options.renderMarkdown);
|
|
@@ -10,27 +10,66 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
|
|
|
10
10
|
const isAttachmentPresent = async (name) => {
|
|
11
11
|
const check = await runtime.evaluate({
|
|
12
12
|
expression: `(() => {
|
|
13
|
-
const expected = ${JSON.stringify(name
|
|
14
|
-
const
|
|
13
|
+
const expected = ${JSON.stringify(name)};
|
|
14
|
+
const normalizedExpected = String(expected || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
15
|
+
const expectedNoExt = normalizedExpected.replace(/\\.[a-z0-9]{1,10}$/i, '');
|
|
16
|
+
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
17
|
+
const matchesExpected = (value) => {
|
|
18
|
+
const text = normalize(value);
|
|
19
|
+
if (!text) return false;
|
|
20
|
+
if (text.includes(normalizedExpected)) return true;
|
|
21
|
+
if (expectedNoExt.length >= 6 && text.includes(expectedNoExt)) return true;
|
|
22
|
+
if (text.includes('…') || text.includes('...')) {
|
|
23
|
+
const marker = text.includes('…') ? '…' : '...';
|
|
24
|
+
const [prefixRaw, suffixRaw] = text.split(marker);
|
|
25
|
+
const prefix = normalize(prefixRaw);
|
|
26
|
+
const suffix = normalize(suffixRaw);
|
|
27
|
+
const target = expectedNoExt.length >= 6 ? expectedNoExt : normalizedExpected;
|
|
28
|
+
const matchesPrefix = !prefix || target.includes(prefix);
|
|
29
|
+
const matchesSuffix = !suffix || target.includes(suffix);
|
|
30
|
+
return matchesPrefix && matchesSuffix;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const promptSelectors = ${JSON.stringify(INPUT_SELECTORS)};
|
|
36
|
+
const locateComposerRoot = () => {
|
|
37
|
+
for (const selector of promptSelectors) {
|
|
38
|
+
const node = document.querySelector(selector);
|
|
39
|
+
if (!node) continue;
|
|
40
|
+
return node.closest('form') ?? node.closest('[data-testid*="composer"]') ?? node.parentElement;
|
|
41
|
+
}
|
|
42
|
+
return document.querySelector('form') ?? document.body;
|
|
43
|
+
};
|
|
44
|
+
const root = locateComposerRoot();
|
|
45
|
+
const chipSelector = [
|
|
15
46
|
'[data-testid*="attachment"]',
|
|
16
47
|
'[data-testid*="chip"]',
|
|
17
|
-
'[data-testid*="upload"]'
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
24
|
-
|
|
48
|
+
'[data-testid*="upload"]',
|
|
49
|
+
'[aria-label*="Remove"]',
|
|
50
|
+
'button[aria-label*="Remove"]',
|
|
51
|
+
'[aria-label*="remove"]',
|
|
52
|
+
].join(',');
|
|
53
|
+
const candidates = root ? Array.from(root.querySelectorAll(chipSelector)) : [];
|
|
54
|
+
const nodes = candidates.length > 0 ? candidates : Array.from(document.querySelectorAll(chipSelector));
|
|
55
|
+
for (const node of nodes) {
|
|
56
|
+
const text = node?.textContent ?? '';
|
|
57
|
+
const aria = node?.getAttribute?.('aria-label') ?? '';
|
|
58
|
+
const title = node?.getAttribute?.('title') ?? '';
|
|
59
|
+
if ([text, aria, title].some(matchesExpected)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
const cardTexts = Array.from(document.querySelectorAll('[aria-label*="Remove"],[aria-label*="remove"]')).map((btn) =>
|
|
26
|
-
btn?.parentElement?.parentElement?.innerText
|
|
65
|
+
btn?.parentElement?.parentElement?.innerText ?? '',
|
|
27
66
|
);
|
|
28
|
-
if (cardTexts.some(
|
|
67
|
+
if (cardTexts.some(matchesExpected)) return true;
|
|
29
68
|
|
|
30
69
|
const inputs = Array.from(document.querySelectorAll('input[type="file"]')).some((el) =>
|
|
31
|
-
Array.from(el.files || []).some((f) => f?.name
|
|
70
|
+
Array.from(el.files || []).some((f) => matchesExpected(f?.name ?? '')),
|
|
32
71
|
);
|
|
33
|
-
return inputs;
|
|
72
|
+
return Boolean(inputs);
|
|
34
73
|
})()`,
|
|
35
74
|
returnByValue: true,
|
|
36
75
|
});
|
|
@@ -311,15 +350,14 @@ export async function waitForAttachmentCompletion(Runtime, timeoutMs, expectedNa
|
|
|
311
350
|
if (expectedNoExt.length >= 6 && raw.includes(expectedNoExt))
|
|
312
351
|
return true;
|
|
313
352
|
if (raw.includes('…') || raw.includes('...')) {
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
353
|
+
const marker = raw.includes('…') ? '…' : '...';
|
|
354
|
+
const [prefixRaw, suffixRaw] = raw.split(marker);
|
|
355
|
+
const prefix = prefixRaw.trim();
|
|
356
|
+
const suffix = suffixRaw.trim();
|
|
357
|
+
const target = expectedNoExt.length >= 6 ? expectedNoExt : normalizedExpected;
|
|
358
|
+
const matchesPrefix = !prefix || target.includes(prefix);
|
|
359
|
+
const matchesSuffix = !suffix || target.includes(suffix);
|
|
360
|
+
return matchesPrefix && matchesSuffix;
|
|
323
361
|
}
|
|
324
362
|
return false;
|
|
325
363
|
});
|
|
@@ -488,14 +526,14 @@ async function waitForAttachmentAnchored(Runtime, expectedName, timeoutMs) {
|
|
|
488
526
|
if (text.includes(normalized)) return true;
|
|
489
527
|
if (normalizedNoExt.length >= 6 && text.includes(normalizedNoExt)) return true;
|
|
490
528
|
if (text.includes('…') || text.includes('...')) {
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
529
|
+
const marker = text.includes('…') ? '…' : '...';
|
|
530
|
+
const [prefixRaw, suffixRaw] = text.split(marker);
|
|
531
|
+
const prefix = (prefixRaw ?? '').toLowerCase();
|
|
532
|
+
const suffix = (suffixRaw ?? '').toLowerCase();
|
|
533
|
+
const target = normalizedNoExt.length >= 6 ? normalizedNoExt : normalized;
|
|
534
|
+
const matchesPrefix = !prefix || target.includes(prefix);
|
|
535
|
+
const matchesSuffix = !suffix || target.includes(suffix);
|
|
536
|
+
return matchesPrefix && matchesSuffix;
|
|
499
537
|
}
|
|
500
538
|
return false;
|
|
501
539
|
};
|
|
@@ -134,7 +134,7 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
134
134
|
else {
|
|
135
135
|
logger('Clicked send button');
|
|
136
136
|
}
|
|
137
|
-
await verifyPromptCommitted(runtime, prompt,
|
|
137
|
+
await verifyPromptCommitted(runtime, prompt, 60_000, logger);
|
|
138
138
|
}
|
|
139
139
|
export async function clearPromptComposer(Runtime, logger) {
|
|
140
140
|
const primarySelectorLiteral = JSON.stringify(PROMPT_PRIMARY_SELECTOR);
|
|
@@ -68,7 +68,13 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
|
|
|
68
68
|
}
|
|
69
69
|
})().finally(() => {
|
|
70
70
|
const exitCode = signal === 'SIGINT' ? 130 : 1;
|
|
71
|
-
process.exit(
|
|
71
|
+
// Vitest treats any `process.exit()` call as an unhandled failure, even if mocked.
|
|
72
|
+
// Keep production behavior (hard-exit on signals) while letting tests observe state changes.
|
|
73
|
+
process.exitCode = exitCode;
|
|
74
|
+
const isTestRun = process.env.VITEST === '1' || process.env.NODE_ENV === 'test';
|
|
75
|
+
if (!isTestRun) {
|
|
76
|
+
process.exit(exitCode);
|
|
77
|
+
}
|
|
72
78
|
});
|
|
73
79
|
};
|
|
74
80
|
for (const signal of signals) {
|
|
@@ -54,11 +54,30 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
54
54
|
if (DOM && typeof DOM.enable === 'function') {
|
|
55
55
|
await DOM.enable();
|
|
56
56
|
}
|
|
57
|
+
const ensureConversationOpen = async () => {
|
|
58
|
+
const { result } = await Runtime.evaluate({ expression: 'location.href', returnByValue: true });
|
|
59
|
+
const href = typeof result?.value === 'string' ? result.value : '';
|
|
60
|
+
if (href.includes('/c/')) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const opened = await openConversationFromSidebarWithRetry(Runtime, {
|
|
64
|
+
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ''),
|
|
65
|
+
preferProjects: true,
|
|
66
|
+
promptPreview: deps.promptPreview,
|
|
67
|
+
}, 15_000);
|
|
68
|
+
if (!opened) {
|
|
69
|
+
throw new Error('Unable to locate prior ChatGPT conversation in sidebar.');
|
|
70
|
+
}
|
|
71
|
+
await waitForLocationChange(Runtime, 15_000);
|
|
72
|
+
};
|
|
57
73
|
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
58
74
|
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
59
75
|
const timeoutMs = config?.timeoutMs ?? 120_000;
|
|
60
|
-
const
|
|
61
|
-
|
|
76
|
+
const pingTimeoutMs = Math.min(5_000, Math.max(1_500, Math.floor(timeoutMs * 0.05)));
|
|
77
|
+
await withTimeout(Runtime.evaluate({ expression: '1+1', returnByValue: true }), pingTimeoutMs, 'Reattach target did not respond');
|
|
78
|
+
await ensureConversationOpen();
|
|
79
|
+
const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger), timeoutMs + 5_000, 'Reattach response timed out');
|
|
80
|
+
const markdown = (await withTimeout(captureMarkdown(Runtime, answer.meta, logger), 15_000, 'Reattach markdown capture timed out')) ?? answer.text;
|
|
62
81
|
if (client && typeof client.close === 'function') {
|
|
63
82
|
try {
|
|
64
83
|
await client.close();
|
|
@@ -122,10 +141,11 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
122
141
|
await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
|
|
123
142
|
}
|
|
124
143
|
else {
|
|
125
|
-
const opened = await
|
|
144
|
+
const opened = await openConversationFromSidebarWithRetry(Runtime, {
|
|
126
145
|
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ''),
|
|
127
146
|
preferProjects: resolved.url !== CHATGPT_URL,
|
|
128
|
-
|
|
147
|
+
promptPreview: deps.promptPreview,
|
|
148
|
+
}, 15_000);
|
|
129
149
|
if (!opened) {
|
|
130
150
|
throw new Error('Unable to locate prior ChatGPT conversation in sidebar.');
|
|
131
151
|
}
|
|
@@ -163,7 +183,10 @@ function extractConversationIdFromUrl(url) {
|
|
|
163
183
|
}
|
|
164
184
|
function buildConversationUrl(runtime, baseUrl) {
|
|
165
185
|
if (runtime.tabUrl) {
|
|
166
|
-
|
|
186
|
+
if (runtime.tabUrl.includes('/c/')) {
|
|
187
|
+
return runtime.tabUrl;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
167
190
|
}
|
|
168
191
|
const conversationId = runtime.conversationId;
|
|
169
192
|
if (!conversationId) {
|
|
@@ -177,11 +200,24 @@ function buildConversationUrl(runtime, baseUrl) {
|
|
|
177
200
|
return null;
|
|
178
201
|
}
|
|
179
202
|
}
|
|
203
|
+
async function withTimeout(task, ms, label) {
|
|
204
|
+
let timeoutId;
|
|
205
|
+
const timeout = new Promise((_, reject) => {
|
|
206
|
+
timeoutId = setTimeout(() => reject(new Error(label)), ms);
|
|
207
|
+
});
|
|
208
|
+
return Promise.race([task, timeout]).finally(() => {
|
|
209
|
+
if (timeoutId) {
|
|
210
|
+
clearTimeout(timeoutId);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
180
214
|
async function openConversationFromSidebar(Runtime, options) {
|
|
181
215
|
const response = await Runtime.evaluate({
|
|
182
216
|
expression: `(() => {
|
|
183
217
|
const conversationId = ${JSON.stringify(options.conversationId ?? null)};
|
|
184
218
|
const preferProjects = ${JSON.stringify(Boolean(options.preferProjects))};
|
|
219
|
+
const promptPreview = ${JSON.stringify(options.promptPreview ?? null)};
|
|
220
|
+
const promptNeedle = promptPreview ? promptPreview.trim().toLowerCase().slice(0, 100) : '';
|
|
185
221
|
const nav = document.querySelector('nav') || document.querySelector('aside') || document.body;
|
|
186
222
|
if (preferProjects) {
|
|
187
223
|
const projectLink = Array.from(nav.querySelectorAll('a,button'))
|
|
@@ -190,28 +226,95 @@ async function openConversationFromSidebar(Runtime, options) {
|
|
|
190
226
|
projectLink.click();
|
|
191
227
|
}
|
|
192
228
|
}
|
|
193
|
-
const
|
|
194
|
-
.
|
|
195
|
-
|
|
196
|
-
|
|
229
|
+
const allElements = Array.from(
|
|
230
|
+
document.querySelectorAll(
|
|
231
|
+
'a,button,[role="link"],[role="button"],[data-href],[data-url],[data-conversation-id],[data-testid*="conversation"],[data-testid*="history"]',
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
const getHref = (el) =>
|
|
235
|
+
el.getAttribute('href') ||
|
|
236
|
+
el.getAttribute('data-href') ||
|
|
237
|
+
el.getAttribute('data-url') ||
|
|
238
|
+
el.dataset?.href ||
|
|
239
|
+
el.dataset?.url ||
|
|
240
|
+
'';
|
|
241
|
+
const toCandidate = (el) => {
|
|
242
|
+
const clickable = el.closest('a,button,[role="link"],[role="button"]') || el;
|
|
243
|
+
const rawText = (el.textContent || clickable.textContent || '').trim();
|
|
244
|
+
return {
|
|
245
|
+
el,
|
|
246
|
+
clickable,
|
|
247
|
+
href: getHref(clickable) || getHref(el),
|
|
248
|
+
conversationId:
|
|
249
|
+
clickable.getAttribute('data-conversation-id') ||
|
|
250
|
+
el.getAttribute('data-conversation-id') ||
|
|
251
|
+
clickable.dataset?.conversationId ||
|
|
252
|
+
el.dataset?.conversationId ||
|
|
253
|
+
'',
|
|
254
|
+
testId: clickable.getAttribute('data-testid') || el.getAttribute('data-testid') || '',
|
|
255
|
+
text: rawText.replace(/\\s+/g, ' ').slice(0, 400),
|
|
256
|
+
inNav: Boolean(clickable.closest('nav,aside')),
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
const candidates = allElements.map(toCandidate);
|
|
260
|
+
const mainCandidates = candidates.filter((item) => !item.inNav);
|
|
261
|
+
const navCandidates = candidates.filter((item) => item.inNav);
|
|
262
|
+
const visible = (item) => {
|
|
263
|
+
const rect = item.clickable.getBoundingClientRect();
|
|
264
|
+
return rect.width > 0 && rect.height > 0;
|
|
265
|
+
};
|
|
266
|
+
const pick = (items) => (items.find(visible) || items[0] || null);
|
|
197
267
|
let target = null;
|
|
198
268
|
if (conversationId) {
|
|
199
|
-
|
|
269
|
+
const byId = (item) =>
|
|
270
|
+
(item.href && item.href.includes('/c/' + conversationId)) ||
|
|
271
|
+
(item.conversationId && item.conversationId === conversationId);
|
|
272
|
+
target = pick(mainCandidates.filter(byId)) || pick(navCandidates.filter(byId));
|
|
273
|
+
}
|
|
274
|
+
if (!target && promptNeedle) {
|
|
275
|
+
const byPrompt = (item) => item.text && item.text.toLowerCase().includes(promptNeedle);
|
|
276
|
+
target = pick(mainCandidates.filter(byPrompt)) || pick(navCandidates.filter(byPrompt));
|
|
200
277
|
}
|
|
201
|
-
if (!target
|
|
202
|
-
|
|
278
|
+
if (!target) {
|
|
279
|
+
const byHref = (item) => item.href && item.href.includes('/c/');
|
|
280
|
+
target = pick(mainCandidates.filter(byHref)) || pick(navCandidates.filter(byHref));
|
|
281
|
+
}
|
|
282
|
+
if (!target) {
|
|
283
|
+
const byTestId = (item) => /conversation|history/i.test(item.testId || '');
|
|
284
|
+
target = pick(mainCandidates.filter(byTestId)) || pick(navCandidates.filter(byTestId));
|
|
203
285
|
}
|
|
204
286
|
if (target) {
|
|
205
|
-
target.scrollIntoView({ block: 'center' });
|
|
206
|
-
target.
|
|
207
|
-
|
|
287
|
+
target.clickable.scrollIntoView({ block: 'center' });
|
|
288
|
+
target.clickable.dispatchEvent(
|
|
289
|
+
new MouseEvent('click', { bubbles: true, cancelable: true, view: window }),
|
|
290
|
+
);
|
|
291
|
+
return {
|
|
292
|
+
ok: true,
|
|
293
|
+
href: target.href || '',
|
|
294
|
+
count: candidates.length,
|
|
295
|
+
scope: target.inNav ? 'nav' : 'main',
|
|
296
|
+
};
|
|
208
297
|
}
|
|
209
|
-
return { ok: false, count:
|
|
298
|
+
return { ok: false, count: candidates.length };
|
|
210
299
|
})()`,
|
|
211
300
|
returnByValue: true,
|
|
212
301
|
});
|
|
213
302
|
return Boolean(response.result?.value?.ok);
|
|
214
303
|
}
|
|
304
|
+
async function openConversationFromSidebarWithRetry(Runtime, options, timeoutMs) {
|
|
305
|
+
const start = Date.now();
|
|
306
|
+
let attempt = 0;
|
|
307
|
+
while (Date.now() - start < timeoutMs) {
|
|
308
|
+
// Retry because project list can hydrate after initial navigation.
|
|
309
|
+
const opened = await openConversationFromSidebar(Runtime, options);
|
|
310
|
+
if (opened) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
attempt += 1;
|
|
314
|
+
await delay(attempt < 5 ? 250 : 500);
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
215
318
|
async function waitForLocationChange(Runtime, timeoutMs) {
|
|
216
319
|
const start = Date.now();
|
|
217
320
|
let lastHref = '';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { formatElapsed } from '../oracle.js';
|
|
3
2
|
import { formatTokenCount } from '../oracle/runUtils.js';
|
|
3
|
+
import { formatFinishLine } from '../oracle/finishLine.js';
|
|
4
4
|
import { runBrowserMode } from '../browserMode.js';
|
|
5
5
|
import { assembleBrowserPrompt } from './prompt.js';
|
|
6
6
|
import { BrowserAutomationError } from '../oracle/errors.js';
|
|
@@ -88,12 +88,22 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
88
88
|
]
|
|
89
89
|
.map((value) => formatTokenCount(value))
|
|
90
90
|
.join('/');
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
const tokensPart = (() => {
|
|
92
|
+
const parts = tokensDisplay.split('/');
|
|
93
|
+
if (parts.length !== 4)
|
|
94
|
+
return tokensDisplay;
|
|
95
|
+
return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
|
|
96
|
+
})();
|
|
97
|
+
const { line1, line2 } = formatFinishLine({
|
|
98
|
+
elapsedMs: browserResult.tookMs,
|
|
99
|
+
model: `${runOptions.model}[browser]`,
|
|
100
|
+
tokensPart,
|
|
101
|
+
detailParts: [runOptions.file && runOptions.file.length > 0 ? `files=${runOptions.file.length}` : null],
|
|
102
|
+
});
|
|
103
|
+
log(chalk.blue(line1));
|
|
104
|
+
if (line2) {
|
|
105
|
+
log(chalk.dim(line2));
|
|
95
106
|
}
|
|
96
|
-
log(chalk.blue(`Finished in ${formatElapsed(browserResult.tookMs)} (${statsParts.join(' | ')})`));
|
|
97
107
|
return {
|
|
98
108
|
usage,
|
|
99
109
|
elapsedMs: browserResult.tookMs,
|
package/dist/src/cli/options.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { InvalidArgumentError } from 'commander';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
2
4
|
import { DEFAULT_MODEL, MODEL_CONFIGS } from '../oracle.js';
|
|
3
5
|
export function collectPaths(value, previous = []) {
|
|
4
6
|
if (!value) {
|
|
@@ -17,6 +19,31 @@ export function mergePathLikeOptions(file, include, filesAlias, pathAlias, paths
|
|
|
17
19
|
const withPathAlias = collectPaths(pathAlias, withFilesAlias);
|
|
18
20
|
return collectPaths(pathsAlias, withPathAlias);
|
|
19
21
|
}
|
|
22
|
+
export function dedupePathInputs(inputs, { cwd = process.cwd() } = {}) {
|
|
23
|
+
const deduped = [];
|
|
24
|
+
const duplicates = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const entry of inputs ?? []) {
|
|
27
|
+
const raw = entry?.trim();
|
|
28
|
+
if (!raw)
|
|
29
|
+
continue;
|
|
30
|
+
let key = raw;
|
|
31
|
+
if (!raw.startsWith('!') && !fg.isDynamicPattern(raw)) {
|
|
32
|
+
const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
33
|
+
key = `path:${path.normalize(absolute)}`;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
key = `pattern:${raw}`;
|
|
37
|
+
}
|
|
38
|
+
if (seen.has(key)) {
|
|
39
|
+
duplicates.push(raw);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
seen.add(key);
|
|
43
|
+
deduped.push(raw);
|
|
44
|
+
}
|
|
45
|
+
return { deduped, duplicates };
|
|
46
|
+
}
|
|
20
47
|
export function collectModelList(value, previous = []) {
|
|
21
48
|
if (!value) {
|
|
22
49
|
return previous;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import kleur from 'kleur';
|
|
3
3
|
import { renderMarkdownAnsi } from './markdownRenderer.js';
|
|
4
|
-
import {
|
|
4
|
+
import { formatFinishLine } from '../oracle/finishLine.js';
|
|
5
5
|
import { sessionStore, wait } from '../sessionStore.js';
|
|
6
6
|
import { formatTokenCount, formatTokenValue } from '../oracle/runUtils.js';
|
|
7
7
|
import { resumeBrowserSession } from '../browser/reattach.js';
|
|
@@ -92,7 +92,7 @@ export async function attachSession(sessionId, options) {
|
|
|
92
92
|
if (message) {
|
|
93
93
|
console.log(dim(message));
|
|
94
94
|
}
|
|
95
|
-
}), { verbose: true }));
|
|
95
|
+
}), { verbose: true }), { promptPreview: metadata.promptPreview });
|
|
96
96
|
const outputTokens = estimateTokenCount(result.answerMarkdown);
|
|
97
97
|
const logWriter = sessionStore.createLogWriter(sessionId);
|
|
98
98
|
logWriter.logLine('[reattach] captured assistant response from existing Chrome tab');
|
|
@@ -516,7 +516,6 @@ export function formatCompletionSummary(metadata, options = {}) {
|
|
|
516
516
|
const modeLabel = metadata.mode === 'browser' ? `${metadata.model ?? 'n/a'}[browser]` : metadata.model ?? 'n/a';
|
|
517
517
|
const usage = metadata.usage;
|
|
518
518
|
const cost = resolveSessionCost(metadata);
|
|
519
|
-
const costPart = cost != null ? ` | ${formatUSD(cost)}` : '';
|
|
520
519
|
const tokensDisplay = [
|
|
521
520
|
usage.inputTokens ?? 0,
|
|
522
521
|
usage.outputTokens ?? 0,
|
|
@@ -530,10 +529,23 @@ export function formatCompletionSummary(metadata, options = {}) {
|
|
|
530
529
|
total_tokens: usage.totalTokens,
|
|
531
530
|
}, index))
|
|
532
531
|
.join('/');
|
|
532
|
+
const tokensPart = (() => {
|
|
533
|
+
const parts = tokensDisplay.split('/');
|
|
534
|
+
if (parts.length !== 4)
|
|
535
|
+
return tokensDisplay;
|
|
536
|
+
return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
|
|
537
|
+
})();
|
|
533
538
|
const filesCount = metadata.options?.file?.length ?? 0;
|
|
534
|
-
const filesPart = filesCount > 0 ? `
|
|
535
|
-
const slugPart = options.includeSlug ? `
|
|
536
|
-
|
|
539
|
+
const filesPart = filesCount > 0 ? `files=${filesCount}` : null;
|
|
540
|
+
const slugPart = options.includeSlug ? `slug=${metadata.id}` : null;
|
|
541
|
+
const { line1, line2 } = formatFinishLine({
|
|
542
|
+
elapsedMs: metadata.elapsedMs,
|
|
543
|
+
model: modeLabel,
|
|
544
|
+
costUsd: cost ?? null,
|
|
545
|
+
tokensPart,
|
|
546
|
+
detailParts: [filesPart, slugPart],
|
|
547
|
+
});
|
|
548
|
+
return line2 ? `${line1} | ${line2}` : line1;
|
|
537
549
|
}
|
|
538
550
|
async function readStoredPrompt(sessionId) {
|
|
539
551
|
const request = await sessionStore.readRequest(sessionId);
|
|
@@ -15,10 +15,9 @@ import { resolveModelConfig } from '../oracle/modelResolver.js';
|
|
|
15
15
|
import { buildPrompt, buildRequestBody } from '../oracle/request.js';
|
|
16
16
|
import { estimateRequestTokens } from '../oracle/tokenEstimate.js';
|
|
17
17
|
import { formatTokenEstimate, formatTokenValue } from '../oracle/runUtils.js';
|
|
18
|
-
import {
|
|
18
|
+
import { formatFinishLine } from '../oracle/finishLine.js';
|
|
19
19
|
import { sanitizeOscProgress } from './oscUtils.js';
|
|
20
20
|
import { readFiles } from '../oracle/files.js';
|
|
21
|
-
import { formatUSD } from '../oracle/format.js';
|
|
22
21
|
import { cwd as getCwd } from 'node:process';
|
|
23
22
|
const isTty = process.stdout.isTTY;
|
|
24
23
|
const dim = (text) => (isTty ? kleur.dim(text) : text);
|
|
@@ -211,10 +210,21 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
211
210
|
total_tokens: aggregateUsage.totalTokens,
|
|
212
211
|
}, idx))
|
|
213
212
|
.join('/');
|
|
214
|
-
const
|
|
213
|
+
const tokensPart = (() => {
|
|
214
|
+
const parts = tokensDisplay.split('/');
|
|
215
|
+
if (parts.length !== 4)
|
|
216
|
+
return tokensDisplay;
|
|
217
|
+
return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
|
|
218
|
+
})();
|
|
215
219
|
const statusColor = summary.rejected.length === 0 ? kleur.green : summary.fulfilled.length > 0 ? kleur.yellow : kleur.red;
|
|
216
220
|
const overallText = `${summary.fulfilled.length}/${multiModels.length} models`;
|
|
217
|
-
|
|
221
|
+
const { line1 } = formatFinishLine({
|
|
222
|
+
elapsedMs: summary.elapsedMs,
|
|
223
|
+
model: overallText,
|
|
224
|
+
costUsd: aggregateUsage.cost ?? null,
|
|
225
|
+
tokensPart,
|
|
226
|
+
});
|
|
227
|
+
log(statusColor(line1));
|
|
218
228
|
const hasFailure = summary.rejected.length > 0;
|
|
219
229
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
220
230
|
status: hasFailure ? 'error' : 'completed',
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { formatUSD } from './format.js';
|
|
2
|
+
export function formatElapsedCompact(ms) {
|
|
3
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
4
|
+
return 'unknown';
|
|
5
|
+
}
|
|
6
|
+
if (ms < 60_000) {
|
|
7
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
8
|
+
}
|
|
9
|
+
if (ms < 60 * 60_000) {
|
|
10
|
+
const minutes = Math.floor(ms / 60_000);
|
|
11
|
+
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
12
|
+
return `${minutes}m${seconds.toString().padStart(2, '0')}s`;
|
|
13
|
+
}
|
|
14
|
+
const hours = Math.floor(ms / (60 * 60_000));
|
|
15
|
+
const minutes = Math.floor((ms % (60 * 60_000)) / 60_000);
|
|
16
|
+
return `${hours}h${minutes.toString().padStart(2, '0')}m`;
|
|
17
|
+
}
|
|
18
|
+
export function formatFinishLine({ elapsedMs, model, costUsd, tokensPart, summaryExtraParts, detailParts, }) {
|
|
19
|
+
const line1Parts = [
|
|
20
|
+
formatElapsedCompact(elapsedMs),
|
|
21
|
+
typeof costUsd === 'number' ? formatUSD(costUsd) : null,
|
|
22
|
+
model,
|
|
23
|
+
tokensPart,
|
|
24
|
+
...(summaryExtraParts ?? []),
|
|
25
|
+
];
|
|
26
|
+
const line1 = line1Parts.filter((part) => typeof part === 'string' && part.length > 0).join(' · ');
|
|
27
|
+
const line2Parts = (detailParts ?? []).filter((part) => typeof part === 'string' && part.length > 0);
|
|
28
|
+
if (line2Parts.length === 0) {
|
|
29
|
+
return { line1 };
|
|
30
|
+
}
|
|
31
|
+
return { line1, line2: line2Parts.join(' | ') };
|
|
32
|
+
}
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -8,7 +8,8 @@ import { DEFAULT_SYSTEM_PROMPT, MODEL_CONFIGS, TOKENIZER_OPTIONS } from './confi
|
|
|
8
8
|
import { readFiles } from './files.js';
|
|
9
9
|
import { buildPrompt, buildRequestBody } from './request.js';
|
|
10
10
|
import { estimateRequestTokens } from './tokenEstimate.js';
|
|
11
|
-
import { formatElapsed
|
|
11
|
+
import { formatElapsed } from './format.js';
|
|
12
|
+
import { formatFinishLine } from './finishLine.js';
|
|
12
13
|
import { getFileTokenStats, printFileTokenStats } from './tokenStats.js';
|
|
13
14
|
import { OracleResponseError, OracleTransportError, PromptValidationError, describeTransportError, toTransportError, } from './errors.js';
|
|
14
15
|
import { createDefaultClientFactory } from './client.js';
|
|
@@ -530,46 +531,48 @@ export async function runOracle(options, deps = {}) {
|
|
|
530
531
|
pricing: { inputUsdPerToken: pricing.inputPerToken, outputUsdPerToken: pricing.outputPerToken },
|
|
531
532
|
})?.totalUsd
|
|
532
533
|
: undefined;
|
|
533
|
-
const elapsedDisplay = formatElapsed(elapsedMs);
|
|
534
|
-
const statsParts = [];
|
|
535
534
|
const effortLabel = modelConfig.reasoning?.effort;
|
|
536
535
|
const modelLabel = effortLabel ? `${modelConfig.model}[${effortLabel}]` : modelConfig.model;
|
|
537
536
|
const sessionIdContainsModel = typeof options.sessionId === 'string' && options.sessionId.toLowerCase().includes(modelConfig.model.toLowerCase());
|
|
538
|
-
// Avoid duplicating the model name in the prefix (session id) and the stats bundle; keep a single source of truth.
|
|
539
|
-
if (!sessionIdContainsModel) {
|
|
540
|
-
statsParts.push(modelLabel);
|
|
541
|
-
}
|
|
542
|
-
if (cost != null) {
|
|
543
|
-
statsParts.push(formatUSD(cost));
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
statsParts.push('cost=N/A');
|
|
547
|
-
}
|
|
548
537
|
const tokensDisplay = [inputTokens, outputTokens, reasoningTokens, totalTokens]
|
|
549
538
|
.map((value, index) => formatTokenValue(value, usage, index))
|
|
550
539
|
.join('/');
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
const
|
|
540
|
+
const tokensPart = (() => {
|
|
541
|
+
const parts = tokensDisplay.split('/');
|
|
542
|
+
if (parts.length !== 4)
|
|
543
|
+
return tokensDisplay;
|
|
544
|
+
return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
|
|
545
|
+
})();
|
|
546
|
+
const modelPart = sessionIdContainsModel ? null : modelLabel;
|
|
547
|
+
const actualInput = usage.input_tokens;
|
|
548
|
+
const estActualPart = (() => {
|
|
549
|
+
if (!options.verbose)
|
|
550
|
+
return null;
|
|
551
|
+
if (actualInput === undefined)
|
|
552
|
+
return null;
|
|
553
|
+
const delta = actualInput - estimatedInputTokens;
|
|
554
|
+
const deltaText = delta === 0 ? '' : delta > 0 ? ` (+${delta.toLocaleString()})` : ` (${delta.toLocaleString()})`;
|
|
555
|
+
return `est→actual=${estimatedInputTokens.toLocaleString()}→${actualInput.toLocaleString()}${deltaText}`;
|
|
556
|
+
})();
|
|
557
|
+
const { line1, line2 } = formatFinishLine({
|
|
558
|
+
elapsedMs,
|
|
559
|
+
model: modelPart,
|
|
560
|
+
costUsd: cost ?? null,
|
|
561
|
+
tokensPart,
|
|
562
|
+
summaryExtraParts: options.sessionId ? [`sid=${options.sessionId}`] : null,
|
|
563
|
+
detailParts: [
|
|
564
|
+
estActualPart,
|
|
565
|
+
!searchEnabled ? 'search=off' : null,
|
|
566
|
+
files.length > 0 ? `files=${files.length}` : null,
|
|
567
|
+
],
|
|
568
|
+
});
|
|
569
569
|
if (!options.silent) {
|
|
570
570
|
log('');
|
|
571
571
|
}
|
|
572
|
-
log(chalk.blue(
|
|
572
|
+
log(chalk.blue(line1));
|
|
573
|
+
if (line2) {
|
|
574
|
+
log(dim(line2));
|
|
575
|
+
}
|
|
573
576
|
return {
|
|
574
577
|
mode: 'live',
|
|
575
578
|
response,
|
|
@@ -445,6 +445,22 @@ async function markZombie(meta, { persist }) {
|
|
|
445
445
|
if (!isZombie(meta)) {
|
|
446
446
|
return meta;
|
|
447
447
|
}
|
|
448
|
+
if (meta.mode === 'browser') {
|
|
449
|
+
const runtime = meta.browser?.runtime;
|
|
450
|
+
if (runtime) {
|
|
451
|
+
const signals = [];
|
|
452
|
+
if (runtime.chromePid) {
|
|
453
|
+
signals.push(isProcessAlive(runtime.chromePid));
|
|
454
|
+
}
|
|
455
|
+
if (runtime.chromePort) {
|
|
456
|
+
const host = runtime.chromeHost ?? '127.0.0.1';
|
|
457
|
+
signals.push(await isPortOpen(host, runtime.chromePort));
|
|
458
|
+
}
|
|
459
|
+
if (signals.some(Boolean)) {
|
|
460
|
+
return meta;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
448
464
|
const updated = {
|
|
449
465
|
...meta,
|
|
450
466
|
status: 'error',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.2 Pro (via gpt-5.1-pro alias), GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bin/oracle-cli.js",
|