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 +19 -0
- package/CONTRIBUTING.md +15 -0
- package/README.md +2 -0
- package/index.ts +61 -19
- package/package.json +1 -1
- package/src/converters.ts +21 -4
- package/src/core.ts +5 -12
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
|
-
|
|
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:
|
|
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:
|
|
139
|
-
transport.stream(
|
|
140
|
-
|
|
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
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
241
|
-
|
|
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 = {
|