@indykish/oracle 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +1252 -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 +125 -0
- package/dist/scripts/run-cli.js +14 -0
- package/dist/scripts/runner.js +1378 -0
- package/dist/scripts/test-browser.js +103 -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 +1067 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
- package/dist/src/browser/actions/attachments.js +1910 -0
- package/dist/src/browser/actions/domEvents.js +19 -0
- package/dist/src/browser/actions/modelSelection.js +485 -0
- package/dist/src/browser/actions/navigation.js +445 -0
- package/dist/src/browser/actions/promptComposer.js +485 -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 +344 -0
- package/dist/src/browser/config.js +103 -0
- package/dist/src/browser/constants.js +71 -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 +1741 -0
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/policies.js +43 -0
- package/dist/src/browser/profileState.js +280 -0
- package/dist/src/browser/prompt.js +152 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/reattach.js +186 -0
- package/dist/src/browser/reattachHelpers.js +382 -0
- package/dist/src/browser/sessionRunner.js +119 -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 +73 -0
- package/dist/src/cli/bridge/codexConfig.js +43 -0
- package/dist/src/cli/bridge/doctor.js +107 -0
- package/dist/src/cli/bridge/host.js +259 -0
- package/dist/src/cli/browserConfig.js +278 -0
- package/dist/src/cli/browserDefaults.js +81 -0
- package/dist/src/cli/bundleWarnings.js +9 -0
- package/dist/src/cli/clipboard.js +10 -0
- package/dist/src/cli/detach.js +11 -0
- package/dist/src/cli/dryRun.js +105 -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/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 +17 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +306 -0
- package/dist/src/cli/options.js +281 -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 +78 -0
- package/dist/src/cli/sessionCommand.js +111 -0
- package/dist/src/cli/sessionDisplay.js +567 -0
- package/dist/src/cli/sessionRunner.js +602 -0
- package/dist/src/cli/sessionTable.js +92 -0
- package/dist/src/cli/tagline.js +258 -0
- package/dist/src/cli/tui/index.js +486 -0
- package/dist/src/cli/writeOutputPath.js +21 -0
- package/dist/src/config.js +26 -0
- package/dist/src/gemini-web/client.js +328 -0
- package/dist/src/gemini-web/executor.js +285 -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 +290 -0
- package/dist/src/mcp/tools/sessionResources.js +75 -0
- package/dist/src/mcp/tools/sessions.js +105 -0
- package/dist/src/mcp/types.js +22 -0
- package/dist/src/mcp/utils.js +37 -0
- package/dist/src/oracle/background.js +141 -0
- package/dist/src/oracle/claude.js +101 -0
- package/dist/src/oracle/client.js +197 -0
- package/dist/src/oracle/config.js +227 -0
- package/dist/src/oracle/errors.js +132 -0
- package/dist/src/oracle/files.js +378 -0
- package/dist/src/oracle/finishLine.js +32 -0
- package/dist/src/oracle/format.js +30 -0
- package/dist/src/oracle/fsAdapter.js +10 -0
- package/dist/src/oracle/gemini.js +195 -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 +13 -0
- package/dist/src/oracle/request.js +50 -0
- package/dist/src/oracle/run.js +596 -0
- package/dist/src/oracle/runUtils.js +31 -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 +533 -0
- package/dist/src/remote/types.js +1 -0
- package/dist/src/sessionManager.js +637 -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 +24 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +115 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function normalizeBrowserModelStrategy(value) {
|
|
2
|
+
if (value == null) {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
const normalized = value.trim().toLowerCase();
|
|
6
|
+
if (!normalized) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
if (normalized === 'select' || normalized === 'current' || normalized === 'ignore') {
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
throw new Error(`Invalid browser model strategy: "${value}". Expected "select", "current", or "ignore".`);
|
|
13
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, installJavaScriptDialogAutoDismissal, } from './actions/navigation.js';
|
|
2
|
+
export { ensureModelSelection } from './actions/modelSelection.js';
|
|
3
|
+
export { submitPrompt, clearPromptComposer } from './actions/promptComposer.js';
|
|
4
|
+
export { clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, } from './actions/attachments.js';
|
|
5
|
+
export { waitForAssistantResponse, readAssistantSnapshot, captureAssistantMarkdown, buildAssistantExtractorForTest, buildConversationDebugExpressionForTest, buildMarkdownFallbackExtractorForTest, buildCopyExpressionForTest, } from './actions/assistantResponse.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { formatFileSection } from '../oracle/markdown.js';
|
|
2
|
+
export function buildAttachmentPlan(sections, { inlineFiles, bundleRequested, maxAttachments = 10, }) {
|
|
3
|
+
if (inlineFiles) {
|
|
4
|
+
const inlineLines = [];
|
|
5
|
+
sections.forEach((section) => {
|
|
6
|
+
inlineLines.push(formatFileSection(section.displayPath, section.content).trimEnd(), '');
|
|
7
|
+
});
|
|
8
|
+
const inlineBlock = inlineLines.join('\n').trim();
|
|
9
|
+
return {
|
|
10
|
+
mode: 'inline',
|
|
11
|
+
inlineBlock,
|
|
12
|
+
inlineFileCount: sections.length,
|
|
13
|
+
attachments: [],
|
|
14
|
+
shouldBundle: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const attachments = sections.map((section) => ({
|
|
18
|
+
path: section.absolutePath,
|
|
19
|
+
displayPath: section.displayPath,
|
|
20
|
+
sizeBytes: Buffer.byteLength(section.content, 'utf8'),
|
|
21
|
+
}));
|
|
22
|
+
const shouldBundle = bundleRequested || attachments.length > maxAttachments;
|
|
23
|
+
return {
|
|
24
|
+
mode: shouldBundle ? 'bundle' : 'upload',
|
|
25
|
+
inlineBlock: '',
|
|
26
|
+
inlineFileCount: 0,
|
|
27
|
+
attachments,
|
|
28
|
+
shouldBundle,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function buildCookiePlan(config) {
|
|
32
|
+
if (config?.inlineCookies && config.inlineCookies.length > 0) {
|
|
33
|
+
const source = config.inlineCookiesSource ?? 'inline';
|
|
34
|
+
return { type: 'inline', description: `Cookies: inline payload (${config.inlineCookies.length}) via ${source}.` };
|
|
35
|
+
}
|
|
36
|
+
if (config?.cookieSync === false) {
|
|
37
|
+
return { type: 'disabled', description: 'Cookies: sync disabled (--browser-no-cookie-sync).' };
|
|
38
|
+
}
|
|
39
|
+
const allowlist = config?.cookieNames && config.cookieNames.length > 0
|
|
40
|
+
? config.cookieNames.join(', ')
|
|
41
|
+
: 'all from Chrome profile';
|
|
42
|
+
return { type: 'copy', description: `Cookies: copy from Chrome (${allowlist}).` };
|
|
43
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { delay } from './utils.js';
|
|
7
|
+
const DEVTOOLS_ACTIVE_PORT_FILENAME = 'DevToolsActivePort';
|
|
8
|
+
const DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS = [
|
|
9
|
+
DEVTOOLS_ACTIVE_PORT_FILENAME,
|
|
10
|
+
path.join('Default', DEVTOOLS_ACTIVE_PORT_FILENAME),
|
|
11
|
+
];
|
|
12
|
+
const CHROME_PID_FILENAME = 'chrome.pid';
|
|
13
|
+
const ORACLE_PROFILE_LOCK_FILENAME = 'oracle-automation.lock';
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
export function getDevToolsActivePortPaths(userDataDir) {
|
|
16
|
+
return DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS.map((relative) => path.join(userDataDir, relative));
|
|
17
|
+
}
|
|
18
|
+
export async function readDevToolsPort(userDataDir) {
|
|
19
|
+
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readFile(candidate, 'utf8');
|
|
22
|
+
const firstLine = raw.split(/\r?\n/u)[0]?.trim();
|
|
23
|
+
const port = Number.parseInt(firstLine ?? '', 10);
|
|
24
|
+
if (Number.isFinite(port)) {
|
|
25
|
+
return port;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// ignore missing/unreadable candidates
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export async function writeDevToolsActivePort(userDataDir, port) {
|
|
35
|
+
const contents = `${port}\n/devtools/browser`;
|
|
36
|
+
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
37
|
+
try {
|
|
38
|
+
await mkdir(path.dirname(candidate), { recursive: true });
|
|
39
|
+
await writeFile(candidate, contents, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// best effort
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function readChromePid(userDataDir) {
|
|
47
|
+
const pidPath = path.join(userDataDir, CHROME_PID_FILENAME);
|
|
48
|
+
try {
|
|
49
|
+
const raw = (await readFile(pidPath, 'utf8')).trim();
|
|
50
|
+
const pid = Number.parseInt(raw, 10);
|
|
51
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return pid;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function writeChromePid(userDataDir, pid) {
|
|
61
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
62
|
+
return;
|
|
63
|
+
const pidPath = path.join(userDataDir, CHROME_PID_FILENAME);
|
|
64
|
+
try {
|
|
65
|
+
await mkdir(path.dirname(pidPath), { recursive: true });
|
|
66
|
+
await writeFile(pidPath, `${Math.trunc(pid)}\n`, 'utf8');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// best effort
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function isProcessAlive(pid) {
|
|
73
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
74
|
+
return false;
|
|
75
|
+
try {
|
|
76
|
+
process.kill(pid, 0);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
// EPERM means "exists but no permission"; treat as alive.
|
|
81
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'EPERM') {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function parseProfileRunLock(payload) {
|
|
88
|
+
if (!payload)
|
|
89
|
+
return null;
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(payload);
|
|
92
|
+
if (!Number.isFinite(parsed.pid) || parsed.pid <= 0)
|
|
93
|
+
return null;
|
|
94
|
+
if (!parsed.lockId || typeof parsed.lockId !== 'string')
|
|
95
|
+
return null;
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function acquireProfileRunLock(userDataDir, options) {
|
|
103
|
+
const timeoutMs = options.timeoutMs;
|
|
104
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const pollMs = typeof options.pollMs === 'number' && Number.isFinite(options.pollMs) && options.pollMs > 0
|
|
108
|
+
? options.pollMs
|
|
109
|
+
: 1000;
|
|
110
|
+
const lockPath = path.join(userDataDir, ORACLE_PROFILE_LOCK_FILENAME);
|
|
111
|
+
const lockId = randomUUID();
|
|
112
|
+
const startedAt = Date.now();
|
|
113
|
+
let warned = false;
|
|
114
|
+
for (;;) {
|
|
115
|
+
try {
|
|
116
|
+
const payload = {
|
|
117
|
+
pid: process.pid,
|
|
118
|
+
lockId,
|
|
119
|
+
createdAt: new Date().toISOString(),
|
|
120
|
+
sessionId: options.sessionId,
|
|
121
|
+
};
|
|
122
|
+
await mkdir(path.dirname(lockPath), { recursive: true });
|
|
123
|
+
await writeFile(lockPath, JSON.stringify(payload), { encoding: 'utf8', flag: 'wx' });
|
|
124
|
+
options.logger?.(`Acquired Oracle profile lock at ${lockPath}`);
|
|
125
|
+
return {
|
|
126
|
+
path: lockPath,
|
|
127
|
+
lockId,
|
|
128
|
+
release: async () => releaseProfileRunLock(lockPath, lockId, options.logger),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const code = error.code;
|
|
133
|
+
if (code !== 'EEXIST') {
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
let existing = parseProfileRunLock(await readFile(lockPath, 'utf8').catch(() => null));
|
|
137
|
+
if (!existing) {
|
|
138
|
+
// Likely partial write / corruption; re-read once, then delete (user preference: delete unreadable lockfiles).
|
|
139
|
+
await delay(200);
|
|
140
|
+
existing = parseProfileRunLock(await readFile(lockPath, 'utf8').catch(() => null));
|
|
141
|
+
if (!existing) {
|
|
142
|
+
options.logger?.('Oracle profile lock unreadable; deleting lockfile.');
|
|
143
|
+
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!existing || !isProcessAlive(existing.pid)) {
|
|
148
|
+
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!warned) {
|
|
152
|
+
const waited = Math.round(timeoutMs / 1000);
|
|
153
|
+
options.logger?.(`Oracle profile lock held by pid ${existing.pid}; waiting up to ${waited}s.`);
|
|
154
|
+
warned = true;
|
|
155
|
+
}
|
|
156
|
+
const elapsed = Date.now() - startedAt;
|
|
157
|
+
if (elapsed >= timeoutMs) {
|
|
158
|
+
throw new Error(`Oracle profile lock still held by pid ${existing.pid} after ${Math.round(elapsed / 1000)}s`);
|
|
159
|
+
}
|
|
160
|
+
await delay(Math.min(pollMs, timeoutMs - elapsed));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export async function releaseProfileRunLock(lockPath, lockId, logger) {
|
|
165
|
+
try {
|
|
166
|
+
const existing = parseProfileRunLock(await readFile(lockPath, 'utf8').catch(() => null));
|
|
167
|
+
if (!existing || existing.lockId !== lockId) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await rm(lockPath, { force: true });
|
|
171
|
+
logger?.(`Released Oracle profile lock ${lockPath}`);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// best effort
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export async function verifyDevToolsReachable({ port, host = '127.0.0.1', attempts = 3, timeoutMs = 3000, }) {
|
|
178
|
+
const versionUrl = `http://${host}:${port}/json/version`;
|
|
179
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
180
|
+
try {
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
183
|
+
const response = await fetch(versionUrl, { signal: controller.signal });
|
|
184
|
+
clearTimeout(timeout);
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
throw new Error(`HTTP ${response.status}`);
|
|
187
|
+
}
|
|
188
|
+
return { ok: true };
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (attempt < attempts - 1) {
|
|
192
|
+
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
+
return { ok: false, error: message };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { ok: false, error: 'unreachable' };
|
|
200
|
+
}
|
|
201
|
+
export async function shouldCleanupManualLoginProfileState(userDataDir, logger, options = {}) {
|
|
202
|
+
if (!options.connectionClosedUnexpectedly) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
const port = await readDevToolsPort(userDataDir);
|
|
206
|
+
if (!port) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
const probe = await (options.probe ?? verifyDevToolsReachable)({ port, host: options.host });
|
|
210
|
+
if (probe.ok) {
|
|
211
|
+
logger?.(`DevTools port ${port} still reachable; preserving manual-login profile state`);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
logger?.(`DevTools port ${port} unreachable (${probe.error}); clearing stale profile state`);
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
export async function cleanupStaleProfileState(userDataDir, logger, options = {}) {
|
|
218
|
+
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
219
|
+
try {
|
|
220
|
+
await rm(candidate, { force: true });
|
|
221
|
+
logger?.(`Removed stale DevToolsActivePort: ${candidate}`);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// ignore cleanup errors
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const lockRemovalMode = options.lockRemovalMode ?? 'never';
|
|
228
|
+
if (lockRemovalMode === 'never') {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const pid = await readChromePid(userDataDir);
|
|
232
|
+
if (!pid) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (isProcessAlive(pid)) {
|
|
236
|
+
logger?.(`Chrome pid ${pid} still alive; skipping profile lock cleanup`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Extra safety: if Chrome is running with this profile (but with a different PID, e.g. user relaunched
|
|
240
|
+
// without remote debugging), never delete lock files.
|
|
241
|
+
if (await isChromeUsingUserDataDir(userDataDir)) {
|
|
242
|
+
logger?.('Detected running Chrome using this profile; skipping profile lock cleanup');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const lockFiles = [
|
|
246
|
+
path.join(userDataDir, 'lockfile'),
|
|
247
|
+
path.join(userDataDir, 'SingletonLock'),
|
|
248
|
+
path.join(userDataDir, 'SingletonSocket'),
|
|
249
|
+
path.join(userDataDir, 'SingletonCookie'),
|
|
250
|
+
];
|
|
251
|
+
for (const lock of lockFiles) {
|
|
252
|
+
await rm(lock, { force: true }).catch(() => undefined);
|
|
253
|
+
}
|
|
254
|
+
logger?.('Cleaned up stale Chrome profile locks');
|
|
255
|
+
}
|
|
256
|
+
async function isChromeUsingUserDataDir(userDataDir) {
|
|
257
|
+
if (process.platform === 'win32') {
|
|
258
|
+
// On Windows, lockfiles are typically held open and removal should fail anyway; avoid expensive process scans.
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const { stdout } = await execFileAsync('ps', ['-ax', '-o', 'command='], { maxBuffer: 10 * 1024 * 1024 });
|
|
263
|
+
const lines = String(stdout ?? '').split('\n');
|
|
264
|
+
const needle = userDataDir;
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
if (!line)
|
|
267
|
+
continue;
|
|
268
|
+
const lower = line.toLowerCase();
|
|
269
|
+
if (!lower.includes('chrome') && !lower.includes('chromium'))
|
|
270
|
+
continue;
|
|
271
|
+
if (line.includes(needle) && lower.includes('user-data-dir')) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// best effort
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, formatFileSection, } from '../oracle.js';
|
|
5
|
+
import { isKnownModel } from '../oracle/modelResolver.js';
|
|
6
|
+
import { buildPromptMarkdown } from '../oracle/promptAssembly.js';
|
|
7
|
+
import { buildAttachmentPlan } from './policies.js';
|
|
8
|
+
const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
|
|
9
|
+
const MEDIA_EXTENSIONS = new Set([
|
|
10
|
+
'.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v',
|
|
11
|
+
'.mp3', '.wav', '.aac', '.flac', '.ogg', '.m4a',
|
|
12
|
+
'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.heic', '.heif',
|
|
13
|
+
'.pdf',
|
|
14
|
+
]);
|
|
15
|
+
export function isMediaFile(filePath) {
|
|
16
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
17
|
+
return MEDIA_EXTENSIONS.has(ext);
|
|
18
|
+
}
|
|
19
|
+
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
20
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
21
|
+
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
22
|
+
const allFilePaths = runOptions.file ?? [];
|
|
23
|
+
const textFilePaths = allFilePaths.filter((f) => !isMediaFile(f));
|
|
24
|
+
const mediaFilePaths = allFilePaths.filter((f) => isMediaFile(f));
|
|
25
|
+
const mediaAttachments = await Promise.all(mediaFilePaths.map(async (filePath) => {
|
|
26
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
27
|
+
const stats = await fs.stat(resolvedPath);
|
|
28
|
+
return {
|
|
29
|
+
path: resolvedPath,
|
|
30
|
+
displayPath: path.relative(cwd, resolvedPath) || path.basename(resolvedPath),
|
|
31
|
+
sizeBytes: stats.size,
|
|
32
|
+
};
|
|
33
|
+
}));
|
|
34
|
+
const files = await readFilesFn(textFilePaths, { cwd });
|
|
35
|
+
const basePrompt = (runOptions.prompt ?? '').trim();
|
|
36
|
+
const userPrompt = basePrompt;
|
|
37
|
+
const systemPrompt = runOptions.system?.trim() || '';
|
|
38
|
+
const sections = createFileSections(files, cwd);
|
|
39
|
+
const markdown = buildPromptMarkdown(systemPrompt, userPrompt, sections);
|
|
40
|
+
const attachmentsPolicy = runOptions.browserInlineFiles
|
|
41
|
+
? 'never'
|
|
42
|
+
: runOptions.browserAttachments ?? 'auto';
|
|
43
|
+
const bundleRequested = Boolean(runOptions.browserBundleFiles);
|
|
44
|
+
const inlinePlan = buildAttachmentPlan(sections, { inlineFiles: true, bundleRequested });
|
|
45
|
+
const uploadPlan = buildAttachmentPlan(sections, { inlineFiles: false, bundleRequested });
|
|
46
|
+
const baseComposerSections = [];
|
|
47
|
+
if (systemPrompt)
|
|
48
|
+
baseComposerSections.push(systemPrompt);
|
|
49
|
+
if (userPrompt)
|
|
50
|
+
baseComposerSections.push(userPrompt);
|
|
51
|
+
const inlineComposerText = [...baseComposerSections, inlinePlan.inlineBlock].filter(Boolean).join('\n\n').trim();
|
|
52
|
+
const selectedPlan = attachmentsPolicy === 'always'
|
|
53
|
+
? uploadPlan
|
|
54
|
+
: attachmentsPolicy === 'never'
|
|
55
|
+
? inlinePlan
|
|
56
|
+
: inlineComposerText.length <= DEFAULT_BROWSER_INLINE_CHAR_BUDGET || sections.length === 0
|
|
57
|
+
? inlinePlan
|
|
58
|
+
: uploadPlan;
|
|
59
|
+
const composerText = (selectedPlan.inlineBlock
|
|
60
|
+
? [...baseComposerSections, selectedPlan.inlineBlock]
|
|
61
|
+
: baseComposerSections)
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join('\n\n')
|
|
64
|
+
.trim();
|
|
65
|
+
const attachments = [...selectedPlan.attachments, ...mediaAttachments];
|
|
66
|
+
const shouldBundle = selectedPlan.shouldBundle;
|
|
67
|
+
let bundleText = null;
|
|
68
|
+
let bundled = null;
|
|
69
|
+
if (shouldBundle) {
|
|
70
|
+
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
71
|
+
const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
|
|
72
|
+
const bundleLines = [];
|
|
73
|
+
sections.forEach((section) => {
|
|
74
|
+
bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
|
|
75
|
+
bundleLines.push('');
|
|
76
|
+
});
|
|
77
|
+
bundleText = `${bundleLines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
78
|
+
await fs.writeFile(bundlePath, bundleText, 'utf8');
|
|
79
|
+
attachments.length = 0;
|
|
80
|
+
attachments.push({
|
|
81
|
+
path: bundlePath,
|
|
82
|
+
displayPath: bundlePath,
|
|
83
|
+
sizeBytes: Buffer.byteLength(bundleText, 'utf8'),
|
|
84
|
+
});
|
|
85
|
+
attachments.push(...mediaAttachments);
|
|
86
|
+
bundled = { originalCount: sections.length, bundlePath };
|
|
87
|
+
}
|
|
88
|
+
const inlineFileCount = selectedPlan.inlineFileCount;
|
|
89
|
+
const modelConfig = isKnownModel(runOptions.model) ? MODEL_CONFIGS[runOptions.model] : MODEL_CONFIGS['gpt-5.1'];
|
|
90
|
+
const tokenizer = deps.tokenizeImpl ?? modelConfig.tokenizer;
|
|
91
|
+
const tokenizerUserContent = inlineFileCount > 0 && selectedPlan.inlineBlock
|
|
92
|
+
? [userPrompt, selectedPlan.inlineBlock].filter((value) => Boolean(value?.trim())).join('\n\n').trim()
|
|
93
|
+
: userPrompt;
|
|
94
|
+
const tokenizerMessages = [
|
|
95
|
+
systemPrompt ? { role: 'system', content: systemPrompt } : null,
|
|
96
|
+
tokenizerUserContent ? { role: 'user', content: tokenizerUserContent } : null,
|
|
97
|
+
].filter(Boolean);
|
|
98
|
+
let estimatedInputTokens = tokenizer(tokenizerMessages.length > 0
|
|
99
|
+
? tokenizerMessages
|
|
100
|
+
: [{ role: 'user', content: '' }], TOKENIZER_OPTIONS);
|
|
101
|
+
const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(selectedPlan.inlineBlock);
|
|
102
|
+
if (!tokenEstimateIncludesInlineFiles && sections.length > 0) {
|
|
103
|
+
const attachmentText = bundleText ??
|
|
104
|
+
sections
|
|
105
|
+
.map((section) => formatFileSection(section.displayPath, section.content).trimEnd())
|
|
106
|
+
.join('\n\n');
|
|
107
|
+
const attachmentTokens = tokenizer([{ role: 'user', content: attachmentText }], TOKENIZER_OPTIONS);
|
|
108
|
+
estimatedInputTokens += attachmentTokens;
|
|
109
|
+
}
|
|
110
|
+
let fallback = null;
|
|
111
|
+
if (attachmentsPolicy === 'auto' && selectedPlan.mode === 'inline' && sections.length > 0) {
|
|
112
|
+
const fallbackComposerText = baseComposerSections.join('\n\n').trim();
|
|
113
|
+
const fallbackAttachments = [...uploadPlan.attachments, ...mediaAttachments];
|
|
114
|
+
let fallbackBundled = null;
|
|
115
|
+
if (uploadPlan.shouldBundle) {
|
|
116
|
+
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-browser-bundle-'));
|
|
117
|
+
const bundlePath = path.join(bundleDir, 'attachments-bundle.txt');
|
|
118
|
+
const bundleLines = [];
|
|
119
|
+
sections.forEach((section) => {
|
|
120
|
+
bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
|
|
121
|
+
bundleLines.push('');
|
|
122
|
+
});
|
|
123
|
+
const fallbackBundleText = `${bundleLines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
124
|
+
await fs.writeFile(bundlePath, fallbackBundleText, 'utf8');
|
|
125
|
+
fallbackAttachments.length = 0;
|
|
126
|
+
fallbackAttachments.push({
|
|
127
|
+
path: bundlePath,
|
|
128
|
+
displayPath: bundlePath,
|
|
129
|
+
sizeBytes: Buffer.byteLength(fallbackBundleText, 'utf8'),
|
|
130
|
+
});
|
|
131
|
+
fallbackAttachments.push(...mediaAttachments);
|
|
132
|
+
fallbackBundled = { originalCount: sections.length, bundlePath };
|
|
133
|
+
}
|
|
134
|
+
fallback = {
|
|
135
|
+
composerText: fallbackComposerText,
|
|
136
|
+
attachments: fallbackAttachments,
|
|
137
|
+
bundled: fallbackBundled,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
markdown,
|
|
142
|
+
composerText,
|
|
143
|
+
estimatedInputTokens,
|
|
144
|
+
attachments,
|
|
145
|
+
inlineFileCount,
|
|
146
|
+
tokenEstimateIncludesInlineFiles,
|
|
147
|
+
attachmentsPolicy,
|
|
148
|
+
attachmentMode: selectedPlan.mode,
|
|
149
|
+
fallback,
|
|
150
|
+
bundled,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { formatBytes } from './utils.js';
|
|
2
|
+
export function buildTokenEstimateSuffix(artifacts) {
|
|
3
|
+
if (artifacts.tokenEstimateIncludesInlineFiles && artifacts.inlineFileCount > 0) {
|
|
4
|
+
const count = artifacts.inlineFileCount;
|
|
5
|
+
const plural = count === 1 ? '' : 's';
|
|
6
|
+
return ` (includes ${count} inline file${plural})`;
|
|
7
|
+
}
|
|
8
|
+
if (artifacts.attachments.length > 0) {
|
|
9
|
+
const count = artifacts.attachments.length;
|
|
10
|
+
const plural = count === 1 ? '' : 's';
|
|
11
|
+
return ` (prompt only; ${count} attachment${plural} excluded)`;
|
|
12
|
+
}
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
export function formatAttachmentLabel(attachment) {
|
|
16
|
+
if (typeof attachment.sizeBytes !== 'number' || Number.isNaN(attachment.sizeBytes)) {
|
|
17
|
+
return attachment.displayPath;
|
|
18
|
+
}
|
|
19
|
+
return `${attachment.displayPath} (${formatBytes(attachment.sizeBytes)})`;
|
|
20
|
+
}
|