@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 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
 
@@ -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</code></pre><button class="copy" type="button" data-copy="#npm-install">Copy</button></div>
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>
@@ -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 double registration.
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.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": ">=1.18.29"
46
+ "opencode": ">=0.0.0-beta-18684"
46
47
  },
47
48
  "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
+ "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
  }