@vymalo/opencode-browser 0.7.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 +21 -0
- package/README.md +321 -0
- package/dist/agent-client.d.ts +54 -0
- package/dist/agent-client.js +180 -0
- package/dist/agent-client.js.map +1 -0
- package/dist/broker.d.ts +70 -0
- package/dist/broker.js +457 -0
- package/dist/broker.js.map +1 -0
- package/dist/catalog.d.ts +45 -0
- package/dist/catalog.js +532 -0
- package/dist/catalog.js.map +1 -0
- package/dist/endpoint.d.ts +41 -0
- package/dist/endpoint.js +117 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +12 -0
- package/dist/lib.js +12 -0
- package/dist/lib.js.map +1 -0
- package/dist/logging.d.ts +15 -0
- package/dist/logging.js +69 -0
- package/dist/logging.js.map +1 -0
- package/dist/opencode.d.ts +20 -0
- package/dist/opencode.js +149 -0
- package/dist/opencode.js.map +1 -0
- package/dist/protocol.d.ts +138 -0
- package/dist/protocol.js +126 -0
- package/dist/protocol.js.map +1 -0
- package/dist/schema.d.ts +51 -0
- package/dist/schema.js +50 -0
- package/dist/schema.js.map +1 -0
- package/dist/token-file.d.ts +12 -0
- package/dist/token-file.js +40 -0
- package/dist/token-file.js.map +1 -0
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +140 -0
- package/dist/tools.js.map +1 -0
- package/dist/transport.d.ts +32 -0
- package/dist/transport.js +71 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol shared by the OpenCode plugin (the bridge **server**) and the
|
|
3
|
+
* browser extension (the **client**). This module is intentionally
|
|
4
|
+
* dependency-free and runtime-agnostic so the extension can mirror it verbatim
|
|
5
|
+
* into a browser bundle without pulling in any Bun/Node code.
|
|
6
|
+
*
|
|
7
|
+
* Canonical copy lives here (`packages/opencode-browser/src/protocol.ts`); the
|
|
8
|
+
* extension keeps a byte-for-byte copy at
|
|
9
|
+
* `apps/browser-extension/src/shared/protocol.ts`. Keep them in sync.
|
|
10
|
+
*
|
|
11
|
+
* Topology: extensions cannot host servers, so the plugin opens the WebSocket
|
|
12
|
+
* server on 127.0.0.1 and the extension's background worker dials out to it.
|
|
13
|
+
* The extension authenticates with a `hello`; the server replies `ready`. From
|
|
14
|
+
* then on the server issues `command` frames and the extension answers with a
|
|
15
|
+
* matching `result` frame (correlated by `id`). The extension may also push
|
|
16
|
+
* unsolicited `event` frames. Heartbeats use `ping`/`pong`.
|
|
17
|
+
*/
|
|
18
|
+
/** Bump when the frame shapes change incompatibly. */
|
|
19
|
+
export const PROTOCOL_VERSION = 1;
|
|
20
|
+
export const BROWSER_ACTIONS = [
|
|
21
|
+
"open",
|
|
22
|
+
"navigate",
|
|
23
|
+
"click",
|
|
24
|
+
"double_click",
|
|
25
|
+
"type",
|
|
26
|
+
"fill",
|
|
27
|
+
"select",
|
|
28
|
+
"scroll",
|
|
29
|
+
"press_key",
|
|
30
|
+
"screenshot",
|
|
31
|
+
"snapshot",
|
|
32
|
+
"get_text",
|
|
33
|
+
"wait",
|
|
34
|
+
"tabs",
|
|
35
|
+
"close",
|
|
36
|
+
"back",
|
|
37
|
+
"forward",
|
|
38
|
+
"reload",
|
|
39
|
+
"hover",
|
|
40
|
+
"activate",
|
|
41
|
+
"drag",
|
|
42
|
+
"upload",
|
|
43
|
+
"get_html",
|
|
44
|
+
"get_attribute",
|
|
45
|
+
"query",
|
|
46
|
+
"eval",
|
|
47
|
+
"console",
|
|
48
|
+
"network",
|
|
49
|
+
"handle_dialog",
|
|
50
|
+
"set_viewport",
|
|
51
|
+
"cookies",
|
|
52
|
+
"targets",
|
|
53
|
+
"release"
|
|
54
|
+
];
|
|
55
|
+
/** Serialize a frame for the wire. */
|
|
56
|
+
export function encodeFrame(frame) {
|
|
57
|
+
return JSON.stringify(frame);
|
|
58
|
+
}
|
|
59
|
+
function isRecord(value) {
|
|
60
|
+
return typeof value === "object" && value !== null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parse and shape-check an inbound frame. Returns the typed frame, or `null` if
|
|
64
|
+
* the payload is not a recognizable frame (caller decides whether to drop or
|
|
65
|
+
* log). Validation is deliberately lightweight — both ends are trusted once the
|
|
66
|
+
* token handshake succeeds; this guards against malformed JSON and version skew,
|
|
67
|
+
* not a hostile peer.
|
|
68
|
+
*/
|
|
69
|
+
export function decodeFrame(raw) {
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(raw);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
if (!isRecord(parsed) || typeof parsed.type !== "string") {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
switch (parsed.type) {
|
|
81
|
+
case "hello":
|
|
82
|
+
return typeof parsed.token === "string" ? parsed : null;
|
|
83
|
+
case "ready":
|
|
84
|
+
return parsed;
|
|
85
|
+
case "command":
|
|
86
|
+
return typeof parsed.id === "string" &&
|
|
87
|
+
typeof parsed.action === "string" &&
|
|
88
|
+
typeof parsed.group === "string"
|
|
89
|
+
? parsed
|
|
90
|
+
: null;
|
|
91
|
+
case "result":
|
|
92
|
+
return typeof parsed.id === "string" && typeof parsed.ok === "boolean"
|
|
93
|
+
? parsed
|
|
94
|
+
: null;
|
|
95
|
+
case "event":
|
|
96
|
+
return typeof parsed.name === "string" ? parsed : null;
|
|
97
|
+
case "release":
|
|
98
|
+
return { v: PROTOCOL_VERSION, type: "release" };
|
|
99
|
+
case "ping":
|
|
100
|
+
return { v: PROTOCOL_VERSION, type: "ping" };
|
|
101
|
+
case "pong":
|
|
102
|
+
return { v: PROTOCOL_VERSION, type: "pong" };
|
|
103
|
+
default:
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let idCounter = 0;
|
|
108
|
+
/**
|
|
109
|
+
* Monotonic correlation id for command frames. Process-local and collision-free
|
|
110
|
+
* within a single bridge instance, which is all that's required (the server is
|
|
111
|
+
* the only id minter).
|
|
112
|
+
*/
|
|
113
|
+
export function nextId() {
|
|
114
|
+
idCounter = (idCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
115
|
+
return `c${idCounter.toString(36)}`;
|
|
116
|
+
}
|
|
117
|
+
export function helloFrame(token, opts = {}) {
|
|
118
|
+
return { v: PROTOCOL_VERSION, type: "hello", token, ...opts };
|
|
119
|
+
}
|
|
120
|
+
export function resultFrame(id, data) {
|
|
121
|
+
return { v: PROTOCOL_VERSION, type: "result", id, ok: true, data };
|
|
122
|
+
}
|
|
123
|
+
export function errorFrame(id, message, code) {
|
|
124
|
+
return { v: PROTOCOL_VERSION, type: "result", id, ok: false, error: { message, code } };
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, neutral schema vocabulary for tool arguments. The tool catalog
|
|
3
|
+
* (`catalog.ts`) is the single source of truth; each adapter turns these specs
|
|
4
|
+
* into its own format:
|
|
5
|
+
* - the OpenCode plugin builds a zod shape (see `inputToZodShape` in tools.ts),
|
|
6
|
+
* - the MCP server emits standard JSON Schema via `toJsonSchema` below.
|
|
7
|
+
*
|
|
8
|
+
* Keeping the vocabulary small (string/number/boolean/array/object + enum +
|
|
9
|
+
* optional + description) avoids dragging a specific zod version across the
|
|
10
|
+
* package boundary while still covering every tool's args.
|
|
11
|
+
*/
|
|
12
|
+
export type ToolGroup = "page" | "control" | "debug";
|
|
13
|
+
export interface StringField {
|
|
14
|
+
type: "string";
|
|
15
|
+
description?: string;
|
|
16
|
+
optional?: boolean;
|
|
17
|
+
enum?: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export interface NumberField {
|
|
20
|
+
type: "number";
|
|
21
|
+
description?: string;
|
|
22
|
+
optional?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface BooleanField {
|
|
25
|
+
type: "boolean";
|
|
26
|
+
description?: string;
|
|
27
|
+
optional?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ArrayField {
|
|
30
|
+
type: "array";
|
|
31
|
+
description?: string;
|
|
32
|
+
optional?: boolean;
|
|
33
|
+
items: Field;
|
|
34
|
+
}
|
|
35
|
+
export interface ObjectField {
|
|
36
|
+
type: "object";
|
|
37
|
+
description?: string;
|
|
38
|
+
optional?: boolean;
|
|
39
|
+
properties: Record<string, Field>;
|
|
40
|
+
}
|
|
41
|
+
export type Field = StringField | NumberField | BooleanField | ArrayField | ObjectField;
|
|
42
|
+
/** Top-level argument map for a tool. */
|
|
43
|
+
export type JsonInput = Record<string, Field>;
|
|
44
|
+
export interface JsonSchema {
|
|
45
|
+
type: "object";
|
|
46
|
+
properties: Record<string, unknown>;
|
|
47
|
+
required: string[];
|
|
48
|
+
additionalProperties: false;
|
|
49
|
+
}
|
|
50
|
+
/** Convert a tool's input spec to a standard JSON Schema object (for MCP). */
|
|
51
|
+
export declare function toJsonSchema(input: JsonInput): JsonSchema;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, neutral schema vocabulary for tool arguments. The tool catalog
|
|
3
|
+
* (`catalog.ts`) is the single source of truth; each adapter turns these specs
|
|
4
|
+
* into its own format:
|
|
5
|
+
* - the OpenCode plugin builds a zod shape (see `inputToZodShape` in tools.ts),
|
|
6
|
+
* - the MCP server emits standard JSON Schema via `toJsonSchema` below.
|
|
7
|
+
*
|
|
8
|
+
* Keeping the vocabulary small (string/number/boolean/array/object + enum +
|
|
9
|
+
* optional + description) avoids dragging a specific zod version across the
|
|
10
|
+
* package boundary while still covering every tool's args.
|
|
11
|
+
*/
|
|
12
|
+
function fieldToJsonSchema(field) {
|
|
13
|
+
const out = { type: field.type };
|
|
14
|
+
if (field.description) {
|
|
15
|
+
out.description = field.description;
|
|
16
|
+
}
|
|
17
|
+
if (field.type === "string" && field.enum) {
|
|
18
|
+
out.enum = [...field.enum];
|
|
19
|
+
}
|
|
20
|
+
if (field.type === "array") {
|
|
21
|
+
out.items = fieldToJsonSchema(field.items);
|
|
22
|
+
}
|
|
23
|
+
if (field.type === "object") {
|
|
24
|
+
const properties = {};
|
|
25
|
+
const required = [];
|
|
26
|
+
for (const [key, value] of Object.entries(field.properties)) {
|
|
27
|
+
properties[key] = fieldToJsonSchema(value);
|
|
28
|
+
if (!value.optional) {
|
|
29
|
+
required.push(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
out.properties = properties;
|
|
33
|
+
out.required = required;
|
|
34
|
+
out.additionalProperties = false;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Convert a tool's input spec to a standard JSON Schema object (for MCP). */
|
|
39
|
+
export function toJsonSchema(input) {
|
|
40
|
+
const properties = {};
|
|
41
|
+
const required = [];
|
|
42
|
+
for (const [key, field] of Object.entries(input)) {
|
|
43
|
+
properties[key] = fieldToJsonSchema(field);
|
|
44
|
+
if (!field.optional) {
|
|
45
|
+
required.push(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA4CH,SAAS,iBAAiB,CAAC,KAAY;IACrC,MAAM,GAAG,GAA4B,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;IAC1D,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;QACtB,GAAG,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,CAAC;IACtC,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QAC1C,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC3B,GAAG,CAAC,KAAK,GAAG,iBAAiB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7C,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,UAAU,GAA4B,EAAE,CAAC;QAC/C,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5D,UAAU,CAAC,GAAG,CAAC,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;YAC3C,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACpB,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACrB,CAAC;QACH,CAAC;QACD,GAAG,CAAC,UAAU,GAAG,UAAU,CAAC;QAC5B,GAAG,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACxB,GAAG,CAAC,oBAAoB,GAAG,KAAK,CAAC;IACnC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,YAAY,CAAC,KAAgB;IAC3C,MAAM,UAAU,GAA4B,EAAE,CAAC;IAC/C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,UAAU,CAAC,GAAG,CAAC,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;YACpB,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,oBAAoB,EAAE,KAAK,EAAE,CAAC;AAC/E,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface BridgeFile {
|
|
2
|
+
port?: number;
|
|
3
|
+
token?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function readBridgeFile(): BridgeFile | null;
|
|
6
|
+
export declare function writeBridgeFile(port: number, token: string): void;
|
|
7
|
+
export type TokenSource = "explicit" | "file" | "generated";
|
|
8
|
+
/** Resolve the bridge token: explicit option wins, else a shared file, else generate. */
|
|
9
|
+
export declare function resolveSharedToken(port: number, explicit: string | undefined, generate: () => string): {
|
|
10
|
+
token: string;
|
|
11
|
+
source: TokenSource;
|
|
12
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Per-user state file so multiple adapters (plugin + MCP server) on one machine
|
|
6
|
+
* share a bridge token without the operator wiring one up. Best-effort: a
|
|
7
|
+
* simultaneous cold start can race (each generates its own token before either
|
|
8
|
+
* writes); set an explicit token if you need that guaranteed. The extension
|
|
9
|
+
* still needs the token pasted into its dashboard.
|
|
10
|
+
*/
|
|
11
|
+
const FILE = join(tmpdir(), "opencode-browser-bridge.json");
|
|
12
|
+
export function readBridgeFile() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(FILE, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function writeBridgeFile(port, token) {
|
|
21
|
+
try {
|
|
22
|
+
writeFileSync(FILE, JSON.stringify({ port, token }), { mode: 0o600 });
|
|
23
|
+
chmodSync(FILE, 0o600);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* best-effort */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Resolve the bridge token: explicit option wins, else a shared file, else generate. */
|
|
30
|
+
export function resolveSharedToken(port, explicit, generate) {
|
|
31
|
+
if (explicit && explicit.length > 0) {
|
|
32
|
+
return { token: explicit, source: "explicit" };
|
|
33
|
+
}
|
|
34
|
+
const existing = readBridgeFile();
|
|
35
|
+
if (existing?.token && (existing.port === undefined || existing.port === port)) {
|
|
36
|
+
return { token: existing.token, source: "file" };
|
|
37
|
+
}
|
|
38
|
+
return { token: generate(), source: "generated" };
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=token-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-file.js","sourceRoot":"","sources":["../src/token-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;;GAMG;AACH,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,8BAA8B,CAAC,CAAC;AAO5D,MAAM,UAAU,cAAc;IAC5B,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAe,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,KAAa;IACzD,IAAI,CAAC;QACH,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,iBAAiB;IACnB,CAAC;AACH,CAAC;AAID,yFAAyF;AACzF,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,QAA4B,EAC5B,QAAsB;IAEtB,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IACjD,CAAC;IACD,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAC;IAClC,IAAI,QAAQ,EAAE,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QAC/E,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACnD,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;AACpD,CAAC"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
+
import type { AgentEndpoint } from "./broker.js";
|
|
3
|
+
import type { Logger } from "./logging.js";
|
|
4
|
+
import type { ResolvedBrowserOptions, ScreenshotResult } from "./types.js";
|
|
5
|
+
/** How a tool reaches the bridge — the endpoint's `send` (host or guest). */
|
|
6
|
+
export type SendFn = AgentEndpoint["send"];
|
|
7
|
+
/** Where the screenshot tool gets its disk-write behavior; swappable in tests. */
|
|
8
|
+
export type SaveScreenshot = (input: {
|
|
9
|
+
group: string;
|
|
10
|
+
worktree: string;
|
|
11
|
+
shot: ScreenshotResult;
|
|
12
|
+
}) => Promise<string>;
|
|
13
|
+
export interface ToolDeps {
|
|
14
|
+
send: SendFn;
|
|
15
|
+
options: ResolvedBrowserOptions;
|
|
16
|
+
logger: Logger;
|
|
17
|
+
saveScreenshot?: SaveScreenshot;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the `browser_*` tool map registered under `Hooks.tool`, filtered to the
|
|
21
|
+
* enabled groups. Each tool is a thin adapter over the shared catalog: validate
|
|
22
|
+
* args (zod), forward to the bridge, render the neutral result.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createBrowserTools(deps: ToolDeps): Record<string, ToolDefinition>;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { mkdir, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { tool } from "@opencode-ai/plugin";
|
|
4
|
+
import { BROWSER_TOOLS } from "./catalog.js";
|
|
5
|
+
const z = tool.schema;
|
|
6
|
+
function asRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Replace anything that isn't filesystem-friendly so a group name is path-safe.
|
|
11
|
+
* Dots are intentionally NOT allowed — otherwise a group like `..` or `a/../b`
|
|
12
|
+
* could traverse out of the screenshot directory.
|
|
13
|
+
*/
|
|
14
|
+
function slugifyGroup(group) {
|
|
15
|
+
const slug = group.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
16
|
+
return slug.length > 0 ? slug : "default";
|
|
17
|
+
}
|
|
18
|
+
/** Build the real disk-writer bound to the resolved screenshot directory. */
|
|
19
|
+
function makeSaveScreenshot(options) {
|
|
20
|
+
return async ({ group, worktree, shot }) => {
|
|
21
|
+
const base = isAbsolute(options.screenshotDir)
|
|
22
|
+
? options.screenshotDir
|
|
23
|
+
: join(worktree, options.screenshotDir);
|
|
24
|
+
const dir = join(base, slugifyGroup(group));
|
|
25
|
+
await mkdir(dir, { recursive: true });
|
|
26
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
27
|
+
const finalPath = join(dir, `${stamp}.png`);
|
|
28
|
+
const tmpPath = `${finalPath}.tmp`;
|
|
29
|
+
await writeFile(tmpPath, Buffer.from(shot.base64, "base64"), { mode: 0o600 });
|
|
30
|
+
await rename(tmpPath, finalPath);
|
|
31
|
+
return finalPath;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function fieldToZod(field) {
|
|
35
|
+
let schema;
|
|
36
|
+
switch (field.type) {
|
|
37
|
+
case "string":
|
|
38
|
+
schema = (field.enum
|
|
39
|
+
? z.enum(field.enum)
|
|
40
|
+
: z.string());
|
|
41
|
+
break;
|
|
42
|
+
case "number":
|
|
43
|
+
schema = z.number();
|
|
44
|
+
break;
|
|
45
|
+
case "boolean":
|
|
46
|
+
schema = z.boolean();
|
|
47
|
+
break;
|
|
48
|
+
case "array":
|
|
49
|
+
schema = z.array(fieldToZod(field.items));
|
|
50
|
+
break;
|
|
51
|
+
case "object":
|
|
52
|
+
schema = z.object(buildShape(field.properties));
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if (field.description) {
|
|
56
|
+
schema = schema.describe(field.description);
|
|
57
|
+
}
|
|
58
|
+
if (field.optional) {
|
|
59
|
+
schema = schema.optional();
|
|
60
|
+
}
|
|
61
|
+
return schema;
|
|
62
|
+
}
|
|
63
|
+
function buildShape(input) {
|
|
64
|
+
const shape = {};
|
|
65
|
+
for (const [key, field] of Object.entries(input)) {
|
|
66
|
+
shape[key] = fieldToZod(field);
|
|
67
|
+
}
|
|
68
|
+
return shape;
|
|
69
|
+
}
|
|
70
|
+
/** Render an adapter-neutral result into OpenCode's text-only ToolResult. */
|
|
71
|
+
async function renderOpenCode(result, deps) {
|
|
72
|
+
if (result.kind === "text") {
|
|
73
|
+
return result.text;
|
|
74
|
+
}
|
|
75
|
+
if (result.kind === "json") {
|
|
76
|
+
return { output: result.text, metadata: asRecord(result.data) };
|
|
77
|
+
}
|
|
78
|
+
// image → write the PNG to disk and hand back the path (tool output is text).
|
|
79
|
+
const path = await deps.saveScreenshot({
|
|
80
|
+
group: deps.group,
|
|
81
|
+
worktree: deps.worktree,
|
|
82
|
+
shot: {
|
|
83
|
+
base64: result.base64,
|
|
84
|
+
width: result.width,
|
|
85
|
+
height: result.height,
|
|
86
|
+
partial: result.partial
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
deps.logger.info("browser_screenshot_saved", { group: deps.group, path });
|
|
90
|
+
const partialNote = result.partial
|
|
91
|
+
? " Note: only the viewport was captured — this executor can't capture beyond it."
|
|
92
|
+
: "";
|
|
93
|
+
return {
|
|
94
|
+
output: `Saved screenshot to ${path} (${result.width}×${result.height}). Use the read tool to view it.${partialNote}`,
|
|
95
|
+
metadata: {
|
|
96
|
+
path,
|
|
97
|
+
width: result.width,
|
|
98
|
+
height: result.height,
|
|
99
|
+
partial: Boolean(result.partial),
|
|
100
|
+
group: deps.group
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build the `browser_*` tool map registered under `Hooks.tool`, filtered to the
|
|
106
|
+
* enabled groups. Each tool is a thin adapter over the shared catalog: validate
|
|
107
|
+
* args (zod), forward to the bridge, render the neutral result.
|
|
108
|
+
*/
|
|
109
|
+
export function createBrowserTools(deps) {
|
|
110
|
+
const enabled = new Set(deps.options.groups);
|
|
111
|
+
const saveScreenshot = deps.saveScreenshot ?? makeSaveScreenshot(deps.options);
|
|
112
|
+
const tools = {};
|
|
113
|
+
for (const spec of BROWSER_TOOLS) {
|
|
114
|
+
if (!enabled.has(spec.group)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const definition = {
|
|
118
|
+
description: spec.description,
|
|
119
|
+
args: buildShape(spec.input),
|
|
120
|
+
async execute(args, ctx) {
|
|
121
|
+
const group = typeof args.group === "string" ? args.group : "";
|
|
122
|
+
const target = typeof args.target === "string" ? args.target : undefined;
|
|
123
|
+
const params = spec.params ? spec.params(args) : args;
|
|
124
|
+
const data = await deps.send(spec.action, group, params, ctx.abort, target);
|
|
125
|
+
const result = spec.result
|
|
126
|
+
? spec.result(data, args)
|
|
127
|
+
: { kind: "text", text: `${spec.name} ok` };
|
|
128
|
+
return renderOpenCode(result, {
|
|
129
|
+
group,
|
|
130
|
+
worktree: ctx.worktree,
|
|
131
|
+
saveScreenshot,
|
|
132
|
+
logger: deps.logger
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
tools[spec.name] = definition;
|
|
137
|
+
}
|
|
138
|
+
return tools;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transport seam for the bridge/broker. The real implementation wraps
|
|
3
|
+
* `Bun.serve`'s WebSocket support; the MCP server provides a Node `ws` variant;
|
|
4
|
+
* tests inject a fake. `listen` is **async** and resolves only once the port is
|
|
5
|
+
* actually bound (and rejects on `EADDRINUSE`) — that's what the broker election
|
|
6
|
+
* relies on to decide host-vs-guest.
|
|
7
|
+
*/
|
|
8
|
+
/** A single connected client (an extension executor or an agent). */
|
|
9
|
+
export interface ClientConnection {
|
|
10
|
+
send(data: string): void;
|
|
11
|
+
close(): void;
|
|
12
|
+
}
|
|
13
|
+
export interface TransportHandlers {
|
|
14
|
+
onOpen(conn: ClientConnection): void;
|
|
15
|
+
onMessage(conn: ClientConnection, data: string): void;
|
|
16
|
+
onClose(conn: ClientConnection): void;
|
|
17
|
+
}
|
|
18
|
+
export interface BridgeTransport {
|
|
19
|
+
/** Bind + start listening. Resolves when bound; rejects on bind failure. */
|
|
20
|
+
listen(opts: {
|
|
21
|
+
host: string;
|
|
22
|
+
port: number;
|
|
23
|
+
}, handlers: TransportHandlers): Promise<void>;
|
|
24
|
+
stop(): void;
|
|
25
|
+
}
|
|
26
|
+
/** True for an "address already in use" style bind error (→ become a guest). */
|
|
27
|
+
export declare function isAddrInUse(err: unknown): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Real transport backed by `Bun.serve` — the production path inside OpenCode
|
|
30
|
+
* (which runs on Bun). Throws if invoked outside Bun.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createBunTransport(): BridgeTransport;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transport seam for the bridge/broker. The real implementation wraps
|
|
3
|
+
* `Bun.serve`'s WebSocket support; the MCP server provides a Node `ws` variant;
|
|
4
|
+
* tests inject a fake. `listen` is **async** and resolves only once the port is
|
|
5
|
+
* actually bound (and rejects on `EADDRINUSE`) — that's what the broker election
|
|
6
|
+
* relies on to decide host-vs-guest.
|
|
7
|
+
*/
|
|
8
|
+
/** True for an "address already in use" style bind error (→ become a guest). */
|
|
9
|
+
export function isAddrInUse(err) {
|
|
10
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11
|
+
return /eaddrinuse|address already in use|in use/i.test(msg);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Real transport backed by `Bun.serve` — the production path inside OpenCode
|
|
15
|
+
* (which runs on Bun). Throws if invoked outside Bun.
|
|
16
|
+
*/
|
|
17
|
+
export function createBunTransport() {
|
|
18
|
+
const bun = globalThis.Bun;
|
|
19
|
+
if (!bun) {
|
|
20
|
+
throw new Error("the opencode-browser bridge requires the Bun runtime (Bun.serve not found)");
|
|
21
|
+
}
|
|
22
|
+
let server = null;
|
|
23
|
+
const conns = new WeakMap();
|
|
24
|
+
return {
|
|
25
|
+
listen({ host, port }, handlers) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
try {
|
|
28
|
+
server = bun.serve({
|
|
29
|
+
hostname: host,
|
|
30
|
+
port,
|
|
31
|
+
fetch(req, srv) {
|
|
32
|
+
if (srv.upgrade(req)) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return new Response("opencode-browser bridge: websocket only", { status: 426 });
|
|
36
|
+
},
|
|
37
|
+
websocket: {
|
|
38
|
+
open(ws) {
|
|
39
|
+
const conn = { send: (d) => ws.send(d), close: () => ws.close() };
|
|
40
|
+
conns.set(ws, conn);
|
|
41
|
+
handlers.onOpen(conn);
|
|
42
|
+
},
|
|
43
|
+
message(ws, message) {
|
|
44
|
+
const conn = conns.get(ws);
|
|
45
|
+
if (conn) {
|
|
46
|
+
handlers.onMessage(conn, typeof message === "string" ? message : Buffer.from(message).toString());
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
close(ws) {
|
|
50
|
+
const conn = conns.get(ws);
|
|
51
|
+
conns.delete(ws);
|
|
52
|
+
if (conn) {
|
|
53
|
+
handlers.onClose(conn);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
resolve(); // Bun.serve throws synchronously on EADDRINUSE; reaching here = bound.
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
reject(err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
stop() {
|
|
66
|
+
server?.stop(true);
|
|
67
|
+
server = null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAoBH,gFAAgF;AAChF,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,OAAO,2CAA2C,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/D,CAAC;AAUD;;;GAGG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,GAAG,GAAI,UAAgC,CAAC,GAAG,CAAC;IAClD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,4EAA4E,CAAC,CAAC;IAChG,CAAC;IACD,IAAI,MAAM,GAAiD,IAAI,CAAC;IAChE,MAAM,KAAK,GAAG,IAAI,OAAO,EAA4B,CAAC;IAEtD,OAAO;QACL,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,QAAQ;YAC7B,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBAC3C,IAAI,CAAC;oBACH,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC;wBACjB,QAAQ,EAAE,IAAI;wBACd,IAAI;wBACJ,KAAK,CAAC,GAAY,EAAE,GAAuC;4BACzD,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gCACrB,OAAO,SAAS,CAAC;4BACnB,CAAC;4BACD,OAAO,IAAI,QAAQ,CAAC,yCAAyC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;wBAClF,CAAC;wBACD,SAAS,EAAE;4BACT,IAAI,CAAC,EAAgB;gCACnB,MAAM,IAAI,GAAqB,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;gCACpF,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;gCACpB,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;4BACxB,CAAC;4BACD,OAAO,CAAC,EAAgB,EAAE,OAAiC;gCACzD,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gCAC3B,IAAI,IAAI,EAAE,CAAC;oCACT,QAAQ,CAAC,SAAS,CAChB,IAAI,EACJ,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,CACxE,CAAC;gCACJ,CAAC;4BACH,CAAC;4BACD,KAAK,CAAC,EAAgB;gCACpB,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gCAC3B,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gCACjB,IAAI,IAAI,EAAE,CAAC;oCACT,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gCACzB,CAAC;4BACH,CAAC;yBACF;qBACF,CAAC,CAAC;oBACH,OAAO,EAAE,CAAC,CAAC,uEAAuE;gBACpF,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,MAAM,CAAC,GAAG,CAAC,CAAC;gBACd,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QACD,IAAI;YACF,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YACnB,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ToolGroup } from "./schema.js";
|
|
2
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
3
|
+
export type { ToolGroup } from "./schema.js";
|
|
4
|
+
/**
|
|
5
|
+
* How the extension should drive the page. Mirrored into the extension, which
|
|
6
|
+
* is where it actually takes effect; the plugin only forwards the preference so
|
|
7
|
+
* the extension can default sensibly when its own setting is unset.
|
|
8
|
+
*
|
|
9
|
+
* - `"auto"` — CDP (chrome.debugger) on Chromium when permitted, else content-script.
|
|
10
|
+
* - `"cdp"` — force the Chrome DevTools Protocol executor (Chromium only).
|
|
11
|
+
* - `"content"` — force synthetic-event content-script executor (works on Firefox).
|
|
12
|
+
*/
|
|
13
|
+
export type ExecutorMode = "auto" | "cdp" | "content";
|
|
14
|
+
/**
|
|
15
|
+
* Per-plugin configuration, supplied as the second argument to the plugin
|
|
16
|
+
* factory by OpenCode (the `[name, options]` tuple form in `plugin`). All
|
|
17
|
+
* fields are optional; see DEFAULT_OPTIONS for the resolved defaults.
|
|
18
|
+
*/
|
|
19
|
+
export interface BrowserPluginOptions {
|
|
20
|
+
/** Master switch. When false the bridge never starts and tools no-op-error. */
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
/** Interface to bind. Keep it loopback unless you really mean it. */
|
|
23
|
+
host?: string;
|
|
24
|
+
/** TCP port for the WebSocket bridge. */
|
|
25
|
+
port?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Shared secret the extension must present. If omitted, a random token is
|
|
28
|
+
* generated at startup and logged once so it can be pasted into the extension.
|
|
29
|
+
*/
|
|
30
|
+
token?: string;
|
|
31
|
+
/** Forwarded executor preference (see ExecutorMode). */
|
|
32
|
+
executor?: ExecutorMode;
|
|
33
|
+
/**
|
|
34
|
+
* Which tool groups to register: `page` (observe), `control` (drive),
|
|
35
|
+
* `debug` (powerful/sensitive). Defaults to `["page","control"]` — `debug`
|
|
36
|
+
* is opt-in. Per-agent control is also possible via OpenCode's tool
|
|
37
|
+
* allow/deny on the `browser_*` names.
|
|
38
|
+
*/
|
|
39
|
+
groups?: ToolGroup[];
|
|
40
|
+
/** Per-command timeout in ms before the tool call rejects. */
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Directory screenshots are written to. Relative paths resolve against the
|
|
44
|
+
* session worktree. Defaults to `.opencode/browser` under the worktree.
|
|
45
|
+
*/
|
|
46
|
+
screenshotDir?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Fully-resolved options with defaults applied. */
|
|
49
|
+
export interface ResolvedBrowserOptions {
|
|
50
|
+
enabled: boolean;
|
|
51
|
+
host: string;
|
|
52
|
+
port: number;
|
|
53
|
+
token: string;
|
|
54
|
+
executor: ExecutorMode;
|
|
55
|
+
groups: ToolGroup[];
|
|
56
|
+
timeoutMs: number;
|
|
57
|
+
screenshotDir: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Shape the extension returns for a `screenshot` command. The PNG travels as
|
|
61
|
+
* base64 (no binary frames over the text protocol); the plugin decodes and
|
|
62
|
+
* writes it to disk.
|
|
63
|
+
*/
|
|
64
|
+
export interface ScreenshotResult {
|
|
65
|
+
/** Base64-encoded PNG bytes (no data: prefix). */
|
|
66
|
+
base64: string;
|
|
67
|
+
width: number;
|
|
68
|
+
height: number;
|
|
69
|
+
/**
|
|
70
|
+
* True when `fullPage` was requested but the executor could only capture the
|
|
71
|
+
* viewport (the content-script backend / Firefox can't capture beyond it).
|
|
72
|
+
*/
|
|
73
|
+
partial?: boolean;
|
|
74
|
+
}
|
package/dist/types.js
ADDED