opencode-cmd-provider 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.
Files changed (46) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +172 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +7 -0
  6. package/dist/src/env.d.ts +7 -0
  7. package/dist/src/env.js +24 -0
  8. package/dist/src/plugin/auth-server.d.ts +30 -0
  9. package/dist/src/plugin/auth-server.js +158 -0
  10. package/dist/src/plugin/auth.d.ts +6 -0
  11. package/dist/src/plugin/auth.js +38 -0
  12. package/dist/src/plugin/index.d.ts +6 -0
  13. package/dist/src/plugin/index.js +39 -0
  14. package/dist/src/plugin/models.d.ts +7 -0
  15. package/dist/src/plugin/models.js +50 -0
  16. package/dist/src/provider/aisdk-types.d.ts +9 -0
  17. package/dist/src/provider/aisdk-types.js +1 -0
  18. package/dist/src/provider/auth-key.d.ts +7 -0
  19. package/dist/src/provider/auth-key.js +65 -0
  20. package/dist/src/provider/command-code-model.d.ts +39 -0
  21. package/dist/src/provider/command-code-model.js +425 -0
  22. package/dist/src/provider/converters.d.ts +33 -0
  23. package/dist/src/provider/converters.js +256 -0
  24. package/dist/src/provider/cost.d.ts +19 -0
  25. package/dist/src/provider/cost.js +19 -0
  26. package/dist/src/provider/index.d.ts +5 -0
  27. package/dist/src/provider/index.js +9 -0
  28. package/dist/src/provider/json-schema.d.ts +1 -0
  29. package/dist/src/provider/json-schema.js +374 -0
  30. package/dist/src/provider/modalities.d.ts +9 -0
  31. package/dist/src/provider/modalities.js +53 -0
  32. package/dist/src/provider/models.d.ts +29 -0
  33. package/dist/src/provider/models.js +229 -0
  34. package/dist/src/provider/pricing.d.ts +24 -0
  35. package/dist/src/provider/pricing.js +188 -0
  36. package/dist/src/provider/project-slug.d.ts +1 -0
  37. package/dist/src/provider/project-slug.js +10 -0
  38. package/dist/src/provider/reasoning.d.ts +29 -0
  39. package/dist/src/provider/reasoning.js +74 -0
  40. package/dist/src/provider/redact.d.ts +2 -0
  41. package/dist/src/provider/redact.js +59 -0
  42. package/dist/src/provider/retry.d.ts +8 -0
  43. package/dist/src/provider/retry.js +83 -0
  44. package/dist/src/provider/stream.d.ts +5 -0
  45. package/dist/src/provider/stream.js +105 -0
  46. package/package.json +58 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-08-15
4
+
5
+ Initial release: Command Code provider + plugin for opencode.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 opencode-cmd-provider contributors
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 ADDED
@@ -0,0 +1,172 @@
1
+ # opencode-cmd-provider
2
+
3
+ [![CI](https://github.com/rashidrazak/opencode-cmd-provider/actions/workflows/ci.yml/badge.svg)](https://github.com/rashidrazak/opencode-cmd-provider/actions/workflows/ci.yml)
4
+
5
+ A provider and plugin for [opencode](https://opencode.ai) that connects to the [Command Code](https://commandcode.ai) Provider API.
6
+
7
+ > **Disclaimer:** This is an unofficial, community-maintained integration. It is not affiliated with, endorsed by, or supported by Command Code. You need your own Command Code account and API key or subscription. Command Code's terms, availability, and pricing apply.
8
+
9
+ ## Install
10
+
11
+ Add the package to your opencode configuration:
12
+
13
+ ```jsonc
14
+ // opencode.json
15
+ {
16
+ "$schema": "https://opencode.ai/config.json",
17
+ "plugin": ["opencode-cmd-provider"],
18
+ "provider": {
19
+ "commandcode": {
20
+ "npm": "opencode-cmd-provider",
21
+ "name": "Command Code",
22
+ "options": { "baseURL": "https://api.commandcode.ai" },
23
+ "models": {
24
+ "claude-sonnet-5": {
25
+ "name": "Claude Sonnet 5",
26
+ "limit": { "context": 200000, "output": 65536 },
27
+ },
28
+ },
29
+ },
30
+ },
31
+ }
32
+ ```
33
+
34
+ > **The `models` map is required.** opencode only fires a provider's `models`
35
+ > discovery hook for providers already in its [models.dev](https://models.dev)
36
+ > catalog, and `commandcode` is not in that catalog yet. Declaring the models you
37
+ > want in `provider.commandcode.models` is what makes them appear in `/models`
38
+ > and usable with `opencode run --model commandcode/...`. The reference config is
39
+ > the one written by `scripts/opencode-fixture.mjs`. When `commandcode` lands in
40
+ > the models.dev catalog, this map becomes optional and discovery works via the
41
+ > plugin's `provider.models` hook.
42
+
43
+ Start or reload opencode, then authenticate:
44
+
45
+ ```txt
46
+ /connect
47
+ ```
48
+
49
+ Select **Command Code**, complete the browser flow, and pick a model with `/models`.
50
+
51
+ ## Authentication
52
+
53
+ ### Browser login
54
+
55
+ Run `/connect` in opencode and select **Command Code**. The browser flow stores the returned credential in opencode's auth store.
56
+
57
+ If automatic transfer from the browser fails, copy the API key shown by Command Code and export it as `COMMANDCODE_API_KEY` (see below).
58
+
59
+ ### Environment variable
60
+
61
+ ```sh
62
+ export COMMANDCODE_API_KEY="user_..."
63
+ ```
64
+
65
+ ### Legacy auth files
66
+
67
+ The provider also reads existing credentials from:
68
+
69
+ - `~/.commandcode/auth.json`
70
+ - `~/.omp/agent/auth.json`
71
+ - `~/.pi/agent/auth.json`
72
+
73
+ Supported examples:
74
+
75
+ ```json
76
+ {
77
+ "apiKey": "user_..."
78
+ }
79
+ ```
80
+
81
+ ```json
82
+ {
83
+ "command-code": {
84
+ "type": "api",
85
+ "key": "user_..."
86
+ }
87
+ }
88
+ ```
89
+
90
+ ```json
91
+ {
92
+ "commandcode": "user_..."
93
+ }
94
+ ```
95
+
96
+ ## Usage
97
+
98
+ Pick a model with `/models`, or run non-interactively:
99
+
100
+ ```sh
101
+ opencode run --model commandcode/claude-sonnet-5 "hello"
102
+ ```
103
+
104
+ Model availability changes over time and is refreshed from the Command Code catalog when opencode loads.
105
+
106
+ ### Reasoning support
107
+
108
+ Reasoning metadata is enriched only for models whose Command Code effort support is known. Supported levels are sent as the documented `reasoning_effort` request field; `off`, unsupported levels, and newly discovered models without metadata do not add reasoning fields to the request. No prompt instructions are injected.
109
+
110
+ Reasoning blocks from completed assistant turns are not replayed to Command Code in later requests; only the user-visible text and completed tool calls are sent back as history. This prevents prior private reasoning traces from interfering with reasoning on follow-up turns.
111
+
112
+ ## Model discovery and offline behavior
113
+
114
+ The provider fetches the current model catalog from:
115
+
116
+ ```txt
117
+ https://api.commandcode.ai/provider/v1/models
118
+ ```
119
+
120
+ The last successful catalog is cached at `<data-dir>/commandcode-models.json`, where `<data-dir>` is opencode's XDG data directory (default `~/.local/share/opencode`).
121
+
122
+ If the endpoint is temporarily unavailable, the provider uses the cached catalog. On a first offline start without a cache, opencode still starts cleanly, but Command Code models remain unavailable until the connection is restored.
123
+
124
+ The following environment variables are intended for tests, local mocks, and compatible API endpoints:
125
+
126
+ | Variable | Purpose |
127
+ | ------------------------------- | ---------------------------------------------------------------------------------------------- |
128
+ | `COMMANDCODE_API_BASE` | Override the Command Code API base URL |
129
+ | `COMMANDCODE_MODELS_URL` | Override the model catalog endpoint |
130
+ | `COMMANDCODE_MODELS_CACHE` | Override the model cache file path |
131
+ | `COMMANDCODE_MODELS_TIMEOUT_MS` | Catalog fetch timeout (defaults to 10 seconds; invalid or non-positive values use the default) |
132
+
133
+ ## Image input
134
+
135
+ Image input is advertised only for models marked with the `image` input modality in the Command Code model catalog. The capability snapshot follows the current official CLI catalog; unknown models default to text-only until their upstream metadata is reviewed.
136
+
137
+ For vision-capable models, image blocks from user messages and tool results are forwarded in Command Code's data-URL wire format. Text-only models reject image content before making a network request instead of silently dropping it.
138
+
139
+ ## Pricing display
140
+
141
+ The Command Code Provider API does not currently include prices in its model catalog. This provider keeps a static table for models with known prices so opencode can display estimated request costs.
142
+
143
+ Models missing from that table display zero cost in opencode. This does **not** mean Command Code will bill the request at zero. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value.
144
+
145
+ ## Update and remove
146
+
147
+ Update the installed package, or remove it from the `plugin` array and the `provider.commandcode` block in your `opencode.json`. The npm package is cached under opencode's plugin cache (`~/.cache/opencode/packages/`); remove the cached directory to fully uninstall.
148
+
149
+ ## Development
150
+
151
+ Build, then run the full suite:
152
+
153
+ ```sh
154
+ npm install
155
+ npm run build
156
+ npm test
157
+ npm run format:check
158
+ ```
159
+
160
+ The headless end-to-end test runs the real opencode CLI against a mock Command Code server through the built package:
161
+
162
+ ```sh
163
+ npm run build && npm run test:e2e
164
+ ```
165
+
166
+ `scripts/opencode-fixture.mjs` writes a throwaway `opencode.json` wiring the local build to the mock endpoints. `test:e2e` is a local dev gate (it needs the real `opencode` binary on PATH) and is excluded from `npm test`.
167
+
168
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process.
169
+
170
+ ## License
171
+
172
+ MIT
@@ -0,0 +1,2 @@
1
+ export { createCommandCode } from "./src/provider/index.js";
2
+ export { default } from "./src/plugin/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // index.ts
2
+ // Dual export: opencode's provider loader scans for the first export whose name
3
+ // starts with "create"; its plugin loader early-returns on a V1 default module.
4
+ // Exactly one create* export + a { id, server } default keeps both loaders safe
5
+ // (see DESIGN.md §4). Enforced by tests/contract.test.ts.
6
+ export { createCommandCode } from "./src/provider/index.js";
7
+ export { default } from "./src/plugin/index.js";
@@ -0,0 +1,7 @@
1
+ export declare const DEFAULT_API_BASE = "https://api.commandcode.ai";
2
+ export declare const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models";
3
+ export declare const DEFAULT_MODELS_TIMEOUT_MS = 10000;
4
+ export declare function getApiBase(env?: NodeJS.ProcessEnv): string;
5
+ export declare function getModelsUrl(env?: NodeJS.ProcessEnv): string;
6
+ export declare function getModelsTimeoutMs(env?: NodeJS.ProcessEnv): number;
7
+ export declare function getDataDir(env?: NodeJS.ProcessEnv, homeDir?: () => string): string;
@@ -0,0 +1,24 @@
1
+ // src/env.ts — COMMANDCODE_* environment overrides with safe defaults (PLAN #2 Part A)
2
+ import { homedir } from "node:os";
3
+ export const DEFAULT_API_BASE = "https://api.commandcode.ai";
4
+ export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models";
5
+ export const DEFAULT_MODELS_TIMEOUT_MS = 10_000;
6
+ export function getApiBase(env = process.env) {
7
+ return env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE;
8
+ }
9
+ export function getModelsUrl(env = process.env) {
10
+ return env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL;
11
+ }
12
+ export function getModelsTimeoutMs(env = process.env) {
13
+ const raw = env.COMMANDCODE_MODELS_TIMEOUT_MS;
14
+ if (!raw)
15
+ return DEFAULT_MODELS_TIMEOUT_MS;
16
+ const parsed = Number(raw);
17
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MODELS_TIMEOUT_MS;
18
+ }
19
+ export function getDataDir(env = process.env, homeDir = homedir) {
20
+ const xdg = env.XDG_DATA_HOME;
21
+ if (xdg)
22
+ return xdg;
23
+ return `${homeDir()}/.local/share/opencode`;
24
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Local HTTP callback server for the Command Code browser auth flow.
3
+ *
4
+ * Starts a one-shot server on a CLI-compatible localhost port. The Command Code
5
+ * Studio website POSTs the user's API key to /callback after they authenticate.
6
+ */
7
+ import { type Server } from "node:http";
8
+ export interface AuthCallback {
9
+ apiKey: string;
10
+ state: string;
11
+ userId: string;
12
+ userName: string;
13
+ keyName: string;
14
+ }
15
+ export interface AuthServer {
16
+ server: Server;
17
+ port: number;
18
+ waitForCallback: Promise<AuthCallback>;
19
+ }
20
+ export interface AuthServerOptions {
21
+ startPort?: number;
22
+ portRange?: number;
23
+ }
24
+ /**
25
+ * Start a local HTTP server that listens for the Command Code Studio
26
+ * to POST the API key after the user authenticates in their browser.
27
+ *
28
+ * The server accepts exactly one valid POST to /callback and then closes.
29
+ */
30
+ export declare function startAuthServer(options?: AuthServerOptions): Promise<AuthServer>;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Local HTTP callback server for the Command Code browser auth flow.
3
+ *
4
+ * Starts a one-shot server on a CLI-compatible localhost port. The Command Code
5
+ * Studio website POSTs the user's API key to /callback after they authenticate.
6
+ */
7
+ import { createServer } from "node:http";
8
+ const DEFAULT_PORT = 5959;
9
+ const DEFAULT_PORT_RANGE = 10;
10
+ function listenOnAvailablePort(server, startPort = DEFAULT_PORT, range = DEFAULT_PORT_RANGE) {
11
+ return new Promise((resolve, reject) => {
12
+ let offset = 0;
13
+ const tryListen = () => {
14
+ const useFallbackPort = startPort === 0 || offset >= range;
15
+ const port = useFallbackPort ? 0 : startPort + offset;
16
+ const onError = (err) => {
17
+ server.off("listening", onListening);
18
+ if (err.code === "EADDRINUSE" && !useFallbackPort) {
19
+ offset += 1;
20
+ tryListen();
21
+ return;
22
+ }
23
+ reject(err);
24
+ };
25
+ const onListening = () => {
26
+ server.off("error", onError);
27
+ const address = server.address();
28
+ resolve(address.port);
29
+ };
30
+ server.once("error", onError);
31
+ server.once("listening", onListening);
32
+ server.listen(port, "127.0.0.1");
33
+ };
34
+ tryListen();
35
+ });
36
+ }
37
+ function closeServer(server) {
38
+ server.close((err) => {
39
+ if (err && err.code !== "ERR_SERVER_NOT_RUNNING") {
40
+ // There is nowhere useful to report this during auth cleanup.
41
+ }
42
+ });
43
+ }
44
+ /**
45
+ * Start a local HTTP server that listens for the Command Code Studio
46
+ * to POST the API key after the user authenticates in their browser.
47
+ *
48
+ * The server accepts exactly one valid POST to /callback and then closes.
49
+ */
50
+ export async function startAuthServer(options = {}) {
51
+ let resolveCallback;
52
+ let rejectCallback;
53
+ const waitForCallback = new Promise((resolve, reject) => {
54
+ resolveCallback = resolve;
55
+ rejectCallback = reject;
56
+ });
57
+ const server = createServer((req, res) => {
58
+ // CORS: allow requests from Command Code domains and localhost for dev.
59
+ const origin = req.headers.origin || "";
60
+ const allowedOrigins = [
61
+ "http://localhost:3000",
62
+ "https://staging.commandcode.ai",
63
+ "https://commandcode.ai",
64
+ ];
65
+ const responseOrigin = allowedOrigins.includes(origin) ? origin : allowedOrigins[0];
66
+ const requestedHeaders = req.headers["access-control-request-headers"];
67
+ const allowHeaders = typeof requestedHeaders === "string" && requestedHeaders.length > 0
68
+ ? requestedHeaders
69
+ : "Content-Type";
70
+ res.setHeader("Access-Control-Allow-Origin", responseOrigin);
71
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
72
+ res.setHeader("Access-Control-Allow-Headers", allowHeaders);
73
+ // Chrome's Private Network Access preflight may require this for an HTTPS
74
+ // page posting to a localhost HTTP callback.
75
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
76
+ res.setHeader("Content-Type", "application/json");
77
+ // Handle CORS preflight.
78
+ if (req.method === "OPTIONS") {
79
+ res.writeHead(204);
80
+ res.end();
81
+ return;
82
+ }
83
+ if (req.url !== "/callback") {
84
+ res.writeHead(404);
85
+ res.end(JSON.stringify({ success: false, error: "Not found" }));
86
+ return;
87
+ }
88
+ if (req.method !== "POST") {
89
+ res.writeHead(405);
90
+ res.end(JSON.stringify({
91
+ success: false,
92
+ error: "Method not allowed. Use POST.",
93
+ }));
94
+ return;
95
+ }
96
+ let body = "";
97
+ req.on("data", (chunk) => {
98
+ body += chunk.toString();
99
+ if (body.length > 10_000)
100
+ req.destroy();
101
+ });
102
+ req.on("end", () => {
103
+ try {
104
+ const parsed = JSON.parse(body);
105
+ if (parsed.error) {
106
+ res.writeHead(200);
107
+ res.end(JSON.stringify({ success: true }));
108
+ const description = typeof parsed.error_description === "string"
109
+ ? parsed.error_description
110
+ : String(parsed.error);
111
+ if (parsed.error === "access_denied") {
112
+ rejectCallback(new Error(description || "Authorization was denied by the user"));
113
+ }
114
+ else {
115
+ rejectCallback(new Error(description || String(parsed.error)));
116
+ }
117
+ closeServer(server);
118
+ return;
119
+ }
120
+ const apiKey = typeof parsed.apiKey === "string" ? parsed.apiKey : "";
121
+ const state = typeof parsed.state === "string" ? parsed.state : "";
122
+ const userId = typeof parsed.userId === "string" ? parsed.userId : "";
123
+ const userName = typeof parsed.userName === "string" ? parsed.userName : "";
124
+ const keyName = typeof parsed.keyName === "string" ? parsed.keyName : "";
125
+ if (!apiKey || !state || !userId || !userName || !keyName) {
126
+ res.writeHead(400);
127
+ res.end(JSON.stringify({
128
+ success: false,
129
+ error: "Missing required fields",
130
+ }));
131
+ return;
132
+ }
133
+ res.writeHead(200);
134
+ res.end(JSON.stringify({ success: true }));
135
+ resolveCallback({ apiKey, state, userId, userName, keyName });
136
+ closeServer(server);
137
+ }
138
+ catch {
139
+ res.writeHead(400);
140
+ res.end(JSON.stringify({ success: false, error: "Invalid JSON" }));
141
+ }
142
+ });
143
+ req.on("error", () => {
144
+ res.writeHead(500);
145
+ res.end(JSON.stringify({ success: false, error: "Request error" }));
146
+ });
147
+ });
148
+ try {
149
+ const port = await listenOnAvailablePort(server, options.startPort ?? DEFAULT_PORT, options.portRange ?? DEFAULT_PORT_RANGE);
150
+ return { server, port, waitForCallback };
151
+ }
152
+ catch (err) {
153
+ const message = err instanceof Error ? err.message : String(err);
154
+ const error = new Error(`Failed to start auth server: ${message}`);
155
+ rejectCallback(error);
156
+ throw error;
157
+ }
158
+ }
@@ -0,0 +1,6 @@
1
+ import type { AuthOAuthResult } from "@opencode-ai/plugin";
2
+ export interface RunAuthFlowOptions {
3
+ startPort?: number;
4
+ timeoutMs?: number;
5
+ }
6
+ export declare function runAuthFlow(options?: RunAuthFlowOptions): Promise<AuthOAuthResult>;
@@ -0,0 +1,38 @@
1
+ // src/plugin/auth.ts — opencode /connect auth flow (PLAN #10)
2
+ //
3
+ // Wraps the local callback server in the opencode AuthOAuthResult shape:
4
+ // the studio URL is opened in the browser, the studio POSTs the API key to
5
+ // the local /callback endpoint, and callback() resolves with the key.
6
+ import { randomBytes } from "node:crypto";
7
+ import { startAuthServer } from "./auth-server.js";
8
+ const STUDIO_BASE_URL = "https://commandcode.ai";
9
+ const DEFAULT_AUTH_TIMEOUT_MS = 15_000;
10
+ function generateStateToken() {
11
+ return randomBytes(32).toString("base64url");
12
+ }
13
+ export async function runAuthFlow(options = {}) {
14
+ const authServer = await startAuthServer({ startPort: options.startPort });
15
+ const stateToken = generateStateToken();
16
+ const callbackUrl = `http://localhost:${authServer.port}/callback`;
17
+ const url = `${STUDIO_BASE_URL}/studio/auth/cli?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(stateToken)}`;
18
+ const timeoutMs = options.timeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS;
19
+ return {
20
+ url,
21
+ instructions: "Complete the flow in your browser. If automatic transfer fails, set COMMANDCODE_API_KEY to the API key shown by Command Code.",
22
+ method: "auto",
23
+ callback: async () => {
24
+ try {
25
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), timeoutMs));
26
+ const callback = await Promise.race([authServer.waitForCallback, timeout]);
27
+ authServer.server.close();
28
+ if (callback.state !== stateToken)
29
+ return { type: "failed" };
30
+ return { type: "success", key: callback.apiKey };
31
+ }
32
+ catch {
33
+ authServer.server.close();
34
+ return { type: "failed" };
35
+ }
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ declare const _default: {
3
+ id: string;
4
+ server: Plugin;
5
+ };
6
+ export default _default;
@@ -0,0 +1,39 @@
1
+ // src/plugin/index.ts — V1 opencode plugin module (PLAN #11)
2
+ import { getApiBase, getModelsUrl, getModelsTimeoutMs, getDataDir } from "../env.js";
3
+ import { loadCommandCodeModels } from "../provider/models.js";
4
+ import { catalogToOpenCodeModels } from "./models.js";
5
+ import { runAuthFlow } from "./auth.js";
6
+ import { join } from "node:path";
7
+ import { mkdirSync } from "node:fs";
8
+ const server = async () => {
9
+ return {
10
+ auth: {
11
+ provider: "commandcode",
12
+ methods: [
13
+ {
14
+ type: "oauth",
15
+ label: "Command Code",
16
+ authorize: async () => runAuthFlow(),
17
+ },
18
+ ],
19
+ },
20
+ provider: {
21
+ id: "commandcode",
22
+ models: async () => {
23
+ const cacheDir = getDataDir();
24
+ mkdirSync(cacheDir, { recursive: true });
25
+ const cachePath = process.env.COMMANDCODE_MODELS_CACHE ?? join(cacheDir, "commandcode-models.json");
26
+ const loaded = await loadCommandCodeModels({
27
+ url: getModelsUrl(),
28
+ cachePath,
29
+ timeoutMs: getModelsTimeoutMs(),
30
+ });
31
+ return catalogToOpenCodeModels(loaded.models, {
32
+ npm: "opencode-cmd-provider",
33
+ url: getApiBase(),
34
+ });
35
+ },
36
+ },
37
+ };
38
+ };
39
+ export default { id: "commandcode", server };
@@ -0,0 +1,7 @@
1
+ import type { Model } from "@opencode-ai/sdk/v2";
2
+ import type { CommandCodeModel } from "../provider/models.js";
3
+ export interface CatalogMappingOptions {
4
+ npm: string;
5
+ url: string;
6
+ }
7
+ export declare function catalogToOpenCodeModels(models: readonly CommandCodeModel[], options: CatalogMappingOptions): Record<string, Model>;
@@ -0,0 +1,50 @@
1
+ import { MODEL_COSTS, ZERO_MODEL_COST } from "../provider/pricing.js";
2
+ import { isReasoningModel, thinkingMetadataForModel } from "../provider/reasoning.js";
3
+ import { inputModalitiesForModel } from "../provider/modalities.js";
4
+ const DEFAULT_MAX_OUTPUT_TOKENS = 65_536;
5
+ export function catalogToOpenCodeModels(models, options) {
6
+ const out = {};
7
+ for (const model of models) {
8
+ const costs = MODEL_COSTS[model.id] ?? ZERO_MODEL_COST;
9
+ const modalities = inputModalitiesForModel(model.id);
10
+ out[model.id] = {
11
+ id: model.id,
12
+ providerID: "commandcode",
13
+ api: { id: model.id, url: options.url, npm: options.npm },
14
+ name: model.name,
15
+ capabilities: {
16
+ temperature: false,
17
+ reasoning: isReasoningModel(model.id),
18
+ attachment: modalities.includes("image"),
19
+ toolcall: true,
20
+ input: {
21
+ text: true,
22
+ image: modalities.includes("image"),
23
+ audio: false,
24
+ video: false,
25
+ pdf: false,
26
+ },
27
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
28
+ interleaved: false,
29
+ },
30
+ cost: {
31
+ input: costs.input,
32
+ output: costs.output,
33
+ cache: { read: costs.cacheRead, write: costs.cacheWrite },
34
+ },
35
+ limit: {
36
+ context: model.contextWindow,
37
+ output: Math.min(model.contextWindow, DEFAULT_MAX_OUTPUT_TOKENS),
38
+ },
39
+ status: "active",
40
+ options: {
41
+ ...(thinkingMetadataForModel(model.id)
42
+ ? { thinking: thinkingMetadataForModel(model.id) }
43
+ : {}),
44
+ },
45
+ headers: {},
46
+ release_date: "",
47
+ };
48
+ }
49
+ return out;
50
+ }
@@ -0,0 +1,9 @@
1
+ import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart, LanguageModelV3Usage, LanguageModelV3Prompt, LanguageModelV3DataContent, LanguageModelV3FunctionTool } from "@ai-sdk/provider";
2
+ export type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3StreamPart, LanguageModelV3Usage, LanguageModelV3Prompt, LanguageModelV3DataContent, LanguageModelV3FunctionTool, };
3
+ /**
4
+ * Call options for doStream/doGenerate. The installed @ai-sdk/provider (3.x)
5
+ * exports LanguageModelV3CallOptions directly, so this is a plain alias.
6
+ * (The plan sketched a local shape with a `mode` field — that is the v2-era
7
+ * shape; v3 call options have no `mode`.)
8
+ */
9
+ export type ModelCallOptions = LanguageModelV3CallOptions;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export interface AuthKeyOptions {
2
+ apiKey?: string;
3
+ env?: NodeJS.ProcessEnv;
4
+ authPaths?: readonly string[];
5
+ homeDir?: () => string;
6
+ }
7
+ export declare function resolveApiKey(options?: AuthKeyOptions): string | undefined;