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`:
|
Or use pi's auth file at `~/.pi/agent/auth.json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai"
|
import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai"
|
||||||
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
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 { DEFAULT_MODELS_URL, fetchCommandCodeModels } from "./src/models.ts"
|
||||||
import { getApiKey, login, refreshToken } from "./src/oauth.ts"
|
import { getApiKey, login, refreshToken } from "./src/oauth.ts"
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ export default async function (pi: ExtensionAPI) {
|
|||||||
api: "commandcode-custom",
|
api: "commandcode-custom",
|
||||||
streamSimple: streamCommandCode,
|
streamSimple: streamCommandCode,
|
||||||
headers: {
|
headers: {
|
||||||
"x-command-code-version": "0.24.1",
|
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
||||||
"x-cli-environment": "production",
|
"x-cli-environment": "production",
|
||||||
},
|
},
|
||||||
oauth: {
|
oauth: {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-commandcode-provider",
|
"name": "pi-commandcode-provider",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pi-commandcode-provider",
|
"name": "pi-commandcode-provider",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mariozechner/pi-ai": "0.72.0"
|
"@mariozechner/pi-ai": "0.72.0"
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pi-commandcode-provider",
|
"name": "pi-commandcode-provider",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"description": "pi custom provider for Command Code API (commandcode.ai)",
|
"description": "pi custom provider for Command Code API (commandcode.ai)",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"index.ts",
|
"index.ts",
|
||||||
"src/",
|
"src/",
|
||||||
"README.md",
|
"README.md",
|
||||||
|
"CHANGELOG.md",
|
||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+17
-7
@@ -42,6 +42,16 @@ function defaultAuthPaths(home: string): string[] {
|
|||||||
return [join(home, ".commandcode", "auth.json"), join(home, ".pi", "agent", "auth.json")]
|
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(
|
export function getApiKey(
|
||||||
options: {
|
options: {
|
||||||
env?: NodeJS.ProcessEnv
|
env?: NodeJS.ProcessEnv
|
||||||
@@ -61,18 +71,18 @@ export function getApiKey(
|
|||||||
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
|
const parsed: unknown = JSON.parse(readFileSync(authPath, "utf-8"))
|
||||||
if (!isRecord(parsed)) continue
|
if (!isRecord(parsed)) continue
|
||||||
|
|
||||||
// Legacy: direct apiKey or commandcode field
|
// Legacy: direct apiKey or commandcode field.
|
||||||
const apiKey = stringValue(parsed.apiKey)
|
const apiKey = stringValue(parsed.apiKey)
|
||||||
if (apiKey) return apiKey
|
if (apiKey) return apiKey
|
||||||
const commandcode = stringValue(parsed.commandcode)
|
const commandcode = stringValue(parsed.commandcode)
|
||||||
if (commandcode) return commandcode
|
if (commandcode) return commandcode
|
||||||
|
|
||||||
// OAuth: pi stores OAuth credentials as {"commandcode": {"type":"oauth","access":"...","refresh":"...","expires":...}}
|
// pi stores OAuth credentials as {"commandcode": {"type":"oauth","access":"..."}}.
|
||||||
const providerKey = isRecord(parsed.commandcode) ? parsed.commandcode : undefined
|
// The official Command Code CLI stores API credentials under "command-code".
|
||||||
if (providerKey && stringValue(providerKey.type) === "oauth") {
|
const providerKey =
|
||||||
const access = stringValue(providerKey.access)
|
apiKeyFromCredentialRecord(parsed.commandcode) ??
|
||||||
if (access) return access
|
apiKeyFromCredentialRecord(parsed["command-code"])
|
||||||
}
|
if (providerKey) return providerKey
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore malformed or unreadable auth files.
|
// Ignore malformed or unreadable auth files.
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-9
@@ -38,6 +38,9 @@ export * from "./converters.ts"
|
|||||||
export * from "./types.ts"
|
export * from "./types.ts"
|
||||||
|
|
||||||
export const DEFAULT_API_BASE = "https://api.commandcode.ai"
|
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 {
|
function defaultUsage(): Usage {
|
||||||
return {
|
return {
|
||||||
@@ -77,6 +80,23 @@ function successStopReason(reason: TerminalReason): StopReason {
|
|||||||
return "stop"
|
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) {
|
export function createStreamCommandCode(deps: CoreDependencies) {
|
||||||
const apiBase = deps.apiBase ?? DEFAULT_API_BASE
|
const apiBase = deps.apiBase ?? DEFAULT_API_BASE
|
||||||
const fetchImpl = deps.fetchImpl ?? fetch
|
const fetchImpl = deps.fetchImpl ?? fetch
|
||||||
@@ -235,7 +255,13 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "reasoning-start": {
|
||||||
|
endTextBlock()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case "reasoning-delta": {
|
case "reasoning-delta": {
|
||||||
|
endTextBlock()
|
||||||
thinkingBlock.push(stringValue(event.text) ?? "")
|
thinkingBlock.push(stringValue(event.text) ?? "")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -245,6 +271,10 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "tool-result": {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case "tool-call": {
|
case "tool-call": {
|
||||||
endTextBlock()
|
endTextBlock()
|
||||||
const toolCall: ToolCallContent = {
|
const toolCall: ToolCallContent = {
|
||||||
@@ -303,9 +333,12 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
try {
|
try {
|
||||||
stream.push({ type: "start", partial: output })
|
stream.push({ type: "start", partial: output })
|
||||||
|
|
||||||
|
const workingDir = cwd()
|
||||||
|
const threadId = uuid()
|
||||||
|
|
||||||
let body: unknown = {
|
let body: unknown = {
|
||||||
config: {
|
config: {
|
||||||
workingDir: cwd(),
|
workingDir,
|
||||||
date: new Date(now()).toISOString().split("T")[0],
|
date: new Date(now()).toISOString().split("T")[0],
|
||||||
environment: getEnvironmentInfo(),
|
environment: getEnvironmentInfo(),
|
||||||
structure: [],
|
structure: [],
|
||||||
@@ -315,18 +348,19 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
gitStatus: "",
|
gitStatus: "",
|
||||||
recentCommits: [],
|
recentCommits: [],
|
||||||
},
|
},
|
||||||
memory: "",
|
memory: null,
|
||||||
taste: "",
|
taste: null,
|
||||||
skills: null,
|
skills: null,
|
||||||
permissionMode: "standard",
|
|
||||||
params: {
|
params: {
|
||||||
model: model.id,
|
model: model.id,
|
||||||
messages: messagesToCC(context.messages),
|
messages: messagesToCC(context.messages),
|
||||||
tools: toolsToJson(context.tools),
|
tools: toolsToJson(context.tools),
|
||||||
system: context.systemPrompt ?? "",
|
system: context.systemPrompt ?? "",
|
||||||
max_tokens: Math.min(options?.maxTokens ?? model.maxTokens, 200_000),
|
max_tokens: generateMaxTokens(model, options),
|
||||||
|
temperature: 0.3,
|
||||||
stream: true,
|
stream: true,
|
||||||
},
|
},
|
||||||
|
threadId,
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextBody = await raceAbort(
|
const nextBody = await raceAbort(
|
||||||
@@ -341,12 +375,11 @@ export function createStreamCommandCode(deps: CoreDependencies) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${apiKey}`,
|
Authorization: `Bearer ${apiKey}`,
|
||||||
"x-command-code-version": "0.24.1",
|
"x-command-code-version": COMMAND_CODE_CLI_VERSION,
|
||||||
"x-cli-environment": "production",
|
"x-cli-environment": "production",
|
||||||
"x-project-slug": "pi-cc",
|
"x-project-slug": projectSlugFromPath(workingDir),
|
||||||
"x-taste-learning": "false",
|
"x-taste-learning": "true",
|
||||||
"x-co-flag": "false",
|
"x-co-flag": "false",
|
||||||
"x-session-id": uuid(),
|
|
||||||
...options?.headers,
|
...options?.headers,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
mapFinishReason,
|
mapFinishReason,
|
||||||
messagesToCC,
|
messagesToCC,
|
||||||
parseStreamEventLine,
|
parseStreamEventLine,
|
||||||
|
projectSlugFromPath,
|
||||||
textContent,
|
textContent,
|
||||||
toJsonSchema,
|
toJsonSchema,
|
||||||
toolsToJson,
|
toolsToJson,
|
||||||
@@ -27,12 +28,13 @@ describe("getApiKey()", () => {
|
|||||||
assert.equal(getApiKey({ env: { COMMANDCODE_API_KEY: "env-key" }, authPaths: [] }), "env-key")
|
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-"))
|
const dir = mkdtempSync(join(tmpdir(), "cc-auth-"))
|
||||||
try {
|
try {
|
||||||
const first = join(dir, "first.json")
|
const first = join(dir, "first.json")
|
||||||
const second = join(dir, "second.json")
|
const second = join(dir, "second.json")
|
||||||
const oauth = join(dir, "oauth.json")
|
const oauth = join(dir, "oauth.json")
|
||||||
|
const official = join(dir, "official.json")
|
||||||
writeFileSync(first, JSON.stringify({ apiKey: "file-key" }))
|
writeFileSync(first, JSON.stringify({ apiKey: "file-key" }))
|
||||||
writeFileSync(second, JSON.stringify({ commandcode: "fallback-key" }))
|
writeFileSync(second, JSON.stringify({ commandcode: "fallback-key" }))
|
||||||
writeFileSync(
|
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: [first, second] }), "file-key")
|
||||||
assert.equal(getApiKey({ env: {}, authPaths: [second] }), "fallback-key")
|
assert.equal(getApiKey({ env: {}, authPaths: [second] }), "fallback-key")
|
||||||
assert.equal(getApiKey({ env: {}, authPaths: [oauth] }), "oauth-access-key")
|
assert.equal(getApiKey({ env: {}, authPaths: [oauth] }), "oauth-access-key")
|
||||||
|
assert.equal(getApiKey({ env: {}, authPaths: [official] }), "official-cli-key")
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(dir, { recursive: true, force: true })
|
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()", () => {
|
describe("textContent()", () => {
|
||||||
it("extracts and joins text blocks", () => {
|
it("extracts and joins text blocks", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
|
|||||||
+29
-3
@@ -142,6 +142,7 @@ describe("streamCommandCode — successful streams", () => {
|
|||||||
server.mockResponse({
|
server.mockResponse({
|
||||||
type: "success",
|
type: "success",
|
||||||
events: [
|
events: [
|
||||||
|
JSON.stringify({ type: "reasoning-start" }),
|
||||||
JSON.stringify({ type: "reasoning-delta", text: "think" }),
|
JSON.stringify({ type: "reasoning-delta", text: "think" }),
|
||||||
JSON.stringify({ type: "reasoning-end" }),
|
JSON.stringify({ type: "reasoning-end" }),
|
||||||
JSON.stringify({ type: "text-delta", text: "Using tool" }),
|
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", "model"]), "deepseek/deepseek-v4-flash")
|
||||||
assert.equal(objectAt(body, ["params", "stream"]), true)
|
assert.equal(objectAt(body, ["params", "stream"]), true)
|
||||||
assert.equal(objectAt(body, ["params", "max_tokens"]), 500)
|
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, ["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(
|
assert.equal(
|
||||||
objectAt(body, ["params", "messages", "1", "content", "0", "text"]),
|
objectAt(body, ["params", "messages", "1", "content", "0", "text"]),
|
||||||
"first response",
|
"first response",
|
||||||
@@ -253,8 +260,11 @@ describe("streamCommandCode — request serialization", () => {
|
|||||||
|
|
||||||
const headers = server.lastRequestHeaders()
|
const headers = server.lastRequestHeaders()
|
||||||
assert.equal(headers.authorization, "Bearer mock-key")
|
assert.equal(headers.authorization, "Bearer mock-key")
|
||||||
assert.equal(headers["x-command-code-version"], "0.24.1")
|
assert.equal(headers["x-command-code-version"], "0.27.2")
|
||||||
assert.equal(headers["x-session-id"], "00000000-0000-4000-8000-000000000000")
|
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 () => {
|
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")
|
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 () => {
|
it("runs onPayload and onResponse hooks", async () => {
|
||||||
server.mockResponse({
|
server.mockResponse({
|
||||||
type: "success",
|
type: "success",
|
||||||
|
|||||||
Reference in New Issue
Block a user