@herouucn/opencode-commandcode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@herouucn/opencode-commandcode",
3
+ "version": "0.1.0",
4
+ "description": "Inject Command Code API provider into opencode — access Claude, GPT, Gemini, DeepSeek, Qwen, Kimi, GLM, MiniMax & Step via unified Command Code API. / 将 Command Code API provider 注入 opencode —— 通过统一的 Command Code API 使用 Claude、GPT、Gemini、DeepSeek、Qwen、Kimi、GLM、MiniMax 和 Step 模型。",
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/herouu/opencode-commandcode#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/herouu/opencode-commandcode/issues"
24
+ },
25
+ "license": "MIT",
26
+ "author": "Brent Weatherall",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/herouu/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
+ "src/"
39
+ ],
40
+ "type": "module",
41
+ "main": "index.ts",
42
+ "types": "index.ts",
43
+ "exports": {
44
+ ".": {
45
+ "import": "./index.ts"
46
+ },
47
+ "./server": {
48
+ "import": "./plugin.ts"
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "lint": "oxlint --deny-warnings src scripts plugin.ts index.ts tests",
56
+ "lint:fix": "oxlint --fix src scripts plugin.ts index.ts tests",
57
+ "format": "oxfmt src scripts plugin.ts index.ts tests package.json tsconfig.json",
58
+ "format:check": "oxfmt --check src scripts plugin.ts index.ts tests package.json tsconfig.json",
59
+ "check": "bun run lint && bun run format:check && bun test tests/unit/ && bun run typecheck",
60
+ "test": "bun test tests/unit/",
61
+ "test:integration": "bun test tests/integration/",
62
+ "test:all": "bun test",
63
+ "typecheck": "tsc --noEmit",
64
+ "sync": "bun run scripts/sync-models.ts",
65
+ "sync:global": "bun run scripts/sync-models.ts --update-global",
66
+ "catalog:ci": "bun run scripts/catalog-sync-ci.ts"
67
+ },
68
+ "devDependencies": {
69
+ "@types/node": "^25.9.1",
70
+ "oxfmt": "0.63.0",
71
+ "oxlint": "1.78.0",
72
+ "typescript": "^6.0.3"
73
+ },
74
+ "peerDependencies": {
75
+ "ai": ">=6.0.0"
76
+ },
77
+ "engines": {
78
+ "bun": ">=1.0.0"
79
+ }
80
+ }
package/plugin.ts ADDED
@@ -0,0 +1,249 @@
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
+ catalogUrl?: string;
27
+ debugStartupLogs?: boolean;
28
+ }
29
+
30
+ // 默认远程目录源:本仓库自己的 main 分支(自给自足,不依赖上游仓库)。
31
+ // 本仓库的 catalog-sync CI 每 6 小时检测上游 command-code 新版本并直推 models.json 到 main。
32
+ // 可用环境变量 COMMANDCODE_CATALOG_URL 或 ~/.config/opencode/opencode-commandcode.json 的
33
+ // catalogUrl 覆盖为任意自定义源。
34
+ const DEFAULT_REMOTE_CATALOG_URL =
35
+ "https://raw.githubusercontent.com/herouu/opencode-commandcode/main/models.json";
36
+
37
+ function loadPluginConfig(): PluginFileConfig {
38
+ const dir = join(homedir(), ".config", "opencode");
39
+ const configPath = [
40
+ join(dir, "opencode-commandcode.json"),
41
+ join(dir, "commandcode-go-opencode-provider.json"),
42
+ ].find((p) => existsSync(p));
43
+ if (!configPath) return {};
44
+ try {
45
+ return JSON.parse(readFileSync(configPath, "utf-8"));
46
+ } catch {
47
+ return {};
48
+ }
49
+ }
50
+
51
+ export type { ModelEntry };
52
+
53
+ function loadBundledModels(): ModelEntry[] | null {
54
+ try {
55
+ return JSON.parse(readFileSync(MODELS_PATH, "utf-8"));
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ // 运行时从远程 URL 拉取 models.json(ModelEntry[] 格式)。
62
+ // 与包内静态文件完全解耦:目录内容由远端仓库的 CI 维护,用户无需升级 npm 包即可拿到新模型。
63
+ async function fetchRemoteCatalog(url: string, timeoutMs = 8000): Promise<ModelEntry[] | null> {
64
+ try {
65
+ const controller = new AbortController();
66
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
67
+ try {
68
+ const res = await fetch(url, {
69
+ signal: controller.signal,
70
+ headers: { Accept: "application/json" },
71
+ });
72
+ if (!res.ok) return null;
73
+ const data = (await res.json()) as unknown;
74
+ if (!Array.isArray(data)) return null;
75
+ const models = data as ModelEntry[];
76
+ if (models.length === 0 || typeof models[0]?.id !== "string") return null;
77
+ return models;
78
+ } finally {
79
+ clearTimeout(timer);
80
+ }
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ function readBundledManifest(): CatalogManifest | null {
87
+ if (!existsSync(MANIFEST_PATH)) return null;
88
+ try {
89
+ return JSON.parse(readFileSync(MANIFEST_PATH, "utf-8")) as CatalogManifest;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ function readBundledVersion(): string | null {
96
+ const manifest = readBundledManifest();
97
+ if (manifest?.commandCodeVersion) return manifest.commandCodeVersion;
98
+ if (!existsSync(VERSION_PATH)) return null;
99
+ try {
100
+ const parts = readFileSync(VERSION_PATH, "utf-8").split("\n");
101
+ const first = parts[0]?.trim();
102
+ return first || null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ export default async function commandcodePlugin() {
109
+ return {
110
+ config: async (config: Record<string, unknown>) => {
111
+ if (!(config as Record<string, unknown>).provider) {
112
+ (config as Record<string, unknown>).provider = { commandcode: {} };
113
+ }
114
+ const cc = (
115
+ (config as Record<string, unknown>).provider as Record<string, Record<string, unknown>>
116
+ )?.commandcode as Record<string, unknown> | undefined;
117
+ if (!cc) return;
118
+
119
+ const pluginCfg = loadPluginConfig();
120
+ const debug = pluginCfg.debugStartupLogs === true;
121
+ const override =
122
+ pluginCfg.commandCodePackagePath?.trim() ||
123
+ process.env.COMMANDCODE_PACKAGE_PATH?.trim() ||
124
+ "";
125
+
126
+ if (!cc.npm) cc.npm = "commandcode-go-opencode-provider";
127
+ if (!cc.name) cc.name = "Command Code";
128
+ if (!cc.env) cc.env = ["COMMANDCODE_API_KEY"];
129
+
130
+ if (cc.models) return;
131
+
132
+ let models: ModelEntry[] = [];
133
+ let catalogSource: "bundled" | "cache" | "opt-in-local" | "remote" = "bundled";
134
+ let commandCodeVersion: string | null = null;
135
+ let degraded = false;
136
+ let degradedReason: string | null = null;
137
+
138
+ const remoteUrl =
139
+ pluginCfg.catalogUrl?.trim() ||
140
+ process.env.COMMANDCODE_CATALOG_URL?.trim() ||
141
+ DEFAULT_REMOTE_CATALOG_URL;
142
+
143
+ // 路线 3:优先运行时拉取远程目录,与 npm 包版本解耦。
144
+ // 设为 "disabled" 可关闭远程拉取,回退到包内静态 models.json。
145
+ const remoteEnabled = remoteUrl !== "disabled";
146
+ if (remoteEnabled) {
147
+ const remote = await fetchRemoteCatalog(remoteUrl);
148
+ if (remote && remote.length > 0) {
149
+ models = remote;
150
+ catalogSource = "remote";
151
+ commandCodeVersion = null;
152
+ } else if (debug) {
153
+ console.warn("[commandcode] remote catalog fetch failed, falling back to bundled/cache");
154
+ }
155
+ }
156
+
157
+ if (models.length === 0 && override) {
158
+ const localCatalog = loadCatalogFromLocalCommandCode({ packagePath: override });
159
+ if (localCatalog && localCatalog.models.length > 0) {
160
+ models = localCatalog.models;
161
+ catalogSource = "opt-in-local";
162
+ commandCodeVersion = localCatalog.version;
163
+ }
164
+ }
165
+
166
+ if (models.length === 0) {
167
+ const bundled = loadBundledModels();
168
+ if (bundled) {
169
+ models = bundled;
170
+ catalogSource = "bundled";
171
+ commandCodeVersion = readBundledVersion();
172
+ const manifest = readBundledManifest();
173
+ if (manifest?.status === "degraded" || manifest?.status === "broken") {
174
+ degraded = true;
175
+ degradedReason =
176
+ manifest.status === "broken"
177
+ ? "bundled catalog marked broken"
178
+ : "bundled catalog has models with no listed price";
179
+ }
180
+ } else {
181
+ const cached = readCatalogCache();
182
+ if (cached) {
183
+ models = cached;
184
+ catalogSource = "cache";
185
+ degraded = true;
186
+ degradedReason = "bundled models.json unreadable; using last-good cache";
187
+ } else {
188
+ degraded = true;
189
+ degradedReason = "no bundled catalog and no cache";
190
+ }
191
+ }
192
+ }
193
+
194
+ if (models.length > 0) {
195
+ try {
196
+ writeCatalogCache(pluginStateDir(), models);
197
+ } catch {
198
+ // ignore cache write
199
+ }
200
+ }
201
+
202
+ cc.models = generateOpencodeModels(models);
203
+
204
+ const summary = {
205
+ catalogSource,
206
+ commandCodeVersion,
207
+ modelCount: models.length,
208
+ reasoningModelCount: models.filter((m) => m.reasoning).length,
209
+ degraded,
210
+ degradedReason,
211
+ };
212
+ try {
213
+ writeStartupSummary(pluginStateDir(), summary);
214
+ } catch {
215
+ // ignore
216
+ }
217
+ if (debug) {
218
+ console.warn("[commandcode]", JSON.stringify(summary));
219
+ }
220
+ },
221
+
222
+ auth: {
223
+ provider: "commandcode",
224
+ methods: [
225
+ {
226
+ type: "api",
227
+ label: "API Key",
228
+ authorize: async (inputs: Record<string, unknown> | undefined) => {
229
+ const rawKey = inputs?.key;
230
+ if (typeof rawKey !== "string") return { type: "failed" as const };
231
+ const key = rawKey.trim();
232
+ if (!key) return { type: "failed" as const };
233
+ return { type: "success" as const, key };
234
+ },
235
+ },
236
+ ],
237
+ loader: async (getAuth: () => Promise<{ type: string; key?: string } | null>) => {
238
+ try {
239
+ const auth = await getAuth();
240
+ if (!auth) return {};
241
+ if (auth.type === "api" && auth.key) return { apiKey: auth.key };
242
+ return {};
243
+ } catch {
244
+ return {};
245
+ }
246
+ },
247
+ },
248
+ };
249
+ }
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,41 @@
1
+ export function catalogBreakTitle(commandCodeVersion: string): string {
2
+ const version = commandCodeVersion.trim().replace(/^v/, "");
3
+ return `[catalog-break] command-code@${version} — model extraction failed`;
4
+ }
5
+
6
+ export function renderCatalogBreakBody(input: {
7
+ commandCodeVersion: string;
8
+ error: string;
9
+ workflowUrl: string;
10
+ bundledCommandCodeVersion: string | null;
11
+ }): string {
12
+ const bundled = input.bundledCommandCodeVersion ?? "(none)";
13
+ return `Model catalog extraction failed for \`command-code@${input.commandCodeVersion}\`.
14
+
15
+ ## Error
16
+
17
+ \`\`\`
18
+ ${input.error}
19
+ \`\`\`
20
+
21
+ ## Workflow
22
+
23
+ ${input.workflowUrl}
24
+
25
+ ## Still serving
26
+
27
+ Bundled catalog is still \`command-code@${bundled}\`. No npm release was published.
28
+
29
+ ## Manual fix
30
+
31
+ - [ ] Check extraction anchors in \`src/catalog.ts\`
32
+ - [ ] Add/adjust unit fixtures under \`tests/fixtures/command-code/\`
33
+ - [ ] Re-run **Catalog sync** with \`force=true\`
34
+ `;
35
+ }
36
+
37
+ export function catalogBreakResolvedComment(input: { commit: string; tag: string }): string {
38
+ return `Catalog sync recovered. Fix commit: \`${input.commit}\`. Release: \`${input.tag}\`.`;
39
+ }
40
+
41
+ export const CATALOG_BREAK_LABEL = "catalog-break";