pi-commandcode-provider 0.6.1 → 0.6.3

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,25 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.3 - 2026-09-02
6
+
7
+ - Fix Oh My Pi chat returning `401 Invalid 'Authorization' header` after `/login`: OMP kept the unresolved `$COMMAND_CODE_API_KEY` placeholder as a literal config API key that shadowed its stored credentials and was sent as the Bearer token. The placeholder is now registered only on pi, where it keeps the API-key login method and `--api-key` working next to OAuth; on OMP the provider omits `apiKey` unless a real key is configured. Placeholders passed by the host are also resolved or stripped on the Provider API and compat stream paths, and the legacy generate transport resolves its key through the same rule.
8
+ - Cover stored `/login` OAuth and API-key credentials, `--api-key`, and env keys end to end on both pi and Oh My Pi, asserting the exact Bearer token the mock API receives. CI now runs the pi end-to-end suite against a real `pi` binary instead of skipping it.
9
+
10
+ ### Contributors
11
+
12
+ - @ebreen — reported and diagnosed the Oh My Pi `/login` 401, and opened the fix that this release builds on (#78).
13
+
14
+ ## 0.6.2 - 2026-09-02
15
+
16
+ - Fix `omp plugin install` on Oh My Pi 18.x, which rejected 0.6.1 because its pi-ai lacks the `registerApiProvider` export; the compat registration now resolves at runtime and is skipped on hosts that register custom APIs themselves.
17
+ - Run the Oh My Pi compatibility suite against a real `omp` binary in CI as a required check, and assert there that the extension loads against OMP's bundled pi packages.
18
+ - Pin the CI memory benchmark and Oh My Pi jobs to Bun 1.4.0, Node 22.23.2, and pi 0.84.4.
19
+
20
+ ### Contributors
21
+
22
+ - @AmeMizuki — reported the failing `omp plugin install` on Oh My Pi 18.1.2.
23
+
5
24
  ## 0.6.1 - 2026-09-01
6
25
 
7
26
  - Expose selectable thinking levels (`minimal`, `low`, `medium`, `high`, `xhigh`) for `meta/muse-spark-1.1`, `meta/muse-spark-1.2`, and `meta/muse-spark-1.2-contributor` through a manual catalog override, so `/thinking` and `Shift+Tab` no longer stay locked on `off` for these reasoning models.
package/CONTRIBUTING.md CHANGED
@@ -52,6 +52,21 @@ COMMANDCODE_E2E_PROVIDER_API_KEY_FILE=/path/to/provider-key npm run test:e2e:liv
52
52
 
53
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
54
 
55
+ ### pi end-to-end
56
+
57
+ `tests/test-pi-local.mjs` runs the extension inside a real `pi` binary against a mock Command Code API, including every credential source (`/login` OAuth and API-key credentials, `--api-key`, env keys). It skips locally when `pi` is not on `PATH`; CI installs pi and runs it as part of `npm test` with `PI_LOCAL_REQUIRED=1`. Point `PI_BIN` at another pi executable to test against a specific version.
58
+
59
+ ### Oh My Pi compatibility
60
+
61
+ `tests/test-omp-compat.mjs` runs the extension inside a real `omp` binary against a mock Command Code API. It skips locally when `omp` is not on `PATH`; CI installs Oh My Pi and runs it as a required check with `OMP_COMPAT_REQUIRED=1`, so a change that only loads on pi fails CI instead of the next `omp plugin install`.
62
+
63
+ To run it locally, point `OMP_BIN` at an omp executable (Oh My Pi needs Bun ≥ 1.3.14):
64
+
65
+ ```sh
66
+ npm install -g @oh-my-pi/pi-coding-agent
67
+ OMP_BIN="$(npm prefix -g)/bin/omp" node tests/test-omp-compat.mjs
68
+ ```
69
+
55
70
  Before opening a PR, run:
56
71
 
57
72
  ```sh
package/README.md CHANGED
@@ -41,6 +41,8 @@ Run `/login` in pi or OMP. Select **Use a subscription**, then **Command Code**.
41
41
 
42
42
  If automatic transfer from the browser fails, copy the API key shown by Command Code and paste it into the terminal prompt.
43
43
 
44
+ On Oh My Pi, `/login` stores those credentials in OMP's credential store and chat uses them directly. If chat still returns `401 Invalid 'Authorization' header`, restart OMP after `/login` and confirm `/commandcode-quota` shows your account.
45
+
44
46
  ### Environment variable
45
47
 
46
48
  ```sh
package/index.ts CHANGED
@@ -6,11 +6,8 @@
6
6
  */
7
7
 
8
8
  import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
9
- import {
10
- registerApiProvider,
11
- streamSimple as streamNativeProvider,
12
- type ApiStreamSimpleFunction,
13
- } from "@earendil-works/pi-ai/compat"
9
+ import * as piAiCompat from "@earendil-works/pi-ai/compat"
10
+ import { streamSimple as streamNativeProvider } from "@earendil-works/pi-ai/compat"
14
11
  import {
15
12
  getAgentDir,
16
13
  type ExtensionAPI,
@@ -20,6 +17,7 @@ import {
20
17
  import { join } from "node:path"
21
18
 
22
19
  import { getConfiguredApiKey } from "./src/api-key.ts"
20
+ import { pickCommandCodeApiKey, withResolvedCommandCodeApiKey } from "./src/converters.ts"
23
21
  import { createStreamCommandCode } from "./src/core.ts"
24
22
  import { calculateCommandCodeCost } from "./src/cost.ts"
25
23
  import {
@@ -45,6 +43,54 @@ import { createCommandCodeTransportRouter } from "./src/transport.ts"
45
43
  const COMMAND_CODE_API = "commandcode-custom"
46
44
  const COMPAT_SOURCE_ID = "pi-commandcode-provider"
47
45
 
46
+ type CompatStreamFunction = (
47
+ model: Parameters<typeof streamNativeProvider>[0],
48
+ context: Parameters<typeof streamNativeProvider>[1],
49
+ options?: Parameters<typeof streamNativeProvider>[2],
50
+ ) => AssistantMessageEventStream
51
+
52
+ /**
53
+ * pi's compat entrypoint exposes `registerApiProvider`; Oh My Pi maps
54
+ * `@earendil-works/pi-ai/compat` onto its own pi-ai, which lacks that export
55
+ * and registers custom APIs itself inside `registerProvider`. Resolve the
56
+ * function at runtime so the extension loads on both hosts.
57
+ */
58
+ function compatApiProviderRegistrar(): ((...args: unknown[]) => unknown) | undefined {
59
+ const register = (piAiCompat as { registerApiProvider?: unknown }).registerApiProvider
60
+ return typeof register === "function" ? (register as (...args: unknown[]) => unknown) : undefined
61
+ }
62
+
63
+ function registerCompatApiProvider(stream: CompatStreamFunction): void {
64
+ compatApiProviderRegistrar()?.(
65
+ { api: COMMAND_CODE_API, stream, streamSimple: stream },
66
+ COMPAT_SOURCE_ID,
67
+ )
68
+ }
69
+
70
+ /**
71
+ * The `apiKey` handed to `registerProvider` means different things per host.
72
+ *
73
+ * pi parses `$COMMAND_CODE_API_KEY` as an env template: unresolved means
74
+ * "not configured", so `/login` credentials and `--api-key` take over, and
75
+ * the entry keeps the API-key auth method registered next to OAuth. Without
76
+ * it pi composes an OAuth-only provider and drops stored `api_key`
77
+ * credentials and `--api-key`.
78
+ *
79
+ * Oh My Pi has no template notion: an unresolved value stays a literal config
80
+ * override that shadows its `/login` credential store and is sent verbatim as
81
+ * `Authorization: Bearer $COMMAND_CODE_API_KEY`. There, omit `apiKey` unless
82
+ * a real key is configured; OMP then reads env keys and stored credentials
83
+ * itself.
84
+ *
85
+ * Hosts are told apart by the same `registerApiProvider` probe used for the
86
+ * compat registry: pi exports it, OMP does not.
87
+ */
88
+ function providerApiKey(): string | undefined {
89
+ const configured = pickCommandCodeApiKey(getConfiguredApiKey(), undefined)
90
+ if (configured) return configured
91
+ return compatApiProviderRegistrar() ? "$COMMAND_CODE_API_KEY" : undefined
92
+ }
93
+
48
94
  function commandCodeHeaders(): Record<string, string> | undefined {
49
95
  if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
50
96
  return { "x-cmd-zdr": "1" }
@@ -61,7 +107,7 @@ function createProviderConfig(
61
107
  return {
62
108
  name: "Command Code",
63
109
  baseUrl: apiBase,
64
- apiKey: getConfiguredApiKey() ?? "$COMMAND_CODE_API_KEY",
110
+ apiKey: providerApiKey(),
65
111
  api: COMMAND_CODE_API,
66
112
  streamSimple: streamCommandCode,
67
113
  headers,
@@ -117,15 +163,18 @@ export default async function (pi: ExtensionAPI) {
117
163
  calculateCost: calculateCommandCodeCost,
118
164
  apiBase: legacyApiBase(apiBase),
119
165
  })
166
+ const resolveStreamOptions = (options?: Parameters<typeof streamNativeProvider>[2]) =>
167
+ withResolvedCommandCodeApiKey(options, getConfiguredApiKey())
120
168
  const transport = createCommandCodeTransportRouter({
121
169
  createStream: () => new AssistantMessageEventStream(),
122
170
  streamProvider: (model, context, options) =>
123
171
  streamNativeProvider(
124
172
  { ...model, api: apiForModelId(model.id), compat: model.compatConfig ?? model.compat },
125
173
  context,
126
- options,
174
+ resolveStreamOptions(options),
127
175
  ),
128
- streamGenerate,
176
+ streamGenerate: (model, context, options) =>
177
+ streamGenerate(model, context, resolveStreamOptions(options)),
129
178
  })
130
179
 
131
180
  // pi dispatches the main chat through the registered provider, but sibling
@@ -134,17 +183,10 @@ export default async function (pi: ExtensionAPI) {
134
183
  // api-registry, which knows nothing about extension providers. Register the
135
184
  // custom api there so those calls reach the same transport. The registry
136
185
  // resolves no credentials for extension providers, so fall back to the
137
- // configured key when the caller passes none.
138
- const compatStream: ApiStreamSimpleFunction = (model, context, options) =>
139
- transport.stream(
140
- model,
141
- context,
142
- options?.apiKey ? options : { ...options, apiKey: getConfiguredApiKey() },
143
- ) as AssistantMessageEventStream
144
- registerApiProvider(
145
- { api: COMMAND_CODE_API, stream: compatStream, streamSimple: compatStream },
146
- COMPAT_SOURCE_ID,
147
- )
186
+ // configured key when the caller passes none or a placeholder.
187
+ const compatStream: CompatStreamFunction = (model, context, options) =>
188
+ transport.stream(model, context, resolveStreamOptions(options)) as AssistantMessageEventStream
189
+ registerCompatApiProvider(compatStream)
148
190
 
149
191
  pi.on("message_end", async (event, ctx) => {
150
192
  if (event.message.role !== "assistant") return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-commandcode-provider",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "pi custom provider for Command Code API (commandcode.ai)",
5
5
  "type": "module",
6
6
  "keywords": [
package/src/converters.ts CHANGED
@@ -147,6 +147,13 @@ export const COMMAND_CODE_PLACEHOLDER_KEYS = new Set([
147
147
  "COMMANDCODE_API_KEY",
148
148
  ])
149
149
 
150
+ function usableCommandCodeApiKey(value: string | undefined): string | undefined {
151
+ const trimmed = typeof value === "string" ? value.trim() : undefined
152
+ if (!trimmed) return undefined
153
+ if (COMMAND_CODE_PLACEHOLDER_KEYS.has(trimmed)) return undefined
154
+ return trimmed
155
+ }
156
+
150
157
  /**
151
158
  * Pick the real API key from a host registry value and/or the env/auth-file
152
159
  * fallback, never returning a literal placeholder or an empty/whitespace value.
@@ -156,10 +163,20 @@ export function pickCommandCodeApiKey(
156
163
  registryKey: string | undefined,
157
164
  hostKey: string | undefined,
158
165
  ): string | undefined {
159
- const trimmed = typeof registryKey === "string" ? registryKey.trim() : undefined
160
- if (!trimmed) return hostKey
161
- if (COMMAND_CODE_PLACEHOLDER_KEYS.has(trimmed)) return hostKey
162
- return trimmed
166
+ return usableCommandCodeApiKey(registryKey) ?? usableCommandCodeApiKey(hostKey)
167
+ }
168
+
169
+ /**
170
+ * Replace a host-supplied placeholder (or missing key) with the configured
171
+ * fallback. Used for both registerProvider and the Provider API stream path.
172
+ */
173
+ export function withResolvedCommandCodeApiKey<T extends { apiKey?: string }>(
174
+ options: T | undefined,
175
+ configuredKey: string | undefined,
176
+ ): T | { apiKey?: string } {
177
+ const apiKey = pickCommandCodeApiKey(options?.apiKey, configuredKey)
178
+ if (options && apiKey === options.apiKey) return options
179
+ return { ...options, apiKey }
163
180
  }
164
181
 
165
182
  export function textContent(message: { content?: unknown }): string {
package/src/core.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  messagesToCC,
20
20
  numberValue,
21
21
  parseStreamEventLine,
22
+ pickCommandCodeApiKey,
22
23
  recordOrEmpty,
23
24
  stringValue,
24
25
  toolsToJson,
@@ -237,22 +238,14 @@ export function createStreamCommandCode(deps: CoreDependencies) {
237
238
 
238
239
  async function run() {
239
240
  // Some hosts pass a literal env-var reference instead of resolving it.
240
- const PLACEHOLDER_API_KEYS = new Set([
241
- "$COMMAND_CODE_API_KEY",
242
- "COMMAND_CODE_API_KEY",
243
- "$COMMANDCODE_API_KEY",
244
- "COMMANDCODE_API_KEY",
245
- ])
246
- const hostKey =
247
- options?.apiKey && !PLACEHOLDER_API_KEYS.has(options.apiKey) ? options.apiKey : undefined
248
-
249
- const apiKey =
250
- hostKey ??
241
+ const apiKey = pickCommandCodeApiKey(
242
+ options?.apiKey,
251
243
  getApiKey({
252
244
  env: deps.env,
253
245
  authPaths: deps.authPaths,
254
246
  homeDir: deps.homeDir,
255
- })
247
+ }),
248
+ )
256
249
 
257
250
  if (!apiKey) {
258
251
  const msg: AssistantMessageLike = {