@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.
- package/package.json +1 -1
- package/src/plugin.ts +86 -21
package/package.json
CHANGED
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
411
|
+
description: "Click an element on page using a CSS selector",
|
|
356
412
|
args: {
|
|
357
|
-
selector: tool.schema.string({ description: "CSS selector for
|
|
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
|
|
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
|
-
|
|
444
|
+
|
|
386
445
|
if (result && result.startsWith("data:image")) {
|
|
387
446
|
const base64Data = result.replace(/^data:image\/\w+;base64,/, "");
|
|
388
|
-
|
|
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
|
}),
|