opencode-cmd-provider 0.1.2 → 1.0.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/CHANGELOG.md CHANGED
@@ -1,12 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0 - 2026-08-16
4
+
5
+ First stable release: API and behavior locked in, shipping now runs through a tag-driven pipeline.
6
+
7
+ - Tag-driven release pipeline: pushing `vX.Y.Z` asserts the tag matches `package.json`, refreshes the bundled model-catalog snapshot (committing it and re-triggering itself if stale), builds, runs the full test suite, publishes to npm via OIDC trusted publishing with provenance, and creates the GitHub Release from the CHANGELOG section (ADR 0002).
8
+ - Added the `release` skill documenting the pre-tag ritual and post-push verification; removed the run-map skill and the wayfinder-loop script.
9
+ - Docs: spell the product name as OpenCode everywhere, list Command Code plans, credit pi-commandcode-provider, and note provenance requires a public repository.
10
+ - The e2e test now uses the inherited PATH instead of a hardcoded home directory.
11
+
12
+ ## 0.1.3 - 2026-08-16
13
+
14
+ Feature: zero-config install via auto-registration from a bundled model snapshot.
15
+
16
+ - The plugin now bundles a snapshot of the Command Code model catalog (`src/catalog/snapshot.ts`, 55 models) and auto-registers the `commandcode` provider — npm, name, `env`, and every model — during its `config` hook. Installing the plugin is enough: all Command Code models appear in `/models` with the `[CMD]` display-name prefix, no `provider.commandcode` block or `models` map required. No network access is ever needed at runtime.
17
+ - Declared config wins: provider-level keys are filled only when unset, declared models are never modified or removed, models that left the catalog stay usable, and `whitelist`/`blacklist` still filter auto-registered models.
18
+ - The snapshot is regenerated from the live catalog at every release via `npm run refresh:snapshot` (`scripts/refresh-snapshot.mjs`); newly published models appear after a plugin update.
19
+ - Deleted the now-dead network machinery: live catalog fetch and cache (`loadCommandCodeModels`, cache file), the `provider.models` hook, `catalogToOpenCodeModels`, and the `COMMANDCODE_MODELS_*` env vars.
20
+ - `COMMANDCODE_API_BASE` now actually overrides the runtime API base (injected as `options.baseURL` when the user declares no `baseURL` of their own).
21
+
3
22
  ## 0.1.2 - 2026-08-15
4
23
 
5
- Fix: tool-call history round-trip and reasoning effort. Live probes against the Command Code API with opencode confirmed the AI SDK v3 shapes differ from what the converters expected:
24
+ Fix: tool-call history round-trip and reasoning effort. Live probes against the Command Code API with OpenCode confirmed the AI SDK v3 shapes differ from what the converters expected:
6
25
 
7
26
  - Assistant tool-call parts carry arguments under `input`, not `args`/`arguments` — the converter read the legacy fields, so every prior tool call was re-sent with empty `{}` arguments, corrupting multi-turn context.
8
27
  - Tool-result outputs use the v3 `{ type: "text" | "error-text" | "json" | "content", ... }` shapes — `resultText` stringified the wrapper, double-encoding results in history. Replaced with `unwrapToolResult`.
9
- - `reasoning_effort` was silently dropped: opencode passes `providerOptions` namespaced per provider (`{ commandcode: { reasoningEffort } }`), but the model read the top-level keys. Added `resolveProviderReasoning` (namespaced + top-level fallback) so the configured thinking level actually reaches the API.
28
+ - `reasoning_effort` was silently dropped: OpenCode passes `providerOptions` namespaced per provider (`{ commandcode: { reasoningEffort } }`), but the model read the top-level keys. Added `resolveProviderReasoning` (namespaced + top-level fallback) so the configured thinking level actually reaches the API.
10
29
 
11
30
  Also: remove duplicated README disclaimer; bump `@ai-sdk/provider` to 4.x and `@types/node` to 26.x.
12
31
 
@@ -16,9 +35,9 @@ Fix: emit `text-start`/`text-end` and `reasoning-start`/`reasoning-end` stream p
16
35
 
17
36
  ## 0.1.0 - 2026-08-15
18
37
 
19
- Initial release: Command Code provider + plugin for opencode. Published to npm as `opencode-cmd-provider@0.1.0` (`latest`), tagged `v0.1.0`.
38
+ Initial release: Command Code provider + plugin for OpenCode. Published to npm as `opencode-cmd-provider@0.1.0` (`latest`), tagged `v0.1.0`.
20
39
 
21
- - `createCommandCode(options)` AI SDK provider (LanguageModelV3) + `default` V1 opencode plugin module in one package.
40
+ - `createCommandCode(options)` AI SDK provider (LanguageModelV3) + `default` V1 OpenCode plugin module in one package.
22
41
  - `/connect` browser auth flow, `COMMANDCODE_API_KEY` env var, and legacy auth file support (`~/.commandcode/auth.json`, `~/.omp/agent/auth.json`, `~/.pi/agent/auth.json`).
23
42
  - Model catalog fetch, parse, and cache with offline fallback; pricing/cost display; image input; reasoning effort.
24
- - Config-declared `models` map required under `provider.commandcode` until `commandcode` lands in opencode's models.dev catalog.
43
+ - Config-declared `models` map required under `provider.commandcode` until `commandcode` lands in OpenCode's models.dev catalog.
package/README.md CHANGED
@@ -3,52 +3,44 @@
3
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
4
  [![npm](https://img.shields.io/npm/v/opencode-cmd-provider)](https://www.npmjs.com/package/opencode-cmd-provider)
5
5
 
6
- A provider and plugin for [opencode](https://opencode.ai) that connects to the [Command Code](https://commandcode.ai) Provider API.
6
+ A provider and plugin for [OpenCode](https://opencode.ai) that connects to the [Command Code](https://commandcode.ai) Provider API. This enables you to use ALL Command Code plans — Go, GOAT, Pro, Max 10×, Max 20×, Provider, Team, and Enterprise — with OpenCode.
7
7
 
8
8
  > **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.
9
9
 
10
10
  ## Install
11
11
 
12
- Add the package to your opencode configuration:
12
+ Add the package to your OpenCode configuration:
13
13
 
14
14
  ```jsonc
15
15
  // opencode.json
16
16
  {
17
17
  "$schema": "https://opencode.ai/config.json",
18
18
  "plugin": ["opencode-cmd-provider"],
19
- "provider": {
20
- "commandcode": {
21
- "npm": "opencode-cmd-provider",
22
- "name": "Command Code",
23
- "options": { "baseURL": "https://api.commandcode.ai" },
24
- "models": {
25
- "claude-sonnet-5": {
26
- "name": "Claude Sonnet 5",
27
- "limit": { "context": 200000, "output": 65536 },
28
- },
29
- "deepseek/deepseek-v4-flash": {
30
- "name": "DeepSeek V4 Flash",
31
- },
32
- },
33
- },
34
- },
35
19
  }
36
20
  ```
37
21
 
38
- > **The `models` map is required, and each key must be the model's full Command
39
- > Code catalog ID.** opencode only fires a provider's `models` discovery hook for
40
- > providers already in its [models.dev](https://models.dev) catalog, and
41
- > `commandcode` is not in that catalog yet so you must declare the models you
42
- > want. The keys are passed straight to the Command Code API, so use the exact
43
- > catalog IDs (some are prefixed, e.g. `deepseek/deepseek-v4-flash`,
44
- > `google/gemini-3.5-flash`, `xai/grok-4.5`). List the current catalog from the
45
- > terminal with `opencode run --model commandcode/... "hi"` after connecting, or
46
- > hit `https://api.commandcode.ai/provider/v1/models`. An unprefixed or wrong ID
47
- > fails with a `403 Model/provider not recognized` error. When `commandcode`
48
- > lands in the models.dev catalog, this map becomes optional and discovery works
49
- > via the plugin's `provider.models` hook.
50
-
51
- Start or reload opencode, then authenticate:
22
+ That's it. The plugin bundles a snapshot of the Command Code model catalog and
23
+ auto-registers the `commandcode` provider npm package, name, API key env var,
24
+ and every model when OpenCode loads. No `provider.commandcode` block, no
25
+ `models` map, no network access needed. All Command Code models appear in
26
+ `/models` with the `[CMD]` display-name prefix (e.g. `[CMD] Claude Sonnet 5`) so
27
+ they aren't confused with same-named models from other providers.
28
+
29
+ You can still declare your own `provider.commandcode` entry to customize
30
+ behavior; your declarations always win and the snapshot fills in only what's
31
+ missing:
32
+
33
+ - Declared provider-level settings (`name`, `options`, `baseURL`, `env`) are
34
+ kept as you wrote them.
35
+ - Declared models stay exactly as you wrote them — the snapshot never modifies
36
+ or removes them, and models that left the catalog remain usable.
37
+ - `whitelist`/`blacklist` on a declared entry filter the auto-registered models
38
+ too, keeping the picker uncluttered.
39
+
40
+ The catalog snapshot is refreshed at every release; newly published Command
41
+ Code models appear after a plugin update.
42
+
43
+ Start or reload OpenCode, then authenticate:
52
44
 
53
45
  ```txt
54
46
  /connect
@@ -60,7 +52,7 @@ Select **Command Code**, complete the browser flow, and pick a model with `/mode
60
52
 
61
53
  ### Browser login
62
54
 
63
- Run `/connect` in opencode and select **Command Code**. The browser flow stores the returned credential in opencode's auth store.
55
+ Run `/connect` in OpenCode and select **Command Code**. The browser flow stores the returned credential in OpenCode's auth store.
64
56
 
65
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).
66
58
 
@@ -109,34 +101,30 @@ Pick a model with `/models`, or run non-interactively:
109
101
  opencode run --model commandcode/claude-sonnet-5 "hello"
110
102
  ```
111
103
 
112
- Model availability changes over time and is refreshed from the Command Code catalog when opencode loads.
113
-
114
- ### Reasoning support
115
-
116
- 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.
117
-
118
- 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.
119
-
120
104
  ## Model discovery and offline behavior
121
105
 
122
- The provider fetches the current model catalog from:
106
+ The plugin ships a snapshot of the Command Code model catalog (`src/catalog/snapshot.ts`)
107
+ and auto-registers every model into OpenCode's config at startup. Model
108
+ availability changes when the package is updated: the snapshot is regenerated
109
+ from the live catalog at every release via `npm run refresh:snapshot`.
123
110
 
124
- ```txt
125
- https://api.commandcode.ai/provider/v1/models
126
- ```
111
+ Auto-registration adds no network latency to OpenCode startup — the snapshot is
112
+ bundled, and the plugin never contacts the Command Code API to list models. The
113
+ plugin works fully offline; model availability never depends on the catalog
114
+ endpoint being reachable.
127
115
 
128
- 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`).
116
+ The following environment variable is intended for tests, local mocks, and
117
+ compatible API endpoints:
129
118
 
130
- 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.
119
+ | Variable | Purpose |
120
+ | ---------------------- | -------------------------------------- |
121
+ | `COMMANDCODE_API_BASE` | Override the Command Code API base URL |
131
122
 
132
- The following environment variables are intended for tests, local mocks, and compatible API endpoints:
123
+ ### Reasoning support
133
124
 
134
- | Variable | Purpose |
135
- | ------------------------------- | ---------------------------------------------------------------------------------------------- |
136
- | `COMMANDCODE_API_BASE` | Override the Command Code API base URL |
137
- | `COMMANDCODE_MODELS_URL` | Override the model catalog endpoint |
138
- | `COMMANDCODE_MODELS_CACHE` | Override the model cache file path |
139
- | `COMMANDCODE_MODELS_TIMEOUT_MS` | Catalog fetch timeout (defaults to 10 seconds; invalid or non-positive values use the default) |
125
+ 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.
126
+
127
+ 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.
140
128
 
141
129
  ## Image input
142
130
 
@@ -146,13 +134,13 @@ For vision-capable models, image blocks from user messages and tool results are
146
134
 
147
135
  ## Pricing display
148
136
 
149
- 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.
137
+ 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.
150
138
 
151
- 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.
139
+ 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.
152
140
 
153
141
  ## Update and remove
154
142
 
155
- 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.
143
+ Update the installed package, or remove it from the `plugin` array in your `opencode.json` (the auto-registered `provider.commandcode` entry is injected by the plugin, so there is no config block to remove). The npm package is cached under OpenCode's plugin cache (`~/.cache/opencode/packages/`); remove the cached directory to fully uninstall.
156
144
 
157
145
  ## Development
158
146
 
@@ -165,16 +153,20 @@ npm test
165
153
  npm run format:check
166
154
  ```
167
155
 
168
- The headless end-to-end test runs the real opencode CLI against a mock Command Code server through the built package:
156
+ The headless end-to-end test runs the real OpenCode CLI against a mock Command Code server through the built package:
169
157
 
170
158
  ```sh
171
159
  npm run build && npm run test:e2e
172
160
  ```
173
161
 
174
- `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`.
162
+ `scripts/opencode-fixture.mjs` writes a throwaway `opencode.json` wiring only the local build as a plugin — no declared provider or models — so `opencode models` proves auto-registration against a real opencode binary. `test:e2e` is a local dev gate (it needs the real `opencode` binary on PATH) and is excluded from `npm test`.
175
163
 
176
164
  See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process.
177
165
 
166
+ ## Credits
167
+
168
+ Inspired by [pi-commandcode-provider](https://github.com/patlux/pi-commandcode-provider).
169
+
178
170
  ## License
179
171
 
180
172
  MIT
package/dist/index.js CHANGED
@@ -2,6 +2,6 @@
2
2
  // Dual export: opencode's provider loader scans for the first export whose name
3
3
  // starts with "create"; its plugin loader early-returns on a V1 default module.
4
4
  // Exactly one create* export + a { id, server } default keeps both loaders safe
5
- // (see DESIGN.md §4). Enforced by tests/contract.test.ts.
5
+ // (verified loader behavior; see ADR-0001). Enforced by tests/contract.test.ts.
6
6
  export { createCommandCode } from "./src/provider/index.js";
7
7
  export { default } from "./src/plugin/index.js";
@@ -0,0 +1,6 @@
1
+ export interface CatalogModel {
2
+ readonly id: string;
3
+ readonly name: string;
4
+ readonly contextLength: number;
5
+ }
6
+ export declare const MODEL_SNAPSHOT: readonly CatalogModel[];
@@ -0,0 +1,62 @@
1
+ // src/catalog/snapshot.ts — GENERATED by scripts/refresh-snapshot.mjs. Do not edit.
2
+ //
3
+ // A copy of the Command Code model catalog (id, name, context length) bundled
4
+ // in the package so the plugin can auto-register every model without network
5
+ // access at runtime. Regenerate with `npm run refresh:snapshot`.
6
+ export const MODEL_SNAPSHOT = [
7
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", contextLength: 1000000 },
8
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextLength: 1000000 },
9
+ { id: "claude-fable-5", name: "Claude Fable 5", contextLength: 1000000 },
10
+ { id: "claude-opus-5", name: "Claude Opus 5", contextLength: 1000000 },
11
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8", contextLength: 1000000 },
12
+ { id: "claude-opus-4-7", name: "Claude Opus 4.7", contextLength: 1000000 },
13
+ { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", contextLength: 200000 },
14
+ { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", contextLength: 1050000 },
15
+ { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", contextLength: 1050000 },
16
+ { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", contextLength: 1050000 },
17
+ { id: "gpt-5.5", name: "GPT-5.5", contextLength: 200000 },
18
+ { id: "gpt-5.4", name: "GPT-5.4", contextLength: 400000 },
19
+ { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", contextLength: 400000 },
20
+ { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", contextLength: 400000 },
21
+ { id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro (latest)", contextLength: 1000000 },
22
+ { id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash (latest)", contextLength: 1000000 },
23
+ { id: "moonshotai/Kimi-K3", name: "Kimi K3", contextLength: 1000000 },
24
+ { id: "moonshotai/Kimi-K2.7-Code", name: "Kimi K2.7 Code", contextLength: 256000 },
25
+ { id: "moonshotai/Kimi-K2.7-Code-Highspeed", name: "Kimi K2.7 Code HighSpeed", contextLength: 262000 },
26
+ { id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6", contextLength: 256000 },
27
+ { id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5", contextLength: 256000 },
28
+ { id: "zai-org/GLM-5.3", name: "GLM-5.3", contextLength: 1000000 },
29
+ { id: "zai-org/GLM-5.2", name: "GLM-5.2", contextLength: 1000000 },
30
+ { id: "zai-org/GLM-5.2-Fast", name: "GLM-5.2 Fast", contextLength: 1000000 },
31
+ { id: "zai-org/GLM-5.1", name: "GLM-5.1", contextLength: 200000 },
32
+ { id: "zai-org/GLM-5", name: "GLM-5", contextLength: 200000 },
33
+ { id: "MiniMaxAI/MiniMax-M3", name: "MiniMax M3", contextLength: 1000000 },
34
+ { id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7", contextLength: 200000 },
35
+ { id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5", contextLength: 200000 },
36
+ { id: "xiaomi/mimo-v2.5-pro", name: "MiMo V2.5 Pro", contextLength: 1000000 },
37
+ { id: "xiaomi/mimo-v2.5", name: "MiMo V2.5", contextLength: 1000000 },
38
+ { id: "Qwen/Qwen3.8-Max", name: "Qwen 3.8 Max", contextLength: 1000000 },
39
+ { id: "Qwen/Qwen3.7-Max", name: "Qwen 3.7 Max", contextLength: 1000000 },
40
+ { id: "Qwen/Qwen3.7-Plus", name: "Qwen 3.7 Plus", contextLength: 1000000 },
41
+ { id: "Qwen/Qwen3.7-Flash", name: "Qwen 3.7 Flash", contextLength: 1000000 },
42
+ { id: "Qwen/Qwen3.6-Max-Preview", name: "Qwen 3.6 Max Preview", contextLength: 200000 },
43
+ { id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus", contextLength: 200000 },
44
+ { id: "stepfun/Step-3.7-Flash", name: "Step 3.7 Flash", contextLength: 256000 },
45
+ { id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash", contextLength: 1000000 },
46
+ { id: "tencent/hy3-paid", name: "Tencent Hy3", contextLength: 262144 },
47
+ { id: "google/gemini-3.7-flash", name: "Gemini 3.7 Flash", contextLength: 1048576 },
48
+ { id: "google/gemini-3.6-flash", name: "Gemini 3.6 Flash", contextLength: 1000000 },
49
+ { id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash", contextLength: 1000000 },
50
+ { id: "google/gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite", contextLength: 1000000 },
51
+ { id: "google/gemini-3.1-flash-lite", name: "Gemini 3.1 Flash Lite", contextLength: 1000000 },
52
+ { id: "sakana/fugu-ultra", name: "Fugu Ultra", contextLength: 1000000 },
53
+ { id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra", contextLength: 1000000 },
54
+ { id: "thinkingmachines/inkling", name: "Inkling", contextLength: 256000 },
55
+ { id: "thinkingmachines/inkling-small", name: "Inkling Small", contextLength: 1000000 },
56
+ { id: "poolside/laguna-s-2.1-free", name: "Laguna S 2.1", contextLength: 256000 },
57
+ { id: "meta/muse-spark-1.1", name: "Muse Spark 1.1", contextLength: 1048576 },
58
+ { id: "meta/muse-spark-1.2", name: "Muse Spark 1.2", contextLength: 1048576 },
59
+ { id: "meta/muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", contextLength: 1048576 },
60
+ { id: "xai/grok-4.5", name: "Grok 4.5", contextLength: 500000 },
61
+ { id: "xai/grok-4.6", name: "Grok 4.6", contextLength: 500000 },
62
+ ];
package/dist/src/env.d.ts CHANGED
@@ -1,7 +1,2 @@
1
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
2
  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;
package/dist/src/env.js CHANGED
@@ -1,24 +1,5 @@
1
- // src/env.ts — COMMANDCODE_* environment overrides with safe defaults (PLAN #2 Part A)
2
- import { homedir } from "node:os";
1
+ // src/env.ts — COMMANDCODE_* environment overrides with safe defaults
3
2
  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
3
  export function getApiBase(env = process.env) {
7
4
  return env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE;
8
5
  }
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
- }
@@ -1,12 +1,18 @@
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";
1
+ // src/plugin/index.ts — V1 opencode plugin module
2
+ import { MODEL_SNAPSHOT } from "../catalog/snapshot.js";
3
+ import { getApiBase } from "../env.js";
4
+ import { augmentConfigCommandCodeModels, autoRegister } from "./models.js";
5
5
  import { runAuthFlow } from "./auth.js";
6
- import { join } from "node:path";
7
- import { mkdirSync } from "node:fs";
8
6
  const server = async () => {
9
7
  return {
8
+ config: async (config) => {
9
+ autoRegister(config, MODEL_SNAPSHOT, {
10
+ npm: "opencode-cmd-provider",
11
+ name: "Command Code",
12
+ baseURL: getApiBase(),
13
+ });
14
+ augmentConfigCommandCodeModels(config);
15
+ },
10
16
  auth: {
11
17
  provider: "commandcode",
12
18
  methods: [
@@ -17,23 +23,6 @@ const server = async () => {
17
23
  },
18
24
  ],
19
25
  },
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
26
  };
38
27
  };
39
28
  export default { id: "commandcode", server };
@@ -1,7 +1,35 @@
1
- import type { Model } from "@opencode-ai/sdk/v2";
2
- import type { CommandCodeModel } from "../provider/models.js";
3
- export interface CatalogMappingOptions {
1
+ import type { Config } from "@opencode-ai/sdk/v2";
2
+ import type { CatalogModel } from "../catalog/snapshot.js";
3
+ export interface AutoRegisterOptions {
4
4
  npm: string;
5
- url: string;
5
+ name: string;
6
+ baseURL: string;
6
7
  }
7
- export declare function catalogToOpenCodeModels(models: readonly CommandCodeModel[], options: CatalogMappingOptions): Record<string, Model>;
8
+ /**
9
+ * Registers the `commandcode` provider entry into opencode's config so every
10
+ * snapshot model is available without any user declaration.
11
+ *
12
+ * opencode only invokes a plugin's `provider.models` hook for providers that
13
+ * already exist in its models.dev catalog; `commandcode` is not in that
14
+ * catalog, so models reach the session through the config-declared
15
+ * `provider.commandcode.models` map. The `config` hook runs before opencode
16
+ * reads `config.provider` and honors in-place mutation (ADR-0001), which makes
17
+ * this the only zero-declaration mechanism available today.
18
+ *
19
+ * Pure function: takes a config and a snapshot, mutates and returns the config.
20
+ * Merge semantics — the user's declared entry always wins:
21
+ * - provider-level keys (`npm`, `name`, `env`, `options.baseURL`) are filled
22
+ * only when unset;
23
+ * - snapshot models are added only when no declared model claims the id (by
24
+ * config key or by the entry's `id`);
25
+ * - declared models are never modified, and declared models that left the
26
+ * catalog stay usable.
27
+ */
28
+ export declare function autoRegister(config: Config, snapshot: readonly CatalogModel[], options: AutoRegisterOptions): Config;
29
+ /**
30
+ * Augments config-declared commandcode models with reasoning metadata and
31
+ * variants so opencode's `ctrl+t` can cycle reasoning effort.
32
+ *
33
+ * In-place mutation of the config object (the plugin `config` hook contract).
34
+ */
35
+ export declare function augmentConfigCommandCodeModels(config: Config): void;
@@ -1,50 +1,94 @@
1
1
  import { MODEL_COSTS, ZERO_MODEL_COST } from "../provider/pricing.js";
2
- import { isReasoningModel, thinkingMetadataForModel } from "../provider/reasoning.js";
2
+ import { reasoningVariantsForModel } from "../provider/reasoning.js";
3
3
  import { inputModalitiesForModel } from "../provider/modalities.js";
4
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
- };
5
+ /**
6
+ * Registers the `commandcode` provider entry into opencode's config so every
7
+ * snapshot model is available without any user declaration.
8
+ *
9
+ * opencode only invokes a plugin's `provider.models` hook for providers that
10
+ * already exist in its models.dev catalog; `commandcode` is not in that
11
+ * catalog, so models reach the session through the config-declared
12
+ * `provider.commandcode.models` map. The `config` hook runs before opencode
13
+ * reads `config.provider` and honors in-place mutation (ADR-0001), which makes
14
+ * this the only zero-declaration mechanism available today.
15
+ *
16
+ * Pure function: takes a config and a snapshot, mutates and returns the config.
17
+ * Merge semantics — the user's declared entry always wins:
18
+ * - provider-level keys (`npm`, `name`, `env`, `options.baseURL`) are filled
19
+ * only when unset;
20
+ * - snapshot models are added only when no declared model claims the id (by
21
+ * config key or by the entry's `id`);
22
+ * - declared models are never modified, and declared models that left the
23
+ * catalog stay usable.
24
+ */
25
+ export function autoRegister(config, snapshot, options) {
26
+ if (snapshot.length === 0)
27
+ return config;
28
+ const provider = (config.provider ??= {});
29
+ const entry = (provider["commandcode"] ??= {});
30
+ entry.npm ??= options.npm;
31
+ entry.name ??= options.name;
32
+ entry.env ??= ["COMMANDCODE_API_KEY"];
33
+ entry.options ??= {};
34
+ entry.options.baseURL ??= options.baseURL;
35
+ entry.models ??= {};
36
+ const declaredById = new Set(Object.values(entry.models)
37
+ .map((model) => model?.id)
38
+ .filter((id) => typeof id === "string"));
39
+ for (const model of snapshot) {
40
+ if (entry.models[model.id] !== undefined || declaredById.has(model.id))
41
+ continue;
42
+ entry.models[model.id] = configModelFor(model);
48
43
  }
49
- return out;
44
+ return config;
45
+ }
46
+ /**
47
+ * Augments config-declared commandcode models with reasoning metadata and
48
+ * variants so opencode's `ctrl+t` can cycle reasoning effort.
49
+ *
50
+ * In-place mutation of the config object (the plugin `config` hook contract).
51
+ */
52
+ export function augmentConfigCommandCodeModels(config) {
53
+ const provider = config.provider?.["commandcode"];
54
+ if (!provider?.models)
55
+ return;
56
+ for (const [modelId, model] of Object.entries(provider.models)) {
57
+ if (!model)
58
+ continue;
59
+ const variants = reasoningVariantsForModel(modelId);
60
+ if (variants) {
61
+ model.reasoning = true;
62
+ model.variants = variants;
63
+ }
64
+ }
65
+ }
66
+ /**
67
+ * Config-schema model entry (not the SDK `Model` shape) for a snapshot model:
68
+ * `[CMD]`-prefixed display name, context/output limits with output capped at
69
+ * 65_536, and metadata enriched from the reasoning, modality, and pricing
70
+ * tables.
71
+ */
72
+ function configModelFor(model) {
73
+ const variants = reasoningVariantsForModel(model.id);
74
+ const costs = MODEL_COSTS[model.id] ?? ZERO_MODEL_COST;
75
+ return {
76
+ name: `[CMD] ${model.name}`,
77
+ limit: {
78
+ context: model.contextLength,
79
+ output: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
80
+ },
81
+ reasoning: variants ? true : undefined,
82
+ variants: variants,
83
+ modalities: {
84
+ input: [...inputModalitiesForModel(model.id)],
85
+ },
86
+ cost: {
87
+ input: costs.input,
88
+ output: costs.output,
89
+ cache_read: costs.cacheRead,
90
+ cache_write: costs.cacheWrite,
91
+ },
92
+ status: "active",
93
+ };
50
94
  }
@@ -20,6 +20,17 @@ export interface ThinkingMetadata {
20
20
  }
21
21
  export declare function thinkingMetadataForModel(modelId: string): ThinkingMetadata | undefined;
22
22
  export declare function isReasoningModel(modelId: string): boolean;
23
+ /**
24
+ * opencode model `variants` for the Command Code model: one entry per supported
25
+ * reasoning effort, keyed by the effort name. Each variant carries
26
+ * `{ reasoningEffort: effort }`, which opencode merges into the request
27
+ * `providerOptions` when the user cycles reasoning with `ctrl+t`. Without a
28
+ * non-empty `variants` map (and `capabilities.reasoning: true`), opencode has
29
+ * nothing to cycle and the effort is never surfaced.
30
+ */
31
+ export declare function reasoningVariantsForModel(modelId: string): Record<string, {
32
+ reasoningEffort: string;
33
+ }> | undefined;
23
34
  export declare function mappedReasoningEffort(model: {
24
35
  reasoning: boolean;
25
36
  thinkingLevelMap?: Partial<Record<PiThinkingLevel, string | null>>;
@@ -65,6 +65,20 @@ export function thinkingMetadataForModel(modelId) {
65
65
  export function isReasoningModel(modelId) {
66
66
  return MODEL_EFFORTS[modelId] !== undefined;
67
67
  }
68
+ /**
69
+ * opencode model `variants` for the Command Code model: one entry per supported
70
+ * reasoning effort, keyed by the effort name. Each variant carries
71
+ * `{ reasoningEffort: effort }`, which opencode merges into the request
72
+ * `providerOptions` when the user cycles reasoning with `ctrl+t`. Without a
73
+ * non-empty `variants` map (and `capabilities.reasoning: true`), opencode has
74
+ * nothing to cycle and the effort is never surfaced.
75
+ */
76
+ export function reasoningVariantsForModel(modelId) {
77
+ const efforts = MODEL_EFFORTS[modelId];
78
+ if (!efforts)
79
+ return undefined;
80
+ return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }]));
81
+ }
68
82
  export function mappedReasoningEffort(model, options) {
69
83
  const level = options?.reasoning;
70
84
  if (!level || level === "off" || !model.reasoning)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-cmd-provider",
3
- "version": "0.1.2",
3
+ "version": "1.0.0",
4
4
  "description": "Command Code provider + plugin for opencode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,8 +17,9 @@
17
17
  "scripts": {
18
18
  "build": "tsc -p tsconfig.json",
19
19
  "typecheck": "tsc --noEmit",
20
+ "refresh:snapshot": "node scripts/refresh-snapshot.mjs",
20
21
  "test": "npm run typecheck && npm run test:unit && npm run test:integration && npm run test:contract && npm run format:check",
21
- "test:unit": "tsx tests/env.test.ts && tsx tests/auth-key.test.ts && tsx tests/converters.test.ts && tsx tests/stream.test.ts && tsx tests/redact.test.ts && tsx tests/cost.test.ts && tsx tests/retry.test.ts && tsx tests/reasoning.test.ts && tsx tests/modalities.test.ts && tsx tests/models.test.ts && tsx tests/oauth.test.ts && tsx tests/plugin-models.test.ts",
22
+ "test:unit": "tsx tests/env.test.ts && tsx tests/auth-key.test.ts && tsx tests/converters.test.ts && tsx tests/stream.test.ts && tsx tests/redact.test.ts && tsx tests/cost.test.ts && tsx tests/retry.test.ts && tsx tests/reasoning.test.ts && tsx tests/modalities.test.ts && tsx tests/snapshot.test.ts && tsx tests/refresh-snapshot.test.ts && tsx tests/oauth.test.ts && tsx tests/plugin-models.test.ts",
22
23
  "test:integration": "tsx tests/integration-do-stream.test.ts && tsx tests/integration-do-generate.test.ts",
23
24
  "test:contract": "tsx tests/contract.test.ts",
24
25
  "test:e2e": "node tests/e2e-opencode.mjs",
@@ -1,29 +0,0 @@
1
- export declare const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models";
2
- export declare const DEFAULT_MODELS_TIMEOUT_MS = 10000;
3
- export interface CommandCodeModel {
4
- id: string;
5
- name: string;
6
- reasoning: boolean;
7
- contextWindow: number;
8
- maxTokens: number;
9
- }
10
- interface FetchCommandCodeModelsOptions {
11
- url?: string;
12
- fetchImpl?: typeof fetch;
13
- signal?: AbortSignal;
14
- timeoutMs?: number;
15
- }
16
- interface LoadCommandCodeModelsOptions extends FetchCommandCodeModelsOptions {
17
- cachePath: string;
18
- }
19
- export interface LoadCommandCodeModelsResult {
20
- models: readonly CommandCodeModel[];
21
- source: "live" | "cache" | "empty";
22
- warning?: string;
23
- }
24
- export declare function getModelsTimeoutMs(env?: NodeJS.ProcessEnv): number;
25
- export declare function commandCodeModelsFromApiResponse(value: unknown): readonly CommandCodeModel[];
26
- export declare function commandCodeModelsFromCache(value: unknown): readonly CommandCodeModel[];
27
- export declare function fetchCommandCodeModels(options?: FetchCommandCodeModelsOptions): Promise<readonly CommandCodeModel[]>;
28
- export declare function loadCommandCodeModels(options: LoadCommandCodeModelsOptions): Promise<LoadCommandCodeModelsResult>;
29
- export {};
@@ -1,229 +0,0 @@
1
- // src/provider/models.ts — model catalog fetch, parse, cache with offline
2
- // fallback (PLAN #7, port of pi's models.ts)
3
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
- import { dirname } from "node:path";
5
- import { isReasoningModel } from "./reasoning.js";
6
- export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models";
7
- export const DEFAULT_MODELS_TIMEOUT_MS = 10_000;
8
- const DEFAULT_MAX_OUTPUT_TOKENS = 65_536;
9
- const MODEL_CACHE_VERSION = 1;
10
- function isRecord(value) {
11
- return typeof value === "object" && value !== null && !Array.isArray(value);
12
- }
13
- function stringField(record, key) {
14
- const value = record[key];
15
- if (typeof value !== "string" || value.length === 0) {
16
- throw new Error(`Expected ${key} to be a non-empty string`);
17
- }
18
- return value;
19
- }
20
- function booleanField(record, key) {
21
- const value = record[key];
22
- if (typeof value !== "boolean")
23
- throw new Error(`Expected ${key} to be a boolean`);
24
- return value;
25
- }
26
- function positiveNumberField(record, key) {
27
- const value = record[key];
28
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
29
- throw new Error(`Expected ${key} to be a positive number`);
30
- }
31
- return value;
32
- }
33
- function parseApiModel(value) {
34
- if (!isRecord(value))
35
- throw new Error("Expected model entry to be an object");
36
- return {
37
- id: stringField(value, "id"),
38
- name: stringField(value, "name"),
39
- contextLength: positiveNumberField(value, "context_length"),
40
- };
41
- }
42
- function parseCachedModel(value) {
43
- if (!isRecord(value))
44
- throw new Error("Expected cached model entry to be an object");
45
- const id = stringField(value, "id");
46
- booleanField(value, "reasoning");
47
- return {
48
- id,
49
- name: stringField(value, "name"),
50
- reasoning: isReasoningModel(id),
51
- contextWindow: positiveNumberField(value, "contextWindow"),
52
- maxTokens: positiveNumberField(value, "maxTokens"),
53
- };
54
- }
55
- function requireModels(models) {
56
- if (models.length === 0)
57
- throw new Error("Command Code returned an empty model catalog");
58
- return models;
59
- }
60
- function errorMessage(error) {
61
- return error instanceof Error ? error.message : String(error);
62
- }
63
- function abortError(reason) {
64
- if (reason instanceof Error)
65
- return reason;
66
- return new DOMException("The operation was aborted", "AbortError");
67
- }
68
- function configuredTimeoutMs(timeoutMs) {
69
- return timeoutMs !== undefined && Number.isFinite(timeoutMs) && timeoutMs > 0
70
- ? timeoutMs
71
- : DEFAULT_MODELS_TIMEOUT_MS;
72
- }
73
- export function getModelsTimeoutMs(env = process.env) {
74
- const raw = env.COMMANDCODE_MODELS_TIMEOUT_MS;
75
- if (!raw)
76
- return DEFAULT_MODELS_TIMEOUT_MS;
77
- const parsed = Number(raw);
78
- return configuredTimeoutMs(parsed);
79
- }
80
- class ModelDiscoveryTimeoutError extends Error {
81
- constructor(timeoutMs) {
82
- super(`Command Code model discovery timed out after ${timeoutMs}ms`);
83
- this.name = "ModelDiscoveryTimeoutError";
84
- }
85
- }
86
- function runWithTimeout(operation, timeoutMs, externalSignal) {
87
- const controller = new AbortController();
88
- let timer;
89
- let settled = false;
90
- let onExternalAbort;
91
- return new Promise((resolve, reject) => {
92
- const cleanup = () => {
93
- if (timer !== undefined)
94
- clearTimeout(timer);
95
- if (onExternalAbort && externalSignal) {
96
- externalSignal.removeEventListener("abort", onExternalAbort);
97
- }
98
- };
99
- const resolveOnce = (value) => {
100
- if (settled)
101
- return;
102
- settled = true;
103
- cleanup();
104
- resolve(value);
105
- };
106
- const rejectOnce = (error) => {
107
- if (settled)
108
- return;
109
- settled = true;
110
- cleanup();
111
- reject(error);
112
- };
113
- const abort = (reason) => {
114
- const error = abortError(reason);
115
- controller.abort(error);
116
- rejectOnce(error);
117
- };
118
- if (externalSignal?.aborted) {
119
- abort(externalSignal.reason);
120
- return;
121
- }
122
- onExternalAbort = () => abort(externalSignal?.reason);
123
- externalSignal?.addEventListener("abort", onExternalAbort, { once: true });
124
- timer = setTimeout(() => abort(new ModelDiscoveryTimeoutError(timeoutMs)), timeoutMs);
125
- Promise.resolve()
126
- .then(() => operation(controller.signal))
127
- .then(resolveOnce, rejectOnce);
128
- });
129
- }
130
- export function commandCodeModelsFromApiResponse(value) {
131
- if (!isRecord(value))
132
- throw new Error("Expected models response to be an object");
133
- if (value.object !== "list")
134
- throw new Error("Expected models response object to be 'list'");
135
- const data = value.data;
136
- if (!Array.isArray(data))
137
- throw new Error("Expected models response data to be an array");
138
- return data.map(parseApiModel).map((model) => ({
139
- id: model.id,
140
- name: `${model.name} (CC)`,
141
- reasoning: isReasoningModel(model.id),
142
- contextWindow: model.contextLength,
143
- maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
144
- }));
145
- }
146
- export function commandCodeModelsFromCache(value) {
147
- if (!isRecord(value))
148
- throw new Error("Expected model cache to be an object");
149
- if (value.version !== MODEL_CACHE_VERSION) {
150
- throw new Error(`Expected model cache version ${MODEL_CACHE_VERSION}`);
151
- }
152
- if (!Array.isArray(value.models))
153
- throw new Error("Expected cached models to be an array");
154
- return requireModels(value.models.map(parseCachedModel));
155
- }
156
- export async function fetchCommandCodeModels(options = {}) {
157
- const url = options.url ?? DEFAULT_MODELS_URL;
158
- const fetchImpl = options.fetchImpl ?? fetch;
159
- const body = await runWithTimeout(async (signal) => {
160
- const response = await fetchImpl(url, {
161
- headers: {
162
- accept: "application/json",
163
- },
164
- signal,
165
- });
166
- if (!response.ok) {
167
- throw new Error(`Failed to fetch Command Code models: ${response.status} ${response.statusText}`);
168
- }
169
- return await response.json();
170
- }, configuredTimeoutMs(options.timeoutMs), options.signal);
171
- return requireModels(commandCodeModelsFromApiResponse(body));
172
- }
173
- async function readCommandCodeModelsCache(cachePath) {
174
- const contents = await readFile(cachePath, "utf-8");
175
- const parsed = JSON.parse(contents);
176
- return commandCodeModelsFromCache(parsed);
177
- }
178
- async function writeCommandCodeModelsCache(cachePath, models) {
179
- await mkdir(dirname(cachePath), { recursive: true });
180
- const temporaryPath = `${cachePath}.${process.pid}.tmp`;
181
- try {
182
- await writeFile(temporaryPath, `${JSON.stringify({ version: MODEL_CACHE_VERSION, models }, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
183
- await rename(temporaryPath, cachePath);
184
- }
185
- finally {
186
- try {
187
- await rm(temporaryPath, { force: true });
188
- }
189
- catch {
190
- // Best-effort cleanup must not hide the original cache write error.
191
- }
192
- }
193
- }
194
- export async function loadCommandCodeModels(options) {
195
- const cachePath = options.cachePath;
196
- try {
197
- const models = await fetchCommandCodeModels(options);
198
- try {
199
- await writeCommandCodeModelsCache(cachePath, models);
200
- return { models, source: "live" };
201
- }
202
- catch (error) {
203
- return {
204
- models,
205
- source: "live",
206
- warning: `Loaded the live Command Code model catalog but could not update ${cachePath}: ${errorMessage(error)}`,
207
- };
208
- }
209
- }
210
- catch (liveError) {
211
- if (options.signal?.aborted)
212
- throw abortError(options.signal.reason ?? liveError);
213
- try {
214
- const models = await readCommandCodeModelsCache(cachePath);
215
- return {
216
- models,
217
- source: "cache",
218
- warning: `Could not refresh the Command Code model catalog (${errorMessage(liveError)}). Using the cached catalog from ${cachePath}.`,
219
- };
220
- }
221
- catch (cacheError) {
222
- return {
223
- models: [],
224
- source: "empty",
225
- warning: `Could not refresh the Command Code model catalog (${errorMessage(liveError)}), and no valid cached catalog is available at ${cachePath} (${errorMessage(cacheError)}). Command Code models will remain unavailable until /commandcode-refresh succeeds.`,
226
- };
227
- }
228
- }
229
- }