all repos — sz-chrome-remote-extension @ cc86d6114291305fcea1ed8b0904c8be0e633767

pi-chrome, patched to drive a signed-in browser on another host through a broker, with an MCP server

pi-chrome 0.15.57: stand in for pi-tui, collect the primer from prompt sections
Igor Soarez igor@soarez.org
Thu, 15 Oct 2026 06:49:35 +0100
commit

cc86d6114291305fcea1ed8b0904c8be0e633767

parent

50c251e082b8122bfd1a6e80427d0dc19fa4f246

2 files changed, 21 insertions(+), 5 deletions(-)

jump to
A mcp/pi-tui.mjs

@@ -0,0 +1,3 @@

+// Stands in for @earendil-works/pi-tui, which pi-chrome imports for its transcript renderers. Those +// draw pi's terminal UI and never run here, so the classes only have to exist. +export class Text {}
M mcp/server.mjs → mcp/server.mjs

@@ -211,6 +211,8 @@ setActiveTools(names) {

active = new Set(names); }, sendMessage() {}, + // pi-chrome logs authorization changes to the session history; there is none here. + appendEntry() {}, exec: async () => ({ code: 1, stdout: "", stderr: "not available in the MCP server" }), };

@@ -223,10 +225,14 @@

// --- load pi-chrome --------------------------------------------------------------------------- // pi-chrome imports typebox at runtime, and its own directory may have no node_modules: point it -// at the copy pinned here, the version pi ships. jiti rather than Node's type stripping, because -// pi-chrome uses constructor parameter properties, which strip-only mode rejects. +// at the copy pinned here, the version pi ships, and pi-tui at a stand-in. jiti rather than Node's +// type stripping, because pi-chrome uses constructor parameter properties, which strip-only mode +// rejects. const jiti = createJiti(import.meta.url, { - alias: { typebox: fileURLToPath(import.meta.resolve("typebox")) }, + alias: { + typebox: fileURLToPath(import.meta.resolve("typebox")), + "@earendil-works/pi-tui": fileURLToPath(new URL("pi-tui.mjs", import.meta.url)), + }, }); const extension = await jiti.import(EXTENSION, { default: true }); extension(pi);

@@ -245,12 +251,19 @@ //

// A fork's primer may name another browser's tools, which differ by client: browser_* in pi, // mcp__<server>__* elsewhere. pi-chrome reads PI_CHROME_OTHER_BROWSER_TOOLS as it builds the // primer, synchronously inside the handler, so the variable only has to hold for that call. +const UPSTREAM_SECTION = "chrome-profile-bridge"; async function instructionsWith(otherTools) { const saved = process.env.PI_CHROME_OTHER_BROWSER_TOOLS; if (otherTools) process.env.PI_CHROME_OTHER_BROWSER_TOOLS = otherTools; - const pending = emit("before_agent_start", { systemPrompt: "" }); + // pi-chrome adds its primer as named prompt sections, one per handler (upstream's and a fork's), + // or, written for an older pi, returns a whole systemPrompt instead. Take both; upstream's goes + // first. + const sections = {}; + const pending = emit("before_agent_start", { systemPrompt: "", systemPromptOptions: { sections } }); process.env.PI_CHROME_OTHER_BROWSER_TOOLS = saved; - const primer = (await pending)?.systemPrompt ?? ""; + const returned = (await pending)?.systemPrompt ?? ""; + const names = Object.keys(sections).sort((a, b) => (b === UPSTREAM_SECTION) - (a === UPSTREAM_SECTION)); + const primer = [...names.map((name) => `<${name}>\n${sections[name]}\n</${name}>`), returned].join("\n"); return [ESSENTIALS, primer.split("\n").filter((line) => !/\/chrome\b/.test(line)).join("\n")].join("\n"); } const OTHER_BROWSER_TOOLS = process.env.PI_CHROME_OTHER_BROWSER_TOOLS?.trim();