@steipete/oracle 0.6.0 → 0.6.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/dist/src/browser/actions/assistantResponse.js +65 -6
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +22 -50
- package/dist/src/browser/profileState.js +171 -0
- package/dist/src/cli/sessionDisplay.js +8 -1
- package/dist/src/cli/sessionRunner.js +0 -5
- package/dist/src/remote/server.js +17 -11
- package/package.json +1 -1
|
@@ -183,7 +183,9 @@ async function pollAssistantCompletion(Runtime, timeoutMs) {
|
|
|
183
183
|
isStopButtonVisible(Runtime),
|
|
184
184
|
isCompletionVisible(Runtime),
|
|
185
185
|
]);
|
|
186
|
-
|
|
186
|
+
// Require at least 2 stable cycles even when completion buttons are visible
|
|
187
|
+
// to ensure DOM text has fully rendered (buttons can appear before text settles)
|
|
188
|
+
if ((completionVisible && stableCycles >= 2) || (!stopVisible && stableCycles >= requiredStableCycles)) {
|
|
187
189
|
return normalized;
|
|
188
190
|
}
|
|
189
191
|
}
|
|
@@ -211,10 +213,36 @@ async function isCompletionVisible(Runtime) {
|
|
|
211
213
|
try {
|
|
212
214
|
const { result } = await Runtime.evaluate({
|
|
213
215
|
expression: `(() => {
|
|
214
|
-
|
|
216
|
+
// Find the LAST assistant turn to check completion status
|
|
217
|
+
// Must match the same logic as buildAssistantExtractor for consistency
|
|
218
|
+
const ASSISTANT_SELECTOR = '${ASSISTANT_ROLE_SELECTOR}';
|
|
219
|
+
const isAssistantTurn = (node) => {
|
|
220
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
221
|
+
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
222
|
+
if (role === 'assistant') return true;
|
|
223
|
+
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
224
|
+
if (testId.includes('assistant')) return true;
|
|
225
|
+
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const turns = Array.from(document.querySelectorAll('${CONVERSATION_TURN_SELECTOR}'));
|
|
229
|
+
let lastAssistantTurn = null;
|
|
230
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
231
|
+
if (isAssistantTurn(turns[i])) {
|
|
232
|
+
lastAssistantTurn = turns[i];
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!lastAssistantTurn) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
// Check if the last assistant turn has finished action buttons (copy, thumbs up/down, share)
|
|
240
|
+
if (lastAssistantTurn.querySelector('${FINISHED_ACTIONS_SELECTOR}')) {
|
|
215
241
|
return true;
|
|
216
242
|
}
|
|
217
|
-
|
|
243
|
+
// Also check for "Done" text in the last assistant turn's markdown
|
|
244
|
+
const markdowns = lastAssistantTurn.querySelectorAll('.markdown');
|
|
245
|
+
return Array.from(markdowns).some((n) => (n.textContent || '').trim() === 'Done');
|
|
218
246
|
})()`,
|
|
219
247
|
returnByValue: true,
|
|
220
248
|
});
|
|
@@ -257,12 +285,27 @@ function buildAssistantSnapshotExpression() {
|
|
|
257
285
|
}
|
|
258
286
|
function buildResponseObserverExpression(timeoutMs) {
|
|
259
287
|
const selectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
288
|
+
const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
289
|
+
const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
|
|
260
290
|
return `(() => {
|
|
261
291
|
${buildClickDispatcher()}
|
|
262
292
|
const SELECTORS = ${selectorsLiteral};
|
|
263
293
|
const STOP_SELECTOR = '${STOP_BUTTON_SELECTOR}';
|
|
264
294
|
const FINISHED_SELECTOR = '${FINISHED_ACTIONS_SELECTOR}';
|
|
295
|
+
const CONVERSATION_SELECTOR = ${conversationLiteral};
|
|
296
|
+
const ASSISTANT_SELECTOR = ${assistantLiteral};
|
|
265
297
|
const settleDelayMs = 800;
|
|
298
|
+
|
|
299
|
+
// Helper to detect assistant turns - matches buildAssistantExtractor logic
|
|
300
|
+
const isAssistantTurn = (node) => {
|
|
301
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
302
|
+
const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
|
|
303
|
+
if (role === 'assistant') return true;
|
|
304
|
+
const testId = (node.getAttribute('data-testid') || '').toLowerCase();
|
|
305
|
+
if (testId.includes('assistant')) return true;
|
|
306
|
+
return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
|
|
307
|
+
};
|
|
308
|
+
|
|
266
309
|
${buildAssistantExtractor('extractFromTurns')}
|
|
267
310
|
|
|
268
311
|
const captureViaObserver = () =>
|
|
@@ -307,6 +350,24 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
307
350
|
}, ${timeoutMs});
|
|
308
351
|
});
|
|
309
352
|
|
|
353
|
+
// Check if the last assistant turn has finished (scoped to avoid detecting old turns)
|
|
354
|
+
const isLastAssistantTurnFinished = () => {
|
|
355
|
+
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
356
|
+
let lastAssistantTurn = null;
|
|
357
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
358
|
+
if (isAssistantTurn(turns[i])) {
|
|
359
|
+
lastAssistantTurn = turns[i];
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!lastAssistantTurn) return false;
|
|
364
|
+
// Check for action buttons in this specific turn
|
|
365
|
+
if (lastAssistantTurn.querySelector(FINISHED_SELECTOR)) return true;
|
|
366
|
+
// Check for "Done" text in this turn's markdown
|
|
367
|
+
const markdowns = lastAssistantTurn.querySelectorAll('.markdown');
|
|
368
|
+
return Array.from(markdowns).some((n) => (n.textContent || '').trim() === 'Done');
|
|
369
|
+
};
|
|
370
|
+
|
|
310
371
|
const waitForSettle = async (snapshot) => {
|
|
311
372
|
const settleWindowMs = 5000;
|
|
312
373
|
const settleIntervalMs = 400;
|
|
@@ -321,9 +382,7 @@ function buildResponseObserverExpression(timeoutMs) {
|
|
|
321
382
|
lastLength = refreshed.text?.length ?? lastLength;
|
|
322
383
|
}
|
|
323
384
|
const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
|
|
324
|
-
const finishedVisible =
|
|
325
|
-
Boolean(document.querySelector(FINISHED_SELECTOR)) ||
|
|
326
|
-
Array.from(document.querySelectorAll('.markdown')).some((n) => (n.textContent || '').trim() === 'Done');
|
|
385
|
+
const finishedVisible = isLastAssistantTurnFinished();
|
|
327
386
|
|
|
328
387
|
if (!stopVisible || finishedVisible) {
|
|
329
388
|
break;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CHATGPT_URL = 'https://chatgpt.com/';
|
|
2
|
-
export const DEFAULT_MODEL_TARGET = 'ChatGPT 5.
|
|
2
|
+
export const DEFAULT_MODEL_TARGET = 'ChatGPT 5.2';
|
|
3
3
|
export const COOKIE_URLS = ['https://chatgpt.com', 'https://chat.openai.com', 'https://atlas.openai.com'];
|
|
4
4
|
export const INPUT_SELECTORS = [
|
|
5
5
|
'textarea[data-id="prompt-textarea"]',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtemp, rm, mkdir
|
|
1
|
+
import { mkdtemp, rm, mkdir } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import net from 'node:net';
|
|
@@ -12,6 +12,7 @@ import { estimateTokenCount, withRetries, delay } from './utils.js';
|
|
|
12
12
|
import { formatElapsed } from '../oracle/format.js';
|
|
13
13
|
import { CHATGPT_URL } from './constants.js';
|
|
14
14
|
import { BrowserAutomationError } from '../oracle/errors.js';
|
|
15
|
+
import { cleanupStaleProfileState, readChromePid, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from './profileState.js';
|
|
15
16
|
export { CHATGPT_URL, DEFAULT_MODEL_TARGET } from './constants.js';
|
|
16
17
|
export { parseDuration, delay, normalizeChatgptUrl } from './utils.js';
|
|
17
18
|
export async function runBrowserMode(options) {
|
|
@@ -98,6 +99,13 @@ export async function runBrowserMode(options) {
|
|
|
98
99
|
remoteChrome: config.remoteChrome,
|
|
99
100
|
}, userDataDir, logger));
|
|
100
101
|
const chromeHost = chrome.host ?? '127.0.0.1';
|
|
102
|
+
// Persist profile state so future manual-login runs can reuse this Chrome.
|
|
103
|
+
if (manualLogin && chrome.port) {
|
|
104
|
+
await writeDevToolsActivePort(userDataDir, chrome.port);
|
|
105
|
+
if (!reusedChrome && chrome.pid) {
|
|
106
|
+
await writeChromePid(userDataDir, chrome.pid);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
101
109
|
let removeTerminationHooks = null;
|
|
102
110
|
try {
|
|
103
111
|
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
|
|
@@ -533,57 +541,21 @@ async function maybeReuseRunningChrome(userDataDir, logger) {
|
|
|
533
541
|
const port = await readDevToolsPort(userDataDir);
|
|
534
542
|
if (!port)
|
|
535
543
|
return null;
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
clearTimeout(timeout);
|
|
542
|
-
if (!response.ok)
|
|
543
|
-
throw new Error(`HTTP ${response.status}`);
|
|
544
|
-
const pidPath = path.join(userDataDir, 'chrome.pid');
|
|
545
|
-
let pid;
|
|
546
|
-
try {
|
|
547
|
-
const rawPid = (await readFile(pidPath, 'utf8')).trim();
|
|
548
|
-
pid = Number.parseInt(rawPid, 10);
|
|
549
|
-
if (Number.isNaN(pid))
|
|
550
|
-
pid = undefined;
|
|
551
|
-
}
|
|
552
|
-
catch {
|
|
553
|
-
pid = undefined;
|
|
554
|
-
}
|
|
555
|
-
logger(`Found running Chrome for ${userDataDir}; reusing (DevTools port ${port}${pid ? `, pid ${pid}` : ''})`);
|
|
556
|
-
return {
|
|
557
|
-
port,
|
|
558
|
-
pid,
|
|
559
|
-
kill: async () => { },
|
|
560
|
-
process: undefined,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
catch (error) {
|
|
564
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
565
|
-
logger(`DevToolsActivePort found for ${userDataDir} but unreachable (${message}); launching new Chrome.`);
|
|
544
|
+
const probe = await verifyDevToolsReachable({ port });
|
|
545
|
+
if (!probe.ok) {
|
|
546
|
+
logger(`DevToolsActivePort found for ${userDataDir} but unreachable (${probe.error}); launching new Chrome.`);
|
|
547
|
+
// Safe cleanup: remove stale DevToolsActivePort; only remove lock files if this was an Oracle-owned pid that died.
|
|
548
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: 'if_oracle_pid_dead' });
|
|
566
549
|
return null;
|
|
567
550
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const raw = await readFile(candidate, 'utf8');
|
|
577
|
-
const firstLine = raw.split(/\r?\n/u)[0]?.trim();
|
|
578
|
-
const port = Number.parseInt(firstLine ?? '', 10);
|
|
579
|
-
if (Number.isFinite(port)) {
|
|
580
|
-
return port;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
catch {
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
return null;
|
|
551
|
+
const pid = await readChromePid(userDataDir);
|
|
552
|
+
logger(`Found running Chrome for ${userDataDir}; reusing (DevTools port ${port}${pid ? `, pid ${pid}` : ''})`);
|
|
553
|
+
return {
|
|
554
|
+
port,
|
|
555
|
+
pid: pid ?? undefined,
|
|
556
|
+
kill: async () => { },
|
|
557
|
+
process: undefined,
|
|
558
|
+
};
|
|
587
559
|
}
|
|
588
560
|
async function runRemoteBrowserMode(promptText, attachments, config, logger, options) {
|
|
589
561
|
const remoteChromeConfig = config.remoteChrome;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const DEVTOOLS_ACTIVE_PORT_FILENAME = 'DevToolsActivePort';
|
|
6
|
+
const DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS = [
|
|
7
|
+
DEVTOOLS_ACTIVE_PORT_FILENAME,
|
|
8
|
+
path.join('Default', DEVTOOLS_ACTIVE_PORT_FILENAME),
|
|
9
|
+
];
|
|
10
|
+
const CHROME_PID_FILENAME = 'chrome.pid';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
export function getDevToolsActivePortPaths(userDataDir) {
|
|
13
|
+
return DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS.map((relative) => path.join(userDataDir, relative));
|
|
14
|
+
}
|
|
15
|
+
export async function readDevToolsPort(userDataDir) {
|
|
16
|
+
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readFile(candidate, 'utf8');
|
|
19
|
+
const firstLine = raw.split(/\r?\n/u)[0]?.trim();
|
|
20
|
+
const port = Number.parseInt(firstLine ?? '', 10);
|
|
21
|
+
if (Number.isFinite(port)) {
|
|
22
|
+
return port;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// ignore missing/unreadable candidates
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
export async function writeDevToolsActivePort(userDataDir, port) {
|
|
32
|
+
const contents = `${port}\n/devtools/browser`;
|
|
33
|
+
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
34
|
+
try {
|
|
35
|
+
await mkdir(path.dirname(candidate), { recursive: true });
|
|
36
|
+
await writeFile(candidate, contents, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// best effort
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function readChromePid(userDataDir) {
|
|
44
|
+
const pidPath = path.join(userDataDir, CHROME_PID_FILENAME);
|
|
45
|
+
try {
|
|
46
|
+
const raw = (await readFile(pidPath, 'utf8')).trim();
|
|
47
|
+
const pid = Number.parseInt(raw, 10);
|
|
48
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return pid;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function writeChromePid(userDataDir, pid) {
|
|
58
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
59
|
+
return;
|
|
60
|
+
const pidPath = path.join(userDataDir, CHROME_PID_FILENAME);
|
|
61
|
+
try {
|
|
62
|
+
await mkdir(path.dirname(pidPath), { recursive: true });
|
|
63
|
+
await writeFile(pidPath, `${Math.trunc(pid)}\n`, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// best effort
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function isProcessAlive(pid) {
|
|
70
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
71
|
+
return false;
|
|
72
|
+
try {
|
|
73
|
+
process.kill(pid, 0);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// EPERM means "exists but no permission"; treat as alive.
|
|
78
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'EPERM') {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function verifyDevToolsReachable({ port, host = '127.0.0.1', attempts = 3, timeoutMs = 3000, }) {
|
|
85
|
+
const versionUrl = `http://${host}:${port}/json/version`;
|
|
86
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
87
|
+
try {
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
90
|
+
const response = await fetch(versionUrl, { signal: controller.signal });
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(`HTTP ${response.status}`);
|
|
94
|
+
}
|
|
95
|
+
return { ok: true };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (attempt < attempts - 1) {
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
+
return { ok: false, error: message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { ok: false, error: 'unreachable' };
|
|
107
|
+
}
|
|
108
|
+
export async function cleanupStaleProfileState(userDataDir, logger, options = {}) {
|
|
109
|
+
for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
|
|
110
|
+
try {
|
|
111
|
+
await rm(candidate, { force: true });
|
|
112
|
+
logger?.(`Removed stale DevToolsActivePort: ${candidate}`);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// ignore cleanup errors
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const lockRemovalMode = options.lockRemovalMode ?? 'never';
|
|
119
|
+
if (lockRemovalMode === 'never') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const pid = await readChromePid(userDataDir);
|
|
123
|
+
if (!pid) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (isProcessAlive(pid)) {
|
|
127
|
+
logger?.(`Chrome pid ${pid} still alive; skipping profile lock cleanup`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Extra safety: if Chrome is running with this profile (but with a different PID, e.g. user relaunched
|
|
131
|
+
// without remote debugging), never delete lock files.
|
|
132
|
+
if (await isChromeUsingUserDataDir(userDataDir)) {
|
|
133
|
+
logger?.('Detected running Chrome using this profile; skipping profile lock cleanup');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const lockFiles = [
|
|
137
|
+
path.join(userDataDir, 'lockfile'),
|
|
138
|
+
path.join(userDataDir, 'SingletonLock'),
|
|
139
|
+
path.join(userDataDir, 'SingletonSocket'),
|
|
140
|
+
path.join(userDataDir, 'SingletonCookie'),
|
|
141
|
+
];
|
|
142
|
+
for (const lock of lockFiles) {
|
|
143
|
+
await rm(lock, { force: true }).catch(() => undefined);
|
|
144
|
+
}
|
|
145
|
+
logger?.('Cleaned up stale Chrome profile locks');
|
|
146
|
+
}
|
|
147
|
+
async function isChromeUsingUserDataDir(userDataDir) {
|
|
148
|
+
if (process.platform === 'win32') {
|
|
149
|
+
// On Windows, lockfiles are typically held open and removal should fail anyway; avoid expensive process scans.
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const { stdout } = await execFileAsync('ps', ['-ax', '-o', 'command='], { maxBuffer: 10 * 1024 * 1024 });
|
|
154
|
+
const lines = String(stdout ?? '').split('\n');
|
|
155
|
+
const needle = userDataDir;
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
if (!line)
|
|
158
|
+
continue;
|
|
159
|
+
const lower = line.toLowerCase();
|
|
160
|
+
if (!lower.includes('chrome') && !lower.includes('chromium'))
|
|
161
|
+
continue;
|
|
162
|
+
if (line.includes(needle) && lower.includes('user-data-dir')) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// best effort
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
@@ -18,7 +18,14 @@ function isProcessAlive(pid) {
|
|
|
18
18
|
return true;
|
|
19
19
|
}
|
|
20
20
|
catch (error) {
|
|
21
|
-
|
|
21
|
+
const code = error instanceof Error ? error.code : undefined;
|
|
22
|
+
if (code === 'ESRCH' || code === 'EINVAL') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (code === 'EPERM') {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
22
29
|
}
|
|
23
30
|
}
|
|
24
31
|
const CLEANUP_TIP = 'Tip: Run "oracle session --clear --hours 24" to prune cached runs (add --all to wipe everything).';
|
|
@@ -360,11 +360,6 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
360
360
|
}
|
|
361
361
|
: undefined,
|
|
362
362
|
});
|
|
363
|
-
if (mode === 'browser') {
|
|
364
|
-
log(dim('Next steps (browser fallback):')); // guides users when automation breaks
|
|
365
|
-
log(dim('- Rerun with --engine api to bypass Chrome entirely.'));
|
|
366
|
-
log(dim('- Or rerun with --engine api --render-markdown [--file …] to generate a single markdown bundle you can paste into ChatGPT manually (add --browser-bundle-files if you still want attachments).'));
|
|
367
|
-
}
|
|
368
363
|
if (modelForStatus) {
|
|
369
364
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
370
365
|
status: 'error',
|
|
@@ -4,12 +4,12 @@ import path from 'node:path';
|
|
|
4
4
|
import net from 'node:net';
|
|
5
5
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
6
6
|
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
-
import { existsSync } from 'node:fs';
|
|
8
7
|
import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
|
|
9
8
|
import chalk from 'chalk';
|
|
10
9
|
import { runBrowserMode } from '../browserMode.js';
|
|
11
10
|
import { loadChromeCookies } from '../browser/chromeCookies.js';
|
|
12
11
|
import { CHATGPT_URL } from '../browser/constants.js';
|
|
12
|
+
import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from '../browser/profileState.js';
|
|
13
13
|
import { normalizeChatgptUrl } from '../browser/utils.js';
|
|
14
14
|
async function findAvailablePort() {
|
|
15
15
|
return await new Promise((resolve, reject) => {
|
|
@@ -209,10 +209,17 @@ export async function serveRemote(options = {}) {
|
|
|
209
209
|
if (preferManualLogin) {
|
|
210
210
|
await mkdir(manualProfileDir, { recursive: true });
|
|
211
211
|
console.log(`Cookie extraction is unavailable on this platform. Using manual-login Chrome profile at ${manualProfileDir}. Remote runs will reuse this profile; sign in once when the browser opens.`);
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
const existingPort = await readDevToolsPort(manualProfileDir);
|
|
213
|
+
if (existingPort) {
|
|
214
|
+
const reachable = await verifyDevToolsReachable({ port: existingPort });
|
|
215
|
+
if (reachable.ok) {
|
|
216
|
+
console.log('Detected an existing automation Chrome session; will reuse it for manual login.');
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.log(`Found stale DevToolsActivePort (port ${existingPort}, ${reachable.error}); launching a fresh manual-login Chrome.`);
|
|
220
|
+
await cleanupStaleProfileState(manualProfileDir, console.log, { lockRemovalMode: 'never' });
|
|
221
|
+
void launchManualLoginChrome(manualProfileDir, CHATGPT_URL, console.log);
|
|
222
|
+
}
|
|
216
223
|
}
|
|
217
224
|
else {
|
|
218
225
|
void launchManualLoginChrome(manualProfileDir, CHATGPT_URL, console.log);
|
|
@@ -459,12 +466,11 @@ async function launchManualLoginChrome(profileDir, url, logger) {
|
|
|
459
466
|
});
|
|
460
467
|
const chosenPort = chrome?.port ?? debugPort ?? null;
|
|
461
468
|
if (chosenPort) {
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
await writeFile(devtoolsFileDefault, contents).catch(() => undefined);
|
|
469
|
+
// Persist DevToolsActivePort eagerly so future runs can attach/reuse this Chrome.
|
|
470
|
+
await writeDevToolsActivePort(profileDir, chosenPort);
|
|
471
|
+
if (chrome?.pid) {
|
|
472
|
+
await writeChromePid(profileDir, chrome.pid);
|
|
473
|
+
}
|
|
468
474
|
logger(`Manual-login Chrome DevTools port: ${chosenPort}`);
|
|
469
475
|
logger(`If needed, DevTools JSON at http://127.0.0.1:${chosenPort}/json/version`);
|
|
470
476
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|