opencode-cmd-provider 1.3.0 → 1.5.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,5 +1,58 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0 - 2026-08-23
4
+
5
+ Feature: the `[CMD] ` display-name prefix for auto-registered models is now
6
+ configurable via `provider.commandcode.options.display_prefix`.
7
+
8
+ ### Features
9
+
10
+ - **Configurable display-name prefix** (issue #60): a string value replaces the
11
+ default `[CMD] ` prefix, and an empty string disables it entirely. The option
12
+ is read-only at resolution time — nothing is persisted into the user's config
13
+ (unlike `npm`/`name`/`env`/`options.baseURL`, which are filled when unset).
14
+ - Non-string values fall back to the default `[CMD] ` prefix.
15
+ - Declared model entries are never renamed — the prefix applies only to models
16
+ auto-registered from the bundled snapshot.
17
+
18
+ ### Chores
19
+
20
+ - New `tests/plugin-models.test.ts` cases: `resolveDisplayPrefix` fallback,
21
+ prefix override (`"CC/"`), empty string disabling the prefix (with non-name
22
+ metadata unaffected), and declared models keeping their names regardless of
23
+ the setting.
24
+
25
+ ## 1.4.0 - 2026-08-23
26
+
27
+ Feature: after a successful `/connect`, the credential is mirrored under
28
+ `command-code` in OpenCode's auth store and to `~/.commandcode/auth.json`
29
+ (official CLI layout), so ecosystem consumers such as OpenChamber's Usage
30
+ page find the key without manual setup.
31
+
32
+ ### Features
33
+
34
+ - **Credential mirroring** (issue #64): `/connect` now writes the credential
35
+ under `command-code` in OpenCode's auth store — refreshed on every
36
+ successful re-auth so the mirror stays in sync — and to
37
+ `~/.commandcode/auth.json` in the official CLI layout, which is only
38
+ written when it does not already hold a different credential, so an
39
+ official CLI login is never clobbered. Writes are atomic; new auth files
40
+ get owner-only permissions and existing file modes are preserved.
41
+ - Mirroring is best-effort: failures are swallowed and never fail `/connect`
42
+ itself, and `mirror: false` opts out (used by the oauth tests). Users who
43
+ authenticate via `COMMANDCODE_API_KEY` alone are unaffected.
44
+ - Existing users re-run `/connect` once (or copy the entry manually) to pick
45
+ the mirror up.
46
+
47
+ ### Chores
48
+
49
+ - New `tests/auth-mirror.test.ts` suite: preserves unrelated auth-store
50
+ entries, owner-only permissions on create, stale-entry refresh on re-auth,
51
+ no-clobber of a differing CLI credential, idempotent no-op, blank-key
52
+ guard, and the end-to-end `/connect` flow including `mirror: false` —
53
+ wired into `test:unit`.
54
+ - Docs: README notes the mirror locations and that mirroring is best-effort.
55
+
3
56
  ## 1.3.0 - 2026-08-23
4
57
 
5
58
  Feature: dual-transport Provider API — non-Go plans now use the documented
package/README.md CHANGED
@@ -49,6 +49,8 @@ Select **Command Code**, complete the browser flow, and pick a model with `/mode
49
49
 
50
50
  Run `/connect` in OpenCode and select **Command Code**. The browser flow stores the returned credential in OpenCode's auth store.
51
51
 
52
+ The credential is also mirrored under `command-code` in OpenCode's auth store and to `~/.commandcode/auth.json` (official CLI layout, only written when that file does not already hold a different credential). Ecosystem consumers such as OpenChamber's Usage page read those locations. Mirroring is best-effort; if it fails, `/connect` still succeeds.
53
+
52
54
  If automatic transfer from the browser fails, copy the API key shown by Command Code and export it as `COMMANDCODE_API_KEY` (see below).
53
55
 
54
56
  ### Environment variable
@@ -131,6 +133,24 @@ can still declare your own `provider.commandcode` entry; your declarations
131
133
  always win and the snapshot fills in only what's missing (`whitelist`/
132
134
  `blacklist` on a declared entry filter the auto-registered models too).
133
135
 
136
+ The `[CMD] ` display-name prefix is configurable through the declared
137
+ provider entry's options:
138
+
139
+ ```jsonc
140
+ {
141
+ "provider": {
142
+ "commandcode": {
143
+ "options": {
144
+ "display_prefix": "", // default "[CMD] "; empty string disables
145
+ },
146
+ },
147
+ },
148
+ }
149
+ ```
150
+
151
+ Declared model entries are never renamed; the prefix applies to
152
+ auto-registered models only.
153
+
134
154
  - Snapshot — `src/catalog/snapshot.ts` (model ids, names, context lengths) plus
135
155
  `src/catalog/facts.ts` (reasoning efforts, per-1M-token rates, input
136
156
  modalities). Regenerated from the live catalog via `npm run refresh:snapshot`.
@@ -1,7 +1,7 @@
1
1
  export declare const FACTS_SOURCE_URL = "https://unpkg.com/command-code@1.32.1/dist/bundled/command-code-knowledge/reference/models.md";
2
2
  export declare const MODALITIES_SOURCE_URL = "https://unpkg.com/command-code@1.32.1/dist/cli.mjs";
3
3
  export declare const FACTS_PACKAGE_VERSION = "1.32.1";
4
- export declare const FACTS_LAST_REFRESHED = "2026-08-22";
4
+ export declare const FACTS_LAST_REFRESHED = "2026-08-23";
5
5
  export declare const MODEL_EFFORTS: Readonly<Record<string, readonly string[]>>;
6
6
  export declare const MODEL_COSTS: Readonly<Record<string, {
7
7
  input: number;
@@ -7,7 +7,7 @@
7
7
  export const FACTS_SOURCE_URL = "https://unpkg.com/command-code@1.32.1/dist/bundled/command-code-knowledge/reference/models.md";
8
8
  export const MODALITIES_SOURCE_URL = "https://unpkg.com/command-code@1.32.1/dist/cli.mjs";
9
9
  export const FACTS_PACKAGE_VERSION = "1.32.1";
10
- export const FACTS_LAST_REFRESHED = "2026-08-22";
10
+ export const FACTS_LAST_REFRESHED = "2026-08-23";
11
11
  export const MODEL_EFFORTS = {
12
12
  "deepseek/deepseek-v4-pro": ["high", "max"],
13
13
  "deepseek/deepseek-v4-flash": ["high", "max"],
@@ -51,5 +51,5 @@ export interface ModelDeals {
51
51
  export declare const MODEL_DEALS: Readonly<Record<string, ModelDeals>>;
52
52
  export declare const PLAN_CATALOG: Readonly<Record<PlanId, PlanInfo>>;
53
53
  export declare const DEAL_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits";
54
- export declare const DEAL_LAST_REFRESHED = "2026-08-22";
54
+ export declare const DEAL_LAST_REFRESHED = "2026-08-23";
55
55
  export declare const DEAL_PACKAGE_VERSION = "docs";
@@ -74,5 +74,5 @@ export const PLAN_CATALOG = {
74
74
  provider: { price: 15, credits: 0, window5h: 0, windowWeek: 0, display: "Provider" },
75
75
  };
76
76
  export const DEAL_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits";
77
- export const DEAL_LAST_REFRESHED = "2026-08-22";
77
+ export const DEAL_LAST_REFRESHED = "2026-08-23";
78
78
  export const DEAL_PACKAGE_VERSION = "docs";
@@ -0,0 +1,11 @@
1
+ export interface MirrorOptions {
2
+ opencodeAuthFile?: string;
3
+ legacyAuthFile?: string;
4
+ }
5
+ export interface MirrorResult {
6
+ opencodeAuthUpdated: boolean;
7
+ legacyAuthUpdated: boolean;
8
+ }
9
+ export declare function defaultOpencodeAuthFile(): string;
10
+ export declare function defaultLegacyAuthFile(): string;
11
+ export declare function mirrorCredential(key: string, options?: MirrorOptions): MirrorResult;
@@ -0,0 +1,74 @@
1
+ // src/plugin/auth-mirror.ts — mirror /connect credentials for ecosystem consumers
2
+ //
3
+ // OpenChamber's command-code quota provider reads the API key from OpenCode's
4
+ // auth store under the key "command-code" (the provider id its own archived
5
+ // plugin used). OpenCode stores this plugin's credential under "commandcode",
6
+ // so after a successful /connect we mirror it under "command-code" as well.
7
+ // We also mirror to ~/.commandcode/auth.json in the official CLI layout,
8
+ // which resolveApiKey already reads as a legacy fallback.
9
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { basename, join } from "node:path";
12
+ function dataHome() {
13
+ return process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
14
+ }
15
+ export function defaultOpencodeAuthFile() {
16
+ return join(dataHome(), "opencode", "auth.json");
17
+ }
18
+ export function defaultLegacyAuthFile() {
19
+ return join(homedir(), ".commandcode", "auth.json");
20
+ }
21
+ function readJsonObject(path) {
22
+ try {
23
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
24
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
25
+ return parsed;
26
+ }
27
+ }
28
+ catch {
29
+ // Missing or malformed file: start from an empty document.
30
+ }
31
+ return {};
32
+ }
33
+ function writeJsonAtomic(path, value) {
34
+ const dir = join(path, "..");
35
+ mkdirSync(dir, { recursive: true });
36
+ const existingMode = existsSync(path) ? statSync(path).mode & 0o777 : 0o600;
37
+ const tmp = join(dir, `.${basename(path)}.${process.pid}.tmp`);
38
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: existingMode });
39
+ renameSync(tmp, path);
40
+ }
41
+ function isCredentialRecord(value) {
42
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
43
+ }
44
+ // OpenCode's auth store is owned by this plugin's ecosystem entry point, so a
45
+ // stale mirror there is refreshed on every successful /connect. The legacy
46
+ // file belongs to the official Command Code CLI; if it already holds a
47
+ // different credential we leave it alone rather than clobber another tool's
48
+ // login.
49
+ export function mirrorCredential(key, options = {}) {
50
+ const trimmed = key.trim();
51
+ if (!trimmed)
52
+ return { opencodeAuthUpdated: false, legacyAuthUpdated: false };
53
+ const entry = { type: "api", key: trimmed };
54
+ const opencodeAuthFile = options.opencodeAuthFile ?? defaultOpencodeAuthFile();
55
+ const opencodeDoc = readJsonObject(opencodeAuthFile);
56
+ const currentOpencode = opencodeDoc["command-code"];
57
+ let opencodeAuthUpdated = false;
58
+ if (!isCredentialRecord(currentOpencode) ||
59
+ currentOpencode.type !== "api" ||
60
+ currentOpencode.key !== trimmed) {
61
+ opencodeDoc["command-code"] = entry;
62
+ writeJsonAtomic(opencodeAuthFile, opencodeDoc);
63
+ opencodeAuthUpdated = true;
64
+ }
65
+ const legacyAuthFile = options.legacyAuthFile ?? defaultLegacyAuthFile();
66
+ const legacyDoc = readJsonObject(legacyAuthFile);
67
+ let legacyAuthUpdated = false;
68
+ if (!isCredentialRecord(legacyDoc["command-code"])) {
69
+ legacyDoc["command-code"] = entry;
70
+ writeJsonAtomic(legacyAuthFile, legacyDoc);
71
+ legacyAuthUpdated = true;
72
+ }
73
+ return { opencodeAuthUpdated, legacyAuthUpdated };
74
+ }
@@ -1,6 +1,9 @@
1
+ import { type MirrorOptions } from "./auth-mirror.js";
1
2
  import type { AuthOAuthResult } from "@opencode-ai/plugin";
2
3
  export interface RunAuthFlowOptions {
3
4
  startPort?: number;
4
5
  timeoutMs?: number;
6
+ /** Credential mirroring targets; pass `false` to disable mirroring. */
7
+ mirror?: MirrorOptions | false;
5
8
  }
6
9
  export declare function runAuthFlow(options?: RunAuthFlowOptions): Promise<AuthOAuthResult>;
@@ -3,8 +3,12 @@
3
3
  // Wraps the local callback server in the opencode AuthOAuthResult shape:
4
4
  // the studio URL is opened in the browser, the studio POSTs the API key to
5
5
  // the local /callback endpoint, and callback() resolves with the key.
6
+ // On success the credential is also mirrored under "command-code" (see
7
+ // auth-mirror.ts) so ecosystem consumers such as OpenChamber's quota
8
+ // provider can find it.
6
9
  import { randomBytes } from "node:crypto";
7
10
  import { startAuthServer } from "./auth-server.js";
11
+ import { mirrorCredential } from "./auth-mirror.js";
8
12
  const STUDIO_BASE_URL = "https://commandcode.ai";
9
13
  const DEFAULT_AUTH_TIMEOUT_MS = 15_000;
10
14
  function generateStateToken() {
@@ -27,6 +31,14 @@ export async function runAuthFlow(options = {}) {
27
31
  authServer.server.close();
28
32
  if (callback.state !== stateToken)
29
33
  return { type: "failed" };
34
+ if (options.mirror !== false) {
35
+ try {
36
+ mirrorCredential(callback.apiKey, options.mirror ?? {});
37
+ }
38
+ catch {
39
+ // Mirroring is best-effort; it must never fail the login itself.
40
+ }
41
+ }
30
42
  return { type: "success", key: callback.apiKey };
31
43
  }
32
44
  catch {
@@ -1,5 +1,13 @@
1
- import type { Config } from "@opencode-ai/sdk/v2";
1
+ import type { Config, ProviderConfig } from "@opencode-ai/sdk/v2";
2
2
  import type { CatalogModel } from "../catalog/snapshot.js";
3
+ export declare const DEFAULT_DISPLAY_PREFIX = "[CMD] ";
4
+ /**
5
+ * Resolves the auto-registration display-name prefix from the user-declared
6
+ * `provider.commandcode.options.display_prefix` key. A string value is used
7
+ * verbatim (empty string disables the prefix); anything else falls back to
8
+ * `[CMD] `. Read-only: never persisted into the user's config.
9
+ */
10
+ export declare function resolveDisplayPrefix(entry: ProviderConfig | undefined): string;
3
11
  export interface AutoRegisterOptions {
4
12
  npm: string;
5
13
  name: string;
@@ -24,6 +32,9 @@ export interface AutoRegisterOptions {
24
32
  * config key or by the entry's `id`);
25
33
  * - declared models are never modified, and declared models that left the
26
34
  * catalog stay usable.
35
+ * - `options.display_prefix` (string) overrides the default `[CMD] `
36
+ * display-name prefix for auto-registered models; an empty string disables
37
+ * the prefix.
27
38
  */
28
39
  export declare function autoRegister(config: Config, snapshot: readonly CatalogModel[], options: AutoRegisterOptions): Config;
29
40
  /**
@@ -2,6 +2,17 @@ import { MODEL_COSTS, ZERO_MODEL_COST } from "../provider/pricing.js";
2
2
  import { reasoningVariantsForModel, isReasoningModel } from "../provider/reasoning.js";
3
3
  import { inputModalitiesForModel } from "../provider/modalities.js";
4
4
  const DEFAULT_MAX_OUTPUT_TOKENS = 65_536;
5
+ export const DEFAULT_DISPLAY_PREFIX = "[CMD] ";
6
+ /**
7
+ * Resolves the auto-registration display-name prefix from the user-declared
8
+ * `provider.commandcode.options.display_prefix` key. A string value is used
9
+ * verbatim (empty string disables the prefix); anything else falls back to
10
+ * `[CMD] `. Read-only: never persisted into the user's config.
11
+ */
12
+ export function resolveDisplayPrefix(entry) {
13
+ const value = entry?.options?.display_prefix;
14
+ return typeof value === "string" ? value : DEFAULT_DISPLAY_PREFIX;
15
+ }
5
16
  /**
6
17
  * Registers the `commandcode` provider entry into opencode's config so every
7
18
  * snapshot model is available without any user declaration.
@@ -21,6 +32,9 @@ const DEFAULT_MAX_OUTPUT_TOKENS = 65_536;
21
32
  * config key or by the entry's `id`);
22
33
  * - declared models are never modified, and declared models that left the
23
34
  * catalog stay usable.
35
+ * - `options.display_prefix` (string) overrides the default `[CMD] `
36
+ * display-name prefix for auto-registered models; an empty string disables
37
+ * the prefix.
24
38
  */
25
39
  export function autoRegister(config, snapshot, options) {
26
40
  if (snapshot.length === 0)
@@ -33,13 +47,14 @@ export function autoRegister(config, snapshot, options) {
33
47
  entry.options ??= {};
34
48
  entry.options.baseURL ??= options.baseURL;
35
49
  entry.models ??= {};
50
+ const prefix = resolveDisplayPrefix(entry);
36
51
  const declaredById = new Set(Object.values(entry.models)
37
52
  .map((model) => model?.id)
38
53
  .filter((id) => typeof id === "string"));
39
54
  for (const model of snapshot) {
40
55
  if (entry.models[model.id] !== undefined || declaredById.has(model.id))
41
56
  continue;
42
- entry.models[model.id] = configModelFor(model);
57
+ entry.models[model.id] = configModelFor(model, prefix);
43
58
  }
44
59
  return config;
45
60
  }
@@ -73,15 +88,15 @@ export function augmentConfigCommandCodeModels(config) {
73
88
  }
74
89
  /**
75
90
  * Config-schema model entry (not the SDK `Model` shape) for a snapshot model:
76
- * `[CMD]`-prefixed display name, context/output limits with output capped at
91
+ * prefixed display name, context/output limits with output capped at
77
92
  * 65_536, and metadata enriched from the reasoning, modality, and pricing
78
93
  * tables.
79
94
  */
80
- function configModelFor(model) {
95
+ function configModelFor(model, prefix = DEFAULT_DISPLAY_PREFIX) {
81
96
  const variants = reasoningVariantsForModel(model.id);
82
97
  const costs = MODEL_COSTS[model.id] ?? ZERO_MODEL_COST;
83
98
  return {
84
- name: `[CMD] ${model.name}`,
99
+ name: `${prefix}${model.name}`,
85
100
  limit: {
86
101
  context: model.contextLength,
87
102
  output: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-cmd-provider",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Command Code provider + plugin for opencode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "refresh:deals": "node scripts/refresh-deals.mjs",
23
23
  "refresh": "npm run refresh:snapshot && npm run refresh:deals -- --fixtures",
24
24
  "test": "npm run typecheck && npm run test:unit && npm run test:integration && npm run test:contract && npm run format:check",
25
- "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/provider-codecs.test.ts && tsx tests/provider-transport.test.ts && tsx tests/provider-parity.test.ts && tsx tests/provider-upgrade-fallback.test.ts && tsx tests/provider-zdr.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/parse-facts.test.ts && tsx tests/parse-modalities.test.ts && tsx tests/catalog-metadata.test.ts && tsx tests/plugin-models.test.ts && tsx tests/vendor.test.ts && tsx tests/deals-enrichment.test.ts && tsx tests/deals-coverage.test.ts && tsx tests/plan-summary.test.ts && tsx tests/html-tables.test.ts && tsx tests/parse-docs.test.ts && tsx tests/refresh-deals.test.ts && tsx tests/tui-deals-panel.test.ts && tsx tests/plugin-install.test.ts",
25
+ "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/provider-codecs.test.ts && tsx tests/provider-transport.test.ts && tsx tests/provider-parity.test.ts && tsx tests/provider-upgrade-fallback.test.ts && tsx tests/provider-zdr.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/auth-mirror.test.ts && tsx tests/parse-facts.test.ts && tsx tests/parse-modalities.test.ts && tsx tests/catalog-metadata.test.ts && tsx tests/plugin-models.test.ts && tsx tests/vendor.test.ts && tsx tests/deals-enrichment.test.ts && tsx tests/deals-coverage.test.ts && tsx tests/plan-summary.test.ts && tsx tests/html-tables.test.ts && tsx tests/parse-docs.test.ts && tsx tests/refresh-deals.test.ts && tsx tests/tui-deals-panel.test.ts && tsx tests/plugin-install.test.ts",
26
26
  "test:integration": "tsx tests/integration-do-stream.test.ts && tsx tests/integration-do-generate.test.ts",
27
27
  "test:contract": "tsx tests/contract.test.ts",
28
28
  "test:e2e": "node tests/e2e-opencode.mjs",