Skip to content
Merged
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 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

* **browser session model** — replace the browser-facing `--workspace` model with explicit `--session <name>` on `opencli browser *`. Browser commands now require a session name, `browser bind`/`unbind` use `--session`, and bind no longer accepts `--domain`, `--path-prefix`, or `--allow-navigate-bound`. Browser primitives keep their session tab by design; the browser namespace no longer exposes `--keep-tab`.
* **adapter site sessions** — replace adapter metadata `browserSession: { reuse: 'site' }` with `siteSession: 'persistent'`, and replace the user override `--reuse <none|site>` / `OPENCLI_BROWSER_REUSE` with `--site-session <ephemeral|persistent>`. Persistent site sessions keep a stable site tab open without idle expiry.
* **doctor** — remove `--no-live` and `--sessions` flags from `opencli doctor`. Doctor always runs the live browser connectivity probe (that's its core job); session enumeration was never part of health diagnosis. The underlying `'sessions'` daemon protocol action and the `BrowserSessionInfo` public type are removed as dead code.

### Internal

* **extension 1.0.12** — drop `handleSessions` action handler (no remaining consumers after doctor cleanup).
* **extension 1.0.11** — switch Browser Bridge lease routing from user-facing workspaces to explicit browser sessions.

## [1.7.16](https://github.com/jackwener/opencli/compare/v1.7.15...v1.7.16) (2026-05-11)
Expand Down
23 changes: 2 additions & 21 deletions extension/dist/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,8 @@ function getSessionFromKey(key) {
function getIdleTimeout(key) {
const session = automationSessions.get(key);
if (session?.kind === "bound") return IDLE_TIMEOUT_NONE;
if (getSurfaceFromKey(key) === "adapter" && (session?.lifecycle === "persistent" || sessionLifecycleOverrides.get(key) === "persistent")) return IDLE_TIMEOUT_NONE;
const adapterPersistent = getSurfaceFromKey(key) === "adapter" && (session?.lifecycle === "persistent" || sessionLifecycleOverrides.get(key) === "persistent");
if (adapterPersistent) return IDLE_TIMEOUT_NONE;
const override = sessionTimeoutOverrides.get(key);
if (override !== void 0) return override;
return getSurfaceFromKey(key) === "browser" ? IDLE_TIMEOUT_INTERACTIVE : IDLE_TIMEOUT_DEFAULT;
Expand Down Expand Up @@ -1292,8 +1293,6 @@ async function handleCommand(cmd) {
return await handleCloseWindow(cmd, leaseKey);
case "cdp":
return await handleCdp(cmd, leaseKey);
case "sessions":
return await handleSessions(cmd);
case "set-file-input":
return await handleSetFileInput(cmd, leaseKey);
case "insert-text":
Expand Down Expand Up @@ -1971,24 +1970,6 @@ async function reconcileTargetLeaseRegistry() {
}
await persistRuntimeState();
}
async function handleSessions(cmd) {
const now = Date.now();
const data = await Promise.all([...automationSessions.entries()].map(async ([leaseKey, session]) => ({
session: session.session,
windowId: session.windowId,
owned: session.owned,
kind: session.kind,
surface: session.surface,
preferredTabId: session.preferredTabId,
contextId: session.contextId,
ownership: session.ownership,
lifecycle: session.lifecycle,
windowRole: session.windowRole,
tabCount: session.preferredTabId !== null ? await chrome.tabs.get(session.preferredTabId).then((tab) => isDebuggableUrl(tab.url) ? 1 : 0).catch(() => 0) : (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isDebuggableUrl(tab.url)).length,
idleMsRemaining: session.idleDeadlineAt <= 0 ? null : Math.max(0, session.idleDeadlineAt - now)
})));
return { id: cmd.id, ok: true, data };
}
async function handleBind(cmd, leaseKey) {
const existing = automationSessions.get(leaseKey);
if (existing?.owned) {
Expand Down
2 changes: 1 addition & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "OpenCLI",
"version": "1.0.11",
"version": "1.0.12",
"description": "Browser automation bridge for the OpenCLI CLI tool. Executes commands in Chrome tab leases via a local daemon.",
"permissions": [
"debugger",
Expand Down
2 changes: 1 addition & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencli-extension",
"version": "1.0.11",
"version": "1.0.12",
"private": true,
"opencli": {
"compatRange": ">=1.7.0"
Expand Down
21 changes: 3 additions & 18 deletions extension/src/background.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,22 +568,6 @@ describe('background tab isolation', () => {
expect(mod.__test__.isTargetUrl('https://example.com/app/', 'https://example.com/app')).toBe(false);
});

it('reports sessions per session', async () => {
const { chrome } = createChromeMock();
vi.stubGlobal('chrome', chrome);

const mod = await import('./background');
mod.__test__.setAutomationWindowId(adapterKey('twitter'), 1);
mod.__test__.setAutomationWindowId(adapterKey('zhihu'), 2);

const result = await mod.__test__.handleSessions({ id: '3', action: 'sessions' });
expect(result.ok).toBe(true);
expect(result.data).toEqual(expect.arrayContaining([
expect.objectContaining({ session: 'twitter', surface: 'adapter', windowId: 1 }),
expect.objectContaining({ session: 'zhihu', surface: 'adapter', windowId: 2 }),
]));
});

it('returns the persisted profile contextId from popup status', async () => {
const { chrome } = createChromeMock();
await chrome.storage.local.set({ opencli_context_id_v1: 'abc123xy' });
Expand Down Expand Up @@ -1292,11 +1276,12 @@ describe('background tab isolation', () => {
// Default for browser:* is 10 min
expect(mod.__test__.getIdleTimeout(browserKey('default'))).toBe(600_000);

// Send a command with custom idleTimeout (in seconds)
// Send a benign command with custom idleTimeout (in seconds)
await mod.__test__.handleCommand({
id: 'custom-1',
action: 'sessions',
action: 'cookies',
session: browserKey('default'),
domain: 'example.com',
idleTimeout: 120,
});

Expand Down
24 changes: 0 additions & 24 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,8 +867,6 @@ async function handleCommand(cmd: Command): Promise<Result> {
return await handleCloseWindow(cmd, leaseKey);
case 'cdp':
return await handleCdp(cmd, leaseKey);
case 'sessions':
return await handleSessions(cmd);
case 'set-file-input':
return await handleSetFileInput(cmd, leaseKey);
case 'insert-text':
Expand Down Expand Up @@ -1667,27 +1665,6 @@ async function reconcileTargetLeaseRegistry(): Promise<void> {
await persistRuntimeState();
}

async function handleSessions(cmd: Command): Promise<Result> {
const now = Date.now();
const data = await Promise.all([...automationSessions.entries()].map(async ([leaseKey, session]) => ({
session: session.session,
windowId: session.windowId,
owned: session.owned,
kind: session.kind,
surface: session.surface,
preferredTabId: session.preferredTabId,
contextId: session.contextId,
ownership: session.ownership,
lifecycle: session.lifecycle,
windowRole: session.windowRole,
tabCount: session.preferredTabId !== null
? (await chrome.tabs.get(session.preferredTabId).then((tab) => isDebuggableUrl(tab.url) ? 1 : 0).catch(() => 0))
: (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isDebuggableUrl(tab.url)).length,
idleMsRemaining: session.idleDeadlineAt <= 0 ? null : Math.max(0, session.idleDeadlineAt - now),
})));
return { id: cmd.id, ok: true, data };
}

async function handleBind(cmd: Command, leaseKey: string): Promise<Result> {
const existing = automationSessions.get(leaseKey);
if (existing?.owned) {
Expand Down Expand Up @@ -1734,7 +1711,6 @@ export const __test__ = {
handleNavigate,
isTargetUrl,
handleTabs,
handleSessions,
handleBind,
resolveTabId,
resetWindowIdleTimer,
Expand Down
2 changes: 1 addition & 1 deletion skills/opencli-usage/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ cd OpenCLI && npm install
npx tsx src/main.ts <command> # same surface, no global install
```

`opencli doctor` prints a structured `DoctorReport` — daemon status, extension connection, version checks. Scope is narrow: it diagnoses the **browser bridge** (daemon + extension + Chrome wiring). `PUBLIC` / `LOCAL` adapters, `opencli list`, `validate`, `verify`, plugin commands, and external-CLI passthrough don't need it to be green — only `COOKIE` / `INTERCEPT` / `UI` adapters and the `opencli browser *` subcommands do. Flags: `--no-live` (skip live browser test), `--sessions` (list active automation sessions), `-v` (verbose).
`opencli doctor` prints a structured `DoctorReport` — daemon status, extension connection, version checks, and a live browser connectivity probe. Scope is narrow: it diagnoses the **browser bridge** (daemon + extension + Chrome wiring). `PUBLIC` / `LOCAL` adapters, `opencli list`, `validate`, `verify`, plugin commands, and external-CLI passthrough don't need it to be green — only `COOKIE` / `INTERCEPT` / `UI` adapters and the `opencli browser *` subcommands do. Flag: `-v` (verbose).

## Prerequisites by command type

Expand Down
8 changes: 1 addition & 7 deletions src/browser/daemon-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

import { DEFAULT_DAEMON_PORT } from '../constants.js';
import type { BrowserSessionInfo } from '../types.js';
import { sleep } from '../utils.js';
import { classifyBrowserError } from './errors.js';
import { resolveProfileContextId } from './profile.js';
Expand All @@ -22,7 +21,7 @@ function generateId(): string {

export interface DaemonCommand {
id: string;
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'wait-download' | 'cdp' | 'frames';
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'wait-download' | 'cdp' | 'frames';
/** Target page identity (targetId). Cross-layer contract with the extension. */
page?: string;
code?: string;
Expand Down Expand Up @@ -248,11 +247,6 @@ export async function sendCommandFull(
return { data: result.data, page: result.page };
}

export async function listSessions(opts?: { contextId?: string }): Promise<BrowserSessionInfo[]> {
const result = await sendCommand('sessions', { ...(opts?.contextId && { contextId: opts.contextId }) });
return Array.isArray(result) ? result : [];
}

export async function bindTab(session: string, opts: { contextId?: string } = {}): Promise<unknown> {
return sendCommand('bind', { session, surface: 'browser', ...opts });
}
4 changes: 1 addition & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2755,13 +2755,11 @@ cli({
program
.command('doctor')
.description('Diagnose opencli browser bridge connectivity')
.option('--no-live', 'Skip live browser connectivity test')
.option('--sessions', 'Show active automation sessions', false)
.option('-v, --verbose', 'Debug output')
.action(async (opts) => {
applyVerbose(opts);
const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION });
const report = await runBrowserDoctor({ cliVersion: PKG_VERSION });
console.log(renderBrowserDoctorReport(report));
});

Expand Down
Loading