@different-ai/opencode-browser 1.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/README.md +109 -0
- package/bin/cli.js +316 -0
- package/extension/background.js +456 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +34 -0
- package/package.json +35 -0
- package/src/host.js +282 -0
- package/src/server.js +379 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// OpenCode Browser Automation - Background Service Worker
|
|
2
|
+
// Native Messaging Host: com.opencode.browser_automation
|
|
3
|
+
|
|
4
|
+
const NATIVE_HOST_NAME = "com.opencode.browser_automation";
|
|
5
|
+
|
|
6
|
+
let nativePort = null;
|
|
7
|
+
let isConnected = false;
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Native Messaging Connection
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
async function connectToNativeHost() {
|
|
14
|
+
if (nativePort) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
nativePort = chrome.runtime.connectNative(NATIVE_HOST_NAME);
|
|
20
|
+
|
|
21
|
+
nativePort.onMessage.addListener(handleNativeMessage);
|
|
22
|
+
|
|
23
|
+
nativePort.onDisconnect.addListener(() => {
|
|
24
|
+
const error = chrome.runtime.lastError?.message;
|
|
25
|
+
console.log("[OpenCode] Native host disconnected:", error);
|
|
26
|
+
nativePort = null;
|
|
27
|
+
isConnected = false;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Ping to verify connection
|
|
31
|
+
const connected = await new Promise((resolve) => {
|
|
32
|
+
const timeout = setTimeout(() => resolve(false), 5000);
|
|
33
|
+
|
|
34
|
+
const pingHandler = (msg) => {
|
|
35
|
+
if (msg.type === "pong") {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
nativePort.onMessage.removeListener(pingHandler);
|
|
38
|
+
resolve(true);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
nativePort.onMessage.addListener(pingHandler);
|
|
43
|
+
nativePort.postMessage({ type: "ping" });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (connected) {
|
|
47
|
+
isConnected = true;
|
|
48
|
+
console.log("[OpenCode] Connected to native host");
|
|
49
|
+
return true;
|
|
50
|
+
} else {
|
|
51
|
+
nativePort.disconnect();
|
|
52
|
+
nativePort = null;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error("[OpenCode] Failed to connect:", error);
|
|
57
|
+
nativePort = null;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function disconnectNativeHost() {
|
|
63
|
+
if (nativePort) {
|
|
64
|
+
nativePort.disconnect();
|
|
65
|
+
nativePort = null;
|
|
66
|
+
isConnected = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Message Handling from Native Host
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
async function handleNativeMessage(message) {
|
|
75
|
+
console.log("[OpenCode] Received from native:", message.type);
|
|
76
|
+
|
|
77
|
+
switch (message.type) {
|
|
78
|
+
case "tool_request":
|
|
79
|
+
await handleToolRequest(message);
|
|
80
|
+
break;
|
|
81
|
+
case "ping":
|
|
82
|
+
sendToNative({ type: "pong" });
|
|
83
|
+
break;
|
|
84
|
+
case "get_status":
|
|
85
|
+
sendToNative({
|
|
86
|
+
type: "status_response",
|
|
87
|
+
connected: isConnected,
|
|
88
|
+
version: chrome.runtime.getManifest().version
|
|
89
|
+
});
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sendToNative(message) {
|
|
95
|
+
if (nativePort) {
|
|
96
|
+
nativePort.postMessage(message);
|
|
97
|
+
} else {
|
|
98
|
+
console.error("[OpenCode] Cannot send - not connected");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Tool Execution
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
async function handleToolRequest(request) {
|
|
107
|
+
const { id, tool, args } = request;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await executeTool(tool, args || {});
|
|
111
|
+
sendToNative({
|
|
112
|
+
type: "tool_response",
|
|
113
|
+
id,
|
|
114
|
+
result: { content: result }
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
sendToNative({
|
|
118
|
+
type: "tool_response",
|
|
119
|
+
id,
|
|
120
|
+
error: { content: error.message || String(error) }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function executeTool(toolName, args) {
|
|
126
|
+
switch (toolName) {
|
|
127
|
+
case "navigate":
|
|
128
|
+
return await toolNavigate(args);
|
|
129
|
+
case "click":
|
|
130
|
+
return await toolClick(args);
|
|
131
|
+
case "type":
|
|
132
|
+
return await toolType(args);
|
|
133
|
+
case "screenshot":
|
|
134
|
+
return await toolScreenshot(args);
|
|
135
|
+
case "snapshot":
|
|
136
|
+
return await toolSnapshot(args);
|
|
137
|
+
case "get_tabs":
|
|
138
|
+
return await toolGetTabs(args);
|
|
139
|
+
case "execute_script":
|
|
140
|
+
return await toolExecuteScript(args);
|
|
141
|
+
case "scroll":
|
|
142
|
+
return await toolScroll(args);
|
|
143
|
+
case "wait":
|
|
144
|
+
return await toolWait(args);
|
|
145
|
+
default:
|
|
146
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Tool Implementations
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
async function getActiveTab() {
|
|
155
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
156
|
+
if (!tab?.id) throw new Error("No active tab found");
|
|
157
|
+
return tab;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function getTabById(tabId) {
|
|
161
|
+
if (tabId) {
|
|
162
|
+
return await chrome.tabs.get(tabId);
|
|
163
|
+
}
|
|
164
|
+
return await getActiveTab();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function toolNavigate({ url, tabId }) {
|
|
168
|
+
if (!url) throw new Error("URL is required");
|
|
169
|
+
|
|
170
|
+
const tab = await getTabById(tabId);
|
|
171
|
+
await chrome.tabs.update(tab.id, { url });
|
|
172
|
+
|
|
173
|
+
// Wait for page to load
|
|
174
|
+
await new Promise((resolve) => {
|
|
175
|
+
const listener = (updatedTabId, info) => {
|
|
176
|
+
if (updatedTabId === tab.id && info.status === "complete") {
|
|
177
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
178
|
+
resolve();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
182
|
+
// Timeout after 30 seconds
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
185
|
+
resolve();
|
|
186
|
+
}, 30000);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return `Navigated to ${url}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function toolClick({ selector, tabId }) {
|
|
193
|
+
if (!selector) throw new Error("Selector is required");
|
|
194
|
+
|
|
195
|
+
const tab = await getTabById(tabId);
|
|
196
|
+
|
|
197
|
+
const result = await chrome.scripting.executeScript({
|
|
198
|
+
target: { tabId: tab.id },
|
|
199
|
+
func: (sel) => {
|
|
200
|
+
const element = document.querySelector(sel);
|
|
201
|
+
if (!element) return { success: false, error: `Element not found: ${sel}` };
|
|
202
|
+
element.click();
|
|
203
|
+
return { success: true };
|
|
204
|
+
},
|
|
205
|
+
args: [selector]
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!result[0]?.result?.success) {
|
|
209
|
+
throw new Error(result[0]?.result?.error || "Click failed");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return `Clicked ${selector}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function toolType({ selector, text, tabId, clear = false }) {
|
|
216
|
+
if (!selector) throw new Error("Selector is required");
|
|
217
|
+
if (text === undefined) throw new Error("Text is required");
|
|
218
|
+
|
|
219
|
+
const tab = await getTabById(tabId);
|
|
220
|
+
|
|
221
|
+
const result = await chrome.scripting.executeScript({
|
|
222
|
+
target: { tabId: tab.id },
|
|
223
|
+
func: (sel, txt, shouldClear) => {
|
|
224
|
+
const element = document.querySelector(sel);
|
|
225
|
+
if (!element) return { success: false, error: `Element not found: ${sel}` };
|
|
226
|
+
|
|
227
|
+
element.focus();
|
|
228
|
+
if (shouldClear) {
|
|
229
|
+
element.value = "";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// For input/textarea, set value directly
|
|
233
|
+
if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
|
|
234
|
+
element.value = element.value + txt;
|
|
235
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
236
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
237
|
+
} else if (element.isContentEditable) {
|
|
238
|
+
document.execCommand("insertText", false, txt);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { success: true };
|
|
242
|
+
},
|
|
243
|
+
args: [selector, text, clear]
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!result[0]?.result?.success) {
|
|
247
|
+
throw new Error(result[0]?.result?.error || "Type failed");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return `Typed "${text}" into ${selector}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function toolScreenshot({ tabId, fullPage = false }) {
|
|
254
|
+
const tab = await getTabById(tabId);
|
|
255
|
+
|
|
256
|
+
// Capture visible area
|
|
257
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
|
|
258
|
+
format: "png"
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return dataUrl;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function toolSnapshot({ tabId }) {
|
|
265
|
+
const tab = await getTabById(tabId);
|
|
266
|
+
|
|
267
|
+
const result = await chrome.scripting.executeScript({
|
|
268
|
+
target: { tabId: tab.id },
|
|
269
|
+
func: () => {
|
|
270
|
+
// Build accessibility tree snapshot
|
|
271
|
+
function getAccessibleName(element) {
|
|
272
|
+
return element.getAttribute("aria-label") ||
|
|
273
|
+
element.getAttribute("alt") ||
|
|
274
|
+
element.getAttribute("title") ||
|
|
275
|
+
element.getAttribute("placeholder") ||
|
|
276
|
+
element.innerText?.slice(0, 100) ||
|
|
277
|
+
"";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function getRole(element) {
|
|
281
|
+
return element.getAttribute("role") ||
|
|
282
|
+
element.tagName.toLowerCase();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildSnapshot(element, depth = 0, uid = 0) {
|
|
286
|
+
if (depth > 10) return { nodes: [], nextUid: uid };
|
|
287
|
+
|
|
288
|
+
const nodes = [];
|
|
289
|
+
const style = window.getComputedStyle(element);
|
|
290
|
+
|
|
291
|
+
// Skip hidden elements
|
|
292
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
293
|
+
return { nodes: [], nextUid: uid };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const isInteractive =
|
|
297
|
+
element.tagName === "A" ||
|
|
298
|
+
element.tagName === "BUTTON" ||
|
|
299
|
+
element.tagName === "INPUT" ||
|
|
300
|
+
element.tagName === "TEXTAREA" ||
|
|
301
|
+
element.tagName === "SELECT" ||
|
|
302
|
+
element.getAttribute("onclick") ||
|
|
303
|
+
element.getAttribute("role") === "button" ||
|
|
304
|
+
element.isContentEditable;
|
|
305
|
+
|
|
306
|
+
const rect = element.getBoundingClientRect();
|
|
307
|
+
const isVisible = rect.width > 0 && rect.height > 0;
|
|
308
|
+
|
|
309
|
+
if (isVisible && (isInteractive || element.innerText?.trim())) {
|
|
310
|
+
const node = {
|
|
311
|
+
uid: `e${uid}`,
|
|
312
|
+
role: getRole(element),
|
|
313
|
+
name: getAccessibleName(element).slice(0, 200),
|
|
314
|
+
tag: element.tagName.toLowerCase()
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
if (element.tagName === "A" && element.href) {
|
|
318
|
+
node.href = element.href;
|
|
319
|
+
}
|
|
320
|
+
if (element.tagName === "INPUT") {
|
|
321
|
+
node.type = element.type;
|
|
322
|
+
node.value = element.value;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Generate a selector
|
|
326
|
+
if (element.id) {
|
|
327
|
+
node.selector = `#${element.id}`;
|
|
328
|
+
} else if (element.className && typeof element.className === "string") {
|
|
329
|
+
const classes = element.className.trim().split(/\s+/).slice(0, 2).join(".");
|
|
330
|
+
if (classes) node.selector = `${element.tagName.toLowerCase()}.${classes}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
nodes.push(node);
|
|
334
|
+
uid++;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
for (const child of element.children) {
|
|
338
|
+
const childResult = buildSnapshot(child, depth + 1, uid);
|
|
339
|
+
nodes.push(...childResult.nodes);
|
|
340
|
+
uid = childResult.nextUid;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { nodes, nextUid: uid };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const { nodes } = buildSnapshot(document.body);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
url: window.location.href,
|
|
350
|
+
title: document.title,
|
|
351
|
+
nodes: nodes.slice(0, 500) // Limit to 500 nodes
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return JSON.stringify(result[0]?.result, null, 2);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function toolGetTabs() {
|
|
360
|
+
const tabs = await chrome.tabs.query({});
|
|
361
|
+
return JSON.stringify(tabs.map(t => ({
|
|
362
|
+
id: t.id,
|
|
363
|
+
url: t.url,
|
|
364
|
+
title: t.title,
|
|
365
|
+
active: t.active,
|
|
366
|
+
windowId: t.windowId
|
|
367
|
+
})), null, 2);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function toolExecuteScript({ code, tabId }) {
|
|
371
|
+
if (!code) throw new Error("Code is required");
|
|
372
|
+
|
|
373
|
+
const tab = await getTabById(tabId);
|
|
374
|
+
|
|
375
|
+
const result = await chrome.scripting.executeScript({
|
|
376
|
+
target: { tabId: tab.id },
|
|
377
|
+
func: new Function(code)
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return JSON.stringify(result[0]?.result);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function toolScroll({ x = 0, y = 0, selector, tabId }) {
|
|
384
|
+
const tab = await getTabById(tabId);
|
|
385
|
+
|
|
386
|
+
// Ensure selector is null (not undefined) for proper serialization
|
|
387
|
+
const sel = selector || null;
|
|
388
|
+
|
|
389
|
+
await chrome.scripting.executeScript({
|
|
390
|
+
target: { tabId: tab.id },
|
|
391
|
+
func: (scrollX, scrollY, sel) => {
|
|
392
|
+
if (sel) {
|
|
393
|
+
const element = document.querySelector(sel);
|
|
394
|
+
if (element) {
|
|
395
|
+
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
window.scrollBy(scrollX, scrollY);
|
|
400
|
+
},
|
|
401
|
+
args: [x, y, sel]
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
return `Scrolled ${sel ? `to ${sel}` : `by (${x}, ${y})`}`;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function toolWait({ ms = 1000 }) {
|
|
408
|
+
await new Promise(resolve => setTimeout(resolve, ms));
|
|
409
|
+
return `Waited ${ms}ms`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// Extension Lifecycle
|
|
414
|
+
// ============================================================================
|
|
415
|
+
|
|
416
|
+
chrome.runtime.onInstalled.addListener(async () => {
|
|
417
|
+
console.log("[OpenCode] Extension installed");
|
|
418
|
+
await connectToNativeHost();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
chrome.runtime.onStartup.addListener(async () => {
|
|
422
|
+
console.log("[OpenCode] Extension started");
|
|
423
|
+
await connectToNativeHost();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Auto-reconnect on action click
|
|
427
|
+
chrome.action.onClicked.addListener(async () => {
|
|
428
|
+
if (!isConnected) {
|
|
429
|
+
const connected = await connectToNativeHost();
|
|
430
|
+
if (connected) {
|
|
431
|
+
chrome.notifications.create({
|
|
432
|
+
type: "basic",
|
|
433
|
+
iconUrl: "icons/icon128.png",
|
|
434
|
+
title: "OpenCode Browser",
|
|
435
|
+
message: "Connected to native host"
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
chrome.notifications.create({
|
|
439
|
+
type: "basic",
|
|
440
|
+
iconUrl: "icons/icon128.png",
|
|
441
|
+
title: "OpenCode Browser",
|
|
442
|
+
message: "Failed to connect. Is the native host installed?"
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
chrome.notifications.create({
|
|
447
|
+
type: "basic",
|
|
448
|
+
iconUrl: "icons/icon128.png",
|
|
449
|
+
title: "OpenCode Browser",
|
|
450
|
+
message: "Already connected"
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Try to connect on load
|
|
456
|
+
connectToNativeHost();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "OpenCode Browser Automation",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Browser automation for OpenCode via Native Messaging",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"tabs",
|
|
8
|
+
"activeTab",
|
|
9
|
+
"scripting",
|
|
10
|
+
"nativeMessaging",
|
|
11
|
+
"storage",
|
|
12
|
+
"notifications"
|
|
13
|
+
],
|
|
14
|
+
"host_permissions": [
|
|
15
|
+
"<all_urls>"
|
|
16
|
+
],
|
|
17
|
+
"background": {
|
|
18
|
+
"service_worker": "background.js",
|
|
19
|
+
"type": "module"
|
|
20
|
+
},
|
|
21
|
+
"action": {
|
|
22
|
+
"default_title": "OpenCode Browser",
|
|
23
|
+
"default_icon": {
|
|
24
|
+
"16": "icons/icon16.png",
|
|
25
|
+
"48": "icons/icon48.png",
|
|
26
|
+
"128": "icons/icon128.png"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"icons": {
|
|
30
|
+
"16": "icons/icon16.png",
|
|
31
|
+
"48": "icons/icon48.png",
|
|
32
|
+
"128": "icons/icon128.png"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@different-ai/opencode-browser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Browser automation for OpenCode via Chrome extension + Native Messaging. Inspired by Claude in Chrome.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"opencode-browser": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/server.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/server.js",
|
|
12
|
+
"install-extension": "node bin/cli.js install"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"opencode",
|
|
16
|
+
"browser",
|
|
17
|
+
"automation",
|
|
18
|
+
"chrome",
|
|
19
|
+
"mcp",
|
|
20
|
+
"claude"
|
|
21
|
+
],
|
|
22
|
+
"author": "Benjamin Shafii",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/different-ai/opencode-browser.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/different-ai/opencode-browser/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/different-ai/opencode-browser#readme",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|