@steipete/oracle 0.18.0 → 0.20.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/README.md +1 -1
- package/dist/bin/oracle-cli.js +76 -76
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/archiveConversation.js +31 -7
- package/dist/src/browser/actions/assistantResponse.js +43 -1
- package/dist/src/browser/actions/attachmentContext.js +239 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
- package/dist/src/browser/actions/attachmentEvidence.js +78 -0
- package/dist/src/browser/actions/attachmentProgress.js +41 -0
- package/dist/src/browser/actions/attachmentPrompt.js +130 -0
- package/dist/src/browser/actions/attachments.js +306 -355
- package/dist/src/browser/actions/deepResearch.js +170 -45
- package/dist/src/browser/actions/modelSelection.js +91 -4
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +364 -186
- package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
- package/dist/src/browser/actions/thinkingStatus.js +2 -5
- package/dist/src/browser/actions/thinkingTime.js +156 -16
- package/dist/src/browser/actions/webSearch.js +96 -0
- package/dist/src/browser/attachRunning.js +46 -2
- package/dist/src/browser/attachmentValidation.js +53 -0
- package/dist/src/browser/cancellation.js +105 -0
- package/dist/src/browser/chatgptImages.js +2 -0
- package/dist/src/browser/chatgptThrottle.js +104 -0
- package/dist/src/browser/chromeLifecycle.js +272 -47
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +525 -274
- package/dist/src/browser/liveTabs.js +3 -4
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +83 -15
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
- package/dist/src/browser/reattach.js +31 -4
- package/dist/src/browser/recoveryTarget.js +131 -0
- package/dist/src/browser/sessionRunner.js +73 -11
- package/dist/src/browser/tabLeaseRegistry.js +434 -38
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +36 -4
- package/dist/src/cli/browserDefaults.js +10 -0
- package/dist/src/cli/browserTabs.js +15 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -48
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +13 -7
- package/dist/src/cli/sessionRunner.js +95 -12
- package/dist/src/config.js +1 -0
- package/dist/src/gemini-web/client.js +67 -21
- package/dist/src/gemini-web/executor.js +4 -0
- package/dist/src/gemini-web/http.js +15 -0
- package/dist/src/gemini-web/models.js +15 -11
- package/dist/src/mcp/server.js +11 -14
- package/dist/src/mcp/tools/chatgptImage.js +4 -4
- package/dist/src/mcp/tools/consult.js +86 -24
- package/dist/src/mcp/tools/projectSources.js +4 -6
- package/dist/src/mcp/tools/sessionResources.js +1 -1
- package/dist/src/mcp/tools/sessions.js +2 -2
- package/dist/src/mcp/tools/wait.js +182 -0
- package/dist/src/mcp/types.js +8 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +73 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +346 -71
- package/dist/src/sessionManager.js +36 -1
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +25 -23
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -683
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -400
- package/dist/docs-site/browser-mode.html +0 -588
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -436
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -368
- package/dist/docs-site/gemini.html +0 -379
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -595
- package/dist/docs-site/mcp.html +0 -368
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -387
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/profileSync.js +0 -141
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { BrowserRunCancelledError } from "../oracle/errors.js";
|
|
3
|
+
const context = new AsyncLocalStorage();
|
|
4
|
+
const cleanupContext = new AsyncLocalStorage();
|
|
5
|
+
export const currentBrowserAbortSignal = () => context.getStore();
|
|
6
|
+
export const withoutBrowserCancellation = (task) => cleanupContext.run(true, () => context.run(undefined, task));
|
|
7
|
+
/** Per-run cancellation, including polling and resources that arrive after their caller left. */
|
|
8
|
+
export class BrowserCancellation {
|
|
9
|
+
signal;
|
|
10
|
+
log;
|
|
11
|
+
aborted;
|
|
12
|
+
onAbort;
|
|
13
|
+
proxies = new WeakMap();
|
|
14
|
+
constructor(signal, log = () => { }) {
|
|
15
|
+
this.signal = signal;
|
|
16
|
+
this.log = log;
|
|
17
|
+
if (signal) {
|
|
18
|
+
let rejectAbort;
|
|
19
|
+
this.aborted = new Promise((_, reject) => {
|
|
20
|
+
rejectAbort = reject;
|
|
21
|
+
});
|
|
22
|
+
this.aborted.catch(() => undefined);
|
|
23
|
+
this.onAbort = () => rejectAbort(new BrowserRunCancelledError());
|
|
24
|
+
if (signal.aborted)
|
|
25
|
+
this.onAbort();
|
|
26
|
+
else
|
|
27
|
+
signal.addEventListener("abort", this.onAbort, { once: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
check() {
|
|
31
|
+
if (this.signal?.aborted)
|
|
32
|
+
throw new BrowserRunCancelledError();
|
|
33
|
+
}
|
|
34
|
+
run(task) {
|
|
35
|
+
return cleanupContext.run(false, () => context.run(this.signal, task));
|
|
36
|
+
}
|
|
37
|
+
dispose() {
|
|
38
|
+
if (this.onAbort)
|
|
39
|
+
this.signal?.removeEventListener("abort", this.onAbort);
|
|
40
|
+
}
|
|
41
|
+
race(pending) {
|
|
42
|
+
return this.aborted ? Promise.race([pending, this.aborted]) : pending;
|
|
43
|
+
}
|
|
44
|
+
call(create) {
|
|
45
|
+
this.check();
|
|
46
|
+
return this.race(create());
|
|
47
|
+
}
|
|
48
|
+
acquire(create, releaseLate) {
|
|
49
|
+
this.check();
|
|
50
|
+
const pending = create().then(async (resource) => {
|
|
51
|
+
if (this.signal?.aborted) {
|
|
52
|
+
await withoutBrowserCancellation(() => releaseLate(resource)).catch((error) => {
|
|
53
|
+
this.log(`[browser] Late cancelled resource cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
});
|
|
55
|
+
throw new BrowserRunCancelledError();
|
|
56
|
+
}
|
|
57
|
+
return resource;
|
|
58
|
+
});
|
|
59
|
+
return this.race(pending);
|
|
60
|
+
}
|
|
61
|
+
client(client) {
|
|
62
|
+
if (!this.signal)
|
|
63
|
+
return client;
|
|
64
|
+
const wrap = (target) => {
|
|
65
|
+
const cached = this.proxies.get(target);
|
|
66
|
+
if (cached)
|
|
67
|
+
return cached;
|
|
68
|
+
const proxy = new Proxy(target, {
|
|
69
|
+
get: (object, key) => {
|
|
70
|
+
const value = Reflect.get(object, key);
|
|
71
|
+
if (typeof value === "function") {
|
|
72
|
+
if ([
|
|
73
|
+
"on",
|
|
74
|
+
"once",
|
|
75
|
+
"off",
|
|
76
|
+
"emit",
|
|
77
|
+
"removeListener",
|
|
78
|
+
"removeAllListeners",
|
|
79
|
+
"addListener",
|
|
80
|
+
"close",
|
|
81
|
+
].includes(String(key)))
|
|
82
|
+
return value.bind(object);
|
|
83
|
+
return (...args) => {
|
|
84
|
+
// CDP event subscriptions are synchronous and must remain removable during cleanup.
|
|
85
|
+
if (typeof args[0] === "function")
|
|
86
|
+
return Reflect.apply(value, object, args);
|
|
87
|
+
if (cleanupContext.getStore())
|
|
88
|
+
return Reflect.apply(value, object, args);
|
|
89
|
+
if (this.signal?.aborted)
|
|
90
|
+
return Promise.reject(new BrowserRunCancelledError());
|
|
91
|
+
const result = Reflect.apply(value, object, args);
|
|
92
|
+
return result && typeof result.then === "function"
|
|
93
|
+
? this.race(Promise.resolve(result))
|
|
94
|
+
: result;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return value && typeof value === "object" ? wrap(value) : value;
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
this.proxies.set(target, proxy);
|
|
101
|
+
return proxy;
|
|
102
|
+
};
|
|
103
|
+
return wrap(client);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -5,6 +5,7 @@ import { ASSISTANT_ROLE_SELECTOR } from "./constants.js";
|
|
|
5
5
|
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
6
6
|
import { delay } from "./utils.js";
|
|
7
7
|
import { readAssistantSnapshot } from "./pageActions.js";
|
|
8
|
+
import { throwIfAssistantUiError } from "./actions/assistantResponse.js";
|
|
8
9
|
import { getOracleHomeDir } from "../oracleHome.js";
|
|
9
10
|
import { resolveSessionArtifactsDir } from "./artifacts.js";
|
|
10
11
|
import { saveAssistantDownloadButtonArtifacts } from "./chatgptFiles.js";
|
|
@@ -456,6 +457,7 @@ export async function collectGeneratedImageArtifacts(params) {
|
|
|
456
457
|
break;
|
|
457
458
|
}
|
|
458
459
|
const latestSnapshot = await readAssistantSnapshot(params.Runtime, params.minTurnIndex ?? undefined).catch(() => null);
|
|
460
|
+
throwIfAssistantUiError(latestSnapshot);
|
|
459
461
|
const snapshotText = typeof latestSnapshot?.text === "string" ? latestSnapshot.text.trim() : "";
|
|
460
462
|
if (snapshotText) {
|
|
461
463
|
latestAnswerText = snapshotText;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detection for ChatGPT's own rate-limit notice.
|
|
4
|
+
*
|
|
5
|
+
* When ChatGPT decides a session is asking for too much at once it puts a modal
|
|
6
|
+
* over the page — "Too many requests … We've temporarily limited access to your
|
|
7
|
+
* conversations … Please wait a few minutes" — with a single "Got it" button.
|
|
8
|
+
*
|
|
9
|
+
* The modal is not marked as an error anywhere a DOM scrape would notice. It is
|
|
10
|
+
* a dialog full of ordinary text, so a menu reader walking the page finds its
|
|
11
|
+
* button and reports it as an available option. That is how a throttle came to be
|
|
12
|
+
* reported as `Unable to find model option matching "Thinking 5.5". Available:
|
|
13
|
+
* Got it` — a message that sends the reader looking for a model-naming bug that
|
|
14
|
+
* does not exist, when the correct response is to wait a few minutes.
|
|
15
|
+
*
|
|
16
|
+
* Observed live: six ChatGPT conversations opened at once triggered it
|
|
17
|
+
* repeatedly, while five did not, and the limit cleared on its own after a
|
|
18
|
+
* pause. Runs must therefore treat this as a transient, recoverable state rather
|
|
19
|
+
* than a failure of the request they were making.
|
|
20
|
+
*/
|
|
21
|
+
const THROTTLE_PHRASES = [
|
|
22
|
+
"too many requests",
|
|
23
|
+
"making requests too quickly",
|
|
24
|
+
"temporarily limited access to your conversations",
|
|
25
|
+
];
|
|
26
|
+
/** True when the text carries ChatGPT's rate-limit wording. */
|
|
27
|
+
export function matchesThrottleNotice(text) {
|
|
28
|
+
if (!text) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const normalized = text.toLowerCase();
|
|
32
|
+
return THROTTLE_PHRASES.some((phrase) => normalized.includes(phrase));
|
|
33
|
+
}
|
|
34
|
+
export function buildThrottleProbeExpression() {
|
|
35
|
+
return `(() => {
|
|
36
|
+
const PHRASES = ${JSON.stringify(THROTTLE_PHRASES)};
|
|
37
|
+
const matches = (value) => {
|
|
38
|
+
const text = (value || '').toLowerCase();
|
|
39
|
+
return PHRASES.some((phrase) => text.includes(phrase));
|
|
40
|
+
};
|
|
41
|
+
// Dialogs first, because that is what this notice is; the body sweep is a
|
|
42
|
+
// fallback for a variant that does not carry the role.
|
|
43
|
+
const dialogs = Array.from(document.querySelectorAll('[role="dialog"], [role="alertdialog"]'));
|
|
44
|
+
for (const dialog of dialogs) {
|
|
45
|
+
const text = (dialog.innerText || '').trim();
|
|
46
|
+
if (matches(text)) return { message: text.slice(0, 400) };
|
|
47
|
+
}
|
|
48
|
+
const body = (document.body && document.body.innerText) || '';
|
|
49
|
+
if (matches(body)) {
|
|
50
|
+
const line = body
|
|
51
|
+
.split('\\n')
|
|
52
|
+
.map((entry) => entry.trim())
|
|
53
|
+
.find((entry) => matches(entry));
|
|
54
|
+
return { message: (line || 'ChatGPT is rate limiting this session.').slice(0, 400) };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
})()`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Looks for the rate-limit notice. Never throws: this runs on paths that are
|
|
61
|
+
* already reporting a different failure, and a probe that failed must not
|
|
62
|
+
* replace the original diagnosis with its own.
|
|
63
|
+
*/
|
|
64
|
+
export async function detectChatGptThrottleNotice(Runtime) {
|
|
65
|
+
try {
|
|
66
|
+
const evaluated = await Runtime.evaluate({
|
|
67
|
+
expression: buildThrottleProbeExpression(),
|
|
68
|
+
returnByValue: true,
|
|
69
|
+
});
|
|
70
|
+
const value = evaluated.result?.value;
|
|
71
|
+
if (value && typeof value.message === "string" && value.message.trim()) {
|
|
72
|
+
return { message: value.message.trim() };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// fall through: no notice detected
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
export function buildThrottleError(notice, context) {
|
|
81
|
+
return new BrowserAutomationError(`ChatGPT is rate limiting this session: ${notice.message} ` +
|
|
82
|
+
"This is a temporary limit on the account, not a problem with the request — " +
|
|
83
|
+
"wait a few minutes and retry, and reduce how many conversations are started at once.", {
|
|
84
|
+
stage: "chatgpt-throttled",
|
|
85
|
+
details: {
|
|
86
|
+
// Named so a caller can branch on it rather than parsing the message.
|
|
87
|
+
retryable: true,
|
|
88
|
+
origin: context.stage,
|
|
89
|
+
notice: notice.message,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Replaces a misleading failure with the real one when the page is throttled.
|
|
95
|
+
* Returns nothing when it is not, leaving the caller's own error intact.
|
|
96
|
+
*/
|
|
97
|
+
export async function throwIfThrottled(Runtime, context, logger) {
|
|
98
|
+
const notice = await detectChatGptThrottleNotice(Runtime);
|
|
99
|
+
if (!notice) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
logger?.(`[browser] ChatGPT rate limit detected during ${context.stage}: ${notice.message}`);
|
|
103
|
+
throw buildThrottleError(notice, context);
|
|
104
|
+
}
|
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import * as childProcess from "node:child_process";
|
|
2
3
|
import net from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
3
5
|
import CDP from "chrome-remote-interface";
|
|
4
|
-
import { launch, Launcher } from "chrome-launcher";
|
|
6
|
+
import { launch, Launcher, } from "chrome-launcher";
|
|
5
7
|
import { cleanupStaleProfileState } from "./profileState.js";
|
|
6
8
|
import { delay } from "./utils.js";
|
|
7
9
|
import { isWsl, resolveWslChromeLaunchRoute } from "./wslHost.js";
|
|
8
10
|
export async function launchChrome(config, userDataDir, logger) {
|
|
9
11
|
const { connectHost, debugBindAddress, usePatchedLauncher } = resolveWslChromeLaunchRoute();
|
|
10
12
|
const debugPort = config.debugPort ?? parseDebugPortEnv();
|
|
13
|
+
const usingCopiedProfile = Boolean(config.copyProfileSource);
|
|
14
|
+
const detachSharedChrome = shouldDetachSharedChrome(config);
|
|
15
|
+
const launchedProfileDirectory = usingCopiedProfile && config.chromeProfile ? config.chromeProfile : "Default";
|
|
16
|
+
await prepareChromeWindowStateForHiddenLaunch({
|
|
17
|
+
config,
|
|
18
|
+
userDataDir,
|
|
19
|
+
profileDirectory: launchedProfileDirectory,
|
|
20
|
+
logger,
|
|
21
|
+
});
|
|
11
22
|
const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress, config.hideWindow ?? false);
|
|
12
23
|
// copy-profile reuses a copied signed-in profile whose cookies are
|
|
13
24
|
// Keychain-encrypted, so it must launch with the real Keychain (not mocked):
|
|
14
25
|
// strip the keychain-mocking flags from both chrome-launcher's defaults and
|
|
15
26
|
// Oracle's set, and ignore the defaults so they aren't re-added.
|
|
16
|
-
const usingCopiedProfile = Boolean(config.copyProfileSource);
|
|
17
27
|
if (usingCopiedProfile && config.chromeProfile) {
|
|
18
28
|
chromeFlags.push(`--profile-directory=${config.chromeProfile}`);
|
|
19
29
|
}
|
|
@@ -26,38 +36,231 @@ export async function launchChrome(config, userDataDir, logger) {
|
|
|
26
36
|
host: connectHost ?? "127.0.0.1",
|
|
27
37
|
requestedPort: debugPort ?? undefined,
|
|
28
38
|
ignoreDefaultFlags: launchOptions.ignoreDefaultFlags,
|
|
39
|
+
detachSharedChrome,
|
|
29
40
|
})
|
|
30
|
-
: await
|
|
41
|
+
: await launchWithStableProcessLifecycle({
|
|
31
42
|
chromePath: config.chromePath ?? undefined,
|
|
32
43
|
chromeFlags: launchOptions.chromeFlags,
|
|
33
44
|
userDataDir,
|
|
34
45
|
handleSIGINT: false,
|
|
35
46
|
port: debugPort ?? undefined,
|
|
36
47
|
ignoreDefaultFlags: launchOptions.ignoreDefaultFlags,
|
|
37
|
-
});
|
|
48
|
+
}, detachSharedChrome);
|
|
38
49
|
const pidLabel = typeof launcher.pid === "number" ? ` (pid ${launcher.pid})` : "";
|
|
39
50
|
const hostLabel = connectHost ? ` on ${connectHost}` : "";
|
|
40
51
|
logger(`Launched Chrome${pidLabel} on port ${launcher.port}${hostLabel}`);
|
|
52
|
+
if (detachSharedChrome) {
|
|
53
|
+
logger("[browser] Browser control: Windows Chrome lifecycle detached=true; windowsHide=true.");
|
|
54
|
+
}
|
|
41
55
|
return Object.assign(launcher, { host: connectHost ?? "127.0.0.1" });
|
|
42
56
|
}
|
|
43
|
-
|
|
57
|
+
function shouldDetachSharedChrome(config, platform = process.platform) {
|
|
58
|
+
return platform === "win32" && config.manualLogin === true && !config.copyProfileSource;
|
|
59
|
+
}
|
|
60
|
+
export const shouldDetachSharedChromeForTest = shouldDetachSharedChrome;
|
|
61
|
+
const spawnDetachedChromeOnWindows = ((command, args, options) => {
|
|
62
|
+
const child = childProcess.spawn(command, args, resolveChromeChildSpawnOptions(options, "win32"));
|
|
63
|
+
child.unref();
|
|
64
|
+
return child;
|
|
65
|
+
});
|
|
66
|
+
function resolveChromeChildSpawnOptions(options, platform = process.platform) {
|
|
67
|
+
return platform === "win32"
|
|
68
|
+
? {
|
|
69
|
+
...options,
|
|
70
|
+
detached: true,
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
}
|
|
73
|
+
: options;
|
|
74
|
+
}
|
|
75
|
+
export function resolveChromeChildSpawnOptionsForTest(options, platform) {
|
|
76
|
+
return resolveChromeChildSpawnOptions(options, platform);
|
|
77
|
+
}
|
|
78
|
+
function chromeLauncherModuleOverrides(detachSharedChrome, platform = process.platform) {
|
|
79
|
+
return detachSharedChrome && platform === "win32"
|
|
80
|
+
? { spawn: spawnDetachedChromeOnWindows }
|
|
81
|
+
: undefined;
|
|
82
|
+
}
|
|
83
|
+
async function launchWithStableProcessLifecycle(options, detachSharedChrome) {
|
|
84
|
+
if (!detachSharedChrome) {
|
|
85
|
+
return launch(options);
|
|
86
|
+
}
|
|
87
|
+
const launcher = new Launcher(options, chromeLauncherModuleOverrides(detachSharedChrome));
|
|
88
|
+
await launcher.launch();
|
|
89
|
+
return launchedChromeFromLauncher(launcher);
|
|
90
|
+
}
|
|
91
|
+
function launchedChromeFromLauncher(launcher) {
|
|
92
|
+
return {
|
|
93
|
+
pid: launcher.pid ?? 0,
|
|
94
|
+
port: launcher.port ?? 0,
|
|
95
|
+
process: launcher.chromeProcess,
|
|
96
|
+
kill: () => launcher.kill(),
|
|
97
|
+
remoteDebuggingPipes: launcher.remoteDebuggingPipes,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export async function positionChromeWindowOffscreen(client, userDataDir, logger) {
|
|
44
101
|
if (process.platform !== "darwin") {
|
|
45
102
|
logger("Window hiding is only supported on macOS");
|
|
46
103
|
return;
|
|
47
104
|
}
|
|
105
|
+
let savedState = false;
|
|
48
106
|
try {
|
|
49
107
|
const { windowId } = await client.Browser.getWindowForTarget();
|
|
108
|
+
if (!(await readSavedChromeWindowState(userDataDir))) {
|
|
109
|
+
const { bounds } = await client.Browser.getWindowBounds({ windowId });
|
|
110
|
+
await writeSavedChromeWindowState(userDataDir, bounds);
|
|
111
|
+
savedState = true;
|
|
112
|
+
}
|
|
50
113
|
await client.Browser.setWindowBounds({
|
|
51
114
|
windowId,
|
|
52
115
|
bounds: { left: -32_000, top: -32_000, windowState: "normal" },
|
|
53
116
|
});
|
|
54
|
-
logger("Chrome window positioned off-screen");
|
|
55
117
|
}
|
|
56
118
|
catch (error) {
|
|
119
|
+
if (savedState) {
|
|
120
|
+
await rm(chromeWindowStatePath(userDataDir), { force: true }).catch(() => undefined);
|
|
121
|
+
}
|
|
57
122
|
const message = error instanceof Error ? error.message : String(error);
|
|
58
123
|
logger(`Failed to position Chrome window off-screen: ${message}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
logger("Chrome window positioned off-screen");
|
|
127
|
+
}
|
|
128
|
+
export async function positionChromeWindowOnscreen(client, userDataDir, logger) {
|
|
129
|
+
if (process.platform !== "darwin") {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const savedState = await readSavedChromeWindowState(userDataDir);
|
|
134
|
+
if (!savedState) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const { windowId } = await client.Browser.getWindowForTarget();
|
|
138
|
+
await client.Browser.setWindowBounds({
|
|
139
|
+
windowId,
|
|
140
|
+
bounds: restoreWindowBounds(savedState.bounds),
|
|
141
|
+
});
|
|
142
|
+
await rm(chromeWindowStatePath(userDataDir), { force: true });
|
|
143
|
+
logger("Chrome window restored to its pre-hide bounds");
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
147
|
+
logger(`Failed to position Chrome window on-screen: ${message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const CHROME_WINDOW_STATE_FILENAME = "oracle-window-state.json";
|
|
151
|
+
const DEFAULT_VISIBLE_WINDOW_BOUNDS = {
|
|
152
|
+
left: 80,
|
|
153
|
+
top: 80,
|
|
154
|
+
width: 1280,
|
|
155
|
+
height: 720,
|
|
156
|
+
windowState: "normal",
|
|
157
|
+
};
|
|
158
|
+
function chromeWindowStatePath(userDataDir) {
|
|
159
|
+
return path.join(userDataDir, CHROME_WINDOW_STATE_FILENAME);
|
|
160
|
+
}
|
|
161
|
+
async function readSavedChromeWindowState(userDataDir) {
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(await readFile(chromeWindowStatePath(userDataDir), "utf8"));
|
|
164
|
+
const bounds = parseChromeWindowBounds(parsed.bounds);
|
|
165
|
+
if (parsed.version !== 1 || !bounds) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return { version: 1, bounds };
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return null;
|
|
59
172
|
}
|
|
60
173
|
}
|
|
174
|
+
async function writeSavedChromeWindowState(userDataDir, bounds) {
|
|
175
|
+
await mkdir(userDataDir, { recursive: true });
|
|
176
|
+
await writeFile(chromeWindowStatePath(userDataDir), `${JSON.stringify({ version: 1, bounds })}\n`, "utf8");
|
|
177
|
+
}
|
|
178
|
+
async function prepareChromeWindowStateForHiddenLaunch({ config, userDataDir, profileDirectory, logger, }) {
|
|
179
|
+
if (process.platform !== "darwin" ||
|
|
180
|
+
config.headless ||
|
|
181
|
+
!config.hideWindow ||
|
|
182
|
+
(await readSavedChromeWindowState(userDataDir))) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const bounds = (await readPersistedChromeWindowBounds(userDataDir, profileDirectory)) ??
|
|
186
|
+
DEFAULT_VISIBLE_WINDOW_BOUNDS;
|
|
187
|
+
await writeSavedChromeWindowState(userDataDir, bounds);
|
|
188
|
+
logger("Recorded Chrome window placement before hidden launch");
|
|
189
|
+
}
|
|
190
|
+
async function readPersistedChromeWindowBounds(userDataDir, profileDirectory) {
|
|
191
|
+
const root = path.resolve(userDataDir);
|
|
192
|
+
const profile = path.resolve(root, profileDirectory);
|
|
193
|
+
if (path.dirname(profile) !== root) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const preferences = JSON.parse(await readFile(path.join(profile, "Preferences"), "utf8"));
|
|
198
|
+
return persistedPlacementToBounds(preferences.browser?.window_placement);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function persistedPlacementToBounds(placement) {
|
|
205
|
+
if (!placement) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (placement.maximized === true) {
|
|
209
|
+
return { windowState: "maximized" };
|
|
210
|
+
}
|
|
211
|
+
const left = finiteNumber(placement.left);
|
|
212
|
+
const top = finiteNumber(placement.top);
|
|
213
|
+
const right = finiteNumber(placement.right);
|
|
214
|
+
const bottom = finiteNumber(placement.bottom);
|
|
215
|
+
if (left === null || top === null || right === null || bottom === null) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const width = right - left;
|
|
219
|
+
const height = bottom - top;
|
|
220
|
+
if (width <= 0 || height <= 0) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return { left, top, width, height, windowState: "normal" };
|
|
224
|
+
}
|
|
225
|
+
function parseChromeWindowBounds(value) {
|
|
226
|
+
if (!value || typeof value !== "object") {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
const bounds = value;
|
|
230
|
+
const windowState = bounds.windowState ?? "normal";
|
|
231
|
+
if (windowState !== "normal") {
|
|
232
|
+
return ["minimized", "maximized", "fullscreen"].includes(windowState) ? { windowState } : null;
|
|
233
|
+
}
|
|
234
|
+
const left = finiteNumber(bounds.left);
|
|
235
|
+
const top = finiteNumber(bounds.top);
|
|
236
|
+
const width = finiteNumber(bounds.width);
|
|
237
|
+
const height = finiteNumber(bounds.height);
|
|
238
|
+
if (left === null ||
|
|
239
|
+
top === null ||
|
|
240
|
+
width === null ||
|
|
241
|
+
height === null ||
|
|
242
|
+
width <= 0 ||
|
|
243
|
+
height <= 0) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return { left, top, width, height, windowState: "normal" };
|
|
247
|
+
}
|
|
248
|
+
function finiteNumber(value) {
|
|
249
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
250
|
+
}
|
|
251
|
+
function restoreWindowBounds(bounds) {
|
|
252
|
+
const windowState = bounds.windowState ?? "normal";
|
|
253
|
+
if (windowState !== "normal") {
|
|
254
|
+
return { windowState };
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
left: bounds.left ?? 80,
|
|
258
|
+
top: bounds.top ?? 80,
|
|
259
|
+
width: bounds.width,
|
|
260
|
+
height: bounds.height,
|
|
261
|
+
windowState: "normal",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
61
264
|
export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logger, opts) {
|
|
62
265
|
const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
|
|
63
266
|
let handling;
|
|
@@ -68,7 +271,8 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
|
|
|
68
271
|
handling = true;
|
|
69
272
|
const inFlight = opts?.isInFlight?.() ?? false;
|
|
70
273
|
const forceCleanup = opts?.forceProfileCleanup ?? false;
|
|
71
|
-
const
|
|
274
|
+
const preserveSharedChrome = opts?.preserveSharedChromeOnSignal ?? false;
|
|
275
|
+
const leaveRunning = (keepBrowser || inFlight || preserveSharedChrome) && !forceCleanup;
|
|
72
276
|
if (leaveRunning) {
|
|
73
277
|
logger(`Received ${signal}; leaving Chrome running${inFlight ? " (assistant response pending)" : ""}`);
|
|
74
278
|
}
|
|
@@ -136,23 +340,28 @@ export async function connectToRemoteChrome(host, port, logger, targetUrl, brows
|
|
|
136
340
|
approvalWaitMs: options?.approvalWaitMs,
|
|
137
341
|
});
|
|
138
342
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
343
|
+
const newTargetUrl = targetUrl || (options?.fallbackToDefault === false ? "about:blank" : undefined);
|
|
344
|
+
if (newTargetUrl) {
|
|
345
|
+
const targetConnection = await connectToNewTarget(host, port, newTargetUrl, logger, {
|
|
346
|
+
opened: () => `Opened dedicated remote Chrome tab targeting ${newTargetUrl}`,
|
|
347
|
+
openFailed: (message) => `Failed to open dedicated remote Chrome tab (${message}); ${options?.fallbackToDefault === false ? "refusing to reuse an unrelated tab" : "falling back to first target"}.`,
|
|
348
|
+
attachFailed: (targetId, message) => `Failed to attach to dedicated remote Chrome tab ${targetId} (${message}); ${options?.fallbackToDefault === false ? "refusing to reuse an unrelated tab" : "falling back to first target"}.`,
|
|
144
349
|
closeFailed: (targetId, message) => `Failed to close unused remote Chrome tab ${targetId}: ${message}`,
|
|
145
350
|
});
|
|
146
351
|
if (targetConnection) {
|
|
147
352
|
return {
|
|
148
353
|
client: targetConnection.client,
|
|
149
354
|
targetId: targetConnection.targetId,
|
|
150
|
-
close: async () => {
|
|
355
|
+
close: async (closeOptions) => {
|
|
151
356
|
await targetConnection.client.close().catch(() => undefined);
|
|
152
|
-
|
|
357
|
+
if (!closeOptions?.preserveTarget)
|
|
358
|
+
await closeRemoteChromeTarget(host, port, targetConnection.targetId, logger);
|
|
153
359
|
},
|
|
154
360
|
};
|
|
155
361
|
}
|
|
362
|
+
if (options?.fallbackToDefault === false) {
|
|
363
|
+
throw new Error("Unable to create a dedicated remote Chrome tab; refusing to reuse an unrelated conversation.");
|
|
364
|
+
}
|
|
156
365
|
}
|
|
157
366
|
const fallbackClient = await CDP({ host, port });
|
|
158
367
|
logger(`Connected to remote Chrome DevTools protocol at ${host}:${port}`);
|
|
@@ -183,7 +392,7 @@ export async function listRemoteChromeTargets(options) {
|
|
|
183
392
|
const targets = await CDP.List({ host: options.host, port: options.port });
|
|
184
393
|
return targets;
|
|
185
394
|
}
|
|
186
|
-
const browser = await
|
|
395
|
+
const browser = await connectToBrowserWebSocket(options.host, options.port, options.browserWSEndpoint, options.logger ?? (() => { }), options.approvalWaitMs);
|
|
187
396
|
try {
|
|
188
397
|
const result = await browser.Target.getTargets();
|
|
189
398
|
return (result.targetInfos ?? []).map((target) => ({
|
|
@@ -223,9 +432,9 @@ export async function connectToRemoteChromeTarget(host, port, logger, options) {
|
|
|
223
432
|
client,
|
|
224
433
|
targetId,
|
|
225
434
|
browserWSEndpoint: options.browserWSEndpoint,
|
|
226
|
-
close: async () => {
|
|
435
|
+
close: async (closeOptions) => {
|
|
227
436
|
await browser.Target.detachFromTarget({ sessionId: attached.sessionId }).catch(() => undefined);
|
|
228
|
-
if (options.closeTargetOnDispose && targetId) {
|
|
437
|
+
if (options.closeTargetOnDispose && targetId && !closeOptions?.preserveTarget) {
|
|
229
438
|
await browser.Target.closeTarget({ targetId }).catch(() => undefined);
|
|
230
439
|
}
|
|
231
440
|
await browser.close().catch(() => undefined);
|
|
@@ -241,33 +450,54 @@ async function connectToBrowserWebSocket(host, port, browserWSEndpoint, logger,
|
|
|
241
450
|
if (!approvalWaitMs || approvalWaitMs <= 0) {
|
|
242
451
|
return (await CDP({ target: browserWSEndpoint, local: true }));
|
|
243
452
|
}
|
|
244
|
-
logger(`Waiting for Chrome remote debugging approval for ${host}:${port}...`);
|
|
245
|
-
const
|
|
453
|
+
logger(`[browser] Waiting for Chrome remote debugging approval for ${host}:${port}...`);
|
|
454
|
+
const startedAt = Date.now();
|
|
455
|
+
const deadline = startedAt + approvalWaitMs;
|
|
456
|
+
const progress = setInterval(() => {
|
|
457
|
+
logger(`[browser] Still waiting for Chrome remote debugging approval for ${host}:${port} (${formatApprovalWait(Date.now() - startedAt)} elapsed). Click Allow in an open Chrome window.`);
|
|
458
|
+
}, 15_000);
|
|
246
459
|
let lastApprovalError;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
460
|
+
try {
|
|
461
|
+
while (Date.now() < deadline) {
|
|
462
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
463
|
+
let timeout;
|
|
464
|
+
let expired = false;
|
|
465
|
+
try {
|
|
466
|
+
const connecting = CDP({ target: browserWSEndpoint, local: true }).then(async (client) => {
|
|
467
|
+
// An approval arriving after our deadline must not leak a connection.
|
|
468
|
+
if (expired)
|
|
469
|
+
await client.close().catch(() => undefined);
|
|
470
|
+
return client;
|
|
471
|
+
});
|
|
472
|
+
return await Promise.race([
|
|
473
|
+
connecting,
|
|
474
|
+
new Promise((_, reject) => {
|
|
475
|
+
timeout = setTimeout(() => {
|
|
476
|
+
expired = true;
|
|
477
|
+
reject(new Error("__oracle_remote_debugging_approval_timeout__"));
|
|
478
|
+
}, remainingMs);
|
|
479
|
+
}),
|
|
480
|
+
]);
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
if (error instanceof Error &&
|
|
484
|
+
error.message === "__oracle_remote_debugging_approval_timeout__") {
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
if (!isRemoteDebuggingApprovalError(error)) {
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
lastApprovalError = error;
|
|
263
491
|
}
|
|
264
|
-
|
|
265
|
-
|
|
492
|
+
finally {
|
|
493
|
+
clearTimeout(timeout);
|
|
266
494
|
}
|
|
267
|
-
lastApprovalError = error;
|
|
268
495
|
await delay(Math.min(500, Math.max(0, deadline - Date.now())));
|
|
269
496
|
}
|
|
270
497
|
}
|
|
498
|
+
finally {
|
|
499
|
+
clearInterval(progress);
|
|
500
|
+
}
|
|
271
501
|
const suffix = lastApprovalError instanceof Error && lastApprovalError.message
|
|
272
502
|
? ` Last Chrome response: ${lastApprovalError.message}`
|
|
273
503
|
: "";
|
|
@@ -598,7 +828,7 @@ function parseDebugPortEnv() {
|
|
|
598
828
|
}
|
|
599
829
|
return value;
|
|
600
830
|
}
|
|
601
|
-
async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, ignoreDefaultFlags, }) {
|
|
831
|
+
async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, ignoreDefaultFlags, detachSharedChrome, }) {
|
|
602
832
|
const launcher = new Launcher({
|
|
603
833
|
chromePath: chromePath ?? undefined,
|
|
604
834
|
chromeFlags,
|
|
@@ -606,7 +836,7 @@ async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host
|
|
|
606
836
|
handleSIGINT: false,
|
|
607
837
|
port: requestedPort ?? undefined,
|
|
608
838
|
ignoreDefaultFlags,
|
|
609
|
-
});
|
|
839
|
+
}, chromeLauncherModuleOverrides(detachSharedChrome));
|
|
610
840
|
if (host) {
|
|
611
841
|
const patched = launcher;
|
|
612
842
|
patched.isDebuggerReady = function patchedIsDebuggerReady() {
|
|
@@ -634,13 +864,8 @@ async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host
|
|
|
634
864
|
};
|
|
635
865
|
}
|
|
636
866
|
await launcher.launch();
|
|
637
|
-
const kill = async () => launcher.kill();
|
|
638
867
|
return {
|
|
639
|
-
|
|
640
|
-
port: launcher.port ?? 0,
|
|
641
|
-
process: launcher.chromeProcess,
|
|
642
|
-
kill,
|
|
868
|
+
...launchedChromeFromLauncher(launcher),
|
|
643
869
|
host: host ?? undefined,
|
|
644
|
-
remoteDebuggingPipes: launcher.remoteDebuggingPipes,
|
|
645
870
|
};
|
|
646
871
|
}
|