@different-ai/opencode-browser 2.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -1,29 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * OpenCode Browser - CLI Installer
3
+ * OpenCode Browser - CLI
4
4
  *
5
- * Installs the Chrome extension for browser automation.
6
- * v2.0: Plugin-based architecture (no daemon, no MCP server)
5
+ * Architecture (v4):
6
+ * OpenCode Plugin <-> Local Broker (unix socket) <-> Native Messaging Host <-> Chrome Extension
7
+ *
8
+ * Commands:
9
+ * install - Install extension + native host
10
+ * uninstall - Remove native host registration
11
+ * status - Show installation status
7
12
  */
8
13
 
9
- import { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync, unlinkSync } from "fs";
14
+ import {
15
+ existsSync,
16
+ mkdirSync,
17
+ writeFileSync,
18
+ readFileSync,
19
+ copyFileSync,
20
+ readdirSync,
21
+ unlinkSync,
22
+ chmodSync,
23
+ } from "fs";
10
24
  import { homedir, platform } from "os";
11
25
  import { join, dirname } from "path";
12
26
  import { fileURLToPath } from "url";
13
- import { execSync } from "child_process";
14
27
  import { createInterface } from "readline";
15
28
 
16
29
  const __filename = fileURLToPath(import.meta.url);
17
30
  const __dirname = dirname(__filename);
18
31
  const PACKAGE_ROOT = join(__dirname, "..");
19
32
 
33
+ const BASE_DIR = join(homedir(), ".opencode-browser");
34
+ const EXTENSION_DIR = join(BASE_DIR, "extension");
35
+ const BROKER_DST = join(BASE_DIR, "broker.cjs");
36
+ const NATIVE_HOST_DST = join(BASE_DIR, "native-host.cjs");
37
+ const CONFIG_DST = join(BASE_DIR, "config.json");
38
+
39
+ const NATIVE_HOST_NAME = "com.opencode.browser_automation";
40
+
20
41
  const COLORS = {
21
42
  reset: "\x1b[0m",
22
43
  bright: "\x1b[1m",
23
44
  red: "\x1b[31m",
24
45
  green: "\x1b[32m",
25
46
  yellow: "\x1b[33m",
26
- blue: "\x1b[34m",
27
47
  cyan: "\x1b[36m",
28
48
  };
29
49
 
@@ -59,9 +79,7 @@ const rl = createInterface({
59
79
 
60
80
  function ask(question) {
61
81
  return new Promise((resolve) => {
62
- rl.question(question, (answer) => {
63
- resolve(answer.trim());
64
- });
82
+ rl.question(question, (answer) => resolve(answer.trim()));
65
83
  });
66
84
  }
67
85
 
@@ -70,14 +88,86 @@ async function confirm(question) {
70
88
  return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
71
89
  }
72
90
 
91
+ function ensureDir(p) {
92
+ mkdirSync(p, { recursive: true });
93
+ }
94
+
95
+ function copyDirRecursive(srcDir, destDir) {
96
+ ensureDir(destDir);
97
+ const entries = readdirSync(srcDir, { recursive: true });
98
+ for (const entry of entries) {
99
+ const srcPath = join(srcDir, entry);
100
+ const destPath = join(destDir, entry);
101
+
102
+ try {
103
+ readdirSync(srcPath);
104
+ ensureDir(destPath);
105
+ } catch {
106
+ ensureDir(dirname(destPath));
107
+ copyFileSync(srcPath, destPath);
108
+ }
109
+ }
110
+ }
111
+
112
+ function getNativeHostDirs(osName) {
113
+ if (osName === "darwin") {
114
+ const base = join(homedir(), "Library", "Application Support");
115
+ return [
116
+ join(base, "Google", "Chrome", "NativeMessagingHosts"),
117
+ join(base, "Chromium", "NativeMessagingHosts"),
118
+ join(base, "BraveSoftware", "Brave-Browser", "NativeMessagingHosts"),
119
+ ];
120
+ }
121
+
122
+ // linux
123
+ const base = join(homedir(), ".config");
124
+ return [
125
+ join(base, "google-chrome", "NativeMessagingHosts"),
126
+ join(base, "chromium", "NativeMessagingHosts"),
127
+ join(base, "BraveSoftware", "Brave-Browser", "NativeMessagingHosts"),
128
+ ];
129
+ }
130
+
131
+ function nativeHostManifestPath(dir) {
132
+ return join(dir, `${NATIVE_HOST_NAME}.json`);
133
+ }
134
+
135
+ function writeNativeHostManifest(dir, extensionId) {
136
+ ensureDir(dir);
137
+
138
+ const manifest = {
139
+ name: NATIVE_HOST_NAME,
140
+ description: "OpenCode Browser native messaging host",
141
+ path: NATIVE_HOST_DST,
142
+ type: "stdio",
143
+ allowed_origins: [`chrome-extension://${extensionId}/`],
144
+ };
145
+
146
+ writeFileSync(nativeHostManifestPath(dir), JSON.stringify(manifest, null, 2) + "\n");
147
+ }
148
+
149
+ function loadConfig() {
150
+ try {
151
+ if (!existsSync(CONFIG_DST)) return null;
152
+ return JSON.parse(readFileSync(CONFIG_DST, "utf-8"));
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+
158
+ function saveConfig(config) {
159
+ ensureDir(BASE_DIR);
160
+ writeFileSync(CONFIG_DST, JSON.stringify(config, null, 2) + "\n");
161
+ }
162
+
73
163
  async function main() {
164
+ const command = process.argv[2];
165
+
74
166
  console.log(`
75
- ${color("cyan", color("bright", "OpenCode Browser v2.0"))}
76
- ${color("cyan", "Browser automation for OpenCode")}
167
+ ${color("cyan", color("bright", "OpenCode Browser v4"))}
168
+ ${color("cyan", "Browser automation plugin (native messaging + per-tab ownership)")}
77
169
  `);
78
170
 
79
- const command = process.argv[2];
80
-
81
171
  if (command === "install") {
82
172
  await install();
83
173
  } else if (command === "uninstall") {
@@ -87,13 +177,14 @@ ${color("cyan", "Browser automation for OpenCode")}
87
177
  } else {
88
178
  log(`
89
179
  ${color("bright", "Usage:")}
90
- npx @different-ai/opencode-browser install Install extension
91
- npx @different-ai/opencode-browser uninstall Remove installation
92
- npx @different-ai/opencode-browser status Check lock status
93
-
94
- ${color("bright", "v2.0 Changes:")}
95
- - Plugin-based architecture (no daemon needed)
96
- - Add plugin to opencode.json, load extension in Chrome, done
180
+ npx @different-ai/opencode-browser install
181
+ npx @different-ai/opencode-browser status
182
+ npx @different-ai/opencode-browser uninstall
183
+
184
+ ${color("bright", "Quick Start:")}
185
+ 1. Run: npx @different-ai/opencode-browser install
186
+ 2. Restart OpenCode
187
+ 3. Use: browser_navigate / browser_click / browser_snapshot
97
188
  `);
98
189
  }
99
190
 
@@ -103,246 +194,200 @@ ${color("bright", "v2.0 Changes:")}
103
194
  async function install() {
104
195
  header("Step 1: Check Platform");
105
196
 
106
- const os = platform();
107
- if (os !== "darwin" && os !== "linux") {
108
- error(`Unsupported platform: ${os}`);
197
+ const osName = platform();
198
+ if (osName !== "darwin" && osName !== "linux") {
199
+ error(`Unsupported platform: ${osName}`);
109
200
  error("OpenCode Browser currently supports macOS and Linux only.");
110
201
  process.exit(1);
111
202
  }
112
- success(`Platform: ${os === "darwin" ? "macOS" : "Linux"}`);
203
+ success(`Platform: ${osName === "darwin" ? "macOS" : "Linux"}`);
113
204
 
114
205
  header("Step 2: Copy Extension Files");
115
206
 
116
- const extensionDir = join(homedir(), ".opencode-browser", "extension");
207
+ ensureDir(BASE_DIR);
117
208
  const srcExtensionDir = join(PACKAGE_ROOT, "extension");
209
+ copyDirRecursive(srcExtensionDir, EXTENSION_DIR);
210
+ success(`Extension files copied to: ${EXTENSION_DIR}`);
118
211
 
119
- mkdirSync(extensionDir, { recursive: true });
212
+ header("Step 3: Load & Pin Extension");
120
213
 
121
- const files = readdirSync(srcExtensionDir, { recursive: true });
122
- for (const file of files) {
123
- const srcPath = join(srcExtensionDir, file);
124
- const destPath = join(extensionDir, file);
214
+ log(`
215
+ To load the extension:
125
216
 
126
- try {
127
- const stat = readdirSync(srcPath);
128
- mkdirSync(destPath, { recursive: true });
129
- } catch {
130
- mkdirSync(dirname(destPath), { recursive: true });
131
- copyFileSync(srcPath, destPath);
132
- }
133
- }
217
+ 1. Open ${color("cyan", "chrome://extensions")}
218
+ 2. Enable ${color("bright", "Developer mode")}
219
+ 3. Click ${color("bright", "Load unpacked")}
220
+ 4. Select:
221
+ ${color("cyan", EXTENSION_DIR)}
134
222
 
135
- success(`Extension files copied to: ${extensionDir}`);
223
+ After loading, ${color("bright", "pin the extension")}: open the Extensions menu (puzzle icon) and click the pin.
224
+ `);
225
+
226
+ await ask(color("bright", "Press Enter when you've loaded and pinned the extension..."));
136
227
 
137
- header("Step 3: Load Extension in Chrome");
228
+ header("Step 4: Get Extension ID");
138
229
 
139
230
  log(`
140
- Works with: ${color("cyan", "Chrome")}, ${color("cyan", "Brave")}, ${color("cyan", "Arc")}, ${color("cyan", "Edge")}, and other Chromium browsers.
231
+ We need the extension ID to register the native messaging host.
141
232
 
142
- To load the extension:
233
+ Find it at ${color("cyan", "chrome://extensions")}:
234
+ - Locate ${color("bright", "OpenCode Browser Automation")}
235
+ - Click ${color("bright", "Details")}
236
+ - Copy the ${color("bright", "ID")}
237
+ `);
143
238
 
144
- 1. Open your browser and go to: ${color("cyan", "chrome://extensions")}
145
- (or ${color("cyan", "brave://extensions")}, ${color("cyan", "arc://extensions")}, etc.)
239
+ const extensionId = await ask(color("bright", "Paste Extension ID: "));
240
+ if (!/^[a-p]{32}$/i.test(extensionId)) {
241
+ warn("That doesn't look like a Chrome extension ID (expected 32 chars a-p). Continuing anyway.");
242
+ }
146
243
 
147
- 2. Enable ${color("bright", "Developer mode")} (toggle in top right)
244
+ header("Step 5: Install Local Host + Broker");
148
245
 
149
- 3. Click ${color("bright", "Load unpacked")}
246
+ const brokerSrc = join(PACKAGE_ROOT, "bin", "broker.cjs");
247
+ const nativeHostSrc = join(PACKAGE_ROOT, "bin", "native-host.cjs");
150
248
 
151
- 4. Select this folder:
152
- ${color("cyan", extensionDir)}
153
- ${os === "darwin" ? color("yellow", "Tip: Press Cmd+Shift+G and paste the path above") : ""}
154
- `);
249
+ copyFileSync(brokerSrc, BROKER_DST);
250
+ copyFileSync(nativeHostSrc, NATIVE_HOST_DST);
155
251
 
156
- await ask(color("bright", "Press Enter when you've loaded the extension..."));
252
+ try {
253
+ chmodSync(BROKER_DST, 0o755);
254
+ } catch {}
255
+ try {
256
+ chmodSync(NATIVE_HOST_DST, 0o755);
257
+ } catch {}
157
258
 
158
- header("Step 4: Configure OpenCode");
259
+ success(`Installed broker: ${BROKER_DST}`);
260
+ success(`Installed native host: ${NATIVE_HOST_DST}`);
159
261
 
160
- const pluginConfig = `{
161
- "$schema": "https://opencode.ai/config.json",
162
- "plugin": ["@different-ai/opencode-browser"]
163
- }`;
262
+ saveConfig({ extensionId, installedAt: new Date().toISOString() });
164
263
 
165
- log(`
166
- Add the plugin to your ${color("cyan", "opencode.json")}:
264
+ header("Step 6: Register Native Messaging Host");
167
265
 
168
- ${color("bright", pluginConfig)}
266
+ const hostDirs = getNativeHostDirs(osName);
267
+ for (const dir of hostDirs) {
268
+ try {
269
+ writeNativeHostManifest(dir, extensionId);
270
+ success(`Wrote native host manifest: ${nativeHostManifestPath(dir)}`);
271
+ } catch (e) {
272
+ warn(`Could not write native host manifest to: ${dir}`);
273
+ }
274
+ }
169
275
 
170
- Or if you already have an opencode.json, just add to the "plugin" array:
171
- ${color("bright", '"plugin": ["@different-ai/opencode-browser"]')}
172
- `);
276
+ header("Step 7: Configure OpenCode");
173
277
 
174
278
  const opencodeJsonPath = join(process.cwd(), "opencode.json");
175
279
 
176
- if (existsSync(opencodeJsonPath)) {
177
- const shouldUpdate = await confirm(`Found opencode.json. Add plugin automatically?`);
280
+ const desiredPlugin = "@different-ai/opencode-browser";
178
281
 
282
+ if (existsSync(opencodeJsonPath)) {
283
+ const shouldUpdate = await confirm("Found opencode.json. Add plugin automatically?");
179
284
  if (shouldUpdate) {
180
285
  try {
181
286
  const config = JSON.parse(readFileSync(opencodeJsonPath, "utf-8"));
182
287
  config.plugin = config.plugin || [];
183
- if (!config.plugin.includes("@different-ai/opencode-browser")) {
184
- config.plugin.push("@different-ai/opencode-browser");
185
- }
186
- // Remove old MCP config if present
288
+ if (!Array.isArray(config.plugin)) config.plugin = [];
289
+ if (!config.plugin.includes(desiredPlugin)) config.plugin.push(desiredPlugin);
290
+
291
+ // Remove MCP config if present
187
292
  if (config.mcp?.browser) {
188
293
  delete config.mcp.browser;
189
- if (Object.keys(config.mcp).length === 0) {
190
- delete config.mcp;
191
- }
294
+ if (Object.keys(config.mcp).length === 0) delete config.mcp;
192
295
  warn("Removed old MCP browser config (replaced by plugin)");
193
296
  }
297
+
194
298
  writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
195
299
  success("Updated opencode.json with plugin");
196
300
  } catch (e) {
197
301
  error(`Failed to update opencode.json: ${e.message}`);
198
- log("Please add the plugin manually.");
199
302
  }
200
303
  }
201
304
  } else {
202
- const shouldCreate = await confirm(`No opencode.json found. Create one?`);
203
-
305
+ const shouldCreate = await confirm("No opencode.json found. Create one?");
204
306
  if (shouldCreate) {
205
- try {
206
- const config = {
207
- $schema: "https://opencode.ai/config.json",
208
- plugin: ["@different-ai/opencode-browser"],
209
- };
210
- writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
211
- success("Created opencode.json with plugin");
212
- } catch (e) {
213
- error(`Failed to create opencode.json: ${e.message}`);
214
- }
215
- }
216
- }
217
-
218
- // Clean up old daemon if present
219
- header("Step 5: Cleanup (v1.x migration)");
220
-
221
- const oldDaemonPlist = join(homedir(), "Library", "LaunchAgents", "com.opencode.browser-daemon.plist");
222
- if (existsSync(oldDaemonPlist)) {
223
- try {
224
- execSync(`launchctl unload "${oldDaemonPlist}" 2>/dev/null || true`, { stdio: "ignore" });
225
- unlinkSync(oldDaemonPlist);
226
- success("Removed old daemon (no longer needed in v2.0)");
227
- } catch {
228
- warn("Could not remove old daemon plist. Remove manually if needed.");
307
+ const config = { $schema: "https://opencode.ai/config.json", plugin: [desiredPlugin] };
308
+ writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
309
+ success("Created opencode.json with plugin");
229
310
  }
230
- } else {
231
- success("No old daemon to clean up");
232
311
  }
233
312
 
234
313
  header("Installation Complete!");
235
314
 
236
315
  log(`
237
- ${color("green", "")} Extension: ${extensionDir}
238
- ${color("green", "")} Plugin: @different-ai/opencode-browser
239
-
240
- ${color("bright", "How it works:")}
241
- 1. OpenCode loads the plugin on startup
242
- 2. Plugin starts WebSocket server on port 19222
243
- 3. Chrome extension connects automatically
244
- 4. Browser tools are available!
245
-
246
- ${color("bright", "Available tools:")}
247
- browser_status - Check if browser is available
248
- browser_kill_session - Take over from another session
249
- browser_navigate - Go to a URL
250
- browser_click - Click an element
251
- browser_type - Type into an input
252
- browser_screenshot - Capture the page
253
- browser_snapshot - Get accessibility tree
254
- browser_get_tabs - List open tabs
255
- browser_scroll - Scroll the page
256
- browser_wait - Wait for duration
257
- browser_execute - Run JavaScript
258
-
259
- ${color("bright", "Multi-session:")}
260
- Only one OpenCode session can use browser at a time.
261
- Use browser_status to check, browser_kill_session to take over.
262
-
263
- ${color("bright", "Test it:")}
264
- Restart OpenCode and try: ${color("cyan", '"Check browser status"')}
316
+ ${color("bright", "What happens now:")}
317
+ - The extension connects to the native host automatically.
318
+ - OpenCode loads the plugin, which talks to the broker.
319
+ - The broker enforces ${color("bright", "per-tab ownership")}. First touch auto-claims.
320
+
321
+ ${color("bright", "Try it:")}
322
+ Restart OpenCode and run: ${color("cyan", "browser_get_tabs")}
265
323
  `);
266
324
  }
267
325
 
268
326
  async function status() {
269
- header("Browser Lock Status");
327
+ header("Status");
270
328
 
271
- const lockFile = join(homedir(), ".opencode-browser", "lock.json");
329
+ success(`Base dir: ${BASE_DIR}`);
330
+ success(`Extension dir present: ${existsSync(EXTENSION_DIR)}`);
331
+ success(`Broker installed: ${existsSync(BROKER_DST)}`);
332
+ success(`Native host installed: ${existsSync(NATIVE_HOST_DST)}`);
272
333
 
273
- if (!existsSync(lockFile)) {
274
- success("Browser available (no lock file)");
275
- return;
334
+ const cfg = loadConfig();
335
+ if (cfg?.extensionId) {
336
+ success(`Configured extension ID: ${cfg.extensionId}`);
337
+ } else {
338
+ warn("No config.json found (run install)");
276
339
  }
277
340
 
278
- try {
279
- const lock = JSON.parse(readFileSync(lockFile, "utf-8"));
280
- log(`
281
- Lock file: ${lockFile}
282
-
283
- PID: ${lock.pid}
284
- Session: ${lock.sessionId}
285
- Started: ${lock.startedAt}
286
- Working directory: ${lock.cwd}
287
- `);
288
-
289
- // Check if process is alive
290
- try {
291
- process.kill(lock.pid, 0);
292
- warn(`Process ${lock.pid} is running. Browser is locked.`);
293
- } catch {
294
- success(`Process ${lock.pid} is dead. Lock is stale and will be auto-cleaned.`);
341
+ const osName = platform();
342
+ const hostDirs = getNativeHostDirs(osName);
343
+ let foundAny = false;
344
+ for (const dir of hostDirs) {
345
+ const p = nativeHostManifestPath(dir);
346
+ if (existsSync(p)) {
347
+ foundAny = true;
348
+ success(`Native host manifest: ${p}`);
295
349
  }
296
- } catch (e) {
297
- error(`Could not read lock file: ${e.message}`);
350
+ }
351
+ if (!foundAny) {
352
+ warn("No native host manifest found. Run: npx @different-ai/opencode-browser install");
298
353
  }
299
354
  }
300
355
 
301
356
  async function uninstall() {
302
- header("Uninstalling OpenCode Browser");
357
+ header("Uninstall");
303
358
 
304
- // Remove old daemon
305
- const os = platform();
306
- if (os === "darwin") {
307
- const plistPath = join(homedir(), "Library", "LaunchAgents", "com.opencode.browser-daemon.plist");
308
- if (existsSync(plistPath)) {
309
- try {
310
- execSync(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "ignore" });
311
- unlinkSync(plistPath);
312
- success("Removed daemon plist");
313
- } catch {}
359
+ const osName = platform();
360
+ const hostDirs = getNativeHostDirs(osName);
361
+ for (const dir of hostDirs) {
362
+ const p = nativeHostManifestPath(dir);
363
+ if (!existsSync(p)) continue;
364
+ try {
365
+ unlinkSync(p);
366
+ success(`Removed native host manifest: ${p}`);
367
+ } catch {
368
+ warn(`Could not remove: ${p}`);
314
369
  }
315
370
  }
316
371
 
317
- // Remove native host registration (v1.x)
318
- const nativeHostDir =
319
- os === "darwin"
320
- ? join(homedir(), "Library", "Application Support", "Google", "Chrome", "NativeMessagingHosts")
321
- : join(homedir(), ".config", "google-chrome", "NativeMessagingHosts");
322
-
323
- const manifestPath = join(nativeHostDir, "com.opencode.browser_automation.json");
324
- if (existsSync(manifestPath)) {
325
- unlinkSync(manifestPath);
326
- success("Removed native host registration");
327
- }
328
-
329
- // Remove lock file
330
- const lockFile = join(homedir(), ".opencode-browser", "lock.json");
331
- if (existsSync(lockFile)) {
332
- unlinkSync(lockFile);
333
- success("Removed lock file");
372
+ for (const p of [BROKER_DST, NATIVE_HOST_DST, CONFIG_DST, join(BASE_DIR, "broker.sock")]) {
373
+ if (!existsSync(p)) continue;
374
+ try {
375
+ unlinkSync(p);
376
+ success(`Removed: ${p}`);
377
+ } catch {
378
+ // ignore
379
+ }
334
380
  }
335
381
 
336
382
  log(`
337
- ${color("bright", "Note:")} Extension files at ~/.opencode-browser/ were not removed.
338
- Remove manually if needed:
339
- rm -rf ~/.opencode-browser/
340
-
341
- Also remove "@different-ai/opencode-browser" from your opencode.json plugin array.
383
+ ${color("bright", "Note:")}
384
+ - The unpacked extension folder remains at: ${EXTENSION_DIR}
385
+ - Remove it manually in ${color("cyan", "chrome://extensions")}
386
+ - Remove ${color("bright", "@different-ai/opencode-browser")} from your opencode.json plugin list if desired.
342
387
  `);
343
388
  }
344
389
 
345
390
  main().catch((e) => {
346
- error(e.message);
391
+ error(e.message || String(e));
347
392
  process.exit(1);
348
393
  });
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // Chrome Native Messaging host for OpenCode Browser.
5
+ // Speaks length-prefixed JSON over stdin/stdout and forwards messages to the local broker over a unix socket.
6
+
7
+ const net = require("net");
8
+ const fs = require("fs");
9
+ const os = require("os");
10
+ const path = require("path");
11
+ const { spawn } = require("child_process");
12
+
13
+ const BASE_DIR = path.join(os.homedir(), ".opencode-browser");
14
+ const SOCKET_PATH = path.join(BASE_DIR, "broker.sock");
15
+ const BROKER_PATH = path.join(BASE_DIR, "broker.cjs");
16
+
17
+ fs.mkdirSync(BASE_DIR, { recursive: true });
18
+
19
+ function createJsonLineParser(onMessage) {
20
+ let buffer = "";
21
+ return (chunk) => {
22
+ buffer += chunk.toString("utf8");
23
+ while (true) {
24
+ const idx = buffer.indexOf("\n");
25
+ if (idx === -1) return;
26
+ const line = buffer.slice(0, idx);
27
+ buffer = buffer.slice(idx + 1);
28
+ if (!line.trim()) continue;
29
+ try {
30
+ onMessage(JSON.parse(line));
31
+ } catch {
32
+ // ignore
33
+ }
34
+ }
35
+ };
36
+ }
37
+
38
+ function writeJsonLine(socket, msg) {
39
+ socket.write(JSON.stringify(msg) + "\n");
40
+ }
41
+
42
+ function maybeStartBroker() {
43
+ try {
44
+ if (!fs.existsSync(BROKER_PATH)) return;
45
+ const child = spawn(process.execPath, [BROKER_PATH], { detached: true, stdio: "ignore" });
46
+ child.unref();
47
+ } catch {
48
+ // ignore
49
+ }
50
+ }
51
+
52
+ async function connectToBroker() {
53
+ return await new Promise((resolve, reject) => {
54
+ const socket = net.createConnection(SOCKET_PATH);
55
+ socket.once("connect", () => resolve(socket));
56
+ socket.once("error", (err) => reject(err));
57
+ });
58
+ }
59
+
60
+ async function ensureBroker() {
61
+ try {
62
+ return await connectToBroker();
63
+ } catch {
64
+ maybeStartBroker();
65
+ for (let i = 0; i < 50; i++) {
66
+ await new Promise((r) => setTimeout(r, 100));
67
+ try {
68
+ return await connectToBroker();
69
+ } catch {}
70
+ }
71
+ throw new Error("Could not connect to broker");
72
+ }
73
+ }
74
+
75
+ // --- Native messaging framing ---
76
+ let stdinBuffer = Buffer.alloc(0);
77
+
78
+ function writeNativeMessage(obj) {
79
+ try {
80
+ const payload = Buffer.from(JSON.stringify(obj), "utf8");
81
+ const header = Buffer.alloc(4);
82
+ header.writeUInt32LE(payload.length, 0);
83
+ process.stdout.write(Buffer.concat([header, payload]));
84
+ } catch (e) {
85
+ console.error("[native-host] write error", e);
86
+ }
87
+ }
88
+
89
+ function onStdinData(chunk, onMessage) {
90
+ stdinBuffer = Buffer.concat([stdinBuffer, chunk]);
91
+ while (stdinBuffer.length >= 4) {
92
+ const len = stdinBuffer.readUInt32LE(0);
93
+ if (stdinBuffer.length < 4 + len) return;
94
+ const body = stdinBuffer.slice(4, 4 + len);
95
+ stdinBuffer = stdinBuffer.slice(4 + len);
96
+ try {
97
+ onMessage(JSON.parse(body.toString("utf8")));
98
+ } catch {
99
+ // ignore
100
+ }
101
+ }
102
+ }
103
+
104
+ (async () => {
105
+ const broker = await ensureBroker();
106
+ broker.setNoDelay(true);
107
+ broker.on("data", createJsonLineParser((msg) => {
108
+ if (msg && msg.type === "to_extension" && msg.message) {
109
+ writeNativeMessage(msg.message);
110
+ }
111
+ }));
112
+
113
+ broker.on("close", () => {
114
+ process.exit(0);
115
+ });
116
+
117
+ broker.on("error", () => {
118
+ process.exit(1);
119
+ });
120
+
121
+ writeJsonLine(broker, { type: "hello", role: "native-host" });
122
+
123
+ process.stdin.on("data", (chunk) =>
124
+ onStdinData(chunk, (message) => {
125
+ // Forward extension-origin messages to broker.
126
+ writeJsonLine(broker, { type: "from_extension", message });
127
+ })
128
+ );
129
+
130
+ process.stdin.on("end", () => {
131
+ try {
132
+ broker.end();
133
+ } catch {}
134
+ process.exit(0);
135
+ });
136
+ })();