pi-commandcode-provider 0.5.0 → 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/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.0 - 2026-08-25
6
+
7
+ - Allow switching from a vision-capable model to a text-only model by omitting historical image tool results while preserving their text output; direct image prompts still fail clearly.
8
+ - Stream incremental tool-call arguments from the `/alpha/generate` transport instead of waiting for the final complete tool-call event.
9
+ - Add a daily GitHub Actions synchronization job that opens or updates a pull request for CLI version, image capability, reasoning, effort, and output-limit changes in the latest published Command Code catalog.
10
+ - Refresh static model capabilities from `command-code@1.32.2`, separating reasoning support from selectable effort levels and honoring model-specific output limits.
11
+ - Reject truncated, aborted, and network-failed generate streams instead of reporting partial responses as successful.
12
+ - Normalize malformed tool results and synthesize missing tool results so follow-up requests preserve valid tool-call history.
13
+ - Refresh display pricing for all 58 current models, including Gemini 3.7 Flash, Qwen 3.8 27B, Ox Alpha, Muse Spark 1.2, and Grok 4.6 long-context rates.
14
+ - Accept the official `COMMAND_CODE_API_KEY` and `CMD_ZDR` environment variables while retaining legacy aliases.
15
+ - Align generate request metadata with the CLI by forwarding stable session IDs, optional temperature, and the CLI user agent.
16
+ - Validate manually pasted API keys, use the CLI's two-minute browser timeout, and reject OAuth state mismatches without closing the callback server.
17
+ - Add `/commandcode-quota` with live credits, plan, usage totals, and rolling-limit diagnostics from Command Code's alpha usage endpoints.
18
+ - Add `zai-org/GLM-5.3` with its verified reasoning efforts and display pricing.
19
+ - Prefer Command Code's Provider API (`/provider/v1/chat/completions` and `/provider/v1/messages`) and automatically fall back to the existing `/alpha/generate` transport only when the Provider API returns `403 upgrade_required` for a Go-plan account.
20
+ - Remember the detected transport for the running process, re-detect it when credentials change, prevent stale in-flight requests from overwriting the new credential's transport, and never fall back for unrelated authentication, permission, rate-limit, network, or server failures.
21
+ - Use Pi's native OpenAI- and Anthropic-compatible providers for Provider API streaming, including adaptive thinking for current reasoning-capable Claude models, while preserving the existing hardened generate transport, dynamic model discovery, offline cache, refresh/status commands, pricing, and OAuth credentials.
22
+ - Let `/login` use browser authentication, an explicit API-key prompt, or a directly pasted API key.
23
+ - Add optional zero-data-retention headers through `CMD_ZDR=1` and the legacy `COMMANDCODE_ZDR=1` alias.
24
+ - Refresh GPT-5.6 Terra and Luna display prices after their temporary 50% promotion ended, and display the current DeepSeek V4 off-peak rates for its time-dependent pricing.
25
+ - Add isolated live E2E profiles for separate Go-, GOAT-, and Provider-plan credentials, covering transport selection, reasoning across turns, quota identity, aborts, tools, GOAT vision, Go image rejection, and packed-package validation.
26
+ - Fix extension load failure on newer pi hosts that reject registering a custom API under a built-in name (`openai-completions`); register under `commandcode-custom` instead and restore the real wire API before native compat dispatch.
27
+
28
+ ### Contributors
29
+
30
+ - @jagaliano — added the live quota dashboard and hardened its integration.
31
+ - @omariqbalnaru — fixed custom API registration for Oh My Pi 17.4.0.
32
+ - @ThomasByr — added GLM-5.3 pricing and reasoning levels.
33
+ - @newCman1 — added DeepSeek V4 vision model support.
34
+
35
+ ## 0.5.1 - 2026-08-11
36
+
37
+ - Add model-specific image input capabilities from the `command-code@1.15.1` catalog and forward user and tool-result images using the current Command Code wire format.
38
+ - Update the Command Code client version header to `1.15.1`.
39
+
40
+ ### Contributors
41
+
42
+ - @DiyarD — reported missing vision support for GPT-5.6 Luna, Muse Spark 1.2, and other vision-capable models.
43
+
5
44
  ## 0.5.0 - 2026-08-07
6
45
 
7
46
  - Stop replaying completed assistant reasoning traces to Command Code while preserving visible text and completed tool calls in follow-up request history.
package/CONTRIBUTING.md CHANGED
@@ -42,6 +42,16 @@ npm run pi:authenticated
42
42
 
43
43
  Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`.
44
44
 
45
+ Run the transport-specific live tests with separate credentials:
46
+
47
+ ```sh
48
+ COMMANDCODE_E2E_GO_API_KEY_FILE=/path/to/go-key npm run test:e2e:live:go
49
+ COMMANDCODE_E2E_GOAT_API_KEY_FILE=/path/to/goat-key npm run test:e2e:live:goat
50
+ COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key npm run test:e2e:live:provider
51
+ ```
52
+
53
+ Use `npm run test:e2e:live:all` with the Go and GOAT file variables to run both subscription transports sequentially. Store keys in a secret manager and export each one to a new mode-`0600` temporary file for the test; never add key files to the repository. Direct `*_API_KEY` variables are intended primarily for protected CI secrets.
54
+
45
55
  Before opening a PR, run:
46
56
 
47
57
  ```sh
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  A custom provider for [pi](https://github.com/earendil-works/pi) that connects to the [Command Code](https://commandcode.ai) Provider API.
7
7
 
8
- > **Disclaimer:** This is an unofficial, community-maintained integration. It is not affiliated with, endorsed by, or supported by Command Code. You need your own Command Code account and API key or subscription. Command Code's terms, availability, and pricing apply.
8
+ > **Disclaimer:** This is an unofficial, community-maintained integration. It is not affiliated with, endorsed by, or supported by Command Code. You need your own Command Code account, API key, and a plan with Provider API access. Command Code's terms, availability, and pricing apply.
9
9
 
10
10
  ## Install
11
11
 
@@ -19,7 +19,7 @@ Start or reload pi, then authenticate:
19
19
  /login
20
20
  ```
21
21
 
22
- Select **Use a subscription**, then **Command Code**. Complete the browser flow and choose a model with `/model`.
22
+ Select **Use a subscription**, then **Command Code**. Choose browser login or paste an API key, then select a model with `/model`.
23
23
 
24
24
  ## Oh My Pi
25
25
 
@@ -33,9 +33,9 @@ Restart OMP or run `/reload`, then use `/login` and select **Use a subscription*
33
33
 
34
34
  ## Authentication
35
35
 
36
- ### Browser login
36
+ ### Login dialog
37
37
 
38
- Run `/login` in pi or OMP. Select **Use a subscription**, then **Command Code**. The browser flow stores the returned credential in the host's auth file.
38
+ Run `/login` in pi or OMP. Select **Use a subscription**, then **Command Code**. Press Enter for browser login, type `key` to open a paste prompt, or paste the API key directly. The selected credential is stored in the host's auth file.
39
39
 
40
40
  <img width="1520" height="554" alt="Select Command Code in pi's login dialog" src="https://github.com/user-attachments/assets/071e929a-6f49-4803-bfec-7a31368fb12a" />
41
41
 
@@ -44,7 +44,7 @@ If automatic transfer from the browser fails, copy the API key shown by Command
44
44
  ### Environment variable
45
45
 
46
46
  ```sh
47
- export COMMANDCODE_API_KEY="user_..."
47
+ export COMMAND_CODE_API_KEY="user_..."
48
48
  ```
49
49
 
50
50
  ### Auth file
@@ -84,9 +84,7 @@ Open `/model` and select one of the models provided by Command Code. Model avail
84
84
 
85
85
  ### Reasoning support
86
86
 
87
- Reasoning metadata is enriched only for models whose Command Code effort support is known. Those models register a model-specific `thinkingLevelMap`, so pi and OMP expose only supported levels. A selected supported level is sent as the documented `params.reasoning_effort` field; `off`, unsupported levels, and newly discovered models without metadata do not add reasoning fields to the request. No prompt instructions are injected.
88
-
89
- Reasoning blocks from completed assistant turns remain visible in pi's local session, but are not replayed to Command Code in later requests. Only the assistant's user-visible text and completed tool calls are sent back as history. This matches the current Command Code CLI behavior and prevents prior private reasoning traces from interfering with reasoning on follow-up turns.
87
+ Reasoning capability and selectable effort levels follow the official CLI catalog independently. Models can therefore be marked as reasoning-capable even when Command Code chooses their depth automatically. Models with explicit effort support also register a model-specific `thinkingLevelMap`, so pi and OMP expose only valid levels. Pi's native OpenAI- and Anthropic-compatible providers translate the selected level for Provider API accounts; the existing Command Code generate transport sends the matching `reasoning_effort` for Go accounts.
90
88
 
91
89
  List Command Code models from the terminal:
92
90
 
@@ -122,6 +120,11 @@ While pi is running, use these provider commands without restarting:
122
120
 
123
121
  - `/commandcode-refresh` fetches and re-registers the current model catalog. Overlapping refreshes are coalesced, and a failed refresh keeps the last valid catalog active.
124
122
  - `/commandcode-status` shows redacted discovery diagnostics, including the source, model count, timestamps, cache path, endpoint, and warning.
123
+ - `/commandcode-quota` shows your Command Code account usage and quota in a dashboard-style layout: credits remaining and used with a percentage, monthly/purchased/free sources, the current plan, available usage totals, the API key name, and the 5-hour and weekly usage windows.
124
+
125
+ The `commandcode-quota` command reads from the Command Code alpha usage endpoints (the same ones the `cmd` CLI `/usage` command uses): `whoami`, `billing/credits`, `billing/subscriptions`, and `usage/summary`. It authenticates with the same API key the provider already uses. If the command cannot reach those endpoints or an endpoint schema changes, unavailable sections are reported explicitly instead of being displayed as zero usage. Output is plain text (via `ui.notify`) so it works across pi and compatible hosts such as OMP.
126
+
127
+ Set `CMD_ZDR=1` to send Command Code's documented `x-cmd-zdr: 1` zero-data-retention header. The legacy `COMMANDCODE_ZDR=1` alias remains supported.
125
128
 
126
129
  The following environment variables are intended for tests, local mocks, and compatible API endpoints:
127
130
 
@@ -132,15 +135,15 @@ The following environment variables are intended for tests, local mocks, and com
132
135
 
133
136
  ## Image input
134
137
 
135
- This provider currently advertises and accepts **text input only**. The extension uses Command Code's legacy `/alpha/generate` protocol, while the public Provider API documentation describes image parts for its documented `/provider/v1` endpoints. The legacy request path has no documented image-part contract, and the model catalog fixture exposes model IDs and context lengths but no image capability or limit fields.
138
+ The provider advertises image input only for models marked with the `image` input modality in the official Command Code CLI model catalog. The capability snapshot currently follows `command-code@1.32.2`; unknown models default to text-only until their upstream metadata is reviewed. A daily GitHub Actions job synchronizes the CLI version, image capabilities, reasoning flags, reasoning efforts, and model-specific output limits with the latest published CLI package and opens or updates a reviewable pull request when they change. Pricing remains manually reviewed because temporary promotions and long-context tiers require explicit review.
136
139
 
137
- To avoid silently dropping or changing image data, the provider rejects image content in user messages and tool results before making a network request. It does not claim image capability or define image-size/count limits. This limitation can be revisited when Command Code documents image parts and limits for the protocol used here.
140
+ For vision-capable models, Pi's native provider adapters forward image blocks from user messages and tool results using the documented OpenAI or Anthropic message schema. Unknown and text-only models remain marked text-only in Pi.
138
141
 
139
142
  ## Pricing display
140
143
 
141
- The Command Code Provider API does not currently include prices in its model catalog. This extension therefore keeps a static table for models with known prices so pi can display estimated request costs.
144
+ The Command Code Provider API does not currently include prices in its model catalog. This extension therefore keeps a static table for models with known prices so pi can display estimated request costs. DeepSeek V4 uses time-dependent rates; pi displays the documented off-peak rate, which applies for 17 hours per day.
142
145
 
143
- Models missing from that table display zero cost in pi. This does **not** mean that Command Code will bill the request at zero. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value.
146
+ Models missing from that table display zero cost in pi. This does **not** mean that Command Code will bill the request at zero. The Command Code Usage page remains authoritative for each request. Check the current [Command Code pricing](https://commandcode.ai/docs/resources/pricing-limits) before relying on the displayed value.
144
147
 
145
148
  ## Update and remove
146
149
 
@@ -181,6 +184,29 @@ npm run pi:authenticated
181
184
 
182
185
  Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`.
183
186
 
187
+ ### Live transport tests
188
+
189
+ Keep Go-, GOAT-, and optional Provider-plan test keys in separate secret-manager entries. Pass them through protected files so the keys do not enter shell history:
190
+
191
+ ```sh
192
+ COMMANDCODE_E2E_GO_API_KEY_FILE=/path/to/go-key \
193
+ npm run test:e2e:live:go
194
+
195
+ COMMANDCODE_E2E_GOAT_API_KEY_FILE=/path/to/goat-key \
196
+ npm run test:e2e:live:goat
197
+
198
+ COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key \
199
+ npm run test:e2e:live:provider
200
+
201
+ COMMANDCODE_E2E_GO_API_KEY_FILE=/path/to/go-key \
202
+ COMMANDCODE_E2E_GOAT_API_KEY_FILE=/path/to/goat-key \
203
+ npm run test:e2e:live:all
204
+ ```
205
+
206
+ Each profile runs with an isolated Pi agent directory and asserts transport selection, reasoning across turns, quota plan identity, abort handling, tool calls, and the packed npm artifact. Go must select `generate` and reject unsupported images; GOAT must select `provider` and complete a live vision request. The profile-specific `*_API_KEY` environment variables are also supported for CI secrets, but key files are preferred for local use.
207
+
208
+ The Go profile defaults to DeepSeek V4 Flash; GOAT defaults to Grok 4.6 because its Provider API stream exposes reasoning consistently across consecutive turns. Override them with `COMMANDCODE_E2E_GO_MODEL`, `COMMANDCODE_E2E_GOAT_MODEL`, or `COMMANDCODE_E2E_PROVIDER_MODEL`. A successful live Anthropic `/provider/v1/messages` test requires a paid account whose plan includes the selected Claude model.
209
+
184
210
  See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process.
185
211
 
186
212
  ## License
package/index.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Command Code provider for pi.
3
3
  *
4
- * Connects pi to Command Code's API (https://api.commandcode.ai/alpha/generate).
5
- * The provider uses pi's legacy extension registration surface because the
6
- * current pi host exposes `registerProvider(name, config)`, including OMP.
4
+ * Uses Command Code's documented Provider API:
5
+ * https://api.commandcode.ai/provider/v1
7
6
  */
8
7
 
9
8
  import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
9
+ import { streamSimple as streamNativeProvider } from "@earendil-works/pi-ai/compat"
10
10
  import {
11
11
  getAgentDir,
12
12
  type ExtensionAPI,
@@ -15,83 +15,109 @@ import {
15
15
  } from "@earendil-works/pi-coding-agent"
16
16
  import { join } from "node:path"
17
17
 
18
- import {
19
- COMMAND_CODE_CLI_VERSION,
20
- COMMAND_CODE_INPUT_TYPES,
21
- createStreamCommandCode,
22
- DEFAULT_API_BASE,
23
- } from "./src/core.ts"
18
+ import { getConfiguredApiKey } from "./src/api-key.ts"
19
+ import { createStreamCommandCode } from "./src/core.ts"
24
20
  import { calculateCommandCodeCost } from "./src/cost.ts"
25
21
  import {
22
+ apiForModelId,
23
+ baseUrlForModel,
26
24
  DEFAULT_MODELS_URL,
25
+ DEFAULT_PROVIDER_API_BASE,
27
26
  getModelsTimeoutMs,
27
+ inputModalitiesForModel,
28
28
  loadCommandCodeModels,
29
+ MODEL_EFFORTS,
29
30
  thinkingMetadataForModel,
30
31
  type CommandCodeModel,
31
32
  } from "./src/models.ts"
32
33
  import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
34
+ import { normalizeCommandCodeMessage } from "./src/overflow.ts"
33
35
  import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts"
36
+ import { registerCommandCodeQuota } from "./src/quota-command.ts"
34
37
  import { createCommandCodeRuntime } from "./src/runtime.ts"
35
- import { normalizeCommandCodeMessage } from "./src/overflow.ts"
38
+ import { createCommandCodeTransportRouter } from "./src/transport.ts"
39
+
40
+ function commandCodeHeaders(): Record<string, string> | undefined {
41
+ if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
42
+ return { "x-cmd-zdr": "1" }
43
+ }
44
+ return undefined
45
+ }
36
46
 
37
47
  function createProviderConfig(
38
48
  models: readonly CommandCodeModel[],
39
49
  apiBase: string,
40
50
  streamCommandCode: ProviderConfig["streamSimple"],
41
51
  ): ProviderConfig {
52
+ const headers = commandCodeHeaders()
42
53
  return {
43
54
  name: "Command Code",
44
55
  baseUrl: apiBase,
45
- // Keep environment authentication dynamic. OAuth credentials are resolved
46
- // by pi's oauth registration, while the custom stream retains its own
47
- // request-time legacy-file fallback for older compatible hosts.
48
- apiKey: "$COMMANDCODE_API_KEY",
49
- authHeader: true,
56
+ apiKey: getConfiguredApiKey() ?? "$COMMAND_CODE_API_KEY",
50
57
  api: "commandcode-custom",
51
58
  streamSimple: streamCommandCode,
52
- headers: {
53
- "x-command-code-version": COMMAND_CODE_CLI_VERSION,
54
- "x-cli-environment": "production",
55
- },
59
+ headers,
56
60
  oauth: {
57
61
  name: "Command Code",
58
62
  login,
59
63
  refreshToken,
60
64
  getApiKey: getOAuthApiKey,
61
65
  },
62
- models: models.map(createProviderModel),
66
+ models: models.map((model) => ({
67
+ id: model.id,
68
+ name: model.name,
69
+ api: "commandcode-custom",
70
+ baseUrl: baseUrlForModel(apiBase, model.api),
71
+ reasoning: model.reasoning,
72
+ ...(thinkingMetadataForModel(model.id) ?? {}),
73
+ input: [...inputModalitiesForModel(model.id)],
74
+ cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
75
+ contextWindow: model.contextWindow,
76
+ maxTokens: model.maxTokens,
77
+ headers,
78
+ compat:
79
+ model.api === "openai-completions"
80
+ ? {
81
+ supportsStore: false,
82
+ supportsDeveloperRole: false,
83
+ supportsReasoningEffort: MODEL_EFFORTS[model.id] !== undefined,
84
+ maxTokensField: "max_tokens",
85
+ }
86
+ : {
87
+ supportsEagerToolInputStreaming: false,
88
+ supportsLongCacheRetention: false,
89
+ supportsCacheControlOnTools: false,
90
+ supportsToolReferences: false,
91
+ ...(model.reasoning ? { forceAdaptiveThinking: true } : {}),
92
+ },
93
+ })),
63
94
  }
64
95
  }
65
96
 
66
- function createProviderModel(model: {
67
- id: string
68
- name: string
69
- reasoning: boolean
70
- contextWindow: number
71
- maxTokens: number
72
- }) {
73
- return {
74
- id: model.id,
75
- name: model.name,
76
- reasoning: model.reasoning,
77
- ...(thinkingMetadataForModel(model.id) ?? {}),
78
- input: COMMAND_CODE_INPUT_TYPES,
79
- cost: MODEL_COSTS[model.id] ?? ZERO_MODEL_COST,
80
- contextWindow: model.contextWindow,
81
- maxTokens: model.maxTokens,
82
- } as const
97
+ function legacyApiBase(providerApiBase: string): string {
98
+ return providerApiBase.replace(/\/provider\/v1\/?$/, "")
83
99
  }
84
100
 
85
101
  export default async function (pi: ExtensionAPI) {
86
- const apiBase = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
102
+ const apiBase = process.env.COMMANDCODE_API_BASE ?? DEFAULT_PROVIDER_API_BASE
87
103
  const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
88
104
  const modelsTimeoutMs = getModelsTimeoutMs()
89
105
  const modelsCachePath =
90
106
  process.env.COMMANDCODE_MODELS_CACHE ?? join(getAgentDir(), "commandcode-models.json")
91
- const streamCommandCode = createStreamCommandCode({
107
+ const streamGenerate = createStreamCommandCode({
92
108
  createStream: () => new AssistantMessageEventStream(),
93
109
  calculateCost: calculateCommandCodeCost,
94
- apiBase,
110
+ apiBase: legacyApiBase(apiBase),
111
+ })
112
+ const transport = createCommandCodeTransportRouter({
113
+ createStream: () => new AssistantMessageEventStream(),
114
+ streamProvider: (model, context, options) =>
115
+ streamNativeProvider(
116
+ { ...model, api: apiForModelId(model.id), compat: model.compatConfig ?? model.compat },
117
+ context,
118
+ options,
119
+ ),
120
+ streamGenerate,
95
121
  })
96
122
 
97
123
  pi.on("message_end", async (event, ctx) => {
@@ -100,6 +126,11 @@ export default async function (pi: ExtensionAPI) {
100
126
  return normalized ? { message: normalized.message } : undefined
101
127
  })
102
128
 
129
+ registerCommandCodeQuota(pi, {
130
+ apiBase: legacyApiBase(apiBase),
131
+ headers: commandCodeHeaders(),
132
+ })
133
+
103
134
  const runtime = createCommandCodeRuntime<ProviderConfig, ExtensionCommandContext>(pi, {
104
135
  endpoint: modelsUrl,
105
136
  cachePath: modelsCachePath,
@@ -109,7 +140,8 @@ export default async function (pi: ExtensionAPI) {
109
140
  cachePath: modelsCachePath,
110
141
  timeoutMs: modelsTimeoutMs,
111
142
  }),
112
- createProviderConfig: (models) => createProviderConfig(models, apiBase, streamCommandCode),
143
+ createProviderConfig: (models) => createProviderConfig(models, apiBase, transport.stream),
144
+ getTransport: transport.getTransport,
113
145
  })
114
146
 
115
147
  await runtime.initialize()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-commandcode-provider",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "pi custom provider for Command Code API (commandcode.ai)",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -29,14 +29,18 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "scripts": {
32
- "test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
32
+ "test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
35
35
  "format": "prettier --write '**/*.{ts,mjs,json,md}'",
36
36
  "pi:isolated": "node scripts/pi-isolated.mjs",
37
37
  "pi:authenticated": "node scripts/pi-authenticated.mjs",
38
- "test:unit": "tsx tests/test-pure-functions.ts",
39
- "test:models": "tsx tests/test-models.ts",
38
+ "check:commandcode-catalog": "tsx .github/scripts/check-commandcode-model-metadata.ts command-code@latest",
39
+ "sync:commandcode-catalog": "tsx .github/scripts/check-commandcode-model-metadata.ts command-code@latest --write",
40
+ "test:quota": "tsx tests/test-quota.ts && tsx tests/test-quota-command.ts",
41
+ "test:unit": "tsx tests/test-api-key.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts && tsx tests/test-runtime.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-overflow.ts && tsx tests/test-stream.ts && tsx tests/test-quota.ts && tsx tests/test-quota-command.ts && tsx tests/test-retry.ts && tsx tests/test-transport.ts",
42
+ "test:api-key": "tsx tests/test-api-key.ts",
43
+ "test:models": "tsx tests/test-models.ts && tsx tests/test-model-metadata-check.ts",
40
44
  "test:runtime": "tsx tests/test-runtime.ts",
41
45
  "test:pricing": "tsx tests/test-pricing.ts",
42
46
  "test:oauth": "tsx tests/test-oauth.ts",
@@ -44,11 +48,16 @@
44
48
  "test:overflow": "tsx tests/test-overflow.ts",
45
49
  "test:stream": "tsx tests/test-stream.ts",
46
50
  "test:retry": "tsx tests/test-retry.ts",
51
+ "test:transport": "tsx tests/test-transport.ts",
47
52
  "test:pi-isolated": "node tests/test-pi-isolated.mjs",
48
53
  "test:pi-authenticated": "node tests/test-pi-authenticated.mjs",
49
54
  "test:pi-local": "node tests/test-pi-local.mjs",
50
55
  "test:smoke": "node tests/test-smoke.mjs",
51
56
  "test:e2e:live": "node tests/test-live-e2e.mjs",
57
+ "test:e2e:live:go": "node scripts/live-e2e-profile.mjs go",
58
+ "test:e2e:live:goat": "node scripts/live-e2e-profile.mjs goat",
59
+ "test:e2e:live:provider": "node scripts/live-e2e-profile.mjs provider",
60
+ "test:e2e:live:all": "node scripts/live-e2e-profile.mjs go goat",
52
61
  "test:cost": "tsx tests/test-cost.ts"
53
62
  },
54
63
  "pi": {
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process"
4
+ import { readFile } from "node:fs/promises"
5
+ import { dirname, resolve } from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
9
+ const liveTest = resolve(projectDir, "tests", "test-live-e2e.mjs")
10
+ const profiles = process.argv.slice(2)
11
+
12
+ if (
13
+ profiles.length === 0 ||
14
+ profiles.some((profile) => profile !== "go" && profile !== "goat" && profile !== "provider")
15
+ ) {
16
+ console.error("Usage: node scripts/live-e2e-profile.mjs <go|goat|provider> [go|goat|provider]")
17
+ process.exit(2)
18
+ }
19
+
20
+ async function credentialFor(profile) {
21
+ const prefix =
22
+ profile === "go"
23
+ ? "COMMANDCODE_E2E_GO"
24
+ : profile === "goat"
25
+ ? "COMMANDCODE_E2E_GOAT"
26
+ : "COMMANDCODE_E2E_PROVIDER"
27
+ const direct = process.env[`${prefix}_API_KEY`]?.trim()
28
+ const file = process.env[`${prefix}_API_KEY_FILE`]
29
+
30
+ if (direct && file)
31
+ throw new Error(`${prefix}_API_KEY and ${prefix}_API_KEY_FILE are mutually exclusive`)
32
+ if (direct) return direct
33
+ if (file) {
34
+ const credential = (await readFile(file, "utf-8")).trim()
35
+ if (credential) return credential
36
+ }
37
+
38
+ throw new Error(`Set ${prefix}_API_KEY_FILE (recommended) or ${prefix}_API_KEY`)
39
+ }
40
+
41
+ function runProfile(profile, apiKey) {
42
+ const modelVariable =
43
+ profile === "go"
44
+ ? "COMMANDCODE_E2E_GO_MODEL"
45
+ : profile === "goat"
46
+ ? "COMMANDCODE_E2E_GOAT_MODEL"
47
+ : "COMMANDCODE_E2E_PROVIDER_MODEL"
48
+ const model =
49
+ process.env[modelVariable] ??
50
+ (profile === "goat" ? "xai/grok-4.6" : "deepseek/deepseek-v4-flash")
51
+ const env = {
52
+ ...process.env,
53
+ COMMAND_CODE_API_KEY: apiKey,
54
+ COMMANDCODE_E2E_MODEL: model,
55
+ COMMANDCODE_E2E_PROFILE: profile,
56
+ }
57
+ delete env.COMMANDCODE_API_KEY
58
+ delete env.COMMANDCODE_E2E_GO_API_KEY
59
+ delete env.COMMANDCODE_E2E_GOAT_API_KEY
60
+ delete env.COMMANDCODE_E2E_PROVIDER_API_KEY
61
+
62
+ return new Promise((resolveRun, reject) => {
63
+ console.log(`[live-e2e:${profile}] model ${model}`)
64
+ const child = spawn(process.execPath, [liveTest], {
65
+ cwd: projectDir,
66
+ env,
67
+ stdio: "inherit",
68
+ })
69
+ child.on("error", reject)
70
+ child.on("close", (code, signal) => {
71
+ if (code === 0) {
72
+ resolveRun()
73
+ return
74
+ }
75
+ reject(new Error(`[live-e2e:${profile}] failed (${signal ?? `exit ${code}`})`))
76
+ })
77
+ })
78
+ }
79
+
80
+ try {
81
+ for (const profile of profiles) {
82
+ await runProfile(profile, await credentialFor(profile))
83
+ }
84
+ } catch (error) {
85
+ console.error(error instanceof Error ? error.message : String(error))
86
+ process.exit(1)
87
+ }
@@ -11,6 +11,7 @@ const env = {
11
11
  ...process.env,
12
12
  PI_SKIP_VERSION_CHECK: "1",
13
13
  }
14
+ delete env.COMMAND_CODE_API_KEY
14
15
  delete env.COMMANDCODE_API_KEY
15
16
 
16
17
  const child = spawn(
@@ -22,6 +22,7 @@ const env = {
22
22
  PI_CODING_AGENT_SESSION_DIR: sessionDir,
23
23
  PI_SKIP_VERSION_CHECK: "1",
24
24
  }
25
+ delete env.COMMAND_CODE_API_KEY
25
26
  delete env.COMMANDCODE_API_KEY
26
27
 
27
28
  let activeChild
package/src/api-key.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { homedir } from "node:os"
3
+ import { join } from "node:path"
4
+
5
+ function isRecord(value: unknown): value is Record<string, unknown> {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value)
7
+ }
8
+
9
+ function stringValue(value: unknown): string | undefined {
10
+ return typeof value === "string" ? value : undefined
11
+ }
12
+
13
+ function defaultAuthPaths(home: string): string[] {
14
+ return [
15
+ join(home, ".commandcode", "auth.json"),
16
+ join(home, ".pi", "agent", "auth.json"),
17
+ join(home, ".omp", "agent", "auth.json"),
18
+ ]
19
+ }
20
+
21
+ function apiKeyFromCredential(value: unknown): string | undefined {
22
+ if (!isRecord(value)) return undefined
23
+
24
+ if (stringValue(value.type) === "oauth") return stringValue(value.access)
25
+ if (stringValue(value.type) === "api") return stringValue(value.key)
26
+ return stringValue(value.access) ?? stringValue(value.key)
27
+ }
28
+
29
+ export function getConfiguredApiKey(
30
+ options: {
31
+ env?: NodeJS.ProcessEnv
32
+ authPaths?: readonly string[]
33
+ homeDir?: () => string
34
+ } = {},
35
+ ): string | undefined {
36
+ const env = options.env ?? process.env
37
+ if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY
38
+ if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY
39
+
40
+ const home = options.homeDir?.() ?? homedir()
41
+ const authPaths = options.authPaths ?? defaultAuthPaths(home)
42
+
43
+ for (const authPath of authPaths) {
44
+ try {
45
+ if (!existsSync(authPath)) continue
46
+ const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
47
+ if (!isRecord(parsed)) continue
48
+
49
+ const apiKey = stringValue(parsed.apiKey)
50
+ if (apiKey) return apiKey
51
+
52
+ const commandcode = stringValue(parsed.commandcode)
53
+ if (commandcode) return commandcode
54
+
55
+ const providerKey = apiKeyFromCredential(parsed.commandcode)
56
+ if (providerKey) return providerKey
57
+
58
+ const commandCode = stringValue(parsed["command-code"])
59
+ if (commandCode) return commandCode
60
+
61
+ const commandCodeKey = apiKeyFromCredential(parsed["command-code"])
62
+ if (commandCodeKey) return commandCodeKey
63
+ } catch {
64
+ // Ignore malformed or unreadable auth files.
65
+ }
66
+ }
67
+
68
+ return undefined
69
+ }
@@ -28,6 +28,7 @@ export interface AuthServer {
28
28
  export interface AuthServerOptions {
29
29
  startPort?: number
30
30
  portRange?: number
31
+ expectedState?: string
31
32
  }
32
33
 
33
34
  function listenOnAvailablePort(
@@ -181,6 +182,12 @@ export async function startAuthServer(options: AuthServerOptions = {}): Promise<
181
182
  return
182
183
  }
183
184
 
185
+ if (options.expectedState !== undefined && state !== options.expectedState) {
186
+ res.writeHead(403)
187
+ res.end(JSON.stringify({ success: false, error: "Invalid state token" }))
188
+ return
189
+ }
190
+
184
191
  res.writeHead(200)
185
192
  res.end(JSON.stringify({ success: true }))
186
193