@frontendxlab/opencode-computer-use 0.1.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/LICENSE +25 -0
- package/README.md +285 -0
- package/SECURITY.md +36 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/opencode-computer-use-repl.js +12 -0
- package/docs/_headers +12 -0
- package/docs/favicon.svg +4 -0
- package/docs/index.html +733 -0
- package/docs/llms-full.txt +146 -0
- package/docs/llms.txt +30 -0
- package/docs/og-card.png +0 -0
- package/docs/robots.txt +7 -0
- package/docs/site.webmanifest +10 -0
- package/docs/sitemap.xml +9 -0
- package/docs/webmcp.js +135 -0
- package/index.js +78 -0
- package/lib/backend.js +190 -0
- package/lib/json-line-peer.mjs +182 -0
- package/lib/options.js +111 -0
- package/lib/output.mjs +212 -0
- package/lib/repl-kernel.mjs +41 -0
- package/lib/repl.mjs +615 -0
- package/lib/sandbox.mjs +291 -0
- package/package.json +57 -0
package/lib/sandbox.mjs
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { runInContext } from "node:vm";
|
|
2
|
+
|
|
3
|
+
const MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
const SANDBOX_FACTORY_SOURCE = String.raw`
|
|
6
|
+
((dispatch, write, emitImage, schedule, cancel) => {
|
|
7
|
+
const decodeBase64 = value => {
|
|
8
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
9
|
+
const bytes = [];
|
|
10
|
+
let buffer = 0;
|
|
11
|
+
let bits = 0;
|
|
12
|
+
for (const character of value.replace(/=+$/u, "")) {
|
|
13
|
+
const index = alphabet.indexOf(character);
|
|
14
|
+
if (index < 0) throw new Error("invalid image data");
|
|
15
|
+
buffer = (buffer << 6) | index;
|
|
16
|
+
bits += 6;
|
|
17
|
+
if (bits >= 8) {
|
|
18
|
+
bits -= 8;
|
|
19
|
+
bytes.push((buffer >> bits) & 255);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return new Uint8Array(bytes);
|
|
23
|
+
};
|
|
24
|
+
const revive = value => {
|
|
25
|
+
if (value === null || typeof value !== "object") return value;
|
|
26
|
+
if (value.type === "bytes") return decodeBase64(value.data);
|
|
27
|
+
if (Array.isArray(value)) return value.map(revive);
|
|
28
|
+
const result = Object.create(null);
|
|
29
|
+
for (const [key, child] of Object.entries(value)) {
|
|
30
|
+
Object.defineProperty(result, key, {
|
|
31
|
+
value: revive(child),
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
const decode = payload => {
|
|
40
|
+
const message = JSON.parse(payload);
|
|
41
|
+
if (!message.ok) throw new Error(message.error);
|
|
42
|
+
return revive(message.value);
|
|
43
|
+
};
|
|
44
|
+
const call = (method, args) => Promise.resolve(dispatch(method, args)).then(decode);
|
|
45
|
+
const appCall = (token, method, args) => call("app:" + method, [token, ...args]);
|
|
46
|
+
const app = token => Object.freeze({
|
|
47
|
+
getAXState: (...args) => appCall(token, "getAXState", args),
|
|
48
|
+
getScreenshot: (...args) => appCall(token, "getScreenshot", args),
|
|
49
|
+
getAXStateAndScreenshot: (...args) => appCall(token, "getAXStateAndScreenshot", args),
|
|
50
|
+
click: (...args) => appCall(token, "click", args),
|
|
51
|
+
drag: (...args) => appCall(token, "drag", args),
|
|
52
|
+
pressKey: (...args) => appCall(token, "pressKey", args),
|
|
53
|
+
scroll: (...args) => appCall(token, "scroll", args),
|
|
54
|
+
setValue: (...args) => appCall(token, "setValue", args),
|
|
55
|
+
typeText: (...args) => appCall(token, "typeText", args),
|
|
56
|
+
performSecondaryAction: (...args) => appCall(token, "performSecondaryAction", args),
|
|
57
|
+
});
|
|
58
|
+
const cua = Object.freeze({
|
|
59
|
+
getState: (...args) => call("getState", args),
|
|
60
|
+
listApps: (...args) => call("listApps", args),
|
|
61
|
+
getApp: name => call("getApp", [name]).then(app),
|
|
62
|
+
rewriteDocumentation: (...args) => call("rewriteDocumentation", args),
|
|
63
|
+
});
|
|
64
|
+
const nodeRepl = Object.freeze({
|
|
65
|
+
write: (value, itemId) => { write(value, itemId); },
|
|
66
|
+
emitImage: value => Promise.resolve(emitImage(value)),
|
|
67
|
+
});
|
|
68
|
+
const timers = new Map();
|
|
69
|
+
let nextTimer = 1;
|
|
70
|
+
const setTimeoutSafe = (callback, delay, ...args) => {
|
|
71
|
+
const id = nextTimer++;
|
|
72
|
+
timers.set(id, schedule(() => {
|
|
73
|
+
timers.delete(id);
|
|
74
|
+
callback(...args);
|
|
75
|
+
}, delay));
|
|
76
|
+
return id;
|
|
77
|
+
};
|
|
78
|
+
const clearTimeoutSafe = id => {
|
|
79
|
+
if (!timers.has(id)) return;
|
|
80
|
+
cancel(timers.get(id));
|
|
81
|
+
timers.delete(id);
|
|
82
|
+
};
|
|
83
|
+
return Object.freeze({ cua, nodeRepl, setTimeout: setTimeoutSafe, clearTimeout: clearTimeoutSafe });
|
|
84
|
+
})
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const SAFE_SANDBOX_GLOBALS = new Set([
|
|
88
|
+
"globalThis",
|
|
89
|
+
"Object",
|
|
90
|
+
"Function",
|
|
91
|
+
"Array",
|
|
92
|
+
"ArrayBuffer",
|
|
93
|
+
"Boolean",
|
|
94
|
+
"BigInt",
|
|
95
|
+
"DataView",
|
|
96
|
+
"Date",
|
|
97
|
+
"Error",
|
|
98
|
+
"EvalError",
|
|
99
|
+
"Infinity",
|
|
100
|
+
"Intl",
|
|
101
|
+
"JSON",
|
|
102
|
+
"Map",
|
|
103
|
+
"Math",
|
|
104
|
+
"NaN",
|
|
105
|
+
"Number",
|
|
106
|
+
"Promise",
|
|
107
|
+
"Proxy",
|
|
108
|
+
"Reflect",
|
|
109
|
+
"RegExp",
|
|
110
|
+
"Set",
|
|
111
|
+
"String",
|
|
112
|
+
"Symbol",
|
|
113
|
+
"SyntaxError",
|
|
114
|
+
"TypeError",
|
|
115
|
+
"URIError",
|
|
116
|
+
"WeakMap",
|
|
117
|
+
"WeakRef",
|
|
118
|
+
"WeakSet",
|
|
119
|
+
"decodeURI",
|
|
120
|
+
"decodeURIComponent",
|
|
121
|
+
"encodeURI",
|
|
122
|
+
"encodeURIComponent",
|
|
123
|
+
"escape",
|
|
124
|
+
"eval",
|
|
125
|
+
"isFinite",
|
|
126
|
+
"isNaN",
|
|
127
|
+
"parseFloat",
|
|
128
|
+
"parseInt",
|
|
129
|
+
"undefined",
|
|
130
|
+
"AsyncDisposableStack",
|
|
131
|
+
"DisposableStack",
|
|
132
|
+
"Iterator",
|
|
133
|
+
"Int8Array",
|
|
134
|
+
"Uint8Array",
|
|
135
|
+
"Uint8ClampedArray",
|
|
136
|
+
"Int16Array",
|
|
137
|
+
"Uint16Array",
|
|
138
|
+
"Int32Array",
|
|
139
|
+
"Uint32Array",
|
|
140
|
+
"Float16Array",
|
|
141
|
+
"Float32Array",
|
|
142
|
+
"Float64Array",
|
|
143
|
+
"BigInt64Array",
|
|
144
|
+
"BigUint64Array",
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
function asError(error) {
|
|
148
|
+
if (error instanceof Error) return error;
|
|
149
|
+
return new Error(typeof error?.message === "string" ? error.message : String(error));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function safeErrorMessage(error) {
|
|
153
|
+
return asError(error).message
|
|
154
|
+
.replace(/\b(token|secret|password|api[_-]?key|authorization)\s*[:=]\s*(?:bearer|basic)?[^\n\r,;]*/giu, "$1=[redacted]")
|
|
155
|
+
.slice(0, 4_000);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function hardenSandboxContext(context) {
|
|
159
|
+
const blockedCodeGeneration = runInContext(
|
|
160
|
+
'(function blockedCodeGeneration() { throw new Error("code generation is disabled in the CUA sandbox"); })',
|
|
161
|
+
context,
|
|
162
|
+
);
|
|
163
|
+
const prototypes = runInContext(
|
|
164
|
+
"[Object.getPrototypeOf(() => {}), Object.getPrototypeOf(async () => {}), Object.getPrototypeOf(function* () {}), Object.getPrototypeOf(async function* () {})]",
|
|
165
|
+
context,
|
|
166
|
+
);
|
|
167
|
+
for (const prototype of prototypes) {
|
|
168
|
+
Object.defineProperty(prototype, "constructor", {
|
|
169
|
+
value: blockedCodeGeneration,
|
|
170
|
+
configurable: false,
|
|
171
|
+
writable: false,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
for (const name of ["Function", "eval", "WebAssembly", "SharedArrayBuffer", "Atomics"]) {
|
|
175
|
+
Object.defineProperty(context, name, {
|
|
176
|
+
value: undefined,
|
|
177
|
+
configurable: false,
|
|
178
|
+
writable: false,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function assertSandboxCode(code) {
|
|
184
|
+
if (/(^|[^A-Za-z0-9_$])import([^A-Za-z0-9_$]|$)/u.test(code)) {
|
|
185
|
+
throw new Error("dynamic imports are disabled in the CUA sandbox");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function sanitizeSandboxContext(context) {
|
|
190
|
+
for (const name of Object.getOwnPropertyNames(context)) {
|
|
191
|
+
if (SAFE_SANDBOX_GLOBALS.has(name)) continue;
|
|
192
|
+
try {
|
|
193
|
+
delete context[name];
|
|
194
|
+
} catch {
|
|
195
|
+
throw new Error(`could not remove sandbox global: ${name}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function encodeSandboxValue(value, seen = new Set()) {
|
|
201
|
+
if (value === undefined) return null;
|
|
202
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
203
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
204
|
+
if (Buffer.isBuffer(value) || value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
|
|
205
|
+
const bytes = Buffer.isBuffer(value)
|
|
206
|
+
? value
|
|
207
|
+
: value instanceof ArrayBuffer
|
|
208
|
+
? new Uint8Array(value)
|
|
209
|
+
: new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
210
|
+
return { type: "bytes", data: bytes.toString("base64") };
|
|
211
|
+
}
|
|
212
|
+
if (typeof value !== "object") return String(value);
|
|
213
|
+
if (seen.has(value)) throw new Error("CUA result contained a cycle");
|
|
214
|
+
seen.add(value);
|
|
215
|
+
try {
|
|
216
|
+
if (Array.isArray(value)) return value.map((item) => encodeSandboxValue(item, seen));
|
|
217
|
+
const result = Object.create(null);
|
|
218
|
+
for (const [key, child] of Object.entries(value)) {
|
|
219
|
+
Object.defineProperty(result, key, {
|
|
220
|
+
value: encodeSandboxValue(child, seen),
|
|
221
|
+
enumerable: true,
|
|
222
|
+
configurable: true,
|
|
223
|
+
writable: true,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return result;
|
|
227
|
+
} finally {
|
|
228
|
+
seen.delete(value);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function encodeSandboxResult(value) {
|
|
233
|
+
const payload = JSON.stringify({ ok: true, value: encodeSandboxValue(value) });
|
|
234
|
+
if (Buffer.byteLength(payload, "utf8") > MAX_FRAME_BYTES) {
|
|
235
|
+
throw new Error("CUA result exceeded the sandbox output limit");
|
|
236
|
+
}
|
|
237
|
+
return payload;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function createSandboxBridge(api, context, activeOutput, isActive, schedule, cancel) {
|
|
241
|
+
const bindings = new Map();
|
|
242
|
+
let nextBinding = 1;
|
|
243
|
+
const dispatch = async (method, args) => {
|
|
244
|
+
try {
|
|
245
|
+
if (!isActive()) throw new Error("CUA calls are only available while js is executing");
|
|
246
|
+
if (method === "getState") {
|
|
247
|
+
const state = await api.getState({ emit: false });
|
|
248
|
+
return encodeSandboxResult({ apps: state.apps });
|
|
249
|
+
}
|
|
250
|
+
if (method === "listApps") return encodeSandboxResult(await api.listApps({ emit: false }));
|
|
251
|
+
if (method === "getApp") {
|
|
252
|
+
const app = args[0];
|
|
253
|
+
if (typeof app !== "string" || !app.trim()) {
|
|
254
|
+
throw new Error("cua.getApp expects a non-empty app name or bundle ID");
|
|
255
|
+
}
|
|
256
|
+
const binding = await api.getApp(app);
|
|
257
|
+
const token = `app-${nextBinding++}`;
|
|
258
|
+
bindings.set(token, binding);
|
|
259
|
+
return encodeSandboxResult(token);
|
|
260
|
+
}
|
|
261
|
+
if (method === "rewriteDocumentation") {
|
|
262
|
+
api.rewriteDocumentation();
|
|
263
|
+
return encodeSandboxResult(undefined);
|
|
264
|
+
}
|
|
265
|
+
if (method.startsWith("app:")) {
|
|
266
|
+
const [token, ...callArgs] = args;
|
|
267
|
+
const binding = bindings.get(token);
|
|
268
|
+
if (!binding) throw new Error("unknown CUA app binding");
|
|
269
|
+
const result = await binding[method.slice(4)](...callArgs);
|
|
270
|
+
return encodeSandboxResult(result);
|
|
271
|
+
}
|
|
272
|
+
throw new Error(`unknown sandbox method: ${method}`);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return JSON.stringify({ ok: false, error: safeErrorMessage(error) });
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const factory = runInContext(SANDBOX_FACTORY_SOURCE, context);
|
|
278
|
+
const facade = factory(
|
|
279
|
+
dispatch,
|
|
280
|
+
(value, itemId) => activeOutput().write(value, itemId),
|
|
281
|
+
value => activeOutput().emitImage(value),
|
|
282
|
+
schedule,
|
|
283
|
+
cancel,
|
|
284
|
+
);
|
|
285
|
+
Object.defineProperties(context, {
|
|
286
|
+
cua: { value: facade.cua, configurable: false, writable: false },
|
|
287
|
+
nodeRepl: { value: facade.nodeRepl, configurable: false, writable: false },
|
|
288
|
+
setTimeout: { value: facade.setTimeout, configurable: false, writable: false },
|
|
289
|
+
clearTimeout: { value: facade.clearTimeout, configurable: false, writable: false },
|
|
290
|
+
});
|
|
291
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@frontendxlab/opencode-computer-use",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A dual OpenCode and OpenCode2 plugin for local cross-platform desktop control.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/rashidtvmr/opencode-computer-use.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://opencode-computer-use.pages.dev/",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/rashidtvmr/opencode-computer-use/issues"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"opencode",
|
|
19
|
+
"opencode-plugin",
|
|
20
|
+
"computer-use",
|
|
21
|
+
"cua",
|
|
22
|
+
"mcp"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "index.js",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./index.js",
|
|
28
|
+
"./server": "./index.js"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"opencode-computer-use-repl": "bin/opencode-computer-use-repl.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"bin/",
|
|
35
|
+
"lib/",
|
|
36
|
+
"docs/",
|
|
37
|
+
"index.js",
|
|
38
|
+
"README.md",
|
|
39
|
+
"SECURITY.md",
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"THIRD_PARTY_NOTICES.md"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18",
|
|
45
|
+
"opencode": ">=1.18.29"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"lint": "node --check index.js && node --check bin/opencode-computer-use-repl.js && node --check lib/backend.js && node --check lib/json-line-peer.mjs && node --check lib/options.js && node --check lib/output.mjs && node --check lib/repl.mjs && node --check lib/repl-kernel.mjs && node --check lib/sandbox.mjs",
|
|
49
|
+
"test": "node --test",
|
|
50
|
+
"prepack": "node --check index.js && node --check bin/opencode-computer-use-repl.js && node -e \"JSON.parse(require('fs').readFileSync('package.json','utf8'))\"",
|
|
51
|
+
"prepublishOnly": "npm run lint && npm test"
|
|
52
|
+
},
|
|
53
|
+
"optionalDependencies": {
|
|
54
|
+
"open-computer-use": "0.3.5"
|
|
55
|
+
},
|
|
56
|
+
"sideEffects": false
|
|
57
|
+
}
|