@different-ai/opencode-browser 2.0.0 → 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 +86 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@different-ai/opencode-browser",
3
- "version": "2.0.0",
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
@@ -19,9 +19,11 @@ import { join } from "path";
19
19
  const WS_PORT = 19222;
20
20
  const BASE_DIR = join(homedir(), ".opencode-browser");
21
21
  const LOCK_FILE = join(BASE_DIR, "lock.json");
22
+ const SCREENSHOTS_DIR = join(BASE_DIR, "screenshots");
22
23
 
23
- // Ensure base dir exists
24
+ // Ensure directories exist
24
25
  mkdirSync(BASE_DIR, { recursive: true });
26
+ mkdirSync(SCREENSHOTS_DIR, { recursive: true });
25
27
 
26
28
  // Session state
27
29
  const sessionId = Math.random().toString(36).slice(2);
@@ -32,6 +34,7 @@ let server: ReturnType<typeof Bun.serve> | null = null;
32
34
  let pendingRequests = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>();
33
35
  let requestId = 0;
34
36
  let hasLock = false;
37
+ let serverFailed = false;
35
38
 
36
39
  // ============================================================================
37
40
  // Lock File Management
@@ -117,7 +120,7 @@ function sleep(ms: number): Promise<void> {
117
120
  async function killSession(targetPid: number): Promise<{ success: boolean; error?: string }> {
118
121
  try {
119
122
  process.kill(targetPid, "SIGTERM");
120
- // Wait a bit for process to die
123
+ // Wait for process to die
121
124
  let attempts = 0;
122
125
  while (isProcessAlive(targetPid) && attempts < 10) {
123
126
  await sleep(100);
@@ -139,7 +142,25 @@ async function killSession(targetPid: number): Promise<{ success: boolean; error
139
142
  // WebSocket Server
140
143
  // ============================================================================
141
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
+
142
158
  function startServer(): boolean {
159
+ if (server) {
160
+ console.error(`[browser-plugin] Server already running`);
161
+ return true;
162
+ }
163
+
143
164
  try {
144
165
  server = Bun.serve({
145
166
  port: WS_PORT,
@@ -169,6 +190,7 @@ function startServer(): boolean {
169
190
  },
170
191
  });
171
192
  console.error(`[browser-plugin] WebSocket server listening on port ${WS_PORT}`);
193
+ serverFailed = false;
172
194
  return true;
173
195
  } catch (e) {
174
196
  console.error(`[browser-plugin] Failed to start server:`, e);
@@ -176,7 +198,7 @@ function startServer(): boolean {
176
198
  }
177
199
  }
178
200
 
179
- function handleMessage(message: { type: string; id?: number; result?: any; error?: any }) {
201
+ function handleMessage(message: { type: string; id?: number; result?: any; error?: any }): void {
180
202
  if (message.type === "tool_response" && message.id !== undefined) {
181
203
  const pending = pendingRequests.get(message.id);
182
204
  if (pending) {
@@ -201,7 +223,7 @@ function sendToChrome(message: any): boolean {
201
223
  }
202
224
 
203
225
  async function executeCommand(tool: string, args: Record<string, any>): Promise<any> {
204
- // Check lock first
226
+ // Check lock and start server if needed
205
227
  const lockResult = tryAcquireLock();
206
228
  if (!lockResult.success) {
207
229
  throw new Error(
@@ -209,7 +231,6 @@ async function executeCommand(tool: string, args: Record<string, any>): Promise<
209
231
  );
210
232
  }
211
233
 
212
- // Start server if not running
213
234
  if (!server) {
214
235
  if (!startServer()) {
215
236
  throw new Error("Failed to start WebSocket server. Port may be in use.");
@@ -271,12 +292,33 @@ process.on("exit", () => {
271
292
  export const BrowserPlugin: Plugin = async (ctx) => {
272
293
  console.error(`[browser-plugin] Initializing (session ${sessionId})`);
273
294
 
274
- // Try to acquire lock and start server on load
275
- const lockResult = tryAcquireLock();
276
- if (lockResult.success) {
277
- 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
+ }
278
319
  } else {
279
- 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`);
280
322
  }
281
323
 
282
324
  return {
@@ -314,7 +356,12 @@ export const BrowserPlugin: Plugin = async (ctx) => {
314
356
  if (!lock) {
315
357
  // No lock, just acquire
316
358
  writeLock();
317
- 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
+ }
318
365
  return "No active session. Browser now connected to this session.";
319
366
  }
320
367
 
@@ -325,14 +372,23 @@ export const BrowserPlugin: Plugin = async (ctx) => {
325
372
  if (!isProcessAlive(lock.pid)) {
326
373
  // Stale lock
327
374
  writeLock();
328
- 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
+ }
329
381
  return `Cleaned stale lock (PID ${lock.pid} was dead). Browser now connected to this session.`;
330
382
  }
331
383
 
332
- // Kill the other session
384
+ // Kill other session and wait for port to be free
333
385
  const result = await killSession(lock.pid);
334
386
  if (result.success) {
335
- if (!server) startServer();
387
+ if (!server) {
388
+ if (!startServer()) {
389
+ throw new Error("Failed to start WebSocket server after killing other session.");
390
+ }
391
+ }
336
392
  return `Killed session ${lock.sessionId} (PID ${lock.pid}). Browser now connected to this session.`;
337
393
  } else {
338
394
  throw new Error(`Failed to kill session: ${result.error}`);
@@ -341,7 +397,7 @@ export const BrowserPlugin: Plugin = async (ctx) => {
341
397
  }),
342
398
 
343
399
  browser_navigate: tool({
344
- description: "Navigate to a URL in the browser",
400
+ description: "Navigate to a URL in browser",
345
401
  args: {
346
402
  url: tool.schema.string({ description: "The URL to navigate to" }),
347
403
  tabId: tool.schema.optional(tool.schema.number({ description: "Optional tab ID" })),
@@ -352,9 +408,9 @@ export const BrowserPlugin: Plugin = async (ctx) => {
352
408
  }),
353
409
 
354
410
  browser_click: tool({
355
- description: "Click an element on the page using a CSS selector",
411
+ description: "Click an element on page using a CSS selector",
356
412
  args: {
357
- selector: tool.schema.string({ description: "CSS selector for the element to click" }),
413
+ selector: tool.schema.string({ description: "CSS selector for element to click" }),
358
414
  tabId: tool.schema.optional(tool.schema.number({ description: "Optional tab ID" })),
359
415
  },
360
416
  async execute(args) {
@@ -365,7 +421,7 @@ export const BrowserPlugin: Plugin = async (ctx) => {
365
421
  browser_type: tool({
366
422
  description: "Type text into an input element",
367
423
  args: {
368
- selector: tool.schema.string({ description: "CSS selector for the input element" }),
424
+ selector: tool.schema.string({ description: "CSS selector for input element" }),
369
425
  text: tool.schema.string({ description: "Text to type" }),
370
426
  clear: tool.schema.optional(tool.schema.boolean({ description: "Clear field before typing" })),
371
427
  tabId: tool.schema.optional(tool.schema.number({ description: "Optional tab ID" })),
@@ -376,17 +432,26 @@ export const BrowserPlugin: Plugin = async (ctx) => {
376
432
  }),
377
433
 
378
434
  browser_screenshot: tool({
379
- description: "Take a screenshot of the current page",
435
+ description: "Take a screenshot of the current page. Saves to ~/.opencode-browser/screenshots/",
380
436
  args: {
381
437
  tabId: tool.schema.optional(tool.schema.number({ description: "Optional tab ID" })),
438
+ name: tool.schema.optional(
439
+ tool.schema.string({ description: "Optional name for screenshot file (without extension)" })
440
+ ),
382
441
  },
383
442
  async execute(args) {
384
443
  const result = await executeCommand("screenshot", args);
385
- // Return as base64 image
444
+
386
445
  if (result && result.startsWith("data:image")) {
387
446
  const base64Data = result.replace(/^data:image\/\w+;base64,/, "");
388
- return { type: "image", data: base64Data, mimeType: "image/png" };
447
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
448
+ const filename = args.name ? `${args.name}.png` : `screenshot-${timestamp}.png`;
449
+ const filepath = join(SCREENSHOTS_DIR, filename);
450
+
451
+ writeFileSync(filepath, Buffer.from(base64Data, "base64"));
452
+ return `Screenshot saved: ${filepath}`;
389
453
  }
454
+
390
455
  return result;
391
456
  },
392
457
  }),