@herouucn/opencode-commandcode 0.1.0 → 0.1.2

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/src/model.ts CHANGED
@@ -1,168 +1,168 @@
1
- import type {
2
- LanguageModelV3,
3
- LanguageModelV3CallOptions,
4
- LanguageModelV3StreamResult,
5
- LanguageModelV3GenerateResult,
6
- LanguageModelV3Content,
7
- LanguageModelV3Usage,
8
- LanguageModelV3FinishReason,
9
- } from "@ai-sdk/provider";
10
- import { buildRequest } from "./convert.js";
11
- import { parseStreamEvents } from "./stream.js";
12
-
13
- const DEFAULT_BASE_URL = "https://api.commandcode.ai";
14
- // x-command-code-version must match the Command Code CLI version for API compatibility
15
- const CC_VERSION = "0.26.20";
16
-
17
- export interface CommandCodeModelOptions {
18
- apiKey: string;
19
- baseURL?: string;
20
- headers?: Record<string, string>;
21
- }
22
-
23
- export class CommandCodeLanguageModel implements LanguageModelV3 {
24
- readonly specificationVersion = "v3" as const;
25
- readonly provider = "commandcode";
26
- readonly modelId: string;
27
- supportedUrls: Record<string, RegExp[]> = {};
28
-
29
- private opts: CommandCodeModelOptions;
30
-
31
- constructor(modelId: string, opts: CommandCodeModelOptions) {
32
- this.modelId = modelId;
33
- this.opts = opts;
34
- }
35
-
36
- private get baseURL(): string {
37
- return this.opts.baseURL ?? DEFAULT_BASE_URL;
38
- }
39
-
40
- private buildHeaders(): Record<string, string> {
41
- return {
42
- "Content-Type": "application/json",
43
- Authorization: `Bearer ${this.opts.apiKey}`,
44
- "x-command-code-version": CC_VERSION,
45
- "x-cli-environment": "production",
46
- "x-project-slug": "opencode",
47
- ...this.opts.headers,
48
- };
49
- }
50
-
51
- async doStream(options: LanguageModelV3CallOptions): Promise<LanguageModelV3StreamResult> {
52
- const body = buildRequest(this.modelId, options);
53
- const requestBody = JSON.stringify(body);
54
-
55
- const controller = new AbortController();
56
- const timeout = setTimeout(
57
- () => controller.abort(new Error("Request timed out after 5 minutes")),
58
- 300_000,
59
- );
60
- const userSignal = options.abortSignal;
61
- if (userSignal) {
62
- const onAbort = () => controller.abort(userSignal.reason);
63
- userSignal.addEventListener("abort", onAbort, { once: true });
64
- }
65
-
66
- try {
67
- const response = await fetch(`${this.baseURL}/alpha/generate`, {
68
- method: "POST",
69
- headers: this.buildHeaders(),
70
- body: requestBody,
71
- signal: controller.signal,
72
- });
73
-
74
- if (!response.ok) {
75
- const errorBody = await response.text().catch(() => "");
76
- let errorMessage = `Command Code API error: ${response.status} ${response.statusText}`;
77
- try {
78
- const parsed = JSON.parse(errorBody);
79
- if (parsed.error?.message) errorMessage = parsed.error.message;
80
- else if (parsed.message) errorMessage = parsed.message;
81
- } catch {
82
- // intentionally silent: error body is not JSON
83
- }
84
- throw new Error(`${errorMessage} [model=${this.modelId}]`);
85
- }
86
-
87
- if (!response.body) {
88
- throw new Error(`Command Code API returned no body [model=${this.modelId}]`);
89
- }
90
-
91
- const responseHeaders: Record<string, string> = {};
92
- response.headers.forEach((v, k) => {
93
- responseHeaders[k] = v;
94
- });
95
-
96
- return {
97
- stream: parseStreamEvents(response.body as ReadableStream<Uint8Array>),
98
- request: { body: requestBody },
99
- response: { headers: responseHeaders },
100
- };
101
- } finally {
102
- clearTimeout(timeout);
103
- }
104
- }
105
-
106
- async doGenerate(options: LanguageModelV3CallOptions): Promise<LanguageModelV3GenerateResult> {
107
- const { stream } = await this.doStream(options);
108
-
109
- const textParts: string[] = [];
110
- const reasoningParts: string[] = [];
111
- const content: LanguageModelV3Content[] = [];
112
- let finishReason: LanguageModelV3FinishReason = { unified: "stop", raw: "stop" };
113
- let usage: LanguageModelV3Usage = {
114
- inputTokens: {
115
- total: undefined,
116
- noCache: undefined,
117
- cacheRead: undefined,
118
- cacheWrite: undefined,
119
- },
120
- outputTokens: { total: undefined, text: undefined, reasoning: undefined },
121
- };
122
-
123
- const reader = stream.getReader();
124
- try {
125
- while (true) {
126
- const { done, value } = await reader.read();
127
- if (done) break;
128
-
129
- switch (value.type) {
130
- case "text-delta":
131
- textParts.push(value.delta);
132
- break;
133
- case "reasoning-delta":
134
- reasoningParts.push(value.delta);
135
- break;
136
- case "tool-call":
137
- content.push({
138
- type: "tool-call",
139
- toolCallId: value.toolCallId,
140
- toolName: value.toolName,
141
- input: value.input,
142
- });
143
- break;
144
- case "finish":
145
- finishReason = value.finishReason;
146
- usage = value.usage;
147
- break;
148
- }
149
- }
150
- } finally {
151
- reader.releaseLock();
152
- stream.cancel();
153
- }
154
-
155
- const text = textParts.join("");
156
- if (text) content.unshift({ type: "text", text });
157
-
158
- const reasoning = reasoningParts.join("");
159
- if (reasoning) content.unshift({ type: "reasoning", text: reasoning });
160
-
161
- return {
162
- content,
163
- finishReason,
164
- usage,
165
- warnings: [],
166
- };
167
- }
168
- }
1
+ import type {
2
+ LanguageModelV3,
3
+ LanguageModelV3CallOptions,
4
+ LanguageModelV3StreamResult,
5
+ LanguageModelV3GenerateResult,
6
+ LanguageModelV3Content,
7
+ LanguageModelV3Usage,
8
+ LanguageModelV3FinishReason,
9
+ } from "@ai-sdk/provider";
10
+ import { buildRequest } from "./convert.js";
11
+ import { parseStreamEvents } from "./stream.js";
12
+
13
+ const DEFAULT_BASE_URL = "https://api.commandcode.ai";
14
+ // x-command-code-version must match the Command Code CLI version for API compatibility
15
+ const CC_VERSION = "0.26.20";
16
+
17
+ export interface CommandCodeModelOptions {
18
+ apiKey: string;
19
+ baseURL?: string;
20
+ headers?: Record<string, string>;
21
+ }
22
+
23
+ export class CommandCodeLanguageModel implements LanguageModelV3 {
24
+ readonly specificationVersion = "v3" as const;
25
+ readonly provider = "commandcode";
26
+ readonly modelId: string;
27
+ supportedUrls: Record<string, RegExp[]> = {};
28
+
29
+ private opts: CommandCodeModelOptions;
30
+
31
+ constructor(modelId: string, opts: CommandCodeModelOptions) {
32
+ this.modelId = modelId;
33
+ this.opts = opts;
34
+ }
35
+
36
+ private get baseURL(): string {
37
+ return this.opts.baseURL ?? DEFAULT_BASE_URL;
38
+ }
39
+
40
+ private buildHeaders(): Record<string, string> {
41
+ return {
42
+ "Content-Type": "application/json",
43
+ Authorization: `Bearer ${this.opts.apiKey}`,
44
+ "x-command-code-version": CC_VERSION,
45
+ "x-cli-environment": "production",
46
+ "x-project-slug": "opencode",
47
+ ...this.opts.headers,
48
+ };
49
+ }
50
+
51
+ async doStream(options: LanguageModelV3CallOptions): Promise<LanguageModelV3StreamResult> {
52
+ const body = buildRequest(this.modelId, options);
53
+ const requestBody = JSON.stringify(body);
54
+
55
+ const controller = new AbortController();
56
+ const timeout = setTimeout(
57
+ () => controller.abort(new Error("Request timed out after 5 minutes")),
58
+ 300_000,
59
+ );
60
+ const userSignal = options.abortSignal;
61
+ if (userSignal) {
62
+ const onAbort = () => controller.abort(userSignal.reason);
63
+ userSignal.addEventListener("abort", onAbort, { once: true });
64
+ }
65
+
66
+ try {
67
+ const response = await fetch(`${this.baseURL}/alpha/generate`, {
68
+ method: "POST",
69
+ headers: this.buildHeaders(),
70
+ body: requestBody,
71
+ signal: controller.signal,
72
+ });
73
+
74
+ if (!response.ok) {
75
+ const errorBody = await response.text().catch(() => "");
76
+ let errorMessage = `Command Code API error: ${response.status} ${response.statusText}`;
77
+ try {
78
+ const parsed = JSON.parse(errorBody);
79
+ if (parsed.error?.message) errorMessage = parsed.error.message;
80
+ else if (parsed.message) errorMessage = parsed.message;
81
+ } catch {
82
+ // intentionally silent: error body is not JSON
83
+ }
84
+ throw new Error(`${errorMessage} [model=${this.modelId}]`);
85
+ }
86
+
87
+ if (!response.body) {
88
+ throw new Error(`Command Code API returned no body [model=${this.modelId}]`);
89
+ }
90
+
91
+ const responseHeaders: Record<string, string> = {};
92
+ response.headers.forEach((v, k) => {
93
+ responseHeaders[k] = v;
94
+ });
95
+
96
+ return {
97
+ stream: parseStreamEvents(response.body as ReadableStream<Uint8Array>),
98
+ request: { body: requestBody },
99
+ response: { headers: responseHeaders },
100
+ };
101
+ } finally {
102
+ clearTimeout(timeout);
103
+ }
104
+ }
105
+
106
+ async doGenerate(options: LanguageModelV3CallOptions): Promise<LanguageModelV3GenerateResult> {
107
+ const { stream } = await this.doStream(options);
108
+
109
+ const textParts: string[] = [];
110
+ const reasoningParts: string[] = [];
111
+ const content: LanguageModelV3Content[] = [];
112
+ let finishReason: LanguageModelV3FinishReason = { unified: "stop", raw: "stop" };
113
+ let usage: LanguageModelV3Usage = {
114
+ inputTokens: {
115
+ total: undefined,
116
+ noCache: undefined,
117
+ cacheRead: undefined,
118
+ cacheWrite: undefined,
119
+ },
120
+ outputTokens: { total: undefined, text: undefined, reasoning: undefined },
121
+ };
122
+
123
+ const reader = stream.getReader();
124
+ try {
125
+ while (true) {
126
+ const { done, value } = await reader.read();
127
+ if (done) break;
128
+
129
+ switch (value.type) {
130
+ case "text-delta":
131
+ textParts.push(value.delta);
132
+ break;
133
+ case "reasoning-delta":
134
+ reasoningParts.push(value.delta);
135
+ break;
136
+ case "tool-call":
137
+ content.push({
138
+ type: "tool-call",
139
+ toolCallId: value.toolCallId,
140
+ toolName: value.toolName,
141
+ input: value.input,
142
+ });
143
+ break;
144
+ case "finish":
145
+ finishReason = value.finishReason;
146
+ usage = value.usage;
147
+ break;
148
+ }
149
+ }
150
+ } finally {
151
+ reader.releaseLock();
152
+ stream.cancel();
153
+ }
154
+
155
+ const text = textParts.join("");
156
+ if (text) content.unshift({ type: "text", text });
157
+
158
+ const reasoning = reasoningParts.join("");
159
+ if (reasoning) content.unshift({ type: "reasoning", text: reasoning });
160
+
161
+ return {
162
+ content,
163
+ finishReason,
164
+ usage,
165
+ warnings: [],
166
+ };
167
+ }
168
+ }
package/src/startup.ts CHANGED
@@ -1,43 +1,43 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
- import { homedir } from "os";
3
- import { join } from "path";
4
- import type { ModelEntry } from "./catalog.js";
5
-
6
- export type { ModelEntry } from "./catalog.js";
7
-
8
- export type StartupSummary = {
9
- catalogSource: "bundled" | "cache" | "opt-in-local" | "remote";
10
- commandCodeVersion: string | null;
11
- modelCount: number;
12
- reasoningModelCount: number;
13
- degraded: boolean;
14
- degradedReason: string | null;
15
- };
16
-
17
- export function pluginStateDir(): string {
18
- const override = process.env.COMMANDCODE_PROVIDER_STATE_DIR?.trim();
19
- if (override) return override;
20
- return join(homedir(), ".local/state/opencode/commandcode-provider");
21
- }
22
-
23
- export function readCatalogCache(dir = pluginStateDir()): ModelEntry[] | null {
24
- const path = join(dir, "catalog-cache.json");
25
- if (!existsSync(path)) return null;
26
- try {
27
- const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
28
- if (!Array.isArray(parsed) || parsed.length === 0) return null;
29
- return parsed as ModelEntry[];
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- export function writeCatalogCache(dir: string, models: ModelEntry[]): void {
36
- mkdirSync(dir, { recursive: true });
37
- writeFileSync(join(dir, "catalog-cache.json"), JSON.stringify(models) + "\n", "utf-8");
38
- }
39
-
40
- export function writeStartupSummary(dir: string, summary: StartupSummary): void {
41
- mkdirSync(dir, { recursive: true });
42
- writeFileSync(join(dir, "startup.json"), JSON.stringify(summary) + "\n", "utf-8");
43
- }
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import type { ModelEntry } from "./catalog.js";
5
+
6
+ export type { ModelEntry } from "./catalog.js";
7
+
8
+ export type StartupSummary = {
9
+ catalogSource: "bundled" | "cache" | "opt-in-local" | "remote";
10
+ commandCodeVersion: string | null;
11
+ modelCount: number;
12
+ reasoningModelCount: number;
13
+ degraded: boolean;
14
+ degradedReason: string | null;
15
+ };
16
+
17
+ export function pluginStateDir(): string {
18
+ const override = process.env.COMMANDCODE_PROVIDER_STATE_DIR?.trim();
19
+ if (override) return override;
20
+ return join(homedir(), ".local/state/opencode/commandcode-provider");
21
+ }
22
+
23
+ export function readCatalogCache(dir = pluginStateDir()): ModelEntry[] | null {
24
+ const path = join(dir, "catalog-cache.json");
25
+ if (!existsSync(path)) return null;
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
28
+ if (!Array.isArray(parsed) || parsed.length === 0) return null;
29
+ return parsed as ModelEntry[];
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ export function writeCatalogCache(dir: string, models: ModelEntry[]): void {
36
+ mkdirSync(dir, { recursive: true });
37
+ writeFileSync(join(dir, "catalog-cache.json"), JSON.stringify(models) + "\n", "utf-8");
38
+ }
39
+
40
+ export function writeStartupSummary(dir: string, summary: StartupSummary): void {
41
+ mkdirSync(dir, { recursive: true });
42
+ writeFileSync(join(dir, "startup.json"), JSON.stringify(summary) + "\n", "utf-8");
43
+ }