pi-commandcode-provider 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pat Woz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # pi-commandcode-provider
2
+
3
+ A [pi](https://github.com/badlogic/pi-mono) custom provider that connects pi to the [Command Code](https://commandcode.ai) API.
4
+
5
+ > **Disclaimer:** This is an unofficial, community-maintained package. I am not affiliated with, endorsed by, or connected to Command Code in any way. This provider simply forwards requests to the public Command Code API using your own API key.
6
+
7
+ > **Note:** This package only provides a model _provider_. It does **not** include an API key. You must bring your own Command Code API key or subscription.
8
+
9
+ > 💰 **Current offer:** Command Code offers [4× usage of DeepSeek V4](https://commandcode.ai/docs/resources/pricing-limits#deepseek-v4-pro-4x-usage) (Pro and Flash) at no extra cost.
10
+
11
+ ## Models
12
+
13
+ 18 models across premium and open-source providers:
14
+
15
+ | Category | Models |
16
+ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | **Anthropic** | Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 4.6, Claude Haiku 4.5 |
18
+ | **OpenAI** | GPT-5.5, GPT-5.4, GPT-5.3 Codex, GPT-5.4 Mini |
19
+ | **Open-source** | DeepSeek V4, DeepSeek V4 Pro, DeepSeek V4 Flash, Kimi K2.6, Kimi K2.5, GLM-5.1, GLM-5, MiniMax M2.7, MiniMax M2.5, Qwen 3.6 Max, Qwen 3.6 Plus |
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ pi install npm:pi-commandcode-provider
25
+ ```
26
+
27
+ Or shorthand:
28
+
29
+ ```sh
30
+ pi install pi-commandcode-provider
31
+ ```
32
+
33
+ Then reload pi:
34
+
35
+ ```txt
36
+ /reload
37
+ ```
38
+
39
+ ## Setup
40
+
41
+ Set your Command Code API key using one of these methods:
42
+
43
+ ### 1. Browser login (recommended)
44
+
45
+ In pi, run:
46
+
47
+ ```txt
48
+ /login
49
+ ```
50
+
51
+ Then select **Command Code** from the provider list.
52
+
53
+ This opens Command Code in your browser and stores the returned API key in pi's auth file. If the browser shows "Copy your API key" because automatic transfer failed, copy that key and paste it into the pi terminal prompt.
54
+
55
+ > Note: `/login commandcode` is not supported by pi currently; use interactive `/login` and select Command Code.
56
+
57
+ ### 2. Environment variable
58
+
59
+ ```sh
60
+ export COMMANDCODE_API_KEY="user_..."
61
+ ```
62
+
63
+ ### 3. Auth file
64
+
65
+ Create `~/.commandcode/auth.json`:
66
+
67
+ ```json
68
+ {
69
+ "apiKey": "user_..."
70
+ }
71
+ ```
72
+
73
+ Or use pi's auth file at `~/.pi/agent/auth.json`:
74
+
75
+ ```json
76
+ {
77
+ "commandcode": "user_..."
78
+ }
79
+ ```
80
+
81
+ ## Usage
82
+
83
+ After installing and setting your API key, select a Command Code model in pi:
84
+
85
+ ```txt
86
+ /model deepseek/deepseek-v4-flash
87
+ ```
88
+
89
+ Any query will then use the Command Code API. You can list available models:
90
+
91
+ ```sh
92
+ pi -e index.ts --list-models
93
+ ```
94
+
95
+ Or within pi:
96
+
97
+ ```txt
98
+ /models
99
+ ```
100
+
101
+ ## Publish
102
+
103
+ ```sh
104
+ npm login
105
+ npm publish --access public
106
+ ```
107
+
108
+ ## License
109
+
110
+ MIT
package/index.ts ADDED
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Command Code provider for pi.
3
+ *
4
+ * Connects pi to Command Code's API (https://api.commandcode.ai/alpha/generate).
5
+ *
6
+ * Authentication (pick one):
7
+ * 1. Run `/login`, then select Command Code — opens browser to commandcode.ai, auto-stores API key
8
+ * 2. Set COMMANDCODE_API_KEY environment variable
9
+ * 3. Place API key in `~/.commandcode/auth.json` or `~/.pi/agent/auth.json`
10
+ * as {"apiKey": "user_..."} or {"commandcode": "user_..."}
11
+ *
12
+ * Models: deepseek-v4-pro, deepseek-v4-flash, claude-sonnet-4-6, claude-opus-4-7, etc.
13
+ */
14
+
15
+ import { calculateCost, createAssistantMessageEventStream } from "@mariozechner/pi-ai"
16
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
17
+
18
+ import { createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
19
+ import { getApiKey, login, refreshToken } from "./src/oauth.ts"
20
+
21
+ const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Model definitions
25
+ // ---------------------------------------------------------------------------
26
+
27
+ const MODELS = [
28
+ // Premium (Anthropic)
29
+ {
30
+ id: "claude-opus-4-7",
31
+ name: "Claude Opus 4.7 (CC)",
32
+ reasoning: true,
33
+ contextWindow: 200_000,
34
+ maxTokens: 32_000,
35
+ },
36
+ {
37
+ id: "claude-opus-4-6",
38
+ name: "Claude Opus 4.6 (CC)",
39
+ reasoning: true,
40
+ contextWindow: 200_000,
41
+ maxTokens: 32_000,
42
+ },
43
+ {
44
+ id: "claude-sonnet-4-6",
45
+ name: "Claude Sonnet 4.6 (CC)",
46
+ reasoning: true,
47
+ contextWindow: 200_000,
48
+ maxTokens: 16_384,
49
+ },
50
+ {
51
+ id: "claude-haiku-4-5-20251001",
52
+ name: "Claude Haiku 4.5 (CC)",
53
+ reasoning: true,
54
+ contextWindow: 200_000,
55
+ maxTokens: 8_192,
56
+ },
57
+ // Premium (OpenAI)
58
+ {
59
+ id: "gpt-5.5",
60
+ name: "GPT-5.5 (CC)",
61
+ reasoning: true,
62
+ contextWindow: 256_000,
63
+ maxTokens: 128_000,
64
+ },
65
+ {
66
+ id: "gpt-5.4",
67
+ name: "GPT-5.4 (CC)",
68
+ reasoning: true,
69
+ contextWindow: 256_000,
70
+ maxTokens: 128_000,
71
+ },
72
+ {
73
+ id: "gpt-5.3-codex",
74
+ name: "GPT-5.3 Codex (CC)",
75
+ reasoning: true,
76
+ contextWindow: 256_000,
77
+ maxTokens: 128_000,
78
+ },
79
+ {
80
+ id: "gpt-5.4-mini",
81
+ name: "GPT-5.4 Mini (CC)",
82
+ reasoning: false,
83
+ contextWindow: 256_000,
84
+ maxTokens: 128_000,
85
+ },
86
+ // Open-source
87
+ {
88
+ id: "deepseek/deepseek-v4-pro",
89
+ name: "DeepSeek V4 Pro (CC)",
90
+ reasoning: true,
91
+ contextWindow: 1_000_000,
92
+ maxTokens: 384_000,
93
+ },
94
+ {
95
+ id: "deepseek/deepseek-v4-flash",
96
+ name: "DeepSeek V4 Flash (CC)",
97
+ reasoning: true,
98
+ contextWindow: 1_000_000,
99
+ maxTokens: 384_000,
100
+ },
101
+ {
102
+ id: "moonshotai/Kimi-K2.6",
103
+ name: "Kimi K2.6 (CC)",
104
+ reasoning: true,
105
+ contextWindow: 262_144,
106
+ maxTokens: 131_072,
107
+ },
108
+ {
109
+ id: "moonshotai/Kimi-K2.5",
110
+ name: "Kimi K2.5 (CC)",
111
+ reasoning: true,
112
+ contextWindow: 262_144,
113
+ maxTokens: 131_072,
114
+ },
115
+ {
116
+ id: "zai-org/GLM-5.1",
117
+ name: "GLM-5.1 (CC)",
118
+ reasoning: true,
119
+ contextWindow: 200_000,
120
+ maxTokens: 131_072,
121
+ },
122
+ {
123
+ id: "zai-org/GLM-5",
124
+ name: "GLM-5 (CC)",
125
+ reasoning: true,
126
+ contextWindow: 200_000,
127
+ maxTokens: 131_072,
128
+ },
129
+ {
130
+ id: "MiniMaxAI/MiniMax-M2.7",
131
+ name: "MiniMax M2.7 (CC)",
132
+ reasoning: true,
133
+ contextWindow: 1_048_576,
134
+ maxTokens: 131_072,
135
+ },
136
+ {
137
+ id: "MiniMaxAI/MiniMax-M2.5",
138
+ name: "MiniMax M2.5 (CC)",
139
+ reasoning: true,
140
+ contextWindow: 1_048_576,
141
+ maxTokens: 131_072,
142
+ },
143
+ {
144
+ id: "Qwen/Qwen3.6-Max-Preview",
145
+ name: "Qwen 3.6 Max (CC)",
146
+ reasoning: true,
147
+ contextWindow: 1_000_000,
148
+ maxTokens: 131_072,
149
+ },
150
+ {
151
+ id: "Qwen/Qwen3.6-Plus",
152
+ name: "Qwen 3.6 Plus (CC)",
153
+ reasoning: true,
154
+ contextWindow: 1_000_000,
155
+ maxTokens: 131_072,
156
+ },
157
+ ]
158
+
159
+ const streamCommandCode = createStreamCommandCode({
160
+ createStream: createAssistantMessageEventStream,
161
+ calculateCost,
162
+ apiBase: API_BASE,
163
+ })
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Extension entry point
167
+ // ---------------------------------------------------------------------------
168
+
169
+ export default function (pi: ExtensionAPI) {
170
+ pi.registerProvider("commandcode", {
171
+ name: "Command Code",
172
+ baseUrl: API_BASE,
173
+ apiKey: "COMMANDCODE_API_KEY",
174
+ authHeader: true,
175
+ api: "commandcode-custom",
176
+ streamSimple: streamCommandCode,
177
+ headers: {
178
+ "x-command-code-version": "0.24.1",
179
+ "x-cli-environment": "production",
180
+ },
181
+ oauth: {
182
+ name: "Command Code",
183
+ login,
184
+ refreshToken,
185
+ getApiKey,
186
+ },
187
+ models: MODELS.map((model) => ({
188
+ id: model.id,
189
+ name: model.name,
190
+ reasoning: model.reasoning,
191
+ input: ["text"],
192
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
193
+ contextWindow: model.contextWindow,
194
+ maxTokens: model.maxTokens,
195
+ })),
196
+ })
197
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "pi-commandcode-provider",
3
+ "version": "0.1.0",
4
+ "description": "pi custom provider for Command Code API (commandcode.ai)",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "commandcode",
10
+ "provider"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/patlux/pi-commandcode-provider.git"
16
+ },
17
+ "homepage": "https://github.com/patlux/pi-commandcode-provider#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/patlux/pi-commandcode-provider/issues"
20
+ },
21
+ "files": [
22
+ "index.ts",
23
+ "src/",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "test": "npm run typecheck && tsx tests/test-pure-functions.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && node tests/test-pi-local.mjs",
29
+ "typecheck": "tsc --noEmit",
30
+ "format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
31
+ "format": "prettier --write '**/*.{ts,mjs,json,md}'",
32
+ "test:unit": "tsx tests/test-pure-functions.ts",
33
+ "test:oauth": "tsx tests/test-oauth.ts",
34
+ "test:abort": "tsx tests/test-abort.ts",
35
+ "test:stream": "tsx tests/test-stream.ts",
36
+ "test:pi-local": "node tests/test-pi-local.mjs",
37
+ "test:smoke": "node tests/test-smoke.mjs"
38
+ },
39
+ "pi": {
40
+ "extensions": [
41
+ "./index.ts"
42
+ ]
43
+ },
44
+ "devDependencies": {
45
+ "@mariozechner/pi-coding-agent": "0.72.0",
46
+ "@types/node": "25.6.0",
47
+ "prettier": "^3.5.0",
48
+ "tsx": "4.21.0",
49
+ "typescript": "6.0.3"
50
+ },
51
+ "dependencies": {
52
+ "@mariozechner/pi-ai": "0.72.0"
53
+ }
54
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Local HTTP callback server for the Command Code browser auth flow.
3
+ *
4
+ * Starts a one-shot server on a CLI-compatible localhost port. The Command Code
5
+ * Studio website POSTs the user's API key to /callback after they authenticate.
6
+ */
7
+
8
+ import { createServer, type Server } from "node:http"
9
+ import type { AddressInfo } from "node:net"
10
+
11
+ const DEFAULT_PORT = 5959
12
+ const DEFAULT_PORT_RANGE = 10
13
+
14
+ export interface AuthCallback {
15
+ apiKey: string
16
+ state: string
17
+ userId: string
18
+ userName: string
19
+ keyName: string
20
+ }
21
+
22
+ export interface AuthServer {
23
+ server: Server
24
+ port: number
25
+ waitForCallback: Promise<AuthCallback>
26
+ }
27
+
28
+ export interface AuthServerOptions {
29
+ startPort?: number
30
+ portRange?: number
31
+ }
32
+
33
+ function listenOnAvailablePort(
34
+ server: Server,
35
+ startPort = DEFAULT_PORT,
36
+ range = DEFAULT_PORT_RANGE,
37
+ ): Promise<number> {
38
+ return new Promise((resolve, reject) => {
39
+ let offset = 0
40
+
41
+ const tryListen = () => {
42
+ const useFallbackPort = startPort === 0 || offset >= range
43
+ const port = useFallbackPort ? 0 : startPort + offset
44
+
45
+ const onError = (err: NodeJS.ErrnoException) => {
46
+ server.off("listening", onListening)
47
+ if (err.code === "EADDRINUSE" && !useFallbackPort) {
48
+ offset += 1
49
+ tryListen()
50
+ return
51
+ }
52
+ reject(err)
53
+ }
54
+
55
+ const onListening = () => {
56
+ server.off("error", onError)
57
+ const address = server.address() as AddressInfo
58
+ resolve(address.port)
59
+ }
60
+
61
+ server.once("error", onError)
62
+ server.once("listening", onListening)
63
+ server.listen(port, "127.0.0.1")
64
+ }
65
+
66
+ tryListen()
67
+ })
68
+ }
69
+
70
+ function closeServer(server: Server) {
71
+ server.close((err: NodeJS.ErrnoException | undefined) => {
72
+ if (err && err.code !== "ERR_SERVER_NOT_RUNNING") {
73
+ // There is nowhere useful to report this during auth cleanup.
74
+ }
75
+ })
76
+ }
77
+
78
+ /**
79
+ * Start a local HTTP server that listens for the Command Code Studio
80
+ * to POST the API key after the user authenticates in their browser.
81
+ *
82
+ * The server accepts exactly one valid POST to /callback and then closes.
83
+ */
84
+ export async function startAuthServer(options: AuthServerOptions = {}): Promise<AuthServer> {
85
+ let resolveCallback!: (value: AuthCallback) => void
86
+ let rejectCallback!: (error: Error) => void
87
+
88
+ const waitForCallback = new Promise<AuthCallback>((resolve, reject) => {
89
+ resolveCallback = resolve
90
+ rejectCallback = reject
91
+ })
92
+
93
+ const server = createServer((req, res) => {
94
+ // CORS: allow requests from Command Code domains and localhost for dev.
95
+ const origin = req.headers.origin || ""
96
+ const allowedOrigins = [
97
+ "http://localhost:3000",
98
+ "https://staging.commandcode.ai",
99
+ "https://commandcode.ai",
100
+ ]
101
+ const responseOrigin = allowedOrigins.includes(origin) ? origin : allowedOrigins[0]
102
+ const requestedHeaders = req.headers["access-control-request-headers"]
103
+
104
+ res.setHeader("Access-Control-Allow-Origin", responseOrigin)
105
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS")
106
+ res.setHeader(
107
+ "Access-Control-Allow-Headers",
108
+ typeof requestedHeaders === "string" && requestedHeaders.length > 0
109
+ ? requestedHeaders
110
+ : "Content-Type",
111
+ )
112
+ // Chrome's Private Network Access preflight may require this for an HTTPS
113
+ // page posting to a localhost HTTP callback.
114
+ res.setHeader("Access-Control-Allow-Private-Network", "true")
115
+ res.setHeader("Content-Type", "application/json")
116
+
117
+ // Handle CORS preflight.
118
+ if (req.method === "OPTIONS") {
119
+ res.writeHead(204)
120
+ res.end()
121
+ return
122
+ }
123
+
124
+ if (req.url !== "/callback") {
125
+ res.writeHead(404)
126
+ res.end(JSON.stringify({ success: false, error: "Not found" }))
127
+ return
128
+ }
129
+
130
+ if (req.method !== "POST") {
131
+ res.writeHead(405)
132
+ res.end(
133
+ JSON.stringify({
134
+ success: false,
135
+ error: "Method not allowed. Use POST.",
136
+ }),
137
+ )
138
+ return
139
+ }
140
+
141
+ let body = ""
142
+ req.on("data", (chunk) => {
143
+ body += chunk.toString()
144
+ if (body.length > 10_000) req.destroy()
145
+ })
146
+
147
+ req.on("end", () => {
148
+ try {
149
+ const parsed = JSON.parse(body) as Record<string, unknown>
150
+
151
+ if (parsed.error) {
152
+ res.writeHead(200)
153
+ res.end(JSON.stringify({ success: true }))
154
+ const description =
155
+ typeof parsed.error_description === "string"
156
+ ? parsed.error_description
157
+ : String(parsed.error)
158
+ if (parsed.error === "access_denied") {
159
+ rejectCallback(new Error(description || "Authorization was denied by the user"))
160
+ } else {
161
+ rejectCallback(new Error(description || String(parsed.error)))
162
+ }
163
+ closeServer(server)
164
+ return
165
+ }
166
+
167
+ const apiKey = typeof parsed.apiKey === "string" ? parsed.apiKey : ""
168
+ const state = typeof parsed.state === "string" ? parsed.state : ""
169
+ const userId = typeof parsed.userId === "string" ? parsed.userId : ""
170
+ const userName = typeof parsed.userName === "string" ? parsed.userName : ""
171
+ const keyName = typeof parsed.keyName === "string" ? parsed.keyName : ""
172
+
173
+ if (!apiKey || !state || !userId || !userName || !keyName) {
174
+ res.writeHead(400)
175
+ res.end(
176
+ JSON.stringify({
177
+ success: false,
178
+ error: "Missing required fields",
179
+ }),
180
+ )
181
+ return
182
+ }
183
+
184
+ res.writeHead(200)
185
+ res.end(JSON.stringify({ success: true }))
186
+
187
+ resolveCallback({ apiKey, state, userId, userName, keyName })
188
+ closeServer(server)
189
+ } catch {
190
+ res.writeHead(400)
191
+ res.end(JSON.stringify({ success: false, error: "Invalid JSON" }))
192
+ }
193
+ })
194
+
195
+ req.on("error", () => {
196
+ res.writeHead(500)
197
+ res.end(JSON.stringify({ success: false, error: "Request error" }))
198
+ })
199
+ })
200
+
201
+ try {
202
+ const port = await listenOnAvailablePort(
203
+ server,
204
+ options.startPort ?? DEFAULT_PORT,
205
+ options.portRange ?? DEFAULT_PORT_RANGE,
206
+ )
207
+ return { server, port, waitForCallback }
208
+ } catch (err) {
209
+ const message = err instanceof Error ? err.message : String(err)
210
+ const error = new Error(`Failed to start auth server: ${message}`)
211
+ rejectCallback(error)
212
+ throw error
213
+ }
214
+ }