@andoai/opencode 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,13 @@
1
+ Copyright 2026 Ando
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @andoai/opencode
2
+
3
+ Official Ando Wallet integration for [OpenCode](https://opencode.ai/).
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ opencode plugin --global @andoai/opencode
9
+ ```
10
+
11
+ Then run `/connect` in OpenCode, choose **Ando**, and select **Connect Ando Wallet**. Your browser opens
12
+ `wallet.andoai.xyz`, where you choose an existing open, funded OpenCode session or open a new one.
13
+
14
+ The selected MPP session credential is returned directly to the OpenCode process over a one-time IPv4
15
+ loopback callback. It is sent in a form POST body, never in the callback URL. OpenCode stores the credential
16
+ in its local authentication store; this plugin does not add it to `opencode.json`.
17
+
18
+ ## Provider
19
+
20
+ The plugin configures the `ando` provider against the OpenAI-compatible Ando MPP endpoint:
21
+
22
+ ```text
23
+ https://inference.andoai.xyz/v1/mpp
24
+ ```
25
+
26
+ No inference content, bearer credential, or private wallet material is published onchain by this plugin.
27
+
28
+ ## License
29
+
30
+ Apache-2.0
@@ -0,0 +1,9 @@
1
+ export type CredentialCallback = {
2
+ callbackUrl: string;
3
+ close: () => Promise<void>;
4
+ state: string;
5
+ waitForCredential: () => Promise<string>;
6
+ };
7
+ export declare function startCredentialCallback(options?: Readonly<{
8
+ timeoutMs?: number;
9
+ }>): Promise<CredentialCallback>;
@@ -0,0 +1,145 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ export async function startCredentialCallback(options = {}) {
4
+ const timeoutMs = options.timeoutMs ?? 5 * 60 * 1_000;
5
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
6
+ throw new Error("OpenCode callback timeout must be a positive integer");
7
+ }
8
+ const state = randomBytes(32).toString("base64url");
9
+ let settled = false;
10
+ let resolveCredential = () => undefined;
11
+ let rejectCredential = () => undefined;
12
+ const credential = new Promise((resolve, reject) => {
13
+ resolveCredential = resolve;
14
+ rejectCredential = reject;
15
+ });
16
+ void credential.catch(() => undefined);
17
+ const server = createServer((request, response) => {
18
+ void handleRequest(request, response, state, (token) => {
19
+ if (settled)
20
+ return;
21
+ settled = true;
22
+ clearTimeout(timeout);
23
+ resolveCredential(token);
24
+ setImmediate(() => server.close());
25
+ });
26
+ });
27
+ server.on("clientError", (_error, socket) => {
28
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
29
+ });
30
+ await new Promise((resolve, reject) => {
31
+ server.once("error", reject);
32
+ server.listen(0, "127.0.0.1", () => {
33
+ server.off("error", reject);
34
+ resolve();
35
+ });
36
+ });
37
+ const address = server.address();
38
+ if (!address || typeof address === "string") {
39
+ server.close();
40
+ throw new Error("OpenCode callback server did not bind to a loopback port");
41
+ }
42
+ const callbackUrl = `http://127.0.0.1:${address.port}/ando/opencode/callback`;
43
+ const timeout = setTimeout(() => {
44
+ if (settled)
45
+ return;
46
+ settled = true;
47
+ rejectCredential(new Error("Ando Wallet approval timed out"));
48
+ server.close();
49
+ }, timeoutMs);
50
+ timeout.unref();
51
+ return {
52
+ callbackUrl,
53
+ state,
54
+ waitForCredential: () => credential,
55
+ close: async () => {
56
+ clearTimeout(timeout);
57
+ if (!settled) {
58
+ settled = true;
59
+ rejectCredential(new Error("Ando Wallet approval was cancelled"));
60
+ }
61
+ await closeServer(server);
62
+ }
63
+ };
64
+ }
65
+ const CALLBACK_PATH = "/ando/opencode/callback";
66
+ const MAX_BODY_BYTES = 8_192;
67
+ const MAX_TOKEN_LENGTH = 6_144;
68
+ async function handleRequest(request, response, expectedState, accept) {
69
+ if (request.method !== "POST") {
70
+ send(response, 405, "This callback accepts POST requests only.", { allow: "POST" });
71
+ request.resume();
72
+ return;
73
+ }
74
+ if (request.url !== CALLBACK_PATH) {
75
+ send(response, 404, "Callback not found.");
76
+ request.resume();
77
+ return;
78
+ }
79
+ const contentType = request.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
80
+ if (contentType !== "application/x-www-form-urlencoded") {
81
+ send(response, 415, "Unsupported callback content type.");
82
+ request.resume();
83
+ return;
84
+ }
85
+ const body = await readBody(request);
86
+ if (body === undefined) {
87
+ send(response, 413, "Callback request is too large.");
88
+ return;
89
+ }
90
+ const form = new URLSearchParams(body);
91
+ const state = form.get("state") ?? "";
92
+ if (!safeEqual(state, expectedState)) {
93
+ send(response, 403, "The wallet approval state did not match.");
94
+ return;
95
+ }
96
+ const credential = form.get("credential") ?? "";
97
+ if (!credential.startsWith("mpp_session_v1_") ||
98
+ credential.length <= "mpp_session_v1_".length ||
99
+ credential.length > MAX_TOKEN_LENGTH ||
100
+ !/^mpp_session_v1_[A-Za-z0-9_-]+$/u.test(credential)) {
101
+ send(response, 400, "The selected Ando session credential is invalid.");
102
+ return;
103
+ }
104
+ send(response, 200, "Ando Wallet connected. You can close this window and return to OpenCode.", {}, true);
105
+ accept(credential);
106
+ }
107
+ async function readBody(request) {
108
+ const chunks = [];
109
+ let length = 0;
110
+ let tooLarge = false;
111
+ for await (const chunk of request) {
112
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
113
+ length += buffer.length;
114
+ if (length > MAX_BODY_BYTES) {
115
+ tooLarge = true;
116
+ continue;
117
+ }
118
+ chunks.push(buffer);
119
+ }
120
+ return tooLarge ? undefined : Buffer.concat(chunks).toString("utf8");
121
+ }
122
+ function safeEqual(actual, expected) {
123
+ const actualBytes = Buffer.from(actual);
124
+ const expectedBytes = Buffer.from(expected);
125
+ return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
126
+ }
127
+ function send(response, status, message, extraHeaders = {}, success = false) {
128
+ const body = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Ando Wallet</title></head><body><main><h1>${success ? "Connected" : "Connection not completed"}</h1><p>${message}</p></main></body></html>`;
129
+ response.writeHead(status, {
130
+ "cache-control": "no-store",
131
+ "content-security-policy": "default-src 'none'; base-uri 'none'; frame-ancestors 'none'",
132
+ "content-type": "text/html; charset=utf-8",
133
+ "referrer-policy": "no-referrer",
134
+ "x-content-type-options": "nosniff",
135
+ ...extraHeaders
136
+ });
137
+ response.end(body);
138
+ }
139
+ function closeServer(server) {
140
+ if (!server.listening)
141
+ return Promise.resolve();
142
+ return new Promise((resolve, reject) => {
143
+ server.close((error) => (error ? reject(error) : resolve()));
144
+ });
145
+ }
@@ -0,0 +1,10 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import { type CredentialCallback } from "./callback.js";
3
+ type StartCallback = (options?: Readonly<{
4
+ timeoutMs?: number;
5
+ }>) => Promise<CredentialCallback>;
6
+ export declare function createAndoOpenCodePlugin(runtime?: Readonly<{
7
+ startCallback?: StartCallback;
8
+ }>): Plugin;
9
+ declare const AndoOpenCodePlugin: Plugin;
10
+ export default AndoOpenCodePlugin;
package/dist/index.js ADDED
@@ -0,0 +1,79 @@
1
+ import { startCredentialCallback } from "./callback.js";
2
+ const WALLET_ORIGIN = "https://wallet.andoai.xyz";
3
+ const INFERENCE_BASE_URL = "https://inference.andoai.xyz/v1/mpp";
4
+ const MODELS = {
5
+ "Ando/Kimi-k2.6": { name: "Kimi K2.6" },
6
+ "Ando/gpt-oss-120b": { name: "GPT OSS 120B" },
7
+ "ando/nemotron-3-ultra-550b-a55b": { name: "Nemotron 3 Ultra 550B" }
8
+ };
9
+ export function createAndoOpenCodePlugin(runtime = {}) {
10
+ const startCallback = runtime.startCallback ?? startCredentialCallback;
11
+ return async () => {
12
+ const pending = new Set();
13
+ return {
14
+ config: async (config) => configureAndoProvider(config),
15
+ auth: {
16
+ provider: "ando",
17
+ methods: [
18
+ {
19
+ type: "oauth",
20
+ label: "Connect Ando Wallet",
21
+ authorize: async () => {
22
+ const callback = await startCallback();
23
+ pending.add(callback);
24
+ const authorizationUrl = new URL("/integrations/opencode/authorize", WALLET_ORIGIN);
25
+ authorizationUrl.searchParams.set("redirect_uri", callback.callbackUrl);
26
+ authorizationUrl.searchParams.set("state", callback.state);
27
+ return {
28
+ method: "auto",
29
+ url: authorizationUrl.toString(),
30
+ instructions: "Choose an open, funded OpenCode session in Ando Wallet.",
31
+ callback: async () => {
32
+ try {
33
+ return {
34
+ type: "success",
35
+ provider: "ando",
36
+ key: await callback.waitForCredential()
37
+ };
38
+ }
39
+ catch {
40
+ return { type: "failed" };
41
+ }
42
+ finally {
43
+ pending.delete(callback);
44
+ await callback.close();
45
+ }
46
+ }
47
+ };
48
+ }
49
+ }
50
+ ]
51
+ },
52
+ dispose: async () => {
53
+ const callbacks = Array.from(pending);
54
+ pending.clear();
55
+ await Promise.all(callbacks.map((callback) => callback.close()));
56
+ }
57
+ };
58
+ };
59
+ }
60
+ function configureAndoProvider(config) {
61
+ const providers = config.provider ?? {};
62
+ const current = providers.ando;
63
+ providers.ando = {
64
+ ...current,
65
+ name: current?.name ?? "Ando",
66
+ npm: current?.npm ?? "@ai-sdk/openai-compatible",
67
+ models: {
68
+ ...MODELS,
69
+ ...current?.models
70
+ },
71
+ options: {
72
+ baseURL: INFERENCE_BASE_URL,
73
+ ...current?.options
74
+ }
75
+ };
76
+ config.provider = providers;
77
+ }
78
+ const AndoOpenCodePlugin = createAndoOpenCodePlugin();
79
+ export default AndoOpenCodePlugin;
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@andoai/opencode",
3
+ "version": "0.1.0",
4
+ "description": "Official Ando Wallet integration for OpenCode",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./server": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "sideEffects": false,
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "prepack": "npm run build",
29
+ "typecheck": "tsc -p tsconfig.json --noEmit",
30
+ "test": "vitest run"
31
+ },
32
+ "devDependencies": {
33
+ "@opencode-ai/plugin": "1.18.10"
34
+ }
35
+ }