Release 0.1.1
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.1 - 2026-05-26
|
||||
|
||||
- Align Command Code generate requests with CLI `0.27.2` headers and payload shape.
|
||||
- Support official Command Code CLI auth files using the `command-code` credential key.
|
||||
- Handle `reasoning-start` and ignore streamed `tool-result` events.
|
||||
- Cap generated `max_tokens` by the selected model and the Command Code output limit.
|
||||
|
||||
## 0.1.0 - 2026-05-05
|
||||
|
||||
- Initial public release.
|
||||
@@ -72,6 +72,17 @@ Create `~/.commandcode/auth.json`:
|
||||
}
|
||||
```
|
||||
|
||||
The official Command Code CLI auth shape is also supported:
|
||||
|
||||
```json
|
||||
{
|
||||
"command-code": {
|
||||
"type": "api",
|
||||
"key": "user_..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or use pi's auth file at `~/.pi/agent/auth.json`:
|
||||
|
||||
```json
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai"
|
||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
||||
|
||||
import { createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
|
||||
import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
|
||||
import { DEFAULT_MODELS_URL, fetchCommandCodeModels } from "./src/models.ts"
|
||||
import { getApiKey, login, refreshToken } from "./src/oauth.ts"
|
||||
|
||||
@@ -43,7 +43,7 @@ export default async function (pi: ExtensionAPI) {
|
||||
api: "commandcode-custom",
|
||||
streamSimple: streamCommandCode,
|
||||
headers: {
|
||||
"x-command-code-version": "0.24.1",
|
||||
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
||||
"x-cli-environment": "production",
|
||||
},
|
||||
oauth: {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-commandcode-provider",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-commandcode-provider",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "0.72.0"
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pi-commandcode-provider",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "pi custom provider for Command Code API (commandcode.ai)",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
@@ -22,6 +22,7 @@
|
||||
"index.ts",
|
||||
"src/",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
|
||||
+17
-7
@@ -42,6 +42,16 @@ function defaultAuthPaths(home: string): string[] {
|
||||
return [join(home, ".commandcode", "auth.json"), join(home, ".pi", "agent", "auth.json")]
|
||||
}
|
||||
|
||||
function apiKeyFromCredentialRecord(value: unknown): string | undefined {
|
||||
if (!isRecord(value)) return undefined
|
||||
|
||||
const type = stringValue(value.type)
|
||||
if (type === "api") return stringValue(value.key)
|
||||
if (type === "oauth") return stringValue(value.access)
|
||||
|
||||
return stringValue(value.key) ?? stringValue(value.access)
|
||||
}
|
||||
|
||||
export function getApiKey(
|
||||
options: {
|
||||
env?: NodeJS.ProcessEnv
|
||||
@@ -61,18 +71,18 @@ export function getApiKey(
|
||||
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
|
||||
if (!isRecord(parsed)) continue
|
||||
|
||||
// Legacy: direct apiKey or commandcode field
|
||||
// Legacy: direct apiKey or commandcode field.
|
||||
const apiKey = stringValue(parsed.apiKey)
|
||||
if (apiKey) return apiKey
|
||||
const commandcode = stringValue(parsed.commandcode)
|
||||
if (commandcode) return commandcode
|
||||
|
||||
// OAuth: pi stores OAuth credentials as {"commandcode": {"type":"oauth","access":"...","refresh":"...","expires":...}}
|
||||
const providerKey = isRecord(parsed.commandcode) ? parsed.commandcode : undefined
|
||||
if (providerKey && stringValue(providerKey.type) === "oauth") {
|
||||
const access = stringValue(providerKey.access)
|
||||
if (access) return access
|
||||
}
|
||||
// pi stores OAuth credentials as {"commandcode": {"type":"oauth","access":"..."}}.
|
||||
// The official Command Code CLI stores API credentials under "command-code".
|
||||
const providerKey =
|
||||
apiKeyFromCredentialRecord(parsed.commandcode) ??
|
||||
apiKeyFromCredentialRecord(parsed["command-code"])
|
||||
if (providerKey) return providerKey
|
||||
} catch {
|
||||
// Ignore malformed or unreadable auth files.
|
||||
}
|
||||
|
||||
+42
-9
@@ -38,6 +38,9 @@ export * from "./converters.ts"
|
||||
export * from "./types.ts"
|
||||
|
||||
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
||||
export const COMMAND_CODE_CLI_VERSION = "0.27.2"
|
||||
|
||||
const DEFAULT_GENERATE_MAX_TOKENS = 64_000
|
||||
|
||||
function defaultUsage(): Usage {
|
||||
return {
|
||||
@@ -77,6 +80,23 @@ function successStopReason(reason: TerminalReason): StopReason {
|
||||
return "stop"
|
||||
}
|
||||
|
||||
function generateMaxTokens(model: ModelLike, options?: StreamOptions): number {
|
||||
return Math.min(
|
||||
options?.maxTokens ?? model.maxTokens,
|
||||
model.maxTokens,
|
||||
DEFAULT_GENERATE_MAX_TOKENS,
|
||||
)
|
||||
}
|
||||
|
||||
export function projectSlugFromPath(pathName: string): string {
|
||||
const slug = pathName
|
||||
.toLowerCase()
|
||||
.replace(/^[a-z]:/i, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
return slug || "project"
|
||||
}
|
||||
|
||||
export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
const apiBase = deps.apiBase ?? DEFAULT_API_BASE
|
||||
const fetchImpl = deps.fetchImpl ?? fetch
|
||||
@@ -235,7 +255,13 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
break
|
||||
}
|
||||
|
||||
case "reasoning-start": {
|
||||
endTextBlock()
|
||||
break
|
||||
}
|
||||
|
||||
case "reasoning-delta": {
|
||||
endTextBlock()
|
||||
thinkingBlock.push(stringValue(event.text) ?? "")
|
||||
break
|
||||
}
|
||||
@@ -245,6 +271,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-result": {
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-call": {
|
||||
endTextBlock()
|
||||
const toolCall: ToolCallContent = {
|
||||
@@ -303,9 +333,12 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
try {
|
||||
stream.push({ type: "start", partial: output })
|
||||
|
||||
const workingDir = cwd()
|
||||
const threadId = uuid()
|
||||
|
||||
let body: unknown = {
|
||||
config: {
|
||||
workingDir: cwd(),
|
||||
workingDir,
|
||||
date: new Date(now()).toISOString().split("T")[0],
|
||||
environment: getEnvironmentInfo(),
|
||||
structure: [],
|
||||
@@ -315,18 +348,19 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
gitStatus: "",
|
||||
recentCommits: [],
|
||||
},
|
||||
memory: "",
|
||||
taste: "",
|
||||
memory: null,
|
||||
taste: null,
|
||||
skills: null,
|
||||
permissionMode: "standard",
|
||||
params: {
|
||||
model: model.id,
|
||||
messages: messagesToCC(context.messages),
|
||||
tools: toolsToJson(context.tools),
|
||||
system: context.systemPrompt ?? "",
|
||||
max_tokens: Math.min(options?.maxTokens ?? model.maxTokens, 200_000),
|
||||
max_tokens: generateMaxTokens(model, options),
|
||||
temperature: 0.3,
|
||||
stream: true,
|
||||
},
|
||||
threadId,
|
||||
}
|
||||
|
||||
const nextBody = await raceAbort(
|
||||
@@ -341,12 +375,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"x-command-code-version": "0.24.1",
|
||||
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
||||
"x-cli-environment": "production",
|
||||
"x-project-slug": "pi-cc",
|
||||
"x-taste-learning": "false",
|
||||
"x-project-slug": projectSlugFromPath(workingDir),
|
||||
"x-taste-learning": "true",
|
||||
"x-co-flag": "false",
|
||||
"x-session-id": uuid(),
|
||||
...options?.headers,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
mapFinishReason,
|
||||
messagesToCC,
|
||||
parseStreamEventLine,
|
||||
projectSlugFromPath,
|
||||
textContent,
|
||||
toJsonSchema,
|
||||
toolsToJson,
|
||||
@@ -27,12 +28,13 @@ describe("getApiKey()", () => {
|
||||
assert.equal(getApiKey({ env: { COMMANDCODE_API_KEY: "env-key" }, authPaths: [] }), "env-key")
|
||||
})
|
||||
|
||||
it("reads apiKey, commandcode, and pi OAuth credential fields from explicit auth paths", () => {
|
||||
it("reads apiKey, commandcode, pi OAuth, and official CLI credential fields", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "cc-auth-"))
|
||||
try {
|
||||
const first = join(dir, "first.json")
|
||||
const second = join(dir, "second.json")
|
||||
const oauth = join(dir, "oauth.json")
|
||||
const official = join(dir, "official.json")
|
||||
writeFileSync(first, JSON.stringify({ apiKey: "file-key" }))
|
||||
writeFileSync(second, JSON.stringify({ commandcode: "fallback-key" }))
|
||||
writeFileSync(
|
||||
@@ -46,9 +48,19 @@ describe("getApiKey()", () => {
|
||||
},
|
||||
}),
|
||||
)
|
||||
writeFileSync(
|
||||
official,
|
||||
JSON.stringify({
|
||||
"command-code": {
|
||||
type: "api",
|
||||
key: "official-cli-key",
|
||||
},
|
||||
}),
|
||||
)
|
||||
assert.equal(getApiKey({ env: {}, authPaths: [first, second] }), "file-key")
|
||||
assert.equal(getApiKey({ env: {}, authPaths: [second] }), "fallback-key")
|
||||
assert.equal(getApiKey({ env: {}, authPaths: [oauth] }), "oauth-access-key")
|
||||
assert.equal(getApiKey({ env: {}, authPaths: [official] }), "official-cli-key")
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
@@ -78,6 +90,16 @@ describe("getApiKey()", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("projectSlugFromPath()", () => {
|
||||
it("matches the official CLI-style slug from an absolute working directory", () => {
|
||||
assert.equal(
|
||||
projectSlugFromPath("/Users/patwoz/dev/Personal/pi/pi-commandcode-provider"),
|
||||
"users-patwoz-dev-personal-pi-pi-commandcode-provider",
|
||||
)
|
||||
assert.equal(projectSlugFromPath("/repo"), "repo")
|
||||
})
|
||||
})
|
||||
|
||||
describe("textContent()", () => {
|
||||
it("extracts and joins text blocks", () => {
|
||||
assert.equal(
|
||||
|
||||
+29
-3
@@ -142,6 +142,7 @@ describe("streamCommandCode — successful streams", () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [
|
||||
JSON.stringify({ type: "reasoning-start" }),
|
||||
JSON.stringify({ type: "reasoning-delta", text: "think" }),
|
||||
JSON.stringify({ type: "reasoning-end" }),
|
||||
JSON.stringify({ type: "text-delta", text: "Using tool" }),
|
||||
@@ -244,7 +245,13 @@ describe("streamCommandCode — request serialization", () => {
|
||||
assert.equal(objectAt(body, ["params", "model"]), "deepseek/deepseek-v4-flash")
|
||||
assert.equal(objectAt(body, ["params", "stream"]), true)
|
||||
assert.equal(objectAt(body, ["params", "max_tokens"]), 500)
|
||||
assert.equal(objectAt(body, ["params", "temperature"]), 0.3)
|
||||
assert.equal(objectAt(body, ["params", "system"]), "You are a test assistant.")
|
||||
assert.equal(objectAt(body, ["memory"]), null)
|
||||
assert.equal(objectAt(body, ["taste"]), null)
|
||||
assert.equal(objectAt(body, ["skills"]), null)
|
||||
assert.equal(objectAt(body, ["permissionMode"]), undefined)
|
||||
assert.equal(objectAt(body, ["threadId"]), "00000000-0000-4000-8000-000000000000")
|
||||
assert.equal(
|
||||
objectAt(body, ["params", "messages", "1", "content", "0", "text"]),
|
||||
"first response",
|
||||
@@ -253,8 +260,11 @@ describe("streamCommandCode — request serialization", () => {
|
||||
|
||||
const headers = server.lastRequestHeaders()
|
||||
assert.equal(headers.authorization, "Bearer mock-key")
|
||||
assert.equal(headers["x-command-code-version"], "0.24.1")
|
||||
assert.equal(headers["x-session-id"], "00000000-0000-4000-8000-000000000000")
|
||||
assert.equal(headers["x-command-code-version"], "0.27.2")
|
||||
assert.equal(headers["x-project-slug"], "repo")
|
||||
assert.equal(headers["x-taste-learning"], "true")
|
||||
assert.equal(headers["x-co-flag"], "false")
|
||||
assert.equal(headers["x-session-id"], undefined)
|
||||
})
|
||||
|
||||
it("caps maxTokens and passes custom headers", async () => {
|
||||
@@ -272,10 +282,26 @@ describe("streamCommandCode — request serialization", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
assert.equal(objectAt(server.lastRequestBody(), ["params", "max_tokens"]), 200_000)
|
||||
assert.equal(objectAt(server.lastRequestBody(), ["params", "max_tokens"]), 64_000)
|
||||
assert.equal(server.lastRequestHeaders()["x-custom"], "value")
|
||||
})
|
||||
|
||||
it("caps default maxTokens by the selected model", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
|
||||
})
|
||||
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
|
||||
|
||||
await collectEvents(
|
||||
streamCommandCode(makeModel({ maxTokens: 8_192 }), makeContext(), {
|
||||
apiKey: "mock-key",
|
||||
}),
|
||||
)
|
||||
|
||||
assert.equal(objectAt(server.lastRequestBody(), ["params", "max_tokens"]), 8_192)
|
||||
})
|
||||
|
||||
it("runs onPayload and onResponse hooks", async () => {
|
||||
server.mockResponse({
|
||||
type: "success",
|
||||
|
||||
Reference in New Issue
Block a user