@brainervirus/opencode-commandcode 0.6.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/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@brainervirus/opencode-commandcode",
3
+ "version": "0.6.0",
4
+ "description": "Command Code API provider for opencode — use Claude, GPT, Gemini, DeepSeek, Qwen, Kimi, GLM, MiniMax, and Step models via Command Code",
5
+ "keywords": [
6
+ "ai",
7
+ "claude",
8
+ "commandcode",
9
+ "deepseek",
10
+ "gemini",
11
+ "glm",
12
+ "gpt",
13
+ "kimi",
14
+ "llm",
15
+ "minimax",
16
+ "opencode",
17
+ "opencode-plugin",
18
+ "opencode-provider",
19
+ "qwen"
20
+ ],
21
+ "homepage": "https://github.com/BrainerVirus/opencode-commandcode#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/BrainerVirus/opencode-commandcode/issues"
24
+ },
25
+ "license": "MIT",
26
+ "author": "Brent Weatherall",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/BrainerVirus/opencode-commandcode.git"
30
+ },
31
+ "files": [
32
+ "index.ts",
33
+ "plugin.ts",
34
+ "models.json",
35
+ "manifest.json",
36
+ "_version.txt",
37
+ "README.md",
38
+ "CHANGELOG.md",
39
+ "src/"
40
+ ],
41
+ "type": "module",
42
+ "main": "index.ts",
43
+ "types": "index.ts",
44
+ "exports": {
45
+ ".": {
46
+ "import": "./index.ts"
47
+ },
48
+ "./server": {
49
+ "import": "./plugin.ts"
50
+ }
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "scripts": {
56
+ "lint": "oxlint --deny-warnings src scripts plugin.ts index.ts tests",
57
+ "lint:fix": "oxlint --fix src scripts plugin.ts index.ts tests",
58
+ "format": "oxfmt src scripts plugin.ts index.ts tests package.json tsconfig.json release.config.cjs",
59
+ "format:check": "oxfmt --check src scripts plugin.ts index.ts tests package.json tsconfig.json release.config.cjs",
60
+ "check": "bun run lint && bun run format:check && bun test tests/unit/ && bun run typecheck",
61
+ "test": "bun test tests/unit/",
62
+ "test:integration": "bun test tests/integration/",
63
+ "test:all": "bun test",
64
+ "typecheck": "tsc --noEmit",
65
+ "verify:release-candidate": "bun run scripts/verify-release-candidate.ts",
66
+ "generate-readme": "bun run scripts/generate-readme.ts",
67
+ "sync": "bun run scripts/sync-models.ts",
68
+ "sync:global": "bun run scripts/sync-models.ts --update-global",
69
+ "catalog:ci": "bun run scripts/catalog-sync-ci.ts"
70
+ },
71
+ "devDependencies": {
72
+ "@semantic-release/commit-analyzer": "13.0.1",
73
+ "@semantic-release/github": "12.0.9",
74
+ "@semantic-release/npm": "13.1.5",
75
+ "@semantic-release/release-notes-generator": "14.1.1",
76
+ "@types/node": "^25.9.1",
77
+ "ai": "6.0.191",
78
+ "oxfmt": "0.63.0",
79
+ "oxlint": "1.78.0",
80
+ "semantic-release": "25.0.9",
81
+ "typescript": "^6.0.3"
82
+ },
83
+ "peerDependencies": {
84
+ "ai": ">=6.0.0"
85
+ },
86
+ "engines": {
87
+ "bun": ">=1.0.0"
88
+ }
89
+ }
package/plugin.ts ADDED
@@ -0,0 +1,197 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import {
6
+ generateOpencodeModels,
7
+ loadCatalogFromLocalCommandCode,
8
+ type ModelEntry,
9
+ } from "./src/catalog.js";
10
+ import {
11
+ readCatalogCache,
12
+ writeCatalogCache,
13
+ writeStartupSummary,
14
+ pluginStateDir,
15
+ } from "./src/startup.js";
16
+ import type { CatalogManifest } from "./src/manifest.js";
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const MODELS_PATH = join(__dirname, "models.json");
20
+ const VERSION_PATH = join(__dirname, "_version.txt");
21
+ const MANIFEST_PATH = join(__dirname, "manifest.json");
22
+
23
+ interface PluginFileConfig {
24
+ disableModelSync?: boolean;
25
+ commandCodePackagePath?: string;
26
+ debugStartupLogs?: boolean;
27
+ }
28
+
29
+ function loadPluginConfig(): PluginFileConfig {
30
+ const dir = join(homedir(), ".config", "opencode");
31
+ const configPath = [
32
+ join(dir, "opencode-commandcode.json"),
33
+ join(dir, "commandcode-go-opencode-provider.json"),
34
+ ].find((p) => existsSync(p));
35
+ if (!configPath) return {};
36
+ try {
37
+ return JSON.parse(readFileSync(configPath, "utf-8"));
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+
43
+ export type { ModelEntry };
44
+
45
+ function loadBundledModels(): ModelEntry[] | null {
46
+ try {
47
+ return JSON.parse(readFileSync(MODELS_PATH, "utf-8"));
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function readBundledManifest(): CatalogManifest | null {
54
+ if (!existsSync(MANIFEST_PATH)) return null;
55
+ try {
56
+ return JSON.parse(readFileSync(MANIFEST_PATH, "utf-8")) as CatalogManifest;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ function readBundledVersion(): string | null {
63
+ const manifest = readBundledManifest();
64
+ if (manifest?.commandCodeVersion) return manifest.commandCodeVersion;
65
+ if (!existsSync(VERSION_PATH)) return null;
66
+ try {
67
+ const parts = readFileSync(VERSION_PATH, "utf-8").split("\n");
68
+ const first = parts[0]?.trim();
69
+ return first || null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export default async function commandcodePlugin() {
76
+ return {
77
+ config: async (config: Record<string, unknown>) => {
78
+ if (!(config as Record<string, unknown>).provider) {
79
+ (config as Record<string, unknown>).provider = { commandcode: {} };
80
+ }
81
+ const cc = (
82
+ (config as Record<string, unknown>).provider as Record<string, Record<string, unknown>>
83
+ )?.commandcode as Record<string, unknown> | undefined;
84
+ if (!cc) return;
85
+
86
+ const pluginCfg = loadPluginConfig();
87
+ const debug = pluginCfg.debugStartupLogs === true;
88
+ const override =
89
+ pluginCfg.commandCodePackagePath?.trim() ||
90
+ process.env.COMMANDCODE_PACKAGE_PATH?.trim() ||
91
+ "";
92
+
93
+ if (!cc.npm) cc.npm = "commandcode-go-opencode-provider";
94
+ if (!cc.name) cc.name = "Command Code";
95
+ if (!cc.env) cc.env = ["COMMANDCODE_API_KEY"];
96
+
97
+ if (cc.models) return;
98
+
99
+ let models: ModelEntry[] = [];
100
+ let catalogSource: "bundled" | "cache" | "opt-in-local" = "bundled";
101
+ let commandCodeVersion: string | null = null;
102
+ let degraded = false;
103
+ let degradedReason: string | null = null;
104
+
105
+ if (override) {
106
+ const localCatalog = loadCatalogFromLocalCommandCode({ packagePath: override });
107
+ if (localCatalog && localCatalog.models.length > 0) {
108
+ models = localCatalog.models;
109
+ catalogSource = "opt-in-local";
110
+ commandCodeVersion = localCatalog.version;
111
+ }
112
+ }
113
+
114
+ if (models.length === 0) {
115
+ const bundled = loadBundledModels();
116
+ if (bundled) {
117
+ models = bundled;
118
+ catalogSource = "bundled";
119
+ commandCodeVersion = readBundledVersion();
120
+ const manifest = readBundledManifest();
121
+ if (manifest?.status === "degraded" || manifest?.status === "broken") {
122
+ degraded = true;
123
+ degradedReason =
124
+ manifest.status === "broken"
125
+ ? "bundled catalog marked broken"
126
+ : "bundled catalog has models with no listed price";
127
+ }
128
+ } else {
129
+ const cached = readCatalogCache();
130
+ if (cached) {
131
+ models = cached;
132
+ catalogSource = "cache";
133
+ degraded = true;
134
+ degradedReason = "bundled models.json unreadable; using last-good cache";
135
+ } else {
136
+ degraded = true;
137
+ degradedReason = "no bundled catalog and no cache";
138
+ }
139
+ }
140
+ }
141
+
142
+ if (models.length > 0) {
143
+ try {
144
+ writeCatalogCache(pluginStateDir(), models);
145
+ } catch {
146
+ // ignore cache write
147
+ }
148
+ }
149
+
150
+ cc.models = generateOpencodeModels(models);
151
+
152
+ const summary = {
153
+ catalogSource,
154
+ commandCodeVersion,
155
+ modelCount: models.length,
156
+ reasoningModelCount: models.filter((m) => m.reasoning).length,
157
+ degraded,
158
+ degradedReason,
159
+ };
160
+ try {
161
+ writeStartupSummary(pluginStateDir(), summary);
162
+ } catch {
163
+ // ignore
164
+ }
165
+ if (debug) {
166
+ console.warn("[commandcode]", JSON.stringify(summary));
167
+ }
168
+ },
169
+
170
+ auth: {
171
+ provider: "commandcode",
172
+ methods: [
173
+ {
174
+ type: "api",
175
+ label: "API Key",
176
+ authorize: async (inputs: Record<string, unknown> | undefined) => {
177
+ const rawKey = inputs?.key;
178
+ if (typeof rawKey !== "string") return { type: "failed" as const };
179
+ const key = rawKey.trim();
180
+ if (!key) return { type: "failed" as const };
181
+ return { type: "success" as const, key };
182
+ },
183
+ },
184
+ ],
185
+ loader: async (getAuth: () => Promise<{ type: string; key?: string } | null>) => {
186
+ try {
187
+ const auth = await getAuth();
188
+ if (!auth) return {};
189
+ if (auth.type === "api" && auth.key) return { apiKey: auth.key };
190
+ return {};
191
+ } catch {
192
+ return {};
193
+ }
194
+ },
195
+ },
196
+ };
197
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+
5
+ export function resolveApiKey(options: {
6
+ apiKey?: string;
7
+ env?: Record<string, string | undefined>;
8
+ authPaths?: string[];
9
+ }): string | undefined {
10
+ if (options.apiKey) return options.apiKey;
11
+
12
+ const envKey = options.env?.COMMANDCODE_API_KEY ?? process.env.COMMANDCODE_API_KEY;
13
+ if (envKey) return envKey;
14
+
15
+ const authPaths = options.authPaths ?? [
16
+ join(homedir(), ".commandcode", "auth.json"),
17
+ join(homedir(), ".pi", "agent", "auth.json"),
18
+ ];
19
+
20
+ for (const p of authPaths) {
21
+ if (!existsSync(p)) continue;
22
+ try {
23
+ const parsed = JSON.parse(readFileSync(p, "utf-8"));
24
+ if (typeof parsed === "object" && parsed !== null) {
25
+ if (typeof parsed.apiKey === "string") return parsed.apiKey;
26
+ if (typeof parsed.commandcode === "string") return parsed.commandcode;
27
+ if (
28
+ typeof parsed.commandcode === "object" &&
29
+ parsed.commandcode !== null &&
30
+ parsed.commandcode.type === "oauth" &&
31
+ typeof parsed.commandcode.access === "string"
32
+ ) {
33
+ return parsed.commandcode.access;
34
+ }
35
+ }
36
+ } catch {
37
+ // intentionally silent: skip unreadable or malformed auth files
38
+ continue;
39
+ }
40
+ }
41
+
42
+ return undefined;
43
+ }
@@ -0,0 +1,40 @@
1
+ export function catalogBreakTitle(commandCodeVersion: string): string {
2
+ return `[catalog-break] command-code@${commandCodeVersion} — model extraction failed`;
3
+ }
4
+
5
+ export function renderCatalogBreakBody(input: {
6
+ commandCodeVersion: string;
7
+ error: string;
8
+ workflowUrl: string;
9
+ bundledCommandCodeVersion: string | null;
10
+ }): string {
11
+ const bundled = input.bundledCommandCodeVersion ?? "(none)";
12
+ return `Model catalog extraction failed for \`command-code@${input.commandCodeVersion}\`.
13
+
14
+ ## Error
15
+
16
+ \`\`\`
17
+ ${input.error}
18
+ \`\`\`
19
+
20
+ ## Workflow
21
+
22
+ ${input.workflowUrl}
23
+
24
+ ## Still serving
25
+
26
+ Bundled catalog is still \`command-code@${bundled}\`. No npm release was published.
27
+
28
+ ## Manual fix
29
+
30
+ - [ ] Check extraction anchors in \`src/catalog.ts\`
31
+ - [ ] Add/adjust unit fixtures under \`tests/fixtures/command-code/\`
32
+ - [ ] Re-run **Catalog sync** with \`force=true\`
33
+ `;
34
+ }
35
+
36
+ export function catalogBreakResolvedComment(input: { commit: string; tag: string }): string {
37
+ return `Catalog sync recovered. Fix commit: \`${input.commit}\`. Release: \`${input.tag}\`.`;
38
+ }
39
+
40
+ export const CATALOG_BREAK_LABEL = "catalog-break";
@@ -0,0 +1,171 @@
1
+ import type { CatalogManifest, CatalogReview, CatalogStatus, CostSources } from "./manifest.js";
2
+
3
+ export type PricedModel = {
4
+ id: string;
5
+ name: string;
6
+ cost: { input: number; output: number };
7
+ };
8
+
9
+ export type CatalogReleaseInput = {
10
+ pluginVersion: string;
11
+ commandCodeVersion: string;
12
+ modelCount: number;
13
+ reasoningModelCount: number;
14
+ status: CatalogStatus;
15
+ costSources: CostSources;
16
+ models: PricedModel[];
17
+ review?: CatalogReview;
18
+ };
19
+
20
+ const DEFAULT_COST = { input: 0.5, output: 2 };
21
+
22
+ export function partitionPrices(
23
+ models: PricedModel[],
24
+ review: CatalogReview | undefined,
25
+ ): { thirdParty: PricedModel[]; free: PricedModel[]; unmatched: PricedModel[] } {
26
+ const byId = new Map(models.map((m) => [m.id, m]));
27
+ if (review) {
28
+ return {
29
+ thirdParty: review.thirdParty.flatMap((id) => (byId.get(id) ? [byId.get(id)!] : [])),
30
+ free: review.free.flatMap((id) => (byId.get(id) ? [byId.get(id)!] : [])),
31
+ unmatched: review.unmatched.flatMap((id) => (byId.get(id) ? [byId.get(id)!] : [])),
32
+ };
33
+ }
34
+ const thirdParty: PricedModel[] = [];
35
+ const free: PricedModel[] = [];
36
+ const unmatched: PricedModel[] = [];
37
+ for (const model of models) {
38
+ if (model.cost.input === 0 && model.cost.output === 0) free.push(model);
39
+ else if (model.cost.input === DEFAULT_COST.input && model.cost.output === DEFAULT_COST.output) {
40
+ unmatched.push(model);
41
+ }
42
+ }
43
+ return { thirdParty, free, unmatched };
44
+ }
45
+
46
+ function n(count: number, one: string, many: string): string {
47
+ return `${count} ${count === 1 ? one : many}`;
48
+ }
49
+
50
+ function headline(status: CatalogStatus): string {
51
+ if (status === "healthy") {
52
+ return "**Catalog is complete.** Prices come from Command Code, official docs, models.dev, or free SKUs.";
53
+ }
54
+ if (status === "broken") {
55
+ return "**Do not use this catalog.** Model extraction failed; OpenCode would not get a current model list.";
56
+ }
57
+ return "**Safe to use.** Every model is listed. Some prices have no listed source — expand the sections below to review them.";
58
+ }
59
+
60
+ function priceSummary(sources: CostSources): string {
61
+ const parts: string[] = [];
62
+ if (sources.cli > 0) parts.push(`${sources.cli} CLI`);
63
+ if (sources.officialDocs > 0) parts.push(`${sources.officialDocs} official docs`);
64
+ if (sources.thirdParty > 0) parts.push(`${sources.thirdParty} models.dev`);
65
+ if (sources.free > 0) parts.push(`${sources.free} free`);
66
+ if (sources.fallback > 0)
67
+ parts.push(`${sources.fallback} hardcoded fallback${sources.fallback === 1 ? "" : "s"}`);
68
+ if (sources.unmatched > 0) parts.push(`${sources.unmatched} no listed price`);
69
+ return parts.join(" · ") || "none";
70
+ }
71
+
72
+ function money(n: number): string {
73
+ return `$${n}`;
74
+ }
75
+
76
+ function modelTable(models: PricedModel[]): string {
77
+ const rows = models
78
+ .map((m) => `| ${m.name} | \`${m.id}\` | ${money(m.cost.input)} / ${money(m.cost.output)} |`)
79
+ .join("\n");
80
+ return `| Model | Id | In / out per 1M |\n| --- | --- | --- |\n${rows}`;
81
+ }
82
+
83
+ function details(summary: string, body: string): string {
84
+ return `<details>\n<summary>${summary}</summary>\n\n${body}\n\n</details>`;
85
+ }
86
+
87
+ export function renderCatalogReleaseNotes(input: CatalogReleaseInput): string {
88
+ const { thirdParty, free, unmatched } = partitionPrices(input.models, input.review);
89
+ const sections = [
90
+ headline(input.status),
91
+ "",
92
+ "| | |",
93
+ "| --- | --- |",
94
+ `| Plugin | ${input.pluginVersion} |`,
95
+ `| Command Code | ${input.commandCodeVersion} |`,
96
+ `| Models | ${input.modelCount} (${input.reasoningModelCount} with reasoning) |`,
97
+ `| Prices | ${priceSummary(input.costSources)} |`,
98
+ ];
99
+
100
+ if (thirdParty.length > 0) {
101
+ sections.push(
102
+ "",
103
+ details(
104
+ n(
105
+ thirdParty.length,
106
+ "model with a models.dev reference price",
107
+ "models with models.dev reference prices",
108
+ ),
109
+ modelTable(thirdParty),
110
+ ),
111
+ );
112
+ }
113
+ if (free.length > 0) {
114
+ sections.push(
115
+ "",
116
+ details(n(free.length, "free model ($0)", "free models ($0)"), modelTable(free)),
117
+ );
118
+ }
119
+ if (unmatched.length > 0) {
120
+ sections.push(
121
+ "",
122
+ details(
123
+ n(
124
+ unmatched.length,
125
+ "model with no listed price ($0.50 / $2)",
126
+ "models with no listed price ($0.50 / $2)",
127
+ ),
128
+ modelTable(unmatched),
129
+ ),
130
+ );
131
+ }
132
+
133
+ sections.push(
134
+ "",
135
+ details(
136
+ "Machine-readable catalog",
137
+ "```json\n" +
138
+ JSON.stringify(
139
+ {
140
+ status: input.status,
141
+ pluginVersion: input.pluginVersion,
142
+ commandCodeVersion: input.commandCodeVersion,
143
+ modelCount: input.modelCount,
144
+ reasoningModelCount: input.reasoningModelCount,
145
+ costSources: input.costSources,
146
+ },
147
+ null,
148
+ 2,
149
+ ) +
150
+ "\n```",
151
+ ),
152
+ );
153
+
154
+ return sections.join("\n") + "\n";
155
+ }
156
+
157
+ export function catalogReleaseNotesFromFiles(input: {
158
+ manifest: CatalogManifest;
159
+ models: PricedModel[];
160
+ }): string {
161
+ return renderCatalogReleaseNotes({
162
+ pluginVersion: input.manifest.pluginVersion,
163
+ commandCodeVersion: input.manifest.commandCodeVersion,
164
+ modelCount: input.manifest.modelCount,
165
+ reasoningModelCount: input.manifest.reasoningModelCount,
166
+ status: input.manifest.status,
167
+ costSources: input.manifest.costSources,
168
+ models: input.models,
169
+ review: input.manifest.review,
170
+ });
171
+ }