From 2a63526b5d0e9c196b3a3614904c25091a919d3a Mon Sep 17 00:00:00 2001 From: Yelban Date: Mon, 6 Apr 2026 16:49:06 +0800 Subject: [PATCH 1/2] fix(extension): probe daemon via HTTP before WebSocket to suppress ERR_CONNECTION_REFUSED When the daemon is not running, `new WebSocket()` triggers a `net::ERR_CONNECTION_REFUSED` error that Chrome logs to the extension's error page before any JS handler can intercept it. This makes the extension appear broken even though it is functioning normally. Fix: gate the WebSocket connection on a `fetch(/ping)` probe. Unlike WebSocket construction, fetch failures are silently catchable. If the daemon is not reachable, we skip the WebSocket attempt entirely and let the keepalive alarm retry later. Also cap eager reconnect attempts at 6 (reaching 60s backoff), then delegate to the keepalive alarm (~24s) to reduce console noise when the daemon is intentionally stopped. Co-Authored-By: Claude Opus 4.6 (1M context) --- extension/src/background.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/extension/src/background.ts b/extension/src/background.ts index 5598273..90d5b01 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -8,7 +8,7 @@ */ import type { Command, Result } from './protocol'; -import { DAEMON_WS_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol'; +import { DAEMON_WS_URL, DAEMON_HTTP_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol'; import * as executor from './cdp'; let ws: WebSocket | null = null; @@ -36,9 +36,23 @@ console.error = (...args: unknown[]) => { _origError(...args); forwardLog('error // ─── WebSocket connection ──────────────────────────────────────────── -function connect(): void { +/** + * Probe the daemon via its /ping HTTP endpoint before attempting a WebSocket + * connection. fetch() failures are silently catchable; new WebSocket() is not + * — Chrome logs ERR_CONNECTION_REFUSED to the extension error page before any + * JS handler can intercept it. By gating on the probe, we avoid noisy errors + * when the daemon is simply not running yet. + */ +async function connect(): Promise { if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return; + try { + const res = await fetch(`${DAEMON_HTTP_URL}/ping`, { signal: AbortSignal.timeout(1000) }); + if (!res.ok) return; // unexpected response — not our daemon + } catch { + return; // daemon not running — skip WebSocket to avoid console noise + } + try { ws = new WebSocket(DAEMON_WS_URL); } catch { @@ -76,10 +90,17 @@ function connect(): void { }; } +/** + * After MAX_EAGER_ATTEMPTS (reaching 60s backoff), stop scheduling reconnects. + * The keepalive alarm (~24s) will still call connect() periodically, but at a + * much lower frequency — reducing console noise when the daemon is not running. + */ +const MAX_EAGER_ATTEMPTS = 6; // 2s, 4s, 8s, 16s, 32s, 60s — then stop + function scheduleReconnect(): void { if (reconnectTimer) return; reconnectAttempts++; - // Exponential backoff: 2s, 4s, 8s, 16s, ..., capped at 60s + if (reconnectAttempts > MAX_EAGER_ATTEMPTS) return; // let keepalive alarm handle it const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY); reconnectTimer = setTimeout(() => { reconnectTimer = null; From 46fbb5124c3de77252e0c8967302cf42b0b89541 Mon Sep 17 00:00:00 2001 From: Yelban Date: Mon, 6 Apr 2026 17:02:10 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore(extension):=20rename=20OpenCLI=20?= =?UTF-8?q?=E2=86=92=20AutoCLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update extension name, description, homepage URL, package name, log tags ([opencli] → [autocli]), and JSDoc to reflect the opencli-rs → AutoCLI rename. Co-Authored-By: Claude Opus 4.6 (1M context) --- extension/manifest.json | 8 ++++---- extension/package.json | 2 +- extension/src/background.ts | 22 +++++++++++----------- extension/src/protocol.ts | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/extension/manifest.json b/extension/manifest.json index 86da887..030bd35 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, - "name": "OpenCLI", + "name": "AutoCLI", "version": "1.2.6", - "description": "Bridge between opencli CLI and your browser — execute commands, read cookies, manage tabs.", + "description": "Bridge between AutoCLI and your browser — execute commands, read cookies, manage tabs.", "permissions": [ "debugger", "tabs", @@ -21,11 +21,11 @@ "128": "icons/icon-128.png" }, "action": { - "default_title": "OpenCLI", + "default_title": "AutoCLI", "default_icon": { "16": "icons/icon-16.png", "32": "icons/icon-32.png" } }, - "homepage_url": "https://github.com/jackwener/opencli" + "homepage_url": "https://github.com/nashsu/AutoCLI" } diff --git a/extension/package.json b/extension/package.json index 9d6abd8..606fe0d 100644 --- a/extension/package.json +++ b/extension/package.json @@ -1,5 +1,5 @@ { - "name": "opencli-extension", + "name": "autocli-extension", "version": "1.2.6", "private": true, "type": "module", diff --git a/extension/src/background.ts b/extension/src/background.ts index 90d5b01..c36e5f9 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -1,9 +1,9 @@ // Based on OpenCLI (https://github.com/jackwener/opencli) by jackwener // Licensed under Apache-2.0. Modified for AutoCLI. /** - * OpenCLI — Service Worker (background script). + * AutoCLI — Service Worker (background script). * - * Connects to the opencli daemon via WebSocket, receives commands, + * Connects to the AutoCLI daemon via WebSocket, receives commands, * dispatches them to Chrome APIs (debugger/tabs/cookies), returns results. */ @@ -61,7 +61,7 @@ async function connect(): Promise { } ws.onopen = () => { - console.log('[opencli] Connected to daemon'); + console.log('[autocli] Connected to daemon'); reconnectAttempts = 0; // Reset on successful connection if (reconnectTimer) { clearTimeout(reconnectTimer); @@ -75,12 +75,12 @@ async function connect(): Promise { const result = await handleCommand(command); ws?.send(JSON.stringify(result)); } catch (err) { - console.error('[opencli] Message handling error:', err); + console.error('[autocli] Message handling error:', err); } }; ws.onclose = () => { - console.log('[opencli] Disconnected from daemon'); + console.log('[autocli] Disconnected from daemon'); ws = null; scheduleReconnect(); }; @@ -109,7 +109,7 @@ function scheduleReconnect(): void { } // ─── Automation window isolation ───────────────────────────────────── -// All opencli operations happen in a dedicated Chrome window so the +// All AutoCLI operations happen in a dedicated Chrome window so the // user's active browsing session is never touched. // The window auto-closes after 120s of idle (no commands). @@ -201,7 +201,7 @@ function initialize(): void { chrome.alarms.create('keepalive', { periodInMinutes: 0.4 }); // ~24 seconds executor.registerListeners(); connect(); - console.log('[opencli] OpenCLI extension initialized'); + console.log('[autocli] AutoCLI extension initialized'); } chrome.runtime.onInstalled.addListener(() => { @@ -272,10 +272,10 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi const tab = await chrome.tabs.get(tabId); if (isDebuggableUrl(tab.url)) return tabId; // Tab exists but URL is not debuggable — fall through to auto-resolve - console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`); + console.warn(`[autocli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`); } catch { // Tab was closed — fall through to auto-resolve - console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`); + console.warn(`[autocli] Tab ${tabId} no longer exists, re-resolving`); } } @@ -296,7 +296,7 @@ async function resolveTabId(tabId: number | undefined, workspace: string): Promi try { const updated = await chrome.tabs.get(reuseTab.id); if (isDebuggableUrl(updated.url)) return reuseTab.id; - console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`); + console.warn(`[autocli] data: URI was intercepted (${updated.url}), creating fresh tab`); } catch { // Tab was closed during navigation } @@ -391,7 +391,7 @@ async function handleNavigate(cmd: Command, workspace: string): Promise setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); timedOut = true; - console.warn(`[opencli] Navigate to ${targetUrl} timed out after 15s`); + console.warn(`[autocli] Navigate to ${targetUrl} timed out after 15s`); resolve(); }, 15000); }); diff --git a/extension/src/protocol.ts b/extension/src/protocol.ts index c869b02..4b10bff 100644 --- a/extension/src/protocol.ts +++ b/extension/src/protocol.ts @@ -1,7 +1,7 @@ // Based on OpenCLI (https://github.com/jackwener/opencli) by jackwener // Licensed under Apache-2.0. Modified for AutoCLI. /** - * opencli browser protocol — shared types between daemon, extension, and CLI. + * AutoCLI browser protocol — shared types between daemon, extension, and CLI. * * 5 actions: exec, navigate, tabs, cookies, screenshot. * Everything else is just JS code sent via 'exec'.