@different-ai/opencode-browser 1.0.4 → 2.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 +78 -52
- package/bin/cli.js +151 -209
- package/extension/background.js +3 -3
- package/extension/manifest.json +2 -3
- package/package.json +20 -9
- package/src/plugin.ts +450 -0
- package/src/daemon.js +0 -207
- package/src/host.js +0 -282
- package/src/server.js +0 -379
package/src/daemon.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Persistent Browser Bridge Daemon
|
|
4
|
-
*
|
|
5
|
-
* Runs as a background service and bridges:
|
|
6
|
-
* - Chrome extension (via WebSocket on localhost)
|
|
7
|
-
* - MCP server (via Unix socket)
|
|
8
|
-
*
|
|
9
|
-
* This allows scheduled jobs to use browser tools even if
|
|
10
|
-
* the OpenCode session that created the job isn't running.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { createServer as createNetServer } from "net";
|
|
14
|
-
import { WebSocketServer } from "ws";
|
|
15
|
-
import { existsSync, mkdirSync, unlinkSync, appendFileSync } from "fs";
|
|
16
|
-
import { homedir } from "os";
|
|
17
|
-
import { join } from "path";
|
|
18
|
-
|
|
19
|
-
const BASE_DIR = join(homedir(), ".opencode-browser");
|
|
20
|
-
const LOG_DIR = join(BASE_DIR, "logs");
|
|
21
|
-
const SOCKET_PATH = join(BASE_DIR, "browser.sock");
|
|
22
|
-
const WS_PORT = 19222;
|
|
23
|
-
|
|
24
|
-
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
|
|
25
|
-
const LOG_FILE = join(LOG_DIR, "daemon.log");
|
|
26
|
-
|
|
27
|
-
function log(...args) {
|
|
28
|
-
const timestamp = new Date().toISOString();
|
|
29
|
-
const message = `[${timestamp}] ${args.join(" ")}\n`;
|
|
30
|
-
appendFileSync(LOG_FILE, message);
|
|
31
|
-
console.error(message.trim());
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
log("Daemon starting...");
|
|
35
|
-
|
|
36
|
-
// State
|
|
37
|
-
let chromeConnection = null;
|
|
38
|
-
let mcpConnections = new Set();
|
|
39
|
-
let pendingRequests = new Map();
|
|
40
|
-
let requestId = 0;
|
|
41
|
-
|
|
42
|
-
// ============================================================================
|
|
43
|
-
// WebSocket Server for Chrome Extension
|
|
44
|
-
// ============================================================================
|
|
45
|
-
|
|
46
|
-
const wss = new WebSocketServer({ port: WS_PORT });
|
|
47
|
-
|
|
48
|
-
wss.on("connection", (ws) => {
|
|
49
|
-
log("Chrome extension connected via WebSocket");
|
|
50
|
-
chromeConnection = ws;
|
|
51
|
-
|
|
52
|
-
ws.on("message", (data) => {
|
|
53
|
-
try {
|
|
54
|
-
const message = JSON.parse(data.toString());
|
|
55
|
-
handleChromeMessage(message);
|
|
56
|
-
} catch (e) {
|
|
57
|
-
log("Failed to parse Chrome message:", e.message);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
ws.on("close", () => {
|
|
62
|
-
log("Chrome extension disconnected");
|
|
63
|
-
chromeConnection = null;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
ws.on("error", (err) => {
|
|
67
|
-
log("Chrome WebSocket error:", err.message);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
wss.on("listening", () => {
|
|
72
|
-
log(`WebSocket server listening on port ${WS_PORT}`);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
function sendToChrome(message) {
|
|
76
|
-
if (chromeConnection && chromeConnection.readyState === 1) {
|
|
77
|
-
chromeConnection.send(JSON.stringify(message));
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function handleChromeMessage(message) {
|
|
84
|
-
log("From Chrome:", message.type);
|
|
85
|
-
|
|
86
|
-
if (message.type === "tool_response") {
|
|
87
|
-
const pending = pendingRequests.get(message.id);
|
|
88
|
-
if (pending) {
|
|
89
|
-
pendingRequests.delete(message.id);
|
|
90
|
-
sendToMcp(pending.socket, {
|
|
91
|
-
type: "tool_response",
|
|
92
|
-
id: pending.mcpId,
|
|
93
|
-
result: message.result,
|
|
94
|
-
error: message.error
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
} else if (message.type === "pong") {
|
|
98
|
-
log("Chrome ping OK");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ============================================================================
|
|
103
|
-
// Unix Socket Server for MCP
|
|
104
|
-
// ============================================================================
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
if (existsSync(SOCKET_PATH)) {
|
|
108
|
-
unlinkSync(SOCKET_PATH);
|
|
109
|
-
}
|
|
110
|
-
} catch {}
|
|
111
|
-
|
|
112
|
-
const unixServer = createNetServer((socket) => {
|
|
113
|
-
log("MCP server connected");
|
|
114
|
-
mcpConnections.add(socket);
|
|
115
|
-
|
|
116
|
-
let buffer = "";
|
|
117
|
-
|
|
118
|
-
socket.on("data", (data) => {
|
|
119
|
-
buffer += data.toString();
|
|
120
|
-
|
|
121
|
-
const lines = buffer.split("\n");
|
|
122
|
-
buffer = lines.pop() || "";
|
|
123
|
-
|
|
124
|
-
for (const line of lines) {
|
|
125
|
-
if (line.trim()) {
|
|
126
|
-
try {
|
|
127
|
-
const message = JSON.parse(line);
|
|
128
|
-
handleMcpMessage(socket, message);
|
|
129
|
-
} catch (e) {
|
|
130
|
-
log("Failed to parse MCP message:", e.message);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
socket.on("close", () => {
|
|
137
|
-
log("MCP server disconnected");
|
|
138
|
-
mcpConnections.delete(socket);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
socket.on("error", (err) => {
|
|
142
|
-
log("MCP socket error:", err.message);
|
|
143
|
-
mcpConnections.delete(socket);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
unixServer.listen(SOCKET_PATH, () => {
|
|
148
|
-
log(`Unix socket listening at ${SOCKET_PATH}`);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
function sendToMcp(socket, message) {
|
|
152
|
-
if (socket && !socket.destroyed) {
|
|
153
|
-
socket.write(JSON.stringify(message) + "\n");
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function handleMcpMessage(socket, message) {
|
|
158
|
-
log("From MCP:", message.type, message.tool || "");
|
|
159
|
-
|
|
160
|
-
if (message.type === "tool_request") {
|
|
161
|
-
if (!chromeConnection) {
|
|
162
|
-
sendToMcp(socket, {
|
|
163
|
-
type: "tool_response",
|
|
164
|
-
id: message.id,
|
|
165
|
-
error: { content: "Chrome extension not connected. Open Chrome and ensure the OpenCode extension is enabled." }
|
|
166
|
-
});
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const id = ++requestId;
|
|
171
|
-
pendingRequests.set(id, { socket, mcpId: message.id });
|
|
172
|
-
|
|
173
|
-
sendToChrome({
|
|
174
|
-
type: "tool_request",
|
|
175
|
-
id,
|
|
176
|
-
tool: message.tool,
|
|
177
|
-
args: message.args
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ============================================================================
|
|
183
|
-
// Health Check
|
|
184
|
-
// ============================================================================
|
|
185
|
-
|
|
186
|
-
setInterval(() => {
|
|
187
|
-
if (chromeConnection) {
|
|
188
|
-
sendToChrome({ type: "ping" });
|
|
189
|
-
}
|
|
190
|
-
}, 30000);
|
|
191
|
-
|
|
192
|
-
// ============================================================================
|
|
193
|
-
// Graceful Shutdown
|
|
194
|
-
// ============================================================================
|
|
195
|
-
|
|
196
|
-
function shutdown() {
|
|
197
|
-
log("Shutting down...");
|
|
198
|
-
wss.close();
|
|
199
|
-
unixServer.close();
|
|
200
|
-
try { unlinkSync(SOCKET_PATH); } catch {}
|
|
201
|
-
process.exit(0);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
process.on("SIGTERM", shutdown);
|
|
205
|
-
process.on("SIGINT", shutdown);
|
|
206
|
-
|
|
207
|
-
log("Daemon started successfully");
|
package/src/host.js
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Native Messaging Host for OpenCode Browser Automation
|
|
4
|
-
*
|
|
5
|
-
* This script is launched by Chrome when the extension connects.
|
|
6
|
-
* It communicates with Chrome via stdin/stdout using Chrome's native messaging protocol.
|
|
7
|
-
* It also connects to an MCP server (or acts as one) to receive tool requests.
|
|
8
|
-
*
|
|
9
|
-
* Chrome Native Messaging Protocol:
|
|
10
|
-
* - Messages are length-prefixed (4 bytes, little-endian, uint32)
|
|
11
|
-
* - Message body is JSON
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { createServer } from "net";
|
|
15
|
-
import { writeFileSync, appendFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
|
|
16
|
-
import { homedir } from "os";
|
|
17
|
-
import { join } from "path";
|
|
18
|
-
|
|
19
|
-
const LOG_DIR = join(homedir(), ".opencode-browser", "logs");
|
|
20
|
-
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
|
|
21
|
-
const LOG_FILE = join(LOG_DIR, "host.log");
|
|
22
|
-
|
|
23
|
-
function log(...args) {
|
|
24
|
-
const timestamp = new Date().toISOString();
|
|
25
|
-
const message = `[${timestamp}] ${args.join(" ")}\n`;
|
|
26
|
-
appendFileSync(LOG_FILE, message);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
log("Native host started");
|
|
30
|
-
|
|
31
|
-
// ============================================================================
|
|
32
|
-
// Chrome Native Messaging Protocol
|
|
33
|
-
// ============================================================================
|
|
34
|
-
|
|
35
|
-
function readMessage() {
|
|
36
|
-
return new Promise((resolve, reject) => {
|
|
37
|
-
let lengthBuffer = Buffer.alloc(0);
|
|
38
|
-
let messageBuffer = Buffer.alloc(0);
|
|
39
|
-
let messageLength = null;
|
|
40
|
-
|
|
41
|
-
const processData = () => {
|
|
42
|
-
// First, read the 4-byte length prefix
|
|
43
|
-
if (messageLength === null) {
|
|
44
|
-
const needed = 4 - lengthBuffer.length;
|
|
45
|
-
const chunk = process.stdin.read(needed);
|
|
46
|
-
if (chunk === null) {
|
|
47
|
-
process.stdin.once("readable", processData);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
52
|
-
lengthBuffer = Buffer.concat([lengthBuffer, chunkBuf]);
|
|
53
|
-
|
|
54
|
-
if (lengthBuffer.length < 4) {
|
|
55
|
-
process.stdin.once("readable", processData);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
messageLength = lengthBuffer.readUInt32LE(0);
|
|
60
|
-
if (messageLength === 0) {
|
|
61
|
-
resolve(null);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Now read the message body
|
|
67
|
-
const needed = messageLength - messageBuffer.length;
|
|
68
|
-
const chunk = process.stdin.read(needed);
|
|
69
|
-
if (chunk === null) {
|
|
70
|
-
process.stdin.once("readable", processData);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
75
|
-
messageBuffer = Buffer.concat([messageBuffer, chunkBuf]);
|
|
76
|
-
|
|
77
|
-
if (messageBuffer.length < messageLength) {
|
|
78
|
-
process.stdin.once("readable", processData);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const message = JSON.parse(messageBuffer.toString("utf8"));
|
|
84
|
-
resolve(message);
|
|
85
|
-
} catch (e) {
|
|
86
|
-
reject(new Error(`Failed to parse message: ${e.message}`));
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
processData();
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function writeMessage(message) {
|
|
95
|
-
const json = JSON.stringify(message);
|
|
96
|
-
const buffer = Buffer.from(json, "utf8");
|
|
97
|
-
const lengthBuffer = Buffer.alloc(4);
|
|
98
|
-
lengthBuffer.writeUInt32LE(buffer.length, 0);
|
|
99
|
-
|
|
100
|
-
process.stdout.write(lengthBuffer);
|
|
101
|
-
process.stdout.write(buffer);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ============================================================================
|
|
105
|
-
// MCP Server Connection
|
|
106
|
-
// ============================================================================
|
|
107
|
-
|
|
108
|
-
const SOCKET_PATH = join(homedir(), ".opencode-browser", "browser.sock");
|
|
109
|
-
let mcpConnected = false;
|
|
110
|
-
let mcpSocket = null;
|
|
111
|
-
let pendingRequests = new Map();
|
|
112
|
-
let requestId = 0;
|
|
113
|
-
|
|
114
|
-
function connectToMcpServer() {
|
|
115
|
-
// We'll create a Unix socket server that the MCP server connects to
|
|
116
|
-
// This way the host can receive tool requests from OpenCode
|
|
117
|
-
|
|
118
|
-
// Clean up old socket
|
|
119
|
-
try {
|
|
120
|
-
if (existsSync(SOCKET_PATH)) {
|
|
121
|
-
unlinkSync(SOCKET_PATH);
|
|
122
|
-
}
|
|
123
|
-
} catch {}
|
|
124
|
-
|
|
125
|
-
const server = createServer((socket) => {
|
|
126
|
-
log("MCP server connected");
|
|
127
|
-
mcpSocket = socket;
|
|
128
|
-
mcpConnected = true;
|
|
129
|
-
|
|
130
|
-
// Notify extension
|
|
131
|
-
writeMessage({ type: "mcp_connected" });
|
|
132
|
-
|
|
133
|
-
let buffer = "";
|
|
134
|
-
|
|
135
|
-
socket.on("data", (data) => {
|
|
136
|
-
buffer += data.toString();
|
|
137
|
-
|
|
138
|
-
// Process complete JSON messages (newline-delimited)
|
|
139
|
-
const lines = buffer.split("\n");
|
|
140
|
-
buffer = lines.pop() || "";
|
|
141
|
-
|
|
142
|
-
for (const line of lines) {
|
|
143
|
-
if (line.trim()) {
|
|
144
|
-
try {
|
|
145
|
-
const message = JSON.parse(line);
|
|
146
|
-
handleMcpMessage(message);
|
|
147
|
-
} catch (e) {
|
|
148
|
-
log("Failed to parse MCP message:", e.message);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
socket.on("close", () => {
|
|
155
|
-
log("MCP server disconnected");
|
|
156
|
-
mcpSocket = null;
|
|
157
|
-
mcpConnected = false;
|
|
158
|
-
writeMessage({ type: "mcp_disconnected" });
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
socket.on("error", (err) => {
|
|
162
|
-
log("MCP socket error:", err.message);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
server.listen(SOCKET_PATH, () => {
|
|
167
|
-
log("Listening for MCP connections on", SOCKET_PATH);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
server.on("error", (err) => {
|
|
171
|
-
log("Server error:", err.message);
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function handleMcpMessage(message) {
|
|
176
|
-
log("Received from MCP:", JSON.stringify(message));
|
|
177
|
-
|
|
178
|
-
if (message.type === "tool_request") {
|
|
179
|
-
// Forward tool request to Chrome extension
|
|
180
|
-
const id = ++requestId;
|
|
181
|
-
pendingRequests.set(id, message.id); // Map our ID to MCP's ID
|
|
182
|
-
|
|
183
|
-
writeMessage({
|
|
184
|
-
type: "tool_request",
|
|
185
|
-
id,
|
|
186
|
-
tool: message.tool,
|
|
187
|
-
args: message.args
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function sendToMcp(message) {
|
|
193
|
-
if (mcpSocket && !mcpSocket.destroyed) {
|
|
194
|
-
mcpSocket.write(JSON.stringify(message) + "\n");
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ============================================================================
|
|
199
|
-
// Handle Messages from Chrome Extension
|
|
200
|
-
// ============================================================================
|
|
201
|
-
|
|
202
|
-
async function handleChromeMessage(message) {
|
|
203
|
-
log("Received from Chrome:", JSON.stringify(message));
|
|
204
|
-
|
|
205
|
-
switch (message.type) {
|
|
206
|
-
case "ping":
|
|
207
|
-
writeMessage({ type: "pong" });
|
|
208
|
-
break;
|
|
209
|
-
|
|
210
|
-
case "tool_response":
|
|
211
|
-
// Forward response back to MCP server
|
|
212
|
-
const mcpId = pendingRequests.get(message.id);
|
|
213
|
-
if (mcpId !== undefined) {
|
|
214
|
-
pendingRequests.delete(message.id);
|
|
215
|
-
sendToMcp({
|
|
216
|
-
type: "tool_response",
|
|
217
|
-
id: mcpId,
|
|
218
|
-
result: message.result,
|
|
219
|
-
error: message.error
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
break;
|
|
223
|
-
|
|
224
|
-
case "get_status":
|
|
225
|
-
writeMessage({
|
|
226
|
-
type: "status_response",
|
|
227
|
-
mcpConnected
|
|
228
|
-
});
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ============================================================================
|
|
234
|
-
// Main Loop
|
|
235
|
-
// ============================================================================
|
|
236
|
-
|
|
237
|
-
async function main() {
|
|
238
|
-
process.stdin.on("end", () => {
|
|
239
|
-
log("stdin ended, Chrome disconnected");
|
|
240
|
-
process.exit(0);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
process.stdin.on("close", () => {
|
|
244
|
-
log("stdin closed, Chrome disconnected");
|
|
245
|
-
process.exit(0);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
connectToMcpServer();
|
|
249
|
-
|
|
250
|
-
while (true) {
|
|
251
|
-
try {
|
|
252
|
-
const message = await readMessage();
|
|
253
|
-
if (message === null) {
|
|
254
|
-
log("Received null message, exiting");
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
await handleChromeMessage(message);
|
|
258
|
-
} catch (error) {
|
|
259
|
-
log("Error reading message:", error.message);
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
log("Native host exiting");
|
|
265
|
-
process.exit(0);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Handle graceful shutdown
|
|
269
|
-
process.on("SIGTERM", () => {
|
|
270
|
-
log("Received SIGTERM");
|
|
271
|
-
process.exit(0);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
process.on("SIGINT", () => {
|
|
275
|
-
log("Received SIGINT");
|
|
276
|
-
process.exit(0);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
main().catch((error) => {
|
|
280
|
-
log("Fatal error:", error.message);
|
|
281
|
-
process.exit(1);
|
|
282
|
-
});
|