@cuny-ai-lab/cail-opencode 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 CUNY AI Lab
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,121 @@
1
- # Temporary Holding Version
1
+ # CUNY AI Lab for OpenCode
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
+ Adds CUNY AI Lab to [OpenCode](https://opencode.ai): the lab's models through the CUNY AI Lab Gateway, and **Sign in with CUNY AI Lab** in OpenCode's connect dialog. It works in the OpenCode desktop app and in OpenCode's terminal interface. You need a CUNY AI Lab account with model access.
4
+
5
+ ## Set up
6
+
7
+ 1. Install the OpenCode desktop app from [opencode.ai/download](https://opencode.ai/download). On a Mac, drag it into Applications.
8
+
9
+ 2. Add CUNY AI Lab to OpenCode with one line. It uses the copy of OpenCode's command-line tool that comes inside the desktop app, so there is nothing else to install.
10
+
11
+ On a Mac, open Terminal, paste this line and press Return:
12
+
13
+ ```bash
14
+ /Applications/OpenCode.app/Contents/Resources/opencode-cli plugin add @cuny-ai-lab/cail-opencode@latest
15
+ ```
16
+
17
+ On Windows, open PowerShell, paste this line and press Enter:
18
+
19
+ ```powershell
20
+ & "$env:LOCALAPPDATA\Programs\ai.opencode.desktop\resources\opencode-cli.exe" plugin add "@cuny-ai-lab/cail-opencode@latest"
21
+ ```
22
+
23
+ If you already use OpenCode in a terminal, `opencode plugin add @cuny-ai-lab/cail-opencode@latest` does the same.
24
+
25
+ If OpenCode was open, quit it and open it again.
26
+
27
+ 3. Sign in. In OpenCode, open the providers list, choose **CUNY AI Lab**, then **Sign in with CUNY AI Lab**. Your browser opens the lab's sign-in page. Sign in with your CUNY Login, click **Connect OpenCode**, and go back to OpenCode.
28
+
29
+ New sessions start on the newest DeepSeek Flash unless you have chosen a default model yourself.
30
+
31
+ If OpenCode runs on another computer than your browser, for example over SSH, choose **Sign in with CUNY AI Lab using a code** instead. Open the link it shows on any computer, click **Connect OpenCode**, and paste the code the page shows. You can also choose **CUNY AI Lab API key** and paste a personal key from [Model access](https://tools.ailab.gc.cuny.edu/model-access).
32
+
33
+ ## What you get
34
+
35
+ * **CUNY AI Lab models**, listed live from the gateway, so new models appear without updating anything. The plugin offers the text models that support tool calls, because OpenCode sends its tools with every request. The list is kept for times when the gateway can't be reached.
36
+ * **Sign in with CUNY AI Lab.** Signing in creates a personal key named "OpenCode on" and your computer's name, which expires after 180 days. It is listed in [Model access](https://tools.ailab.gc.cuny.edu/model-access), where you can revoke it. Signing in again from the same computer replaces that key.
37
+ * **The newest DeepSeek Flash as the default model**, unless you have set one. A `model` in your `opencode.json` always wins.
38
+
39
+ ## Troubleshooting
40
+
41
+ **CUNY AI Lab isn't in the providers list**
42
+ Run the line from step 2 with `plugin check` in place of `plugin add` and the package name, for example `/Applications/OpenCode.app/Contents/Resources/opencode-cli plugin check` on a Mac. It should list `cail`. If it does, quit OpenCode and open it again.
43
+
44
+ **"Your CUNY AI Lab sign-in has expired"**
45
+ Keys from signing in last 180 days. Sign in with CUNY AI Lab again.
46
+
47
+ **"Your CUNY AI Lab access is not active yet"**
48
+ Signing in works only for accounts with lab model access. Open [Model access](https://tools.ailab.gc.cuny.edu/model-access) to see how to request it.
49
+
50
+ **Two "OpenCode on …" accounts**
51
+ OpenCode keeps each sign-in as its own account, and plugins can't remove one. After you sign in again, the older account is marked "Replaced by a newer CUNY AI Lab sign-in" and its key no longer works. You can remove it.
52
+
53
+ ## Privacy and security
54
+
55
+ * OpenCode stores your key in its own credential store on your computer. The plugin sends it only to the CUNY AI Lab gateway, with each model request.
56
+ * OpenCode also lists models from other providers, including free models from OpenCode's own service. Only CUNY AI Lab models go through the lab's gateway and its zero-data-retention providers.
57
+ * Advanced users can set `AILAB_API_KEY` instead of signing in.
58
+
59
+ ## Updates and removal
60
+
61
+ OpenCode does not update plugins by itself. To update, run the line from step 2 with `update` in place of `add`, then quit OpenCode and open it again. On a Mac:
62
+
63
+ ```bash
64
+ /Applications/OpenCode.app/Contents/Resources/opencode-cli plugin update @cuny-ai-lab/cail-opencode@latest
65
+ ```
66
+
67
+ To remove the plugin, use `remove` in place of `add`. To revoke the key it created, use [Model access](https://tools.ailab.gc.cuny.edu/model-access).
68
+
69
+ ---
70
+
71
+ ## For developers
72
+
73
+ ### What the package contains
74
+
75
+ ```text
76
+ src/index.ts the plugin: CUNY AI Lab integration and sign-in methods, provider, default model
77
+ src/signin.ts PKCE, the loopback callback, the copy-code fallback, the key exchange
78
+ src/catalog.ts gateway catalog → OpenCode models
79
+ test/unit bun tests with a fake OpenCode context
80
+ test/opencode-smoke.ts the packed package in a real OpenCode, against test/mock-gateway.ts
81
+ test/live.ts optional check against the real gateway
82
+ ```
83
+
84
+ The published package is the compiled `dist/` and has no dependencies. OpenCode's `Plugin.define` only returns its argument, so the plugin exports a plain object and uses `@opencode/plugin` for types alone.
85
+
86
+ ### How it fits OpenCode
87
+
88
+ * The plugin registers an integration `cail` with four methods: browser sign-in (an OAuth method in `auto` mode with a `127.0.0.1` callback), sign-in with a code (`code` mode), a pasted key, and `AILAB_API_KEY`. Sign-in uses the lab's local-app connect flow with `client=opencode`, documented in the Model Access API README. The returned personal key is stored as an OAuth credential whose expiry is the key's; `refresh` fails with a sign-in-again message once it has expired.
89
+ * A new sign-in sends `replace_key_id` with the key from the active sign-in credential, so the lab rotates it. OpenCode stores the new sign-in as a separate account and plugins cannot delete credentials, so the plugin marks the replaced account `needs_auth`. A pasted key is never replaced.
90
+ * The provider `cail` uses `@opencode/ai/providers/openai-compatible` at `https://tools.ailab.gc.cuny.edu/v1`, with `activation: "auto"` so it is active once the integration has a credential. Models come from `GET /v1/models` at startup and every 15 minutes, with the last good list kept in plugin storage.
91
+ * A model transform sets the newest DeepSeek Flash as the default only when no default is set. Signed out of OpenCode's own service, OpenCode otherwise falls back to the first available model, which can be a free OpenCode model.
92
+
93
+ ### Developing
94
+
95
+ ```bash
96
+ bun install
97
+ bun run typecheck
98
+ bun test test/unit
99
+ bun run test:opencode # packed plugin in OpenCode 2.2.7, mock gateway
100
+ OPENCODE_VERSION=latest bun run test:opencode
101
+ CAIL_LIVE_KEY=... bun run test:live # see below
102
+ ```
103
+
104
+ `test:opencode` downloads the OpenCode release named by `opencode.tested` in `package.json` (or `OPENCODE_VERSION`), checks it against the release's `SHA256SUMS`, and runs it with an empty home directory and a minimal environment. It signs in by code and in the browser, lists the models, checks the default, and sends prompts that reach the mock gateway with the signed-in key.
105
+
106
+ `test:live` reads `CAIL_LIVE_KEY`, passes it to OpenCode as `AILAB_API_KEY` so it never appears in OpenCode's command-line log, and asks the newest DeepSeek Flash for a reply through the real gateway. It uses `OPENCODE_BIN`, or `opencode` on `PATH`.
107
+
108
+ ### Compatibility
109
+
110
+ OpenCode releases often, and its plugin API is marked experimental. CI runs the smoke test against the pinned version on Linux, macOS and Windows, and the `opencode-latest` workflow runs it every day against the newest 2.x release. When that fails, fix the plugin, raise `opencode.tested`, and release.
111
+
112
+ ### Publishing
113
+
114
+ Releases are published by GitHub Actions through npm trusted publishing. Bump the version and push the tag from `main`:
115
+
116
+ ```bash
117
+ npm version patch
118
+ git push --follow-tags
119
+ ```
120
+
121
+ The `publish` job in `.github/workflows/ci.yml` runs for `v*` tags after the unit and OpenCode jobs pass, checks the tag against `package.json`, and publishes with provenance. The trusted publisher on npmjs.com is tied to that workflow filename.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * CUNY AI Lab model catalog → OpenCode `Model.Info[]`.
3
+ *
4
+ * Availability and capability metadata both come from the live gateway:
5
+ * `GET https://tools.ailab.gc.cuny.edu/v1/models` returns OpenAI-style entries
6
+ * enriched with `name`, `task`, `capabilities`, `context_length`, `pricing`,
7
+ * `status` and `sunset`. The public catalog at ailab.gc.cuny.edu/models reads
8
+ * the same data, so the plugin ships no model list.
9
+ *
10
+ * Only text-generation models that accept tool calls are offered, because
11
+ * OpenCode sends its tools on every request and the gateway rejects tool calls
12
+ * to models without function calling. Speech, image and sunset models are
13
+ * dropped.
14
+ *
15
+ * Pure functions, no I/O.
16
+ */
17
+ import type { Model } from "@opencode/plugin";
18
+ export declare const PROVIDER_ID = "cail";
19
+ export declare const PROVIDER_NAME = "CUNY AI Lab";
20
+ export declare const DEFAULT_ORIGIN = "https://tools.ailab.gc.cuny.edu";
21
+ export declare class CatalogError extends Error {
22
+ name: string;
23
+ }
24
+ export interface CatalogEntry {
25
+ readonly id: string;
26
+ readonly [field: string]: unknown;
27
+ }
28
+ /** Validate the OpenAI-style list shape and return entries with usable ids. */
29
+ export declare function parseCatalog(payload: unknown): CatalogEntry[];
30
+ export declare function toModel(entry: CatalogEntry): Model.Info;
31
+ /**
32
+ * The newest DeepSeek Flash, or undefined. Newer means a higher version
33
+ * (v4 before v4.1), then a higher release suffix (0731 before 1017).
34
+ */
35
+ export declare function newestDeepSeekFlash<T extends {
36
+ id: string;
37
+ }>(models: readonly T[]): T | undefined;
38
+ /** Convert a raw `/v1/models` payload into OpenCode models. Throws CatalogError when the payload is malformed. */
39
+ export declare function toModels(payload: unknown, { now }?: {
40
+ now?: number;
41
+ }): Model.Info[];
@@ -0,0 +1,106 @@
1
+ export const PROVIDER_ID = "cail";
2
+ export const PROVIDER_NAME = "CUNY AI Lab";
3
+ export const DEFAULT_ORIGIN = "https://tools.ailab.gc.cuny.edu";
4
+ const DEFAULT_CONTEXT = 128_000;
5
+ const MAX_OUTPUT = 32_000;
6
+ export class CatalogError extends Error {
7
+ name = "CatalogError";
8
+ }
9
+ /** Validate the OpenAI-style list shape and return entries with usable ids. */
10
+ export function parseCatalog(payload) {
11
+ const data = payload?.data;
12
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || !Array.isArray(data)) {
13
+ throw new CatalogError("CUNY AI Lab did not return a standard model list (expected { data: [...] }).");
14
+ }
15
+ return data.filter((entry) => !!entry && typeof entry === "object" && typeof entry.id === "string" && entry.id.trim() !== "");
16
+ }
17
+ function capabilities(entry) {
18
+ return Array.isArray(entry.capabilities)
19
+ ? entry.capabilities.filter((capability) => typeof capability === "string")
20
+ : [];
21
+ }
22
+ function isTextGeneration(entry) {
23
+ const caps = capabilities(entry);
24
+ if (caps.length > 0)
25
+ return caps.includes("text-generation");
26
+ if (typeof entry.task === "string")
27
+ return /text generation/i.test(entry.task);
28
+ // No capability information at all: offer the model with conservative defaults.
29
+ return true;
30
+ }
31
+ function supportsTools(entry) {
32
+ const caps = capabilities(entry);
33
+ return caps.length === 0 || caps.includes("function-calling");
34
+ }
35
+ function isRetired(entry, now) {
36
+ if (typeof entry.status === "string" && /^(retired|disabled|inactive|removed)$/i.test(entry.status))
37
+ return true;
38
+ if (typeof entry.sunset === "string") {
39
+ const sunset = Date.parse(entry.sunset);
40
+ if (!Number.isNaN(sunset) && sunset <= now)
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ function positiveInteger(value) {
46
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined;
47
+ }
48
+ function cost(entry) {
49
+ const pricing = entry.pricing;
50
+ if (!pricing || pricing.currency !== "USD" || pricing.unit !== "million_tokens")
51
+ return [];
52
+ const price = (value) => (typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0);
53
+ const zero = price(0);
54
+ return [{ input: price(pricing.input), output: price(pricing.output), cache: { read: zero, write: zero } }];
55
+ }
56
+ export function toModel(entry) {
57
+ const caps = capabilities(entry);
58
+ const context = positiveInteger(entry.context_length) ?? DEFAULT_CONTEXT;
59
+ const name = (typeof entry.name === "string" && entry.name.trim()) ||
60
+ (typeof entry.model_name === "string" && entry.model_name.trim()) ||
61
+ entry.id;
62
+ return {
63
+ id: entry.id,
64
+ modelID: entry.id,
65
+ providerID: PROVIDER_ID,
66
+ name,
67
+ capabilities: {
68
+ tools: true,
69
+ input: caps.includes("vision") ? ["text", "image"] : ["text"],
70
+ output: ["text"],
71
+ },
72
+ variants: [],
73
+ time: { released: 0 },
74
+ cost: cost(entry),
75
+ status: "active",
76
+ enabled: true,
77
+ limit: { context, output: Math.min(MAX_OUTPUT, Math.max(1024, Math.floor(context / 2))) },
78
+ };
79
+ }
80
+ const DEEPSEEK_FLASH = /^deepseek-v(\d+(?:\.\d+)?)-flash(?:-(\d+))?$/i;
81
+ /**
82
+ * The newest DeepSeek Flash, or undefined. Newer means a higher version
83
+ * (v4 before v4.1), then a higher release suffix (0731 before 1017).
84
+ */
85
+ export function newestDeepSeekFlash(models) {
86
+ let best;
87
+ let bestRank = [-1, -1];
88
+ for (const model of models) {
89
+ const match = DEEPSEEK_FLASH.exec(model.id);
90
+ if (!match)
91
+ continue;
92
+ const rank = [Number(match[1]), Number(match[2] ?? 0)];
93
+ if (rank[0] > bestRank[0] || (rank[0] === bestRank[0] && rank[1] > bestRank[1])) {
94
+ best = model;
95
+ bestRank = rank;
96
+ }
97
+ }
98
+ return best;
99
+ }
100
+ /** Convert a raw `/v1/models` payload into OpenCode models. Throws CatalogError when the payload is malformed. */
101
+ export function toModels(payload, { now = Date.now() } = {}) {
102
+ return parseCatalog(payload)
103
+ .filter((entry) => isTextGeneration(entry) && supportsTools(entry) && !isRetired(entry, now))
104
+ .map(toModel);
105
+ }
106
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CUNY AI Lab for OpenCode.
3
+ *
4
+ * Adds "CUNY AI Lab" to OpenCode's providers, with "Sign in with CUNY AI Lab"
5
+ * in the app's connect dialog, the Lab's models listed live from the gateway,
6
+ * and the newest DeepSeek Flash as the default model when no default is set.
7
+ *
8
+ * The plugin is a plain object: OpenCode's `Plugin.define` only returns its
9
+ * argument, so the published package has no runtime dependencies.
10
+ */
11
+ import type { Plugin } from "@opencode/plugin";
12
+ export declare const PLUGIN_ID = "cail";
13
+ export interface Options {
14
+ /** The Lab's origin. Only tests set this. */
15
+ readonly origin?: string;
16
+ /** Hostname override for the key name. Only tests set this. */
17
+ readonly hostname?: string;
18
+ /** Fetch override. Only tests set this. */
19
+ readonly fetch?: typeof globalThis.fetch;
20
+ }
21
+ export declare function createPlugin(overrides?: Options): Plugin.Plugin;
22
+ declare const _default: Plugin.Plugin;
23
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,196 @@
1
+ import { DEFAULT_ORIGIN, PROVIDER_ID, PROVIDER_NAME, newestDeepSeekFlash, toModels } from "./catalog.js";
2
+ import { EXPIRED_MESSAGE, SIGN_IN_TIMEOUT_MS, codeFromInput, connectUrl, deviceName, exchangeCode, keyIdFromKey, pkcePair, randomState, startCallbackServer, } from "./signin.js";
3
+ export const PLUGIN_ID = "cail";
4
+ const BROWSER_METHOD = "browser";
5
+ const CODE_METHOD = "code";
6
+ const CATALOG_KEY = "catalog";
7
+ const CATALOG_TIMEOUT_MS = 10_000;
8
+ const CATALOG_REFRESH_MS = 15 * 60 * 1000;
9
+ function origin(options) {
10
+ const value = options.origin;
11
+ return typeof value === "string" && /^https?:\/\/[^/]+$/.test(value) ? value : DEFAULT_ORIGIN;
12
+ }
13
+ export function createPlugin(overrides = {}) {
14
+ return {
15
+ id: PLUGIN_ID,
16
+ async setup(ctx) {
17
+ const lab = overrides.origin ?? origin(ctx.options);
18
+ const fetch = overrides.fetch ?? globalThis.fetch;
19
+ const device = deviceName(overrides.hostname ?? (typeof ctx.options.hostname === "string" ? ctx.options.hostname : undefined));
20
+ // The last catalog read keeps models listed when OpenCode starts offline.
21
+ const cached = await ctx.storage.get(CATALOG_KEY).catch(() => undefined);
22
+ const state = { models: modelsFrom(cached), raw: JSON.stringify(cached ?? null) };
23
+ const credential = (issued, methodID) => ({
24
+ type: "oauth",
25
+ methodID: methodID,
26
+ access: issued.key,
27
+ refresh: "",
28
+ expires: issued.expires,
29
+ metadata: { signedIn: new Date().toISOString().slice(0, 10), ...(issued.keyName ? { keyName: issued.keyName } : {}) },
30
+ });
31
+ // A new sign-in replaces the key from this computer's last sign-in. A
32
+ // pasted key is a "key" credential and is never replaced.
33
+ const previousSignIn = async () => {
34
+ try {
35
+ const connection = await ctx.integration.connection.active(PROVIDER_ID);
36
+ if (!connection)
37
+ return undefined;
38
+ const value = await ctx.integration.connection.resolve(connection);
39
+ const keyId = value?.type === "oauth" ? keyIdFromKey(value.access) : undefined;
40
+ return keyId ? { connection, keyId } : undefined;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ };
46
+ // OpenCode keeps every sign-in as its own account and plugins cannot
47
+ // remove one, so mark the account whose key the Lab just replaced.
48
+ const signIn = async (previous, exchange, methodID) => {
49
+ const issued = await exchange(previous?.keyId);
50
+ if (previous) {
51
+ await ctx.integration.connection
52
+ .status({
53
+ integrationID: PROVIDER_ID,
54
+ connection: previous.connection,
55
+ status: {
56
+ status: "needs_auth",
57
+ message: "Replaced by a newer CUNY AI Lab sign-in. You can remove this account.",
58
+ },
59
+ })
60
+ .catch(() => { });
61
+ }
62
+ return credential(issued, methodID);
63
+ };
64
+ let pending;
65
+ const refresh = async (value) => {
66
+ if (value.expires > Date.now())
67
+ return value;
68
+ throw new Error(EXPIRED_MESSAGE);
69
+ };
70
+ await ctx.integration.transform((editor) => {
71
+ editor.update(PROVIDER_ID, (integration) => {
72
+ integration.name = PROVIDER_NAME;
73
+ });
74
+ editor.method.update({
75
+ integrationID: PROVIDER_ID,
76
+ method: { id: BROWSER_METHOD, type: "oauth", label: "Sign in with CUNY AI Lab" },
77
+ refresh,
78
+ label: (value) => labelFor(value),
79
+ authorize: async () => {
80
+ pending?.close();
81
+ const { verifier, challenge } = pkcePair();
82
+ const state = randomState();
83
+ const previous = await previousSignIn();
84
+ const server = await startCallbackServer({
85
+ state,
86
+ complete: (code) => signIn(previous, (replaceKeyId) => exchangeCode({ code, verifier, replaceKeyId, origin: lab, fetch }), BROWSER_METHOD),
87
+ });
88
+ pending = server;
89
+ return {
90
+ mode: "auto",
91
+ url: connectUrl(lab, { device, challenge, redirectUri: server.redirectUri, state }),
92
+ instructions: "Sign in with your CUNY Login in the browser and click Connect OpenCode.",
93
+ expiresAt: Date.now() + SIGN_IN_TIMEOUT_MS,
94
+ callback: server.result,
95
+ };
96
+ },
97
+ });
98
+ editor.method.update({
99
+ integrationID: PROVIDER_ID,
100
+ method: { id: CODE_METHOD, type: "oauth", label: "Sign in with CUNY AI Lab using a code" },
101
+ refresh,
102
+ label: (value) => labelFor(value),
103
+ authorize: async () => {
104
+ const { verifier, challenge } = pkcePair();
105
+ const previous = await previousSignIn();
106
+ return {
107
+ mode: "code",
108
+ url: connectUrl(lab, { device, challenge }),
109
+ instructions: "Sign in with your CUNY Login, click Connect OpenCode, and paste the code the page shows.",
110
+ expiresAt: Date.now() + SIGN_IN_TIMEOUT_MS,
111
+ callback: (input) => signIn(previous, (replaceKeyId) => exchangeCode({ code: codeFromInput(input), verifier, replaceKeyId, origin: lab, fetch }), CODE_METHOD),
112
+ };
113
+ },
114
+ });
115
+ editor.method.update({
116
+ integrationID: PROVIDER_ID,
117
+ method: { type: "key", label: "CUNY AI Lab API key" },
118
+ });
119
+ editor.method.update({
120
+ integrationID: PROVIDER_ID,
121
+ method: { type: "env", names: ["AILAB_API_KEY"] },
122
+ });
123
+ });
124
+ await ctx.provider.transform((editor) => {
125
+ editor.add({
126
+ info: {
127
+ id: PROVIDER_ID,
128
+ integrationID: PROVIDER_ID,
129
+ name: PROVIDER_NAME,
130
+ activation: "auto",
131
+ package: "@opencode/ai/providers/openai-compatible",
132
+ settings: { baseURL: `${lab}/v1`, provider: PROVIDER_ID },
133
+ },
134
+ models: state.models,
135
+ });
136
+ });
137
+ // OpenCode falls back to the first available model, which can be a free
138
+ // model from OpenCode's own service. Prefer the newest DeepSeek Flash
139
+ // unless a default is already set, for example by `model` in the config.
140
+ await ctx.model.transform((editor) => {
141
+ if (editor.default.get())
142
+ return;
143
+ const preferred = newestDeepSeekFlash(editor.list(PROVIDER_ID));
144
+ if (preferred)
145
+ editor.default.set(PROVIDER_ID, preferred.id);
146
+ });
147
+ const load = async () => {
148
+ let payload;
149
+ try {
150
+ const response = await fetch(`${lab}/v1/models`, {
151
+ headers: { accept: "application/json" },
152
+ signal: AbortSignal.timeout(CATALOG_TIMEOUT_MS),
153
+ });
154
+ if (!response.ok)
155
+ return;
156
+ payload = await response.json();
157
+ toModels(payload);
158
+ }
159
+ catch {
160
+ // Keep the last good list through outages instead of flickering models.
161
+ return;
162
+ }
163
+ const raw = JSON.stringify(payload);
164
+ if (raw === state.raw)
165
+ return;
166
+ state.raw = raw;
167
+ state.models = toModels(payload);
168
+ await ctx.storage.set(CATALOG_KEY, payload).catch(() => { });
169
+ await ctx.provider.reload();
170
+ };
171
+ void load();
172
+ const timer = setInterval(() => void load(), CATALOG_REFRESH_MS);
173
+ return () => {
174
+ clearInterval(timer);
175
+ pending?.close();
176
+ };
177
+ },
178
+ };
179
+ }
180
+ function modelsFrom(payload) {
181
+ try {
182
+ return payload === undefined ? [] : toModels(payload);
183
+ }
184
+ catch {
185
+ return [];
186
+ }
187
+ }
188
+ function labelFor(value) {
189
+ const name = value.metadata?.keyName;
190
+ const date = value.metadata?.signedIn;
191
+ if (typeof name !== "string")
192
+ return undefined;
193
+ return typeof date === "string" ? `${name}, signed in ${date}` : name;
194
+ }
195
+ export default createPlugin();
196
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,aAAa,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACxG,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,UAAU,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,mBAAmB,GAGpB,MAAM,aAAa,CAAA;AAEpB,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAA;AAC/B,MAAM,cAAc,GAAG,SAAS,CAAA;AAChC,MAAM,WAAW,GAAG,MAAM,CAAA;AAC1B,MAAM,WAAW,GAAG,SAAS,CAAA;AAC7B,MAAM,kBAAkB,GAAG,MAAM,CAAA;AACjC,MAAM,kBAAkB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAWzC,SAAS,MAAM,CAAC,OAAkC;IAChD,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAA;IAC5B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,cAAc,CAAA;AAC/F,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,YAAqB,EAAE;IAClD,OAAO;QACL,EAAE,EAAE,SAAS;QACb,KAAK,CAAC,KAAK,CAAC,GAAG;YACb,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,IAAI,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACnD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAA;YACjD,MAAM,MAAM,GAAG,UAAU,CACvB,SAAS,CAAC,QAAQ,IAAI,CAAC,OAAO,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CACpG,CAAA;YAED,0EAA0E;YAC1E,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;YACxE,MAAM,KAAK,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE,CAAA;YAEjF,MAAM,UAAU,GAAG,CAAC,MAAiB,EAAE,QAAgB,EAAoB,EAAE,CAAC,CAAC;gBAC7E,IAAI,EAAE,OAAO;gBACb,QAAQ,EAAE,QAAwC;gBAClD,MAAM,EAAE,MAAM,CAAC,GAAG;gBAClB,OAAO,EAAE,EAAE;gBACX,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;aACtH,CAAC,CAAA;YAEF,sEAAsE;YACtE,0DAA0D;YAC1D,MAAM,cAAc,GAAG,KAAK,IAAI,EAAE;gBAChC,IAAI,CAAC;oBACH,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;oBACvE,IAAI,CAAC,UAAU;wBAAE,OAAO,SAAS,CAAA;oBACjC,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;oBAClE,MAAM,KAAK,GAAG,KAAK,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;oBAC9E,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;gBAClD,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,SAAS,CAAA;gBAClB,CAAC;YACH,CAAC,CAAA;YAED,qEAAqE;YACrE,mEAAmE;YACnE,MAAM,MAAM,GAAG,KAAK,EAClB,QAAoD,EACpD,QAAkE,EAClE,QAAgB,EAChB,EAAE;gBACF,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;gBAC9C,IAAI,QAAQ,EAAE,CAAC;oBACb,MAAM,GAAG,CAAC,WAAW,CAAC,UAAU;yBAC7B,MAAM,CAAC;wBACN,aAAa,EAAE,WAAW;wBAC1B,UAAU,EAAE,QAAQ,CAAC,UAAU;wBAC/B,MAAM,EAAE;4BACN,MAAM,EAAE,YAAY;4BACpB,OAAO,EAAE,uEAAuE;yBACjF;qBACF,CAAC;yBACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;gBACpB,CAAC;gBACD,OAAO,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;YACrC,CAAC,CAAA;YAED,IAAI,OAAqD,CAAA;YACzD,MAAM,OAAO,GAAG,KAAK,EAAE,KAAuB,EAAE,EAAE;gBAChD,IAAI,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE;oBAAE,OAAO,KAAK,CAAA;gBAC5C,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAA;YAClC,CAAC,CAAA;YAED,MAAM,GAAG,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE;gBACzC,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,WAAW,EAAE,EAAE;oBACzC,WAAW,CAAC,IAAI,GAAG,aAAa,CAAA;gBAClC,CAAC,CAAC,CAAA;gBACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;oBACnB,aAAa,EAAE,WAAW;oBAC1B,MAAM,EAAE,EAAE,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,0BAA0B,EAAE;oBAChF,OAAO;oBACP,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC;oBACjC,SAAS,EAAE,KAAK,IAAI,EAAE;wBACpB,OAAO,EAAE,KAAK,EAAE,CAAA;wBAChB,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE,CAAA;wBAC1C,MAAM,KAAK,GAAG,WAAW,EAAE,CAAA;wBAC3B,MAAM,QAAQ,GAAG,MAAM,cAAc,EAAE,CAAA;wBACvC,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC;4BACvC,KAAK;4BACL,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CACjB,MAAM,CACJ,QAAQ,EACR,CAAC,YAAY,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EACpF,cAAc,CACf;yBACJ,CAAC,CAAA;wBACF,OAAO,GAAG,MAAM,CAAA;wBAChB,OAAO;4BACL,IAAI,EAAE,MAAM;4BACZ,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC;4BACnF,YAAY,EAAE,yEAAyE;4BACvF,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,kBAAkB;4BAC1C,QAAQ,EAAE,MAAM,CAAC,MAAM;yBACxB,CAAA;oBACH,CAAC;iBACF,CAAC,CAAA;gBACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;oBACnB,aAAa,EAAE,WAAW;oBAC1B,MAAM,EAAE,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,uCAAuC,EAAE;oBAC1F,OAAO;oBACP,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC;oBACjC,SAAS,EAAE,KAAK,IAAI,EAAE;wBACpB,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,QAAQ,EAAE,CAAA;wBAC1C,MAAM,QAAQ,GAAG,MAAM,cAAc,EAAE,CAAA;wBACvC,OAAO;4BACL,IAAI,EAAE,MAAM;4BACZ,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;4BAC3C,YAAY,EAAE,0FAA0F;4BACxG,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,kBAAkB;4BAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAClB,MAAM,CACJ,QAAQ,EACR,CAAC,YAAY,EAAE,EAAE,CACf,YAAY,CAAC,EAAE,IAAI,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAC1F,WAAW,CACZ;yBACJ,CAAA;oBACH,CAAC;iBACF,CAAC,CAAA;gBACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;oBACnB,aAAa,EAAE,WAAW;oBAC1B,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,qBAAqB,EAAE;iBACtD,CAAC,CAAA;gBACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;oBACnB,aAAa,EAAE,WAAW;oBAC1B,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,eAAe,CAAC,EAAE;iBAClD,CAAC,CAAA;YACJ,CAAC,CAAC,CAAA;YAEF,MAAM,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE;gBACtC,MAAM,CAAC,GAAG,CAAC;oBACT,IAAI,EAAE;wBACJ,EAAE,EAAE,WAA0B;wBAC9B,aAAa,EAAE,WAA0D;wBACzE,IAAI,EAAE,aAAa;wBACnB,UAAU,EAAE,MAAM;wBAClB,OAAO,EAAE,0CAA0C;wBACnD,QAAQ,EAAE,EAAE,OAAO,EAAE,GAAG,GAAG,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE;qBAC1D;oBACD,MAAM,EAAE,KAAK,CAAC,MAAM;iBACrB,CAAC,CAAA;YACJ,CAAC,CAAC,CAAA;YAEF,wEAAwE;YACxE,sEAAsE;YACtE,yEAAyE;YACzE,MAAM,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE;gBACnC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE;oBAAE,OAAM;gBAChC,MAAM,SAAS,GAAG,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAA;gBAC/D,IAAI,SAAS;oBAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,EAAE,CAAC,CAAA;YAC9D,CAAC,CAAC,CAAA;YAEF,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;gBACtB,IAAI,OAAgB,CAAA;gBACpB,IAAI,CAAC;oBACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,GAAG,YAAY,EAAE;wBAC/C,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;wBACvC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,kBAAkB,CAAC;qBAChD,CAAC,CAAA;oBACF,IAAI,CAAC,QAAQ,CAAC,EAAE;wBAAE,OAAM;oBACxB,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;oBAC/B,QAAQ,CAAC,OAAO,CAAC,CAAA;gBACnB,CAAC;gBAAC,MAAM,CAAC;oBACP,wEAAwE;oBACxE,OAAM;gBACR,CAAC;gBACD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;gBACnC,IAAI,GAAG,KAAK,KAAK,CAAC,GAAG;oBAAE,OAAM;gBAC7B,KAAK,CAAC,GAAG,GAAG,GAAG,CAAA;gBACf,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAA;gBAChC,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,OAAgB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;gBACpE,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAA;YAC7B,CAAC,CAAA;YAED,KAAK,IAAI,EAAE,CAAA;YACX,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,EAAE,kBAAkB,CAAC,CAAA;YAChE,OAAO,GAAG,EAAE;gBACV,aAAa,CAAC,KAAK,CAAC,CAAA;gBACpB,OAAO,EAAE,KAAK,EAAE,CAAA;YAClB,CAAC,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,OAAgB;IAClC,IAAI,CAAC;QACH,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAAuB;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAA;IACpC,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAA;IACrC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAA;IAC9C,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,eAAe,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACvE,CAAC;AAED,eAAe,YAAY,EAAE,CAAA"}
@@ -0,0 +1,48 @@
1
+ export declare const CLIENT = "opencode";
2
+ export declare const EXPIRED_MESSAGE = "Your CUNY AI Lab sign-in has expired. Sign in with CUNY AI Lab again.";
3
+ export declare const SIGN_IN_TIMEOUT_MS: number;
4
+ export declare function pkcePair(): {
5
+ verifier: string;
6
+ challenge: string;
7
+ };
8
+ export declare function randomState(): string;
9
+ /** The computer's name as the Lab shows it: the hostname, limited to the characters it accepts. */
10
+ export declare function deviceName(hostname?: string): string;
11
+ /** The key id inside a personal key, formatted as the UUID the Lab uses. */
12
+ export declare function keyIdFromKey(key: unknown): string | undefined;
13
+ export declare function isPersonalKey(key: unknown): key is string;
14
+ export declare function connectUrl(origin: string, input: {
15
+ device: string;
16
+ challenge: string;
17
+ redirectUri?: string;
18
+ state?: string;
19
+ }): string;
20
+ /** Accept the code itself, or the callback address a browser on another computer could not open. */
21
+ export declare function codeFromInput(input: unknown, state?: string): string;
22
+ export interface IssuedKey {
23
+ readonly key: string;
24
+ readonly expires: number;
25
+ readonly keyName: string | undefined;
26
+ }
27
+ export declare function exchangeCode(input: {
28
+ code: string;
29
+ verifier: string;
30
+ replaceKeyId?: string;
31
+ origin: string;
32
+ fetch?: typeof globalThis.fetch;
33
+ }): Promise<IssuedKey>;
34
+ export interface CallbackServer<T> {
35
+ readonly redirectUri: string;
36
+ readonly result: Promise<T>;
37
+ close(): void;
38
+ }
39
+ /**
40
+ * One-shot loopback server for the connect page's redirect. It runs the
41
+ * exchange before answering, so the browser shows the real outcome, and it
42
+ * closes itself when the sign-in finishes or times out.
43
+ */
44
+ export declare function startCallbackServer<T>(input: {
45
+ state: string;
46
+ complete: (code: string) => Promise<T>;
47
+ timeoutMs?: number;
48
+ }): Promise<CallbackServer<T>>;
package/dist/signin.js ADDED
@@ -0,0 +1,208 @@
1
+ /**
2
+ * "Sign in with CUNY AI Lab" for OpenCode.
3
+ *
4
+ * OpenCode opens the Lab's connect page in the browser with a PKCE challenge.
5
+ * After CUNY Login the member approves OpenCode, and the page hands back a
6
+ * one-time code: to a loopback callback on this computer, or, in the code
7
+ * method, as a code to paste. The plugin trades the code and its verifier for
8
+ * a personal key named "OpenCode on <computer>" that expires after 180 days.
9
+ * Signing in again replaces the key from the previous sign-in rather than
10
+ * adding another.
11
+ *
12
+ * Nothing here logs the code, the verifier, or the key.
13
+ */
14
+ import { createHash, randomBytes } from "node:crypto";
15
+ import { createServer } from "node:http";
16
+ import os from "node:os";
17
+ export const CLIENT = "opencode";
18
+ export const EXPIRED_MESSAGE = "Your CUNY AI Lab sign-in has expired. Sign in with CUNY AI Lab again.";
19
+ export const SIGN_IN_TIMEOUT_MS = 10 * 60 * 1000;
20
+ const EXCHANGE_TIMEOUT_MS = 30_000;
21
+ const CALLBACK_PATH = "/callback";
22
+ const SIGN_IN_CODE = /^[A-Za-z0-9_-]{43}$/;
23
+ const PERSONAL_KEY = /^sk-cail-p_([0-9a-f]{32})_[A-Za-z0-9]{40}$/;
24
+ const base64Url = (bytes) => Buffer.from(bytes).toString("base64url");
25
+ export function pkcePair() {
26
+ const verifier = base64Url(randomBytes(32));
27
+ return { verifier, challenge: base64Url(createHash("sha256").update(verifier).digest()) };
28
+ }
29
+ export function randomState() {
30
+ return base64Url(randomBytes(24));
31
+ }
32
+ /** The computer's name as the Lab shows it: the hostname, limited to the characters it accepts. */
33
+ export function deviceName(hostname = os.hostname()) {
34
+ const name = hostname
35
+ .replace(/\.local$/i, "")
36
+ .replace(/[^A-Za-z0-9._ -]+/g, "-")
37
+ .slice(0, 64)
38
+ .replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, "");
39
+ return name || "computer";
40
+ }
41
+ /** The key id inside a personal key, formatted as the UUID the Lab uses. */
42
+ export function keyIdFromKey(key) {
43
+ const hex = typeof key === "string" ? PERSONAL_KEY.exec(key)?.[1] : undefined;
44
+ if (!hex)
45
+ return undefined;
46
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
47
+ }
48
+ export function isPersonalKey(key) {
49
+ return typeof key === "string" && PERSONAL_KEY.test(key);
50
+ }
51
+ export function connectUrl(origin, input) {
52
+ const query = new URLSearchParams({ client: CLIENT, device: input.device, code_challenge: input.challenge });
53
+ if (input.redirectUri && input.state) {
54
+ query.set("redirect_uri", input.redirectUri);
55
+ query.set("state", input.state);
56
+ }
57
+ return `${origin}/model-access/connect?${query}`;
58
+ }
59
+ /** Accept the code itself, or the callback address a browser on another computer could not open. */
60
+ export function codeFromInput(input, state) {
61
+ const value = String(input ?? "").trim();
62
+ if (SIGN_IN_CODE.test(value))
63
+ return value;
64
+ let url;
65
+ try {
66
+ url = new URL(value);
67
+ }
68
+ catch {
69
+ throw new Error("That is not a CUNY AI Lab sign-in code. Copy the code from the browser and try again.");
70
+ }
71
+ if (state !== undefined && url.searchParams.get("state") !== state) {
72
+ throw new Error("That address belongs to a different sign-in. Start signing in again.");
73
+ }
74
+ const code = url.searchParams.get("code") ?? "";
75
+ if (!SIGN_IN_CODE.test(code)) {
76
+ throw new Error("That address has no sign-in code. Copy the code from the browser and try again.");
77
+ }
78
+ return code;
79
+ }
80
+ function errorMessage(payload) {
81
+ const body = payload;
82
+ if (typeof body?.message === "string")
83
+ return body.message;
84
+ if (typeof body?.error?.message === "string")
85
+ return body.error.message;
86
+ return undefined;
87
+ }
88
+ export async function exchangeCode(input) {
89
+ const body = { code: input.code, code_verifier: input.verifier };
90
+ if (input.replaceKeyId)
91
+ body.replace_key_id = input.replaceKeyId;
92
+ let response;
93
+ try {
94
+ response = await (input.fetch ?? globalThis.fetch)(`${input.origin}/model-access/v1/connect/token`, {
95
+ method: "POST",
96
+ headers: { accept: "application/json", "content-type": "application/json" },
97
+ body: JSON.stringify(body),
98
+ signal: AbortSignal.timeout(EXCHANGE_TIMEOUT_MS),
99
+ });
100
+ }
101
+ catch {
102
+ throw new Error("OpenCode could not reach CUNY AI Lab to finish signing in. Check your connection and try again.");
103
+ }
104
+ let payload;
105
+ try {
106
+ payload = await response.json();
107
+ }
108
+ catch {
109
+ payload = undefined;
110
+ }
111
+ if (!response.ok)
112
+ throw new Error(errorMessage(payload) ?? `CUNY AI Lab sign-in failed (HTTP ${response.status}).`);
113
+ const result = payload;
114
+ const expires = result?.credential?.expiresAt;
115
+ if (!isPersonalKey(result?.key) || typeof expires !== "number" || !Number.isSafeInteger(expires)) {
116
+ throw new Error("CUNY AI Lab sent an unexpected sign-in response.");
117
+ }
118
+ const label = result.credential?.label;
119
+ return { key: result.key, expires, keyName: typeof label === "string" ? label : undefined };
120
+ }
121
+ const escapeHtml = (value) => value.replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character] ?? character);
122
+ function page(title, message) {
123
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title><style>body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:15vh auto;padding:0 1.5rem;color:#182d40}@media(prefers-color-scheme:dark){body{background:#0c1620;color:#e3ebf3}}</style></head><body><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p></body></html>`;
124
+ }
125
+ /**
126
+ * One-shot loopback server for the connect page's redirect. It runs the
127
+ * exchange before answering, so the browser shows the real outcome, and it
128
+ * closes itself when the sign-in finishes or times out.
129
+ */
130
+ export async function startCallbackServer(input) {
131
+ let resolve;
132
+ let reject;
133
+ const result = new Promise((onResolve, onReject) => {
134
+ resolve = onResolve;
135
+ reject = onReject;
136
+ });
137
+ result.catch(() => { });
138
+ let claimed = false;
139
+ let settled = false;
140
+ let server;
141
+ const finish = (outcome) => {
142
+ if (settled)
143
+ return;
144
+ settled = true;
145
+ clearTimeout(timer);
146
+ server?.close();
147
+ if ("error" in outcome)
148
+ reject(outcome.error);
149
+ else
150
+ resolve(outcome.value);
151
+ };
152
+ const timer = setTimeout(() => finish({ error: new Error("CUNY AI Lab sign-in timed out. Start signing in again.") }), input.timeoutMs ?? SIGN_IN_TIMEOUT_MS);
153
+ server = createServer((request, response) => {
154
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
155
+ const send = (status, title, message) => {
156
+ response.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
157
+ response.end(page(title, message));
158
+ };
159
+ if (request.method !== "GET" || url.pathname !== CALLBACK_PATH || url.searchParams.get("state") !== input.state) {
160
+ send(404, "Not found", "This is OpenCode's CUNY AI Lab sign-in helper. Nothing is waiting at this address.");
161
+ return;
162
+ }
163
+ if (claimed || settled) {
164
+ send(409, "Already handled", "OpenCode has already finished this sign-in. You can close this tab.");
165
+ return;
166
+ }
167
+ if (url.searchParams.get("error") === "access_denied") {
168
+ send(200, "Sign-in cancelled", "OpenCode was not connected. You can close this tab.");
169
+ finish({ error: new Error("Sign-in cancelled") });
170
+ return;
171
+ }
172
+ let code;
173
+ try {
174
+ code = codeFromInput(url.href, input.state);
175
+ }
176
+ catch (error) {
177
+ send(400, "Sign-in failed", error.message);
178
+ return;
179
+ }
180
+ claimed = true;
181
+ input.complete(code).then((value) => {
182
+ send(200, "OpenCode is connected", "OpenCode is signed in to CUNY AI Lab. You can close this tab.");
183
+ finish({ value });
184
+ }, (error) => {
185
+ send(502, "Sign-in failed", `${error.message} Return to OpenCode to try again.`);
186
+ finish({ error });
187
+ });
188
+ });
189
+ const listening = server;
190
+ await new Promise((onListen, onError) => {
191
+ listening.once("error", onError);
192
+ listening.listen(0, "127.0.0.1", () => {
193
+ listening.off("error", onError);
194
+ onListen();
195
+ });
196
+ });
197
+ const address = listening.address();
198
+ if (!address || typeof address === "string") {
199
+ finish({ error: new Error("OpenCode could not start its sign-in helper.") });
200
+ throw new Error("OpenCode could not start its sign-in helper.");
201
+ }
202
+ return {
203
+ redirectUri: `http://127.0.0.1:${address.port}${CALLBACK_PATH}`,
204
+ result,
205
+ close: () => finish({ error: new Error("Sign-in cancelled") }),
206
+ };
207
+ }
208
+ //# sourceMappingURL=signin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signin.js","sourceRoot":"","sources":["../src/signin.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@cuny-ai-lab/cail-opencode",
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": "CUNY AI Lab models and Sign in with CUNY AI Lab for OpenCode",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/CUNY-AI-Lab/cail-opencode.git"
10
+ },
11
+ "homepage": "https://github.com/CUNY-AI-Lab/cail-opencode#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/CUNY-AI-Lab/cail-opencode/issues"
14
+ },
15
+ "keywords": [
16
+ "opencode",
17
+ "opencode-plugin",
18
+ "cuny",
19
+ "cuny-ai-lab"
20
+ ],
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "provenance": true
38
+ },
39
+ "opencode": {
40
+ "tested": "2.2.7"
41
+ },
42
+ "scripts": {
43
+ "build": "bun -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
44
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p test/tsconfig.json --noEmit",
45
+ "test": "bun test test/unit",
46
+ "test:opencode": "bun run build && bun test/opencode-smoke.ts",
47
+ "prepack": "bun run build",
48
+ "test:live": "bun run build && bun test/live.ts"
49
+ },
50
+ "devDependencies": {
51
+ "@opencode/plugin": "2.2.7",
52
+ "@types/bun": "1.4.4",
53
+ "typescript": "5.9.3"
54
+ },
55
+ "packageManager": "bun@1.4.3"
56
+ }