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 +21 -0
- package/README.md +110 -0
- package/index.ts +197 -0
- package/package.json +54 -0
- package/src/auth-server.ts +214 -0
- package/src/converters.ts +276 -0
- package/src/core.ts +458 -0
- package/src/oauth.ts +167 -0
- package/src/types.ts +156 -0
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
|
+
}
|