@jackwener/opencli 1.2.4 → 1.2.6
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/.github/workflows/build-extension.yml +0 -15
- package/README.md +10 -4
- package/README.zh-CN.md +10 -4
- package/docs/guide/browser-bridge.md +3 -3
- package/docs/zh/guide/browser-bridge.md +3 -3
- package/extension/dist/background.js +8 -16
- package/extension/manifest.json +1 -1
- package/extension/package.json +1 -1
- package/extension/src/background.ts +10 -24
- package/extension/src/cdp.ts +6 -3
- package/package.json +1 -1
|
@@ -45,26 +45,12 @@ jobs:
|
|
|
45
45
|
cd extension-package
|
|
46
46
|
zip -r ../opencli-extension.zip .
|
|
47
47
|
|
|
48
|
-
- name: Create Extension CRX
|
|
49
|
-
run: |
|
|
50
|
-
npm install -g crx3
|
|
51
|
-
if [ -n "${{ secrets.CRX_PRIVATE_KEY }}" ]; then
|
|
52
|
-
echo "Found CRX_PRIVATE_KEY, signing extension..."
|
|
53
|
-
echo "${{ secrets.CRX_PRIVATE_KEY }}" > crx-key.pem
|
|
54
|
-
crx3 pack extension-package -o opencli-extension.crx -p crx-key.pem
|
|
55
|
-
rm crx-key.pem
|
|
56
|
-
else
|
|
57
|
-
echo "No CRX_PRIVATE_KEY configured. Generating CRX with a temporary random key..."
|
|
58
|
-
crx3 pack extension-package -o opencli-extension.crx
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
48
|
- name: Upload Artifacts (Action Run)
|
|
62
49
|
uses: actions/upload-artifact@v4
|
|
63
50
|
with:
|
|
64
51
|
name: opencli-extension-build
|
|
65
52
|
path: |
|
|
66
53
|
opencli-extension.zip
|
|
67
|
-
opencli-extension.crx
|
|
68
54
|
retention-days: 7
|
|
69
55
|
|
|
70
56
|
- name: Attach to GitHub Release
|
|
@@ -73,7 +59,6 @@ jobs:
|
|
|
73
59
|
with:
|
|
74
60
|
files: |
|
|
75
61
|
opencli-extension.zip
|
|
76
|
-
opencli-extension.crx
|
|
77
62
|
draft: false
|
|
78
63
|
prerelease: false
|
|
79
64
|
env:
|
package/README.md
CHANGED
|
@@ -61,11 +61,15 @@ OpenCLI connects to your browser through a lightweight **Browser Bridge** Chrome
|
|
|
61
61
|
You can install the extension via either method:
|
|
62
62
|
|
|
63
63
|
**Method 1: Download Pre-built Release (Recommended)**
|
|
64
|
-
1. Go to the GitHub [Releases page](https://github.com/jackwener/opencli/releases) and download the latest `opencli-extension.zip
|
|
65
|
-
2.
|
|
66
|
-
3.
|
|
64
|
+
1. Go to the GitHub [Releases page](https://github.com/jackwener/opencli/releases) and download the latest `opencli-extension.zip`.
|
|
65
|
+
2. Unzip the file and open `chrome://extensions`, enable **Developer mode** (top-right toggle).
|
|
66
|
+
3. Click **Load unpacked** and select the unzipped folder.
|
|
67
67
|
|
|
68
|
-
**Method 2: Load
|
|
68
|
+
**Method 2: Load from npm Package**
|
|
69
|
+
1. After installing opencli via npm, open `chrome://extensions` and enable **Developer mode**.
|
|
70
|
+
2. Click **Load unpacked** and select `node_modules/@jackwener/opencli/extension` directory.
|
|
71
|
+
|
|
72
|
+
**Method 3: Load Source (For Developers)**
|
|
69
73
|
1. Open `chrome://extensions` and enable **Developer mode**.
|
|
70
74
|
2. Click **Load unpacked** and select the `extension/` directory from this repository.
|
|
71
75
|
|
|
@@ -324,6 +328,8 @@ npx vitest run tests/e2e/ # E2E tests
|
|
|
324
328
|
|
|
325
329
|
- **"Extension not connected"**
|
|
326
330
|
- Ensure the opencli Browser Bridge extension is installed and **enabled** in `chrome://extensions`.
|
|
331
|
+
- **"attach failed: Cannot access a chrome-extension:// URL"**
|
|
332
|
+
- Another Chrome extension (e.g. youmind, New Tab Override, or AI assistant extensions) may be interfering. Try **disabling other extensions** temporarily, then retry.
|
|
327
333
|
- **Empty data returns or 'Unauthorized' error**
|
|
328
334
|
- Your login session in Chrome might have expired. Open a normal Chrome tab, navigate to the target site, and log in or refresh the page.
|
|
329
335
|
- **Node API errors**
|
package/README.zh-CN.md
CHANGED
|
@@ -62,11 +62,15 @@ OpenCLI 通过轻量化的 **Browser Bridge** Chrome 扩展 + 微型 daemon 与
|
|
|
62
62
|
你可以选择以下任一方式安装扩展:
|
|
63
63
|
|
|
64
64
|
**方式一:下载构建好的安装包(推荐)**
|
|
65
|
-
1. 到 GitHub [Releases 页面](https://github.com/jackwener/opencli/releases) 下载最新的 `opencli-extension.zip
|
|
66
|
-
2.
|
|
67
|
-
3.
|
|
65
|
+
1. 到 GitHub [Releases 页面](https://github.com/jackwener/opencli/releases) 下载最新的 `opencli-extension.zip`。
|
|
66
|
+
2. 解压后打开 Chrome 的 `chrome://extensions`,启用右上角的 **开发者模式**。
|
|
67
|
+
3. 点击 **加载已解压的扩展程序**,选择解压后的文件夹。
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
**方式二:从 npm 包加载**
|
|
70
|
+
1. 通过 npm 安装 opencli 后,打开 `chrome://extensions`,启用 **开发者模式**。
|
|
71
|
+
2. 点击 **加载已解压的扩展程序**,选择 `node_modules/@jackwener/opencli/extension` 目录。
|
|
72
|
+
|
|
73
|
+
**方式三:加载源码(针对开发者)**
|
|
70
74
|
1. 同样在 `chrome://extensions` 开启 **开发者模式**。
|
|
71
75
|
2. 点击 **加载已解压的扩展程序**,选择本仓库代码树中的 `extension/` 文件夹。
|
|
72
76
|
|
|
@@ -307,6 +311,8 @@ opencli cascade https://api.example.com/data
|
|
|
307
311
|
|
|
308
312
|
- **"Extension not connected" 报错**
|
|
309
313
|
- 确保你当前的 Chrome 已安装且**开启了** opencli Browser Bridge 扩展(在 `chrome://extensions` 中检查)。
|
|
314
|
+
- **"attach failed: Cannot access a chrome-extension:// URL" 报错**
|
|
315
|
+
- 其他 Chrome 扩展(如 youmind、New Tab Override 或 AI 助手类扩展)可能产生冲突。请尝试**暂时禁用其他扩展**后重试。
|
|
310
316
|
- **返回空数据,或者报错 "Unauthorized"**
|
|
311
317
|
- Chrome 里的登录态可能已经过期。请打开当前 Chrome 页面,在新标签页重新手工登录或刷新该页面。
|
|
312
318
|
- **Node API 错误 (如 parseArgs, fs 等)**
|
|
@@ -8,9 +8,9 @@ OpenCLI connects to your browser through a lightweight **Browser Bridge** Chrome
|
|
|
8
8
|
|
|
9
9
|
### Method 1: Download Pre-built Release (Recommended)
|
|
10
10
|
|
|
11
|
-
1. Go to the GitHub [Releases page](https://github.com/jackwener/opencli/releases) and download the latest `opencli-extension.zip
|
|
12
|
-
2.
|
|
13
|
-
3.
|
|
11
|
+
1. Go to the GitHub [Releases page](https://github.com/jackwener/opencli/releases) and download the latest `opencli-extension.zip`.
|
|
12
|
+
2. Unzip the file and open `chrome://extensions`, enable **Developer mode** (top-right toggle).
|
|
13
|
+
3. Click **Load unpacked** and select the unzipped folder.
|
|
14
14
|
|
|
15
15
|
### Method 2: Load Unpacked Source (For Developers)
|
|
16
16
|
|
|
@@ -8,9 +8,9 @@ OpenCLI 通过轻量级 **Browser Bridge** Chrome 扩展 + 微守护进程连接
|
|
|
8
8
|
|
|
9
9
|
### 方法 1:下载预构建版本(推荐)
|
|
10
10
|
|
|
11
|
-
1. 前往 GitHub [Releases 页面](https://github.com/jackwener/opencli/releases) 下载最新的 `opencli-extension.zip
|
|
12
|
-
2.
|
|
13
|
-
3.
|
|
11
|
+
1. 前往 GitHub [Releases 页面](https://github.com/jackwener/opencli/releases) 下载最新的 `opencli-extension.zip`。
|
|
12
|
+
2. 解压后打开 `chrome://extensions`,启用**开发者模式**。
|
|
13
|
+
3. 点击**加载已解压的扩展程序**,选择解压后的文件夹。
|
|
14
14
|
|
|
15
15
|
### 方法 2:加载源码(开发者)
|
|
16
16
|
|
|
@@ -6,7 +6,7 @@ const WS_RECONNECT_MAX_DELAY = 6e4;
|
|
|
6
6
|
|
|
7
7
|
const attached = /* @__PURE__ */ new Set();
|
|
8
8
|
function isDebuggableUrl$1(url) {
|
|
9
|
-
if (!url) return
|
|
9
|
+
if (!url) return true;
|
|
10
10
|
return !url.startsWith("chrome://") && !url.startsWith("chrome-extension://");
|
|
11
11
|
}
|
|
12
12
|
async function ensureAttached(tabId) {
|
|
@@ -36,6 +36,7 @@ async function ensureAttached(tabId) {
|
|
|
36
36
|
await chrome.debugger.attach({ tabId }, "1.3");
|
|
37
37
|
} catch (e) {
|
|
38
38
|
const msg = e instanceof Error ? e.message : String(e);
|
|
39
|
+
const hint = msg.includes("chrome-extension://") ? ". Tip: another Chrome extension may be interfering — try disabling other extensions" : "";
|
|
39
40
|
if (msg.includes("Another debugger is already attached")) {
|
|
40
41
|
try {
|
|
41
42
|
await chrome.debugger.detach({ tabId });
|
|
@@ -44,10 +45,10 @@ async function ensureAttached(tabId) {
|
|
|
44
45
|
try {
|
|
45
46
|
await chrome.debugger.attach({ tabId }, "1.3");
|
|
46
47
|
} catch {
|
|
47
|
-
throw new Error(`attach failed: ${msg}`);
|
|
48
|
+
throw new Error(`attach failed: ${msg}${hint}`);
|
|
48
49
|
}
|
|
49
50
|
} else {
|
|
50
|
-
throw new Error(`attach failed: ${msg}`);
|
|
51
|
+
throw new Error(`attach failed: ${msg}${hint}`);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
attached.add(tabId);
|
|
@@ -242,6 +243,7 @@ async function getAutomationWindow(workspace) {
|
|
|
242
243
|
automationSessions.set(workspace, session);
|
|
243
244
|
console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
|
|
244
245
|
resetWindowIdleTimer(workspace);
|
|
246
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
245
247
|
return session.windowId;
|
|
246
248
|
}
|
|
247
249
|
chrome.windows.onRemoved.addListener((windowId) => {
|
|
@@ -302,14 +304,13 @@ async function handleCommand(cmd) {
|
|
|
302
304
|
}
|
|
303
305
|
}
|
|
304
306
|
function isDebuggableUrl(url) {
|
|
305
|
-
if (!url) return
|
|
307
|
+
if (!url) return true;
|
|
306
308
|
return !url.startsWith("chrome://") && !url.startsWith("chrome-extension://");
|
|
307
309
|
}
|
|
308
310
|
async function resolveTabId(tabId, workspace) {
|
|
309
311
|
if (tabId !== void 0) {
|
|
310
312
|
try {
|
|
311
313
|
const tab = await chrome.tabs.get(tabId);
|
|
312
|
-
console.log(`[opencli] resolveTabId: explicit tabId=${tabId}, url=${tab.url}`);
|
|
313
314
|
if (isDebuggableUrl(tab.url)) return tabId;
|
|
314
315
|
console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
|
|
315
316
|
} catch {
|
|
@@ -319,11 +320,7 @@ async function resolveTabId(tabId, workspace) {
|
|
|
319
320
|
const windowId = await getAutomationWindow(workspace);
|
|
320
321
|
const tabs = await chrome.tabs.query({ windowId });
|
|
321
322
|
const debuggableTab = tabs.find((t) => t.id && isDebuggableUrl(t.url));
|
|
322
|
-
if (debuggableTab?.id)
|
|
323
|
-
console.log(`[opencli] resolveTabId: found debuggable tab ${debuggableTab.id} (${debuggableTab.url})`);
|
|
324
|
-
return debuggableTab.id;
|
|
325
|
-
}
|
|
326
|
-
console.warn(`[opencli] resolveTabId: no debuggable tabs found, tabs: ${tabs.map((t) => `${t.id}=${t.url}`).join(", ")}`);
|
|
323
|
+
if (debuggableTab?.id) return debuggableTab.id;
|
|
327
324
|
const reuseTab = tabs.find((t) => t.id);
|
|
328
325
|
if (reuseTab?.id) {
|
|
329
326
|
await chrome.tabs.update(reuseTab.id, { url: "data:text/html,<html></html>" });
|
|
@@ -331,12 +328,7 @@ async function resolveTabId(tabId, workspace) {
|
|
|
331
328
|
try {
|
|
332
329
|
const updated = await chrome.tabs.get(reuseTab.id);
|
|
333
330
|
if (isDebuggableUrl(updated.url)) return reuseTab.id;
|
|
334
|
-
console.warn(`[opencli]
|
|
335
|
-
await chrome.tabs.update(reuseTab.id, { url: "data:text/html,<html></html>" });
|
|
336
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
337
|
-
const updated2 = await chrome.tabs.get(reuseTab.id);
|
|
338
|
-
if (isDebuggableUrl(updated2.url)) return reuseTab.id;
|
|
339
|
-
console.warn(`[opencli] data: URI also intercepted, creating fresh tab`);
|
|
331
|
+
console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`);
|
|
340
332
|
} catch {
|
|
341
333
|
}
|
|
342
334
|
}
|
package/extension/manifest.json
CHANGED
package/extension/package.json
CHANGED
|
@@ -88,7 +88,7 @@ function scheduleReconnect(): void {
|
|
|
88
88
|
// ─── Automation window isolation ─────────────────────────────────────
|
|
89
89
|
// All opencli operations happen in a dedicated Chrome window so the
|
|
90
90
|
// user's active browsing session is never touched.
|
|
91
|
-
// The window auto-closes after
|
|
91
|
+
// The window auto-closes after 120s of idle (no commands).
|
|
92
92
|
|
|
93
93
|
type AutomationSession = {
|
|
94
94
|
windowId: number;
|
|
@@ -152,6 +152,8 @@ async function getAutomationWindow(workspace: string): Promise<number> {
|
|
|
152
152
|
automationSessions.set(workspace, session);
|
|
153
153
|
console.log(`[opencli] Created automation window ${session.windowId} (${workspace})`);
|
|
154
154
|
resetWindowIdleTimer(workspace);
|
|
155
|
+
// Brief delay to let Chrome load the initial data: URI tab
|
|
156
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
155
157
|
return session.windowId;
|
|
156
158
|
}
|
|
157
159
|
|
|
@@ -229,7 +231,7 @@ async function handleCommand(cmd: Command): Promise<Result> {
|
|
|
229
231
|
|
|
230
232
|
/** Check if a URL can be attached via CDP (not chrome:// or chrome-extension://) */
|
|
231
233
|
function isDebuggableUrl(url?: string): boolean {
|
|
232
|
-
if (!url) return
|
|
234
|
+
if (!url) return true; // empty/undefined = tab still loading, allow it
|
|
233
235
|
return !url.startsWith('chrome://') && !url.startsWith('chrome-extension://');
|
|
234
236
|
}
|
|
235
237
|
|
|
@@ -245,7 +247,6 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
|
|
|
245
247
|
if (tabId !== undefined) {
|
|
246
248
|
try {
|
|
247
249
|
const tab = await chrome.tabs.get(tabId);
|
|
248
|
-
console.log(`[opencli] resolveTabId: explicit tabId=${tabId}, url=${tab.url}`);
|
|
249
250
|
if (isDebuggableUrl(tab.url)) return tabId;
|
|
250
251
|
// Tab exists but URL is not debuggable — fall through to auto-resolve
|
|
251
252
|
console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
|
|
@@ -258,42 +259,27 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi
|
|
|
258
259
|
// Get (or create) the automation window
|
|
259
260
|
const windowId = await getAutomationWindow(workspace);
|
|
260
261
|
|
|
261
|
-
// Prefer an existing debuggable tab
|
|
262
|
+
// Prefer an existing debuggable tab
|
|
262
263
|
const tabs = await chrome.tabs.query({ windowId });
|
|
263
264
|
const debuggableTab = tabs.find(t => t.id && isDebuggableUrl(t.url));
|
|
264
|
-
if (debuggableTab?.id)
|
|
265
|
-
console.log(`[opencli] resolveTabId: found debuggable tab ${debuggableTab.id} (${debuggableTab.url})`);
|
|
266
|
-
return debuggableTab.id;
|
|
267
|
-
}
|
|
268
|
-
console.warn(`[opencli] resolveTabId: no debuggable tabs found, tabs: ${tabs.map(t => `${t.id}=${t.url}`).join(', ')}`);
|
|
265
|
+
if (debuggableTab?.id) return debuggableTab.id;
|
|
269
266
|
|
|
270
|
-
// No debuggable tab
|
|
271
|
-
//
|
|
272
|
-
// Reuse the first existing tab by navigating it to about:blank (avoids
|
|
273
|
-
// accumulating orphan tabs if chrome.tabs.create is also intercepted).
|
|
267
|
+
// No debuggable tab — another extension may have hijacked the tab URL.
|
|
268
|
+
// Try to reuse by navigating to a data: URI (not interceptable by New Tab Override).
|
|
274
269
|
const reuseTab = tabs.find(t => t.id);
|
|
275
270
|
if (reuseTab?.id) {
|
|
276
271
|
await chrome.tabs.update(reuseTab.id, { url: 'data:text/html,<html></html>' });
|
|
277
|
-
// Wait for the navigation to take effect
|
|
278
272
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
279
|
-
// Verify the URL is actually debuggable (New Tab Override may have intercepted)
|
|
280
273
|
try {
|
|
281
274
|
const updated = await chrome.tabs.get(reuseTab.id);
|
|
282
275
|
if (isDebuggableUrl(updated.url)) return reuseTab.id;
|
|
283
|
-
|
|
284
|
-
console.warn(`[opencli] about:blank was intercepted (${updated.url}), trying data: URI`);
|
|
285
|
-
await chrome.tabs.update(reuseTab.id, { url: 'data:text/html,<html></html>' });
|
|
286
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
287
|
-
const updated2 = await chrome.tabs.get(reuseTab.id);
|
|
288
|
-
if (isDebuggableUrl(updated2.url)) return reuseTab.id;
|
|
289
|
-
// data: URI also intercepted — create a brand new tab
|
|
290
|
-
console.warn(`[opencli] data: URI also intercepted, creating fresh tab`);
|
|
276
|
+
console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`);
|
|
291
277
|
} catch {
|
|
292
278
|
// Tab was closed during navigation
|
|
293
279
|
}
|
|
294
280
|
}
|
|
295
281
|
|
|
296
|
-
//
|
|
282
|
+
// Fallback: create a new tab
|
|
297
283
|
const newTab = await chrome.tabs.create({ windowId, url: 'data:text/html,<html></html>', active: true });
|
|
298
284
|
if (!newTab.id) throw new Error('Failed to create tab in automation window');
|
|
299
285
|
return newTab.id;
|
package/extension/src/cdp.ts
CHANGED
|
@@ -10,7 +10,7 @@ const attached = new Set<number>();
|
|
|
10
10
|
|
|
11
11
|
/** Check if a URL can be attached via CDP */
|
|
12
12
|
function isDebuggableUrl(url?: string): boolean {
|
|
13
|
-
if (!url) return
|
|
13
|
+
if (!url) return true; // empty/undefined = tab still loading, allow it
|
|
14
14
|
return !url.startsWith('chrome://') && !url.startsWith('chrome-extension://');
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -47,15 +47,18 @@ async function ensureAttached(tabId: number): Promise<void> {
|
|
|
47
47
|
await chrome.debugger.attach({ tabId }, '1.3');
|
|
48
48
|
} catch (e: unknown) {
|
|
49
49
|
const msg = e instanceof Error ? e.message : String(e);
|
|
50
|
+
const hint = msg.includes('chrome-extension://')
|
|
51
|
+
? '. Tip: another Chrome extension may be interfering — try disabling other extensions'
|
|
52
|
+
: '';
|
|
50
53
|
if (msg.includes('Another debugger is already attached')) {
|
|
51
54
|
try { await chrome.debugger.detach({ tabId }); } catch { /* ignore */ }
|
|
52
55
|
try {
|
|
53
56
|
await chrome.debugger.attach({ tabId }, '1.3');
|
|
54
57
|
} catch {
|
|
55
|
-
throw new Error(`attach failed: ${msg}`);
|
|
58
|
+
throw new Error(`attach failed: ${msg}${hint}`);
|
|
56
59
|
}
|
|
57
60
|
} else {
|
|
58
|
-
throw new Error(`attach failed: ${msg}`);
|
|
61
|
+
throw new Error(`attach failed: ${msg}${hint}`);
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
attached.add(tabId);
|