@jeffreyjyz/opencode-command-code 0.3.19 → 0.4.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/dist/access.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { canonicalizeModelId, PLAN_RULES } from "./gating";
1
+ import { PLAN_RULES } from "./constants/gating";
2
+ import { canonicalizeModelId } from "./gating";
2
3
  export { canonicalizeModelId, PLAN_RULES };
3
4
  export type PlanLike = {
4
5
  planId: string;
package/dist/api.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export declare const API_BASE = "https://api.commandcode.ai";
2
1
  export declare function authHeaders(key: string): Record<string, string>;
3
2
  export declare function getJson(path: string, key: string, timeoutMs?: number): Promise<unknown>;
4
3
  export type Whoami = {
@@ -0,0 +1,6 @@
1
+ /** Fallback lookup paths for the cmduse CLI (CMDUSE_BIN wins when set). */
2
+ export declare const CMDUSE_BIN_CANDIDATES: readonly string[];
3
+ /** Fallback lookup paths for mpc, the per-model catalog scraper. */
4
+ export declare const MPC_BIN_CANDIDATES: readonly string[];
5
+ /** Fallback lookup paths for ocuse (OCUSE_BIN wins when set). */
6
+ export declare const OCUSE_BIN_CANDIDATES: readonly string[];
@@ -0,0 +1,12 @@
1
+ import type { spawn } from "node:child_process";
2
+ /** Default invocation: one-shot dashboard, no colors / no live redraw. */
3
+ export declare const DEFAULT_ARGS: readonly string[];
4
+ /** cmduse subcommands — when the arg starts with one, pass it through
5
+ * untouched; otherwise it is extra flags for the one-shot dashboard. */
6
+ export declare const SUBCOMMANDS: ReadonlySet<string>;
7
+ /** `detached` is load-bearing, not tidiness: it starts the child in its own
8
+ * session, so it has no controlling terminal. cmduse's snapshot() paints a
9
+ * "fetching usage…" spinner straight to /dev/tty — piping stdout/stderr does
10
+ * not stop it — which would corrupt the opencode TUI this process inherits its
11
+ * tty from. Detaching makes that open fail; stdout/stderr stay piped. */
12
+ export declare const SPAWN_OPTIONS: Parameters<typeof spawn>[2];
@@ -0,0 +1,6 @@
1
+ /** Command Code account/billing API. */
2
+ export declare const API_BASE = "https://api.commandcode.ai";
3
+ /** Provider (inference) API root; the lanes append their wire path. */
4
+ export declare const PROVIDER_BASE = "https://api.commandcode.ai/provider/v1";
5
+ /** Default provider base URL used when the host passes none. */
6
+ export declare const DEFAULT_BASE_URL = "https://api.commandcode.ai";
@@ -0,0 +1,22 @@
1
+ import type { Category } from "../gating";
2
+ /** model id -> category (models absent from this table have unknown category) */
3
+ export declare const MODEL_CATEGORIES: Record<string, Category>;
4
+ /** planId -> access rules; plan ids absent from this table get everything */
5
+ export declare const PLAN_RULES: Record<string, {
6
+ allowedCategories: Category[];
7
+ blockedModels: string[];
8
+ }>;
9
+ /** Models the API hard-blocks (403 MODEL_NOT_IN_PLAN) per plan, beyond the
10
+ * category rules. Empirically probed — the CLI bundle misses these because it
11
+ * tracks category via serving lane, not per-model plan entitlements. */
12
+ export declare const HARD_BLOCKED: Record<string, string[]>;
13
+ /** canonical known model ids (lowercase compare) */
14
+ export declare const KNOWN_MODELS: string[];
15
+ /** deprecated/aliased model id -> canonical id */
16
+ export declare const MODEL_ALIASES: Record<string, string>;
17
+ /** ISO timestamp the snapshot was extracted (absent on pre-metadata files). */
18
+ export declare const GATE_EXTRACTED_AT: string | undefined;
19
+ /** Command Code CLI version the snapshot was scraped from. */
20
+ export declare const GATE_CLI_VERSION: string | undefined;
21
+ /** Warn once a process when gating.json is older than this many days. */
22
+ export declare const GATE_STALE_DAYS = 30;
@@ -0,0 +1,29 @@
1
+ /** Every Command Code model shares these capabilities; hoisted so each record
2
+ * does not rebuild them. Vision (`attachment` + `input.image`) is applied per
3
+ * model from the generated catalog in the two model builders. */
4
+ export declare const MODEL_CAPABILITIES: {
5
+ temperature: boolean;
6
+ reasoning: boolean;
7
+ attachment: boolean;
8
+ toolcall: boolean;
9
+ input: {
10
+ text: boolean;
11
+ audio: boolean;
12
+ image: boolean;
13
+ video: boolean;
14
+ pdf: boolean;
15
+ };
16
+ output: {
17
+ text: boolean;
18
+ audio: boolean;
19
+ image: boolean;
20
+ video: boolean;
21
+ pdf: boolean;
22
+ };
23
+ };
24
+ /** Context window advertised when the listing API gives no length. */
25
+ export declare const DEFAULT_CONTEXT_LENGTH = 128000;
26
+ /** Max output tokens advertised per model. */
27
+ export declare const DEFAULT_OUTPUT_TOKENS = 32000;
28
+ /** Provider wire ceiling for a request's max tokens. */
29
+ export declare const DEFAULT_MAX_TOKENS = 64000;
@@ -0,0 +1,18 @@
1
+ /** Dir segment under an XDG base that holds our caches/diagnostics. */
2
+ export declare const CACHE_DIR = "command-code";
3
+ /** Live model list disk cache file (under CACHE_DIR). */
4
+ export declare const MODELS_CACHE_FILE = "models.json";
5
+ /** Startup timing log file (under CACHE_DIR). */
6
+ export declare const STARTUP_LOG_FILE = "startup.log";
7
+ /**
8
+ * mpc per-model catalog disk cache file (under CACHE_DIR), one per OpenCode Go
9
+ * plan: mpc bakes the selected plan's per-model allowance into the rows, so a
10
+ * cached catalog is only valid for the plan it was fetched under.
11
+ */
12
+ export declare function ccCatalogFile(plan: string): string;
13
+ /** opencode's own config/data dir segment. */
14
+ export declare const OPENCODE_DIR = "opencode";
15
+ /** opencode's message store file (under OPENCODE_DIR in the data root). */
16
+ export declare const OPENCODE_DB_FILE = "opencode.db";
17
+ /** Sidebar prefs file (under OPENCODE_DIR in the config root). */
18
+ export declare const PREFS_FILE = "command-code.json";
@@ -0,0 +1,28 @@
1
+ /** Provider-id prefix shared by both Command Code lanes and the plugin id. */
2
+ export declare const PROVIDER_ID_PREFIX = "command-code";
3
+ /** Plugin id (stable contract; the v2 `setup` definition and the TUI share it). */
4
+ export declare const PLUGIN_ID = "command-code";
5
+ /** Integration id backing both lanes' credentials (/connect + env). */
6
+ export declare const INTEGRATION_ID = "command-code";
7
+ /** Display name for the integration and the no-key warning. */
8
+ export declare const INTEGRATION_NAME = "Command Code";
9
+ /** OpenCode Go provider id (ocuse reads its store). */
10
+ export declare const GO_PROVIDER_ID = "opencode-go";
11
+ /** OpenCode Zen provider id and its `opencode-zen` alias prefix. */
12
+ export declare const ZEN_PROVIDER_ID = "opencode";
13
+ export declare const ZEN_PROVIDER_PREFIX = "opencode-zen";
14
+ /** TUI plugin id (stable contract; test/tui.test.ts pins it). */
15
+ export declare const TUI_ID = "command-code.tui";
16
+ /** The cmd_usage tool description, shared by the v1 and v2 halves. */
17
+ export declare const TOOL_DESCRIPTION = "Fetch live Command Code plan/usage: plan name, monthly credits, 5-hour & weekly windows, billing-period summary. Pass arg='plans' for the plan comparison table only, or extra cmduse flags (e.g. '--tz +05:30 daily').";
18
+ export type Lane = {
19
+ id: "command-code-anthropic" | "command-code-openai";
20
+ name: string;
21
+ /** v2 runtime package (first-party, distinct from the v1 `npm` value). */
22
+ pkg: string;
23
+ /** Wire field reasoning must round-trip through on the openai-compatible
24
+ * lane; v2 spells the old `interleaved: {field}` as `compatibility`. */
25
+ reasoningField?: string;
26
+ };
27
+ /** The Anthropic and OpenAI-compatible provider lanes this plugin owns. */
28
+ export declare const LANES: [Lane, Lane];
@@ -0,0 +1,33 @@
1
+ import type { Category } from "../gating";
2
+ import type { Prefs } from "../sidebar/prefs";
3
+ /**
4
+ * The sidebar is 42 columns wide with 2+2 padding, and our panel keeps one more
5
+ * column clear on the right, so a rendered row must stay within this. Rows that
6
+ * cannot fit are split across an indented continuation rather than wrapping in
7
+ * the host's renderer, which is what made the panel look broken.
8
+ */
9
+ export declare const ROW_WIDTH = 37;
10
+ /** Tier label per gating category. */
11
+ export declare const TIER_DISPLAY: Readonly<Record<Category, string>>;
12
+ /**
13
+ * OpenCode Go plans the panel can render: the same token rates, different
14
+ * per-model monthly limits. Which one a user is on is not discoverable (Go has
15
+ * no usage API and the store records no subscription), so it is a setting.
16
+ */
17
+ export declare const OC_PLANS: readonly ["go", "go-plus"];
18
+ export type OcPlan = (typeof OC_PLANS)[number];
19
+ /** Plan used when the user has not set one: behaviour is unchanged for them. */
20
+ export declare const OC_PLAN_DEFAULT: OcPlan;
21
+ /** Go's documented per-model window shares of the monthly allowance. */
22
+ export declare const GO_WINDOW_SHARE: {
23
+ readonly fiveHour: 0.2;
24
+ readonly weekly: 0.5;
25
+ readonly month: 1;
26
+ };
27
+ /** 5-hour window length in seconds. Mirrors cmduse_core::FIVE_HOUR_SECS. */
28
+ export declare const FIVE_HOUR_SECS: number;
29
+ /** Weekly window length in seconds. Mirrors cmduse_core::WEEKLY_SECS. */
30
+ export declare const WEEKLY_SECS: number;
31
+ /** Colour is OFF unless asked for; the OpenCode Go plan defaults to `go` (see
32
+ * sidebar/prefs.ts). */
33
+ export declare const PREF_DEFAULTS: Prefs;
@@ -0,0 +1,13 @@
1
+ /** Sidebar poll cadence: one cmduse/ocuse spawn plus the local store reads. */
2
+ export declare const POLL_MS = 5000;
3
+ /** How long one sidebar CLI spawn may run before it is killed. An outage can
4
+ * leave a fetch hanging through cmduse's retry ladder, and the poll interval
5
+ * would otherwise stack spawns on top of each other. */
6
+ export declare const CHILD_TIMEOUT_MS = 30000;
7
+ /** Process-lifetime cache for the live model list (src/models.ts). */
8
+ export declare const MODELS_CACHE_TTL_MS: number;
9
+ /** Disk cache for mpc's per-model catalog (src/sidebar/data.ts). An hour, not
10
+ * six: a running panel then picks up new models and promotions. */
11
+ export declare const CATALOG_CACHE_TTL_MS: number;
12
+ /** Re-read window for the sidebar colour prefs file (src/sidebar/prefs.ts). */
13
+ export declare const PREFS_CACHE_MS = 5000;
package/dist/gating.d.ts CHANGED
@@ -1,23 +1,5 @@
1
1
  export type Category = "opensource" | "premium";
2
- /** model id -> category (models absent from this table have unknown category) */
3
- export declare const MODEL_CATEGORIES: Record<string, Category>;
4
- /** planId -> access rules; plan ids absent from this table get everything */
5
- export declare const PLAN_RULES: Record<string, {
6
- allowedCategories: Category[];
7
- blockedModels: string[];
8
- }>;
9
- /** Models the API hard-blocks (403 MODEL_NOT_IN_PLAN) per plan, beyond the
10
- * category rules. Empirically probed — the CLI bundle misses these because it
11
- * tracks category via serving lane, not per-model plan entitlements. */
12
- export declare const HARD_BLOCKED: Record<string, string[]>;
13
- /** canonical known model ids (lowercase compare) */
14
- declare const KNOWN_MODELS: string[];
15
- export { KNOWN_MODELS };
16
- /** ISO timestamp the snapshot was extracted (absent on pre-metadata files). */
17
- export declare const GATE_EXTRACTED_AT: string | undefined;
18
- /** Command Code CLI version the snapshot was scraped from. */
19
- export declare const GATE_CLI_VERSION: string | undefined;
20
- /** Warn once per process when gating.json is older than 30 days. */
2
+ /** Warn once per process when gating.json is older than GATE_STALE_DAYS. */
21
3
  export declare function warnIfGatingStale(): void;
22
4
  export declare function canonicalizeModelId(model: string): string;
23
5
  /** Bare model id with any provider qualifier stripped: everything after the
package/dist/index.d.ts CHANGED
@@ -2,9 +2,9 @@ import type { Plugin as PluginV1 } from "@opencode-ai/plugin";
2
2
  export declare const CommandCodePlugin: PluginV1;
3
3
  export { createCommandCode } from "./provider";
4
4
  declare const _default: {
5
- /** v1 entrypoint (opencode >=1.18.29 object entrypoints). */
6
- server: PluginV1;
7
5
  id: string;
8
6
  setup: (context: import("@opencode/plugin/promise/plugin").Context) => Promise<import("@opencode/plugin/promise/plugin").Cleanup | void> | import("@opencode/plugin/promise/plugin").Cleanup | void;
7
+ /** v1 entrypoint (opencode >=1.18.29 object entrypoints). */
8
+ server: PluginV1;
9
9
  };
10
10
  export default _default;
package/dist/index.js CHANGED
@@ -2414,7 +2414,7 @@ function superRefine(fn) {
2414
2414
  // package.json
2415
2415
  var package_default = {
2416
2416
  name: "@jeffreyjyz/opencode-command-code",
2417
- version: "0.3.19",
2417
+ version: "0.4.0",
2418
2418
  description: "Command Code provider + usage dashboard for opencode: two providers (Anthropic wire for Claude, OpenAI wire for everything else) against api.commandcode.ai, live model list, plan gating, /cmd-usage. Requires the cmduse CLI for usage rendering.",
2419
2419
  type: "module",
2420
2420
  main: "dist/index.js",
@@ -2438,7 +2438,7 @@ var package_default = {
2438
2438
  },
2439
2439
  devDependencies: {
2440
2440
  "@types/bun": "latest",
2441
- typescript: "^5",
2441
+ typescript: "^7",
2442
2442
  "@opencode-ai/plugin": "*",
2443
2443
  "@opencode/plugin": "^2.0.11",
2444
2444
  "@ai-sdk/provider": "3.0.8"
@@ -2461,8 +2461,12 @@ var package_default = {
2461
2461
  }
2462
2462
  };
2463
2463
 
2464
- // src/api.ts
2464
+ // src/constants/endpoints.ts
2465
2465
  var API_BASE = "https://api.commandcode.ai";
2466
+ var PROVIDER_BASE = `${API_BASE}/provider/v1`;
2467
+ var DEFAULT_BASE_URL = API_BASE;
2468
+
2469
+ // src/api.ts
2466
2470
  function authHeaders(key) {
2467
2471
  return { Authorization: `Bearer ${key}`, Accept: "application/json" };
2468
2472
  }
@@ -2935,7 +2939,12 @@ var MODEL_CATALOG = {
2935
2939
  name: "Qwen 3.7 Flash",
2936
2940
  context: 1e6,
2937
2941
  efforts: null,
2938
- cost: { input: 0.03, output: 0.13, cacheRead: 0.006, cacheWrite: 0.038 },
2942
+ cost: {
2943
+ input: 0.03,
2944
+ output: 0.13,
2945
+ cacheRead: 0.006,
2946
+ cacheWrite: 0.038
2947
+ },
2939
2948
  modalities: ["text", "image"],
2940
2949
  minPlan: "Go"
2941
2950
  },
@@ -3207,16 +3216,22 @@ function reasoningVariants(id) {
3207
3216
 
3208
3217
  // src/cli.ts
3209
3218
  import { spawn } from "child_process";
3210
- function cmduseCandidates(env = process.env) {
3211
- const candidates = [
3212
- env.CMDUSE_BIN,
3213
- "cmduse",
3214
- "/opt/homebrew/bin/cmduse",
3215
- "/usr/local/bin/cmduse"
3216
- ];
3217
- return candidates.filter((bin) => Boolean(bin));
3218
- }
3219
- var CANDIDATES = cmduseCandidates();
3219
+
3220
+ // src/constants/binaries.ts
3221
+ import { homedir } from "os";
3222
+ import { join } from "path";
3223
+ var CMDUSE_BIN_CANDIDATES = [
3224
+ "cmduse",
3225
+ "/opt/homebrew/bin/cmduse",
3226
+ "/usr/local/bin/cmduse"
3227
+ ];
3228
+ var MPC_BIN_CANDIDATES = [
3229
+ "mpc",
3230
+ join(homedir(), ".bun/bin/mpc"),
3231
+ "/opt/homebrew/bin/mpc"
3232
+ ];
3233
+
3234
+ // src/constants/cli.ts
3220
3235
  var DEFAULT_ARGS = ["-1", "--plain"];
3221
3236
  var SUBCOMMANDS = new Set([
3222
3237
  "plans",
@@ -3228,6 +3243,13 @@ var SUBCOMMANDS = new Set([
3228
3243
  "statusline",
3229
3244
  "config"
3230
3245
  ]);
3246
+
3247
+ // src/cli.ts
3248
+ function cmduseCandidates(env = process.env) {
3249
+ const candidates = [env.CMDUSE_BIN, ...CMDUSE_BIN_CANDIDATES];
3250
+ return candidates.filter((bin) => Boolean(bin));
3251
+ }
3252
+ var CANDIDATES = cmduseCandidates();
3231
3253
  function splitCliArgs(input) {
3232
3254
  const out = [];
3233
3255
  let cur = "";
@@ -3269,7 +3291,7 @@ function splitCliArgs(input) {
3269
3291
  function buildCmduseArgs(arg) {
3270
3292
  const tokens = splitCliArgs(arg);
3271
3293
  if (!tokens.length)
3272
- return DEFAULT_ARGS;
3294
+ return [...DEFAULT_ARGS];
3273
3295
  if (tokens.some((t) => SUBCOMMANDS.has(t)))
3274
3296
  return tokens;
3275
3297
  return [...DEFAULT_ARGS, ...tokens];
@@ -3328,9 +3350,48 @@ async function runCmduse(arg, opts = {}) {
3328
3350
  throw new Error(`cmduse binary not found (install: brew install JeffreyJYZ/tap/cmduse)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, { cause: last });
3329
3351
  }
3330
3352
 
3353
+ // src/constants/models.ts
3354
+ var MODEL_CAPABILITIES = {
3355
+ temperature: true,
3356
+ reasoning: true,
3357
+ attachment: false,
3358
+ toolcall: true,
3359
+ input: { text: true, audio: false, image: false, video: false, pdf: false },
3360
+ output: {
3361
+ text: true,
3362
+ audio: false,
3363
+ image: false,
3364
+ video: false,
3365
+ pdf: false
3366
+ }
3367
+ };
3368
+ var DEFAULT_CONTEXT_LENGTH = 128000;
3369
+ var DEFAULT_OUTPUT_TOKENS = 32000;
3370
+ var DEFAULT_MAX_TOKENS = 64000;
3371
+
3372
+ // src/constants/providers.ts
3373
+ var PROVIDER_ID_PREFIX = "command-code";
3374
+ var PLUGIN_ID = PROVIDER_ID_PREFIX;
3375
+ var INTEGRATION_ID = PROVIDER_ID_PREFIX;
3376
+ var INTEGRATION_NAME = "Command Code";
3377
+ var TOOL_DESCRIPTION = "Fetch live Command Code plan/usage: plan name, monthly credits, 5-hour & weekly windows, billing-period summary. Pass arg='plans' for the plan comparison table only, or extra cmduse flags (e.g. '--tz +05:30 daily').";
3378
+ var LANES = [
3379
+ {
3380
+ id: "command-code-anthropic",
3381
+ name: "Command Code (Anthropic)",
3382
+ pkg: "@opencode/ai/providers/anthropic"
3383
+ },
3384
+ {
3385
+ id: "command-code-openai",
3386
+ name: "Command Code (OpenAI)",
3387
+ pkg: "@opencode/ai/providers/openai-compatible",
3388
+ reasoningField: "reasoning_content"
3389
+ }
3390
+ ];
3391
+
3331
3392
  // src/key.ts
3332
3393
  import { readFile } from "fs/promises";
3333
- import { homedir } from "os";
3394
+ import { homedir as homedir2 } from "os";
3334
3395
  async function resolveKey(getAuth) {
3335
3396
  const env = process.env.CMD_API_KEY;
3336
3397
  if (env)
@@ -3341,7 +3402,7 @@ async function resolveKey(getAuth) {
3341
3402
  return auth.key;
3342
3403
  } catch {}
3343
3404
  try {
3344
- const text = await readFile(`${homedir()}/.commandcode/auth.json`, "utf8");
3405
+ const text = await readFile(`${homedir2()}/.commandcode/auth.json`, "utf8");
3345
3406
  const v = JSON.parse(text);
3346
3407
  if (typeof v.apiKey === "string" && v.apiKey)
3347
3408
  return v.apiKey;
@@ -3670,7 +3731,7 @@ var gating_default = {
3670
3731
  }
3671
3732
  };
3672
3733
 
3673
- // src/gating.ts
3734
+ // src/constants/gating.ts
3674
3735
  var MODEL_CATEGORIES = gating_default.categories;
3675
3736
  var PLAN_RULES = gating_default.plans;
3676
3737
  var HARD_BLOCKED = gating_default.hardBlocked;
@@ -3679,6 +3740,8 @@ var MODEL_ALIASES = gating_default.aliases;
3679
3740
  var GATE_EXTRACTED_AT = gating_default.extractedAt;
3680
3741
  var GATE_CLI_VERSION = gating_default.cliVersion;
3681
3742
  var GATE_STALE_DAYS = 30;
3743
+
3744
+ // src/gating.ts
3682
3745
  var staleWarned = false;
3683
3746
  function warnIfGatingStale() {
3684
3747
  if (staleWarned || !GATE_EXTRACTED_AT)
@@ -3745,8 +3808,11 @@ function siblingCategory(model) {
3745
3808
  return best ? MODEL_CATEGORIES[best] : undefined;
3746
3809
  }
3747
3810
 
3811
+ // src/constants/timing.ts
3812
+ var MODELS_CACHE_TTL_MS = 5 * 60 * 1000;
3813
+ var CATALOG_CACHE_TTL_MS = 60 * 60 * 1000;
3814
+
3748
3815
  // src/models.ts
3749
- var CACHE_TTL_MS = 5 * 60 * 1000;
3750
3816
  var cache = null;
3751
3817
  function isClaude(id) {
3752
3818
  const bare = id.includes("/") ? id.slice(id.lastIndexOf("/") + 1) : id;
@@ -3762,8 +3828,8 @@ function splitModels(models) {
3762
3828
  async function loadModels(key) {
3763
3829
  warnIfGatingStale();
3764
3830
  let models;
3765
- const fresh = cache && cache.key === key && Date.now() - cache.at < CACHE_TTL_MS;
3766
- if (fresh) {
3831
+ const fresh = cache && cache.key === key && Date.now() - cache.at < MODELS_CACHE_TTL_MS;
3832
+ if (fresh && cache) {
3767
3833
  models = cache.models;
3768
3834
  } else {
3769
3835
  try {
@@ -3801,11 +3867,18 @@ async function loadModels(key) {
3801
3867
 
3802
3868
  // src/modelsCache.ts
3803
3869
  import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
3804
- import { homedir as homedir2 } from "os";
3805
- import { join } from "path";
3870
+ import { homedir as homedir3 } from "os";
3871
+ import { join as join2 } from "path";
3872
+
3873
+ // src/constants/paths.ts
3874
+ var CACHE_DIR = "command-code";
3875
+ var MODELS_CACHE_FILE = "models.json";
3876
+ var STARTUP_LOG_FILE = "startup.log";
3877
+
3878
+ // src/modelsCache.ts
3806
3879
  function cachePath() {
3807
- const base = process.env.XDG_CACHE_HOME ?? join(homedir2(), ".cache");
3808
- return join(base, "command-code", "models.json");
3880
+ const base = process.env.XDG_CACHE_HOME ?? join2(homedir3(), ".cache");
3881
+ return join2(base, CACHE_DIR, MODELS_CACHE_FILE);
3809
3882
  }
3810
3883
  async function readModelsCache() {
3811
3884
  try {
@@ -3820,7 +3893,7 @@ async function readModelsCache() {
3820
3893
  async function writeModelsCache(cache) {
3821
3894
  try {
3822
3895
  const path = cachePath();
3823
- await mkdir(join(path, ".."), { recursive: true });
3896
+ await mkdir(join2(path, ".."), { recursive: true });
3824
3897
  await writeFile(path, JSON.stringify(cache));
3825
3898
  } catch {}
3826
3899
  }
@@ -3914,7 +3987,9 @@ function anthropicReducer() {
3914
3987
  const open = (index, kind) => {
3915
3988
  const id = `${kind === "text" ? "text" : "reasoning"}-${index}`;
3916
3989
  blocks.set(index, { type: kind, id });
3917
- return [{ type: kind === "text" ? "text-start" : "reasoning-start", id }];
3990
+ return [
3991
+ { type: kind === "text" ? "text-start" : "reasoning-start", id }
3992
+ ];
3918
3993
  };
3919
3994
  return {
3920
3995
  feed(event) {
@@ -3936,7 +4011,13 @@ function anthropicReducer() {
3936
4011
  if (blockType === "tool_use") {
3937
4012
  const id = text(block.id) || `tool-${index}`;
3938
4013
  blocks.set(index, { type: "tool_use", id });
3939
- return [{ type: "tool-input-start", id, toolName: text(block.name) }];
4014
+ return [
4015
+ {
4016
+ type: "tool-input-start",
4017
+ id,
4018
+ toolName: text(block.name)
4019
+ }
4020
+ ];
3940
4021
  }
3941
4022
  blocks.set(index, { type: "other", id: `block-${index}` });
3942
4023
  return [];
@@ -3949,17 +4030,29 @@ function anthropicReducer() {
3949
4030
  if (textDelta) {
3950
4031
  const parts = block?.type === "text" ? [] : open(index, "text");
3951
4032
  const id = blocks.get(index)?.id ?? `text-${index}`;
3952
- return [...parts, { type: "text-delta", id, delta: textDelta }];
4033
+ return [
4034
+ ...parts,
4035
+ { type: "text-delta", id, delta: textDelta }
4036
+ ];
3953
4037
  }
3954
4038
  const thinking = text(delta.thinking);
3955
4039
  if (thinking) {
3956
4040
  const parts = block?.type === "thinking" ? [] : open(index, "thinking");
3957
4041
  const id = blocks.get(index)?.id ?? `reasoning-${index}`;
3958
- return [...parts, { type: "reasoning-delta", id, delta: thinking }];
4042
+ return [
4043
+ ...parts,
4044
+ { type: "reasoning-delta", id, delta: thinking }
4045
+ ];
3959
4046
  }
3960
4047
  const partial = text(delta.partial_json);
3961
4048
  if (partial && block?.type === "tool_use") {
3962
- return [{ type: "tool-input-delta", id: block.id, delta: partial }];
4049
+ return [
4050
+ {
4051
+ type: "tool-input-delta",
4052
+ id: block.id,
4053
+ delta: partial
4054
+ }
4055
+ ];
3963
4056
  }
3964
4057
  return [];
3965
4058
  }
@@ -4176,7 +4269,11 @@ function openaiReducer() {
4176
4269
  return parts;
4177
4270
  },
4178
4271
  close() {
4179
- const parts = [...closeReasoning(), ...closeText(), ...flushTools()];
4272
+ const parts = [
4273
+ ...closeReasoning(),
4274
+ ...closeText(),
4275
+ ...flushTools()
4276
+ ];
4180
4277
  if (pending && !finished) {
4181
4278
  finished = true;
4182
4279
  parts.push({
@@ -4191,7 +4288,6 @@ function openaiReducer() {
4191
4288
  }
4192
4289
 
4193
4290
  // src/provider/wire.ts
4194
- var DEFAULT_MAX_TOKENS = 64000;
4195
4291
  function capTokens(value) {
4196
4292
  const n = typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_MAX_TOKENS;
4197
4293
  return Math.min(n, DEFAULT_MAX_TOKENS);
@@ -4345,7 +4441,11 @@ function anthropicBody(model, prompt, options) {
4345
4441
  };
4346
4442
  if (system)
4347
4443
  body.system = [
4348
- { type: "text", text: system, cache_control: { type: "ephemeral" } }
4444
+ {
4445
+ type: "text",
4446
+ text: system,
4447
+ cache_control: { type: "ephemeral" }
4448
+ }
4349
4449
  ];
4350
4450
  if (options.tools?.length) {
4351
4451
  body.tools = options.tools.map((t) => ({
@@ -4443,7 +4543,6 @@ function requestBody(lane, model, prompt, options) {
4443
4543
  }
4444
4544
 
4445
4545
  // src/provider/index.ts
4446
- var DEFAULT_BASE_URL = "https://api.commandcode.ai";
4447
4546
  function laneOf(modelId) {
4448
4547
  const bare = modelId.includes("/") ? modelId.slice(modelId.lastIndexOf("/") + 1) : modelId;
4449
4548
  return bare.toLowerCase().startsWith("claude") ? "anthropic" : "openai";
@@ -4507,10 +4606,11 @@ function createCommandCode(options = {}, images = () => false) {
4507
4606
  const detail = await response.text().catch(() => "");
4508
4607
  throw new Error(`command-code ${lane} ${response.status}: ${detail.slice(0, 300)}`);
4509
4608
  }
4609
+ const responseBody = response.body;
4510
4610
  const reducer = lane === "anthropic" ? anthropicReducer() : openaiReducer();
4511
4611
  const stream = new ReadableStream({
4512
4612
  async start(controller) {
4513
- const reader = response.body.getReader();
4613
+ const reader = responseBody.getReader();
4514
4614
  const decoder = new TextDecoder;
4515
4615
  const emit = (parts) => {
4516
4616
  for (const part of parts)
@@ -4546,7 +4646,12 @@ function createCommandCode(options = {}, images = () => false) {
4546
4646
  raw: "unknown"
4547
4647
  };
4548
4648
  let usage = {
4549
- inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
4649
+ inputTokens: {
4650
+ total: 0,
4651
+ noCache: 0,
4652
+ cacheRead: 0,
4653
+ cacheWrite: 0
4654
+ },
4550
4655
  outputTokens: { total: 0, text: 0, reasoning: 0 }
4551
4656
  };
4552
4657
  const textOf = {};
@@ -4559,7 +4664,10 @@ function createCommandCode(options = {}, images = () => false) {
4559
4664
  if (part.type === "text-delta")
4560
4665
  textOf[String(part.id)] = (textOf[String(part.id)] ?? "") + String(part.delta);
4561
4666
  else if (part.type === "reasoning-delta") {
4562
- content.push({ type: "reasoning", text: String(part.delta) });
4667
+ content.push({
4668
+ type: "reasoning",
4669
+ text: String(part.delta)
4670
+ });
4563
4671
  } else if (part.type === "tool-input-start") {
4564
4672
  toolsById.set(String(part.id), {
4565
4673
  name: String(part.toolName),
@@ -4597,43 +4705,25 @@ function createCommandCode(options = {}, images = () => false) {
4597
4705
 
4598
4706
  // src/startupLog.ts
4599
4707
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
4600
- import { homedir as homedir3 } from "os";
4601
- import { join as join2 } from "path";
4708
+ import { homedir as homedir4 } from "os";
4709
+ import { join as join3 } from "path";
4602
4710
  function setupTimingLine(t) {
4603
4711
  return `setup: key=${t.key}ms register=${t.register}ms connection=${t.connection}ms refresh=${t.refresh}ms models=${t.models}`;
4604
4712
  }
4605
4713
  function startupLogPath() {
4606
- const base = process.env.XDG_CACHE_HOME ?? join2(homedir3(), ".cache");
4607
- return join2(base, "command-code", "startup.log");
4714
+ const base = process.env.XDG_CACHE_HOME ?? join3(homedir4(), ".cache");
4715
+ return join3(base, CACHE_DIR, STARTUP_LOG_FILE);
4608
4716
  }
4609
4717
  async function writeStartupLine(line) {
4610
4718
  try {
4611
4719
  const path = startupLogPath();
4612
- await mkdir2(join2(path, ".."), { recursive: true });
4720
+ await mkdir2(join3(path, ".."), { recursive: true });
4613
4721
  await appendFile(path, `[${new Date().toISOString()}] ${line}
4614
4722
  `);
4615
4723
  } catch {}
4616
4724
  }
4617
4725
 
4618
4726
  // src/v2.ts
4619
- var PLUGIN_ID = "command-code";
4620
- var PROVIDER_BASE = "https://api.commandcode.ai/provider/v1";
4621
- var INTEGRATION_ID = "command-code";
4622
- var INTEGRATION_NAME = "Command Code";
4623
- var LANES = [
4624
- {
4625
- id: "command-code-anthropic",
4626
- name: "Command Code (Anthropic)",
4627
- pkg: "@opencode/ai/providers/anthropic"
4628
- },
4629
- {
4630
- id: "command-code-openai",
4631
- name: "Command Code (OpenAI)",
4632
- pkg: "@opencode/ai/providers/openai-compatible",
4633
- reasoningField: "reasoning_content"
4634
- }
4635
- ];
4636
- var TOOL_DESCRIPTION = "Fetch live Command Code plan/usage: plan name, monthly credits, 5-hour & weekly windows, billing-period summary. Pass arg='plans' for the plan comparison table only, or extra cmduse flags (e.g. '--tz +05:30 daily').";
4637
4727
  function costEntry(id) {
4638
4728
  const cost = modelCost(id);
4639
4729
  return cost ? [
@@ -4662,7 +4752,10 @@ function toV2Model(m, lane) {
4662
4752
  cost: costEntry(m.id),
4663
4753
  status: "active",
4664
4754
  enabled: true,
4665
- limit: { context: m.contextLength || 128000, output: 32000 },
4755
+ limit: {
4756
+ context: m.contextLength || DEFAULT_CONTEXT_LENGTH,
4757
+ output: DEFAULT_OUTPUT_TOKENS
4758
+ },
4666
4759
  ...lane.reasoningField ? { compatibility: { reasoningField: lane.reasoningField } } : {}
4667
4760
  };
4668
4761
  return info;
@@ -4759,7 +4852,10 @@ var commandCodeV2 = {
4759
4852
  if (provider.package === "")
4760
4853
  provider.package = lane.pkg;
4761
4854
  provider.activation = localKey ? "enabled" : "auto";
4762
- provider.settings = { ...provider.settings, baseURL: PROVIDER_BASE };
4855
+ provider.settings = {
4856
+ ...provider.settings,
4857
+ baseURL: PROVIDER_BASE
4858
+ };
4763
4859
  if (localKey && provider.settings.apiKey === undefined) {
4764
4860
  provider.settings.apiKey = localKey;
4765
4861
  }
@@ -4880,14 +4976,6 @@ var PROVIDER_NPM = `${package_default.name}@${package_default.version}`;
4880
4976
  function tool(input) {
4881
4977
  return input;
4882
4978
  }
4883
- var MODEL_CAPABILITIES = {
4884
- temperature: true,
4885
- reasoning: true,
4886
- attachment: false,
4887
- toolcall: true,
4888
- input: { text: true, audio: false, image: false, video: false, pdf: false },
4889
- output: { text: true, audio: false, image: false, video: false, pdf: false }
4890
- };
4891
4979
  function costFor(id) {
4892
4980
  const cost = modelCost(id);
4893
4981
  return {
@@ -4909,11 +4997,17 @@ function toModelDefs(models, providerID, npm, interleaved = false) {
4909
4997
  interleaved,
4910
4998
  ...supportsImage(m.id) ? {
4911
4999
  attachment: true,
4912
- input: { ...MODEL_CAPABILITIES.input, image: true }
5000
+ input: {
5001
+ ...MODEL_CAPABILITIES.input,
5002
+ image: true
5003
+ }
4913
5004
  } : {}
4914
5005
  },
4915
5006
  cost: costFor(m.id),
4916
- limit: { context: m.contextLength || 128000, output: 32000 },
5007
+ limit: {
5008
+ context: m.contextLength || DEFAULT_CONTEXT_LENGTH,
5009
+ output: DEFAULT_OUTPUT_TOKENS
5010
+ },
4917
5011
  status: "active",
4918
5012
  options: {},
4919
5013
  headers: {},
@@ -5026,7 +5120,9 @@ var CommandCodePlugin = async (_input) => {
5026
5120
  },
5027
5121
  async execute(args) {
5028
5122
  const key = await resolveKey();
5029
- return runCmduse(args.arg ?? "", { env: { CMD_API_KEY: key } });
5123
+ return runCmduse(args.arg ?? "", {
5124
+ env: { CMD_API_KEY: key }
5125
+ });
5030
5126
  }
5031
5127
  })
5032
5128
  }
@@ -1,6 +1,5 @@
1
1
  import type { LanguageModelV3 } from "@ai-sdk/provider";
2
2
  import { type WireLane } from "./wire";
3
- export declare const DEFAULT_BASE_URL = "https://api.commandcode.ai";
4
3
  export interface CommandCodeProviderOptions {
5
4
  /** Bearer key; the v2 host passes the resolved connection credential here. */
6
5
  apiKey?: string;
@@ -1,12 +1,6 @@
1
- import { spawn } from "node:child_process";
1
+ import { type OcPlan } from "../constants/sidebar";
2
2
  import { type ModelMeta, type Usage } from "./rows";
3
3
  import { type ZenUsage } from "./zen";
4
- /** `detached` is load-bearing, not tidiness: it starts the child in its own
5
- * session, so it has no controlling terminal. cmduse's snapshot() paints a
6
- * "fetching usage…" spinner straight to /dev/tty — piping stdout/stderr does
7
- * not stop it — which would corrupt the opencode TUI this process inherits its
8
- * tty from. Detaching makes that open fail; stdout/stderr stay piped. */
9
- export declare const SPAWN_OPTIONS: Parameters<typeof spawn>[2];
10
4
  /** Parse `cmduse -1 --json` into the usage snapshot. */
11
5
  export declare function parseUsageJson(text: string): Usage;
12
6
  /**
@@ -23,8 +17,17 @@ export declare function requireSnapshot(usage: Usage): Usage;
23
17
  * kind of session. Rows that carry only one side are kept.
24
18
  */
25
19
  export declare function parseMpcJson(text: string): Map<string, ModelMeta>;
26
- /** mpc's catalog, cached on disk: it scrapes live docs over the network. */
27
- export declare function loadMeta(): Promise<Map<string, ModelMeta>>;
20
+ /**
21
+ * mpc's args for the per-model catalog. `--shape off` is the recursion break
22
+ * (mpc's default `--shape auto` spawns reqshape, which spawns mpc back), never
23
+ * dropped; `--oc-plan` selects which Go plan's per-model allowance is baked in.
24
+ */
25
+ export declare function metaArgs(plan: OcPlan): string[];
26
+ /** ocuse's args: `--plan` selects the Go plan's per-model limits. */
27
+ export declare function zenArgs(plan: OcPlan): string[];
28
+ /** mpc's catalog, cached on disk: it scrapes live docs over the network. The
29
+ * cache is per plan, since mpc bakes that plan's per-model allowance into it. */
30
+ export declare function loadMeta(plan?: OcPlan): Promise<Map<string, ModelMeta>>;
28
31
  /** One usage snapshot from cmduse. Throws when cmduse is missing, fails, or
29
32
  * returns the errored defaults its JSON carries when the account API is
30
33
  * unreachable (see `requireSnapshot`). */
@@ -34,4 +37,4 @@ export declare function loadUsage(): Promise<Usage>;
34
37
  * (those providers have no usage API). Throws when ocuse is missing or fails,
35
38
  * and the caller then omits the panel.
36
39
  */
37
- export declare function loadZen(): Promise<ZenUsage>;
40
+ export declare function loadZen(plan?: OcPlan): Promise<ZenUsage>;
@@ -1,10 +1,12 @@
1
+ import { type OcPlan } from "../constants/sidebar";
1
2
  export interface Prefs {
2
3
  colors: boolean;
4
+ /** OpenCode Go plan the sidebar renders per-model limits for. */
5
+ ocPlan: OcPlan;
3
6
  }
4
- export declare const DEFAULTS: Prefs;
5
7
  /** Preferences live beside opencode's own config, XDG layout honoured. */
6
8
  export declare function prefsPath(env?: NodeJS.ProcessEnv): string;
7
- /** Only a literal `true` enables colour, so a typo cannot turn the panel on. */
9
+ /** Only a literal `true` enables colour; an unknown plan falls back to `go`. */
8
10
  export declare function parsePrefs(raw: unknown): Prefs;
9
- /** Env override, then the file, then the default. Cached for `CACHE_MS`. */
11
+ /** Env override, then the file, then the default. Cached for `PREFS_CACHE_MS`. */
10
12
  export declare function loadPrefs(env?: NodeJS.ProcessEnv, now?: number): Prefs;
@@ -21,11 +21,11 @@ export type SidebarRow = [
21
21
  export declare function pctTone(pct: number): Tone;
22
22
  /**
23
23
  * The sidebar is 42 columns wide with 2+2 padding, and our panel keeps one more
24
- * column clear on the right, so a rendered row must stay within this. Rows that
25
- * cannot fit are split across an indented continuation (`""` label) rather than
26
- * wrapping in the host's renderer, which is what made the panel look broken.
24
+ * column clear on the right, so a rendered row must stay within ROW_WIDTH.
25
+ * Rows that cannot fit are split across an indented continuation (`""` label)
26
+ * rather than wrapping in the host's renderer, which is what made the panel
27
+ * look broken.
27
28
  */
28
- export declare const ROW_WIDTH = 37;
29
29
  /** A horizontal rule between blocks. */
30
30
  export declare function separator(): SidebarRow;
31
31
  /** Truncate to `width`, ellipsis included. */
@@ -43,6 +43,8 @@ export interface ModelMeta {
43
43
  /** Cheapest plan that serves the model, from the generated catalog. */
44
44
  minPlan?: string | null;
45
45
  allowance?: number;
46
+ /** Unbounded on the CommandCode side: `allowance` is 0 by convention. */
47
+ free?: boolean;
46
48
  rates?: {
47
49
  input: number;
48
50
  output: number;
@@ -67,6 +69,8 @@ export interface ModelMeta {
67
69
  provider?: string;
68
70
  plan?: string;
69
71
  allowance?: number;
72
+ /** Unbounded on the OpenCode Go side: its allowance is 0 by convention. */
73
+ free?: boolean;
70
74
  rates?: {
71
75
  input: number;
72
76
  output: number;
@@ -112,6 +116,13 @@ export interface Usage {
112
116
  }
113
117
  /** Money at credit scale: cents, but "$60" not "$60.00". */
114
118
  export declare function money(value: number): string;
119
+ /**
120
+ * A monthly allowance as it appears on the panel. A free/unbounded model has no
121
+ * budget to divide — mpc reports `free: true` and allowance 0 — so it reads `∞`
122
+ * rather than the misleading `$0/mo` (0 is not a budget). Both sides call this,
123
+ * so a free CommandCode model and a free OpenCode Go one render alike.
124
+ */
125
+ export declare function allowanceLabel(limit: number, free?: boolean): string;
115
126
  /** Rate scale needs more precision than credit scale ($0.0036). */
116
127
  export declare function rate(value: number): string;
117
128
  export declare function count(value: number): string;
@@ -1,7 +1,3 @@
1
- /** 5-hour window length in seconds. Mirrors cmduse_core::FIVE_HOUR_SECS. */
2
- export declare const FIVE_HOUR_SECS: number;
3
- /** Weekly window length in seconds. Mirrors cmduse_core::WEEKLY_SECS. */
4
- export declare const WEEKLY_SECS: number;
5
1
  /**
6
2
  * Percentage of a rolling window that has elapsed, or undefined when the
7
3
  * window has not started (or has no reset time). `nowSecs` is whole seconds —
@@ -1,10 +1,4 @@
1
1
  import { type ModelMeta, type ModelUsage, type SidebarRow } from "./rows";
2
- /** Go's documented per-model window shares of the monthly allowance. */
3
- export declare const GO_WINDOW_SHARE: {
4
- readonly fiveHour: 0.2;
5
- readonly weekly: 0.5;
6
- readonly month: 1;
7
- };
8
2
  export interface ZenTotals {
9
3
  requests: number;
10
4
  cost: number;
package/dist/tui.d.ts CHANGED
@@ -33,8 +33,8 @@ interface V1Api {
33
33
  export declare const tui: (api: V1Api) => Promise<void>;
34
34
  export declare const commandCodeTui: TuiPluginNs.Definition;
35
35
  declare const _default: {
36
- tui: (api: V1Api) => Promise<void>;
37
36
  id: string;
38
37
  setup: (context: TuiPluginNs.Context) => Promise<TuiPluginNs.Cleanup | void> | TuiPluginNs.Cleanup | void;
38
+ tui: typeof tui;
39
39
  };
40
40
  export default _default;
package/dist/tui.js CHANGED
@@ -4,13 +4,28 @@ import { createComponent as _$createComponent2 } from "@opentui/solid";
4
4
 
5
5
  // src/cli.ts
6
6
  import { spawn } from "child_process";
7
+
8
+ // src/constants/binaries.ts
9
+ import { homedir } from "os";
10
+ import { join } from "path";
11
+ var CMDUSE_BIN_CANDIDATES = ["cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
12
+ var MPC_BIN_CANDIDATES = ["mpc", join(homedir(), ".bun/bin/mpc"), "/opt/homebrew/bin/mpc"];
13
+ var OCUSE_BIN_CANDIDATES = ["ocuse", "/opt/homebrew/bin/ocuse", "/usr/local/bin/ocuse"];
14
+
15
+ // src/constants/cli.ts
16
+ var DEFAULT_ARGS = ["-1", "--plain"];
17
+ var SUBCOMMANDS = new Set(["plans", "models", "daily", "hourly", "model", "session", "statusline", "config"]);
18
+ var SPAWN_OPTIONS = {
19
+ stdio: ["ignore", "pipe", "pipe"],
20
+ detached: true
21
+ };
22
+
23
+ // src/cli.ts
7
24
  function cmduseCandidates(env = process.env) {
8
- const candidates = [env.CMDUSE_BIN, "cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
25
+ const candidates = [env.CMDUSE_BIN, ...CMDUSE_BIN_CANDIDATES];
9
26
  return candidates.filter((bin) => Boolean(bin));
10
27
  }
11
28
  var CANDIDATES = cmduseCandidates();
12
- var DEFAULT_ARGS = ["-1", "--plain"];
13
- var SUBCOMMANDS = new Set(["plans", "models", "daily", "hourly", "model", "session", "statusline", "config"]);
14
29
  function splitCliArgs(input) {
15
30
  const out = [];
16
31
  let cur = "";
@@ -52,7 +67,7 @@ function splitCliArgs(input) {
52
67
  function buildCmduseArgs(arg) {
53
68
  const tokens = splitCliArgs(arg);
54
69
  if (!tokens.length)
55
- return DEFAULT_ARGS;
70
+ return [...DEFAULT_ARGS];
56
71
  if (tokens.some((t) => SUBCOMMANDS.has(t)))
57
72
  return tokens;
58
73
  return [...DEFAULT_ARGS, ...tokens];
@@ -118,6 +133,13 @@ async function runCmduse(arg, opts = {}) {
118
133
  });
119
134
  }
120
135
 
136
+ // src/constants/providers.ts
137
+ var PROVIDER_ID_PREFIX = "command-code";
138
+ var GO_PROVIDER_ID = "opencode-go";
139
+ var ZEN_PROVIDER_ID = "opencode";
140
+ var ZEN_PROVIDER_PREFIX = "opencode-zen";
141
+ var TUI_ID = "command-code.tui";
142
+
121
143
  // src/sidebar/panel.tsx
122
144
  import { createComponent as _$createComponent } from "@opentui/solid";
123
145
  import { insertNode as _$insertNode } from "@opentui/solid";
@@ -451,7 +473,7 @@ var gating_default = {
451
473
  }
452
474
  };
453
475
 
454
- // src/gating.ts
476
+ // src/constants/gating.ts
455
477
  var MODEL_CATEGORIES = gating_default.categories;
456
478
  var PLAN_RULES = gating_default.plans;
457
479
  var HARD_BLOCKED = gating_default.hardBlocked;
@@ -459,6 +481,28 @@ var KNOWN_MODELS = gating_default.knownModels;
459
481
  var MODEL_ALIASES = gating_default.aliases;
460
482
  var GATE_EXTRACTED_AT = gating_default.extractedAt;
461
483
  var GATE_CLI_VERSION = gating_default.cliVersion;
484
+
485
+ // src/constants/sidebar.ts
486
+ var ROW_WIDTH = 37;
487
+ var TIER_DISPLAY = {
488
+ opensource: "open source",
489
+ premium: "premium"
490
+ };
491
+ var OC_PLANS = ["go", "go-plus"];
492
+ var OC_PLAN_DEFAULT = "go";
493
+ var GO_WINDOW_SHARE = {
494
+ fiveHour: 0.2,
495
+ weekly: 0.5,
496
+ month: 1
497
+ };
498
+ var FIVE_HOUR_SECS = 5 * 3600;
499
+ var WEEKLY_SECS = 7 * 86400;
500
+ var PREF_DEFAULTS = {
501
+ colors: false,
502
+ ocPlan: OC_PLAN_DEFAULT
503
+ };
504
+
505
+ // src/gating.ts
462
506
  function findKnown(s) {
463
507
  const k = s.toLowerCase();
464
508
  return KNOWN_MODELS.find((m) => m.toLowerCase() === k);
@@ -475,8 +519,6 @@ function canonicalizeModelId(model) {
475
519
  }
476
520
 
477
521
  // src/sidebar/windows.ts
478
- var FIVE_HOUR_SECS = 5 * 3600;
479
- var WEEKLY_SECS = 7 * 86400;
480
522
  function elapsedPct(resetAtMs, durSecs, nowSecs) {
481
523
  if (typeof resetAtMs !== "number" || !Number.isFinite(resetAtMs))
482
524
  return;
@@ -526,7 +568,6 @@ function pctTone(pct) {
526
568
  return "warn";
527
569
  return "ok";
528
570
  }
529
- var ROW_WIDTH = 37;
530
571
  function separator() {
531
572
  return ["\u2500".repeat(ROW_WIDTH), "", "muted"];
532
573
  }
@@ -541,14 +582,13 @@ function parts(row) {
541
582
  return [` ${row[1]}`, ""];
542
583
  return [row[1] ? `${row[0]}: ` : row[0], row[1]];
543
584
  }
544
- var TIER_DISPLAY = {
545
- opensource: "open source",
546
- premium: "premium"
547
- };
548
585
  function money(value) {
549
586
  const rounded = Math.round(value * 100) / 100;
550
587
  return `$${Number.isInteger(rounded) ? rounded : rounded.toFixed(2)}`;
551
588
  }
589
+ function allowanceLabel(limit, free) {
590
+ return free === true || limit <= 0 ? "\u221E" : `${money(limit)}/mo`;
591
+ }
552
592
  function rate(value) {
553
593
  if (value === 0)
554
594
  return "$0";
@@ -656,7 +696,7 @@ function modelRows(meta, usage, session, fallbackName) {
656
696
  else if (meta.minPlan)
657
697
  rows.push(["Min plan", meta.minPlan, "base"]);
658
698
  if (typeof meta.allowance === "number")
659
- rows.push(["Allowance", `${money(meta.allowance)}/mo`, "base"]);
699
+ rows.push(["Allowance", allowanceLabel(meta.allowance, meta.free), "base"]);
660
700
  if (meta.deal) {
661
701
  const ends = meta.deal.ends?.match(/([A-Z][a-z]{2})[a-z]*\s+(\d{1,2})/);
662
702
  rows.push(["Deal", ends ? `${meta.deal.badge} \xB7 ends ${ends[1]} ${ends[2]}` : meta.deal.badge, "ok"]);
@@ -790,41 +830,71 @@ function Panel(props) {
790
830
 
791
831
  // src/sidebar/prefs.ts
792
832
  import { readFileSync } from "fs";
793
- import { homedir } from "os";
794
- import { join } from "path";
795
- var DEFAULTS = {
796
- colors: false
797
- };
798
- var CACHE_MS = 5000;
833
+ import { homedir as homedir2 } from "os";
834
+ import { join as join2 } from "path";
835
+
836
+ // src/constants/paths.ts
837
+ var CACHE_DIR = "command-code";
838
+ function ccCatalogFile(plan) {
839
+ return `cc-catalog-${plan}.json`;
840
+ }
841
+ var OPENCODE_DIR = "opencode";
842
+ var OPENCODE_DB_FILE = "opencode.db";
843
+ var PREFS_FILE = "command-code.json";
844
+
845
+ // src/constants/timing.ts
846
+ var POLL_MS = 5000;
847
+ var CHILD_TIMEOUT_MS = 30000;
848
+ var MODELS_CACHE_TTL_MS = 5 * 60 * 1000;
849
+ var CATALOG_CACHE_TTL_MS = 60 * 60 * 1000;
850
+ var PREFS_CACHE_MS = 5000;
851
+
852
+ // src/sidebar/prefs.ts
799
853
  var cached;
800
854
  function prefsPath(env = process.env) {
801
- const base = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
802
- return join(base, "opencode", "command-code.json");
855
+ const base = env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config");
856
+ return join2(base, OPENCODE_DIR, PREFS_FILE);
857
+ }
858
+ function planFrom(value, source) {
859
+ if (value === undefined)
860
+ return;
861
+ if (typeof value === "string" && OC_PLANS.includes(value)) {
862
+ return value;
863
+ }
864
+ console.warn(`[command-code] ignoring unknown OpenCode plan ${JSON.stringify(value)} (${source}); using "${OC_PLAN_DEFAULT}"`);
865
+ return;
803
866
  }
804
867
  function parsePrefs(raw) {
805
- const value = raw?.colors;
868
+ const obj = raw;
806
869
  return {
807
- colors: value === true
870
+ colors: obj?.colors === true,
871
+ ocPlan: planFrom(obj?.ocPlan, "file") ?? OC_PLAN_DEFAULT
808
872
  };
809
873
  }
810
874
  function loadPrefs(env = process.env, now = Date.now()) {
811
- if (cached && now - cached.at < CACHE_MS)
875
+ if (cached && now - cached.at < PREFS_CACHE_MS)
812
876
  return cached.prefs;
877
+ let prefs = PREF_DEFAULTS;
878
+ try {
879
+ prefs = parsePrefs(JSON.parse(readFileSync(prefsPath(env), "utf8")));
880
+ } catch {}
813
881
  const flag = env.CMD_COLORS;
814
- let prefs = DEFAULTS;
815
882
  if (flag === "1" || flag === "true")
816
883
  prefs = {
884
+ ...prefs,
817
885
  colors: true
818
886
  };
819
887
  else if (flag === "0" || flag === "false")
820
888
  prefs = {
889
+ ...prefs,
821
890
  colors: false
822
891
  };
823
- else {
824
- try {
825
- prefs = parsePrefs(JSON.parse(readFileSync(prefsPath(env), "utf8")));
826
- } catch {}
827
- }
892
+ const envPlan = planFrom(env.CMD_OC_PLAN, "CMD_OC_PLAN");
893
+ if (envPlan !== undefined)
894
+ prefs = {
895
+ ...prefs,
896
+ ocPlan: envPlan
897
+ };
828
898
  cached = {
829
899
  at: now,
830
900
  prefs
@@ -1950,15 +2020,10 @@ function minPlan(id) {
1950
2020
  // src/sidebar/data.ts
1951
2021
  import { spawn as spawn2 } from "child_process";
1952
2022
  import { mkdir, readFile, writeFile } from "fs/promises";
1953
- import { homedir as homedir2 } from "os";
1954
- import { dirname, join as join2 } from "path";
2023
+ import { homedir as homedir3 } from "os";
2024
+ import { dirname, join as join3 } from "path";
1955
2025
 
1956
2026
  // src/sidebar/zen.ts
1957
- var GO_WINDOW_SHARE = {
1958
- fiveHour: 0.2,
1959
- weekly: 0.5,
1960
- month: 1
1961
- };
1962
2027
  function totals(value) {
1963
2028
  const v = value ?? {};
1964
2029
  const num = (key) => typeof v[key] === "number" ? v[key] : 0;
@@ -2035,7 +2100,7 @@ function zenRows(usage, meta, session, fallbackName) {
2035
2100
  if (!meta)
2036
2101
  return rows;
2037
2102
  if (typeof limit === "number") {
2038
- rows.push(["Allowance", `${money(limit)}/mo`, "base"]);
2103
+ rows.push(["Allowance", allowanceLabel(limit, oc?.free), "base"]);
2039
2104
  }
2040
2105
  if (oc?.rates) {
2041
2106
  const cache = [`cache read ${rate(oc.rates.cacheRead)}`];
@@ -2055,14 +2120,8 @@ function zenRows(usage, meta, session, fallbackName) {
2055
2120
 
2056
2121
  // src/sidebar/data.ts
2057
2122
  var CMDUSE = cmduseCandidates();
2058
- var MPC = ["mpc", join2(homedir2(), ".bun/bin/mpc"), "/opt/homebrew/bin/mpc"];
2059
- var OCUSE = [process.env.OCUSE_BIN, "ocuse", "/opt/homebrew/bin/ocuse", "/usr/local/bin/ocuse"].filter((bin) => Boolean(bin));
2060
- var CACHE_TTL_MS = 60 * 60 * 1000;
2061
- var SPAWN_OPTIONS = {
2062
- stdio: ["ignore", "pipe", "pipe"],
2063
- detached: true
2064
- };
2065
- var CHILD_TIMEOUT_MS = 30000;
2123
+ var MPC = MPC_BIN_CANDIDATES;
2124
+ var OCUSE = [process.env.OCUSE_BIN, ...OCUSE_BIN_CANDIDATES].filter((bin) => Boolean(bin));
2066
2125
  function run(bin, args) {
2067
2126
  return new Promise((resolve, reject) => {
2068
2127
  const child = spawn2(bin, args, SPAWN_OPTIONS);
@@ -2147,6 +2206,7 @@ function parseMpcJson(text) {
2147
2206
  key: row.key ?? modelKey(row.name),
2148
2207
  name: row.name,
2149
2208
  allowance: cc?.allowance,
2209
+ free: cc?.free === true ? true : undefined,
2150
2210
  rates: cc?.pricing,
2151
2211
  intelligence: cc?.ability ?? undefined,
2152
2212
  tps: cc?.tps ?? undefined,
@@ -2158,6 +2218,7 @@ function parseMpcJson(text) {
2158
2218
  provider: oc.provider,
2159
2219
  plan: oc.plan,
2160
2220
  allowance: oc.allowance,
2221
+ free: oc.free === true ? true : undefined,
2161
2222
  rates: oc.pricing,
2162
2223
  ability: oc.ability ?? undefined,
2163
2224
  tps: oc.tps ?? undefined
@@ -2168,20 +2229,26 @@ function parseMpcJson(text) {
2168
2229
  }
2169
2230
  return meta;
2170
2231
  }
2171
- function cachePath() {
2172
- const base = process.env.XDG_CACHE_HOME ?? join2(homedir2(), ".cache");
2173
- return join2(base, "command-code", "cc-catalog.json");
2232
+ function cachePath(plan) {
2233
+ const base = process.env.XDG_CACHE_HOME ?? join3(homedir3(), ".cache");
2234
+ return join3(base, CACHE_DIR, ccCatalogFile(plan));
2174
2235
  }
2175
- async function loadMeta() {
2236
+ function metaArgs(plan) {
2237
+ return ["--json", "--shape", "off", "--oc-plan", plan];
2238
+ }
2239
+ function zenArgs(plan) {
2240
+ return ["-1", "--json", "--plan", plan];
2241
+ }
2242
+ async function loadMeta(plan = OC_PLAN_DEFAULT) {
2176
2243
  try {
2177
- const cached = JSON.parse(await readFile(cachePath(), "utf8"));
2178
- if (cached.fetchedAt && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
2244
+ const cached = JSON.parse(await readFile(cachePath(plan), "utf8"));
2245
+ if (cached.fetchedAt && Date.now() - cached.fetchedAt < CATALOG_CACHE_TTL_MS) {
2179
2246
  return new Map(cached.rows ?? []);
2180
2247
  }
2181
2248
  } catch {}
2182
- const meta = parseMpcJson(await runFirst(MPC, ["--json"]));
2249
+ const meta = parseMpcJson(await runFirst(MPC, metaArgs(plan)));
2183
2250
  try {
2184
- const path = cachePath();
2251
+ const path = cachePath(plan);
2185
2252
  await mkdir(dirname(path), {
2186
2253
  recursive: true
2187
2254
  });
@@ -2195,19 +2262,19 @@ async function loadMeta() {
2195
2262
  async function loadUsage() {
2196
2263
  return requireSnapshot(parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"])));
2197
2264
  }
2198
- async function loadZen() {
2199
- return parseZenJson(await runFirst(OCUSE, ["-1", "--json"]));
2265
+ async function loadZen(plan = OC_PLAN_DEFAULT) {
2266
+ return parseZenJson(await runFirst(OCUSE, zenArgs(plan)));
2200
2267
  }
2201
2268
 
2202
2269
  // src/sidebar/usageDb.ts
2203
2270
  import { Database } from "bun:sqlite";
2204
- import { homedir as homedir3 } from "os";
2205
- import { join as join3 } from "path";
2271
+ import { homedir as homedir4 } from "os";
2272
+ import { join as join4 } from "path";
2206
2273
  function usageDbPath() {
2207
2274
  if (process.env.OPENCODE_DB)
2208
2275
  return process.env.OPENCODE_DB;
2209
- const base = process.env.XDG_DATA_HOME ?? join3(homedir3(), ".local", "share");
2210
- return join3(base, "opencode", "opencode.db");
2276
+ const base = process.env.XDG_DATA_HOME ?? join4(homedir4(), ".local", "share");
2277
+ return join4(base, OPENCODE_DIR, OPENCODE_DB_FILE);
2211
2278
  }
2212
2279
  function usage(value) {
2213
2280
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
@@ -2333,15 +2400,14 @@ function loadSessionUsage(sessionID, path = usageDbPath()) {
2333
2400
  }
2334
2401
 
2335
2402
  // src/sidebar/useRows.ts
2336
- var POLL_MS = 5000;
2337
2403
  function sessionKind(providerID) {
2338
2404
  if (!providerID)
2339
2405
  return "unknown";
2340
- if (providerID.startsWith("command-code"))
2406
+ if (providerID.startsWith(PROVIDER_ID_PREFIX))
2341
2407
  return "ours";
2342
- if (providerID.startsWith("opencode-go"))
2408
+ if (providerID.startsWith(GO_PROVIDER_ID))
2343
2409
  return "go";
2344
- if (providerID === "opencode" || providerID.startsWith("opencode-zen"))
2410
+ if (providerID === ZEN_PROVIDER_ID || providerID.startsWith(ZEN_PROVIDER_PREFIX))
2345
2411
  return "zen";
2346
2412
  return "other";
2347
2413
  }
@@ -2396,7 +2462,7 @@ function useRows(activeModelId, providerID, sessionID) {
2396
2462
  const [sessionUsage, setSessionUsage] = createSignal();
2397
2463
  const [wasOurs, setWasOurs] = createSignal(false);
2398
2464
  const refreshMeta = () => {
2399
- loadMeta().then((next) => {
2465
+ loadMeta(loadPrefs().ocPlan).then((next) => {
2400
2466
  rememberMeta(next);
2401
2467
  if (next.size > 0)
2402
2468
  setMeta(next);
@@ -2431,7 +2497,7 @@ function useRows(activeModelId, providerID, sessionID) {
2431
2497
  setUsage(usageRows(snapshot));
2432
2498
  refreshModelUsage(activeModelId());
2433
2499
  } else if (kind === "go" || kind === "zen") {
2434
- setZen(await loadZen());
2500
+ setZen(await loadZen(loadPrefs().ocPlan));
2435
2501
  }
2436
2502
  } catch {} finally {
2437
2503
  inFlight = false;
@@ -2481,7 +2547,6 @@ function useRows(activeModelId, providerID, sessionID) {
2481
2547
  }
2482
2548
 
2483
2549
  // src/tui.tsx
2484
- var ID = "command-code.tui";
2485
2550
  var themed = (colors) => loadPrefs().colors ? colors : mono(colors);
2486
2551
  function PanelV1(props) {
2487
2552
  const current = () => props.api.state.session.get(props.sessionID)?.model;
@@ -2520,7 +2585,7 @@ function PanelV2(props) {
2520
2585
  });
2521
2586
  }
2522
2587
  var commandCodeTui = {
2523
- id: ID,
2588
+ id: TUI_ID,
2524
2589
  setup(ctx) {
2525
2590
  ctx.ui.slot({
2526
2591
  append: "sidebar.content",
package/dist/v2.d.ts CHANGED
@@ -1,21 +1,6 @@
1
1
  import type { Plugin as PluginNs } from "@opencode/plugin";
2
+ import { type Lane } from "./constants/providers";
2
3
  import { type CmdModel } from "./models";
3
- export declare const PLUGIN_ID = "command-code";
4
- export declare const PROVIDER_BASE = "https://api.commandcode.ai/provider/v1";
5
- /** Shared integration backing both lanes' credentials (/connect + env). */
6
- export declare const INTEGRATION_ID = "command-code";
7
- export declare const INTEGRATION_NAME = "Command Code";
8
- type Lane = {
9
- id: "command-code-anthropic" | "command-code-openai";
10
- name: string;
11
- /** v2 runtime package (first-party, distinct from the v1 `npm` value). */
12
- pkg: string;
13
- /** Wire field reasoning must round-trip through on the openai-compatible
14
- * lane; v2 spells the old `interleaved: {field}` as `compatibility`. */
15
- reasoningField?: string;
16
- };
17
- export type { Lane };
18
- export declare const TOOL_DESCRIPTION = "Fetch live Command Code plan/usage: plan name, monthly credits, 5-hour & weekly windows, billing-period summary. Pass arg='plans' for the plan comparison table only, or extra cmduse flags (e.g. '--tz +05:30 daily').";
19
4
  /** API/known model → v2 Model.Info. Capabilities, variants and $/1M cost all
20
5
  * come from the generated catalog (the listing API publishes none of them);
21
6
  * only the open lane needs the reasoning round-trip field. */
@@ -51,3 +36,4 @@ export declare function credentialKey(ctx: CredentialContext): Promise<string |
51
36
  * own rewrite list (@opentui/*, solid-js).
52
37
  */
53
38
  export declare const commandCodeV2: PluginNs.Plugin;
39
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.3.19",
3
+ "version": "0.4.0",
4
4
  "description": "Command Code provider + usage dashboard for opencode: two providers (Anthropic wire for Claude, OpenAI wire for everything else) against api.commandcode.ai, live model list, plan gating, /cmd-usage. Requires the cmduse CLI for usage rendering.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "latest",
27
- "typescript": "^5",
27
+ "typescript": "^7",
28
28
  "@opencode-ai/plugin": "*",
29
29
  "@opencode/plugin": "^2.0.11",
30
30
  "@ai-sdk/provider": "3.0.8"