@steipete/oracle 0.7.2 → 0.7.3
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/dist/src/browser/chromeLifecycle.js +2 -0
- package/dist/src/browser/index.js +6 -0
- package/dist/src/browser/reattach.js +192 -17
- package/dist/src/cli/notifier.js +8 -2
- package/dist/src/cli/oscUtils.js +1 -19
- package/dist/src/cli/sessionDisplay.js +6 -3
- package/dist/src/cli/sessionTable.js +5 -1
- package/dist/src/oracle/files.js +8 -1
- package/dist/src/oracle/modelResolver.js +11 -4
- package/dist/src/oracle/multiModelRunner.js +3 -14
- package/dist/src/oracle/oscProgress.js +12 -61
- package/dist/src/oracle/run.js +62 -34
- package/dist/src/sessionManager.js +91 -2
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +43 -26
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +0 -0
|
@@ -25,6 +25,7 @@ export async function launchChrome(config, userDataDir, logger) {
|
|
|
25
25
|
chromePath: config.chromePath ?? undefined,
|
|
26
26
|
chromeFlags,
|
|
27
27
|
userDataDir,
|
|
28
|
+
handleSIGINT: false,
|
|
28
29
|
port: debugPort ?? undefined,
|
|
29
30
|
});
|
|
30
31
|
const pidLabel = typeof launcher.pid === 'number' ? ` (pid ${launcher.pid})` : '';
|
|
@@ -216,6 +217,7 @@ async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host
|
|
|
216
217
|
chromePath: chromePath ?? undefined,
|
|
217
218
|
chromeFlags,
|
|
218
219
|
userDataDir,
|
|
220
|
+
handleSIGINT: false,
|
|
219
221
|
port: requestedPort ?? undefined,
|
|
220
222
|
});
|
|
221
223
|
if (host) {
|
|
@@ -37,12 +37,14 @@ export async function runBrowserMode(options) {
|
|
|
37
37
|
if (!runtimeHintCb || !chrome?.port) {
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
+
const conversationId = lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined;
|
|
40
41
|
const hint = {
|
|
41
42
|
chromePid: chrome.pid,
|
|
42
43
|
chromePort: chrome.port,
|
|
43
44
|
chromeHost,
|
|
44
45
|
chromeTargetId: lastTargetId,
|
|
45
46
|
tabUrl: lastUrl,
|
|
47
|
+
conversationId,
|
|
46
48
|
userDataDir,
|
|
47
49
|
controllerPid: process.pid,
|
|
48
50
|
};
|
|
@@ -947,6 +949,10 @@ function isWsl() {
|
|
|
947
949
|
return true;
|
|
948
950
|
return os.release().toLowerCase().includes('microsoft');
|
|
949
951
|
}
|
|
952
|
+
function extractConversationIdFromUrl(url) {
|
|
953
|
+
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
954
|
+
return match?.[1];
|
|
955
|
+
}
|
|
950
956
|
async function resolveUserDataBaseDir() {
|
|
951
957
|
// On WSL, Chrome launched via Windows can choke on UNC paths; prefer a Windows-backed temp folder.
|
|
952
958
|
if (isWsl()) {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import CDP from 'chrome-remote-interface';
|
|
2
|
-
import
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { mkdtemp, mkdir, rm } from 'node:fs/promises';
|
|
5
|
+
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from './pageActions.js';
|
|
6
|
+
import { launchChrome, connectToChrome, hideChromeWindow } from './chromeLifecycle.js';
|
|
7
|
+
import { resolveBrowserConfig } from './config.js';
|
|
8
|
+
import { syncCookies } from './cookies.js';
|
|
9
|
+
import { CHATGPT_URL } from './constants.js';
|
|
10
|
+
import { delay } from './utils.js';
|
|
3
11
|
function pickTarget(targets, runtime) {
|
|
4
12
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
5
13
|
return undefined;
|
|
@@ -18,33 +26,114 @@ function pickTarget(targets, runtime) {
|
|
|
18
26
|
return targets.find((t) => t.type === 'page') ?? targets[0];
|
|
19
27
|
}
|
|
20
28
|
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
29
|
+
const recoverSession = deps.recoverSession ??
|
|
30
|
+
(async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
|
|
21
31
|
if (!runtime.chromePort) {
|
|
22
|
-
|
|
32
|
+
logger('No running Chrome detected; reopening browser to locate the session.');
|
|
33
|
+
return recoverSession(runtime, config);
|
|
23
34
|
}
|
|
24
35
|
const host = runtime.chromeHost ?? '127.0.0.1';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
try {
|
|
37
|
+
const listTargets = deps.listTargets ??
|
|
38
|
+
(async () => {
|
|
39
|
+
const targets = await CDP.List({ host, port: runtime.chromePort });
|
|
40
|
+
return targets;
|
|
41
|
+
});
|
|
42
|
+
const connect = deps.connect ?? ((options) => CDP(options));
|
|
43
|
+
const targetList = (await listTargets());
|
|
44
|
+
const target = pickTarget(targetList, runtime);
|
|
45
|
+
const client = (await connect({
|
|
46
|
+
host,
|
|
47
|
+
port: runtime.chromePort,
|
|
48
|
+
target: target?.targetId,
|
|
49
|
+
}));
|
|
50
|
+
const { Runtime, DOM } = client;
|
|
51
|
+
if (Runtime?.enable) {
|
|
52
|
+
await Runtime.enable();
|
|
53
|
+
}
|
|
54
|
+
if (DOM && typeof DOM.enable === 'function') {
|
|
55
|
+
await DOM.enable();
|
|
56
|
+
}
|
|
57
|
+
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
58
|
+
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
59
|
+
const timeoutMs = config?.timeoutMs ?? 120_000;
|
|
60
|
+
const answer = await waitForResponse(Runtime, timeoutMs, logger);
|
|
61
|
+
const markdown = (await captureMarkdown(Runtime, answer.meta, logger)) ?? answer.text;
|
|
62
|
+
if (client && typeof client.close === 'function') {
|
|
63
|
+
try {
|
|
64
|
+
await client.close();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { answerText: answer.text, answerMarkdown: markdown };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
logger(`Existing Chrome reattach failed (${message}); reopening browser to locate the session.`);
|
|
75
|
+
return recoverSession(runtime, config);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
79
|
+
const resolved = resolveBrowserConfig(config ?? {});
|
|
80
|
+
const manualLogin = Boolean(resolved.manualLogin);
|
|
81
|
+
const userDataDir = manualLogin
|
|
82
|
+
? resolved.manualLoginProfileDir ?? path.join(os.homedir(), '.oracle', 'browser-profile')
|
|
83
|
+
: await mkdtemp(path.join(os.tmpdir(), 'oracle-reattach-'));
|
|
84
|
+
if (manualLogin) {
|
|
85
|
+
await mkdir(userDataDir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
const chrome = await launchChrome(resolved, userDataDir, logger);
|
|
88
|
+
const chromeHost = chrome.host ?? '127.0.0.1';
|
|
89
|
+
const client = await connectToChrome(chrome.port, logger, chromeHost);
|
|
90
|
+
const { Network, Page, Runtime, DOM } = client;
|
|
39
91
|
if (Runtime?.enable) {
|
|
40
92
|
await Runtime.enable();
|
|
41
93
|
}
|
|
42
94
|
if (DOM && typeof DOM.enable === 'function') {
|
|
43
95
|
await DOM.enable();
|
|
44
96
|
}
|
|
97
|
+
if (!resolved.headless && resolved.hideWindow) {
|
|
98
|
+
await hideChromeWindow(chrome, logger);
|
|
99
|
+
}
|
|
100
|
+
let appliedCookies = 0;
|
|
101
|
+
if (!manualLogin && resolved.cookieSync) {
|
|
102
|
+
appliedCookies = await syncCookies(Network, resolved.url, resolved.chromeProfile, logger, {
|
|
103
|
+
allowErrors: resolved.allowCookieErrors,
|
|
104
|
+
filterNames: resolved.cookieNames ?? undefined,
|
|
105
|
+
inlineCookies: resolved.inlineCookies ?? undefined,
|
|
106
|
+
cookiePath: resolved.chromeCookiePath ?? undefined,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
|
|
110
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
111
|
+
await ensureLoggedIn(Runtime, logger, { appliedCookies });
|
|
112
|
+
if (resolved.url !== CHATGPT_URL) {
|
|
113
|
+
await navigateToChatGPT(Page, Runtime, resolved.url, logger);
|
|
114
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
115
|
+
}
|
|
116
|
+
await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
|
|
117
|
+
const conversationUrl = buildConversationUrl(runtime, resolved.url);
|
|
118
|
+
if (conversationUrl) {
|
|
119
|
+
logger(`Reopening conversation at ${conversationUrl}`);
|
|
120
|
+
await navigateToChatGPT(Page, Runtime, conversationUrl, logger);
|
|
121
|
+
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
122
|
+
await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const opened = await openConversationFromSidebar(Runtime, {
|
|
126
|
+
conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ''),
|
|
127
|
+
preferProjects: resolved.url !== CHATGPT_URL,
|
|
128
|
+
});
|
|
129
|
+
if (!opened) {
|
|
130
|
+
throw new Error('Unable to locate prior ChatGPT conversation in sidebar.');
|
|
131
|
+
}
|
|
132
|
+
await waitForLocationChange(Runtime, 15_000);
|
|
133
|
+
}
|
|
45
134
|
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
46
135
|
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
47
|
-
const timeoutMs =
|
|
136
|
+
const timeoutMs = resolved.timeoutMs ?? 120_000;
|
|
48
137
|
const answer = await waitForResponse(Runtime, timeoutMs, logger);
|
|
49
138
|
const markdown = (await captureMarkdown(Runtime, answer.meta, logger)) ?? answer.text;
|
|
50
139
|
if (client && typeof client.close === 'function') {
|
|
@@ -55,5 +144,91 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
55
144
|
// ignore
|
|
56
145
|
}
|
|
57
146
|
}
|
|
147
|
+
if (!resolved.keepBrowser && !manualLogin) {
|
|
148
|
+
try {
|
|
149
|
+
await chrome.kill();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// ignore
|
|
153
|
+
}
|
|
154
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
155
|
+
}
|
|
58
156
|
return { answerText: answer.text, answerMarkdown: markdown };
|
|
59
157
|
}
|
|
158
|
+
function extractConversationIdFromUrl(url) {
|
|
159
|
+
if (!url)
|
|
160
|
+
return undefined;
|
|
161
|
+
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
162
|
+
return match?.[1];
|
|
163
|
+
}
|
|
164
|
+
function buildConversationUrl(runtime, baseUrl) {
|
|
165
|
+
if (runtime.tabUrl) {
|
|
166
|
+
return runtime.tabUrl;
|
|
167
|
+
}
|
|
168
|
+
const conversationId = runtime.conversationId;
|
|
169
|
+
if (!conversationId) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const base = new URL(baseUrl);
|
|
174
|
+
return `${base.origin}/c/${conversationId}`;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function openConversationFromSidebar(Runtime, options) {
|
|
181
|
+
const response = await Runtime.evaluate({
|
|
182
|
+
expression: `(() => {
|
|
183
|
+
const conversationId = ${JSON.stringify(options.conversationId ?? null)};
|
|
184
|
+
const preferProjects = ${JSON.stringify(Boolean(options.preferProjects))};
|
|
185
|
+
const nav = document.querySelector('nav') || document.querySelector('aside') || document.body;
|
|
186
|
+
if (preferProjects) {
|
|
187
|
+
const projectLink = Array.from(nav.querySelectorAll('a,button'))
|
|
188
|
+
.find((el) => (el.textContent || '').trim().toLowerCase() === 'projects');
|
|
189
|
+
if (projectLink) {
|
|
190
|
+
projectLink.click();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const links = Array.from(nav.querySelectorAll('a[href]'))
|
|
194
|
+
.filter((el) => el instanceof HTMLAnchorElement)
|
|
195
|
+
.map((el) => el);
|
|
196
|
+
const convoLinks = links.filter((el) => el.href.includes('/c/'));
|
|
197
|
+
let target = null;
|
|
198
|
+
if (conversationId) {
|
|
199
|
+
target = convoLinks.find((el) => el.href.includes('/c/' + conversationId));
|
|
200
|
+
}
|
|
201
|
+
if (!target && convoLinks.length > 0) {
|
|
202
|
+
target = convoLinks[0];
|
|
203
|
+
}
|
|
204
|
+
if (target) {
|
|
205
|
+
target.scrollIntoView({ block: 'center' });
|
|
206
|
+
target.click();
|
|
207
|
+
return { ok: true, href: target.href, count: convoLinks.length };
|
|
208
|
+
}
|
|
209
|
+
return { ok: false, count: convoLinks.length };
|
|
210
|
+
})()`,
|
|
211
|
+
returnByValue: true,
|
|
212
|
+
});
|
|
213
|
+
return Boolean(response.result?.value?.ok);
|
|
214
|
+
}
|
|
215
|
+
async function waitForLocationChange(Runtime, timeoutMs) {
|
|
216
|
+
const start = Date.now();
|
|
217
|
+
let lastHref = '';
|
|
218
|
+
while (Date.now() - start < timeoutMs) {
|
|
219
|
+
const { result } = await Runtime.evaluate({ expression: 'location.href', returnByValue: true });
|
|
220
|
+
const href = typeof result?.value === 'string' ? result.value : '';
|
|
221
|
+
if (lastHref && href !== lastHref) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
lastHref = href;
|
|
225
|
+
await delay(200);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
229
|
+
export const __test__ = {
|
|
230
|
+
pickTarget,
|
|
231
|
+
extractConversationIdFromUrl,
|
|
232
|
+
buildConversationUrl,
|
|
233
|
+
openConversationFromSidebar,
|
|
234
|
+
};
|
package/dist/src/cli/notifier.js
CHANGED
|
@@ -2,6 +2,7 @@ import notifier from 'toasted-notifier';
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { formatUSD, formatNumber } from '../oracle/format.js';
|
|
4
4
|
import { MODEL_CONFIGS } from '../oracle/config.js';
|
|
5
|
+
import { estimateUsdCost } from 'tokentally';
|
|
5
6
|
import fs from 'node:fs/promises';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { createRequire } from 'node:module';
|
|
@@ -131,8 +132,13 @@ function inferCost(payload) {
|
|
|
131
132
|
const config = MODEL_CONFIGS[model];
|
|
132
133
|
if (!config?.pricing)
|
|
133
134
|
return undefined;
|
|
134
|
-
return (
|
|
135
|
-
usage.outputTokens
|
|
135
|
+
return (estimateUsdCost({
|
|
136
|
+
usage: { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens },
|
|
137
|
+
pricing: {
|
|
138
|
+
inputUsdPerToken: config.pricing.inputPerToken,
|
|
139
|
+
outputUsdPerToken: config.pricing.outputPerToken,
|
|
140
|
+
},
|
|
141
|
+
})?.totalUsd ?? undefined);
|
|
136
142
|
}
|
|
137
143
|
function parseToggle(value) {
|
|
138
144
|
if (value == null)
|
package/dist/src/cli/oscUtils.js
CHANGED
|
@@ -1,20 +1,2 @@
|
|
|
1
1
|
// Utilities for handling OSC progress codes embedded in stored logs.
|
|
2
|
-
|
|
3
|
-
const OSC_END = '\u001b\\';
|
|
4
|
-
/**
|
|
5
|
-
* Optionally removes OSC 9;4 progress sequences (used by Ghostty/WezTerm to show progress bars).
|
|
6
|
-
* Keep them when replaying to a real TTY; strip when piping to non-TTY outputs.
|
|
7
|
-
*/
|
|
8
|
-
export function sanitizeOscProgress(text, keepOsc) {
|
|
9
|
-
if (keepOsc) {
|
|
10
|
-
return text;
|
|
11
|
-
}
|
|
12
|
-
let current = text;
|
|
13
|
-
while (current.includes(OSC_PROGRESS_PREFIX)) {
|
|
14
|
-
const start = current.indexOf(OSC_PROGRESS_PREFIX);
|
|
15
|
-
const end = current.indexOf(OSC_END, start + OSC_PROGRESS_PREFIX.length);
|
|
16
|
-
const cutEnd = end === -1 ? start + OSC_PROGRESS_PREFIX.length : end + OSC_END.length;
|
|
17
|
-
current = `${current.slice(0, start)}${current.slice(cutEnd)}`;
|
|
18
|
-
}
|
|
19
|
-
return current;
|
|
20
|
-
}
|
|
2
|
+
export { sanitizeOscProgress } from 'osc-progress';
|
|
@@ -76,10 +76,13 @@ export async function attachSession(sessionId, options) {
|
|
|
76
76
|
const isVerbose = Boolean(process.env.ORACLE_VERBOSE_RENDER);
|
|
77
77
|
const runtime = metadata.browser?.runtime;
|
|
78
78
|
const controllerAlive = isProcessAlive(runtime?.controllerPid);
|
|
79
|
-
const
|
|
79
|
+
const hasChromeDisconnect = metadata.response?.incompleteReason === 'chrome-disconnected';
|
|
80
|
+
const statusAllowsReattach = metadata.status === 'running' || (metadata.status === 'error' && hasChromeDisconnect);
|
|
81
|
+
const hasFallbackSessionInfo = Boolean(runtime?.chromePort || runtime?.tabUrl || runtime?.conversationId);
|
|
82
|
+
const canReattach = statusAllowsReattach &&
|
|
80
83
|
metadata.mode === 'browser' &&
|
|
81
|
-
|
|
82
|
-
(
|
|
84
|
+
hasFallbackSessionInfo &&
|
|
85
|
+
(hasChromeDisconnect || (runtime?.controllerPid && !controllerAlive));
|
|
83
86
|
if (canReattach) {
|
|
84
87
|
const portInfo = runtime?.chromePort ? `port ${runtime.chromePort}` : 'unknown port';
|
|
85
88
|
const urlInfo = runtime?.tabUrl ? `url=${runtime.tabUrl}` : 'url=unknown';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import kleur from 'kleur';
|
|
3
3
|
import { MODEL_CONFIGS } from '../oracle.js';
|
|
4
|
+
import { estimateUsdCost } from 'tokentally';
|
|
4
5
|
const isRich = (rich) => rich ?? Boolean(process.stdout.isTTY && chalk.level > 0);
|
|
5
6
|
const dim = (text, rich) => (rich ? kleur.dim(text) : text);
|
|
6
7
|
export const STATUS_PAD = 9;
|
|
@@ -48,7 +49,10 @@ export function resolveSessionCost(meta) {
|
|
|
48
49
|
}
|
|
49
50
|
const input = meta.usage.inputTokens ?? 0;
|
|
50
51
|
const output = meta.usage.outputTokens ?? 0;
|
|
51
|
-
const cost =
|
|
52
|
+
const cost = estimateUsdCost({
|
|
53
|
+
usage: { inputTokens: input, outputTokens: output },
|
|
54
|
+
pricing: { inputUsdPerToken: pricing.inputPerToken, outputUsdPerToken: pricing.outputPerToken },
|
|
55
|
+
})?.totalUsd ?? 0;
|
|
52
56
|
return cost > 0 ? cost : null;
|
|
53
57
|
}
|
|
54
58
|
export function formatTimestampAligned(iso) {
|
package/dist/src/oracle/files.js
CHANGED
|
@@ -13,7 +13,14 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
|
|
|
13
13
|
const useNativeFilesystem = fsModule === DEFAULT_FS || isNativeFsModule(fsModule);
|
|
14
14
|
let candidatePaths = [];
|
|
15
15
|
if (useNativeFilesystem) {
|
|
16
|
-
|
|
16
|
+
if (partitioned.globPatterns.length === 0 &&
|
|
17
|
+
partitioned.excludePatterns.length === 0 &&
|
|
18
|
+
partitioned.literalDirectories.length === 0) {
|
|
19
|
+
candidatePaths = Array.from(new Set(partitioned.literalFiles));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
candidatePaths = await expandWithNativeGlob(partitioned, cwd);
|
|
23
|
+
}
|
|
17
24
|
}
|
|
18
25
|
else {
|
|
19
26
|
if (partitioned.globPatterns.length > 0 || partitioned.excludePatterns.length > 0) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MODEL_CONFIGS, PRO_MODELS } from './config.js';
|
|
2
2
|
import { countTokens as countTokensGpt5Pro } from 'gpt-tokenizer/model/gpt-5-pro';
|
|
3
|
+
import { pricingFromUsdPerMillion } from 'tokentally';
|
|
3
4
|
const OPENROUTER_DEFAULT_BASE = 'https://openrouter.ai/api/v1';
|
|
4
5
|
const OPENROUTER_MODELS_ENDPOINT = 'https://openrouter.ai/api/v1/models';
|
|
5
6
|
export function isKnownModel(model) {
|
|
@@ -96,10 +97,16 @@ export async function resolveModelConfig(model, options = {}) {
|
|
|
96
97
|
provider: known?.provider ?? 'other',
|
|
97
98
|
inputLimit: info.context_length ?? known?.inputLimit ?? 200_000,
|
|
98
99
|
pricing: info.pricing && info.pricing.prompt != null && info.pricing.completion != null
|
|
99
|
-
? {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
? (() => {
|
|
101
|
+
const pricing = pricingFromUsdPerMillion({
|
|
102
|
+
inputUsdPerMillion: info.pricing.prompt,
|
|
103
|
+
outputUsdPerMillion: info.pricing.completion,
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
inputPerToken: pricing.inputUsdPerToken,
|
|
107
|
+
outputPerToken: pricing.outputUsdPerToken,
|
|
108
|
+
};
|
|
109
|
+
})()
|
|
103
110
|
: known?.pricing ?? null,
|
|
104
111
|
supportsBackground: known?.supportsBackground ?? true,
|
|
105
112
|
supportsSearch: known?.supportsSearch ?? true,
|
|
@@ -2,24 +2,13 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from '../oracle.js';
|
|
4
4
|
import { sessionStore } from '../sessionStore.js';
|
|
5
|
-
|
|
6
|
-
const OSC_PROGRESS_END = '\u001b\\';
|
|
5
|
+
import { findOscProgressSequences, OSC_PROGRESS_PREFIX } from 'osc-progress';
|
|
7
6
|
function forwardOscProgress(chunk, shouldForward) {
|
|
8
7
|
if (!shouldForward || !chunk.includes(OSC_PROGRESS_PREFIX)) {
|
|
9
8
|
return;
|
|
10
9
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const start = chunk.indexOf(OSC_PROGRESS_PREFIX, searchFrom);
|
|
14
|
-
if (start === -1) {
|
|
15
|
-
break;
|
|
16
|
-
}
|
|
17
|
-
const end = chunk.indexOf(OSC_PROGRESS_END, start + OSC_PROGRESS_PREFIX.length);
|
|
18
|
-
if (end === -1) {
|
|
19
|
-
break;
|
|
20
|
-
}
|
|
21
|
-
process.stdout.write(chunk.slice(start, end + OSC_PROGRESS_END.length));
|
|
22
|
-
searchFrom = end + OSC_PROGRESS_END.length;
|
|
10
|
+
for (const seq of findOscProgressSequences(chunk)) {
|
|
11
|
+
process.stdout.write(seq.raw);
|
|
23
12
|
}
|
|
24
13
|
}
|
|
25
14
|
const defaultDeps = {
|
|
@@ -1,66 +1,17 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
-
|
|
3
|
-
const ST = '\u001b\\';
|
|
4
|
-
function sanitizeLabel(label) {
|
|
5
|
-
const withoutEscape = label.split('\u001b').join('');
|
|
6
|
-
const withoutBellAndSt = withoutEscape.replaceAll('\u0007', '').replaceAll('\u009c', '');
|
|
7
|
-
return withoutBellAndSt.replaceAll(']', '').trim();
|
|
8
|
-
}
|
|
2
|
+
import { startOscProgress as startOscProgressShared, supportsOscProgress as supportsOscProgressShared, } from 'osc-progress';
|
|
9
3
|
export function supportsOscProgress(env = process.env, isTty = process.stdout.isTTY) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
if (env.ORACLE_FORCE_OSC_PROGRESS === '1') {
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
const termProgram = (env.TERM_PROGRAM ?? '').toLowerCase();
|
|
20
|
-
if (termProgram.includes('ghostty')) {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
if (termProgram.includes('wezterm')) {
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
if (env.WT_SESSION) {
|
|
27
|
-
return true; // Windows Terminal exposes this
|
|
28
|
-
}
|
|
29
|
-
return false;
|
|
4
|
+
return supportsOscProgressShared(env, isTty, {
|
|
5
|
+
disableEnvVar: 'ORACLE_NO_OSC_PROGRESS',
|
|
6
|
+
forceEnvVar: 'ORACLE_FORCE_OSC_PROGRESS',
|
|
7
|
+
});
|
|
30
8
|
}
|
|
31
9
|
export function startOscProgress(options = {}) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return () => {
|
|
40
|
-
write(`${OSC}0;0;${cleanLabel}${ST}`);
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
const target = Math.max(targetMs, 1_000);
|
|
44
|
-
const send = (state, percent) => {
|
|
45
|
-
const clamped = Math.max(0, Math.min(100, Math.round(percent)));
|
|
46
|
-
write(`${OSC}${state};${clamped};${cleanLabel}${ST}`);
|
|
47
|
-
};
|
|
48
|
-
const startedAt = Date.now();
|
|
49
|
-
send(1, 0); // activate progress bar
|
|
50
|
-
const timer = setInterval(() => {
|
|
51
|
-
const elapsed = Date.now() - startedAt;
|
|
52
|
-
const percent = Math.min(99, (elapsed / target) * 100);
|
|
53
|
-
send(1, percent);
|
|
54
|
-
}, 900);
|
|
55
|
-
timer.unref?.();
|
|
56
|
-
let stopped = false;
|
|
57
|
-
return () => {
|
|
58
|
-
// multiple callers may try to stop
|
|
59
|
-
if (stopped) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
stopped = true;
|
|
63
|
-
clearInterval(timer);
|
|
64
|
-
send(0, 0); // clear the progress bar
|
|
65
|
-
};
|
|
10
|
+
return startOscProgressShared({
|
|
11
|
+
...options,
|
|
12
|
+
// Preserve Oracle's previous default: progress emits to stdout.
|
|
13
|
+
write: options.write ?? ((text) => process.stdout.write(text)),
|
|
14
|
+
disableEnvVar: 'ORACLE_NO_OSC_PROGRESS',
|
|
15
|
+
forceEnvVar: 'ORACLE_FORCE_OSC_PROGRESS',
|
|
16
|
+
});
|
|
66
17
|
}
|
package/dist/src/oracle/run.js
CHANGED
|
@@ -19,8 +19,10 @@ import { createFsAdapter } from './fsAdapter.js';
|
|
|
19
19
|
import { resolveGeminiModelId } from './gemini.js';
|
|
20
20
|
import { resolveClaudeModelId } from './claude.js';
|
|
21
21
|
import { renderMarkdownAnsi } from '../cli/markdownRenderer.js';
|
|
22
|
+
import { createLiveRenderer } from 'markdansi';
|
|
22
23
|
import { executeBackgroundResponse } from './background.js';
|
|
23
24
|
import { formatTokenEstimate, formatTokenValue, resolvePreviewMode } from './runUtils.js';
|
|
25
|
+
import { estimateUsdCost } from 'tokentally';
|
|
24
26
|
import { defaultOpenRouterBaseUrl, isKnownModel, isOpenRouterBaseUrl, isProModel, resolveModelConfig, normalizeOpenRouterBaseUrl, } from './modelResolver.js';
|
|
25
27
|
const isStdoutTty = process.stdout.isTTY && chalk.level > 0;
|
|
26
28
|
const dim = (text) => (isStdoutTty ? kleur.dim(text) : text);
|
|
@@ -309,8 +311,8 @@ export async function runOracle(options, deps = {}) {
|
|
|
309
311
|
let response = null;
|
|
310
312
|
let elapsedMs = 0;
|
|
311
313
|
let sawTextDelta = false;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
+
let streamedText = '';
|
|
315
|
+
let lastLiveFrameAtMs = 0;
|
|
314
316
|
let answerHeaderPrinted = false;
|
|
315
317
|
const allowAnswerHeader = options.suppressAnswerHeader !== true;
|
|
316
318
|
const timeoutExceeded = () => now() - runStart >= timeoutMs;
|
|
@@ -378,31 +380,52 @@ export async function runOracle(options, deps = {}) {
|
|
|
378
380
|
},
|
|
379
381
|
});
|
|
380
382
|
}
|
|
383
|
+
let liveRenderer = null;
|
|
381
384
|
try {
|
|
385
|
+
liveRenderer =
|
|
386
|
+
isTty && !renderPlain
|
|
387
|
+
? createLiveRenderer({
|
|
388
|
+
write: stdoutWrite,
|
|
389
|
+
width: process.stdout.columns ?? 80,
|
|
390
|
+
renderFrame: renderMarkdownAnsi,
|
|
391
|
+
})
|
|
392
|
+
: null;
|
|
382
393
|
for await (const event of stream) {
|
|
383
394
|
throwIfTimedOut();
|
|
384
395
|
const isTextDelta = event.type === 'chunk' || event.type === 'response.output_text.delta';
|
|
385
|
-
if (isTextDelta)
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
396
|
+
if (!isTextDelta)
|
|
397
|
+
continue;
|
|
398
|
+
stopOscProgress();
|
|
399
|
+
stopHeartbeatNow();
|
|
400
|
+
sawTextDelta = true;
|
|
401
|
+
ensureAnswerHeader();
|
|
402
|
+
if (options.silent || typeof event.delta !== 'string')
|
|
403
|
+
continue;
|
|
404
|
+
// Always keep the log/bookkeeping sink up to date.
|
|
405
|
+
sinkWrite(event.delta);
|
|
406
|
+
if (renderPlain) {
|
|
407
|
+
// Plain mode: stream directly to stdout regardless of write sink.
|
|
408
|
+
stdoutWrite(event.delta);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (liveRenderer) {
|
|
412
|
+
streamedText += event.delta;
|
|
413
|
+
const currentMs = now();
|
|
414
|
+
const due = currentMs - lastLiveFrameAtMs >= 120;
|
|
415
|
+
const hasNewline = event.delta.includes('\n');
|
|
416
|
+
if (hasNewline || due) {
|
|
417
|
+
liveRenderer.render(streamedText);
|
|
418
|
+
lastLiveFrameAtMs = currentMs;
|
|
405
419
|
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// Non-TTY streams should still surface output; fall back to raw stdout.
|
|
423
|
+
stdoutWrite(event.delta);
|
|
424
|
+
}
|
|
425
|
+
if (liveRenderer) {
|
|
426
|
+
streamedText = streamedText.trim();
|
|
427
|
+
if (streamedText.length > 0) {
|
|
428
|
+
liveRenderer.render(streamedText);
|
|
406
429
|
}
|
|
407
430
|
}
|
|
408
431
|
throwIfTimedOut();
|
|
@@ -414,6 +437,9 @@ export async function runOracle(options, deps = {}) {
|
|
|
414
437
|
log(chalk.yellow(describeTransportError(transportError, timeoutMs)));
|
|
415
438
|
throw transportError;
|
|
416
439
|
}
|
|
440
|
+
finally {
|
|
441
|
+
liveRenderer?.finish();
|
|
442
|
+
}
|
|
417
443
|
response = await stream.finalResponse();
|
|
418
444
|
throwIfTimedOut();
|
|
419
445
|
stopHeartbeatNow();
|
|
@@ -428,21 +454,17 @@ export async function runOracle(options, deps = {}) {
|
|
|
428
454
|
}
|
|
429
455
|
// We only add spacing when streamed text was printed.
|
|
430
456
|
if (sawTextDelta && !options.silent) {
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
if (shouldRenderAfterStream) {
|
|
434
|
-
const rendered = renderMarkdownAnsi(fullStreamedText);
|
|
435
|
-
stdoutWrite(rendered);
|
|
436
|
-
if (!rendered.endsWith('\n')) {
|
|
437
|
-
stdoutWrite('\n');
|
|
438
|
-
}
|
|
439
|
-
log('');
|
|
440
|
-
}
|
|
441
|
-
else if (renderPlain) {
|
|
457
|
+
const shouldRenderAfterStream = isTty && !renderPlain && streamedText.length > 0;
|
|
458
|
+
if (renderPlain) {
|
|
442
459
|
// Plain streaming already wrote chunks; ensure clean separation.
|
|
443
460
|
stdoutWrite('\n');
|
|
444
461
|
}
|
|
462
|
+
else if (!shouldRenderAfterStream) {
|
|
463
|
+
// Non-TTY streams should still surface output; ensure separation.
|
|
464
|
+
log('');
|
|
465
|
+
}
|
|
445
466
|
else {
|
|
467
|
+
// Live-rendered mode already drew the final frame; only separate from logs.
|
|
446
468
|
log('');
|
|
447
469
|
}
|
|
448
470
|
}
|
|
@@ -503,7 +525,10 @@ export async function runOracle(options, deps = {}) {
|
|
|
503
525
|
const totalTokens = usage.total_tokens ?? inputTokens + outputTokens + reasoningTokens;
|
|
504
526
|
const pricing = modelConfig.pricing ?? undefined;
|
|
505
527
|
const cost = pricing
|
|
506
|
-
?
|
|
528
|
+
? estimateUsdCost({
|
|
529
|
+
usage: { inputTokens, outputTokens, reasoningTokens, totalTokens },
|
|
530
|
+
pricing: { inputUsdPerToken: pricing.inputPerToken, outputUsdPerToken: pricing.outputPerToken },
|
|
531
|
+
})?.totalUsd
|
|
507
532
|
: undefined;
|
|
508
533
|
const elapsedDisplay = formatElapsed(elapsedMs);
|
|
509
534
|
const statsParts = [];
|
|
@@ -541,6 +566,9 @@ export async function runOracle(options, deps = {}) {
|
|
|
541
566
|
statsParts.push(`files=${files.length}`);
|
|
542
567
|
}
|
|
543
568
|
const sessionPrefix = options.sessionId ? `${options.sessionId} ` : '';
|
|
569
|
+
if (!options.silent) {
|
|
570
|
+
log('');
|
|
571
|
+
}
|
|
544
572
|
log(chalk.blue(`Finished ${sessionPrefix}in ${elapsedDisplay} (${statsParts.join(' | ')})`));
|
|
545
573
|
return {
|
|
546
574
|
mode: 'live',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import { createWriteStream } from 'node:fs';
|
|
4
|
+
import net from 'node:net';
|
|
4
5
|
import { DEFAULT_MODEL } from './oracle.js';
|
|
5
6
|
import { safeModelSlug } from './oracle/modelResolver.js';
|
|
6
7
|
import { getOracleHomeDir } from './oracleHome.js';
|
|
@@ -15,6 +16,7 @@ const MODEL_JSON_EXTENSION = '.json';
|
|
|
15
16
|
const MODEL_LOG_EXTENSION = '.log';
|
|
16
17
|
const MAX_STATUS_LIMIT = 1000;
|
|
17
18
|
const ZOMBIE_MAX_AGE_MS = 60 * 60 * 1000; // 60 minutes
|
|
19
|
+
const CHROME_RUNTIME_TIMEOUT_MS = 250;
|
|
18
20
|
const DEFAULT_SLUG = 'session';
|
|
19
21
|
const MAX_SLUG_WORDS = 5;
|
|
20
22
|
const MIN_CUSTOM_SLUG_WORDS = 3;
|
|
@@ -245,7 +247,8 @@ async function readModernSessionMetadata(sessionId) {
|
|
|
245
247
|
return null;
|
|
246
248
|
}
|
|
247
249
|
const enriched = await attachModelRuns(parsed, sessionId);
|
|
248
|
-
|
|
250
|
+
const runtimeChecked = await markDeadBrowser(enriched, { persist: false });
|
|
251
|
+
return await markZombie(runtimeChecked, { persist: false });
|
|
249
252
|
}
|
|
250
253
|
catch {
|
|
251
254
|
return null;
|
|
@@ -256,7 +259,8 @@ async function readLegacySessionMetadata(sessionId) {
|
|
|
256
259
|
const raw = await fs.readFile(legacySessionPath(sessionId), 'utf8');
|
|
257
260
|
const parsed = JSON.parse(raw);
|
|
258
261
|
const enriched = await attachModelRuns(parsed, sessionId);
|
|
259
|
-
|
|
262
|
+
const runtimeChecked = await markDeadBrowser(enriched, { persist: false });
|
|
263
|
+
return await markZombie(runtimeChecked, { persist: false });
|
|
260
264
|
}
|
|
261
265
|
catch {
|
|
262
266
|
return null;
|
|
@@ -294,6 +298,7 @@ export async function listSessionsMetadata() {
|
|
|
294
298
|
for (const entry of entries) {
|
|
295
299
|
let meta = await readSessionMetadata(entry);
|
|
296
300
|
if (meta) {
|
|
301
|
+
meta = await markDeadBrowser(meta, { persist: true });
|
|
297
302
|
meta = await markZombie(meta, { persist: true }); // keep stored metadata consistent with zombie detection
|
|
298
303
|
metas.push(meta);
|
|
299
304
|
}
|
|
@@ -451,6 +456,44 @@ async function markZombie(meta, { persist }) {
|
|
|
451
456
|
}
|
|
452
457
|
return updated;
|
|
453
458
|
}
|
|
459
|
+
async function markDeadBrowser(meta, { persist }) {
|
|
460
|
+
if (meta.status !== 'running' || meta.mode !== 'browser') {
|
|
461
|
+
return meta;
|
|
462
|
+
}
|
|
463
|
+
const runtime = meta.browser?.runtime;
|
|
464
|
+
if (!runtime) {
|
|
465
|
+
return meta;
|
|
466
|
+
}
|
|
467
|
+
const signals = [];
|
|
468
|
+
if (runtime.chromePid) {
|
|
469
|
+
signals.push(isProcessAlive(runtime.chromePid));
|
|
470
|
+
}
|
|
471
|
+
if (runtime.chromePort) {
|
|
472
|
+
const host = runtime.chromeHost ?? '127.0.0.1';
|
|
473
|
+
signals.push(await isPortOpen(host, runtime.chromePort));
|
|
474
|
+
}
|
|
475
|
+
if (signals.length === 0 || signals.some(Boolean)) {
|
|
476
|
+
return meta;
|
|
477
|
+
}
|
|
478
|
+
const response = meta.response
|
|
479
|
+
? {
|
|
480
|
+
...meta.response,
|
|
481
|
+
status: 'error',
|
|
482
|
+
incompleteReason: meta.response.incompleteReason ?? 'chrome-disconnected',
|
|
483
|
+
}
|
|
484
|
+
: { status: 'error', incompleteReason: 'chrome-disconnected' };
|
|
485
|
+
const updated = {
|
|
486
|
+
...meta,
|
|
487
|
+
status: 'error',
|
|
488
|
+
errorMessage: 'Browser session ended (Chrome is no longer reachable)',
|
|
489
|
+
completedAt: new Date().toISOString(),
|
|
490
|
+
response,
|
|
491
|
+
};
|
|
492
|
+
if (persist) {
|
|
493
|
+
await fs.writeFile(metaPath(meta.id), JSON.stringify(updated, null, 2), 'utf8');
|
|
494
|
+
}
|
|
495
|
+
return updated;
|
|
496
|
+
}
|
|
454
497
|
function isZombie(meta) {
|
|
455
498
|
if (meta.status !== 'running') {
|
|
456
499
|
return false;
|
|
@@ -465,3 +508,49 @@ function isZombie(meta) {
|
|
|
465
508
|
}
|
|
466
509
|
return Date.now() - startedMs > ZOMBIE_MAX_AGE_MS;
|
|
467
510
|
}
|
|
511
|
+
function isProcessAlive(pid) {
|
|
512
|
+
if (!pid)
|
|
513
|
+
return false;
|
|
514
|
+
try {
|
|
515
|
+
process.kill(pid, 0);
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
const code = error instanceof Error ? error.code : undefined;
|
|
520
|
+
if (code === 'ESRCH' || code === 'EINVAL') {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
if (code === 'EPERM') {
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function isPortOpen(host, port) {
|
|
530
|
+
if (!port || port <= 0 || port > 65535) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
return new Promise((resolve) => {
|
|
534
|
+
const socket = net.createConnection({ host, port });
|
|
535
|
+
let settled = false;
|
|
536
|
+
const cleanup = (result) => {
|
|
537
|
+
if (settled)
|
|
538
|
+
return;
|
|
539
|
+
settled = true;
|
|
540
|
+
socket.removeAllListeners();
|
|
541
|
+
socket.end();
|
|
542
|
+
socket.destroy();
|
|
543
|
+
socket.unref();
|
|
544
|
+
resolve(result);
|
|
545
|
+
};
|
|
546
|
+
const timer = setTimeout(() => cleanup(false), CHROME_RUNTIME_TIMEOUT_MS);
|
|
547
|
+
socket.once('connect', () => {
|
|
548
|
+
clearTimeout(timer);
|
|
549
|
+
cleanup(true);
|
|
550
|
+
});
|
|
551
|
+
socket.once('error', () => {
|
|
552
|
+
clearTimeout(timer);
|
|
553
|
+
cleanup(false);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
}
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|
|
@@ -8,6 +8,26 @@
|
|
|
8
8
|
"oracle": "dist/bin/oracle-cli.js",
|
|
9
9
|
"oracle-mcp": "dist/bin/oracle-mcp.js"
|
|
10
10
|
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"docs:list": "tsx scripts/docs-list.ts",
|
|
13
|
+
"build": "tsc -p tsconfig.build.json && pnpm run build:vendor",
|
|
14
|
+
"build:vendor": "node -e \"const fs=require('fs'); const path=require('path'); const vendorRoot=path.join('dist','vendor'); fs.rmSync(vendorRoot,{recursive:true,force:true}); const vendors=[['oracle-notifier']]; vendors.forEach(([name])=>{const src=path.join('vendor',name); const dest=path.join(vendorRoot,name); fs.mkdirSync(dest,{recursive:true}); if(fs.existsSync(src)){fs.cpSync(src,dest,{recursive:true,force:true});}});\"",
|
|
15
|
+
"start": "pnpm run build && node ./dist/scripts/run-cli.js",
|
|
16
|
+
"oracle": "pnpm start",
|
|
17
|
+
"check": "pnpm run typecheck",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"lint": "pnpm run typecheck && biome lint .",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:mcp": "pnpm run build && pnpm run test:mcp:unit && pnpm run test:mcp:mcporter",
|
|
22
|
+
"test:mcp:unit": "vitest run tests/mcp*.test.ts tests/mcp/**/*.test.ts",
|
|
23
|
+
"test:mcp:mcporter": "npx -y mcporter list oracle-local --schema --config config/mcporter.json && npx -y mcporter call oracle-local.sessions limit:1 --config config/mcporter.json",
|
|
24
|
+
"test:browser": "pnpm run build && tsx scripts/test-browser.ts && ./scripts/browser-smoke.sh",
|
|
25
|
+
"test:live": "ORACLE_LIVE_TEST=1 vitest run tests/live --exclude tests/live/openai-live.test.ts",
|
|
26
|
+
"test:pro": "ORACLE_LIVE_TEST=1 vitest run tests/live/openai-live.test.ts",
|
|
27
|
+
"test:coverage": "vitest run --coverage",
|
|
28
|
+
"prepare": "pnpm run build",
|
|
29
|
+
"mcp": "pnpm run build && node ./dist/bin/oracle-mcp.js"
|
|
30
|
+
},
|
|
11
31
|
"files": [
|
|
12
32
|
"dist/**/*",
|
|
13
33
|
"assets-oracle-icon.png",
|
|
@@ -57,25 +77,27 @@
|
|
|
57
77
|
"json5": "^2.2.3",
|
|
58
78
|
"keytar": "^7.9.0",
|
|
59
79
|
"kleur": "^4.1.5",
|
|
60
|
-
"markdansi": "
|
|
61
|
-
"openai": "^6.
|
|
80
|
+
"markdansi": "0.1.7",
|
|
81
|
+
"openai": "^6.15.0",
|
|
82
|
+
"osc-progress": "^0.1.0",
|
|
62
83
|
"shiki": "^3.20.0",
|
|
63
84
|
"sqlite3": "^5.1.7",
|
|
64
85
|
"toasted-notifier": "^10.1.0",
|
|
86
|
+
"tokentally": "github:steipete/tokentally#v0.1.0",
|
|
65
87
|
"zod": "^4.2.1"
|
|
66
88
|
},
|
|
67
89
|
"devDependencies": {
|
|
68
90
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
69
|
-
"@biomejs/biome": "^2.3.
|
|
91
|
+
"@biomejs/biome": "^2.3.10",
|
|
70
92
|
"@cdktf/node-pty-prebuilt-multiarch": "0.10.2",
|
|
71
93
|
"@types/chrome-remote-interface": "^0.33.0",
|
|
72
94
|
"@types/inquirer": "^9.0.9",
|
|
73
95
|
"@types/node": "^25.0.3",
|
|
74
96
|
"@vitest/coverage-v8": "4.0.16",
|
|
75
|
-
"devtools-protocol": "0.0.
|
|
97
|
+
"devtools-protocol": "0.0.1561482",
|
|
76
98
|
"es-toolkit": "^1.43.0",
|
|
77
99
|
"esbuild": "^0.27.2",
|
|
78
|
-
"puppeteer-core": "^24.
|
|
100
|
+
"puppeteer-core": "^24.34.0",
|
|
79
101
|
"tsx": "^4.21.0",
|
|
80
102
|
"typescript": "^5.9.3",
|
|
81
103
|
"vitest": "^4.0.16"
|
|
@@ -83,23 +105,18 @@
|
|
|
83
105
|
"optionalDependencies": {
|
|
84
106
|
"win-dpapi": "npm:@primno/dpapi@2.0.1"
|
|
85
107
|
},
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"test:pro": "ORACLE_LIVE_TEST=1 vitest run tests/live/openai-live.test.ts",
|
|
102
|
-
"test:coverage": "vitest run --coverage",
|
|
103
|
-
"mcp": "pnpm run build && node ./dist/bin/oracle-mcp.js"
|
|
104
|
-
}
|
|
105
|
-
}
|
|
108
|
+
"pnpm": {
|
|
109
|
+
"overrides": {
|
|
110
|
+
"devtools-protocol": "0.0.1561482",
|
|
111
|
+
"win-dpapi": "npm:@primno/dpapi@2.0.1"
|
|
112
|
+
},
|
|
113
|
+
"onlyBuiltDependencies": [
|
|
114
|
+
"@cdktf/node-pty-prebuilt-multiarch",
|
|
115
|
+
"esbuild",
|
|
116
|
+
"keytar",
|
|
117
|
+
"sqlite3",
|
|
118
|
+
"win-dpapi"
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b"
|
|
122
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Oracle Notifier helper (macOS, arm64)
|
|
2
|
+
|
|
3
|
+
Builds a tiny signed helper app for macOS notifications with the Oracle icon.
|
|
4
|
+
|
|
5
|
+
## Build
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd vendor/oracle-notifier
|
|
9
|
+
# Optional: notarize by setting App Store Connect key credentials
|
|
10
|
+
export APP_STORE_CONNECT_API_KEY_P8="$(cat AuthKey_XXXXXX.p8)" # with literal newlines or \n escaped
|
|
11
|
+
export APP_STORE_CONNECT_KEY_ID=XXXXXX
|
|
12
|
+
export APP_STORE_CONNECT_ISSUER_ID=YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY
|
|
13
|
+
./build-notifier.sh
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Requires Xcode command line tools (swiftc) and a macOS Developer ID certificate. Without a valid cert, the build fails (no ad-hoc fallback).
|
|
17
|
+
- If `APP_STORE_CONNECT_*` vars are set, the script notarizes and staples the ticket.
|
|
18
|
+
- Output: `OracleNotifier.app` (arm64 only), bundled with `OracleIcon.icns`.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
The CLI prefers this helper on macOS; if it fails or is missing, it falls back to toasted-notifier/terminal-notifier.
|
|
22
|
+
|
|
23
|
+
## Permissions
|
|
24
|
+
After first run, allow notifications for “Oracle Notifier” in System Settings → Notifications.
|
|
File without changes
|