@steipete/oracle 0.9.0 → 0.11.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 +1 -1
- package/README.md +107 -49
- package/dist/bin/oracle-cli.js +551 -410
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/archiveConversation.js +224 -0
- package/dist/src/browser/actions/assistantResponse.js +175 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +342 -119
- package/dist/src/browser/actions/navigation.js +183 -137
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +152 -91
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +276 -63
- package/dist/src/browser/config.js +59 -16
- package/dist/src/browser/constants.js +25 -12
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +250 -77
- package/dist/src/browser/domDebug.js +50 -1
- package/dist/src/browser/index.js +1559 -692
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +127 -42
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +178 -73
- package/dist/src/browser/reattachHelpers.js +32 -27
- package/dist/src/browser/sessionRunner.js +89 -25
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +24 -20
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +102 -48
- package/dist/src/cli/browserDefaults.js +51 -26
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +62 -26
- package/dist/src/cli/duplicatePromptGuard.js +12 -4
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +131 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +82 -21
- package/dist/src/cli/sessionDisplay.js +213 -87
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +149 -95
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +18 -12
- package/dist/src/mcp/tools/consult.js +246 -67
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +12 -5
- package/dist/src/mcp/utils.js +21 -8
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +160 -135
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +78 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +67 -62
- package/vendor/oracle-notifier/README.md +2 -0
- 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/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/profileSync.js +0 -141
- /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
* - Verifies the DevTools /json/version endpoint responds.
|
|
6
6
|
* - Prints a WSL-friendly firewall hint if the port is unreachable.
|
|
7
7
|
*/
|
|
8
|
-
import { setTimeout as sleep } from
|
|
9
|
-
import { launch } from
|
|
10
|
-
import os from
|
|
11
|
-
import { readFileSync } from
|
|
8
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
9
|
+
import { launch } from "chrome-launcher";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
12
|
const DEFAULT_PORT = 45871;
|
|
13
|
-
const port = normalizePort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT) ??
|
|
13
|
+
const port = normalizePort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT) ??
|
|
14
|
+
DEFAULT_PORT;
|
|
14
15
|
const hostHint = resolveWslHost();
|
|
15
|
-
const targetHost = hostHint ??
|
|
16
|
+
const targetHost = hostHint ?? "127.0.0.1";
|
|
16
17
|
function normalizePort(raw) {
|
|
17
18
|
if (!raw)
|
|
18
19
|
return null;
|
|
@@ -22,18 +23,18 @@ function normalizePort(raw) {
|
|
|
22
23
|
return value;
|
|
23
24
|
}
|
|
24
25
|
function isWsl() {
|
|
25
|
-
if (process.platform !==
|
|
26
|
+
if (process.platform !== "linux")
|
|
26
27
|
return false;
|
|
27
28
|
if (process.env.WSL_DISTRO_NAME)
|
|
28
29
|
return true;
|
|
29
|
-
return os.release().toLowerCase().includes(
|
|
30
|
+
return os.release().toLowerCase().includes("microsoft");
|
|
30
31
|
}
|
|
31
32
|
function resolveWslHost() {
|
|
32
33
|
if (!isWsl())
|
|
33
34
|
return null;
|
|
34
35
|
try {
|
|
35
|
-
const resolv = readFileSync(
|
|
36
|
-
for (const line of resolv.split(
|
|
36
|
+
const resolv = readFileSync("/etc/resolv.conf", "utf8");
|
|
37
|
+
for (const line of resolv.split("\n")) {
|
|
37
38
|
const match = line.match(/^nameserver\s+([0-9.]+)/);
|
|
38
39
|
if (match?.[1])
|
|
39
40
|
return match[1];
|
|
@@ -49,19 +50,21 @@ function firewallHint(host, devtoolsPort) {
|
|
|
49
50
|
return null;
|
|
50
51
|
return [
|
|
51
52
|
`DevTools port ${host}:${devtoolsPort} is blocked from WSL.`,
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
"",
|
|
54
|
+
"PowerShell (admin):",
|
|
54
55
|
`New-NetFirewallRule -DisplayName 'Chrome DevTools ${devtoolsPort}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${devtoolsPort}`,
|
|
55
56
|
"New-NetFirewallRule -DisplayName 'Chrome DevTools (chrome.exe)' -Direction Inbound -Action Allow -Program 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' -Protocol TCP",
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
].join(
|
|
57
|
+
"",
|
|
58
|
+
"Re-run ./runner pnpm test:browser after adding the rule.",
|
|
59
|
+
].join("\n");
|
|
59
60
|
}
|
|
60
61
|
async function fetchVersion(host, devtoolsPort) {
|
|
61
62
|
const controller = new AbortController();
|
|
62
63
|
const timer = setTimeout(() => controller.abort(), 5000);
|
|
63
64
|
try {
|
|
64
|
-
const res = await fetch(`http://${host}:${devtoolsPort}/json/version`, {
|
|
65
|
+
const res = await fetch(`http://${host}:${devtoolsPort}/json/version`, {
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
});
|
|
65
68
|
if (!res.ok)
|
|
66
69
|
return false;
|
|
67
70
|
const json = (await res.json());
|
|
@@ -78,7 +81,7 @@ async function main() {
|
|
|
78
81
|
console.log(`[browser-test] launching Chrome on ${targetHost}:${port} (headful)…`);
|
|
79
82
|
const chrome = await launch({
|
|
80
83
|
port,
|
|
81
|
-
chromeFlags: [
|
|
84
|
+
chromeFlags: ["--remote-debugging-address=0.0.0.0"],
|
|
82
85
|
});
|
|
83
86
|
let ok = await fetchVersion(targetHost, chrome.port);
|
|
84
87
|
if (!ok) {
|
|
@@ -98,6 +101,6 @@ async function main() {
|
|
|
98
101
|
process.exit(1);
|
|
99
102
|
}
|
|
100
103
|
main().catch((error) => {
|
|
101
|
-
console.error(
|
|
104
|
+
console.error("[browser-test] Unexpected failure:", error instanceof Error ? error.message : String(error));
|
|
102
105
|
process.exit(1);
|
|
103
106
|
});
|
|
@@ -8,30 +8,30 @@
|
|
|
8
8
|
* Then run this script:
|
|
9
9
|
* npx tsx scripts/test-remote-chrome.ts <remote-host> [port]
|
|
10
10
|
*/
|
|
11
|
-
import CDP from
|
|
11
|
+
import CDP from "chrome-remote-interface";
|
|
12
12
|
async function main() {
|
|
13
|
-
const host = process.argv[2] ||
|
|
14
|
-
const port = parseInt(process.argv[3] ||
|
|
13
|
+
const host = process.argv[2] || "localhost";
|
|
14
|
+
const port = parseInt(process.argv[3] || "9222", 10);
|
|
15
15
|
console.log(`Attempting to connect to Chrome at ${host}:${port}...`);
|
|
16
16
|
try {
|
|
17
17
|
// Test connection
|
|
18
18
|
const client = await CDP({ host, port });
|
|
19
|
-
console.log(
|
|
19
|
+
console.log("✓ Connected to Chrome DevTools Protocol");
|
|
20
20
|
const { Network, Page, Runtime } = client;
|
|
21
21
|
// Enable domains
|
|
22
22
|
await Promise.all([Network.enable(), Page.enable()]);
|
|
23
|
-
console.log(
|
|
23
|
+
console.log("✓ Enabled Network and Page domains");
|
|
24
24
|
// Get browser version info
|
|
25
25
|
const version = await CDP.Version({ host, port });
|
|
26
26
|
console.log(`✓ Browser: ${version.Browser}`);
|
|
27
|
-
console.log(`✓ Protocol: ${version[
|
|
27
|
+
console.log(`✓ Protocol: ${version["Protocol-Version"]}`);
|
|
28
28
|
// Navigate to ChatGPT
|
|
29
|
-
console.log(
|
|
30
|
-
await Page.navigate({ url:
|
|
29
|
+
console.log("\nNavigating to ChatGPT...");
|
|
30
|
+
await Page.navigate({ url: "https://chatgpt.com/" });
|
|
31
31
|
await Page.loadEventFired();
|
|
32
|
-
console.log(
|
|
32
|
+
console.log("✓ Page loaded");
|
|
33
33
|
// Check current URL
|
|
34
|
-
const evalResult = await Runtime.evaluate({ expression:
|
|
34
|
+
const evalResult = await Runtime.evaluate({ expression: "window.location.href" });
|
|
35
35
|
console.log(`✓ Current URL: ${evalResult.result.value}`);
|
|
36
36
|
// Check if logged in (look for specific elements)
|
|
37
37
|
const checkLogin = await Runtime.evaluate({
|
|
@@ -49,19 +49,19 @@ async function main() {
|
|
|
49
49
|
});
|
|
50
50
|
console.log(`✓ Login status: ${JSON.stringify(checkLogin.result.value)}`);
|
|
51
51
|
await client.close();
|
|
52
|
-
console.log(
|
|
53
|
-
console.log(
|
|
54
|
-
console.log(
|
|
55
|
-
console.log(
|
|
56
|
-
console.log(
|
|
52
|
+
console.log("\n✓ POC successful! Remote Chrome connection works.");
|
|
53
|
+
console.log("\nTo use Oracle with remote Chrome, you would need to:");
|
|
54
|
+
console.log("1. Ensure cookies are loaded in remote Chrome");
|
|
55
|
+
console.log("2. Configure Oracle with --remote-chrome <host:port> to use this instance");
|
|
56
|
+
console.log("3. Ensure Oracle skips local Chrome launch when --remote-chrome is specified");
|
|
57
57
|
}
|
|
58
58
|
catch (error) {
|
|
59
|
-
console.error(
|
|
60
|
-
console.log(
|
|
61
|
-
console.log(
|
|
59
|
+
console.error("✗ Connection failed:", error instanceof Error ? error.message : error);
|
|
60
|
+
console.log("\nTroubleshooting:");
|
|
61
|
+
console.log("1. Ensure Chrome is running on remote machine with:");
|
|
62
62
|
console.log(` google-chrome --remote-debugging-port=${port} --remote-debugging-address=0.0.0.0`);
|
|
63
|
-
console.log(
|
|
64
|
-
console.log(
|
|
63
|
+
console.log("2. Check firewall allows connections to port", port);
|
|
64
|
+
console.log("3. Verify network connectivity to", host);
|
|
65
65
|
process.exit(1);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
export function normalizeHostPort(hostname, port) {
|
|
4
4
|
const trimmed = hostname.trim();
|
|
5
|
-
const unwrapped = trimmed.startsWith(
|
|
6
|
-
if (unwrapped.includes(
|
|
5
|
+
const unwrapped = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
6
|
+
if (unwrapped.includes(":")) {
|
|
7
7
|
return `[${unwrapped}]:${port}`;
|
|
8
8
|
}
|
|
9
9
|
return `${unwrapped}:${port}`;
|
|
@@ -11,7 +11,7 @@ export function normalizeHostPort(hostname, port) {
|
|
|
11
11
|
export function parseHostPort(raw) {
|
|
12
12
|
const target = raw.trim();
|
|
13
13
|
if (!target) {
|
|
14
|
-
throw new Error(
|
|
14
|
+
throw new Error("Expected host:port but received an empty value.");
|
|
15
15
|
}
|
|
16
16
|
const ipv6Match = target.match(/^\[(.+)]:(\d+)$/);
|
|
17
17
|
let hostname;
|
|
@@ -21,43 +21,43 @@ export function parseHostPort(raw) {
|
|
|
21
21
|
portSegment = ipv6Match[2]?.trim();
|
|
22
22
|
}
|
|
23
23
|
else {
|
|
24
|
-
const lastColon = target.lastIndexOf(
|
|
24
|
+
const lastColon = target.lastIndexOf(":");
|
|
25
25
|
if (lastColon === -1) {
|
|
26
26
|
throw new Error(`Invalid host:port format: ${target}. Expected host:port (IPv6 must use [host]:port notation).`);
|
|
27
27
|
}
|
|
28
28
|
hostname = target.slice(0, lastColon).trim();
|
|
29
29
|
portSegment = target.slice(lastColon + 1).trim();
|
|
30
|
-
if (hostname.includes(
|
|
30
|
+
if (hostname.includes(":")) {
|
|
31
31
|
throw new Error(`Invalid host:port format: ${target}. Wrap IPv6 addresses in brackets, e.g. "[2001:db8::1]:9473".`);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
if (!hostname) {
|
|
35
35
|
throw new Error(`Invalid host:port format: ${target}. Host portion is missing.`);
|
|
36
36
|
}
|
|
37
|
-
const port = Number.parseInt(portSegment ??
|
|
37
|
+
const port = Number.parseInt(portSegment ?? "", 10);
|
|
38
38
|
if (!Number.isFinite(port) || port <= 0 || port > 65_535) {
|
|
39
|
-
throw new Error(`Invalid port: "${portSegment ??
|
|
39
|
+
throw new Error(`Invalid port: "${portSegment ?? ""}". Expected a number between 1 and 65535.`);
|
|
40
40
|
}
|
|
41
41
|
return { hostname, port };
|
|
42
42
|
}
|
|
43
43
|
export function parseBridgeConnectionString(input) {
|
|
44
44
|
const raw = input.trim();
|
|
45
45
|
if (!raw) {
|
|
46
|
-
throw new Error(
|
|
46
|
+
throw new Error("Missing connection string.");
|
|
47
47
|
}
|
|
48
48
|
let url;
|
|
49
49
|
try {
|
|
50
|
-
url = raw.includes(
|
|
50
|
+
url = raw.includes("://") ? new URL(raw) : new URL(`oracle+tcp://${raw}`);
|
|
51
51
|
}
|
|
52
52
|
catch (error) {
|
|
53
53
|
throw new Error(`Invalid connection string: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
54
|
}
|
|
55
55
|
const hostname = url.hostname?.trim();
|
|
56
|
-
const port = Number.parseInt(url.port ??
|
|
56
|
+
const port = Number.parseInt(url.port ?? "", 10);
|
|
57
57
|
if (!hostname || !Number.isFinite(port) || port <= 0 || port > 65_535) {
|
|
58
58
|
throw new Error(`Invalid connection string host: ${raw}. Expected host:port.`);
|
|
59
59
|
}
|
|
60
|
-
const token = url.searchParams.get(
|
|
60
|
+
const token = url.searchParams.get("token")?.trim() ?? "";
|
|
61
61
|
if (!token) {
|
|
62
62
|
throw new Error('Connection string is missing token. Expected "?token=...".');
|
|
63
63
|
}
|
|
@@ -74,11 +74,11 @@ export function formatBridgeConnectionString(connection, options = {}) {
|
|
|
74
74
|
return `${base}?${params.toString()}`;
|
|
75
75
|
}
|
|
76
76
|
export function looksLikePath(value) {
|
|
77
|
-
return value.includes(
|
|
77
|
+
return value.includes("/") || value.includes("\\") || value.endsWith(".json");
|
|
78
78
|
}
|
|
79
79
|
export async function readBridgeConnectionArtifact(filePath) {
|
|
80
80
|
const resolved = path.resolve(process.cwd(), filePath);
|
|
81
|
-
const raw = await fs.readFile(resolved,
|
|
81
|
+
const raw = await fs.readFile(resolved, "utf8");
|
|
82
82
|
let parsed;
|
|
83
83
|
try {
|
|
84
84
|
parsed = JSON.parse(raw);
|
|
@@ -86,15 +86,15 @@ export async function readBridgeConnectionArtifact(filePath) {
|
|
|
86
86
|
catch (error) {
|
|
87
87
|
throw new Error(`Failed to parse connection artifact JSON at ${resolved}: ${error instanceof Error ? error.message : String(error)}`);
|
|
88
88
|
}
|
|
89
|
-
if (!parsed || typeof parsed !==
|
|
89
|
+
if (!parsed || typeof parsed !== "object") {
|
|
90
90
|
throw new Error(`Invalid connection artifact at ${resolved}: expected an object.`);
|
|
91
91
|
}
|
|
92
92
|
const remoteHost = parsed.remoteHost;
|
|
93
93
|
const remoteToken = parsed.remoteToken;
|
|
94
|
-
if (typeof remoteHost !==
|
|
94
|
+
if (typeof remoteHost !== "string" || remoteHost.trim().length === 0) {
|
|
95
95
|
throw new Error(`Invalid connection artifact at ${resolved}: remoteHost is missing.`);
|
|
96
96
|
}
|
|
97
|
-
if (typeof remoteToken !==
|
|
97
|
+
if (typeof remoteToken !== "string" || remoteToken.trim().length === 0) {
|
|
98
98
|
throw new Error(`Invalid connection artifact at ${resolved}: remoteToken is missing.`);
|
|
99
99
|
}
|
|
100
100
|
// Validate host formatting early so downstream checks don't crash.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import JSON5 from
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import JSON5 from "json5";
|
|
4
4
|
export async function readUserConfigFile(configPath) {
|
|
5
5
|
try {
|
|
6
|
-
const raw = await fs.readFile(configPath,
|
|
6
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
7
7
|
const parsed = JSON5.parse(raw);
|
|
8
8
|
return { config: parsed ?? {}, loaded: true };
|
|
9
9
|
}
|
|
10
10
|
catch (error) {
|
|
11
11
|
const code = error.code;
|
|
12
|
-
if (code ===
|
|
12
|
+
if (code === "ENOENT") {
|
|
13
13
|
return { config: {}, loaded: false };
|
|
14
14
|
}
|
|
15
15
|
throw new Error(`Failed to read ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -20,9 +20,9 @@ export async function writeUserConfigFile(configPath, config) {
|
|
|
20
20
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
21
21
|
const contents = `${JSON.stringify(config, null, 2)}\n`;
|
|
22
22
|
const tempPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
|
|
23
|
-
await fs.writeFile(tempPath, contents, { encoding:
|
|
23
|
+
await fs.writeFile(tempPath, contents, { encoding: "utf8", mode: 0o600 });
|
|
24
24
|
await fs.rename(tempPath, configPath);
|
|
25
|
-
if (process.platform !==
|
|
25
|
+
if (process.platform !== "win32") {
|
|
26
26
|
await fs.chmod(configPath, 0o600).catch(() => undefined);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
export function isProjectChatgptUrl(url) {
|
|
2
|
+
return /\/project(?:[/?#]|$)/i.test(url ?? "");
|
|
3
|
+
}
|
|
4
|
+
export function resolveBrowserArchiveDecision({ mode = "auto", chatgptUrl, conversationUrl, researchMode, followUpCount, }) {
|
|
5
|
+
if (mode === "never") {
|
|
6
|
+
return { mode, shouldArchive: false, reason: "disabled" };
|
|
7
|
+
}
|
|
8
|
+
if (!conversationUrl) {
|
|
9
|
+
return { mode, shouldArchive: false, reason: "missing-conversation-url" };
|
|
10
|
+
}
|
|
11
|
+
if (mode === "always") {
|
|
12
|
+
return { mode, shouldArchive: true, reason: "forced" };
|
|
13
|
+
}
|
|
14
|
+
if (isProjectChatgptUrl(chatgptUrl) || isProjectChatgptUrl(conversationUrl)) {
|
|
15
|
+
return { mode, shouldArchive: false, reason: "project-conversation" };
|
|
16
|
+
}
|
|
17
|
+
if (researchMode === "deep") {
|
|
18
|
+
return { mode, shouldArchive: false, reason: "deep-research" };
|
|
19
|
+
}
|
|
20
|
+
if ((followUpCount ?? 0) > 0) {
|
|
21
|
+
return { mode, shouldArchive: false, reason: "multi-turn" };
|
|
22
|
+
}
|
|
23
|
+
return { mode, shouldArchive: true, reason: "successful-one-shot" };
|
|
24
|
+
}
|
|
25
|
+
export async function archiveChatGptConversation(Runtime, logger, { mode, conversationUrl, }) {
|
|
26
|
+
const evaluated = await Runtime.evaluate({
|
|
27
|
+
expression: buildArchiveConversationExpression(),
|
|
28
|
+
awaitPromise: true,
|
|
29
|
+
returnByValue: true,
|
|
30
|
+
});
|
|
31
|
+
const value = evaluated.result?.value;
|
|
32
|
+
const resolvedUrl = value?.conversationUrl ?? conversationUrl ?? undefined;
|
|
33
|
+
if (value?.status === "archived") {
|
|
34
|
+
logger("[browser] Archived ChatGPT conversation after saving local artifacts.");
|
|
35
|
+
return { mode, attempted: true, archived: true, conversationUrl: resolvedUrl };
|
|
36
|
+
}
|
|
37
|
+
const reason = value?.status === "skipped" ? value.reason : "archive-failed";
|
|
38
|
+
const error = value?.status === "failed" ? value.error : undefined;
|
|
39
|
+
logger(`[browser] ChatGPT archive skipped (${error ?? reason}).`);
|
|
40
|
+
return {
|
|
41
|
+
mode,
|
|
42
|
+
attempted: true,
|
|
43
|
+
archived: false,
|
|
44
|
+
reason,
|
|
45
|
+
conversationUrl: resolvedUrl,
|
|
46
|
+
error,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function buildArchiveConversationExpressionForTest() {
|
|
50
|
+
return buildArchiveConversationExpression();
|
|
51
|
+
}
|
|
52
|
+
function buildArchiveConversationExpression() {
|
|
53
|
+
return `(() => {
|
|
54
|
+
const conversationUrl = typeof location === 'object' ? location.href : null;
|
|
55
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
|
+
const normalize = (value) =>
|
|
57
|
+
String(value ?? '')
|
|
58
|
+
.replace(/\\s+/g, ' ')
|
|
59
|
+
.trim()
|
|
60
|
+
.toLowerCase();
|
|
61
|
+
const isVisible = (element) => {
|
|
62
|
+
if (!element || !(element instanceof HTMLElement)) return false;
|
|
63
|
+
const rect = element.getBoundingClientRect();
|
|
64
|
+
const style = getComputedStyle(element);
|
|
65
|
+
return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
|
|
66
|
+
};
|
|
67
|
+
const labelFor = (element) =>
|
|
68
|
+
normalize([
|
|
69
|
+
element.getAttribute?.('aria-label'),
|
|
70
|
+
element.getAttribute?.('title'),
|
|
71
|
+
element.textContent,
|
|
72
|
+
].filter(Boolean).join(' '));
|
|
73
|
+
const click = (element) => {
|
|
74
|
+
const rect = element.getBoundingClientRect();
|
|
75
|
+
const eventInit = {
|
|
76
|
+
bubbles: true,
|
|
77
|
+
cancelable: true,
|
|
78
|
+
view: window,
|
|
79
|
+
clientX: rect.left + rect.width / 2,
|
|
80
|
+
clientY: rect.top + rect.height / 2,
|
|
81
|
+
button: 0,
|
|
82
|
+
};
|
|
83
|
+
if (typeof PointerEvent === 'function') {
|
|
84
|
+
element.dispatchEvent(new PointerEvent('pointerdown', {
|
|
85
|
+
...eventInit,
|
|
86
|
+
buttons: 1,
|
|
87
|
+
pointerId: 1,
|
|
88
|
+
pointerType: 'mouse',
|
|
89
|
+
isPrimary: true,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
element.dispatchEvent(new MouseEvent('mousedown', { ...eventInit, buttons: 1 }));
|
|
93
|
+
if (typeof PointerEvent === 'function') {
|
|
94
|
+
element.dispatchEvent(new PointerEvent('pointerup', {
|
|
95
|
+
...eventInit,
|
|
96
|
+
buttons: 0,
|
|
97
|
+
pointerId: 1,
|
|
98
|
+
pointerType: 'mouse',
|
|
99
|
+
isPrimary: true,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
element.dispatchEvent(new MouseEvent('mouseup', { ...eventInit, buttons: 0 }));
|
|
103
|
+
element.dispatchEvent(new MouseEvent('click', { ...eventInit, buttons: 0 }));
|
|
104
|
+
};
|
|
105
|
+
const findConversationMenuButton = () => {
|
|
106
|
+
const buttons = Array.from(document.querySelectorAll('button,[role="button"]'))
|
|
107
|
+
.filter((element) => element instanceof HTMLElement && isVisible(element));
|
|
108
|
+
const labelled = buttons
|
|
109
|
+
.map((element) => ({ element, label: labelFor(element), rect: element.getBoundingClientRect() }))
|
|
110
|
+
.filter(({ label }) =>
|
|
111
|
+
label.includes('more') ||
|
|
112
|
+
label.includes('conversation options') ||
|
|
113
|
+
label.includes('open menu') ||
|
|
114
|
+
label.includes('więcej') ||
|
|
115
|
+
label.includes('opcje')
|
|
116
|
+
);
|
|
117
|
+
const headerCandidates = labelled
|
|
118
|
+
.filter(({ rect }) => rect.top < 180 && rect.right > window.innerWidth - 420)
|
|
119
|
+
.sort((a, b) => b.rect.right - a.rect.right);
|
|
120
|
+
return (headerCandidates[0] ?? labelled[0])?.element ?? null;
|
|
121
|
+
};
|
|
122
|
+
const visibleMenuCandidates = () => {
|
|
123
|
+
const menuRoots = Array.from(document.querySelectorAll('[role="menu"]'))
|
|
124
|
+
.filter((element) => element instanceof HTMLElement && isVisible(element));
|
|
125
|
+
const roots = menuRoots.length > 0 ? menuRoots : [document];
|
|
126
|
+
return roots.flatMap((root) =>
|
|
127
|
+
Array.from(root.querySelectorAll('[role="menuitem"],[role="option"],button,div[tabindex],a')),
|
|
128
|
+
).filter((element) => element instanceof HTMLElement && isVisible(element));
|
|
129
|
+
};
|
|
130
|
+
const findArchiveMenuItem = () => {
|
|
131
|
+
const candidates = visibleMenuCandidates();
|
|
132
|
+
return candidates.find((element) => {
|
|
133
|
+
const label = labelFor(element);
|
|
134
|
+
if (!label) return false;
|
|
135
|
+
if (label.includes('unarchive') || label.includes('restore')) return false;
|
|
136
|
+
return label.includes('archive') || label.includes('archiwizuj');
|
|
137
|
+
}) ?? null;
|
|
138
|
+
};
|
|
139
|
+
const findArchiveConfirmationButton = () => {
|
|
140
|
+
const candidates = Array.from(document.querySelectorAll('[role="dialog"] button,[role="dialog"] [role="button"]'))
|
|
141
|
+
.filter((element) => element instanceof HTMLElement && isVisible(element));
|
|
142
|
+
return candidates.find((element) => {
|
|
143
|
+
const label = labelFor(element);
|
|
144
|
+
if (!label) return false;
|
|
145
|
+
if (label.includes('unarchive') || label.includes('restore')) return false;
|
|
146
|
+
return label === 'archive' || label === 'archiwizuj' || label.includes('archive conversation');
|
|
147
|
+
}) ?? null;
|
|
148
|
+
};
|
|
149
|
+
const hasUnarchiveMenuItem = () => {
|
|
150
|
+
const candidates = visibleMenuCandidates();
|
|
151
|
+
return candidates.some((element) => {
|
|
152
|
+
const label = labelFor(element);
|
|
153
|
+
return (
|
|
154
|
+
label.includes('unarchive') ||
|
|
155
|
+
label.includes('restore') ||
|
|
156
|
+
label.includes('przywróć') ||
|
|
157
|
+
label.includes('przywroc')
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
const hasArchiveConfirmation = () => {
|
|
162
|
+
const visibleText = Array.from(document.querySelectorAll('[role="status"],[role="alert"],[data-testid*="toast"],[class*="toast"],[class*="snackbar"]'))
|
|
163
|
+
.filter((element) => element instanceof HTMLElement && isVisible(element))
|
|
164
|
+
.map((element) => labelFor(element))
|
|
165
|
+
.join(' ');
|
|
166
|
+
return (
|
|
167
|
+
visibleText.includes('archived') ||
|
|
168
|
+
visibleText.includes('conversation archived') ||
|
|
169
|
+
visibleText.includes('chat archived') ||
|
|
170
|
+
visibleText.includes('zarchiwizowano') ||
|
|
171
|
+
visibleText.includes('archiwum')
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
const waitForArchiveConfirmation = async () => {
|
|
175
|
+
const deadline = Date.now() + 3000;
|
|
176
|
+
while (Date.now() < deadline) {
|
|
177
|
+
if (conversationUrl && location.href !== conversationUrl) return true;
|
|
178
|
+
if (hasArchiveConfirmation()) return true;
|
|
179
|
+
await sleep(150);
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
};
|
|
183
|
+
const verifyArchivedStateFromMenu = async () => {
|
|
184
|
+
const menuButton = findConversationMenuButton();
|
|
185
|
+
if (!menuButton) return false;
|
|
186
|
+
click(menuButton);
|
|
187
|
+
await sleep(300);
|
|
188
|
+
const archived = hasUnarchiveMenuItem();
|
|
189
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
190
|
+
return archived;
|
|
191
|
+
};
|
|
192
|
+
return (async () => {
|
|
193
|
+
const menuButton = findConversationMenuButton();
|
|
194
|
+
if (!menuButton) {
|
|
195
|
+
return { status: 'skipped', reason: 'conversation-menu-not-found', conversationUrl };
|
|
196
|
+
}
|
|
197
|
+
click(menuButton);
|
|
198
|
+
await sleep(350);
|
|
199
|
+
const archiveItem = findArchiveMenuItem();
|
|
200
|
+
if (!archiveItem) {
|
|
201
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
202
|
+
return { status: 'skipped', reason: 'archive-menu-item-not-found', conversationUrl };
|
|
203
|
+
}
|
|
204
|
+
click(archiveItem);
|
|
205
|
+
await sleep(350);
|
|
206
|
+
const confirmButton = findArchiveConfirmationButton();
|
|
207
|
+
if (confirmButton) {
|
|
208
|
+
click(confirmButton);
|
|
209
|
+
await sleep(500);
|
|
210
|
+
}
|
|
211
|
+
if (await waitForArchiveConfirmation()) {
|
|
212
|
+
return { status: 'archived', conversationUrl };
|
|
213
|
+
}
|
|
214
|
+
if (await verifyArchivedStateFromMenu()) {
|
|
215
|
+
return { status: 'archived', conversationUrl };
|
|
216
|
+
}
|
|
217
|
+
return { status: 'skipped', reason: 'archive-not-confirmed', conversationUrl };
|
|
218
|
+
})().catch((error) => ({
|
|
219
|
+
status: 'failed',
|
|
220
|
+
error: error instanceof Error ? error.message : String(error),
|
|
221
|
+
conversationUrl,
|
|
222
|
+
}));
|
|
223
|
+
})()`;
|
|
224
|
+
}
|