@different-ai/opencode-browser 2.0.1 → 2.0.2

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/plugin.ts +78 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@different-ai/opencode-browser",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Browser automation plugin for OpenCode. Control your real Chrome browser with existing logins and cookies.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/plugin.ts CHANGED
@@ -34,6 +34,7 @@ let server: ReturnType<typeof Bun.serve> | null = null;
34
34
  let pendingRequests = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>();
35
35
  let requestId = 0;
36
36
  let hasLock = false;
37
+ let serverFailed = false;
37
38
 
38
39
  // ============================================================================
39
40
  // Lock File Management
@@ -119,7 +120,7 @@ function sleep(ms: number): Promise<void> {
119
120
  async function killSession(targetPid: number): Promise<{ success: boolean; error?: string }> {
120
121
  try {
121
122
  process.kill(targetPid, "SIGTERM");
122
- // Wait a bit for process to die
123
+ // Wait for process to die
123
124
  let attempts = 0;
124
125
  while (isProcessAlive(targetPid) && attempts < 10) {
125
126
  await sleep(100);
@@ -141,7 +142,25 @@ async function killSession(targetPid: number): Promise<{ success: boolean; error
141
142
  // WebSocket Server
142
143
  // ============================================================================
143
144
 
145
+ function checkPortAvailable(): boolean {
146
+ try {
147
+ const testSocket = Bun.connect({ port: WS_PORT, timeout: 1000 });
148
+ testSocket.end();
149
+ return true;
150
+ } catch (e) {
151
+ if ((e as any).code === "ECONNREFUSED") {
152
+ return false;
153
+ }
154
+ return true;
155
+ }
156
+ }
157
+
144
158
  function startServer(): boolean {
159
+ if (server) {
160
+ console.error(`[browser-plugin] Server already running`);
161
+ return true;
162
+ }
163
+
145
164
  try {
146
165
  server = Bun.serve({
147
166
  port: WS_PORT,
@@ -171,6 +190,7 @@ function startServer(): boolean {
171
190
  },
172
191
  });
173
192
  console.error(`[browser-plugin] WebSocket server listening on port ${WS_PORT}`);
193
+ serverFailed = false;
174
194
  return true;
175
195
  } catch (e) {
176
196
  console.error(`[browser-plugin] Failed to start server:`, e);
@@ -178,7 +198,7 @@ function startServer(): boolean {
178
198
  }
179
199
  }
180
200
 
181
- function handleMessage(message: { type: string; id?: number; result?: any; error?: any }) {
201
+ function handleMessage(message: { type: string; id?: number; result?: any; error?: any }): void {
182
202
  if (message.type === "tool_response" && message.id !== undefined) {
183
203
  const pending = pendingRequests.get(message.id);
184
204
  if (pending) {
@@ -203,7 +223,7 @@ function sendToChrome(message: any): boolean {
203
223
  }
204
224
 
205
225
  async function executeCommand(tool: string, args: Record<string, any>): Promise<any> {
206
- // Check lock first
226
+ // Check lock and start server if needed
207
227
  const lockResult = tryAcquireLock();
208
228
  if (!lockResult.success) {
209
229
  throw new Error(
@@ -211,7 +231,6 @@ async function executeCommand(tool: string, args: Record<string, any>): Promise<
211
231
  );
212
232
  }
213
233
 
214
- // Start server if not running
215
234
  if (!server) {
216
235
  if (!startServer()) {
217
236
  throw new Error("Failed to start WebSocket server. Port may be in use.");
@@ -273,12 +292,33 @@ process.on("exit", () => {
273
292
  export const BrowserPlugin: Plugin = async (ctx) => {
274
293
  console.error(`[browser-plugin] Initializing (session ${sessionId})`);
275
294
 
276
- // Try to acquire lock and start server on load
277
- const lockResult = tryAcquireLock();
278
- if (lockResult.success) {
279
- startServer();
295
+ // Check port availability on load, don't try to acquire lock yet
296
+ checkPortAvailable();
297
+
298
+ // Check lock status and set appropriate state
299
+ const lock = readLock();
300
+ if (!lock) {
301
+ // No lock - just check if we can start server
302
+ console.error(`[browser-plugin] No lock file, checking port...`);
303
+ if (!startServer()) {
304
+ serverFailed = true;
305
+ }
306
+ } else if (lock.sessionId === sessionId) {
307
+ // We own the lock - start server
308
+ console.error(`[browser-plugin] Already have lock, starting server...`);
309
+ if (!startServer()) {
310
+ serverFailed = true;
311
+ }
312
+ } else if (!isProcessAlive(lock.pid)) {
313
+ // Stale lock - take it and start server
314
+ console.error(`[browser-plugin] Stale lock from dead PID ${lock.pid}, taking over...`);
315
+ writeLock();
316
+ if (!startServer()) {
317
+ serverFailed = true;
318
+ }
280
319
  } else {
281
- console.error(`[browser-plugin] Lock held by PID ${lockResult.lock?.pid}, tools will fail until lock is released`);
320
+ // Another session has the lock
321
+ console.error(`[browser-plugin] Lock held by PID ${lock.pid}, tools will fail until lock is released`);
282
322
  }
283
323
 
284
324
  return {
@@ -316,7 +356,12 @@ export const BrowserPlugin: Plugin = async (ctx) => {
316
356
  if (!lock) {
317
357
  // No lock, just acquire
318
358
  writeLock();
319
- if (!server) startServer();
359
+ // Start server if needed
360
+ if (!server) {
361
+ if (!startServer()) {
362
+ throw new Error("Failed to start WebSocket server after acquiring lock.");
363
+ }
364
+ }
320
365
  return "No active session. Browser now connected to this session.";
321
366
  }
322
367
 
@@ -327,14 +372,23 @@ export const BrowserPlugin: Plugin = async (ctx) => {
327
372
  if (!isProcessAlive(lock.pid)) {
328
373
  // Stale lock
329
374
  writeLock();
330
- if (!server) startServer();
375
+ // Start server if needed
376
+ if (!server) {
377
+ if (!startServer()) {
378
+ throw new Error("Failed to start WebSocket server after cleaning stale lock.");
379
+ }
380
+ }
331
381
  return `Cleaned stale lock (PID ${lock.pid} was dead). Browser now connected to this session.`;
332
382
  }
333
383
 
334
- // Kill the other session
384
+ // Kill other session and wait for port to be free
335
385
  const result = await killSession(lock.pid);
336
386
  if (result.success) {
337
- if (!server) startServer();
387
+ if (!server) {
388
+ if (!startServer()) {
389
+ throw new Error("Failed to start WebSocket server after killing other session.");
390
+ }
391
+ }
338
392
  return `Killed session ${lock.sessionId} (PID ${lock.pid}). Browser now connected to this session.`;
339
393
  } else {
340
394
  throw new Error(`Failed to kill session: ${result.error}`);
@@ -343,7 +397,7 @@ export const BrowserPlugin: Plugin = async (ctx) => {
343
397
  }),
344
398
 
345
399
  browser_navigate: tool({
346
- description: "Navigate to a URL in the browser",
400
+ description: "Navigate to a URL in browser",
347
401
  args: {
348
402
  url: tool.schema.string({ description: "The URL to navigate to" }),
349
403
  tabId: tool.schema.optional(tool.schema.number({ description: "Optional tab ID" })),
@@ -354,9 +408,9 @@ export const BrowserPlugin: Plugin = async (ctx) => {
354
408
  }),
355
409
 
356
410
  browser_click: tool({
357
- description: "Click an element on the page using a CSS selector",
411
+ description: "Click an element on page using a CSS selector",
358
412
  args: {
359
- selector: tool.schema.string({ description: "CSS selector for the element to click" }),
413
+ selector: tool.schema.string({ description: "CSS selector for element to click" }),
360
414
  tabId: tool.schema.optional(tool.schema.number({ description: "Optional tab ID" })),
361
415
  },
362
416
  async execute(args) {
@@ -367,7 +421,7 @@ export const BrowserPlugin: Plugin = async (ctx) => {
367
421
  browser_type: tool({
368
422
  description: "Type text into an input element",
369
423
  args: {
370
- selector: tool.schema.string({ description: "CSS selector for the input element" }),
424
+ selector: tool.schema.string({ description: "CSS selector for input element" }),
371
425
  text: tool.schema.string({ description: "Text to type" }),
372
426
  clear: tool.schema.optional(tool.schema.boolean({ description: "Clear field before typing" })),
373
427
  tabId: tool.schema.optional(tool.schema.number({ description: "Optional tab ID" })),
@@ -378,26 +432,26 @@ export const BrowserPlugin: Plugin = async (ctx) => {
378
432
  }),
379
433
 
380
434
  browser_screenshot: tool({
381
- description: "Take a screenshot of the current page. Saves to ~/.opencode-browser/screenshots/ and returns the file path.",
435
+ description: "Take a screenshot of the current page. Saves to ~/.opencode-browser/screenshots/",
382
436
  args: {
383
437
  tabId: tool.schema.optional(tool.schema.number({ description: "Optional tab ID" })),
384
- name: tool.schema.optional(tool.schema.string({ description: "Optional name for the screenshot file (without extension)" })),
438
+ name: tool.schema.optional(
439
+ tool.schema.string({ description: "Optional name for screenshot file (without extension)" })
440
+ ),
385
441
  },
386
442
  async execute(args) {
387
443
  const result = await executeCommand("screenshot", args);
388
-
444
+
389
445
  if (result && result.startsWith("data:image")) {
390
- // Extract base64 data and save to file
391
446
  const base64Data = result.replace(/^data:image\/\w+;base64,/, "");
392
447
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
393
448
  const filename = args.name ? `${args.name}.png` : `screenshot-${timestamp}.png`;
394
449
  const filepath = join(SCREENSHOTS_DIR, filename);
395
-
450
+
396
451
  writeFileSync(filepath, Buffer.from(base64Data, "base64"));
397
-
398
452
  return `Screenshot saved: ${filepath}`;
399
453
  }
400
-
454
+
401
455
  return result;
402
456
  },
403
457
  }),