Files
oh-my-pi/packages/coding-agent/src/cli/browser-relay-cli.ts
T
can1357 4a946e2cfc fix(coding-agent/tools): serialized tab grouping operations in the relay bridge
- Serialized group and ungroup operations to prevent duplicate tab group creation races.
- Queued and serially drained tab grouping requests in the relay bridge to prevent overlapping RPCs.
- Mirrored tab group titles to session storage and healed duplicate groups during background service worker recovery.
- Renamed run-cancellation utility to run-scope and updated corresponding module and test references.
2026-08-02 18:07:07 +02:00

120 lines
4.8 KiB
TypeScript

/**
* `omp browser-relay` implementation: serve the local CDP relay and install
* its Chrome extension. Standalone CLI command — console output here is
* intentional user-facing output.
*/
import * as path from "node:path";
import { getBrowserRelayDir } from "@oh-my-pi/pi-utils";
import { probeRelayServer } from "../tools/browser/relay/daemon";
import backgroundJs from "../tools/browser/relay/extension-assets/background.js.txt" with { type: "text" };
import manifestJson from "../tools/browser/relay/extension-assets/manifest.json.txt" with { type: "text" };
import optionsHtml from "../tools/browser/relay/extension-assets/options.html.txt" with { type: "text" };
import optionsJs from "../tools/browser/relay/extension-assets/options.js.txt" with { type: "text" };
import { DEFAULT_RELAY_URL } from "../tools/browser/relay/kind";
import { type RelayServer, startRelayServer } from "../tools/browser/relay/server";
export const BROWSER_RELAY_ACTIONS = ["serve", "install"] as const;
export type BrowserRelayAction = (typeof BROWSER_RELAY_ACTIONS)[number];
export interface BrowserRelayCommandArgs {
action: BrowserRelayAction;
port: number;
token?: string;
/** Install target directory; defaults to ~/.omp/browser-relay/extension. */
dir?: string;
/** Gather tabs the agent actively drives into an 'omp' Chrome tab group (default true). */
group?: boolean;
verbose?: boolean;
}
const EXTENSION_FILES: Record<string, string> = {
"background.js": backgroundJs,
"manifest.json": manifestJson,
"options.html": optionsHtml,
"options.js": optionsJs,
};
/** Default port of the relay endpoint (kept in sync with DEFAULT_RELAY_URL). */
export const DEFAULT_RELAY_PORT = Number(new URL(DEFAULT_RELAY_URL).port);
export async function runBrowserRelayCommand(args: BrowserRelayCommandArgs): Promise<void> {
if (args.action === "install") {
await runInstall(args.dir);
return;
}
await runServe(args);
}
async function runInstall(dirOverride: string | undefined): Promise<void> {
const dir = dirOverride ? path.resolve(dirOverride) : path.join(getBrowserRelayDir(), "extension");
for (const name in EXTENSION_FILES) {
await Bun.write(path.join(dir, name), EXTENSION_FILES[name]!);
}
console.log(`Installed the OMP Browser Relay extension to ${dir}`);
console.log("");
console.log("Finish setup in Chrome:");
console.log(" 1. Open chrome://extensions and enable Developer mode.");
console.log(` 2. Click "Load unpacked" and select: ${dir}`);
console.log(" 3. Enable the mode: omp config set browser.relay true");
console.log("");
console.log("omp starts the relay automatically when the browser tool needs it;");
console.log("run `omp browser-relay` yourself only for --token or --no-group.");
console.log("The extension badge shows 'on' once it reaches a relay.");
}
async function runServe(args: BrowserRelayCommandArgs): Promise<void> {
const log = args.verbose
? (message: string, data?: Record<string, unknown>) => {
console.error(`[relay] ${message}${data ? ` ${JSON.stringify(data)}` : ""}`);
}
: undefined;
let relay: RelayServer;
try {
relay = startRelayServer({ port: args.port, token: args.token, group: args.group !== false, log });
} catch (err) {
// The port is machine-global while relays can be started by any project's
// broker (or by hand): losing the bind to a live relay is success.
if (err instanceof Error && "code" in err && err.code === "EADDRINUSE") {
if (await probeRelayServer(`http://127.0.0.1:${args.port}`)) {
console.log(`omp browser relay already running on http://127.0.0.1:${args.port}; nothing to do.`);
return;
}
console.error(`Port ${args.port} is in use by something that is not an omp browser relay.`);
process.exit(1);
}
throw err;
}
console.log(`omp browser relay listening on http://127.0.0.1:${args.port}`);
console.log(` extension endpoint ws://127.0.0.1:${args.port}/ext${args.token ? "?token=***" : ""}`);
if (args.port === DEFAULT_RELAY_PORT) {
console.log(" enable with omp config set browser.relay true");
} else {
console.log(
` enable with omp config set browser.relay true && omp config set browser.relayUrl http://127.0.0.1:${args.port}`,
);
}
console.log("Waiting for the OMP Browser Relay extension to connect (omp browser-relay install)...");
let announced = false;
const readiness = setInterval(() => {
if (relay.bridge.ready && !announced) {
announced = true;
console.log("Extension connected. The omp browser tool can now drive your tabs.");
} else if (!relay.bridge.ready && announced) {
announced = false;
console.log("Extension disconnected; waiting for it to reconnect...");
}
}, 500);
const shutdown = () => {
clearInterval(readiness);
relay.stop();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
// Serve runs until SIGINT/SIGTERM; keep the process alive.
await new Promise<never>(() => {});
}