@frontendxlab/opencode-computer-use 0.1.1 → 0.1.3
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 +34 -1
- package/SECURITY.md +5 -0
- package/THIRD_PARTY_NOTICES.md +4 -0
- package/bin/opencode-computer-use-setup.js +498 -0
- package/docs/index.html +4 -1
- package/docs/llms-full.txt +13 -2
- package/docs/llms.txt +1 -1
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ capabilities remain OS and session dependent.
|
|
|
75
75
|
|
|
76
76
|
- Node.js 18 or newer.
|
|
77
77
|
- OpenCode 1.18.29 or newer for the hybrid V1 entrypoint.
|
|
78
|
-
- OpenCode2 V2 for the `setup(ctx)` entrypoint.
|
|
78
|
+
- OpenCode2 V2 beta 19271 or newer for the `setup(ctx)` entrypoint.
|
|
79
79
|
- A signed-in graphical desktop session.
|
|
80
80
|
- macOS Accessibility and Screen Recording permissions.
|
|
81
81
|
- Linux AT-SPI and the input/capture services required by the native runtime.
|
|
@@ -91,8 +91,41 @@ local MCP server named `cua_repl` by default.
|
|
|
91
91
|
|
|
92
92
|
```sh
|
|
93
93
|
npm install @frontendxlab/opencode-computer-use
|
|
94
|
+
# or
|
|
95
|
+
pnpm install @frontendxlab/opencode-computer-use
|
|
94
96
|
```
|
|
95
97
|
|
|
98
|
+
The package `postinstall` step detects the installed OpenCode generation and
|
|
99
|
+
whether the install is local or global. It safely updates `opencode.json` or
|
|
100
|
+
`opencode.jsonc`, preserves comments and existing settings, and is idempotent.
|
|
101
|
+
When both generations are installed, it uses an existing `plugin` or `plugins`
|
|
102
|
+
configuration to disambiguate. If that is not possible, it makes no change and
|
|
103
|
+
prints both manual configuration formats.
|
|
104
|
+
|
|
105
|
+
You can run setup explicitly at any time:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
npx opencode-computer-use-setup
|
|
109
|
+
npx opencode-computer-use-setup --local --v2
|
|
110
|
+
npx opencode-computer-use-setup --global --v1
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Use `--dry-run` to inspect the action without changing files. Set
|
|
114
|
+
`OPENCODE_COMPUTER_USE_SKIP_SETUP=1` to disable the postinstall hook. Setup also
|
|
115
|
+
skips automatically when `CI=1` or `CI=true`. Restart OpenCode after a
|
|
116
|
+
successful setup so the plugin is loaded.
|
|
117
|
+
|
|
118
|
+
Recent pnpm releases may require explicit approval before running lifecycle
|
|
119
|
+
scripts from dependencies. If pnpm reports `ERR_PNPM_IGNORED_BUILDS`, approve
|
|
120
|
+
this package once and rerun the install:
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
pnpm approve-builds @frontendxlab/opencode-computer-use
|
|
124
|
+
pnpm install
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If scripts are intentionally disabled, run `pnpm exec opencode-computer-use-setup` manually instead.
|
|
128
|
+
|
|
96
129
|
A local checkout can be loaded by replacing the package name with its absolute
|
|
97
130
|
path.
|
|
98
131
|
|
package/SECURITY.md
CHANGED
|
@@ -23,6 +23,11 @@ sandbox.
|
|
|
23
23
|
turn-ended notification.
|
|
24
24
|
- Desktop environment variables are allowlisted.
|
|
25
25
|
- Screenshot and text results remain local MCP content.
|
|
26
|
+
- The postinstall setup only adds this package to OpenCode's plugin list, keeps
|
|
27
|
+
JSONC comments, creates a `.bak` before replacement, and never edits a
|
|
28
|
+
conflicting or malformed configuration. Ambiguous setup is non-destructive
|
|
29
|
+
and prints manual instructions. CI and explicit skip environments bypass
|
|
30
|
+
postinstall mutation.
|
|
26
31
|
|
|
27
32
|
## Operator responsibilities
|
|
28
33
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -14,6 +14,10 @@ The persistent JavaScript session adapter in `lib/repl.mjs`,
|
|
|
14
14
|
The native runtime is installed separately as the optional dependency
|
|
15
15
|
`open-computer-use@0.3.5`, which is distributed under the same MIT license.
|
|
16
16
|
|
|
17
|
+
The automatic setup command uses `jsonc-parser@3.3.1`, copyright Microsoft
|
|
18
|
+
Corporation and contributors, under the MIT license. It is used only to make
|
|
19
|
+
targeted edits to OpenCode JSONC configuration files.
|
|
20
|
+
|
|
17
21
|
The adapter was changed to resolve the optional runtime package, use a
|
|
18
22
|
minimal desktop environment allowlist, expose the session through the
|
|
19
23
|
OpenCode plugin entrypoint, and run model code in a restricted VM context.
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { applyEdits, modify, parse, parseTree } from "jsonc-parser";
|
|
10
|
+
|
|
11
|
+
const PACKAGE_NAME = "@frontendxlab/opencode-computer-use";
|
|
12
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const DEFAULT_V1_OPTIONS = { backend: "auto" };
|
|
14
|
+
|
|
15
|
+
export const FALLBACK_INSTRUCTIONS = `
|
|
16
|
+
Automatic OpenCode setup could not safely choose a configuration.
|
|
17
|
+
|
|
18
|
+
For OpenCode 1, add this to opencode.json or opencode.jsonc:
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
"$schema": "https://opencode.ai/config.json",
|
|
22
|
+
"plugin": [
|
|
23
|
+
["@frontendxlab/opencode-computer-use", { "backend": "auto" }]
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
For OpenCode2 V2, add this to opencode.json or opencode.jsonc:
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
"$schema": "https://opencode.ai/config.json",
|
|
31
|
+
"plugins": [
|
|
32
|
+
{
|
|
33
|
+
"package": "@frontendxlab/opencode-computer-use",
|
|
34
|
+
"options": { "backend": "auto" }
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
For global OpenCode2 V2 setup, run:
|
|
40
|
+
|
|
41
|
+
opencode2 plugin add @frontendxlab/opencode-computer-use
|
|
42
|
+
|
|
43
|
+
After changing configuration, restart OpenCode:
|
|
44
|
+
|
|
45
|
+
opencode service restart
|
|
46
|
+
opencode2 service restart
|
|
47
|
+
`.trim();
|
|
48
|
+
|
|
49
|
+
function print(text, stream = "stdout") {
|
|
50
|
+
process[stream].write(`${text}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseArguments(argv) {
|
|
54
|
+
const result = {
|
|
55
|
+
scope: "auto",
|
|
56
|
+
flavor: "auto",
|
|
57
|
+
dryRun: false,
|
|
58
|
+
help: false,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
62
|
+
const argument = argv[index];
|
|
63
|
+
if (argument === "--global" || argument === "-g") {
|
|
64
|
+
result.scope = "global";
|
|
65
|
+
} else if (argument === "--local" || argument === "--project") {
|
|
66
|
+
result.scope = "local";
|
|
67
|
+
} else if (argument === "--v1") {
|
|
68
|
+
result.flavor = "v1";
|
|
69
|
+
} else if (argument === "--v2") {
|
|
70
|
+
result.flavor = "v2";
|
|
71
|
+
} else if (argument === "--opencode-version") {
|
|
72
|
+
const value = argv[++index];
|
|
73
|
+
if (value !== "1" && value !== "2") {
|
|
74
|
+
throw new Error("--opencode-version must be 1 or 2");
|
|
75
|
+
}
|
|
76
|
+
result.flavor = value === "1" ? "v1" : "v2";
|
|
77
|
+
} else if (argument === "--dry-run") {
|
|
78
|
+
result.dryRun = true;
|
|
79
|
+
} else if (argument === "--help" || argument === "-h") {
|
|
80
|
+
result.help = true;
|
|
81
|
+
} else if (argument === "--auto") {
|
|
82
|
+
continue;
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`Unknown setup option: ${argument}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function usage() {
|
|
92
|
+
return `Usage: opencode-computer-use-setup [options]
|
|
93
|
+
|
|
94
|
+
Options:
|
|
95
|
+
--auto Detect OpenCode generation and scope (default)
|
|
96
|
+
--local, --project Configure the current project
|
|
97
|
+
--global Configure the current user's global config
|
|
98
|
+
--v1 Configure the OpenCode 1 plugin format
|
|
99
|
+
--v2 Configure the OpenCode2 V2 plugin format
|
|
100
|
+
--opencode-version 1|2 Select the OpenCode generation explicitly
|
|
101
|
+
--dry-run Show the action without changing files
|
|
102
|
+
--help, -h Show this help
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function executableCandidates(command, env = process.env) {
|
|
107
|
+
const pathValue = env.PATH || "";
|
|
108
|
+
const directories = pathValue.split(delimiter).filter(Boolean);
|
|
109
|
+
if (process.platform === "win32") {
|
|
110
|
+
const extensions = (env.PATHEXT || ".EXE;.CMD;.BAT").split(";").filter(Boolean);
|
|
111
|
+
return directories.flatMap((directory) => [
|
|
112
|
+
join(directory, command),
|
|
113
|
+
...extensions.map((extension) => join(directory, `${command}${extension.toLowerCase()}`)),
|
|
114
|
+
...extensions.map((extension) => join(directory, `${command}${extension}`)),
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
117
|
+
return directories.map((directory) => join(directory, command));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isExecutableFile(path) {
|
|
121
|
+
try {
|
|
122
|
+
return statSync(path).isFile();
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveCommand(command, env = process.env) {
|
|
129
|
+
return executableCandidates(command, env).find(isExecutableFile) || command;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function probeCommand(command, env = process.env) {
|
|
133
|
+
const executable = resolveCommand(command, env);
|
|
134
|
+
const result = spawnSync(executable, ["--version"], {
|
|
135
|
+
encoding: "utf8",
|
|
136
|
+
env,
|
|
137
|
+
shell: false,
|
|
138
|
+
windowsHide: true,
|
|
139
|
+
timeout: 10_000,
|
|
140
|
+
});
|
|
141
|
+
if (result.error || result.status !== 0) {
|
|
142
|
+
return {
|
|
143
|
+
command,
|
|
144
|
+
available: false,
|
|
145
|
+
version: null,
|
|
146
|
+
output: `${result.stdout || ""}${result.stderr || ""}`.trim(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const output = `${result.stdout || ""}\n${result.stderr || ""}`.trim();
|
|
150
|
+
const match = output.match(/(?:opencode2?|opencode)\s+v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/iu);
|
|
151
|
+
return {
|
|
152
|
+
command,
|
|
153
|
+
available: true,
|
|
154
|
+
version: match?.[1] || output.split(/\r?\n/u)[0] || "unknown",
|
|
155
|
+
output,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function detectOpenCode(env = process.env) {
|
|
160
|
+
return {
|
|
161
|
+
v1: probeCommand("opencode", env),
|
|
162
|
+
v2: probeCommand("opencode2", env),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function packageKey(entry) {
|
|
167
|
+
if (typeof entry === "string") return entry;
|
|
168
|
+
if (Array.isArray(entry) && typeof entry[0] === "string") return entry[0];
|
|
169
|
+
if (entry && typeof entry === "object" && typeof entry.package === "string") return entry.package;
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hasPackage(entries) {
|
|
174
|
+
return Array.isArray(entries) && entries.some((entry) => packageKey(entry) === PACKAGE_NAME);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function configFlavor(text) {
|
|
178
|
+
const root = parseTree(text);
|
|
179
|
+
if (!root || root.type !== "object") return null;
|
|
180
|
+
const names = (root.children || [])
|
|
181
|
+
.filter((child) => child.type === "property")
|
|
182
|
+
.map((property) => property.children?.[0]?.value)
|
|
183
|
+
.filter((name) => name === "plugin" || name === "plugins");
|
|
184
|
+
const hasV1 = names.includes("plugin");
|
|
185
|
+
const hasV2 = names.includes("plugins");
|
|
186
|
+
if (hasV1 && hasV2) return "ambiguous";
|
|
187
|
+
if (hasV1) return "v1";
|
|
188
|
+
if (hasV2) return "v2";
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function existingConfigFiles(root, scope, env = process.env) {
|
|
193
|
+
if (env.OPENCODE_CONFIG) return [resolve(env.OPENCODE_CONFIG)];
|
|
194
|
+
const bases = scope === "global"
|
|
195
|
+
? [globalConfigDir(env)]
|
|
196
|
+
: [root, join(root, ".opencode")];
|
|
197
|
+
const files = [];
|
|
198
|
+
for (const base of bases) {
|
|
199
|
+
for (const name of ["opencode.jsonc", "opencode.json"]) {
|
|
200
|
+
const file = join(base, name);
|
|
201
|
+
if (existsSync(file) && !files.includes(file)) files.push(file);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return files;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function configFiles(root, scope, env = process.env) {
|
|
208
|
+
return existingConfigFiles(root, scope, env);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function globalConfigDir(env = process.env) {
|
|
212
|
+
if (env.OPENCODE_CONFIG_DIR) return resolve(env.OPENCODE_CONFIG_DIR);
|
|
213
|
+
if (env.XDG_CONFIG_HOME) return join(resolve(env.XDG_CONFIG_HOME), "opencode");
|
|
214
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
215
|
+
return join(resolve(home), ".config", "opencode");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function detectConfigFlavor(root, scope, env = process.env) {
|
|
219
|
+
const files = existingConfigFiles(root, scope, env);
|
|
220
|
+
let detected = null;
|
|
221
|
+
for (const file of files) {
|
|
222
|
+
let text;
|
|
223
|
+
try {
|
|
224
|
+
text = readFileSync(file, "utf8");
|
|
225
|
+
} catch {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const flavor = configFlavor(text);
|
|
229
|
+
if (flavor && detected && flavor !== detected) return "ambiguous";
|
|
230
|
+
if (flavor) detected = flavor;
|
|
231
|
+
}
|
|
232
|
+
return detected;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function inferScope(requested, env = process.env) {
|
|
236
|
+
if (requested === "local" || requested === "global") return requested;
|
|
237
|
+
const globalFlag = env.npm_config_global ?? env.NPM_CONFIG_GLOBAL;
|
|
238
|
+
if (globalFlag === "true" || globalFlag === true || globalFlag === "1") return "global";
|
|
239
|
+
return "local";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function defaultConfigFile(root, scope, env) {
|
|
243
|
+
const configDir = scope === "global" ? globalConfigDir(env) : root;
|
|
244
|
+
return join(configDir, "opencode.json");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function addEntry(entries, flavor) {
|
|
248
|
+
if (hasPackage(entries)) return { entries, changed: false };
|
|
249
|
+
const entry = flavor === "v1"
|
|
250
|
+
? [PACKAGE_NAME, { ...DEFAULT_V1_OPTIONS }]
|
|
251
|
+
: PACKAGE_NAME;
|
|
252
|
+
return { entries: [...entries, entry], changed: true };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function newConfig(flavor) {
|
|
256
|
+
const key = flavor === "v1" ? "plugin" : "plugins";
|
|
257
|
+
const entry = flavor === "v1"
|
|
258
|
+
? [[PACKAGE_NAME, { ...DEFAULT_V1_OPTIONS }]]
|
|
259
|
+
: [PACKAGE_NAME];
|
|
260
|
+
return {
|
|
261
|
+
$schema: "https://opencode.ai/config.json",
|
|
262
|
+
[key]: entry,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function updateConfigText(text, flavor) {
|
|
267
|
+
const parsed = parse(text);
|
|
268
|
+
if (text.trim() && (parsed === undefined || parsed === null || typeof parsed !== "object" || Array.isArray(parsed))) {
|
|
269
|
+
throw new Error("configuration is not a JSON or JSONC object");
|
|
270
|
+
}
|
|
271
|
+
const data = parsed && typeof parsed === "object" ? parsed : {};
|
|
272
|
+
if (data.plugin !== undefined && data.plugins !== undefined) {
|
|
273
|
+
throw new Error("configuration contains both plugin and plugins fields");
|
|
274
|
+
}
|
|
275
|
+
const key = flavor === "v1" ? "plugin" : "plugins";
|
|
276
|
+
const current = data[key];
|
|
277
|
+
if (current !== undefined && !Array.isArray(current)) {
|
|
278
|
+
throw new Error(`configuration field "${key}" is not an array`);
|
|
279
|
+
}
|
|
280
|
+
const result = addEntry(current || [], flavor);
|
|
281
|
+
if (!result.changed) return { text, changed: false };
|
|
282
|
+
const edits = modify(text, [key], result.entries, {
|
|
283
|
+
formattingOptions: {
|
|
284
|
+
insertSpaces: true,
|
|
285
|
+
tabSize: 2,
|
|
286
|
+
eol: text.includes("\r\n") ? "\r\n" : "\n",
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
return { text: applyEdits(text, edits), changed: true };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function writeConfig(file, content, dryRun) {
|
|
293
|
+
if (dryRun) return;
|
|
294
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
295
|
+
const mode = existsSync(file) ? statSync(file).mode & 0o777 : 0o600;
|
|
296
|
+
if (existsSync(file)) {
|
|
297
|
+
const backup = `${file}.bak`;
|
|
298
|
+
copyFileSync(file, backup);
|
|
299
|
+
chmodSync(backup, mode);
|
|
300
|
+
}
|
|
301
|
+
const temporary = `${file}.tmp-${process.pid}-${randomUUID()}`;
|
|
302
|
+
writeFileSync(temporary, content, { encoding: "utf8", mode, flag: "wx" });
|
|
303
|
+
chmodSync(temporary, mode);
|
|
304
|
+
if (process.platform === "win32" && existsSync(file)) rmSync(file, { force: true });
|
|
305
|
+
renameSync(temporary, file);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function validateConfigFiles(root, scope, env) {
|
|
309
|
+
for (const file of existingConfigFiles(root, scope, env)) {
|
|
310
|
+
const text = readFileSync(file, "utf8");
|
|
311
|
+
const data = parse(text);
|
|
312
|
+
if (text.trim() && (data === undefined || data === null || typeof data !== "object" || Array.isArray(data))) {
|
|
313
|
+
throw new Error(`configuration is not valid JSON or JSONC: ${file}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function packageAlreadyConfigured(root, scope, env) {
|
|
319
|
+
for (const file of existingConfigFiles(root, scope, env)) {
|
|
320
|
+
try {
|
|
321
|
+
const data = parse(readFileSync(file, "utf8"));
|
|
322
|
+
const key = data?.plugins !== undefined ? "plugins" : data?.plugin !== undefined ? "plugin" : undefined;
|
|
323
|
+
if (key && hasPackage(data[key])) return file;
|
|
324
|
+
} catch {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function redactMessage(value) {
|
|
332
|
+
return String(value)
|
|
333
|
+
.replace(/\b(token|secret|password|api[_-]?key|authorization)\s*[:=]\s*(?:bearer|basic)?[^\n\r,;]*/giu, "$1=[redacted]")
|
|
334
|
+
.slice(0, 500);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function runPluginCli(flavor, scope, root, env) {
|
|
338
|
+
const command = flavor === "v1" ? "opencode" : "opencode2";
|
|
339
|
+
const args = ["plugin", "add", PACKAGE_NAME];
|
|
340
|
+
if (flavor === "v1" && scope === "global") args.push("--global");
|
|
341
|
+
const result = spawnSync(resolveCommand(command, env), args, {
|
|
342
|
+
cwd: root,
|
|
343
|
+
encoding: "utf8",
|
|
344
|
+
env,
|
|
345
|
+
shell: process.platform === "win32",
|
|
346
|
+
windowsHide: true,
|
|
347
|
+
timeout: 120_000,
|
|
348
|
+
});
|
|
349
|
+
if (result.error || result.status !== 0) {
|
|
350
|
+
const detail = redactMessage(`${result.stdout || ""}${result.stderr || ""}`.trim());
|
|
351
|
+
throw new Error(`${command} plugin add failed${detail ? `: ${detail}` : ""}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function configForSetup(root, scope, flavor, env, dryRun) {
|
|
356
|
+
const files = existingConfigFiles(root, scope, env);
|
|
357
|
+
if (files.length > 0) {
|
|
358
|
+
const flavors = files.map((file) => {
|
|
359
|
+
try {
|
|
360
|
+
return configFlavor(readFileSync(file, "utf8"));
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
if (flavors.includes("ambiguous")) {
|
|
366
|
+
throw new Error("configuration contains both plugin and plugins fields");
|
|
367
|
+
}
|
|
368
|
+
if (new Set(flavors.filter(Boolean)).size > 1) {
|
|
369
|
+
throw new Error("multiple conflicting OpenCode configuration files were found");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const file = files[0] || defaultConfigFile(root, scope, env);
|
|
373
|
+
if (!existsSync(file)) {
|
|
374
|
+
const content = JSON.stringify(newConfig(flavor), null, 2) + "\n";
|
|
375
|
+
writeConfig(file, content, dryRun);
|
|
376
|
+
return { file, changed: true };
|
|
377
|
+
}
|
|
378
|
+
const result = updateConfigText(readFileSync(file, "utf8"), flavor);
|
|
379
|
+
writeConfig(file, result.text, dryRun);
|
|
380
|
+
return { file, changed: result.changed };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function chooseFlavor(requested, detection, config) {
|
|
384
|
+
if (config === "ambiguous") {
|
|
385
|
+
throw new Error("configuration contains both plugin and plugins fields");
|
|
386
|
+
}
|
|
387
|
+
if (requested !== "auto") {
|
|
388
|
+
if (config && config !== requested) {
|
|
389
|
+
throw new Error(`selected ${requested} conflicts with the existing ${config} configuration`);
|
|
390
|
+
}
|
|
391
|
+
return requested;
|
|
392
|
+
}
|
|
393
|
+
if (detection.v1.available && detection.v2.available) {
|
|
394
|
+
if (config === "v1" || config === "v2") return config;
|
|
395
|
+
throw new Error("both OpenCode 1 and OpenCode2 are installed, and the configuration is ambiguous");
|
|
396
|
+
}
|
|
397
|
+
if (detection.v1.available) return "v1";
|
|
398
|
+
if (detection.v2.available) return "v2";
|
|
399
|
+
throw new Error("neither opencode nor opencode2 was found");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function chooseRoot(scope, env) {
|
|
403
|
+
if (scope === "global") return process.cwd();
|
|
404
|
+
const initial = env.INIT_CWD || process.cwd();
|
|
405
|
+
return resolve(initial);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export async function setup(options = {}, env = process.env, io = print) {
|
|
409
|
+
const requestedScope = options.scope || "auto";
|
|
410
|
+
const requestedFlavor = options.flavor || "auto";
|
|
411
|
+
const scope = inferScope(requestedScope, env);
|
|
412
|
+
const root = chooseRoot(scope, env);
|
|
413
|
+
if (env.OPENCODE_CONFIG_CONTENT) {
|
|
414
|
+
throw new Error("OPENCODE_CONFIG_CONTENT is immutable; add the plugin to that configuration manually");
|
|
415
|
+
}
|
|
416
|
+
const detection = detectOpenCode(env);
|
|
417
|
+
const config = detectConfigFlavor(root, scope, env);
|
|
418
|
+
const flavor = chooseFlavor(requestedFlavor, detection, config);
|
|
419
|
+
const dryRun = Boolean(options.dryRun);
|
|
420
|
+
let result;
|
|
421
|
+
if (scope === "global" && !dryRun) {
|
|
422
|
+
validateConfigFiles(root, scope, env);
|
|
423
|
+
const existing = packageAlreadyConfigured(root, scope, env);
|
|
424
|
+
if (existing) {
|
|
425
|
+
result = { file: existing, changed: false };
|
|
426
|
+
} else {
|
|
427
|
+
runPluginCli(flavor, scope, root, env);
|
|
428
|
+
const configured = packageAlreadyConfigured(root, scope, env);
|
|
429
|
+
if (!configured) throw new Error(`${flavor.toUpperCase()} plugin command did not register the package`);
|
|
430
|
+
result = { file: configured, changed: true };
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
result = configForSetup(root, scope, flavor, env, dryRun);
|
|
434
|
+
}
|
|
435
|
+
const action = options.dryRun
|
|
436
|
+
? (result.changed ? "would configure" : "already configured")
|
|
437
|
+
: (result.changed ? "configured" : "already configured");
|
|
438
|
+
io(`OpenCode Computer Use setup ${action} for ${flavor.toUpperCase()} (${scope}).`);
|
|
439
|
+
if (result.changed) io(`Configuration: ${result.file}`);
|
|
440
|
+
return { ...result, scope, flavor, detection };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function printHelp() {
|
|
444
|
+
print(usage());
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function isDevelopmentInstall(env) {
|
|
448
|
+
const initial = env.INIT_CWD ? resolve(env.INIT_CWD) : undefined;
|
|
449
|
+
return env.npm_lifecycle_event === "postinstall" && initial === PACKAGE_ROOT;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export async function main(argv = process.argv.slice(2), env = process.env, io = print) {
|
|
453
|
+
let options;
|
|
454
|
+
try {
|
|
455
|
+
options = parseArguments(argv);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
io(`[opencode-computer-use] ${error instanceof Error ? error.message : String(error)}`, "stderr");
|
|
458
|
+
io(usage(), "stderr");
|
|
459
|
+
return 2;
|
|
460
|
+
}
|
|
461
|
+
if (options.help) {
|
|
462
|
+
printHelp();
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
if (env.CI === "true" || env.CI === "1") {
|
|
466
|
+
io("OpenCode Computer Use setup skipped in CI.");
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
if (isDevelopmentInstall(env)) {
|
|
470
|
+
io("OpenCode Computer Use setup skipped for a package-development install.");
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
if (env.OPENCODE_COMPUTER_USE_SKIP_SETUP === "1") {
|
|
474
|
+
io("OpenCode Computer Use setup skipped by OPENCODE_COMPUTER_USE_SKIP_SETUP.");
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
477
|
+
return setup(options, env, io)
|
|
478
|
+
.then(() => 0)
|
|
479
|
+
.catch((error) => {
|
|
480
|
+
io(`[opencode-computer-use] Automatic setup was not changed: ${error instanceof Error ? error.message : String(error)}`, "stderr");
|
|
481
|
+
io(FALLBACK_INSTRUCTIONS, "stderr");
|
|
482
|
+
return 0;
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function sameFile(left, right) {
|
|
487
|
+
try {
|
|
488
|
+
return realpathSync(left) === realpathSync(right);
|
|
489
|
+
} catch {
|
|
490
|
+
return resolve(left) === resolve(right);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : "";
|
|
495
|
+
const scriptPath = fileURLToPath(import.meta.url);
|
|
496
|
+
if (invokedPath && sameFile(invokedPath, scriptPath)) {
|
|
497
|
+
process.exitCode = await main();
|
|
498
|
+
}
|
package/docs/index.html
CHANGED
|
@@ -456,7 +456,10 @@
|
|
|
456
456
|
cd opencode-computer-use
|
|
457
457
|
npm install</code></pre><button class="copy" type="button" data-copy="#clone">Copy</button></div>
|
|
458
458
|
<p>For a published-package install, use:</p>
|
|
459
|
-
<div class="code"><pre><code id="npm-install">npm install @frontendxlab/opencode-computer-use
|
|
459
|
+
<div class="code"><pre><code id="npm-install">npm install @frontendxlab/opencode-computer-use
|
|
460
|
+
# pnpm install @frontendxlab/opencode-computer-use also works</code></pre><button class="copy" type="button" data-copy="#npm-install">Copy</button></div>
|
|
461
|
+
<p>The package postinstall step detects OpenCode 1 versus OpenCode2 and local versus global installation. It edits JSONC without discarding comments, avoids duplicate entries, and prints both manual formats when the choice is ambiguous. Run it explicitly with <code>npx opencode-computer-use-setup</code>, or add <code>--local</code>, <code>--global</code>, <code>--v1</code>, or <code>--v2</code> when you want to choose explicitly.</p>
|
|
462
|
+
<p>Recent pnpm releases can require explicit lifecycle-script approval. If pnpm reports <code>ERR_PNPM_IGNORED_BUILDS</code>, run <code>pnpm approve-builds @frontendxlab/opencode-computer-use</code> once, then rerun <code>pnpm install</code>. If scripts are disabled, run <code>pnpm exec opencode-computer-use-setup</code> manually. CI environments skip setup automatically.</p>
|
|
460
463
|
<div class="tabs" role="tablist" aria-label="OpenCode generation">
|
|
461
464
|
<button role="tab" id="tab-v1" aria-controls="panel-v1" aria-selected="true">OpenCode 1</button>
|
|
462
465
|
<button role="tab" id="tab-v2" aria-controls="panel-v2" aria-selected="false" tabindex="-1">OpenCode2</button>
|
package/docs/llms-full.txt
CHANGED
|
@@ -83,6 +83,14 @@ and notifications.
|
|
|
83
83
|
6. Install
|
|
84
84
|
Published package:
|
|
85
85
|
npm install @frontendxlab/opencode-computer-use
|
|
86
|
+
The package postinstall step detects OpenCode 1 versus OpenCode2 and local
|
|
87
|
+
versus global installation, edits JSONC without dropping comments, and is
|
|
88
|
+
idempotent. If both generations are ambiguous it makes no change and prints
|
|
89
|
+
manual instructions. Run it explicitly with npx opencode-computer-use-setup;
|
|
90
|
+
use --local, --global, --v1, or --v2 to choose explicitly. If pnpm reports
|
|
91
|
+
ERR_PNPM_IGNORED_BUILDS, approve the package once with pnpm approve-builds
|
|
92
|
+
@frontendxlab/opencode-computer-use, or run pnpm exec
|
|
93
|
+
opencode-computer-use-setup manually. CI environments skip setup.
|
|
86
94
|
OpenCode 1 (opencode.json or opencode.jsonc, plugin list):
|
|
87
95
|
{ "$schema": "https://opencode.ai/config.json",
|
|
88
96
|
"plugin": [["@frontendxlab/opencode-computer-use", { "backend": "auto" }]] }
|
|
@@ -126,8 +134,11 @@ before high-risk use. Manual read-only checks before acting:
|
|
|
126
134
|
open-computer-use list-apps
|
|
127
135
|
|
|
128
136
|
9. Troubleshooting (from the docs page)
|
|
129
|
-
- cua_repl not listed: check the active generation config, restart the TUI
|
|
130
|
-
run opencode mcp list, avoid
|
|
137
|
+
- cua_repl not listed: check the active generation config, restart the TUI or
|
|
138
|
+
background service after installation, run opencode mcp list, and avoid
|
|
139
|
+
double registration. For global OpenCode2, run opencode2 plugin add
|
|
140
|
+
@frontendxlab/opencode-computer-use if the package was installed before
|
|
141
|
+
automatic setup was available.
|
|
131
142
|
- Node not found: set OPENCODE_NODE or fix server PATH.
|
|
132
143
|
- Empty app list: run open-computer-use list-apps in the same graphical
|
|
133
144
|
session; confirm the app runs and desktop accessibility is available.
|
package/docs/llms.txt
CHANGED
|
@@ -27,6 +27,6 @@
|
|
|
27
27
|
- Safety profiles: safetyMode=read-only blocks mutating CUA methods and changes js annotations; appAccess supports exact-name and ID allow/deny rules.
|
|
28
28
|
- Reliability: app bindings and output are bounded; native timeouts send cancellation; shutdown sends a best-effort turn-ended notification.
|
|
29
29
|
- No arbitrary shell escape. No run_shell in the model-facing API. Restricted VM blocks require, process, fetch, filesystem, dynamic import, and code generation.
|
|
30
|
-
- Published package: @frontendxlab/opencode-computer-use.
|
|
30
|
+
- Published package: @frontendxlab/opencode-computer-use. Its postinstall setup detects OpenCode generation and local/global scope, preserves JSONC, and prints manual instructions when ambiguous. Explicit setup: npx opencode-computer-use-setup --local|--global --v1|--v2.
|
|
31
31
|
- Requirements: Node.js 18 or newer, OpenCode 1.18.29 or newer (V1) or OpenCode2 V2, plus a signed-in graphical desktop session with OS accessibility and capture permissions.
|
|
32
32
|
- Native runtimes packaged for darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-arm64, win32-x64. Live smoke tests passed on Linux x64 only; macOS and Windows need on-host validation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontendxlab/opencode-computer-use",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "A dual OpenCode and OpenCode2 plugin for local cross-platform desktop control.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"./server": "./index.js"
|
|
29
29
|
},
|
|
30
30
|
"bin": {
|
|
31
|
-
"opencode-computer-use-repl": "bin/opencode-computer-use-repl.js"
|
|
31
|
+
"opencode-computer-use-repl": "bin/opencode-computer-use-repl.js",
|
|
32
|
+
"opencode-computer-use-setup": "bin/opencode-computer-use-setup.js"
|
|
32
33
|
},
|
|
33
34
|
"files": [
|
|
34
35
|
"bin/",
|
|
@@ -42,16 +43,20 @@
|
|
|
42
43
|
],
|
|
43
44
|
"engines": {
|
|
44
45
|
"node": ">=18",
|
|
45
|
-
"opencode": ">=
|
|
46
|
+
"opencode": ">=0.0.0-beta-18684"
|
|
46
47
|
},
|
|
47
48
|
"scripts": {
|
|
48
|
-
"
|
|
49
|
+
"postinstall": "node bin/opencode-computer-use-setup.js --auto",
|
|
50
|
+
"lint": "node --check index.js && node --check bin/opencode-computer-use-repl.js && node --check bin/opencode-computer-use-setup.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
51
|
"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'))\"",
|
|
52
|
+
"prepack": "node --check index.js && node --check bin/opencode-computer-use-repl.js && node --check bin/opencode-computer-use-setup.js && node -e \"JSON.parse(require('fs').readFileSync('package.json','utf8'))\"",
|
|
51
53
|
"prepublishOnly": "npm run lint && npm test"
|
|
52
54
|
},
|
|
53
55
|
"optionalDependencies": {
|
|
54
56
|
"open-computer-use": "0.3.5"
|
|
55
57
|
},
|
|
56
|
-
"sideEffects": false
|
|
58
|
+
"sideEffects": false,
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"jsonc-parser": "^3.3.1"
|
|
61
|
+
}
|
|
57
62
|
}
|