@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.
Files changed (98) hide show
  1. package/README.md +20 -1
  2. package/README.zh-CN.md +20 -1
  3. package/dist/browser/daemon-client.d.ts +1 -1
  4. package/dist/browser/index.d.ts +1 -2
  5. package/dist/browser/index.js +1 -5
  6. package/dist/browser/mcp.d.ts +5 -8
  7. package/dist/browser/mcp.js +9 -10
  8. package/dist/browser/page.d.ts +8 -1
  9. package/dist/browser/page.js +23 -17
  10. package/dist/browser.test.js +6 -6
  11. package/dist/cli-manifest.json +394 -14
  12. package/dist/clis/apple-podcasts/episodes.d.ts +1 -0
  13. package/dist/clis/apple-podcasts/episodes.js +28 -0
  14. package/dist/clis/apple-podcasts/search.d.ts +1 -0
  15. package/dist/clis/apple-podcasts/search.js +29 -0
  16. package/dist/clis/apple-podcasts/top.d.ts +1 -0
  17. package/dist/clis/apple-podcasts/top.js +34 -0
  18. package/dist/clis/apple-podcasts/utils.d.ts +11 -0
  19. package/dist/clis/apple-podcasts/utils.js +30 -0
  20. package/dist/clis/apple-podcasts/utils.test.d.ts +1 -0
  21. package/dist/clis/apple-podcasts/utils.test.js +57 -0
  22. package/dist/clis/chatwise/history.js +18 -1
  23. package/dist/clis/discord-app/channels.js +33 -21
  24. package/dist/clis/twitter/accept.d.ts +1 -0
  25. package/dist/clis/twitter/accept.js +202 -0
  26. package/dist/clis/twitter/followers.js +30 -22
  27. package/dist/clis/twitter/following.js +19 -14
  28. package/dist/clis/twitter/notifications.js +29 -22
  29. package/dist/clis/twitter/reply-dm.d.ts +1 -0
  30. package/dist/clis/twitter/reply-dm.js +181 -0
  31. package/dist/clis/twitter/search.js +50 -12
  32. package/dist/clis/weread/book.d.ts +1 -0
  33. package/dist/clis/weread/book.js +26 -0
  34. package/dist/clis/weread/highlights.d.ts +1 -0
  35. package/dist/clis/weread/highlights.js +23 -0
  36. package/dist/clis/weread/notebooks.d.ts +1 -0
  37. package/dist/clis/weread/notebooks.js +21 -0
  38. package/dist/clis/weread/notes.d.ts +1 -0
  39. package/dist/clis/weread/notes.js +29 -0
  40. package/dist/clis/weread/ranking.d.ts +1 -0
  41. package/dist/clis/weread/ranking.js +28 -0
  42. package/dist/clis/weread/search.d.ts +1 -0
  43. package/dist/clis/weread/search.js +25 -0
  44. package/dist/clis/weread/shelf.d.ts +1 -0
  45. package/dist/clis/weread/shelf.js +24 -0
  46. package/dist/clis/weread/utils.d.ts +20 -0
  47. package/dist/clis/weread/utils.js +72 -0
  48. package/dist/clis/weread/utils.test.d.ts +1 -0
  49. package/dist/clis/weread/utils.test.js +85 -0
  50. package/dist/daemon.js +2 -2
  51. package/dist/doctor.d.ts +0 -21
  52. package/dist/doctor.js +2 -24
  53. package/dist/main.js +6 -16
  54. package/dist/runtime.d.ts +1 -4
  55. package/dist/runtime.js +1 -4
  56. package/dist/setup.js +2 -2
  57. package/extension/dist/background.js +484 -0
  58. package/extension/manifest.json +1 -1
  59. package/extension/package.json +1 -1
  60. package/extension/src/background.ts +99 -22
  61. package/extension/src/protocol.ts +1 -1
  62. package/package.json +1 -1
  63. package/src/browser/daemon-client.ts +1 -1
  64. package/src/browser/index.ts +1 -6
  65. package/src/browser/mcp.ts +14 -15
  66. package/src/browser/page.ts +23 -17
  67. package/src/browser.test.ts +6 -6
  68. package/src/clis/apple-podcasts/episodes.ts +28 -0
  69. package/src/clis/apple-podcasts/search.ts +29 -0
  70. package/src/clis/apple-podcasts/top.ts +34 -0
  71. package/src/clis/apple-podcasts/utils.test.ts +72 -0
  72. package/src/clis/apple-podcasts/utils.ts +37 -0
  73. package/src/clis/chatwise/history.ts +15 -1
  74. package/src/clis/discord-app/channels.ts +33 -21
  75. package/src/clis/twitter/accept.ts +213 -0
  76. package/src/clis/twitter/followers.ts +36 -29
  77. package/src/clis/twitter/following.ts +25 -20
  78. package/src/clis/twitter/notifications.ts +34 -27
  79. package/src/clis/twitter/reply-dm.ts +193 -0
  80. package/src/clis/twitter/search.ts +53 -13
  81. package/src/clis/weread/book.ts +28 -0
  82. package/src/clis/weread/highlights.ts +25 -0
  83. package/src/clis/weread/notebooks.ts +23 -0
  84. package/src/clis/weread/notes.ts +31 -0
  85. package/src/clis/weread/ranking.ts +29 -0
  86. package/src/clis/weread/search.ts +26 -0
  87. package/src/clis/weread/shelf.ts +26 -0
  88. package/src/clis/weread/utils.test.ts +104 -0
  89. package/src/clis/weread/utils.ts +74 -0
  90. package/src/daemon.ts +2 -2
  91. package/src/doctor.ts +2 -19
  92. package/src/main.ts +5 -11
  93. package/src/runtime.ts +2 -6
  94. package/src/setup.ts +2 -2
  95. package/tests/e2e/public-commands.test.ts +68 -1
  96. package/dist/clis/grok/debug.d.ts +0 -1
  97. package/dist/clis/grok/debug.js +0 -45
  98. 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 (30s)'));
99
- }, 30000);
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 { PlaywrightMCP } from './browser/index.js';
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 PlaywrightMCP();
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 { Strategy, fullName, getRegistry, strategyLabel } from './registry.js';
11
+ import { fullName, getRegistry, strategyLabel } from './registry.js';
12
12
  import { render as renderOutput } from './output.js';
13
- import { PlaywrightMCP } from './browser/index.js';
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: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
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: PlaywrightMCP, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
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(PlaywrightMCP, async (page) => {
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(PlaywrightMCP, async (page) => {
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 (PlaywrightMCP or test mocks) */
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 ?? '45', 10);
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 { PlaywrightMCP } from './browser/index.js';
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 PlaywrightMCP();
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "opencli Browser Bridge",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "description": "Bridge between opencli CLI and your browser — execute commands, read cookies, manage tabs.",
6
6
  "permissions": [
7
7
  "debugger",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencli-extension",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {