Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,136 changes: 1,078 additions & 1,058 deletions extension/dist/background.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"tabs",
"cookies",
"activeTab",
"alarms"
"alarms",
"storage"
],
"host_permissions": [
"<all_urls>"
Expand Down
52 changes: 52 additions & 0 deletions extension/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,50 @@
border-radius: 3px;
font-size: 11px;
}
.port-section {
margin-top: 12px;
padding: 10px 12px;
border-radius: 8px;
background: #f5f5f5;
}
.port-section label {
font-size: 11px;
color: #666;
display: block;
margin-bottom: 6px;
}
.port-row {
display: flex;
gap: 6px;
align-items: center;
}
.port-row input {
flex: 1;
padding: 5px 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
font-family: inherit;
outline: none;
}
.port-row input:focus { border-color: #007aff; }
.port-row button {
padding: 5px 12px;
border: none;
border-radius: 4px;
background: #007aff;
color: #fff;
font-size: 12px;
cursor: pointer;
}
.port-row button:hover { background: #006ae0; }
.port-msg {
font-size: 11px;
margin-top: 4px;
height: 16px;
}
.port-msg.ok { color: #34c759; }
.port-msg.err { color: #ff3b30; }
.footer {
margin-top: 14px;
text-align: center;
Expand All @@ -76,6 +120,14 @@ <h1>OpenCLI</h1>
<div class="hint" id="hint">
This is normal. The extension connects automatically when you run any <code>opencli</code> command.
</div>
<div class="port-section">
<label>Daemon Port</label>
<div class="port-row">
<input type="number" id="portInput" min="1024" max="65535" placeholder="19825">
<button id="portSave">Save</button>
</div>
<div class="port-msg" id="portMsg"></div>
</div>
<div class="footer">
<a href="https://github.com/jackwener/opencli" target="_blank">Documentation</a>
</div>
Expand Down
31 changes: 28 additions & 3 deletions extension/popup.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
const DEFAULT_PORT = 19825;

// Load saved port into input
chrome.storage.local.get('daemonPort', (result) => {
document.getElementById('portInput').value = result.daemonPort || DEFAULT_PORT;
});

// Save port
document.getElementById('portSave').addEventListener('click', () => {
const input = document.getElementById('portInput');
const msg = document.getElementById('portMsg');
const val = parseInt(input.value, 10);
if (!Number.isInteger(val) || val < 1024 || val > 65535) {
msg.textContent = 'Port must be 1024-65535';
msg.className = 'port-msg err';
return;
}
chrome.storage.local.set({ daemonPort: val }, () => {
msg.textContent = 'Saved';
msg.className = 'port-msg ok';
setTimeout(() => { msg.textContent = ''; }, 2000);
});
});

// Query connection status from background service worker
chrome.runtime.sendMessage({ type: 'getStatus' }, (resp) => {
const dot = document.getElementById('dot');
Expand All @@ -9,17 +33,18 @@ chrome.runtime.sendMessage({ type: 'getStatus' }, (resp) => {
hint.style.display = 'block';
return;
}
const portLabel = resp.port ? ` <span style="color:#999">(port ${resp.port})</span>` : '';
if (resp.connected) {
dot.className = 'dot connected';
status.innerHTML = '<strong>Connected to daemon</strong>';
status.innerHTML = `<strong>Connected to daemon</strong>${portLabel}`;
hint.style.display = 'none';
} else if (resp.reconnecting) {
dot.className = 'dot connecting';
status.innerHTML = '<strong>Reconnecting...</strong>';
status.innerHTML = `<strong>Reconnecting...</strong>${portLabel}`;
hint.style.display = 'none';
} else {
dot.className = 'dot disconnected';
status.innerHTML = '<strong>No daemon connected</strong>';
status.innerHTML = `<strong>No daemon connected</strong>${portLabel}`;
hint.style.display = 'block';
}
});
7 changes: 7 additions & 0 deletions extension/src/background.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ function createChromeMock() {
cookies: {
getAll: vi.fn(async () => []),
},
storage: {
local: {
get: vi.fn((_key: string, cb: (result: Record<string, unknown>) => void) => cb({})),
set: vi.fn((_items: Record<string, unknown>, cb?: () => void) => cb?.()),
},
onChanged: { addListener: vi.fn() } as Listener<(changes: Record<string, unknown>, areaName: string) => void>,
},
};

return { chrome, tabs, query, create, update };
Expand Down
51 changes: 47 additions & 4 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,51 @@
declare const __OPENCLI_COMPAT_RANGE__: string;

import type { Command, Result } from './protocol';
import { DAEMON_WS_URL, DAEMON_PING_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
import { DAEMON_PORT, DAEMON_HOST, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
import * as executor from './cdp';
import * as identity from './identity';

// ─── Dynamic daemon port ────────────────────────────────────────────────
// Each Chrome profile has independent chrome.storage.local, so different
// profiles can target different daemon ports for multi-profile support.

let daemonPort: number = DAEMON_PORT;

function getDaemonWsUrl(): string {
return `ws://${DAEMON_HOST}:${daemonPort}/ext`;
}

function getDaemonPingUrl(): string {
return `http://${DAEMON_HOST}:${daemonPort}/ping`;
}

function loadDaemonPort(): Promise<void> {
return new Promise((resolve) => {
chrome.storage.local.get('daemonPort', (result) => {
if (result.daemonPort && Number.isInteger(result.daemonPort) && result.daemonPort >= 1024 && result.daemonPort <= 65535) {
daemonPort = result.daemonPort;
}
resolve();
});
});
}

chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local' || !changes.daemonPort) return;
const newPort = changes.daemonPort.newValue;
if (newPort && Number.isInteger(newPort) && newPort >= 1024 && newPort <= 65535) {
daemonPort = newPort;
} else {
daemonPort = DAEMON_PORT;
}
if (ws) {
ws.close();
ws = null;
}
reconnectAttempts = 0;
void connect();
});

let ws: WebSocket | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let reconnectAttempts = 0;
Expand Down Expand Up @@ -48,14 +89,14 @@ async function connect(): Promise<void> {
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;

try {
const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1000) });
const res = await fetch(getDaemonPingUrl(), { 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);
ws = new WebSocket(getDaemonWsUrl());
} catch {
scheduleReconnect();
return;
Expand Down Expand Up @@ -262,12 +303,13 @@ chrome.tabs.onRemoved.addListener((tabId) => {

let initialized = false;

function initialize(): void {
async function initialize(): Promise<void> {
if (initialized) return;
initialized = true;
chrome.alarms.create('keepalive', { periodInMinutes: 0.4 }); // ~24 seconds
executor.registerListeners();
executor.registerFrameTracking();
await loadDaemonPort();
void connect();
console.log('[opencli] OpenCLI extension initialized');
}
Expand All @@ -291,6 +333,7 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
sendResponse({
connected: ws?.readyState === WebSocket.OPEN,
reconnecting: reconnectTimer !== null,
port: daemonPort,
});
}
return false;
Expand Down
5 changes: 2 additions & 3 deletions src/browser/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import * as fs from 'node:fs';
import type { IPage } from '../types.js';
import type { IBrowserFactory } from '../runtime.js';
import { Page } from './page.js';
import { getDaemonHealth, requestDaemonShutdown } from './daemon-client.js';
import { DEFAULT_DAEMON_PORT } from '../constants.js';
import { getDaemonHealth, requestDaemonShutdown, getEffectiveDaemonPort } from './daemon-client.js';
import { BrowserConnectError } from '../errors.js';
import { PKG_VERSION } from '../version.js';

Expand Down Expand Up @@ -154,7 +153,7 @@ export class BrowserBridge implements IBrowserFactory {

throw new BrowserConnectError(
'Failed to start opencli daemon',
`Try running manually:\n node ${daemonPath}\nMake sure port ${DEFAULT_DAEMON_PORT} is available.`,
`Try running manually:\n node ${daemonPath}\nMake sure port ${getEffectiveDaemonPort()} is available.`,
'daemon-not-running',
);
}
Expand Down
6 changes: 5 additions & 1 deletion src/browser/daemon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import type { BrowserSessionInfo } from '../types.js';
import { sleep } from '../utils.js';
import { classifyBrowserError } from './errors.js';

const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
export function getEffectiveDaemonPort(): number {
return parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
}

const DAEMON_PORT = getEffectiveDaemonPort();
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
const OPENCLI_HEADERS = { 'X-OpenCLI': '1' };

Expand Down
6 changes: 5 additions & 1 deletion src/browser/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import { BrowserConnectError, type BrowserConnectKind } from '../errors.js';
import { DEFAULT_DAEMON_PORT } from '../constants.js';

function getEffectiveDaemonPort(): number {
return parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
}

/**
* Unified browser error classification.
*
Expand Down Expand Up @@ -102,7 +106,7 @@ export function formatBrowserConnectError(kind: ConnectFailureKind, detail?: str
case 'daemon-not-running':
return new BrowserConnectError(
'Cannot connect to opencli daemon.' + (detail ? `\n\n${detail}` : ''),
`The daemon should auto-start. If it keeps failing, make sure port ${DEFAULT_DAEMON_PORT} is available.`,
`The daemon should auto-start. If it keeps failing, make sure port ${getEffectiveDaemonPort()} is available.`,
kind,
);
case 'extension-not-connected':
Expand Down
4 changes: 3 additions & 1 deletion src/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export type DoctorReport = {
extensionVersion?: string;
latestExtensionVersion?: string;
connectivity?: ConnectivityResult;
port?: number;
sessions?: Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>;
issues: string[];
};
Expand Down Expand Up @@ -204,6 +205,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
extensionFlaky,
extensionVersion,
latestExtensionVersion,
port: health.status?.port,
connectivity,
sessions,
issues,
Expand All @@ -219,7 +221,7 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
: report.daemonRunning ? styleText('green', '[OK]') : styleText('red', '[MISSING]');
const daemonLabel = report.daemonFlaky
? 'unstable (running during live check, then stopped)'
: report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` + (report.daemonVersion ? ` (v${report.daemonVersion})` : '') : 'not running';
: report.daemonRunning ? `running on port ${report.port ?? DEFAULT_DAEMON_PORT}` + (report.daemonVersion ? ` (v${report.daemonVersion})` : '') : 'not running';
lines.push(`${daemonIcon} Daemon: ${daemonLabel}`);

// Extension status
Expand Down