pi-commandcode-provider 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 - 2026-05-26
4
+
5
+ - Align Command Code generate requests with CLI `0.27.2` headers and payload shape.
6
+ - Support official Command Code CLI auth files using the `command-code` credential key.
7
+ - Handle `reasoning-start` and ignore streamed `tool-result` events.
8
+ - Cap generated `max_tokens` by the selected model and the Command Code output limit.
9
+
10
+ ## 0.1.0 - 2026-05-05
11
+
12
+ - Initial public release.
package/README.md CHANGED
@@ -6,17 +6,17 @@ A [pi](https://github.com/badlogic/pi-mono) custom provider that connects pi to
6
6
 
7
7
  > **Note:** This package only provides a model _provider_. It does **not** include an API key. You must bring your own Command Code API key or subscription.
8
8
 
9
- > 💰 **Current offer:** Command Code offers [4× usage of DeepSeek V4](https://commandcode.ai/docs/resources/pricing-limits#deepseek-v4-pro-4x-usage) (Pro and Flash) at no extra cost.
9
+ > 💰 **Current offers:** Command Code offers [4× usage of DeepSeek V4 Pro](https://commandcode.ai/docs/resources/pricing-limits#deepseek-v4-pro-4x-usage) and [2× usage of Qwen 3.7 Max](https://commandcode.ai/docs/resources/pricing-limits#qwen-3.7-max-2x-usage).
10
10
 
11
11
  ## Models
12
12
 
13
- 18 models across premium and open-source providers:
13
+ Models are fetched live from Command Code's Provider API at startup, so new models like Qwen 3.7 Max show up without a package release.
14
14
 
15
- | Category | Models |
16
- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
17
- | **Anthropic** | Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 4.6, Claude Haiku 4.5 |
18
- | **OpenAI** | GPT-5.5, GPT-5.4, GPT-5.3 Codex, GPT-5.4 Mini |
19
- | **Open-source** | DeepSeek V4, DeepSeek V4 Pro, DeepSeek V4 Flash, Kimi K2.6, Kimi K2.5, GLM-5.1, GLM-5, MiniMax M2.7, MiniMax M2.5, Qwen 3.6 Max, Qwen 3.6 Plus |
15
+ You can list the current Command Code models with:
16
+
17
+ ```sh
18
+ pi -e index.ts --list-models
19
+ ```
20
20
 
21
21
  ## Install
22
22
 
@@ -50,6 +50,8 @@ In pi, run:
50
50
 
51
51
  Then select **Command Code** from the provider list.
52
52
 
53
+ <img width="1520" height="554" alt="image" src="https://github.com/user-attachments/assets/071e929a-6f49-4803-bfec-7a31368fb12a" />
54
+
53
55
  This opens Command Code in your browser and stores the returned API key in pi's auth file. If the browser shows "Copy your API key" because automatic transfer failed, copy that key and paste it into the pi terminal prompt.
54
56
 
55
57
  > Note: `/login commandcode` is not supported by pi currently; use interactive `/login` and select Command Code.
@@ -70,6 +72,17 @@ Create `~/.commandcode/auth.json`:
70
72
  }
71
73
  ```
72
74
 
75
+ The official Command Code CLI auth shape is also supported:
76
+
77
+ ```json
78
+ {
79
+ "command-code": {
80
+ "type": "api",
81
+ "key": "user_..."
82
+ }
83
+ }
84
+ ```
85
+
73
86
  Or use pi's auth file at `~/.pi/agent/auth.json`:
74
87
 
75
88
  ```json
@@ -86,18 +99,22 @@ After installing and setting your API key, select a Command Code model in pi:
86
99
  /model deepseek/deepseek-v4-flash
87
100
  ```
88
101
 
89
- Any query will then use the Command Code API. You can list available models:
102
+ Any query will then use the Command Code API. You can list available models within pi:
90
103
 
91
- ```sh
92
- pi -e index.ts --list-models
104
+ ```txt
105
+ /models
93
106
  ```
94
107
 
95
- Or within pi:
108
+ ## Model discovery
109
+
110
+ On startup, the provider fetches:
96
111
 
97
112
  ```txt
98
- /models
113
+ https://api.commandcode.ai/provider/v1/models
99
114
  ```
100
115
 
116
+ For tests or local mocks, override it with `COMMANDCODE_MODELS_URL`.
117
+
101
118
  ## Publish
102
119
 
103
120
  ```sh
package/index.ts CHANGED
@@ -9,152 +9,18 @@
9
9
  * 3. Place API key in `~/.commandcode/auth.json` or `~/.pi/agent/auth.json`
10
10
  * as {"apiKey": "user_..."} or {"commandcode": "user_..."}
11
11
  *
12
- * Models: deepseek-v4-pro, deepseek-v4-flash, claude-sonnet-4-6, claude-opus-4-7, etc.
12
+ * Models are fetched from Command Code's Provider API at startup.
13
13
  */
14
14
 
15
15
  import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai"
16
16
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
17
17
 
18
- import { createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
18
+ import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
19
+ import { DEFAULT_MODELS_URL, fetchCommandCodeModels } from "./src/models.ts"
19
20
  import { getApiKey, login, refreshToken } from "./src/oauth.ts"
20
21
 
21
22
  const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
22
-
23
- // ---------------------------------------------------------------------------
24
- // Model definitions
25
- // ---------------------------------------------------------------------------
26
-
27
- const MODELS = [
28
- // Premium (Anthropic)
29
- {
30
- id: "claude-opus-4-7",
31
- name: "Claude Opus 4.7 (CC)",
32
- reasoning: true,
33
- contextWindow: 200_000,
34
- maxTokens: 32_000,
35
- },
36
- {
37
- id: "claude-opus-4-6",
38
- name: "Claude Opus 4.6 (CC)",
39
- reasoning: true,
40
- contextWindow: 200_000,
41
- maxTokens: 32_000,
42
- },
43
- {
44
- id: "claude-sonnet-4-6",
45
- name: "Claude Sonnet 4.6 (CC)",
46
- reasoning: true,
47
- contextWindow: 200_000,
48
- maxTokens: 16_384,
49
- },
50
- {
51
- id: "claude-haiku-4-5-20251001",
52
- name: "Claude Haiku 4.5 (CC)",
53
- reasoning: true,
54
- contextWindow: 200_000,
55
- maxTokens: 8_192,
56
- },
57
- // Premium (OpenAI)
58
- {
59
- id: "gpt-5.5",
60
- name: "GPT-5.5 (CC)",
61
- reasoning: true,
62
- contextWindow: 256_000,
63
- maxTokens: 128_000,
64
- },
65
- {
66
- id: "gpt-5.4",
67
- name: "GPT-5.4 (CC)",
68
- reasoning: true,
69
- contextWindow: 256_000,
70
- maxTokens: 128_000,
71
- },
72
- {
73
- id: "gpt-5.3-codex",
74
- name: "GPT-5.3 Codex (CC)",
75
- reasoning: true,
76
- contextWindow: 256_000,
77
- maxTokens: 128_000,
78
- },
79
- {
80
- id: "gpt-5.4-mini",
81
- name: "GPT-5.4 Mini (CC)",
82
- reasoning: false,
83
- contextWindow: 256_000,
84
- maxTokens: 128_000,
85
- },
86
- // Open-source
87
- {
88
- id: "deepseek/deepseek-v4-pro",
89
- name: "DeepSeek V4 Pro (CC)",
90
- reasoning: true,
91
- contextWindow: 1_000_000,
92
- maxTokens: 384_000,
93
- },
94
- {
95
- id: "deepseek/deepseek-v4-flash",
96
- name: "DeepSeek V4 Flash (CC)",
97
- reasoning: true,
98
- contextWindow: 1_000_000,
99
- maxTokens: 384_000,
100
- },
101
- {
102
- id: "moonshotai/Kimi-K2.6",
103
- name: "Kimi K2.6 (CC)",
104
- reasoning: true,
105
- contextWindow: 262_144,
106
- maxTokens: 131_072,
107
- },
108
- {
109
- id: "moonshotai/Kimi-K2.5",
110
- name: "Kimi K2.5 (CC)",
111
- reasoning: true,
112
- contextWindow: 262_144,
113
- maxTokens: 131_072,
114
- },
115
- {
116
- id: "zai-org/GLM-5.1",
117
- name: "GLM-5.1 (CC)",
118
- reasoning: true,
119
- contextWindow: 200_000,
120
- maxTokens: 131_072,
121
- },
122
- {
123
- id: "zai-org/GLM-5",
124
- name: "GLM-5 (CC)",
125
- reasoning: true,
126
- contextWindow: 200_000,
127
- maxTokens: 131_072,
128
- },
129
- {
130
- id: "MiniMaxAI/MiniMax-M2.7",
131
- name: "MiniMax M2.7 (CC)",
132
- reasoning: true,
133
- contextWindow: 1_048_576,
134
- maxTokens: 131_072,
135
- },
136
- {
137
- id: "MiniMaxAI/MiniMax-M2.5",
138
- name: "MiniMax M2.5 (CC)",
139
- reasoning: true,
140
- contextWindow: 1_048_576,
141
- maxTokens: 131_072,
142
- },
143
- {
144
- id: "Qwen/Qwen3.6-Max-Preview",
145
- name: "Qwen 3.6 Max (CC)",
146
- reasoning: true,
147
- contextWindow: 1_000_000,
148
- maxTokens: 131_072,
149
- },
150
- {
151
- id: "Qwen/Qwen3.6-Plus",
152
- name: "Qwen 3.6 Plus (CC)",
153
- reasoning: true,
154
- contextWindow: 1_000_000,
155
- maxTokens: 131_072,
156
- },
157
- ]
23
+ const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
158
24
 
159
25
  const streamCommandCode = createStreamCommandCode({
160
26
  createStream: createAssistantMessageEventStream,
@@ -166,7 +32,9 @@ const streamCommandCode = createStreamCommandCode({
166
32
  // Extension entry point
167
33
  // ---------------------------------------------------------------------------
168
34
 
169
- export default function (pi: ExtensionAPI) {
35
+ export default async function (pi: ExtensionAPI) {
36
+ const models = await fetchCommandCodeModels({ url: MODELS_URL })
37
+
170
38
  pi.registerProvider("commandcode", {
171
39
  name: "Command Code",
172
40
  baseUrl: API_BASE,
@@ -175,7 +43,7 @@ export default function (pi: ExtensionAPI) {
175
43
  api: "commandcode-custom",
176
44
  streamSimple: streamCommandCode,
177
45
  headers: {
178
- "x-command-code-version": "0.24.1",
46
+ "x-command-code-version": COMMAND_CODE_CLI_VERSION,
179
47
  "x-cli-environment": "production",
180
48
  },
181
49
  oauth: {
@@ -184,11 +52,11 @@ export default function (pi: ExtensionAPI) {
184
52
  refreshToken,
185
53
  getApiKey,
186
54
  },
187
- models: MODELS.map((model) => ({
55
+ models: models.map((model) => ({
188
56
  id: model.id,
189
57
  name: model.name,
190
58
  reasoning: model.reasoning,
191
- input: ["text"],
59
+ input: ["text"] as const,
192
60
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
193
61
  contextWindow: model.contextWindow,
194
62
  maxTokens: model.maxTokens,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-commandcode-provider",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "pi custom provider for Command Code API (commandcode.ai)",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -22,14 +22,16 @@
22
22
  "index.ts",
23
23
  "src/",
24
24
  "README.md",
25
+ "CHANGELOG.md",
25
26
  "LICENSE"
26
27
  ],
27
28
  "scripts": {
28
- "test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && node tests/test-pi-local.mjs",
29
+ "test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && node tests/test-pi-local.mjs",
29
30
  "typecheck": "tsc --noEmit",
30
31
  "format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
31
32
  "format": "prettier --write '**/*.{ts,mjs,json,md}'",
32
33
  "test:unit": "tsx tests/test-pure-functions.ts",
34
+ "test:models": "tsx tests/test-models.ts",
33
35
  "test:oauth": "tsx tests/test-oauth.ts",
34
36
  "test:abort": "tsx tests/test-abort.ts",
35
37
  "test:stream": "tsx tests/test-stream.ts",
package/src/converters.ts CHANGED
@@ -42,6 +42,16 @@ function defaultAuthPaths(home: string): string[] {
42
42
  return [join(home, ".commandcode", "auth.json"), join(home, ".pi", "agent", "auth.json")]
43
43
  }
44
44
 
45
+ function apiKeyFromCredentialRecord(value: unknown): string | undefined {
46
+ if (!isRecord(value)) return undefined
47
+
48
+ const type = stringValue(value.type)
49
+ if (type === "api") return stringValue(value.key)
50
+ if (type === "oauth") return stringValue(value.access)
51
+
52
+ return stringValue(value.key) ?? stringValue(value.access)
53
+ }
54
+
45
55
  export function getApiKey(
46
56
  options: {
47
57
  env?: NodeJS.ProcessEnv
@@ -61,18 +71,18 @@ export function getApiKey(
61
71
  const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
62
72
  if (!isRecord(parsed)) continue
63
73
 
64
- // Legacy: direct apiKey or commandcode field
74
+ // Legacy: direct apiKey or commandcode field.
65
75
  const apiKey = stringValue(parsed.apiKey)
66
76
  if (apiKey) return apiKey
67
77
  const commandcode = stringValue(parsed.commandcode)
68
78
  if (commandcode) return commandcode
69
79
 
70
- // OAuth: pi stores OAuth credentials as {"commandcode": {"type":"oauth","access":"...","refresh":"...","expires":...}}
71
- const providerKey = isRecord(parsed.commandcode) ? parsed.commandcode : undefined
72
- if (providerKey && stringValue(providerKey.type) === "oauth") {
73
- const access = stringValue(providerKey.access)
74
- if (access) return access
75
- }
80
+ // pi stores OAuth credentials as {"commandcode": {"type":"oauth","access":"..."}}.
81
+ // The official Command Code CLI stores API credentials under "command-code".
82
+ const providerKey =
83
+ apiKeyFromCredentialRecord(parsed.commandcode) ??
84
+ apiKeyFromCredentialRecord(parsed["command-code"])
85
+ if (providerKey) return providerKey
76
86
  } catch {
77
87
  // Ignore malformed or unreadable auth files.
78
88
  }
package/src/core.ts CHANGED
@@ -38,6 +38,9 @@ export * from "./converters.ts"
38
38
  export * from "./types.ts"
39
39
 
40
40
  export const DEFAULT_API_BASE = "https://api.commandcode.ai"
41
+ export const COMMAND_CODE_CLI_VERSION = "0.27.2"
42
+
43
+ const DEFAULT_GENERATE_MAX_TOKENS = 64_000
41
44
 
42
45
  function defaultUsage(): Usage {
43
46
  return {
@@ -77,6 +80,23 @@ function successStopReason(reason: TerminalReason): StopReason {
77
80
  return "stop"
78
81
  }
79
82
 
83
+ function generateMaxTokens(model: ModelLike, options?: StreamOptions): number {
84
+ return Math.min(
85
+ options?.maxTokens ?? model.maxTokens,
86
+ model.maxTokens,
87
+ DEFAULT_GENERATE_MAX_TOKENS,
88
+ )
89
+ }
90
+
91
+ export function projectSlugFromPath(pathName: string): string {
92
+ const slug = pathName
93
+ .toLowerCase()
94
+ .replace(/^[a-z]:/i, "")
95
+ .replace(/[^a-z0-9]+/g, "-")
96
+ .replace(/^-+|-+$/g, "")
97
+ return slug || "project"
98
+ }
99
+
80
100
  export function createStreamCommandCode(deps: CoreDependencies) {
81
101
  const apiBase = deps.apiBase ?? DEFAULT_API_BASE
82
102
  const fetchImpl = deps.fetchImpl ?? fetch
@@ -235,7 +255,13 @@ export function createStreamCommandCode(deps: CoreDependencies) {
235
255
  break
236
256
  }
237
257
 
258
+ case "reasoning-start": {
259
+ endTextBlock()
260
+ break
261
+ }
262
+
238
263
  case "reasoning-delta": {
264
+ endTextBlock()
239
265
  thinkingBlock.push(stringValue(event.text) ?? "")
240
266
  break
241
267
  }
@@ -245,6 +271,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
245
271
  break
246
272
  }
247
273
 
274
+ case "tool-result": {
275
+ break
276
+ }
277
+
248
278
  case "tool-call": {
249
279
  endTextBlock()
250
280
  const toolCall: ToolCallContent = {
@@ -303,9 +333,12 @@ export function createStreamCommandCode(deps: CoreDependencies) {
303
333
  try {
304
334
  stream.push({ type: "start", partial: output })
305
335
 
336
+ const workingDir = cwd()
337
+ const threadId = uuid()
338
+
306
339
  let body: unknown = {
307
340
  config: {
308
- workingDir: cwd(),
341
+ workingDir,
309
342
  date: new Date(now()).toISOString().split("T")[0],
310
343
  environment: getEnvironmentInfo(),
311
344
  structure: [],
@@ -315,18 +348,19 @@ export function createStreamCommandCode(deps: CoreDependencies) {
315
348
  gitStatus: "",
316
349
  recentCommits: [],
317
350
  },
318
- memory: "",
319
- taste: "",
351
+ memory: null,
352
+ taste: null,
320
353
  skills: null,
321
- permissionMode: "standard",
322
354
  params: {
323
355
  model: model.id,
324
356
  messages: messagesToCC(context.messages),
325
357
  tools: toolsToJson(context.tools),
326
358
  system: context.systemPrompt ?? "",
327
- max_tokens: Math.min(options?.maxTokens ?? model.maxTokens, 200_000),
359
+ max_tokens: generateMaxTokens(model, options),
360
+ temperature: 0.3,
328
361
  stream: true,
329
362
  },
363
+ threadId,
330
364
  }
331
365
 
332
366
  const nextBody = await raceAbort(
@@ -341,12 +375,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
341
375
  headers: {
342
376
  "Content-Type": "application/json",
343
377
  Authorization: `Bearer ${apiKey}`,
344
- "x-command-code-version": "0.24.1",
378
+ "x-command-code-version": COMMAND_CODE_CLI_VERSION,
345
379
  "x-cli-environment": "production",
346
- "x-project-slug": "pi-cc",
347
- "x-taste-learning": "false",
380
+ "x-project-slug": projectSlugFromPath(workingDir),
381
+ "x-taste-learning": "true",
348
382
  "x-co-flag": "false",
349
- "x-session-id": uuid(),
350
383
  ...options?.headers,
351
384
  },
352
385
  body: JSON.stringify(body),
package/src/models.ts ADDED
@@ -0,0 +1,85 @@
1
+ export const DEFAULT_MODELS_URL = "https://api.commandcode.ai/provider/v1/models"
2
+
3
+ const DEFAULT_MAX_OUTPUT_TOKENS = 65_536
4
+
5
+ interface ApiModel {
6
+ id: string
7
+ name: string
8
+ contextLength: number
9
+ }
10
+
11
+ export interface CommandCodeModel {
12
+ id: string
13
+ name: string
14
+ reasoning: boolean
15
+ contextWindow: number
16
+ maxTokens: number
17
+ }
18
+
19
+ interface FetchCommandCodeModelsOptions {
20
+ url?: string
21
+ fetchImpl?: typeof fetch
22
+ }
23
+
24
+ function isRecord(value: unknown): value is Record<string, unknown> {
25
+ return typeof value === "object" && value !== null
26
+ }
27
+
28
+ function stringField(record: Record<string, unknown>, key: string): string {
29
+ const value = record[key]
30
+ if (typeof value !== "string") throw new Error(`Expected ${key} to be a string`)
31
+ return value
32
+ }
33
+
34
+ function numberField(record: Record<string, unknown>, key: string): number {
35
+ const value = record[key]
36
+ if (typeof value !== "number") throw new Error(`Expected ${key} to be a number`)
37
+ return value
38
+ }
39
+
40
+ function parseApiModel(value: unknown): ApiModel {
41
+ if (!isRecord(value)) throw new Error("Expected model entry to be an object")
42
+
43
+ return {
44
+ id: stringField(value, "id"),
45
+ name: stringField(value, "name"),
46
+ contextLength: numberField(value, "context_length"),
47
+ }
48
+ }
49
+
50
+ export function commandCodeModelsFromApiResponse(value: unknown): readonly CommandCodeModel[] {
51
+ if (!isRecord(value)) throw new Error("Expected models response to be an object")
52
+ if (value.object !== "list") throw new Error("Expected models response object to be 'list'")
53
+
54
+ const data = value.data
55
+ if (!Array.isArray(data)) throw new Error("Expected models response data to be an array")
56
+
57
+ return data.map(parseApiModel).map((model) => ({
58
+ id: model.id,
59
+ name: `${model.name} (CC)`,
60
+ reasoning: true,
61
+ contextWindow: model.contextLength,
62
+ maxTokens: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
63
+ }))
64
+ }
65
+
66
+ export async function fetchCommandCodeModels(
67
+ options: FetchCommandCodeModelsOptions = {},
68
+ ): Promise<readonly CommandCodeModel[]> {
69
+ const url = options.url ?? DEFAULT_MODELS_URL
70
+ const fetchImpl = options.fetchImpl ?? fetch
71
+ const response = await fetchImpl(url, {
72
+ headers: {
73
+ accept: "application/json",
74
+ },
75
+ })
76
+
77
+ if (!response.ok) {
78
+ throw new Error(
79
+ `Failed to fetch Command Code models: ${response.status} ${response.statusText}`,
80
+ )
81
+ }
82
+
83
+ const body: unknown = await response.json()
84
+ return commandCodeModelsFromApiResponse(body)
85
+ }