ai-cc-router 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/Dockerfile +32 -0
- package/LICENSE +21 -0
- package/README.md +395 -0
- package/accounts.example.json +16 -0
- package/dist/cli/cmd-accounts.js +142 -0
- package/dist/cli/cmd-configure.js +37 -0
- package/dist/cli/cmd-docker.js +140 -0
- package/dist/cli/cmd-service.js +193 -0
- package/dist/cli/cmd-setup.js +248 -0
- package/dist/cli/cmd-start.js +80 -0
- package/dist/cli/cmd-status.js +46 -0
- package/dist/cli/cmd-stop.js +128 -0
- package/dist/cli/index.js +38 -0
- package/dist/config/manager.js +56 -0
- package/dist/config/paths.js +11 -0
- package/dist/proxy/logger.js +34 -0
- package/dist/proxy/server.js +178 -0
- package/dist/proxy/stats.js +21 -0
- package/dist/proxy/token-pool.js +47 -0
- package/dist/proxy/token-refresher.js +114 -0
- package/dist/proxy/types.js +1 -0
- package/dist/ui/Dashboard.js +110 -0
- package/dist/utils/claude-config.js +76 -0
- package/dist/utils/platform.js +13 -0
- package/dist/utils/token-extractor.js +90 -0
- package/dist/utils/token-validator.js +24 -0
- package/docker-compose.yml +45 -0
- package/litellm-config.yaml +44 -0
- package/package.json +64 -0
package/Dockerfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# ── Build stage ───────────────────────────────────────────────────────────────
|
|
2
|
+
FROM node:22-alpine AS builder
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
COPY package*.json ./
|
|
6
|
+
RUN npm ci
|
|
7
|
+
|
|
8
|
+
COPY tsconfig.json ./
|
|
9
|
+
COPY src/ ./src/
|
|
10
|
+
RUN npm run build
|
|
11
|
+
|
|
12
|
+
# ── Runtime stage ─────────────────────────────────────────────────────────────
|
|
13
|
+
FROM node:22-alpine AS runtime
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
|
|
16
|
+
ENV NODE_ENV=production
|
|
17
|
+
# Port defaults — overridden by docker-compose environment section
|
|
18
|
+
ENV PORT=3456
|
|
19
|
+
ENV ACCOUNTS_PATH=/app/accounts.json
|
|
20
|
+
|
|
21
|
+
# Install only production deps
|
|
22
|
+
COPY package*.json ./
|
|
23
|
+
RUN npm ci --omit=dev && npm cache clean --force
|
|
24
|
+
|
|
25
|
+
COPY --from=builder /app/dist ./dist
|
|
26
|
+
|
|
27
|
+
EXPOSE 3456
|
|
28
|
+
|
|
29
|
+
# accounts.json is expected to be mounted at runtime via docker-compose volume
|
|
30
|
+
# The container will exit with a clear error if it's not present
|
|
31
|
+
ENTRYPOINT ["node", "dist/cli/index.js"]
|
|
32
|
+
CMD ["start"]
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CC-Router Contributors
|
|
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,395 @@
|
|
|
1
|
+
# CC-Router
|
|
2
|
+
|
|
3
|
+
**Round-robin proxy for multiple Claude Max accounts.**
|
|
4
|
+
Distribute Claude Code requests across N subscriptions to multiply your throughput.
|
|
5
|
+
|
|
6
|
+
[](https://github.com/VictorMinemu/CC-Router/actions/workflows/ci.yml)
|
|
7
|
+
[](https://www.npmjs.com/package/ai-cc-router)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
> **Warning**
|
|
13
|
+
> Read the [disclaimer](#disclaimer) before using this tool.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## How it works
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Claude Code (terminal)
|
|
21
|
+
│
|
|
22
|
+
│ ANTHROPIC_BASE_URL=http://localhost:3456
|
|
23
|
+
│ ANTHROPIC_AUTH_TOKEN=proxy-managed
|
|
24
|
+
▼
|
|
25
|
+
┌─────────────────────────────────────┐
|
|
26
|
+
│ CC-Router :3456 │
|
|
27
|
+
│ │
|
|
28
|
+
│ 1. Receives request /v1/messages │
|
|
29
|
+
│ 2. Round-robin → picks account N │
|
|
30
|
+
│ 3. Refreshes token if expiring │
|
|
31
|
+
│ 4. Injects Authorization: Bearer │
|
|
32
|
+
│ 5. Forwards to Anthropic (or │
|
|
33
|
+
│ LiteLLM for advanced mode) │
|
|
34
|
+
└──────────────┬──────────────────────┘
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
api.anthropic.com
|
|
38
|
+
(authenticated with
|
|
39
|
+
OAuth token of account N)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
All standard Claude Code features work transparently: streaming, extended thinking, tool use, prompt caching.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Use cases
|
|
47
|
+
|
|
48
|
+
### Heavy user — one account isn't enough
|
|
49
|
+
|
|
50
|
+
Claude Max has rate limits per account. If you hit them regularly mid-session — waiting for cooldowns, getting 429s — you're a good candidate.
|
|
51
|
+
|
|
52
|
+
With two accounts you double your effective rate limit. With three, you triple it. The proxy distributes requests automatically; you don't change how you use Claude Code at all.
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
1 account → hit limit, wait 60s, continue
|
|
56
|
+
3 accounts → request rotates across all three, limit effectively tripled
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### Team sharing accounts — fewer subscriptions, same throughput
|
|
62
|
+
|
|
63
|
+
A team of five doesn't need five Max subscriptions. In practice, developers don't all peak at the same time. Three accounts can comfortably serve five people working normal hours.
|
|
64
|
+
|
|
65
|
+
#### Example setup: 5 devs, 3 accounts
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
cc-router (hosted on a shared machine or VPS)
|
|
69
|
+
│
|
|
70
|
+
├── max-account-1 ← alice's subscription
|
|
71
|
+
├── max-account-2 ← bob's subscription
|
|
72
|
+
└── max-account-3 ← carol's subscription
|
|
73
|
+
│
|
|
74
|
+
└── serves: alice, bob, carol, dave, eve
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Each developer sets their `ANTHROPIC_BASE_URL` to the shared proxy. Done. The proxy handles routing and token refresh invisibly.
|
|
78
|
+
|
|
79
|
+
#### Cost example
|
|
80
|
+
|
|
81
|
+
| Setup | Monthly cost |
|
|
82
|
+
|-------|-------------|
|
|
83
|
+
| 5 individual Max subscriptions | 5 × $100 = **$500/mo** |
|
|
84
|
+
| 3 shared via cc-router | 3 × $100 = **$300/mo** |
|
|
85
|
+
|
|
86
|
+
You save $200/mo without any loss in capability for a typical team workload.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### Hosting cc-router on a shared machine
|
|
91
|
+
|
|
92
|
+
Run cc-router on a machine everyone on the team can reach — a home server, a VPS, or a spare machine on the office network.
|
|
93
|
+
|
|
94
|
+
#### On the server
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm install -g cc-router
|
|
98
|
+
cc-router setup # configure the 3 shared accounts
|
|
99
|
+
cc-router service install # auto-start on boot
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
By default cc-router binds to `localhost`. To accept connections from other machines, set the `HOST` environment variable:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Listen on all interfaces (team LAN or VPS)
|
|
106
|
+
HOST=0.0.0.0 cc-router start
|
|
107
|
+
|
|
108
|
+
# Or configure it permanently in the service
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### On each developer's machine
|
|
112
|
+
|
|
113
|
+
No installation needed. Just set two environment variables in `~/.claude/settings.json`:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"env": {
|
|
118
|
+
"ANTHROPIC_BASE_URL": "http://192.168.1.50:3456",
|
|
119
|
+
"ANTHROPIC_AUTH_TOKEN": "proxy-managed"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Replace `192.168.1.50` with the server's IP or hostname. Then run `claude` normally.
|
|
125
|
+
|
|
126
|
+
Or use the CLI to write the settings automatically:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
cc-router configure --port 3456
|
|
130
|
+
# Then manually update ANTHROPIC_BASE_URL to the remote IP
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### Hosting on a VPS (internet-accessible)
|
|
136
|
+
|
|
137
|
+
If your team is distributed or works remotely, run cc-router on a VPS and expose it over HTTPS via a reverse proxy.
|
|
138
|
+
|
|
139
|
+
#### Recommended nginx config
|
|
140
|
+
|
|
141
|
+
```nginx
|
|
142
|
+
server {
|
|
143
|
+
listen 443 ssl;
|
|
144
|
+
server_name cc-router.yourcompany.com;
|
|
145
|
+
|
|
146
|
+
# ... SSL cert config (e.g. Let's Encrypt) ...
|
|
147
|
+
|
|
148
|
+
location / {
|
|
149
|
+
proxy_pass http://127.0.0.1:3456;
|
|
150
|
+
proxy_buffering off; # required for SSE streaming
|
|
151
|
+
proxy_read_timeout 300s; # required for long thinking requests
|
|
152
|
+
proxy_set_header X-Forwarded-For $remote_addr;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Each developer then points to:
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"env": {
|
|
161
|
+
"ANTHROPIC_BASE_URL": "https://cc-router.yourcompany.com",
|
|
162
|
+
"ANTHROPIC_AUTH_TOKEN": "proxy-managed"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Security note:** if the proxy is internet-accessible, add authentication at the nginx level (basic auth, mTLS, or IP allowlist) so only your team can use it. cc-router does not implement user authentication itself.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Quickstart
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# 1. Install
|
|
175
|
+
npm install -g cc-router
|
|
176
|
+
|
|
177
|
+
# 2. Wizard: extract tokens + configure Claude Code automatically
|
|
178
|
+
cc-router setup
|
|
179
|
+
|
|
180
|
+
# 3. Start the proxy
|
|
181
|
+
cc-router start
|
|
182
|
+
|
|
183
|
+
# 4. Use Claude Code normally — the proxy is transparent
|
|
184
|
+
claude
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
That's it. Claude Code will route through the proxy without any further changes.
|
|
188
|
+
|
|
189
|
+
**Optional:** install as a system service so it starts automatically on boot:
|
|
190
|
+
```bash
|
|
191
|
+
cc-router service install
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Installation
|
|
197
|
+
|
|
198
|
+
**Requirements:** Node.js 20 or 22.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npm install -g ai-cc-router
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Verify:
|
|
205
|
+
```bash
|
|
206
|
+
cc-router --version
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Setup by platform
|
|
212
|
+
|
|
213
|
+
### macOS
|
|
214
|
+
|
|
215
|
+
cc-router can extract OAuth tokens directly from the macOS Keychain — no manual copy-pasting needed.
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
cc-router setup
|
|
219
|
+
# Select "Extract automatically from macOS Keychain"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
For multiple accounts, you need to switch accounts in Claude Code between extractions:
|
|
223
|
+
```bash
|
|
224
|
+
# Account 1 is already logged in — run setup and extract
|
|
225
|
+
cc-router setup
|
|
226
|
+
|
|
227
|
+
# To add account 2:
|
|
228
|
+
claude logout && claude login # log in with account 2
|
|
229
|
+
cc-router setup --add # extract and merge
|
|
230
|
+
claude logout && claude login # log back in with account 1
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Linux
|
|
234
|
+
|
|
235
|
+
Tokens are read from `~/.claude/.credentials.json`:
|
|
236
|
+
```bash
|
|
237
|
+
cc-router setup
|
|
238
|
+
# Select "Read from ~/.claude/.credentials.json"
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Make sure Claude Code is installed and you have run `claude login` at least once.
|
|
242
|
+
|
|
243
|
+
### Windows
|
|
244
|
+
|
|
245
|
+
Same as Linux — tokens are read from `~/.claude/.credentials.json` (Windows path: `%USERPROFILE%\.claude\.credentials.json`).
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
cc-router setup
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## CLI Reference
|
|
254
|
+
|
|
255
|
+
```text
|
|
256
|
+
cc-router setup Interactive wizard: extract tokens + configure Claude Code
|
|
257
|
+
cc-router setup --add Add another account to an existing configuration
|
|
258
|
+
|
|
259
|
+
cc-router start Start proxy on localhost:3456 (foreground)
|
|
260
|
+
cc-router start --daemon Start in background via PM2
|
|
261
|
+
cc-router start --litellm Start with LiteLLM in Docker (advanced mode)
|
|
262
|
+
|
|
263
|
+
cc-router stop Stop proxy + restore Claude Code to normal auth
|
|
264
|
+
cc-router stop --keep-config Stop proxy only (keep settings.json)
|
|
265
|
+
cc-router revert Restore Claude Code to normal authentication
|
|
266
|
+
|
|
267
|
+
cc-router status Live dashboard (updates every 2s, press q to quit)
|
|
268
|
+
cc-router status --json Print current stats as JSON and exit
|
|
269
|
+
|
|
270
|
+
cc-router accounts list List configured accounts (live stats if proxy is running)
|
|
271
|
+
cc-router accounts add Add an account interactively
|
|
272
|
+
cc-router accounts remove <id> Remove an account
|
|
273
|
+
|
|
274
|
+
cc-router service install Register cc-router to start on system boot (PM2)
|
|
275
|
+
cc-router service uninstall Remove from system startup
|
|
276
|
+
cc-router service status Show PM2 service status
|
|
277
|
+
cc-router service logs Tail proxy logs from PM2
|
|
278
|
+
|
|
279
|
+
cc-router configure (Re)write ~/.claude/settings.json
|
|
280
|
+
cc-router configure --show Show current Claude Code proxy settings
|
|
281
|
+
cc-router configure --remove Remove cc-router settings (same as revert without stopping)
|
|
282
|
+
|
|
283
|
+
cc-router docker up Start full Docker stack (cc-router + LiteLLM)
|
|
284
|
+
cc-router docker up --build Rebuild cc-router image before starting
|
|
285
|
+
cc-router docker down Stop Docker containers
|
|
286
|
+
cc-router docker logs Tail all Docker logs
|
|
287
|
+
cc-router docker ps Show container status
|
|
288
|
+
cc-router docker restart [service] Restart a service
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Modes of operation
|
|
294
|
+
|
|
295
|
+
### Standalone (default — no Docker)
|
|
296
|
+
|
|
297
|
+
```text
|
|
298
|
+
Claude Code → cc-router:3456 → api.anthropic.com
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Best for personal use. No Docker required.
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
cc-router start
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Full mode with LiteLLM (optional — requires Docker)
|
|
308
|
+
|
|
309
|
+
```text
|
|
310
|
+
Claude Code → cc-router:3456 → LiteLLM:4000 → api.anthropic.com
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Adds a LiteLLM layer for usage logging, rate limiting, and a web dashboard at `http://localhost:4000/ui`.
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
cc-router docker up
|
|
317
|
+
# or: cc-router start --litellm
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
See [docs/litellm-setup.md](docs/litellm-setup.md) for details.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Reverting to normal Claude Code
|
|
325
|
+
|
|
326
|
+
To stop using cc-router and go back to normal Claude Code authentication:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
cc-router revert
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
This stops the proxy process and removes cc-router's settings from `~/.claude/settings.json`. Claude Code will use its own authentication on the next launch.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Status dashboard
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
cc-router status
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```text
|
|
343
|
+
CC-Router · standalone → api.anthropic.com · up 2h 14m · [q] quit
|
|
344
|
+
|
|
345
|
+
ACCOUNTS 2/2 healthy
|
|
346
|
+
|
|
347
|
+
● max-account-1 ok req 142 err 0 expires 6h 48m last 2s ago
|
|
348
|
+
● max-account-2 ok req 139 err 0 expires 6h 51m last 5s ago
|
|
349
|
+
|
|
350
|
+
TOTALS requests 281 · errors 0 · refreshes 2
|
|
351
|
+
|
|
352
|
+
RECENT ACTIVITY
|
|
353
|
+
14:23:01 → max-account-1 route
|
|
354
|
+
14:22:58 → max-account-2 route
|
|
355
|
+
14:22:45 ↻ max-account-1 refresh
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Press `q` to quit. Run with `--json` for non-interactive output.
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Security
|
|
363
|
+
|
|
364
|
+
- Tokens are stored locally in `~/.cc-router/accounts.json`, **never in the repository**
|
|
365
|
+
- The file is excluded by `.gitignore`
|
|
366
|
+
- Writes are atomic (write to `.tmp`, then rename) — no corruption on crash
|
|
367
|
+
- Keychain reads use `execFile` with a fixed argument array — no shell injection
|
|
368
|
+
- No telemetry, no external logging
|
|
369
|
+
|
|
370
|
+
See [docs/security.md](docs/security.md) for details.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Disclaimer
|
|
375
|
+
|
|
376
|
+
> CC-Router uses the OAuth tokens of your own Claude Max subscriptions.
|
|
377
|
+
>
|
|
378
|
+
> **Read Anthropic's Terms of Service before using this tool.**
|
|
379
|
+
> Using multiple Max subscriptions to increase throughput may violate the ToS. Anthropic has been known to ban accounts for unusual OAuth usage patterns.
|
|
380
|
+
>
|
|
381
|
+
> The authors are not responsible for any account bans, loss of access, or other consequences resulting from the use of this software. Use at your own risk.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Contributing
|
|
386
|
+
|
|
387
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
388
|
+
|
|
389
|
+
Bug reports → [GitHub Issues](https://github.com/VictorMinemu/CC-Router/issues)
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## License
|
|
394
|
+
|
|
395
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "max-account-1",
|
|
4
|
+
"accessToken": "sk-ant-oat01-REPLACE_ME",
|
|
5
|
+
"refreshToken": "sk-ant-ort01-REPLACE_ME",
|
|
6
|
+
"expiresAt": 1748658860000,
|
|
7
|
+
"scopes": ["user:inference", "user:profile"]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"id": "max-account-2",
|
|
11
|
+
"accessToken": "sk-ant-oat01-REPLACE_ME",
|
|
12
|
+
"refreshToken": "sk-ant-ort01-REPLACE_ME",
|
|
13
|
+
"expiresAt": 1748658860000,
|
|
14
|
+
"scopes": ["user:inference", "user:profile"]
|
|
15
|
+
}
|
|
16
|
+
]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadAccounts, accountsFileExists, writeAccountsAtomic } from "../config/manager.js";
|
|
3
|
+
import { saveAccounts } from "../proxy/token-refresher.js";
|
|
4
|
+
import { formatExpiry, redactToken } from "../utils/token-extractor.js";
|
|
5
|
+
import { PROXY_PORT } from "../config/paths.js";
|
|
6
|
+
export function registerAccounts(program) {
|
|
7
|
+
const accounts = program
|
|
8
|
+
.command("accounts")
|
|
9
|
+
.description("Manage Claude Max accounts in the token pool");
|
|
10
|
+
// ── accounts list ────────────────────────────────────────────────────────
|
|
11
|
+
accounts
|
|
12
|
+
.command("list")
|
|
13
|
+
.description("List all configured accounts and their status")
|
|
14
|
+
.option("--json", "Output as JSON")
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
// Try to get live stats from the running proxy first
|
|
17
|
+
const liveStats = await fetchLiveStats();
|
|
18
|
+
if (!accountsFileExists()) {
|
|
19
|
+
console.log(chalk.yellow("No accounts configured. Run: cc-router setup"));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const stored = loadAccounts();
|
|
23
|
+
if (stored.length === 0) {
|
|
24
|
+
console.log(chalk.yellow("accounts.json is empty. Run: cc-router setup"));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (opts.json) {
|
|
28
|
+
console.log(JSON.stringify(liveStats ?? stored, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
console.log(chalk.bold(`\n Accounts (${stored.length} configured)\n`));
|
|
32
|
+
if (liveStats) {
|
|
33
|
+
console.log(chalk.green(" ● Proxy is running — showing live stats\n"));
|
|
34
|
+
for (const s of liveStats) {
|
|
35
|
+
const status = s.healthy
|
|
36
|
+
? chalk.green("✓ healthy")
|
|
37
|
+
: chalk.red("✗ unhealthy");
|
|
38
|
+
const busy = s.busy ? chalk.yellow(" [busy]") : "";
|
|
39
|
+
const exp = s.expiresInMs > 0
|
|
40
|
+
? chalk.yellow(formatMs(s.expiresInMs))
|
|
41
|
+
: chalk.red("EXPIRED");
|
|
42
|
+
console.log(` ${chalk.bold(s.id.padEnd(24))}` +
|
|
43
|
+
` ${status}${busy}` +
|
|
44
|
+
` requests: ${chalk.cyan(String(s.requestCount).padStart(5))}` +
|
|
45
|
+
` errors: ${chalk.red(String(s.errorCount).padStart(3))}` +
|
|
46
|
+
` expires: ${exp}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(chalk.gray(" (Proxy not running — showing stored configuration)\n"));
|
|
51
|
+
for (const a of stored) {
|
|
52
|
+
const exp = formatExpiry(a.tokens.expiresAt);
|
|
53
|
+
const expColor = a.tokens.expiresAt > Date.now()
|
|
54
|
+
? chalk.yellow(exp)
|
|
55
|
+
: chalk.red(exp);
|
|
56
|
+
console.log(` ${chalk.bold(a.id.padEnd(24))}` +
|
|
57
|
+
` ${redactToken(a.tokens.accessToken).padEnd(26)}` +
|
|
58
|
+
` expires: ${expColor}` +
|
|
59
|
+
` scopes: ${chalk.gray(a.tokens.scopes.join(" "))}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log();
|
|
63
|
+
});
|
|
64
|
+
// ── accounts add ─────────────────────────────────────────────────────────
|
|
65
|
+
accounts
|
|
66
|
+
.command("add")
|
|
67
|
+
.description("Add a new Claude Max account interactively")
|
|
68
|
+
.action(async () => {
|
|
69
|
+
const { setupSingleAccount } = await import("./cmd-setup.js");
|
|
70
|
+
const existing = accountsFileExists() ? loadAccounts() : [];
|
|
71
|
+
const account = await setupSingleAccount(existing.length + 1);
|
|
72
|
+
if (!account) {
|
|
73
|
+
console.log(chalk.yellow("\nNo account added.\n"));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Merge: replace by ID if already exists, otherwise append
|
|
77
|
+
const merged = [
|
|
78
|
+
...existing.filter(a => a.id !== account.id),
|
|
79
|
+
account,
|
|
80
|
+
];
|
|
81
|
+
saveAccounts(merged);
|
|
82
|
+
console.log(chalk.green(`\n✓ Account "${account.id}" added (${merged.length} total).\n`));
|
|
83
|
+
console.log(chalk.gray(" Restart the proxy to load the new account: cc-router start\n"));
|
|
84
|
+
});
|
|
85
|
+
// ── accounts remove ───────────────────────────────────────────────────────
|
|
86
|
+
accounts
|
|
87
|
+
.command("remove <id>")
|
|
88
|
+
.description("Remove an account by its ID")
|
|
89
|
+
.action(async (id) => {
|
|
90
|
+
if (!accountsFileExists()) {
|
|
91
|
+
console.log(chalk.yellow("No accounts configured."));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const existing = loadAccounts();
|
|
95
|
+
const filtered = existing.filter(a => a.id !== id);
|
|
96
|
+
if (filtered.length === existing.length) {
|
|
97
|
+
console.log(chalk.red(`✗ Account "${id}" not found.`));
|
|
98
|
+
console.log(chalk.gray(` Available: ${existing.map(a => a.id).join(", ")}`));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
102
|
+
const sure = await confirm({
|
|
103
|
+
message: `Remove "${id}"? This cannot be undone.`,
|
|
104
|
+
default: false,
|
|
105
|
+
});
|
|
106
|
+
if (!sure) {
|
|
107
|
+
console.log(chalk.gray("Cancelled."));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
writeAccountsAtomic(filtered.map(a => ({
|
|
111
|
+
id: a.id,
|
|
112
|
+
accessToken: a.tokens.accessToken,
|
|
113
|
+
refreshToken: a.tokens.refreshToken,
|
|
114
|
+
expiresAt: a.tokens.expiresAt,
|
|
115
|
+
scopes: a.tokens.scopes,
|
|
116
|
+
})));
|
|
117
|
+
console.log(chalk.green(`✓ Removed "${id}". ${filtered.length} account(s) remaining.`));
|
|
118
|
+
if (filtered.length === 0) {
|
|
119
|
+
console.log(chalk.yellow(" No accounts left. Run: cc-router setup"));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
124
|
+
async function fetchLiveStats() {
|
|
125
|
+
try {
|
|
126
|
+
const res = await fetch(`http://localhost:${PROXY_PORT}/cc-router/health`, {
|
|
127
|
+
signal: AbortSignal.timeout(1_000),
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
return null;
|
|
131
|
+
const data = await res.json();
|
|
132
|
+
return data.accounts;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function formatMs(ms) {
|
|
139
|
+
const h = Math.floor(ms / 3_600_000);
|
|
140
|
+
const m = Math.floor((ms % 3_600_000) / 60_000);
|
|
141
|
+
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
|
142
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { writeClaudeSettings, removeClaudeSettings, readClaudeProxySettings } from "../utils/claude-config.js";
|
|
3
|
+
import { PROXY_PORT, CLAUDE_SETTINGS_PATH } from "../config/paths.js";
|
|
4
|
+
export function registerConfigure(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("configure")
|
|
7
|
+
.description("Update ~/.claude/settings.json to point to the proxy (or remove the config)")
|
|
8
|
+
.option("--remove", "Remove cc-router settings from ~/.claude/settings.json")
|
|
9
|
+
.option("--port <port>", "Proxy port to configure", String(PROXY_PORT))
|
|
10
|
+
.option("--show", "Show current Claude Code proxy settings")
|
|
11
|
+
.action((opts) => {
|
|
12
|
+
if (opts.show) {
|
|
13
|
+
const current = readClaudeProxySettings();
|
|
14
|
+
if (current.baseUrl) {
|
|
15
|
+
console.log(chalk.green(" Claude Code is configured to use cc-router:"));
|
|
16
|
+
console.log(` ANTHROPIC_BASE_URL = ${chalk.cyan(current.baseUrl)}`);
|
|
17
|
+
console.log(` ANTHROPIC_AUTH_TOKEN = ${chalk.gray(current.authToken ?? "(not set)")}`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(chalk.yellow(" Claude Code is NOT configured to use cc-router."));
|
|
21
|
+
console.log(chalk.gray(` Run: cc-router configure`));
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (opts.remove) {
|
|
26
|
+
removeClaudeSettings();
|
|
27
|
+
console.log(chalk.green("✓ Removed cc-router settings from ~/.claude/settings.json"));
|
|
28
|
+
console.log(chalk.gray(" Claude Code will use its default authentication on next launch."));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const port = parseInt(opts.port, 10);
|
|
32
|
+
writeClaudeSettings(port);
|
|
33
|
+
console.log(chalk.green(`✓ Updated ${CLAUDE_SETTINGS_PATH}`));
|
|
34
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL = http://localhost:${port}`));
|
|
35
|
+
console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN = proxy-managed`));
|
|
36
|
+
});
|
|
37
|
+
}
|