@andrewting19/oracle 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +300 -0
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +1480 -0
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/scripts/agent-send.js +147 -0
- package/dist/scripts/browser-tools.js +536 -0
- package/dist/scripts/check.js +21 -0
- package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
- package/dist/scripts/docs-list.js +110 -0
- package/dist/scripts/git-policy.js +127 -0
- package/dist/scripts/run-cli.js +14 -0
- package/dist/scripts/runner.js +1386 -0
- package/dist/scripts/test-browser.js +106 -0
- package/dist/scripts/test-remote-chrome.js +68 -0
- package/dist/src/bridge/connection.js +103 -0
- package/dist/src/bridge/userConfigFile.js +28 -0
- package/dist/src/browser/actions/assistantResponse.js +1085 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
- package/dist/src/browser/actions/attachments.js +1939 -0
- package/dist/src/browser/actions/domEvents.js +19 -0
- package/dist/src/browser/actions/modelSelection.js +549 -0
- package/dist/src/browser/actions/navigation.js +451 -0
- package/dist/src/browser/actions/promptComposer.js +487 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
- package/dist/src/browser/actions/thinkingTime.js +206 -0
- package/dist/src/browser/chromeLifecycle.js +346 -0
- package/dist/src/browser/config.js +105 -0
- package/dist/src/browser/constants.js +75 -0
- package/dist/src/browser/cookies.js +191 -0
- package/dist/src/browser/detect.js +164 -0
- package/dist/src/browser/domDebug.js +36 -0
- package/dist/src/browser/index.js +1826 -0
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/policies.js +46 -0
- package/dist/src/browser/profileState.js +285 -0
- package/dist/src/browser/prompt.js +182 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/providerDomFlow.js +17 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
- package/dist/src/browser/providers/index.js +2 -0
- package/dist/src/browser/reattach.js +189 -0
- package/dist/src/browser/reattachHelpers.js +387 -0
- package/dist/src/browser/sessionRunner.js +131 -0
- package/dist/src/browser/types.js +1 -0
- package/dist/src/browser/utils.js +122 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/cli/bridge/claudeConfig.js +54 -0
- package/dist/src/cli/bridge/client.js +81 -0
- package/dist/src/cli/bridge/codexConfig.js +43 -0
- package/dist/src/cli/bridge/doctor.js +115 -0
- package/dist/src/cli/bridge/host.js +261 -0
- package/dist/src/cli/browserConfig.js +293 -0
- package/dist/src/cli/browserDefaults.js +82 -0
- package/dist/src/cli/bundleWarnings.js +9 -0
- package/dist/src/cli/clipboard.js +10 -0
- package/dist/src/cli/detach.js +14 -0
- package/dist/src/cli/dryRun.js +109 -0
- package/dist/src/cli/duplicatePromptGuard.js +14 -0
- package/dist/src/cli/engine.js +41 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/fileSize.js +11 -0
- package/dist/src/cli/format.js +13 -0
- package/dist/src/cli/help.js +77 -0
- package/dist/src/cli/hiddenAliases.js +22 -0
- package/dist/src/cli/markdownBundle.js +21 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +316 -0
- package/dist/src/cli/options.js +305 -0
- package/dist/src/cli/oscUtils.js +2 -0
- package/dist/src/cli/promptRequirement.js +17 -0
- package/dist/src/cli/renderFlags.js +9 -0
- package/dist/src/cli/renderOutput.js +26 -0
- package/dist/src/cli/rootAlias.js +30 -0
- package/dist/src/cli/runOptions.js +90 -0
- package/dist/src/cli/sessionCommand.js +121 -0
- package/dist/src/cli/sessionDisplay.js +670 -0
- package/dist/src/cli/sessionLineage.js +60 -0
- package/dist/src/cli/sessionRunner.js +630 -0
- package/dist/src/cli/sessionTable.js +96 -0
- package/dist/src/cli/tagline.js +255 -0
- package/dist/src/cli/tui/index.js +499 -0
- package/dist/src/cli/writeOutputPath.js +21 -0
- package/dist/src/config.js +26 -0
- package/dist/src/gemini-web/browserSessionManager.js +81 -0
- package/dist/src/gemini-web/client.js +339 -0
- package/dist/src/gemini-web/executionClients.js +1 -0
- package/dist/src/gemini-web/executionMode.js +16 -0
- package/dist/src/gemini-web/executor.js +443 -0
- package/dist/src/gemini-web/index.js +1 -0
- package/dist/src/gemini-web/types.js +1 -0
- package/dist/src/heartbeat.js +43 -0
- package/dist/src/mcp/server.js +40 -0
- package/dist/src/mcp/tools/consult.js +307 -0
- package/dist/src/mcp/tools/sessionResources.js +75 -0
- package/dist/src/mcp/tools/sessions.js +114 -0
- package/dist/src/mcp/types.js +22 -0
- package/dist/src/mcp/utils.js +45 -0
- package/dist/src/oracle/background.js +141 -0
- package/dist/src/oracle/claude.js +107 -0
- package/dist/src/oracle/client.js +235 -0
- package/dist/src/oracle/config.js +192 -0
- package/dist/src/oracle/errors.js +132 -0
- package/dist/src/oracle/files.js +402 -0
- package/dist/src/oracle/finishLine.js +34 -0
- package/dist/src/oracle/format.js +30 -0
- package/dist/src/oracle/fsAdapter.js +10 -0
- package/dist/src/oracle/gemini.js +194 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/markdown.js +46 -0
- package/dist/src/oracle/modelResolver.js +183 -0
- package/dist/src/oracle/multiModelRunner.js +153 -0
- package/dist/src/oracle/oscProgress.js +24 -0
- package/dist/src/oracle/promptAssembly.js +16 -0
- package/dist/src/oracle/request.js +58 -0
- package/dist/src/oracle/run.js +628 -0
- package/dist/src/oracle/runUtils.js +34 -0
- package/dist/src/oracle/tokenEstimate.js +37 -0
- package/dist/src/oracle/tokenStats.js +39 -0
- package/dist/src/oracle/tokenStringifier.js +24 -0
- package/dist/src/oracle/types.js +1 -0
- package/dist/src/oracle.js +12 -0
- package/dist/src/oracleHome.js +13 -0
- package/dist/src/remote/client.js +129 -0
- package/dist/src/remote/health.js +113 -0
- package/dist/src/remote/remoteServiceConfig.js +31 -0
- package/dist/src/remote/server.js +544 -0
- package/dist/src/remote/types.js +1 -0
- package/dist/src/sessionManager.js +643 -0
- package/dist/src/sessionStore.js +56 -0
- package/dist/src/version.js +39 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/README.md +26 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +120 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +26 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
2
|
+
import { delay } from "./utils.js";
|
|
3
|
+
import { readAssistantSnapshot } from "./pageActions.js";
|
|
4
|
+
export function pickTarget(targets, runtime) {
|
|
5
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
if (runtime.chromeTargetId) {
|
|
9
|
+
const byId = targets.find((t) => t.targetId === runtime.chromeTargetId);
|
|
10
|
+
if (byId)
|
|
11
|
+
return byId;
|
|
12
|
+
}
|
|
13
|
+
if (runtime.tabUrl) {
|
|
14
|
+
const byUrl = targets.find((t) => t.url?.startsWith(runtime.tabUrl)) ||
|
|
15
|
+
targets.find((t) => runtime.tabUrl.startsWith(t.url || ""));
|
|
16
|
+
if (byUrl)
|
|
17
|
+
return byUrl;
|
|
18
|
+
}
|
|
19
|
+
return targets.find((t) => t.type === "page") ?? targets[0];
|
|
20
|
+
}
|
|
21
|
+
export function extractConversationIdFromUrl(url) {
|
|
22
|
+
if (!url)
|
|
23
|
+
return undefined;
|
|
24
|
+
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
25
|
+
return match?.[1];
|
|
26
|
+
}
|
|
27
|
+
export function buildConversationUrl(runtime, baseUrl) {
|
|
28
|
+
if (runtime.tabUrl) {
|
|
29
|
+
if (runtime.tabUrl.includes("/c/")) {
|
|
30
|
+
return runtime.tabUrl;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const conversationId = runtime.conversationId;
|
|
35
|
+
if (!conversationId) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const base = new URL(baseUrl);
|
|
40
|
+
const pathRoot = base.pathname.replace(/\/$/, "");
|
|
41
|
+
const prefix = pathRoot === "/" ? "" : pathRoot;
|
|
42
|
+
return `${base.origin}${prefix}/c/${conversationId}`;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function withTimeout(task, ms, label) {
|
|
49
|
+
let timeoutId;
|
|
50
|
+
const timeout = new Promise((_, reject) => {
|
|
51
|
+
timeoutId = setTimeout(() => reject(new Error(label)), ms);
|
|
52
|
+
});
|
|
53
|
+
return Promise.race([task, timeout]).finally(() => {
|
|
54
|
+
if (timeoutId) {
|
|
55
|
+
clearTimeout(timeoutId);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export async function openConversationFromSidebar(Runtime, options, attempt = 0) {
|
|
60
|
+
const response = await Runtime.evaluate({
|
|
61
|
+
expression: `(() => {
|
|
62
|
+
const conversationId = ${JSON.stringify(options.conversationId ?? null)};
|
|
63
|
+
const preferProjects = ${JSON.stringify(Boolean(options.preferProjects))};
|
|
64
|
+
const promptPreview = ${JSON.stringify(options.promptPreview ?? null)};
|
|
65
|
+
const attemptIndex = ${Math.max(0, attempt)};
|
|
66
|
+
const promptNeedleFull = promptPreview ? promptPreview.trim().toLowerCase().slice(0, 100) : '';
|
|
67
|
+
const promptNeedleShort = promptNeedleFull.replace(/\\s*\\d{4,}\\s*$/, '').trim();
|
|
68
|
+
const promptNeedles = Array.from(new Set([promptNeedleFull, promptNeedleShort].filter(Boolean)));
|
|
69
|
+
const nav = document.querySelector('nav') || document.querySelector('aside') || document.body;
|
|
70
|
+
if (preferProjects) {
|
|
71
|
+
const projectLink = Array.from(nav.querySelectorAll('a,button'))
|
|
72
|
+
.find((el) => (el.textContent || '').trim().toLowerCase() === 'projects');
|
|
73
|
+
if (projectLink) {
|
|
74
|
+
projectLink.click();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const allElements = Array.from(
|
|
78
|
+
document.querySelectorAll(
|
|
79
|
+
'a,button,[role="link"],[role="button"],[data-href],[data-url],[data-conversation-id],[data-testid*="conversation"],[data-testid*="history"]',
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
const getHref = (el) =>
|
|
83
|
+
el.getAttribute('href') ||
|
|
84
|
+
el.getAttribute('data-href') ||
|
|
85
|
+
el.getAttribute('data-url') ||
|
|
86
|
+
el.dataset?.href ||
|
|
87
|
+
el.dataset?.url ||
|
|
88
|
+
'';
|
|
89
|
+
const toCandidate = (el) => {
|
|
90
|
+
const clickable = el.closest('a,button,[role="link"],[role="button"]') || el;
|
|
91
|
+
const rawText = (el.textContent || clickable.textContent || '').trim();
|
|
92
|
+
return {
|
|
93
|
+
el,
|
|
94
|
+
clickable,
|
|
95
|
+
href: getHref(clickable) || getHref(el),
|
|
96
|
+
conversationId:
|
|
97
|
+
clickable.getAttribute('data-conversation-id') ||
|
|
98
|
+
el.getAttribute('data-conversation-id') ||
|
|
99
|
+
clickable.dataset?.conversationId ||
|
|
100
|
+
el.dataset?.conversationId ||
|
|
101
|
+
'',
|
|
102
|
+
testId: clickable.getAttribute('data-testid') || el.getAttribute('data-testid') || '',
|
|
103
|
+
text: rawText.replace(/\\s+/g, ' ').slice(0, 400),
|
|
104
|
+
inNav: Boolean(clickable.closest('nav,aside')),
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
const candidates = allElements.map(toCandidate);
|
|
108
|
+
const mainCandidates = candidates.filter((item) => !item.inNav);
|
|
109
|
+
const navCandidates = candidates.filter((item) => item.inNav);
|
|
110
|
+
const visible = (item) => {
|
|
111
|
+
const rect = item.clickable.getBoundingClientRect();
|
|
112
|
+
return rect.width > 0 && rect.height > 0;
|
|
113
|
+
};
|
|
114
|
+
const pick = (items) => (items.find(visible) || items[0] || null);
|
|
115
|
+
const pickWithAttempt = (items) => {
|
|
116
|
+
if (!items.length) return null;
|
|
117
|
+
const visibleItems = items.filter(visible);
|
|
118
|
+
const pool = visibleItems.length > 0 ? visibleItems : items;
|
|
119
|
+
const index = Math.min(attemptIndex, pool.length - 1);
|
|
120
|
+
return pool[index] ?? null;
|
|
121
|
+
};
|
|
122
|
+
let target = null;
|
|
123
|
+
if (conversationId) {
|
|
124
|
+
const byId = (item) =>
|
|
125
|
+
(item.href && item.href.includes('/c/' + conversationId)) ||
|
|
126
|
+
(item.conversationId && item.conversationId === conversationId);
|
|
127
|
+
target = pick(mainCandidates.filter(byId)) || pick(navCandidates.filter(byId));
|
|
128
|
+
}
|
|
129
|
+
if (!target && promptNeedles.length > 0) {
|
|
130
|
+
const byPrompt = (item) => promptNeedles.some((needle) => item.text && item.text.toLowerCase().includes(needle));
|
|
131
|
+
const sortBySpecificity = (items) =>
|
|
132
|
+
items
|
|
133
|
+
.filter(byPrompt)
|
|
134
|
+
.sort((a, b) => (a.text?.length ?? 0) - (b.text?.length ?? 0));
|
|
135
|
+
target = pickWithAttempt(sortBySpecificity(mainCandidates)) || pickWithAttempt(sortBySpecificity(navCandidates));
|
|
136
|
+
}
|
|
137
|
+
if (!target) {
|
|
138
|
+
const byHref = (item) => item.href && item.href.includes('/c/');
|
|
139
|
+
target = pickWithAttempt(mainCandidates.filter(byHref)) || pickWithAttempt(navCandidates.filter(byHref));
|
|
140
|
+
}
|
|
141
|
+
if (!target) {
|
|
142
|
+
const byTestId = (item) => /conversation|history/i.test(item.testId || '');
|
|
143
|
+
target = pickWithAttempt(mainCandidates.filter(byTestId)) || pickWithAttempt(navCandidates.filter(byTestId));
|
|
144
|
+
}
|
|
145
|
+
if (target) {
|
|
146
|
+
target.clickable.scrollIntoView({ block: 'center' });
|
|
147
|
+
target.clickable.dispatchEvent(
|
|
148
|
+
new MouseEvent('click', { bubbles: true, cancelable: true, view: window }),
|
|
149
|
+
);
|
|
150
|
+
// Fallback: some project-sidebar items don't navigate on click, force the URL.
|
|
151
|
+
if (target.href && target.href.includes('/c/')) {
|
|
152
|
+
const targetUrl = target.href.startsWith('http')
|
|
153
|
+
? target.href
|
|
154
|
+
: new URL(target.href, location.origin).toString();
|
|
155
|
+
if (targetUrl && targetUrl !== location.href) {
|
|
156
|
+
location.href = targetUrl;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
href: target.href || '',
|
|
162
|
+
count: candidates.length,
|
|
163
|
+
scope: target.inNav ? 'nav' : 'main',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { ok: false, count: candidates.length };
|
|
167
|
+
})()`,
|
|
168
|
+
returnByValue: true,
|
|
169
|
+
});
|
|
170
|
+
return Boolean(response.result?.value?.ok);
|
|
171
|
+
}
|
|
172
|
+
export async function openConversationFromSidebarWithRetry(Runtime, options, timeoutMs) {
|
|
173
|
+
const start = Date.now();
|
|
174
|
+
let attempt = 0;
|
|
175
|
+
while (Date.now() - start < timeoutMs) {
|
|
176
|
+
// Retry because project list can hydrate after initial navigation.
|
|
177
|
+
const opened = await openConversationFromSidebar(Runtime, options, attempt);
|
|
178
|
+
if (opened) {
|
|
179
|
+
if (options.promptPreview) {
|
|
180
|
+
const matched = await waitForPromptPreview(Runtime, options.promptPreview, 10_000);
|
|
181
|
+
if (matched) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
attempt += 1;
|
|
190
|
+
await delay(attempt < 5 ? 250 : 500);
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
export async function waitForPromptPreview(Runtime, promptPreview, timeoutMs) {
|
|
195
|
+
const needleFull = promptPreview.trim().toLowerCase().slice(0, 120);
|
|
196
|
+
const needleShort = needleFull.replace(/\\s*\\d{4,}\\s*$/, "").trim();
|
|
197
|
+
const needles = Array.from(new Set([needleFull, needleShort].filter(Boolean)));
|
|
198
|
+
if (needles.length === 0)
|
|
199
|
+
return false;
|
|
200
|
+
const selectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
201
|
+
const expression = `(() => {
|
|
202
|
+
const needles = ${JSON.stringify(needles)};
|
|
203
|
+
const root =
|
|
204
|
+
document.querySelector('section[data-testid="screen-threadFlyOut"]') ||
|
|
205
|
+
document.querySelector('[data-testid="chat-thread"]') ||
|
|
206
|
+
document.querySelector('main') ||
|
|
207
|
+
document.querySelector('[role="main"]');
|
|
208
|
+
if (!root) return false;
|
|
209
|
+
const userTurns = Array.from(root.querySelectorAll('[data-message-author-role="user"], [data-turn="user"]'));
|
|
210
|
+
const collectText = (nodes) =>
|
|
211
|
+
nodes
|
|
212
|
+
.map((node) => (node.innerText || node.textContent || ''))
|
|
213
|
+
.join(' ')
|
|
214
|
+
.toLowerCase();
|
|
215
|
+
let text = collectText(userTurns);
|
|
216
|
+
let hasTurns = userTurns.length > 0;
|
|
217
|
+
if (!text) {
|
|
218
|
+
const turns = Array.from(root.querySelectorAll(${selectorLiteral}));
|
|
219
|
+
hasTurns = hasTurns || turns.length > 0;
|
|
220
|
+
text = collectText(turns);
|
|
221
|
+
}
|
|
222
|
+
if (!text) {
|
|
223
|
+
text = (root.innerText || root.textContent || '').toLowerCase();
|
|
224
|
+
}
|
|
225
|
+
return needles.some((needle) => text.includes(needle));
|
|
226
|
+
})()`;
|
|
227
|
+
const start = Date.now();
|
|
228
|
+
while (Date.now() - start < timeoutMs) {
|
|
229
|
+
try {
|
|
230
|
+
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
231
|
+
if (result?.value === true) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// ignore
|
|
237
|
+
}
|
|
238
|
+
await delay(300);
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
export async function waitForLocationChange(Runtime, timeoutMs) {
|
|
243
|
+
const start = Date.now();
|
|
244
|
+
let lastHref = "";
|
|
245
|
+
while (Date.now() - start < timeoutMs) {
|
|
246
|
+
const { result } = await Runtime.evaluate({ expression: "location.href", returnByValue: true });
|
|
247
|
+
const href = typeof result?.value === "string" ? result.value : "";
|
|
248
|
+
if (lastHref && href !== lastHref) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
lastHref = href;
|
|
252
|
+
await delay(200);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export async function readConversationTurnIndex(Runtime, logger) {
|
|
256
|
+
const selectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
257
|
+
try {
|
|
258
|
+
const { result } = await Runtime.evaluate({
|
|
259
|
+
expression: `document.querySelectorAll(${selectorLiteral}).length`,
|
|
260
|
+
returnByValue: true,
|
|
261
|
+
});
|
|
262
|
+
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
263
|
+
if (!Number.isFinite(raw)) {
|
|
264
|
+
throw new Error("Turn count not numeric");
|
|
265
|
+
}
|
|
266
|
+
return Math.max(0, Math.floor(raw) - 1);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (logger?.verbose) {
|
|
270
|
+
logger(`Failed to read conversation turn index: ${error instanceof Error ? error.message : String(error)}`);
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function normalizeForComparison(text) {
|
|
276
|
+
return String(text || "")
|
|
277
|
+
.toLowerCase()
|
|
278
|
+
.replace(/\\s+/g, " ")
|
|
279
|
+
.trim();
|
|
280
|
+
}
|
|
281
|
+
export function buildPromptEchoMatcher(promptPreview) {
|
|
282
|
+
const normalizedPrompt = normalizeForComparison(promptPreview ?? "");
|
|
283
|
+
if (!normalizedPrompt) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const promptPrefix = normalizedPrompt.length >= 80
|
|
287
|
+
? normalizedPrompt.slice(0, Math.min(200, normalizedPrompt.length))
|
|
288
|
+
: "";
|
|
289
|
+
const minFragment = Math.min(40, normalizedPrompt.length);
|
|
290
|
+
return {
|
|
291
|
+
isEcho: (text) => {
|
|
292
|
+
const normalized = normalizeForComparison(text);
|
|
293
|
+
if (!normalized)
|
|
294
|
+
return false;
|
|
295
|
+
if (normalized === normalizedPrompt)
|
|
296
|
+
return true;
|
|
297
|
+
if (promptPrefix.length > 0 && normalized.startsWith(promptPrefix))
|
|
298
|
+
return true;
|
|
299
|
+
if (normalized.length >= minFragment && normalizedPrompt.startsWith(normalized)) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
if (normalized.includes("…") || normalized.includes("...")) {
|
|
303
|
+
const marker = normalized.includes("…") ? "…" : "...";
|
|
304
|
+
const [prefixRaw, suffixRaw] = normalized.split(marker);
|
|
305
|
+
const prefix = prefixRaw?.trim() ?? "";
|
|
306
|
+
const suffix = suffixRaw?.trim() ?? "";
|
|
307
|
+
if (!prefix && !suffix)
|
|
308
|
+
return false;
|
|
309
|
+
if (prefix && !normalizedPrompt.includes(prefix))
|
|
310
|
+
return false;
|
|
311
|
+
if (suffix && !normalizedPrompt.includes(suffix))
|
|
312
|
+
return false;
|
|
313
|
+
const fragmentLength = prefix.length + suffix.length;
|
|
314
|
+
return fragmentLength >= minFragment;
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export async function recoverPromptEcho(Runtime, answer, matcher, logger, minTurnIndex, timeoutMs) {
|
|
321
|
+
if (!matcher || !matcher.isEcho(answer.text)) {
|
|
322
|
+
return answer;
|
|
323
|
+
}
|
|
324
|
+
logger("Detected prompt echo while reattaching; waiting for assistant response...");
|
|
325
|
+
const deadline = Date.now() + Math.min(timeoutMs, 15_000);
|
|
326
|
+
let bestText = null;
|
|
327
|
+
let stableCount = 0;
|
|
328
|
+
while (Date.now() < deadline) {
|
|
329
|
+
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex ?? undefined).catch(() => null);
|
|
330
|
+
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
331
|
+
if (!text || matcher.isEcho(text)) {
|
|
332
|
+
await delay(300);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (!bestText || text.length > bestText.length) {
|
|
336
|
+
bestText = text;
|
|
337
|
+
stableCount = 0;
|
|
338
|
+
}
|
|
339
|
+
else if (text === bestText) {
|
|
340
|
+
stableCount += 1;
|
|
341
|
+
}
|
|
342
|
+
if (stableCount >= 2) {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
await delay(300);
|
|
346
|
+
}
|
|
347
|
+
if (bestText) {
|
|
348
|
+
logger("Recovered assistant response after prompt echo during reattach");
|
|
349
|
+
return { ...answer, text: bestText };
|
|
350
|
+
}
|
|
351
|
+
return answer;
|
|
352
|
+
}
|
|
353
|
+
export function alignPromptEchoPair(answerText, answerMarkdown, matcher, logger, messages) {
|
|
354
|
+
if (!matcher) {
|
|
355
|
+
return { answerText, answerMarkdown, textEcho: false, markdownEcho: false, isEcho: false };
|
|
356
|
+
}
|
|
357
|
+
let textEcho = matcher.isEcho(answerText);
|
|
358
|
+
let markdownEcho = matcher.isEcho(answerMarkdown);
|
|
359
|
+
if (textEcho && !markdownEcho && answerMarkdown) {
|
|
360
|
+
if (logger && messages?.text) {
|
|
361
|
+
logger(messages.text);
|
|
362
|
+
}
|
|
363
|
+
answerText = answerMarkdown;
|
|
364
|
+
textEcho = false;
|
|
365
|
+
}
|
|
366
|
+
if (markdownEcho && !textEcho && answerText) {
|
|
367
|
+
if (logger && messages?.markdown) {
|
|
368
|
+
logger(messages.markdown);
|
|
369
|
+
}
|
|
370
|
+
answerMarkdown = answerText;
|
|
371
|
+
markdownEcho = false;
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
answerText,
|
|
375
|
+
answerMarkdown,
|
|
376
|
+
textEcho,
|
|
377
|
+
markdownEcho,
|
|
378
|
+
isEcho: textEcho || markdownEcho,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
export function alignPromptEchoMarkdown(answerText, answerMarkdown, matcher, logger) {
|
|
382
|
+
const aligned = alignPromptEchoPair(answerText, answerMarkdown, matcher, logger, {
|
|
383
|
+
text: "Aligned prompt-echo text to copied markdown during reattach",
|
|
384
|
+
markdown: "Aligned prompt-echo markdown to response text during reattach",
|
|
385
|
+
});
|
|
386
|
+
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
387
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { formatTokenCount } from "../oracle/runUtils.js";
|
|
3
|
+
import { formatFinishLine } from "../oracle/finishLine.js";
|
|
4
|
+
import { runBrowserMode } from "../browserMode.js";
|
|
5
|
+
import { assembleBrowserPrompt } from "./prompt.js";
|
|
6
|
+
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
7
|
+
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
|
|
8
|
+
const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
|
|
9
|
+
const executeBrowser = deps.executeBrowser ?? runBrowserMode;
|
|
10
|
+
const promptArtifacts = await assemblePrompt(runOptions, { cwd });
|
|
11
|
+
if (runOptions.verbose) {
|
|
12
|
+
log(chalk.dim(`[verbose] Browser config: ${JSON.stringify({
|
|
13
|
+
...browserConfig,
|
|
14
|
+
})}`));
|
|
15
|
+
log(chalk.dim(`[verbose] Browser prompt length: ${promptArtifacts.composerText.length} chars`));
|
|
16
|
+
if (promptArtifacts.attachments.length > 0) {
|
|
17
|
+
const attachmentList = promptArtifacts.attachments
|
|
18
|
+
.map((attachment) => attachment.displayPath)
|
|
19
|
+
.join(", ");
|
|
20
|
+
log(chalk.dim(`[verbose] Browser attachments: ${attachmentList}`));
|
|
21
|
+
if (promptArtifacts.bundled) {
|
|
22
|
+
log(chalk.yellow(`[browser] Bundled ${promptArtifacts.bundled.originalCount} files into ${promptArtifacts.bundled.bundlePath}.`));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (runOptions.file &&
|
|
26
|
+
runOptions.file.length > 0 &&
|
|
27
|
+
promptArtifacts.attachmentMode === "inline") {
|
|
28
|
+
log(chalk.dim("[verbose] Browser will paste file contents inline (no uploads)."));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (promptArtifacts.bundled) {
|
|
32
|
+
log(chalk.dim(`Packed ${promptArtifacts.bundled.originalCount} files into 1 bundle (contents counted in token estimate).`));
|
|
33
|
+
}
|
|
34
|
+
const headerLine = `Launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens.`;
|
|
35
|
+
const automationLogger = ((message) => {
|
|
36
|
+
if (typeof message !== "string")
|
|
37
|
+
return;
|
|
38
|
+
const shouldAlwaysPrint = message.startsWith("[browser] ") && /fallback|retry/i.test(message);
|
|
39
|
+
if (!runOptions.verbose && !shouldAlwaysPrint)
|
|
40
|
+
return;
|
|
41
|
+
log(message);
|
|
42
|
+
});
|
|
43
|
+
automationLogger.verbose = Boolean(runOptions.verbose);
|
|
44
|
+
automationLogger.sessionLog = runOptions.verbose ? log : () => { };
|
|
45
|
+
log(headerLine);
|
|
46
|
+
log(chalk.dim("This run can take up to an hour (usually ~10 minutes)."));
|
|
47
|
+
if (runOptions.verbose) {
|
|
48
|
+
log(chalk.dim("Chrome automation does not stream output; this may take a minute..."));
|
|
49
|
+
}
|
|
50
|
+
const persistRuntimeHint = deps.persistRuntimeHint ?? (() => { });
|
|
51
|
+
let browserResult;
|
|
52
|
+
try {
|
|
53
|
+
browserResult = await executeBrowser({
|
|
54
|
+
prompt: promptArtifacts.composerText,
|
|
55
|
+
attachments: promptArtifacts.attachments,
|
|
56
|
+
fallbackSubmission: promptArtifacts.fallback
|
|
57
|
+
? {
|
|
58
|
+
prompt: promptArtifacts.fallback.composerText,
|
|
59
|
+
attachments: promptArtifacts.fallback.attachments,
|
|
60
|
+
}
|
|
61
|
+
: undefined,
|
|
62
|
+
config: browserConfig,
|
|
63
|
+
log: automationLogger,
|
|
64
|
+
heartbeatIntervalMs: runOptions.heartbeatIntervalMs,
|
|
65
|
+
verbose: runOptions.verbose,
|
|
66
|
+
runtimeHintCb: async (runtime) => {
|
|
67
|
+
await persistRuntimeHint({
|
|
68
|
+
...runtime,
|
|
69
|
+
controllerPid: runtime.controllerPid ?? process.pid,
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof BrowserAutomationError) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
const message = error instanceof Error ? error.message : "Browser automation failed.";
|
|
79
|
+
throw new BrowserAutomationError(message, { stage: "execute-browser" }, error);
|
|
80
|
+
}
|
|
81
|
+
if (!runOptions.silent) {
|
|
82
|
+
log(chalk.bold("Answer:"));
|
|
83
|
+
log(browserResult.answerMarkdown || browserResult.answerText || chalk.dim("(no text output)"));
|
|
84
|
+
log("");
|
|
85
|
+
}
|
|
86
|
+
const answerText = browserResult.answerMarkdown || browserResult.answerText || "";
|
|
87
|
+
const usage = {
|
|
88
|
+
inputTokens: promptArtifacts.estimatedInputTokens,
|
|
89
|
+
outputTokens: browserResult.answerTokens,
|
|
90
|
+
reasoningTokens: 0,
|
|
91
|
+
totalTokens: promptArtifacts.estimatedInputTokens + browserResult.answerTokens,
|
|
92
|
+
};
|
|
93
|
+
const tokensDisplay = [
|
|
94
|
+
usage.inputTokens,
|
|
95
|
+
usage.outputTokens,
|
|
96
|
+
usage.reasoningTokens,
|
|
97
|
+
usage.totalTokens,
|
|
98
|
+
]
|
|
99
|
+
.map((value) => formatTokenCount(value))
|
|
100
|
+
.join("/");
|
|
101
|
+
const tokensPart = (() => {
|
|
102
|
+
const parts = tokensDisplay.split("/");
|
|
103
|
+
if (parts.length !== 4)
|
|
104
|
+
return tokensDisplay;
|
|
105
|
+
return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
|
|
106
|
+
})();
|
|
107
|
+
const { line1, line2 } = formatFinishLine({
|
|
108
|
+
elapsedMs: browserResult.tookMs,
|
|
109
|
+
model: `${runOptions.model}[browser]`,
|
|
110
|
+
tokensPart,
|
|
111
|
+
detailParts: [
|
|
112
|
+
runOptions.file && runOptions.file.length > 0 ? `files=${runOptions.file.length}` : null,
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
log(chalk.blue(line1));
|
|
116
|
+
if (line2) {
|
|
117
|
+
log(chalk.dim(line2));
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
usage,
|
|
121
|
+
elapsedMs: browserResult.tookMs,
|
|
122
|
+
runtime: {
|
|
123
|
+
chromePid: browserResult.chromePid,
|
|
124
|
+
chromePort: browserResult.chromePort,
|
|
125
|
+
chromeHost: browserResult.chromeHost,
|
|
126
|
+
userDataDir: browserResult.userDataDir,
|
|
127
|
+
controllerPid: browserResult.controllerPid ?? process.pid,
|
|
128
|
+
},
|
|
129
|
+
answerText,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export function parseDuration(input, fallback) {
|
|
2
|
+
if (!input) {
|
|
3
|
+
return fallback;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = input.trim();
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
return fallback;
|
|
8
|
+
}
|
|
9
|
+
const lowercase = trimmed.toLowerCase();
|
|
10
|
+
if (/^[0-9]+$/.test(lowercase)) {
|
|
11
|
+
return Number(lowercase);
|
|
12
|
+
}
|
|
13
|
+
const normalized = lowercase.replace(/\s+/g, "");
|
|
14
|
+
const singleMatch = /^([0-9]+)(ms|s|m|h)$/i.exec(normalized);
|
|
15
|
+
if (singleMatch && singleMatch[0].length === normalized.length) {
|
|
16
|
+
const value = Number(singleMatch[1]);
|
|
17
|
+
return convertUnit(value, singleMatch[2]);
|
|
18
|
+
}
|
|
19
|
+
const multiDuration = /([0-9]+)(ms|h|m|s)/g;
|
|
20
|
+
let total = 0;
|
|
21
|
+
let lastIndex = 0;
|
|
22
|
+
let match = multiDuration.exec(normalized);
|
|
23
|
+
while (match !== null) {
|
|
24
|
+
total += convertUnit(Number(match[1]), match[2]);
|
|
25
|
+
lastIndex = multiDuration.lastIndex;
|
|
26
|
+
match = multiDuration.exec(normalized);
|
|
27
|
+
}
|
|
28
|
+
if (total > 0 && lastIndex === normalized.length) {
|
|
29
|
+
return total;
|
|
30
|
+
}
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
function convertUnit(value, unitRaw) {
|
|
34
|
+
const unit = unitRaw?.toLowerCase();
|
|
35
|
+
switch (unit) {
|
|
36
|
+
case "ms":
|
|
37
|
+
return value;
|
|
38
|
+
case "s":
|
|
39
|
+
return value * 1000;
|
|
40
|
+
case "m":
|
|
41
|
+
return value * 60_000;
|
|
42
|
+
case "h":
|
|
43
|
+
return value * 3_600_000;
|
|
44
|
+
default:
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function delay(ms) {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
export function estimateTokenCount(text) {
|
|
52
|
+
if (!text) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
const words = text.trim().split(/\s+/).filter(Boolean);
|
|
56
|
+
const estimate = Math.max(words.length * 0.75, text.length / 4);
|
|
57
|
+
return Math.max(1, Math.round(estimate));
|
|
58
|
+
}
|
|
59
|
+
export async function withRetries(task, options = {}) {
|
|
60
|
+
const { retries = 2, delayMs = 250, onRetry } = options;
|
|
61
|
+
let attempt = 0;
|
|
62
|
+
while (attempt <= retries) {
|
|
63
|
+
try {
|
|
64
|
+
return await task();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (attempt === retries) {
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
attempt += 1;
|
|
71
|
+
onRetry?.(attempt, error);
|
|
72
|
+
await delay(delayMs * attempt);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw new Error("withRetries exhausted without result");
|
|
76
|
+
}
|
|
77
|
+
export function formatBytes(size) {
|
|
78
|
+
if (!Number.isFinite(size) || size < 0) {
|
|
79
|
+
return "n/a";
|
|
80
|
+
}
|
|
81
|
+
if (size < 1024) {
|
|
82
|
+
return `${size} B`;
|
|
83
|
+
}
|
|
84
|
+
if (size < 1024 * 1024) {
|
|
85
|
+
return `${(size / 1024).toFixed(1)} KB`;
|
|
86
|
+
}
|
|
87
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Normalizes a ChatGPT URL, ensuring it is absolute, uses http/https, and trims whitespace.
|
|
91
|
+
* Falls back to the provided default when input is empty/undefined.
|
|
92
|
+
*/
|
|
93
|
+
export function normalizeChatgptUrl(raw, fallback) {
|
|
94
|
+
const candidate = raw?.trim();
|
|
95
|
+
if (!candidate) {
|
|
96
|
+
return fallback;
|
|
97
|
+
}
|
|
98
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(candidate);
|
|
99
|
+
const withScheme = hasScheme ? candidate : `https://${candidate}`;
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = new URL(withScheme);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error(`Invalid ChatGPT URL: "${raw}". Provide an absolute http(s) URL.`);
|
|
106
|
+
}
|
|
107
|
+
if (!/^https?:$/i.test(parsed.protocol)) {
|
|
108
|
+
throw new Error(`Invalid ChatGPT URL protocol: "${parsed.protocol}". Use http or https.`);
|
|
109
|
+
}
|
|
110
|
+
// Preserve user-provided path/query; URL#toString will normalize trailing slashes appropriately.
|
|
111
|
+
return parsed.toString();
|
|
112
|
+
}
|
|
113
|
+
export function isTemporaryChatUrl(url) {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = new URL(url);
|
|
116
|
+
const value = (parsed.searchParams.get("temporary-chat") ?? "").trim().toLowerCase();
|
|
117
|
+
return value === "true" || value === "1" || value === "yes";
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runBrowserMode, CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET, parseDuration, normalizeChatgptUrl, isTemporaryChatUrl, } from "./browser/index.js";
|