opencode-apigee 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chelayel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,109 @@
1
- # Temporary Holding Version
1
+ # opencode-apigee
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Use **Gemini on Vertex AI behind an Apigee gateway** as a model provider in
4
+ [OpenCode](https://opencode.ai).
5
+
6
+ Many companies expose Vertex AI through an Apigee gateway that wants an OAuth
7
+ token from its own client-credentials endpoint instead of a Google API key or
8
+ gcloud login. OpenCode's Google providers can't do that. This plugin does: it
9
+ gets the token with your client id and secret, keeps it fresh, and sends every
10
+ request to the gateway with it. Everything else is OpenCode's standard Gemini
11
+ support.
12
+
13
+ ## Setup
14
+
15
+ **1. Add the plugin** to `~/.config/opencode/opencode.json` (or a project's
16
+ `opencode.json`), with your gateway's details:
17
+
18
+ ```json
19
+ {
20
+ "$schema": "https://opencode.ai/config.json",
21
+ "plugin": [
22
+ ["opencode-apigee", {
23
+ "gateway": "my-gw.example.com",
24
+ "project": "my-gcp-project",
25
+ "location": "us-central1",
26
+ "tokenUrl": "https://auth.example.com/oauth/token",
27
+ "clientId": "my-client-id",
28
+ "models": ["gemini-3.8-flash", "gemini-3.1-pro"]
29
+ }]
30
+ ]
31
+ }
32
+ ```
33
+
34
+ **2. Store the client secret** in OpenCode's credential store:
35
+
36
+ ```bash
37
+ opencode auth login
38
+ ```
39
+
40
+ Pick **Gemini via Apigee** from the list (type "apigee" to filter) and paste
41
+ the client secret where it asks for an API key.
42
+
43
+ (or set `APIGEE_CLIENT_SECRET` in the environment).
44
+
45
+ **3. Pick a model**: `/models` in OpenCode → **Gemini via Apigee**, or
46
+ `opencode run --model apigee/gemini-3.8-flash "…"`.
47
+
48
+ ### Already using AI Relay?
49
+
50
+ If you set the gateway up with [AI Relay](https://github.com/Chelayel/ai-relay)
51
+ (`airelay gemini setup`), the plugin reads the same values from
52
+ `~/.airelay/config.properties` — gateway, project, location, token URL, client
53
+ id and secret, and the published models. Then step 1 is just:
54
+
55
+ ```json
56
+ { "plugin": ["opencode-apigee"] }
57
+ ```
58
+
59
+ ## Options
60
+
61
+ Each setting comes from, in order: the secret stored with `opencode auth login`
62
+ (secret only), the plugin options, an environment variable, then AI Relay's
63
+ config.
64
+
65
+ | Option | Environment | AI Relay key | |
66
+ | --- | --- | --- | --- |
67
+ | `gateway` | `APIGEE_GATEWAY` | `vertex.endpoint` | Gateway host (`https://` is assumed) or a full URL |
68
+ | `project` | `APIGEE_PROJECT` | `vertex.project` | Google Cloud project behind the gateway |
69
+ | `location` | `APIGEE_LOCATION` | `vertex.location` | Default `us-central1` |
70
+ | `tokenUrl` | `APIGEE_TOKEN_URL` | `apigee.token.url` | OAuth client-credentials endpoint |
71
+ | `clientId` | `APIGEE_CLIENT_ID` | `apigee.client.id` | |
72
+ | `clientSecret` | `APIGEE_CLIENT_SECRET` | `apigee.client.secret` | Prefer `opencode auth login` over the config file |
73
+ | `models` | `APIGEE_MODELS` | `apigee.agents` | Model ids the gateway publishes (array, or comma-separated) |
74
+ | `providerId` | | | Provider id in OpenCode, default `apigee` |
75
+ | `name` | | | Display name, default `Gemini via Apigee` |
76
+ | `airelay` | | | `false` to ignore AI Relay's config |
77
+
78
+ A model you describe yourself under `provider.apigee.models` in
79
+ `opencode.json` keeps your description; the plugin only adds the rest.
80
+
81
+ ## How it works
82
+
83
+ - Requests go through OpenCode's own Gemini provider (`@ai-sdk/google`) with
84
+ its base URL set to the gateway's Vertex path,
85
+ `https://<gateway>/v1/projects/<project>/locations/<location>/publishers/google`,
86
+ so each call lands on
87
+ `…/models/<model>:streamGenerateContent` — Vertex's own URL, with the same
88
+ request body.
89
+ - The token comes from `POST <tokenUrl>?grant_type=client_credentials` with the
90
+ client id and secret as HTTP Basic auth, and is cached until a minute before
91
+ it expires. Concurrent requests share one token fetch.
92
+ - Each request has the provider's API-key header replaced by
93
+ `Authorization: Bearer <token>`. If the gateway answers 401, the token is
94
+ fetched again and the request retried once.
95
+
96
+ Tested with OpenCode 1.19.4, end to end against a local stand-in for the
97
+ gateway and its token endpoint.
98
+
99
+ ## Development
100
+
101
+ ```bash
102
+ bun install
103
+ bun test
104
+ bun run build
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,12 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ /**
3
+ * Gemini on Vertex AI behind an Apigee gateway, as an OpenCode provider.
4
+ *
5
+ * The provider is `@ai-sdk/google` aimed at the gateway's Vertex publisher
6
+ * path: its request URL is then Vertex's own, and the body is the same
7
+ * `generateContent` JSON. What differs is the credential — an OAuth token from
8
+ * the gateway's client-credentials endpoint instead of a Google API key — and
9
+ * that is the whole of this plugin: a fetch that swaps the one for the other.
10
+ * The client secret is OpenCode's to keep (`opencode auth login` → Apigee).
11
+ */
12
+ export declare const ApigeePlugin: Plugin;
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ import { baseUrl, displayName, resolveSettings } from "./settings.js";
2
+ import { createApigeeFetch, createTokenSource } from "./token.js";
3
+ // Only the plugin is exported: OpenCode treats exported functions as plugins.
4
+ /** What a Gemini model on Vertex can do; a gateway id OpenCode cannot look up gets these. */
5
+ const GEMINI_LIMITS = { context: 1_048_576, output: 65_536 };
6
+ /**
7
+ * Gemini on Vertex AI behind an Apigee gateway, as an OpenCode provider.
8
+ *
9
+ * The provider is `@ai-sdk/google` aimed at the gateway's Vertex publisher
10
+ * path: its request URL is then Vertex's own, and the body is the same
11
+ * `generateContent` JSON. What differs is the credential — an OAuth token from
12
+ * the gateway's client-credentials endpoint instead of a Google API key — and
13
+ * that is the whole of this plugin: a fetch that swaps the one for the other.
14
+ * The client secret is OpenCode's to keep (`opencode auth login` → Apigee).
15
+ */
16
+ export const ApigeePlugin = async (_input, rawOptions) => {
17
+ const options = (rawOptions ?? {});
18
+ const providerId = options.providerId?.trim() || "apigee";
19
+ return {
20
+ config: async (config) => {
21
+ const { settings } = resolveSettings(options);
22
+ const models = {};
23
+ for (const id of settings.models) {
24
+ models[id] = {
25
+ name: displayName(id),
26
+ tool_call: true,
27
+ attachment: true,
28
+ reasoning: /gemini-([3-9]|2\.5)/.test(id),
29
+ limit: GEMINI_LIMITS,
30
+ modalities: { input: ["text", "image", "pdf"], output: ["text"] },
31
+ };
32
+ }
33
+ config.provider ??= {};
34
+ const existing = (config.provider[providerId] ?? {});
35
+ config.provider[providerId] = {
36
+ name: options.name?.trim() || "Gemini via Apigee",
37
+ npm: "@ai-sdk/google",
38
+ ...existing,
39
+ // A model the user described in opencode.json keeps their description.
40
+ models: { ...models, ...(existing.models ?? {}) },
41
+ options: {
42
+ ...(settings.gateway && settings.project ? { baseURL: baseUrl(settings) } : {}),
43
+ ...(existing.options ?? {}),
44
+ },
45
+ };
46
+ },
47
+ auth: {
48
+ provider: providerId,
49
+ methods: [
50
+ {
51
+ type: "api",
52
+ label: "Apigee client secret",
53
+ },
54
+ ],
55
+ loader: async (getAuth) => {
56
+ const stored = await getAuth().catch(() => undefined);
57
+ const secret = stored && stored.type === "api" ? stored.key : undefined;
58
+ const { settings, missing } = resolveSettings(options, process.env, secret);
59
+ if (missing.length)
60
+ throw new Error(`opencode-apigee: missing ${missing.join(", ")}.`);
61
+ const tokens = createTokenSource(settings.tokenUrl, settings.clientId, settings.clientSecret);
62
+ return {
63
+ baseURL: baseUrl(settings),
64
+ // Required by @ai-sdk/google and never sent: the fetch replaces it with the token.
65
+ apiKey: "apigee",
66
+ fetch: createApigeeFetch(tokens),
67
+ };
68
+ },
69
+ },
70
+ };
71
+ };
@@ -0,0 +1,53 @@
1
+ /** Everything needed to reach Gemini on Vertex AI through an Apigee gateway. */
2
+ export interface ApigeeSettings {
3
+ /** Gateway base, e.g. `https://my-gw.example.com`; a bare host gets `https://`. */
4
+ gateway: string;
5
+ project: string;
6
+ location: string;
7
+ tokenUrl: string;
8
+ clientId: string;
9
+ clientSecret: string;
10
+ /** Model ids the gateway publishes. */
11
+ models: string[];
12
+ }
13
+ /** Plugin options, as written in opencode.json: `["opencode-apigee", { ... }]`. */
14
+ export interface ApigeeOptions {
15
+ gateway?: string;
16
+ project?: string;
17
+ location?: string;
18
+ tokenUrl?: string;
19
+ clientId?: string;
20
+ clientSecret?: string;
21
+ models?: string[] | string;
22
+ /** Provider id in OpenCode (default `apigee`). */
23
+ providerId?: string;
24
+ /** Display name in OpenCode (default `Gemini via Apigee`). */
25
+ name?: string;
26
+ /** Read settings from AI Relay's config file when not given here (default true). */
27
+ airelay?: boolean;
28
+ }
29
+ /**
30
+ * Settings from, in order: the secret OpenCode stored (`opencode auth login`),
31
+ * plugin options, `APIGEE_*` environment variables, and AI Relay's
32
+ * `~/.airelay/config.properties` — so someone already set up with
33
+ * `airelay gemini setup` only adds the plugin.
34
+ */
35
+ export declare function resolveSettings(options?: ApigeeOptions, env?: Record<string, string | undefined>, storedSecret?: string, airelay?: Record<string, string>): {
36
+ settings: ApigeeSettings;
37
+ missing: string[];
38
+ };
39
+ /**
40
+ * The Vertex publisher path on the gateway. `@ai-sdk/google` appends
41
+ * `/models/<id>:streamGenerateContent`, which together is Vertex's own URL.
42
+ */
43
+ export declare function baseUrl(s: ApigeeSettings): string;
44
+ /** AI Relay's config file, or nothing. Its path follows AIRELAY_CONFIG like AI Relay itself. */
45
+ export declare function readAirelayConfig(env?: Record<string, string | undefined>): Record<string, string>;
46
+ /**
47
+ * java.util.Properties text: `key=value` or `key:value`, `#`/`!` comments,
48
+ * backslash escapes (`https\://…` is how Java writes a URL) and line
49
+ * continuations.
50
+ */
51
+ export declare function parseProperties(text: string): Record<string, string>;
52
+ /** `gemini-3.1-pro-preview` → "Gemini 3.1 Pro (preview)"; anything else as it is. */
53
+ export declare function displayName(id: string): string;
@@ -0,0 +1,103 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ /**
5
+ * Settings from, in order: the secret OpenCode stored (`opencode auth login`),
6
+ * plugin options, `APIGEE_*` environment variables, and AI Relay's
7
+ * `~/.airelay/config.properties` — so someone already set up with
8
+ * `airelay gemini setup` only adds the plugin.
9
+ */
10
+ export function resolveSettings(options = {}, env = process.env, storedSecret, airelay = options.airelay === false ? {} : readAirelayConfig(env)) {
11
+ const pick = (opt, envKey, airelayKey) => nonBlank(opt) ?? nonBlank(env[envKey]) ?? nonBlank(airelay[airelayKey]);
12
+ const models = list(options.models) ?? list(env.APIGEE_MODELS) ?? list(airelay["apigee.agents"]) ?? list(airelay["gemini.model"]) ?? [];
13
+ const settings = {
14
+ gateway: pick(options.gateway, "APIGEE_GATEWAY", "vertex.endpoint") ?? "",
15
+ project: pick(options.project, "APIGEE_PROJECT", "vertex.project") ?? nonBlank(env.GOOGLE_CLOUD_PROJECT) ?? "",
16
+ location: pick(options.location, "APIGEE_LOCATION", "vertex.location") ?? "us-central1",
17
+ tokenUrl: pick(options.tokenUrl, "APIGEE_TOKEN_URL", "apigee.token.url") ?? "",
18
+ clientId: pick(options.clientId, "APIGEE_CLIENT_ID", "apigee.client.id") ?? "",
19
+ clientSecret: nonBlank(storedSecret) ?? pick(options.clientSecret, "APIGEE_CLIENT_SECRET", "apigee.client.secret") ?? "",
20
+ models,
21
+ };
22
+ const missing = [];
23
+ if (!settings.gateway)
24
+ missing.push("gateway (APIGEE_GATEWAY)");
25
+ if (!settings.project)
26
+ missing.push("project (APIGEE_PROJECT)");
27
+ if (!settings.tokenUrl)
28
+ missing.push("tokenUrl (APIGEE_TOKEN_URL)");
29
+ if (!settings.clientId)
30
+ missing.push("clientId (APIGEE_CLIENT_ID)");
31
+ if (!settings.clientSecret)
32
+ missing.push("client secret (`opencode auth login` → Apigee, or APIGEE_CLIENT_SECRET)");
33
+ return { settings, missing };
34
+ }
35
+ /**
36
+ * The Vertex publisher path on the gateway. `@ai-sdk/google` appends
37
+ * `/models/<id>:streamGenerateContent`, which together is Vertex's own URL.
38
+ */
39
+ export function baseUrl(s) {
40
+ const gateway = /^https?:\/\//i.test(s.gateway) ? s.gateway : `https://${s.gateway}`;
41
+ return `${gateway.replace(/\/+$/, "")}/v1/projects/${encodeURIComponent(s.project)}/locations/${encodeURIComponent(s.location)}/publishers/google`;
42
+ }
43
+ /** AI Relay's config file, or nothing. Its path follows AIRELAY_CONFIG like AI Relay itself. */
44
+ export function readAirelayConfig(env = process.env) {
45
+ const file = nonBlank(env.AIRELAY_CONFIG) ?? join(homedir(), ".airelay", "config.properties");
46
+ try {
47
+ return parseProperties(readFileSync(file, "utf8"));
48
+ }
49
+ catch {
50
+ return {};
51
+ }
52
+ }
53
+ /**
54
+ * java.util.Properties text: `key=value` or `key:value`, `#`/`!` comments,
55
+ * backslash escapes (`https\://…` is how Java writes a URL) and line
56
+ * continuations.
57
+ */
58
+ export function parseProperties(text) {
59
+ const out = {};
60
+ const lines = text.split(/\r?\n/);
61
+ for (let i = 0; i < lines.length; i++) {
62
+ let line = lines[i].replace(/^\s+/, "");
63
+ if (!line || line.startsWith("#") || line.startsWith("!"))
64
+ continue;
65
+ while (/(^|[^\\])(\\\\)*\\$/.test(line) && i + 1 < lines.length)
66
+ line = line.slice(0, -1) + lines[++i].replace(/^\s+/, "");
67
+ let k = 0;
68
+ while (k < line.length && !"=: \t".includes(line[k]))
69
+ k += line[k] === "\\" ? 2 : 1;
70
+ const key = unescape(line.slice(0, k));
71
+ let rest = line.slice(k).replace(/^[ \t]*/, "");
72
+ if (rest.startsWith("=") || rest.startsWith(":"))
73
+ rest = rest.slice(1).replace(/^[ \t]*/, "");
74
+ out[key] = unescape(rest);
75
+ }
76
+ return out;
77
+ }
78
+ function unescape(s) {
79
+ return s.replace(/\\(u[0-9a-fA-F]{4}|.)/g, (_, c) => {
80
+ if (c.length === 5)
81
+ return String.fromCharCode(parseInt(c.slice(1), 16));
82
+ return c === "t" ? "\t" : c === "n" ? "\n" : c === "r" ? "\r" : c === "f" ? "\f" : c;
83
+ });
84
+ }
85
+ function nonBlank(s) {
86
+ return s && s.trim() ? s.trim() : undefined;
87
+ }
88
+ function list(v) {
89
+ const items = (Array.isArray(v) ? v : (v ?? "").split(",")).map((m) => m.trim()).filter(Boolean);
90
+ return items.length ? [...new Set(items)] : undefined;
91
+ }
92
+ /** `gemini-3.1-pro-preview` → "Gemini 3.1 Pro (preview)"; anything else as it is. */
93
+ export function displayName(id) {
94
+ if (!/^gemini-/i.test(id))
95
+ return id;
96
+ const words = id.split("-");
97
+ const preview = words[words.length - 1].toLowerCase() === "preview";
98
+ const name = (preview ? words.slice(0, -1) : words)
99
+ .map((w) => (/^\d/.test(w) ? w : w.toLowerCase() === "lite" ? "Lite" : w[0].toUpperCase() + w.slice(1)))
100
+ .join(" ")
101
+ .replace("Flash Lite", "Flash-Lite");
102
+ return preview ? `${name} (preview)` : name;
103
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The Apigee OAuth token: client credentials, sent the way AI Relay sends them
3
+ * (`grant_type=client_credentials` on the token URL, HTTP Basic with the client
4
+ * id and secret, empty body), cached until a minute before it expires.
5
+ */
6
+ export interface TokenSource {
7
+ token(): Promise<string>;
8
+ /** Forget the cached token, after the gateway refused it. */
9
+ invalidate(): void;
10
+ }
11
+ export declare function createTokenSource(tokenUrl: string, clientId: string, clientSecret: string, fetchImpl?: typeof fetch, now?: () => number): TokenSource;
12
+ /**
13
+ * A fetch for `@ai-sdk/google` aimed at the gateway: its `x-goog-api-key`
14
+ * header is replaced by the Apigee bearer token. A 401 means the token went
15
+ * stale early (rotated, revoked): it is fetched again and the request retried
16
+ * once.
17
+ */
18
+ export declare function createApigeeFetch(tokens: TokenSource, baseFetch?: typeof fetch): typeof fetch;
package/dist/token.js ADDED
@@ -0,0 +1,54 @@
1
+ export function createTokenSource(tokenUrl, clientId, clientSecret, fetchImpl = fetch, now = Date.now) {
2
+ let cached;
3
+ let pending;
4
+ async function request() {
5
+ const url = tokenUrl + (tokenUrl.includes("?") ? "&" : "?") + "grant_type=client_credentials";
6
+ const basic = Buffer.from(`${clientId}:${clientSecret}`, "utf8").toString("base64");
7
+ const res = await fetchImpl(url, {
8
+ method: "POST",
9
+ headers: { Authorization: `Basic ${basic}`, "Content-Type": "application/x-www-form-urlencoded" },
10
+ });
11
+ if (!res.ok)
12
+ throw new Error(`Apigee token request failed (HTTP ${res.status}). Check the token URL, client id and secret.`);
13
+ const body = (await res.json().catch(() => ({})));
14
+ if (typeof body.access_token !== "string" || !body.access_token)
15
+ throw new Error("Apigee token response had no access_token.");
16
+ const expiresIn = Number(body.expires_in) > 0 ? Number(body.expires_in) : 3600;
17
+ cached = { token: body.access_token, expiresAt: now() + Math.max(30, expiresIn - 60) * 1000 };
18
+ return body.access_token;
19
+ }
20
+ return {
21
+ async token() {
22
+ if (cached && cached.expiresAt > now())
23
+ return cached.token;
24
+ // Concurrent requests share one token fetch rather than racing several.
25
+ pending ??= request().finally(() => (pending = undefined));
26
+ return pending;
27
+ },
28
+ invalidate() {
29
+ cached = undefined;
30
+ },
31
+ };
32
+ }
33
+ /**
34
+ * A fetch for `@ai-sdk/google` aimed at the gateway: its `x-goog-api-key`
35
+ * header is replaced by the Apigee bearer token. A 401 means the token went
36
+ * stale early (rotated, revoked): it is fetched again and the request retried
37
+ * once.
38
+ */
39
+ export function createApigeeFetch(tokens, baseFetch = fetch) {
40
+ const send = async (input, init) => {
41
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
42
+ headers.delete("x-goog-api-key");
43
+ headers.set("Authorization", `Bearer ${await tokens.token()}`);
44
+ return baseFetch(input, { ...init, headers });
45
+ };
46
+ const apigeeFetch = async (input, init) => {
47
+ const res = await send(input, init);
48
+ if (res.status !== 401)
49
+ return res;
50
+ tokens.invalidate();
51
+ return send(input, init);
52
+ };
53
+ return apigeeFetch;
54
+ }
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "opencode-apigee",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "OpenCode provider plugin: Gemini on Vertex AI behind an Apigee gateway (OAuth client credentials).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": ["dist", "README.md", "LICENSE"],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "test": "bun test",
18
+ "prepublishOnly": "bun run build && bun test"
19
+ },
20
+ "keywords": ["opencode", "opencode-plugin", "apigee", "vertex", "gemini", "google"],
21
+ "author": "Chelayel",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/Chelayel/opencode-apigee"
26
+ },
27
+ "peerDependencies": {
28
+ "@opencode-ai/plugin": ">=1.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@opencode-ai/plugin": "1.19.4",
32
+ "@types/bun": "^1.3.0",
33
+ "@types/node": "^22.0.0",
34
+ "typescript": "^5.6.0"
35
+ }
36
+ }