@jackwener/opencli 1.0.0 → 1.0.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/README.md +20 -1
- package/README.zh-CN.md +20 -1
- package/dist/browser/daemon-client.d.ts +1 -1
- package/dist/browser/index.d.ts +1 -2
- package/dist/browser/index.js +1 -5
- package/dist/browser/mcp.d.ts +5 -8
- package/dist/browser/mcp.js +9 -10
- package/dist/browser/page.d.ts +8 -1
- package/dist/browser/page.js +23 -17
- package/dist/browser.test.js +6 -6
- package/dist/cli-manifest.json +394 -14
- package/dist/clis/apple-podcasts/episodes.d.ts +1 -0
- package/dist/clis/apple-podcasts/episodes.js +28 -0
- package/dist/clis/apple-podcasts/search.d.ts +1 -0
- package/dist/clis/apple-podcasts/search.js +29 -0
- package/dist/clis/apple-podcasts/top.d.ts +1 -0
- package/dist/clis/apple-podcasts/top.js +34 -0
- package/dist/clis/apple-podcasts/utils.d.ts +11 -0
- package/dist/clis/apple-podcasts/utils.js +30 -0
- package/dist/clis/apple-podcasts/utils.test.d.ts +1 -0
- package/dist/clis/apple-podcasts/utils.test.js +57 -0
- package/dist/clis/chatwise/history.js +18 -1
- package/dist/clis/discord-app/channels.js +33 -21
- package/dist/clis/twitter/accept.d.ts +1 -0
- package/dist/clis/twitter/accept.js +202 -0
- package/dist/clis/twitter/followers.js +30 -22
- package/dist/clis/twitter/following.js +19 -14
- package/dist/clis/twitter/notifications.js +29 -22
- package/dist/clis/twitter/reply-dm.d.ts +1 -0
- package/dist/clis/twitter/reply-dm.js +181 -0
- package/dist/clis/twitter/search.js +50 -12
- package/dist/clis/weread/book.d.ts +1 -0
- package/dist/clis/weread/book.js +26 -0
- package/dist/clis/weread/highlights.d.ts +1 -0
- package/dist/clis/weread/highlights.js +23 -0
- package/dist/clis/weread/notebooks.d.ts +1 -0
- package/dist/clis/weread/notebooks.js +21 -0
- package/dist/clis/weread/notes.d.ts +1 -0
- package/dist/clis/weread/notes.js +29 -0
- package/dist/clis/weread/ranking.d.ts +1 -0
- package/dist/clis/weread/ranking.js +28 -0
- package/dist/clis/weread/search.d.ts +1 -0
- package/dist/clis/weread/search.js +25 -0
- package/dist/clis/weread/shelf.d.ts +1 -0
- package/dist/clis/weread/shelf.js +24 -0
- package/dist/clis/weread/utils.d.ts +20 -0
- package/dist/clis/weread/utils.js +72 -0
- package/dist/clis/weread/utils.test.d.ts +1 -0
- package/dist/clis/weread/utils.test.js +85 -0
- package/dist/daemon.js +2 -2
- package/dist/doctor.d.ts +0 -21
- package/dist/doctor.js +2 -24
- package/dist/main.js +6 -16
- package/dist/runtime.d.ts +1 -4
- package/dist/runtime.js +1 -4
- package/dist/setup.js +2 -2
- package/extension/dist/background.js +484 -0
- package/extension/manifest.json +1 -1
- package/extension/package.json +1 -1
- package/extension/src/background.ts +99 -22
- package/extension/src/protocol.ts +1 -1
- package/package.json +1 -1
- package/src/browser/daemon-client.ts +1 -1
- package/src/browser/index.ts +1 -6
- package/src/browser/mcp.ts +14 -15
- package/src/browser/page.ts +23 -17
- package/src/browser.test.ts +6 -6
- package/src/clis/apple-podcasts/episodes.ts +28 -0
- package/src/clis/apple-podcasts/search.ts +29 -0
- package/src/clis/apple-podcasts/top.ts +34 -0
- package/src/clis/apple-podcasts/utils.test.ts +72 -0
- package/src/clis/apple-podcasts/utils.ts +37 -0
- package/src/clis/chatwise/history.ts +15 -1
- package/src/clis/discord-app/channels.ts +33 -21
- package/src/clis/twitter/accept.ts +213 -0
- package/src/clis/twitter/followers.ts +36 -29
- package/src/clis/twitter/following.ts +25 -20
- package/src/clis/twitter/notifications.ts +34 -27
- package/src/clis/twitter/reply-dm.ts +193 -0
- package/src/clis/twitter/search.ts +53 -13
- package/src/clis/weread/book.ts +28 -0
- package/src/clis/weread/highlights.ts +25 -0
- package/src/clis/weread/notebooks.ts +23 -0
- package/src/clis/weread/notes.ts +31 -0
- package/src/clis/weread/ranking.ts +29 -0
- package/src/clis/weread/search.ts +26 -0
- package/src/clis/weread/shelf.ts +26 -0
- package/src/clis/weread/utils.test.ts +104 -0
- package/src/clis/weread/utils.ts +74 -0
- package/src/daemon.ts +2 -2
- package/src/doctor.ts +2 -19
- package/src/main.ts +5 -11
- package/src/runtime.ts +2 -6
- package/src/setup.ts +2 -2
- package/tests/e2e/public-commands.test.ts +68 -1
- package/dist/clis/grok/debug.d.ts +0 -1
- package/dist/clis/grok/debug.js +0 -45
- package/src/clis/grok/debug.ts +0 -49
package/dist/daemon.js
CHANGED
|
@@ -95,8 +95,8 @@ async function handleRequest(req, res) {
|
|
|
95
95
|
const result = await new Promise((resolve, reject) => {
|
|
96
96
|
const timer = setTimeout(() => {
|
|
97
97
|
pending.delete(body.id);
|
|
98
|
-
reject(new Error('Command timeout (
|
|
99
|
-
},
|
|
98
|
+
reject(new Error('Command timeout (120s)'));
|
|
99
|
+
}, 120000);
|
|
100
100
|
pending.set(body.id, { resolve, reject, timer });
|
|
101
101
|
extensionWs.send(JSON.stringify(body));
|
|
102
102
|
});
|
package/dist/doctor.d.ts
CHANGED
|
@@ -30,24 +30,3 @@ export declare function checkConnectivity(opts?: {
|
|
|
30
30
|
}): Promise<ConnectivityResult>;
|
|
31
31
|
export declare function runBrowserDoctor(opts?: DoctorOptions): Promise<DoctorReport>;
|
|
32
32
|
export declare function renderBrowserDoctorReport(report: DoctorReport): string;
|
|
33
|
-
export declare const PLAYWRIGHT_TOKEN_ENV = "PLAYWRIGHT_MCP_EXTENSION_TOKEN";
|
|
34
|
-
export declare function discoverExtensionToken(): string | null;
|
|
35
|
-
export declare function checkExtensionInstalled(): {
|
|
36
|
-
installed: boolean;
|
|
37
|
-
browsers: string[];
|
|
38
|
-
};
|
|
39
|
-
export declare function applyBrowserDoctorFix(): Promise<string[]>;
|
|
40
|
-
export declare function getDefaultShellRcPath(): string;
|
|
41
|
-
export declare function getDefaultMcpConfigPaths(): string[];
|
|
42
|
-
export declare function readTokenFromShellContent(_content: string): string | null;
|
|
43
|
-
export declare function upsertShellToken(content: string): string;
|
|
44
|
-
export declare function upsertJsonConfigToken(content: string): string;
|
|
45
|
-
export declare function readTomlConfigToken(_content: string): string | null;
|
|
46
|
-
export declare function upsertTomlConfigToken(content: string): string;
|
|
47
|
-
export declare function shortenPath(p: string): string;
|
|
48
|
-
export declare function toolName(_p: string): string;
|
|
49
|
-
export declare function fileExists(filePath: string): boolean;
|
|
50
|
-
export declare function writeFileWithMkdir(_p: string, _c: string): void;
|
|
51
|
-
export declare function checkTokenConnectivity(opts?: {
|
|
52
|
-
timeout?: number;
|
|
53
|
-
}): Promise<ConnectivityResult>;
|
package/dist/doctor.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
9
|
-
import {
|
|
9
|
+
import { BrowserBridge } from './browser/index.js';
|
|
10
10
|
/**
|
|
11
11
|
* Test connectivity by attempting a real browser command.
|
|
12
12
|
*/
|
|
13
13
|
export async function checkConnectivity(opts) {
|
|
14
14
|
const start = Date.now();
|
|
15
15
|
try {
|
|
16
|
-
const mcp = new
|
|
16
|
+
const mcp = new BrowserBridge();
|
|
17
17
|
const page = await mcp.connect({ timeout: opts?.timeout ?? 8 });
|
|
18
18
|
// Try a simple eval to verify end-to-end connectivity
|
|
19
19
|
await page.evaluate('1 + 1');
|
|
@@ -82,25 +82,3 @@ export function renderBrowserDoctorReport(report) {
|
|
|
82
82
|
}
|
|
83
83
|
return lines.join('\n');
|
|
84
84
|
}
|
|
85
|
-
// Backward compatibility exports (no-ops for things that no longer exist)
|
|
86
|
-
export const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
87
|
-
export function discoverExtensionToken() { return null; }
|
|
88
|
-
export function checkExtensionInstalled() { return { installed: false, browsers: [] }; }
|
|
89
|
-
export function applyBrowserDoctorFix() { return Promise.resolve([]); }
|
|
90
|
-
export function getDefaultShellRcPath() { return ''; }
|
|
91
|
-
export function getDefaultMcpConfigPaths() { return []; }
|
|
92
|
-
export function readTokenFromShellContent(_content) { return null; }
|
|
93
|
-
export function upsertShellToken(content) { return content; }
|
|
94
|
-
export function upsertJsonConfigToken(content) { return content; }
|
|
95
|
-
export function readTomlConfigToken(_content) { return null; }
|
|
96
|
-
export function upsertTomlConfigToken(content) { return content; }
|
|
97
|
-
export function shortenPath(p) { return p; }
|
|
98
|
-
export function toolName(_p) { return ''; }
|
|
99
|
-
export function fileExists(filePath) { try {
|
|
100
|
-
return require('node:fs').existsSync(filePath);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return false;
|
|
104
|
-
} }
|
|
105
|
-
export function writeFileWithMkdir(_p, _c) { }
|
|
106
|
-
export async function checkTokenConnectivity(opts) { return checkConnectivity(opts); }
|
package/dist/main.js
CHANGED
|
@@ -8,9 +8,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { Command } from 'commander';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import { discoverClis, executeCommand } from './engine.js';
|
|
11
|
-
import {
|
|
11
|
+
import { fullName, getRegistry, strategyLabel } from './registry.js';
|
|
12
12
|
import { render as renderOutput } from './output.js';
|
|
13
|
-
import {
|
|
13
|
+
import { BrowserBridge } from './browser/index.js';
|
|
14
14
|
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
|
|
15
15
|
import { PKG_VERSION } from './version.js';
|
|
16
16
|
import { getCompletions, printCompletionScript } from './completion.js';
|
|
@@ -101,15 +101,15 @@ program.command('verify').description('Validate + smoke test').argument('[target
|
|
|
101
101
|
process.exitCode = r.ok ? 0 : 1;
|
|
102
102
|
});
|
|
103
103
|
program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click <labels>', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")')
|
|
104
|
-
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory:
|
|
104
|
+
.action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserBridge, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
|
|
105
105
|
program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
|
|
106
106
|
.action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
|
|
107
107
|
program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
|
|
108
|
-
.action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory:
|
|
108
|
+
.action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: BrowserBridge, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
|
|
109
109
|
program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
|
|
110
110
|
.action(async (url, opts) => {
|
|
111
111
|
const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
|
|
112
|
-
const result = await browserSession(
|
|
112
|
+
const result = await browserSession(BrowserBridge, async (page) => {
|
|
113
113
|
// Navigate to the site first for cookie context
|
|
114
114
|
try {
|
|
115
115
|
const siteUrl = new URL(url);
|
|
@@ -196,17 +196,7 @@ for (const [, cmd] of registry) {
|
|
|
196
196
|
process.env.OPENCLI_VERBOSE = '1';
|
|
197
197
|
let result;
|
|
198
198
|
if (cmd.browser) {
|
|
199
|
-
result = await browserSession(
|
|
200
|
-
// Cookie/header strategies require same-origin context for credentialed fetch.
|
|
201
|
-
// In CDP mode the active tab may be on an unrelated domain, causing CORS failures.
|
|
202
|
-
// Navigate to the command's domain first (mirrors cascade command behavior).
|
|
203
|
-
if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
|
|
204
|
-
try {
|
|
205
|
-
await page.goto(`https://${cmd.domain}`);
|
|
206
|
-
await page.wait(2);
|
|
207
|
-
}
|
|
208
|
-
catch { }
|
|
209
|
-
}
|
|
199
|
+
result = await browserSession(BrowserBridge, async (page) => {
|
|
210
200
|
return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
|
|
211
201
|
});
|
|
212
202
|
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime utilities: timeouts and browser session management.
|
|
3
|
-
*/
|
|
4
1
|
import type { IPage } from './types.js';
|
|
5
2
|
export declare const DEFAULT_BROWSER_CONNECT_TIMEOUT: number;
|
|
6
3
|
export declare const DEFAULT_BROWSER_COMMAND_TIMEOUT: number;
|
|
@@ -17,7 +14,7 @@ export declare function runWithTimeout<T>(promise: Promise<T>, opts: {
|
|
|
17
14
|
* Timeout with milliseconds unit. Used for low-level internal timeouts.
|
|
18
15
|
*/
|
|
19
16
|
export declare function withTimeoutMs<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T>;
|
|
20
|
-
/** Interface for browser factory (
|
|
17
|
+
/** Interface for browser factory (BrowserBridge or test mocks) */
|
|
21
18
|
export interface IBrowserFactory {
|
|
22
19
|
connect(opts?: {
|
|
23
20
|
timeout?: number;
|
package/dist/runtime.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime utilities: timeouts and browser session management.
|
|
3
|
-
*/
|
|
4
1
|
export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
5
|
-
export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '
|
|
2
|
+
export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '60', 10);
|
|
6
3
|
export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_EXPLORE_TIMEOUT ?? '120', 10);
|
|
7
4
|
export const DEFAULT_BROWSER_SMOKE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_SMOKE_TIMEOUT ?? '60', 10);
|
|
8
5
|
/**
|
package/dist/setup.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { checkDaemonStatus } from './browser/discover.js';
|
|
9
9
|
import { checkConnectivity } from './doctor.js';
|
|
10
|
-
import {
|
|
10
|
+
import { BrowserBridge } from './browser/index.js';
|
|
11
11
|
export async function runSetup(opts = {}) {
|
|
12
12
|
console.log();
|
|
13
13
|
console.log(chalk.bold(' opencli setup') + chalk.dim(' — browser bridge configuration'));
|
|
@@ -23,7 +23,7 @@ export async function runSetup(opts = {}) {
|
|
|
23
23
|
console.log(chalk.dim(' The daemon starts automatically when you run a browser command.'));
|
|
24
24
|
console.log(chalk.dim(' Starting daemon now...'));
|
|
25
25
|
// Try to spawn daemon
|
|
26
|
-
const mcp = new
|
|
26
|
+
const mcp = new BrowserBridge();
|
|
27
27
|
try {
|
|
28
28
|
await mcp.connect({ timeout: 5 });
|
|
29
29
|
await mcp.close();
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
//#region src/protocol.ts
|
|
2
|
+
/** Default daemon port */
|
|
3
|
+
var DAEMON_PORT = 19825;
|
|
4
|
+
var DAEMON_HOST = "localhost";
|
|
5
|
+
var DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
|
|
6
|
+
`${DAEMON_HOST}${DAEMON_PORT}`;
|
|
7
|
+
/** Base reconnect delay for extension WebSocket (ms) */
|
|
8
|
+
var WS_RECONNECT_BASE_DELAY = 2e3;
|
|
9
|
+
/** Max reconnect delay (ms) */
|
|
10
|
+
var WS_RECONNECT_MAX_DELAY = 6e4;
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/cdp.ts
|
|
13
|
+
/**
|
|
14
|
+
* CDP execution via chrome.debugger API.
|
|
15
|
+
*
|
|
16
|
+
* chrome.debugger only needs the "debugger" permission — no host_permissions.
|
|
17
|
+
* It can attach to any http/https tab. Avoid chrome:// and chrome-extension://
|
|
18
|
+
* tabs (resolveTabId in background.ts filters them).
|
|
19
|
+
*/
|
|
20
|
+
var attached = /* @__PURE__ */ new Set();
|
|
21
|
+
async function ensureAttached(tabId) {
|
|
22
|
+
if (attached.has(tabId)) return;
|
|
23
|
+
try {
|
|
24
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
25
|
+
} catch (e) {
|
|
26
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
27
|
+
if (msg.includes("Another debugger is already attached")) {
|
|
28
|
+
try {
|
|
29
|
+
await chrome.debugger.detach({ tabId });
|
|
30
|
+
} catch {}
|
|
31
|
+
try {
|
|
32
|
+
await chrome.debugger.attach({ tabId }, "1.3");
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(`attach failed: ${msg}`);
|
|
35
|
+
}
|
|
36
|
+
} else throw new Error(`attach failed: ${msg}`);
|
|
37
|
+
}
|
|
38
|
+
attached.add(tabId);
|
|
39
|
+
try {
|
|
40
|
+
await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
async function evaluate(tabId, expression) {
|
|
44
|
+
await ensureAttached(tabId);
|
|
45
|
+
const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
|
|
46
|
+
expression,
|
|
47
|
+
returnByValue: true,
|
|
48
|
+
awaitPromise: true
|
|
49
|
+
});
|
|
50
|
+
if (result.exceptionDetails) {
|
|
51
|
+
const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error";
|
|
52
|
+
throw new Error(errMsg);
|
|
53
|
+
}
|
|
54
|
+
return result.result?.value;
|
|
55
|
+
}
|
|
56
|
+
var evaluateAsync = evaluate;
|
|
57
|
+
/**
|
|
58
|
+
* Capture a screenshot via CDP Page.captureScreenshot.
|
|
59
|
+
* Returns base64-encoded image data.
|
|
60
|
+
*/
|
|
61
|
+
async function screenshot(tabId, options = {}) {
|
|
62
|
+
await ensureAttached(tabId);
|
|
63
|
+
const format = options.format ?? "png";
|
|
64
|
+
if (options.fullPage) {
|
|
65
|
+
const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics");
|
|
66
|
+
const size = metrics.cssContentSize || metrics.contentSize;
|
|
67
|
+
if (size) await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", {
|
|
68
|
+
mobile: false,
|
|
69
|
+
width: Math.ceil(size.width),
|
|
70
|
+
height: Math.ceil(size.height),
|
|
71
|
+
deviceScaleFactor: 1
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const params = { format };
|
|
76
|
+
if (format === "jpeg" && options.quality !== void 0) params.quality = Math.max(0, Math.min(100, options.quality));
|
|
77
|
+
return (await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params)).data;
|
|
78
|
+
} finally {
|
|
79
|
+
if (options.fullPage) await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => {});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function detach(tabId) {
|
|
83
|
+
if (!attached.has(tabId)) return;
|
|
84
|
+
attached.delete(tabId);
|
|
85
|
+
try {
|
|
86
|
+
chrome.debugger.detach({ tabId });
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
function registerListeners() {
|
|
90
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
91
|
+
attached.delete(tabId);
|
|
92
|
+
});
|
|
93
|
+
chrome.debugger.onDetach.addListener((source) => {
|
|
94
|
+
if (source.tabId) attached.delete(source.tabId);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/background.ts
|
|
99
|
+
var ws = null;
|
|
100
|
+
var reconnectTimer = null;
|
|
101
|
+
var reconnectAttempts = 0;
|
|
102
|
+
var _origLog = console.log.bind(console);
|
|
103
|
+
var _origWarn = console.warn.bind(console);
|
|
104
|
+
var _origError = console.error.bind(console);
|
|
105
|
+
function forwardLog(level, args) {
|
|
106
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
107
|
+
try {
|
|
108
|
+
const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
109
|
+
ws.send(JSON.stringify({
|
|
110
|
+
type: "log",
|
|
111
|
+
level,
|
|
112
|
+
msg,
|
|
113
|
+
ts: Date.now()
|
|
114
|
+
}));
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
console.log = (...args) => {
|
|
118
|
+
_origLog(...args);
|
|
119
|
+
forwardLog("info", args);
|
|
120
|
+
};
|
|
121
|
+
console.warn = (...args) => {
|
|
122
|
+
_origWarn(...args);
|
|
123
|
+
forwardLog("warn", args);
|
|
124
|
+
};
|
|
125
|
+
console.error = (...args) => {
|
|
126
|
+
_origError(...args);
|
|
127
|
+
forwardLog("error", args);
|
|
128
|
+
};
|
|
129
|
+
function connect() {
|
|
130
|
+
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
|
|
131
|
+
try {
|
|
132
|
+
ws = new WebSocket(DAEMON_WS_URL);
|
|
133
|
+
} catch {
|
|
134
|
+
scheduleReconnect();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
ws.onopen = () => {
|
|
138
|
+
console.log("[opencli] Connected to daemon");
|
|
139
|
+
reconnectAttempts = 0;
|
|
140
|
+
if (reconnectTimer) {
|
|
141
|
+
clearTimeout(reconnectTimer);
|
|
142
|
+
reconnectTimer = null;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
ws.onmessage = async (event) => {
|
|
146
|
+
try {
|
|
147
|
+
const result = await handleCommand(JSON.parse(event.data));
|
|
148
|
+
ws?.send(JSON.stringify(result));
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error("[opencli] Message handling error:", err);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
ws.onclose = () => {
|
|
154
|
+
console.log("[opencli] Disconnected from daemon");
|
|
155
|
+
ws = null;
|
|
156
|
+
scheduleReconnect();
|
|
157
|
+
};
|
|
158
|
+
ws.onerror = () => {
|
|
159
|
+
ws?.close();
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function scheduleReconnect() {
|
|
163
|
+
if (reconnectTimer) return;
|
|
164
|
+
reconnectAttempts++;
|
|
165
|
+
const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
|
|
166
|
+
reconnectTimer = setTimeout(() => {
|
|
167
|
+
reconnectTimer = null;
|
|
168
|
+
connect();
|
|
169
|
+
}, delay);
|
|
170
|
+
}
|
|
171
|
+
var automationWindowId = null;
|
|
172
|
+
var windowIdleTimer = null;
|
|
173
|
+
var WINDOW_IDLE_TIMEOUT = 3e4;
|
|
174
|
+
function resetWindowIdleTimer() {
|
|
175
|
+
if (windowIdleTimer) clearTimeout(windowIdleTimer);
|
|
176
|
+
windowIdleTimer = setTimeout(async () => {
|
|
177
|
+
if (automationWindowId !== null) {
|
|
178
|
+
try {
|
|
179
|
+
await chrome.windows.remove(automationWindowId);
|
|
180
|
+
console.log(`[opencli] Automation window ${automationWindowId} closed (idle timeout)`);
|
|
181
|
+
} catch {}
|
|
182
|
+
automationWindowId = null;
|
|
183
|
+
}
|
|
184
|
+
windowIdleTimer = null;
|
|
185
|
+
}, WINDOW_IDLE_TIMEOUT);
|
|
186
|
+
}
|
|
187
|
+
/** Get or create the dedicated automation window. */
|
|
188
|
+
async function getAutomationWindow() {
|
|
189
|
+
if (automationWindowId !== null) try {
|
|
190
|
+
await chrome.windows.get(automationWindowId);
|
|
191
|
+
return automationWindowId;
|
|
192
|
+
} catch {
|
|
193
|
+
automationWindowId = null;
|
|
194
|
+
}
|
|
195
|
+
automationWindowId = (await chrome.windows.create({
|
|
196
|
+
url: "about:blank",
|
|
197
|
+
focused: false,
|
|
198
|
+
width: 1280,
|
|
199
|
+
height: 900,
|
|
200
|
+
type: "normal"
|
|
201
|
+
})).id;
|
|
202
|
+
console.log(`[opencli] Created automation window ${automationWindowId}`);
|
|
203
|
+
return automationWindowId;
|
|
204
|
+
}
|
|
205
|
+
chrome.windows.onRemoved.addListener((windowId) => {
|
|
206
|
+
if (windowId === automationWindowId) {
|
|
207
|
+
console.log("[opencli] Automation window closed");
|
|
208
|
+
automationWindowId = null;
|
|
209
|
+
if (windowIdleTimer) {
|
|
210
|
+
clearTimeout(windowIdleTimer);
|
|
211
|
+
windowIdleTimer = null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
var initialized = false;
|
|
216
|
+
function initialize() {
|
|
217
|
+
if (initialized) return;
|
|
218
|
+
initialized = true;
|
|
219
|
+
chrome.alarms.create("keepalive", { periodInMinutes: .4 });
|
|
220
|
+
registerListeners();
|
|
221
|
+
connect();
|
|
222
|
+
console.log("[opencli] Browser Bridge extension initialized");
|
|
223
|
+
}
|
|
224
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
225
|
+
initialize();
|
|
226
|
+
});
|
|
227
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
228
|
+
initialize();
|
|
229
|
+
});
|
|
230
|
+
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
231
|
+
if (alarm.name === "keepalive") connect();
|
|
232
|
+
});
|
|
233
|
+
async function handleCommand(cmd) {
|
|
234
|
+
resetWindowIdleTimer();
|
|
235
|
+
try {
|
|
236
|
+
switch (cmd.action) {
|
|
237
|
+
case "exec": return await handleExec(cmd);
|
|
238
|
+
case "navigate": return await handleNavigate(cmd);
|
|
239
|
+
case "tabs": return await handleTabs(cmd);
|
|
240
|
+
case "cookies": return await handleCookies(cmd);
|
|
241
|
+
case "screenshot": return await handleScreenshot(cmd);
|
|
242
|
+
case "close-window": return await handleCloseWindow(cmd);
|
|
243
|
+
default: return {
|
|
244
|
+
id: cmd.id,
|
|
245
|
+
ok: false,
|
|
246
|
+
error: `Unknown action: ${cmd.action}`
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return {
|
|
251
|
+
id: cmd.id,
|
|
252
|
+
ok: false,
|
|
253
|
+
error: err instanceof Error ? err.message : String(err)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** Check if a URL is a debuggable web page (not chrome:// or extension page) */
|
|
258
|
+
function isWebUrl(url) {
|
|
259
|
+
if (!url) return false;
|
|
260
|
+
return !url.startsWith("chrome://") && !url.startsWith("chrome-extension://");
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Resolve target tab in the automation window.
|
|
264
|
+
* If explicit tabId is given, use that directly.
|
|
265
|
+
* Otherwise, find or create a tab in the dedicated automation window.
|
|
266
|
+
*/
|
|
267
|
+
async function resolveTabId(tabId) {
|
|
268
|
+
if (tabId !== void 0) return tabId;
|
|
269
|
+
const windowId = await getAutomationWindow();
|
|
270
|
+
const tabs = await chrome.tabs.query({ windowId });
|
|
271
|
+
const webTab = tabs.find((t) => t.id && isWebUrl(t.url));
|
|
272
|
+
if (webTab?.id) return webTab.id;
|
|
273
|
+
if (tabs.length > 0 && tabs[0]?.id) return tabs[0].id;
|
|
274
|
+
const newTab = await chrome.tabs.create({
|
|
275
|
+
windowId,
|
|
276
|
+
url: "about:blank",
|
|
277
|
+
active: true
|
|
278
|
+
});
|
|
279
|
+
if (!newTab.id) throw new Error("Failed to create tab in automation window");
|
|
280
|
+
return newTab.id;
|
|
281
|
+
}
|
|
282
|
+
async function handleExec(cmd) {
|
|
283
|
+
if (!cmd.code) return {
|
|
284
|
+
id: cmd.id,
|
|
285
|
+
ok: false,
|
|
286
|
+
error: "Missing code"
|
|
287
|
+
};
|
|
288
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
289
|
+
try {
|
|
290
|
+
const data = await evaluateAsync(tabId, cmd.code);
|
|
291
|
+
return {
|
|
292
|
+
id: cmd.id,
|
|
293
|
+
ok: true,
|
|
294
|
+
data
|
|
295
|
+
};
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return {
|
|
298
|
+
id: cmd.id,
|
|
299
|
+
ok: false,
|
|
300
|
+
error: err instanceof Error ? err.message : String(err)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function handleNavigate(cmd) {
|
|
305
|
+
if (!cmd.url) return {
|
|
306
|
+
id: cmd.id,
|
|
307
|
+
ok: false,
|
|
308
|
+
error: "Missing url"
|
|
309
|
+
};
|
|
310
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
311
|
+
await chrome.tabs.update(tabId, { url: cmd.url });
|
|
312
|
+
await new Promise((resolve) => {
|
|
313
|
+
chrome.tabs.get(tabId).then((tab) => {
|
|
314
|
+
if (tab.status === "complete") {
|
|
315
|
+
resolve();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const listener = (id, info) => {
|
|
319
|
+
if (id === tabId && info.status === "complete") {
|
|
320
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
321
|
+
resolve();
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
325
|
+
setTimeout(() => {
|
|
326
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
327
|
+
resolve();
|
|
328
|
+
}, 15e3);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
const tab = await chrome.tabs.get(tabId);
|
|
332
|
+
return {
|
|
333
|
+
id: cmd.id,
|
|
334
|
+
ok: true,
|
|
335
|
+
data: {
|
|
336
|
+
title: tab.title,
|
|
337
|
+
url: tab.url,
|
|
338
|
+
tabId
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async function handleTabs(cmd) {
|
|
343
|
+
switch (cmd.op) {
|
|
344
|
+
case "list": {
|
|
345
|
+
const data = (await chrome.tabs.query({})).filter((t) => isWebUrl(t.url)).map((t, i) => ({
|
|
346
|
+
index: i,
|
|
347
|
+
tabId: t.id,
|
|
348
|
+
url: t.url,
|
|
349
|
+
title: t.title,
|
|
350
|
+
active: t.active
|
|
351
|
+
}));
|
|
352
|
+
return {
|
|
353
|
+
id: cmd.id,
|
|
354
|
+
ok: true,
|
|
355
|
+
data
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
case "new": {
|
|
359
|
+
const tab = await chrome.tabs.create({
|
|
360
|
+
url: cmd.url,
|
|
361
|
+
active: true
|
|
362
|
+
});
|
|
363
|
+
return {
|
|
364
|
+
id: cmd.id,
|
|
365
|
+
ok: true,
|
|
366
|
+
data: {
|
|
367
|
+
tabId: tab.id,
|
|
368
|
+
url: tab.url
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
case "close": {
|
|
373
|
+
if (cmd.index !== void 0) {
|
|
374
|
+
const target = (await chrome.tabs.query({}))[cmd.index];
|
|
375
|
+
if (!target?.id) return {
|
|
376
|
+
id: cmd.id,
|
|
377
|
+
ok: false,
|
|
378
|
+
error: `Tab index ${cmd.index} not found`
|
|
379
|
+
};
|
|
380
|
+
await chrome.tabs.remove(target.id);
|
|
381
|
+
detach(target.id);
|
|
382
|
+
return {
|
|
383
|
+
id: cmd.id,
|
|
384
|
+
ok: true,
|
|
385
|
+
data: { closed: target.id }
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
389
|
+
await chrome.tabs.remove(tabId);
|
|
390
|
+
detach(tabId);
|
|
391
|
+
return {
|
|
392
|
+
id: cmd.id,
|
|
393
|
+
ok: true,
|
|
394
|
+
data: { closed: tabId }
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
case "select": {
|
|
398
|
+
if (cmd.index === void 0 && cmd.tabId === void 0) return {
|
|
399
|
+
id: cmd.id,
|
|
400
|
+
ok: false,
|
|
401
|
+
error: "Missing index or tabId"
|
|
402
|
+
};
|
|
403
|
+
if (cmd.tabId !== void 0) {
|
|
404
|
+
await chrome.tabs.update(cmd.tabId, { active: true });
|
|
405
|
+
return {
|
|
406
|
+
id: cmd.id,
|
|
407
|
+
ok: true,
|
|
408
|
+
data: { selected: cmd.tabId }
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const target = (await chrome.tabs.query({}))[cmd.index];
|
|
412
|
+
if (!target?.id) return {
|
|
413
|
+
id: cmd.id,
|
|
414
|
+
ok: false,
|
|
415
|
+
error: `Tab index ${cmd.index} not found`
|
|
416
|
+
};
|
|
417
|
+
await chrome.tabs.update(target.id, { active: true });
|
|
418
|
+
return {
|
|
419
|
+
id: cmd.id,
|
|
420
|
+
ok: true,
|
|
421
|
+
data: { selected: target.id }
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
default: return {
|
|
425
|
+
id: cmd.id,
|
|
426
|
+
ok: false,
|
|
427
|
+
error: `Unknown tabs op: ${cmd.op}`
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function handleCookies(cmd) {
|
|
432
|
+
const details = {};
|
|
433
|
+
if (cmd.domain) details.domain = cmd.domain;
|
|
434
|
+
if (cmd.url) details.url = cmd.url;
|
|
435
|
+
const data = (await chrome.cookies.getAll(details)).map((c) => ({
|
|
436
|
+
name: c.name,
|
|
437
|
+
value: c.value,
|
|
438
|
+
domain: c.domain,
|
|
439
|
+
path: c.path,
|
|
440
|
+
secure: c.secure,
|
|
441
|
+
httpOnly: c.httpOnly,
|
|
442
|
+
expirationDate: c.expirationDate
|
|
443
|
+
}));
|
|
444
|
+
return {
|
|
445
|
+
id: cmd.id,
|
|
446
|
+
ok: true,
|
|
447
|
+
data
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async function handleScreenshot(cmd) {
|
|
451
|
+
const tabId = await resolveTabId(cmd.tabId);
|
|
452
|
+
try {
|
|
453
|
+
const data = await screenshot(tabId, {
|
|
454
|
+
format: cmd.format,
|
|
455
|
+
quality: cmd.quality,
|
|
456
|
+
fullPage: cmd.fullPage
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
id: cmd.id,
|
|
460
|
+
ok: true,
|
|
461
|
+
data
|
|
462
|
+
};
|
|
463
|
+
} catch (err) {
|
|
464
|
+
return {
|
|
465
|
+
id: cmd.id,
|
|
466
|
+
ok: false,
|
|
467
|
+
error: err instanceof Error ? err.message : String(err)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function handleCloseWindow(cmd) {
|
|
472
|
+
if (automationWindowId !== null) {
|
|
473
|
+
try {
|
|
474
|
+
await chrome.windows.remove(automationWindowId);
|
|
475
|
+
} catch {}
|
|
476
|
+
automationWindowId = null;
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
id: cmd.id,
|
|
480
|
+
ok: true,
|
|
481
|
+
data: { closed: true }
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
//#endregion
|
package/extension/manifest.json
CHANGED