Files
opencli/src/browser/daemon-client.ts
T
jackwener b2fa7daf57 feat: replace @playwright/mcp with lightweight daemon + Chrome Extension
Architecture:
- Micro-daemon (HTTP + WebSocket bridge, ~190 lines, auto-start/idle-exit)
- Chrome MV3 Extension using chrome.debugger CDP (10KB build)
- 5 protocol actions: exec, navigate, tabs, cookies, screenshot
- All DOM ops via JS evaluate — no extension update needed for new features

Key features:
- CDP Runtime.evaluate for JS execution in page context
- Tab management, cookie access via Chrome APIs
- Auto-start daemon on cold boot, idle auto-exit (5min)
- Minimal permissions: debugger, tabs, cookies, activeTab, alarms

Tested: zhihu hot (14.3s), twitter timeline (9.3s)
2026-03-19 16:21:17 +08:00

90 lines
2.2 KiB
TypeScript

/**
* HTTP client for communicating with the opencli daemon.
*
* Provides a typed send() function that posts a Command and returns a Result.
*/
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? '19825', 10);
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
let _idCounter = 0;
function generateId(): string {
return `cmd_${Date.now()}_${++_idCounter}`;
}
export interface DaemonCommand {
id: string;
action: 'exec' | 'navigate' | 'tabs' | 'cookies';
tabId?: number;
code?: string;
url?: string;
op?: string;
index?: number;
domain?: string;
}
export interface DaemonResult {
id: string;
ok: boolean;
data?: unknown;
error?: string;
}
/**
* Check if daemon is running.
*/
export async function isDaemonRunning(): Promise<boolean> {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 2000);
const res = await fetch(`${DAEMON_URL}/status`, { signal: controller.signal });
clearTimeout(timer);
return res.ok;
} catch {
return false;
}
}
/**
* Check if daemon is running AND the extension is connected.
*/
export async function isExtensionConnected(): Promise<boolean> {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 2000);
const res = await fetch(`${DAEMON_URL}/status`, { signal: controller.signal });
clearTimeout(timer);
if (!res.ok) return false;
const data = await res.json() as { extensionConnected?: boolean };
return !!data.extensionConnected;
} catch {
return false;
}
}
/**
* Send a command to the daemon and wait for a result.
*/
export async function sendCommand(
action: DaemonCommand['action'],
params: Omit<DaemonCommand, 'id' | 'action'> = {},
): Promise<unknown> {
const id = generateId();
const command: DaemonCommand = { id, action, ...params };
const res = await fetch(`${DAEMON_URL}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(command),
});
const result = (await res.json()) as DaemonResult;
if (!result.ok) {
throw new Error(result.error ?? 'Daemon command failed');
}
return result.data;
}