Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Add repository commands for testing the current checkout either in a logged-out, automatically cleaned-up pi environment or with existing credentials and only Command Code models enabled.
- Refresh display pricing for the current Command Code model catalog, remove expired Qwen promotional rates, add current free and discounted models, and require review when temporary prices expire.
- Use the host-provided `pi-ai` and `pi-coding-agent` core packages instead of installing private runtime copies, including for local and out-of-store development checkouts.

Expand Down
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,27 @@ npm run test:models
npm run test:oauth
npm run test:abort
npm run test:stream
npm run test:pi-isolated
npm run test:pi-authenticated
npm run test:pi-local
```

Start an isolated pi instance with only the current checkout installed and no existing Command Code credentials:

```sh
npm run pi:isolated
```

Run `/login` inside pi. Temporary credentials, configuration, and sessions are deleted when pi exits.

Start the current checkout with your existing pi credentials and only Command Code models in the model picker:

```sh
npm run pi:authenticated
```

Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`.

Before opening a PR, run:

```sh
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,22 @@ omp plugin uninstall pi-commandcode-provider

## Development

Start an isolated pi instance with only the current checkout installed and no existing Command Code credentials:

```sh
npm run pi:isolated
```

Run `/login` inside pi. Temporary credentials, configuration, and sessions are deleted when pi exits.

Start the current checkout with your existing pi credentials and only Command Code models in the model picker:

```sh
npm run pi:authenticated
```

Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`.

See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process.

## License
Expand Down
8 changes: 5 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
import { join } from "node:path"

import { getApiKey as getStoredApiKey } from "./src/converters.ts"
import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
import { calculateCommandCodeCost } from "./src/cost.ts"
import { DEFAULT_MODELS_URL, loadCommandCodeModels } from "./src/models.ts"
import { getApiKey, login, refreshToken } from "./src/oauth.ts"
import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
import { MODEL_COSTS, ZERO_MODEL_COST } from "./src/pricing.ts"

const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
Expand All @@ -38,6 +39,7 @@ const streamCommandCode = createStreamCommandCode({
// ---------------------------------------------------------------------------

export default async function (pi: ExtensionAPI) {
const storedApiKey = getStoredApiKey()
const { models, warning } = await loadCommandCodeModels({
url: MODELS_URL,
cachePath: MODELS_CACHE_PATH,
Expand All @@ -48,7 +50,7 @@ export default async function (pi: ExtensionAPI) {
pi.registerProvider("commandcode", {
name: "Command Code",
baseUrl: API_BASE,
apiKey: "$COMMANDCODE_API_KEY",
apiKey: storedApiKey,
authHeader: true,
api: "commandcode-custom",
streamSimple: streamCommandCode,
Expand All @@ -60,7 +62,7 @@ export default async function (pi: ExtensionAPI) {
name: "Command Code",
login,
refreshToken,
getApiKey,
getApiKey: getOAuthApiKey,
},
models: models.map((model) => ({
id: model.id,
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,29 @@
"files": [
"index.ts",
"src/",
"scripts/",
"README.md",
"CHANGELOG.md",
"CONTRIBUTING.md",
"RELEASE.md",
"LICENSE"
],
"scripts": {
"test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
"test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.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",
"typecheck": "tsc --noEmit",
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
"format": "prettier --write '**/*.{ts,mjs,json,md}'",
"pi:isolated": "node scripts/pi-isolated.mjs",
"pi:authenticated": "node scripts/pi-authenticated.mjs",
"test:unit": "tsx tests/test-pure-functions.ts",
"test:models": "tsx tests/test-models.ts",
"test:pricing": "tsx tests/test-pricing.ts",
"test:oauth": "tsx tests/test-oauth.ts",
"test:abort": "tsx tests/test-abort.ts",
"test:stream": "tsx tests/test-stream.ts",
"test:retry": "tsx tests/test-retry.ts",
"test:pi-isolated": "node tests/test-pi-isolated.mjs",
"test:pi-authenticated": "node tests/test-pi-authenticated.mjs",
"test:pi-local": "node tests/test-pi-local.mjs",
"test:smoke": "node tests/test-smoke.mjs",
"test:cost": "tsx tests/test-cost.ts"
Expand Down
49 changes: 49 additions & 0 deletions scripts/pi-authenticated.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env node

import { spawn } from "node:child_process"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const extensionPath = resolve(repoRoot, "index.ts")

const env = {
...process.env,
PI_SKIP_VERSION_CHECK: "1",
}
delete env.COMMANDCODE_API_KEY

const child = spawn(
"pi",
[
"--no-extensions",
"--extension",
extensionPath,
"--provider",
"commandcode",
"--model",
"gpt-5.6-luna",
"--models",
"commandcode/*",
...process.argv.slice(2),
],
{
cwd: repoRoot,
env,
stdio: "inherit",
},
)

for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => child.kill(signal))
}

child.once("error", (error) => {
console.error(`Could not start pi: ${error.message}`)
process.exitCode = 1
})

child.once("exit", (status, signal) => {
if (signal) process.kill(process.pid, signal)
process.exitCode = status ?? 1
})
78 changes: 78 additions & 0 deletions scripts/pi-isolated.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env node

import { spawn } from "node:child_process"
import { mkdir, mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { dirname, join, resolve } from "node:path"
import { fileURLToPath } from "node:url"

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const testRoot = await mkdtemp(join(tmpdir(), "pi-commandcode-isolated-"))
const agentDir = join(testRoot, "agent")
const sessionDir = join(testRoot, "sessions")

await mkdir(agentDir, { mode: 0o700 })
await mkdir(sessionDir, { mode: 0o700 })

const env = {
...process.env,
HOME: testRoot,
USERPROFILE: testRoot,
PI_CODING_AGENT_DIR: agentDir,
PI_CODING_AGENT_SESSION_DIR: sessionDir,
PI_SKIP_VERSION_CHECK: "1",
}
delete env.COMMANDCODE_API_KEY

let activeChild
let receivedSignal

function forwardSignal(signal) {
receivedSignal = signal
activeChild?.kill(signal)
}

const forwardSigint = () => forwardSignal("SIGINT")
const forwardSigterm = () => forwardSignal("SIGTERM")
process.on("SIGINT", forwardSigint)
process.on("SIGTERM", forwardSigterm)

function runPi(args) {
return new Promise((resolveRun, rejectRun) => {
const child = spawn("pi", args, { cwd: repoRoot, env, stdio: "inherit" })
activeChild = child
child.once("error", rejectRun)
child.once("exit", (status, signal) => {
activeChild = undefined
resolveRun({ status, signal })
})
})
}

let result
try {
console.error("Installing the current checkout into an isolated pi environment...")
const install = await runPi(["install", repoRoot, "--no-approve"])
if (install.status !== 0 || install.signal) {
result = install
} else {
console.error("Starting pi. Temporary auth and sessions will be removed on exit.")
result = await runPi([
"--no-approve",
"--provider",
"commandcode",
"--model",
"gpt-5.6-luna",
...process.argv.slice(2),
])
}
} finally {
process.removeListener("SIGINT", forwardSigint)
process.removeListener("SIGTERM", forwardSigterm)
await rm(testRoot, { recursive: true, force: true })
console.error("Removed the isolated pi environment.")
}

const signal = receivedSignal ?? result?.signal
if (signal) process.kill(process.pid, signal)
process.exitCode = result?.status ?? 1
73 changes: 73 additions & 0 deletions tests/test-pi-authenticated.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import assert from "node:assert/strict"
import { spawnSync } from "node:child_process"
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { delimiter, dirname, join, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { describe, it } from "node:test"

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const launcher = join(repoRoot, "scripts", "pi-authenticated.mjs")

function runLauncher() {
const fakeBin = mkdtempSync(join(tmpdir(), "pi-commandcode-fake-bin-"))
const logPath = join(fakeBin, "call.json")
const fakePi = join(fakeBin, "pi")

writeFileSync(
fakePi,
`#!/bin/sh
node - "$@" <<'NODE'
const { writeFileSync } = require("node:fs")
writeFileSync(process.env.FAKE_PI_LOG, JSON.stringify({
args: process.argv.slice(2),
agentDir: process.env.PI_CODING_AGENT_DIR ?? null,
apiKey: process.env.COMMANDCODE_API_KEY ?? null,
skipVersionCheck: process.env.PI_SKIP_VERSION_CHECK,
}))
NODE
`,
{ mode: 0o700 },
)

try {
const result = spawnSync(process.execPath, [launcher, "--thinking", "high"], {
cwd: repoRoot,
env: {
...process.env,
PATH: `${fakeBin}${delimiter}${process.env.PATH ?? ""}`,
FAKE_PI_LOG: logPath,
PI_CODING_AGENT_DIR: "/existing/pi-agent",
COMMANDCODE_API_KEY: "existing-key",
},
encoding: "utf8",
})
return { result, call: JSON.parse(readFileSync(logPath, "utf8")) }
} finally {
rmSync(fakeBin, { recursive: true, force: true })
}
}

describe("authenticated pi launcher", () => {
it("loads only the checkout extension and leaves auth resolution to existing files", () => {
const { result, call } = runLauncher()

assert.equal(result.status, 0)
assert.deepEqual(call.args, [
"--no-extensions",
"--extension",
join(repoRoot, "index.ts"),
"--provider",
"commandcode",
"--model",
"gpt-5.6-luna",
"--models",
"commandcode/*",
"--thinking",
"high",
])
assert.equal(call.agentDir, "/existing/pi-agent")
assert.equal(call.apiKey, null)
assert.equal(call.skipVersionCheck, "1")
})
})
Loading
Loading