cchost 0.3.0 → 0.3.1
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/CHANGELOG.md +34 -0
- package/README.md +64 -16
- package/lib/cli.js +257 -109
- package/lib/env.js +20 -45
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ All notable changes to `cchost` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.3.1] - 2026-05-19
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Custom API endpoints are now configured through `cc add`, not `cc env`.**
|
|
13
|
+
The standalone `cc env` command (shipped in 0.3.0) is removed; endpoint
|
|
14
|
+
configuration is folded into the profile-creation command:
|
|
15
|
+
- `cc add <profile> --custom` — create a custom-endpoint profile via an
|
|
16
|
+
interactive wizard; the token is never echoed.
|
|
17
|
+
- `cc add <profile> --custom --base-url=URL --token=TOKEN [--model/--opus/--sonnet/--haiku/--subagent=NAME]`
|
|
18
|
+
— non-interactive create.
|
|
19
|
+
- Re-running `cc add <profile> --custom` on an existing endpoint profile
|
|
20
|
+
edits it in place (wizard prefills current values; with flags, only the
|
|
21
|
+
flags you pass are changed).
|
|
22
|
+
- `cc add --custom` is atomic — cancelling the wizard leaves no profile
|
|
23
|
+
behind.
|
|
24
|
+
- **Endpoint inspection moved to `cc list`.** `cc env show` is replaced by
|
|
25
|
+
`cc list <profile>` — a per-profile detail view. `cc list <profile> --reveal`
|
|
26
|
+
unmasks the API token; `cc list <profile> --json` emits it as an object
|
|
27
|
+
(token always masked in JSON).
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
|
|
31
|
+
- `cc env` and `cc env show` — superseded by `cc add --custom` and
|
|
32
|
+
`cc list <profile>`.
|
|
33
|
+
|
|
34
|
+
### Notes
|
|
35
|
+
|
|
36
|
+
- A profile is still born subscription or endpoint and stays that way:
|
|
37
|
+
`cc add --custom` refuses a profile that already has Anthropic OAuth
|
|
38
|
+
credentials. `.cc-env.json` is still never migrated and never linked, and is
|
|
39
|
+
still written with user-only file permissions.
|
|
40
|
+
|
|
8
41
|
## [0.3.0] - 2026-05-19
|
|
9
42
|
|
|
10
43
|
### Added
|
|
@@ -71,6 +104,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
71
104
|
per-process state and must stay per-profile.
|
|
72
105
|
- Cross-platform claim narrowed to the verified scope (Windows and Linux).
|
|
73
106
|
|
|
107
|
+
[0.3.1]: https://github.com/allenhack638/cchost/releases/tag/v0.3.1
|
|
74
108
|
[0.3.0]: https://github.com/allenhack638/cchost/releases/tag/v0.3.0
|
|
75
109
|
[0.2.1]: https://github.com/allenhack638/cchost/releases/tag/v0.2.1
|
|
76
110
|
[0.2.0]: https://github.com/allenhack638/cchost/releases/tag/v0.2.0
|
package/README.md
CHANGED
|
@@ -16,11 +16,10 @@ Requires Node.js 18+ and Claude Code 2.1.140+ already installed. Get Claude Code
|
|
|
16
16
|
|
|
17
17
|
| Command | What it does |
|
|
18
18
|
| --- | --- |
|
|
19
|
-
| `cc add <profile>` | Create a profile
|
|
19
|
+
| `cc add <profile>` | Create a subscription profile (logs in with Anthropic OAuth on first use). |
|
|
20
|
+
| `cc add <profile> --custom [...]` | Create — or, if it already exists, edit — a profile routed through a custom API endpoint. Opens a wizard, or takes `--base-url=URL --token=TOKEN [--model=…]`. See [Custom endpoints](#custom-endpoints-kimi-openrouter-etc). |
|
|
20
21
|
| `cc use <profile> [...args]` | Launch Claude under that profile. Profile must already exist. Args after the name are forwarded verbatim to `claude`. |
|
|
21
|
-
| `cc list [--json]` |
|
|
22
|
-
| `cc env <profile> [...]` | Configure a custom API endpoint for a profile (interactive wizard or flags). See [Custom endpoints](#custom-endpoints-kimi-openrouter-etc). |
|
|
23
|
-
| `cc env <profile> show [--reveal]` | Print a profile's endpoint config. Token masked by default; `--reveal` prints it in full. |
|
|
22
|
+
| `cc list [<profile>] [--json] [--reveal]` | List every profile (login state, email, shared dirs N/4, endpoint), or show one profile in detail. `--reveal` unmasks a custom endpoint's API token. |
|
|
24
23
|
| `cc migrate <src> <dest> [--force]` | Copy artifacts from `default`\|`shared` to `shared`\|`<profile>`. Skip-on-collision by default; `--force` overwrites. |
|
|
25
24
|
| `cc link <profile> [<profile>...]` | Link a profile's artifact dirs to `~/.claude-shared/`. Migrates existing content; renames on collision. |
|
|
26
25
|
| `cc unlink <profile>` | Restore a profile to its own private artifact dirs (copies shared content back). |
|
|
@@ -49,6 +48,29 @@ scratch false - 0/4 subscription
|
|
|
49
48
|
work true work@example.com 0/4 subscription
|
|
50
49
|
```
|
|
51
50
|
|
|
51
|
+
The **Shared** column is a count: how many of the 4 shareable directories
|
|
52
|
+
(`projects`, `skills`, `agents`, `commands`) are linked to `~/.claude-shared/`.
|
|
53
|
+
|
|
54
|
+
- `0/4` — fully isolated; the profile keeps its own private copies.
|
|
55
|
+
- `4/4` — fully shared; all four point at the common pool.
|
|
56
|
+
- `1/4`, `2/4`, `3/4` — **partial**, and not a normal state. `cc link` links
|
|
57
|
+
all four at once and `cc unlink` unlinks all four at once, so you should
|
|
58
|
+
normally see only `0/4` or `4/4`.
|
|
59
|
+
|
|
60
|
+
A partial count has two causes:
|
|
61
|
+
|
|
62
|
+
1. **A link partially failed** — one directory could not be linked (a
|
|
63
|
+
permission issue, or it was locked) while the others succeeded. `cc link`
|
|
64
|
+
reports per-directory failures so this is never silent.
|
|
65
|
+
2. **An upgrade from an older cchost.** Versions before `0.2.1` linked only
|
|
66
|
+
`projects/` — the other three shareable directories did not exist yet. A
|
|
67
|
+
profile linked under an older version therefore shows `1/4` after you
|
|
68
|
+
upgrade.
|
|
69
|
+
|
|
70
|
+
In both cases the fix is the same — re-run `cc link <profile>`. Already-linked
|
|
71
|
+
directories are skipped and the missing ones are linked, bringing the profile
|
|
72
|
+
to `4/4`.
|
|
73
|
+
|
|
52
74
|
### What migrate copies and what link shares
|
|
53
75
|
|
|
54
76
|
| Artifact | `cc migrate` copies it | `cc link` shares it | Notes |
|
|
@@ -104,31 +126,57 @@ If two profiles share history and you launch them against the same project at th
|
|
|
104
126
|
|
|
105
127
|
## Custom endpoints (Kimi, OpenRouter, etc.)
|
|
106
128
|
|
|
107
|
-
A profile can route through any Anthropic-compatible API endpoint instead of Anthropic's subscription OAuth — Moonshot/Kimi, OpenRouter, Requesty, a self-hosted vLLM, or a corporate proxy.
|
|
129
|
+
A profile can route through any Anthropic-compatible API endpoint instead of Anthropic's subscription OAuth — Moonshot/Kimi, OpenRouter, Requesty, a self-hosted vLLM, or a corporate proxy. You declare this when you create the profile, with `cc add <profile> --custom`.
|
|
108
130
|
|
|
109
|
-
> **Billing:**
|
|
131
|
+
> **Billing:** a custom-endpoint profile bills through the **third-party provider**, using the API key you supply — **not** your Anthropic subscription. You are responsible for that provider's usage charges. A subscription profile is unaffected.
|
|
110
132
|
|
|
111
|
-
|
|
133
|
+
A custom-endpoint profile and a subscription profile are mutually exclusive: a profile is born one or the other and stays that way. `cc add --custom` refuses to run on a profile that already has Anthropic OAuth credentials — create custom-endpoint profiles fresh.
|
|
112
134
|
|
|
113
135
|
### Worked example — Moonshot / Kimi
|
|
114
136
|
|
|
115
137
|
```bash
|
|
116
|
-
cc add kimi
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
--model=kimi-k2.5
|
|
138
|
+
cc add kimi --custom --base-url=https://api.moonshot.ai/anthropic \
|
|
139
|
+
--token=sk-your-moonshot-key \
|
|
140
|
+
--model=kimi-k2.5
|
|
120
141
|
cc use kimi
|
|
121
142
|
```
|
|
122
143
|
|
|
123
144
|
Moonshot's endpoint URL ends in `/anthropic` — Claude Code appends `/v1/messages` itself, so do **not** add `/v1`. Most third-party providers serve a single model name; pass it as `--model` and it is applied to every tier (opus/sonnet/haiku/subagent). To override a tier, pass `--opus=`, `--sonnet=`, `--haiku=`, or `--subagent=`.
|
|
124
145
|
|
|
125
|
-
Run `cc
|
|
146
|
+
Run `cc add <profile> --custom` with no value flags for an interactive wizard instead — it prompts for every field and never echoes the token. `cc add --custom` is atomic: if you cancel the wizard, no profile is left behind.
|
|
147
|
+
|
|
148
|
+
### Editing a custom-endpoint profile
|
|
149
|
+
|
|
150
|
+
Re-run `cc add <profile> --custom` on a profile that already has a custom endpoint and it switches to **edit mode**: the wizard prefills current values, and with flags only the flags you pass are changed.
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
cc add kimi --custom --model=kimi-k3 # change just the model
|
|
154
|
+
cc add kimi --custom --token=sk-rotated # rotate the API key
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Inspect a config with `cc list <profile>` — the API token is masked:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Profile: kimi
|
|
161
|
+
Directory: /home/you/.claude-profiles/kimi
|
|
162
|
+
LoggedIn: false
|
|
163
|
+
Email: -
|
|
164
|
+
Shared: 0/4
|
|
165
|
+
Endpoint: api.moonshot.ai
|
|
166
|
+
Base URL: https://api.moonshot.ai/anthropic
|
|
167
|
+
Token: sk-you...key9 (32 chars)
|
|
168
|
+
Model: kimi-k2.5
|
|
169
|
+
Opus: kimi-k2.5
|
|
170
|
+
Sonnet: kimi-k2.5
|
|
171
|
+
Haiku: kimi-k2.5
|
|
172
|
+
Subagent: kimi-k2.5
|
|
173
|
+
```
|
|
126
174
|
|
|
127
|
-
|
|
175
|
+
`cc list <profile> --reveal` prints the full API key instead — do not paste that output anywhere.
|
|
128
176
|
|
|
129
|
-
There is no
|
|
177
|
+
There is no way to convert a custom-endpoint profile back to a subscription profile. To stop using an endpoint, `cc remove` the profile and recreate it.
|
|
130
178
|
|
|
131
|
-
When you launch
|
|
179
|
+
When you launch a custom-endpoint profile, `cc use` prints a one-line reminder to stderr:
|
|
132
180
|
|
|
133
181
|
```
|
|
134
182
|
[cchost] Profile 'kimi' → api.moonshot.ai (custom endpoint billing applies)
|
|
@@ -151,7 +199,7 @@ The env var is set on the child process only — that's all `claude` needs.
|
|
|
151
199
|
|
|
152
200
|
`~/.claude-profiles/<profile>/` holds real OAuth credentials. Treat it like SSH keys: don't commit it, don't share it, don't sync it to public cloud storage. The `.gitignore` shipped with this project excludes profile directories from anywhere they might land. `cc migrate` and `cc link` never copy `.credentials.json` or `.claude.json` across profiles.
|
|
153
201
|
|
|
154
|
-
`.cc-env.json` (custom-endpoint config) contains a third-party **API key** — treat it exactly like `.credentials.json`. It is written with user-only file permissions, and `cc migrate`/`cc link` never copy or link it across profiles. `cc
|
|
202
|
+
`.cc-env.json` (custom-endpoint config) contains a third-party **API key** — treat it exactly like `.credentials.json`. It is written with user-only file permissions, and `cc migrate`/`cc link` never copy or link it across profiles. `cc list <profile>` masks the token; only `cc list <profile> --reveal` prints it in full.
|
|
155
203
|
|
|
156
204
|
## Troubleshooting
|
|
157
205
|
|
package/lib/cli.js
CHANGED
|
@@ -15,10 +15,9 @@ const color = require('./color');
|
|
|
15
15
|
const HELP = `cc - Multi-profile launcher for Claude Code
|
|
16
16
|
|
|
17
17
|
Usage:
|
|
18
|
-
cc add <profile>
|
|
18
|
+
cc add <profile> [--custom [...]] Create a profile (--custom: route through a custom API endpoint)
|
|
19
19
|
cc use <profile> [...args] Launch Claude under a named profile
|
|
20
|
-
cc list [--json]
|
|
21
|
-
cc env <profile> [show] [...] Configure a custom API endpoint for a profile (wizard or flags)
|
|
20
|
+
cc list [<profile>] [--json] [--reveal] List all profiles, or show one profile in detail
|
|
22
21
|
cc migrate <src> <dest> [--force] Copy artifact dirs (and config files) between profiles
|
|
23
22
|
cc link <p>... Link each profile's artifact dirs to ~/.claude-shared/
|
|
24
23
|
cc unlink <profile> Restore a profile's private artifact dirs
|
|
@@ -32,13 +31,43 @@ for that launch. Args after the profile name are forwarded verbatim to claude.
|
|
|
32
31
|
`;
|
|
33
32
|
|
|
34
33
|
const HELP_BY_CMD = {
|
|
35
|
-
add: `cc add <profile>
|
|
34
|
+
add: `cc add <profile> [--custom [--base-url=URL --token=TOKEN ...]]
|
|
36
35
|
|
|
37
36
|
Create a new profile directory without launching Claude.
|
|
38
37
|
|
|
38
|
+
cc add <profile>
|
|
39
|
+
Create a regular profile. It logs in with your Anthropic
|
|
40
|
+
subscription (OAuth) on the first 'cc use'.
|
|
41
|
+
|
|
42
|
+
cc add <profile> --custom
|
|
43
|
+
Create a profile routed through a custom, Anthropic-compatible API
|
|
44
|
+
endpoint — Moonshot/Kimi, OpenRouter, a self-hosted vLLM, a corporate
|
|
45
|
+
proxy, and so on. Opens an interactive wizard; the API token is never
|
|
46
|
+
echoed to the screen.
|
|
47
|
+
|
|
48
|
+
cc add <profile> --custom --base-url=URL --token=TOKEN
|
|
49
|
+
[--model=NAME] [--opus=NAME] [--sonnet=NAME]
|
|
50
|
+
[--haiku=NAME] [--subagent=NAME]
|
|
51
|
+
Same, non-interactive. --base-url and --token are required.
|
|
52
|
+
|
|
53
|
+
Re-running 'cc add <profile> --custom' on an existing endpoint profile
|
|
54
|
+
EDITS its configuration: the wizard prefills current values, and with
|
|
55
|
+
flags only the flags you pass are changed. View a config with
|
|
56
|
+
'cc list <profile>'.
|
|
57
|
+
|
|
58
|
+
Billing: a custom-endpoint profile bills through the third-party provider,
|
|
59
|
+
NOT your Anthropic subscription. A profile is born subscription or endpoint
|
|
60
|
+
and stays that way — 'cc add --custom' refuses a profile that already has
|
|
61
|
+
Anthropic OAuth credentials.
|
|
62
|
+
|
|
63
|
+
The endpoint config lives at ~/.claude-profiles/<profile>/.cc-env.json with
|
|
64
|
+
user-only permissions. It is never migrated and never linked.
|
|
65
|
+
|
|
39
66
|
Examples:
|
|
40
67
|
cc add work
|
|
41
|
-
cc add
|
|
68
|
+
cc add kimi --custom
|
|
69
|
+
cc add kimi --custom --base-url=https://api.moonshot.ai/anthropic --token=sk-... --model=kimi-k2.5
|
|
70
|
+
cc add kimi --custom --model=kimi-k3 # edit just the model later
|
|
42
71
|
`,
|
|
43
72
|
use: `cc use <profile> [...args]
|
|
44
73
|
|
|
@@ -54,7 +83,9 @@ Examples:
|
|
|
54
83
|
cc use work --resume
|
|
55
84
|
cc use personal -p "summarize this codebase"
|
|
56
85
|
`,
|
|
57
|
-
list: `cc list [--json]
|
|
86
|
+
list: `cc list [<profile>] [--json] [--reveal]
|
|
87
|
+
|
|
88
|
+
Without a profile name: list every profile as a table.
|
|
58
89
|
|
|
59
90
|
Columns:
|
|
60
91
|
Profile - profile name
|
|
@@ -66,45 +97,15 @@ Columns:
|
|
|
66
97
|
Endpoint - custom-endpoint host from .cc-env.json, or "subscription" if the
|
|
67
98
|
profile uses Anthropic's subscription OAuth
|
|
68
99
|
|
|
69
|
-
With
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Configure a custom, Anthropic-compatible API endpoint for a profile. This
|
|
74
|
-
routes the profile through a third-party provider (Moonshot/Kimi, OpenRouter,
|
|
75
|
-
vLLM, a corporate proxy, ...) instead of Anthropic's subscription OAuth.
|
|
76
|
-
|
|
77
|
-
Billing: an endpoint profile bills through the third-party provider, NOT your
|
|
78
|
-
Anthropic subscription.
|
|
100
|
+
With a profile name: print that one profile's detail, including its custom
|
|
101
|
+
endpoint configuration if it has one. The API token is masked by default;
|
|
102
|
+
--reveal prints it in full — do not paste that output anywhere.
|
|
79
103
|
|
|
80
|
-
|
|
81
|
-
cc
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
cc env <profile> --base-url=URL --token=TOKEN
|
|
87
|
-
[--model=NAME] [--opus=NAME] [--sonnet=NAME]
|
|
88
|
-
[--haiku=NAME] [--subagent=NAME]
|
|
89
|
-
Non-interactive. --base-url and --token are required when creating;
|
|
90
|
-
when editing, only the flags you pass are changed.
|
|
91
|
-
|
|
92
|
-
cc env <profile> show [--reveal]
|
|
93
|
-
Print the endpoint configuration. The token is masked by default.
|
|
94
|
-
--reveal prints the FULL API key — do not paste that output anywhere.
|
|
95
|
-
|
|
96
|
-
A profile is born either OAuth or endpoint and stays that way. cc env refuses
|
|
97
|
-
to run on a profile that already has OAuth credentials. To stop using an
|
|
98
|
-
endpoint, run 'cc remove <profile>' and recreate the profile.
|
|
99
|
-
|
|
100
|
-
The config lives at ~/.claude-profiles/<profile>/.cc-env.json with user-only
|
|
101
|
-
permissions. It is never migrated and never linked.
|
|
102
|
-
|
|
103
|
-
Examples:
|
|
104
|
-
cc add kimi
|
|
105
|
-
cc env kimi --base-url=https://api.moonshot.ai/anthropic --token=sk-... --model=kimi-k2.5
|
|
106
|
-
cc env kimi show
|
|
107
|
-
cc use kimi
|
|
104
|
+
cc list Table of all profiles
|
|
105
|
+
cc list --json Same, as a JSON array
|
|
106
|
+
cc list <profile> Detail for a single profile
|
|
107
|
+
cc list <profile> --reveal Detail with the full API token shown
|
|
108
|
+
cc list <profile> --json Detail as a JSON object (token always masked)
|
|
108
109
|
`,
|
|
109
110
|
migrate: `cc migrate <src> <dest> [--force]
|
|
110
111
|
|
|
@@ -273,14 +274,104 @@ function parseFlagsStrict(args, flagNames, { allowPositional = false } = {}) {
|
|
|
273
274
|
return { flags, positional };
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
// Flag name -> .cc-env.json field, for `cc add --custom`.
|
|
278
|
+
const ENDPOINT_VALUE_FLAGS = {
|
|
279
|
+
'--base-url': 'base_url',
|
|
280
|
+
'--token': 'auth_token',
|
|
281
|
+
'--model': 'model',
|
|
282
|
+
'--opus': 'opus',
|
|
283
|
+
'--sonnet': 'sonnet',
|
|
284
|
+
'--haiku': 'haiku',
|
|
285
|
+
'--subagent': 'subagent',
|
|
286
|
+
};
|
|
287
|
+
|
|
276
288
|
async function cmdAdd(rest) {
|
|
277
|
-
const
|
|
278
|
-
|
|
289
|
+
const positional = [];
|
|
290
|
+
const provided = {};
|
|
291
|
+
let custom = false;
|
|
292
|
+
|
|
293
|
+
for (const a of rest) {
|
|
294
|
+
if (a.startsWith('-')) {
|
|
295
|
+
const eq = a.indexOf('=');
|
|
296
|
+
const flagName = eq === -1 ? a : a.slice(0, eq);
|
|
297
|
+
if (flagName === '--custom') {
|
|
298
|
+
custom = true;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (Object.prototype.hasOwnProperty.call(ENDPOINT_VALUE_FLAGS, flagName)) {
|
|
302
|
+
if (eq === -1) {
|
|
303
|
+
throw usageError(`Flag ${flagName} requires a value: ${flagName}=VALUE`);
|
|
304
|
+
}
|
|
305
|
+
provided[ENDPOINT_VALUE_FLAGS[flagName]] = a.slice(eq + 1);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
throw usageError(`Unknown flag: ${flagName}`);
|
|
309
|
+
}
|
|
310
|
+
positional.push(a);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const [name, ...extra] = positional;
|
|
314
|
+
if (!name) {
|
|
315
|
+
throw usageError('Usage: cc add <profile> [--custom [--base-url=URL --token=TOKEN ...]]');
|
|
316
|
+
}
|
|
279
317
|
if (extra.length > 0) throw usageError(`cc add takes no extra arguments: ${extra[0]}`);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
318
|
+
profiles.validateProfileName(name);
|
|
319
|
+
|
|
320
|
+
if (Object.keys(provided).length > 0 && !custom) {
|
|
321
|
+
throw usageError(
|
|
322
|
+
'Endpoint flags (--base-url, --token, --model, ...) require --custom: ' +
|
|
323
|
+
`cc add ${name} --custom ...`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Plain subscription profile.
|
|
328
|
+
if (!custom) {
|
|
329
|
+
const { dir } = profiles.addProfile(name);
|
|
330
|
+
console.log(`Created profile "${name}" at ${dir}.`);
|
|
331
|
+
console.log(`Next: cc use ${name}`);
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Custom endpoint profile. A fresh profile is created and configured; an
|
|
336
|
+
// existing endpoint profile is edited in place. Either way the directory
|
|
337
|
+
// must exist before runConfigure reads/writes .cc-env.json inside it.
|
|
338
|
+
const dir = profiles.profileDir(name);
|
|
339
|
+
const existed = fs.existsSync(dir);
|
|
340
|
+
if (!existed) profiles.addProfile(name);
|
|
341
|
+
|
|
342
|
+
let result;
|
|
343
|
+
try {
|
|
344
|
+
result = await envLib.runConfigure(name, dir, provided);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
// A freshly-created profile that could not be configured leaves nothing
|
|
347
|
+
// behind — `cc add --custom` is atomic.
|
|
348
|
+
if (!existed) {
|
|
349
|
+
try {
|
|
350
|
+
removeRecursivelyPreservingLinks(dir);
|
|
351
|
+
} catch {
|
|
352
|
+
// best-effort cleanup
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!result.written) {
|
|
359
|
+
// Wizard cancelled.
|
|
360
|
+
if (!existed) {
|
|
361
|
+
try {
|
|
362
|
+
removeRecursivelyPreservingLinks(dir);
|
|
363
|
+
} catch {
|
|
364
|
+
// best-effort cleanup
|
|
365
|
+
}
|
|
366
|
+
console.log(`Cancelled. Profile "${name}" was not created.`);
|
|
367
|
+
} else {
|
|
368
|
+
console.log('Cancelled. No changes written.');
|
|
369
|
+
}
|
|
370
|
+
return result.code;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!existed) console.log(`Next: cc use ${name}`);
|
|
374
|
+
return result.code;
|
|
284
375
|
}
|
|
285
376
|
|
|
286
377
|
async function cmdUse(rest) {
|
|
@@ -308,8 +399,113 @@ async function cmdUse(rest) {
|
|
|
308
399
|
return spawnClaude({ args: passthrough, profileDir: dir, extraEnv });
|
|
309
400
|
}
|
|
310
401
|
|
|
402
|
+
// `cc list <profile>` — detail for a single profile, including its custom
|
|
403
|
+
// endpoint configuration if it has one.
|
|
404
|
+
function cmdListOne(name, { json, reveal }) {
|
|
405
|
+
if (json && reveal) {
|
|
406
|
+
throw usageError('--reveal cannot be combined with --json.');
|
|
407
|
+
}
|
|
408
|
+
profiles.validateProfileName(name);
|
|
409
|
+
const dir = profiles.profileDir(name);
|
|
410
|
+
if (!fs.existsSync(dir)) {
|
|
411
|
+
const e = new Error(`Profile "${name}" does not exist. Create it with: cc add ${name}`);
|
|
412
|
+
e.exitCode = 1;
|
|
413
|
+
throw e;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const total = share.SHARED_SUBDIRS.length;
|
|
417
|
+
const sharedCount = share.SHARED_SUBDIRS.filter((sub) =>
|
|
418
|
+
isLinkLike(path.join(dir, sub)),
|
|
419
|
+
).length;
|
|
420
|
+
const loggedIn = profiles.hasCredentials(dir);
|
|
421
|
+
const email = profiles.readEmail(dir);
|
|
422
|
+
|
|
423
|
+
let config = null;
|
|
424
|
+
let configError = false;
|
|
425
|
+
try {
|
|
426
|
+
config = envLib.readEnvConfig(dir);
|
|
427
|
+
} catch {
|
|
428
|
+
configError = true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (json) {
|
|
432
|
+
const obj = {
|
|
433
|
+
name,
|
|
434
|
+
dir,
|
|
435
|
+
loggedIn,
|
|
436
|
+
email,
|
|
437
|
+
shared: sharedCount,
|
|
438
|
+
endpoint: envLib.endpointSummary(dir),
|
|
439
|
+
};
|
|
440
|
+
if (config) {
|
|
441
|
+
obj.endpointConfig = {
|
|
442
|
+
base_url: config.base_url || null,
|
|
443
|
+
// The token is always masked in JSON output — scripts never need it.
|
|
444
|
+
auth_token: config.auth_token ? envLib.maskToken(config.auth_token) : null,
|
|
445
|
+
model: config.model || null,
|
|
446
|
+
opus: config.opus || config.model || null,
|
|
447
|
+
sonnet: config.sonnet || config.model || null,
|
|
448
|
+
haiku: config.haiku || config.model || null,
|
|
449
|
+
subagent: config.subagent || config.model || null,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
console.log(`Profile: ${name}`);
|
|
457
|
+
console.log(`Directory: ${dir}`);
|
|
458
|
+
console.log(`LoggedIn: ${loggedIn ? 'true' : 'false'}`);
|
|
459
|
+
console.log(`Email: ${email || '-'}`);
|
|
460
|
+
console.log(`Shared: ${sharedCount}/${total}`);
|
|
461
|
+
|
|
462
|
+
if (configError) {
|
|
463
|
+
console.log('Endpoint: (invalid config — .cc-env.json is malformed)');
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
if (!config) {
|
|
467
|
+
console.log('Endpoint: subscription');
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
console.log(
|
|
472
|
+
`Endpoint: ${config.base_url ? envLib.hostOf(config.base_url) : '(invalid config)'}`,
|
|
473
|
+
);
|
|
474
|
+
console.log(` Base URL: ${config.base_url || '-'}`);
|
|
475
|
+
if (reveal) {
|
|
476
|
+
console.log(` Token: ${config.auth_token || '-'}`);
|
|
477
|
+
} else {
|
|
478
|
+
console.log(` Token: ${config.auth_token ? envLib.maskToken(config.auth_token) : '-'}`);
|
|
479
|
+
}
|
|
480
|
+
console.log(` Model: ${config.model || '-'}`);
|
|
481
|
+
console.log(` Opus: ${config.opus || config.model || '-'}`);
|
|
482
|
+
console.log(` Sonnet: ${config.sonnet || config.model || '-'}`);
|
|
483
|
+
console.log(` Haiku: ${config.haiku || config.model || '-'}`);
|
|
484
|
+
console.log(` Subagent: ${config.subagent || config.model || '-'}`);
|
|
485
|
+
if (reveal) {
|
|
486
|
+
console.log('');
|
|
487
|
+
console.log('WARNING: the full API key is printed above. Do not paste this output');
|
|
488
|
+
console.log('into chats, issues, screenshots, or logs.');
|
|
489
|
+
}
|
|
490
|
+
return 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
311
493
|
function cmdList(rest) {
|
|
312
|
-
const { flags } = parseFlagsStrict(rest, ['--json']
|
|
494
|
+
const { flags, positional } = parseFlagsStrict(rest, ['--json', '--reveal'], {
|
|
495
|
+
allowPositional: true,
|
|
496
|
+
});
|
|
497
|
+
const json = flags.has('--json');
|
|
498
|
+
const reveal = flags.has('--reveal');
|
|
499
|
+
const [name, ...extra] = positional;
|
|
500
|
+
if (extra.length > 0) throw usageError(`Unexpected argument: ${extra[0]}`);
|
|
501
|
+
|
|
502
|
+
if (name) {
|
|
503
|
+
return cmdListOne(name, { json, reveal });
|
|
504
|
+
}
|
|
505
|
+
if (reveal) {
|
|
506
|
+
throw usageError('--reveal requires a profile name: cc list <profile> --reveal');
|
|
507
|
+
}
|
|
508
|
+
|
|
313
509
|
const list = profiles.listProfiles();
|
|
314
510
|
const total = share.SHARED_SUBDIRS.length;
|
|
315
511
|
|
|
@@ -473,62 +669,6 @@ async function cmdMigrate(rest) {
|
|
|
473
669
|
return 0;
|
|
474
670
|
}
|
|
475
671
|
|
|
476
|
-
// Flag name -> config field for `cc env`.
|
|
477
|
-
const ENV_VALUE_FLAGS = {
|
|
478
|
-
'--base-url': 'base_url',
|
|
479
|
-
'--token': 'auth_token',
|
|
480
|
-
'--model': 'model',
|
|
481
|
-
'--opus': 'opus',
|
|
482
|
-
'--sonnet': 'sonnet',
|
|
483
|
-
'--haiku': 'haiku',
|
|
484
|
-
'--subagent': 'subagent',
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
async function cmdEnv(rest) {
|
|
488
|
-
const positional = [];
|
|
489
|
-
const provided = {};
|
|
490
|
-
let reveal = false;
|
|
491
|
-
|
|
492
|
-
for (const a of rest) {
|
|
493
|
-
if (a.startsWith('-')) {
|
|
494
|
-
const eq = a.indexOf('=');
|
|
495
|
-
const flagName = eq === -1 ? a : a.slice(0, eq);
|
|
496
|
-
if (flagName === '--reveal') {
|
|
497
|
-
reveal = true;
|
|
498
|
-
continue;
|
|
499
|
-
}
|
|
500
|
-
if (Object.prototype.hasOwnProperty.call(ENV_VALUE_FLAGS, flagName)) {
|
|
501
|
-
if (eq === -1) {
|
|
502
|
-
throw usageError(`Flag ${flagName} requires a value: ${flagName}=VALUE`);
|
|
503
|
-
}
|
|
504
|
-
provided[ENV_VALUE_FLAGS[flagName]] = a.slice(eq + 1);
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
throw usageError(`Unknown flag: ${flagName}`);
|
|
508
|
-
}
|
|
509
|
-
positional.push(a);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const [name, sub, ...extra] = positional;
|
|
513
|
-
if (!name) throw usageError('Usage: cc env <profile> [show] [--base-url=URL --token=TOKEN ...]');
|
|
514
|
-
if (extra.length > 0) throw usageError(`Unexpected argument: ${extra[0]}`);
|
|
515
|
-
profiles.validateProfileName(name);
|
|
516
|
-
const dir = profiles.profileDir(name);
|
|
517
|
-
|
|
518
|
-
if (sub === 'show') {
|
|
519
|
-
if (Object.keys(provided).length > 0) {
|
|
520
|
-
throw usageError('cc env <profile> show does not take configuration flags.');
|
|
521
|
-
}
|
|
522
|
-
return envLib.runShow(name, dir, { reveal });
|
|
523
|
-
}
|
|
524
|
-
if (sub) throw usageError(`Unknown subcommand for cc env: ${sub}`);
|
|
525
|
-
if (reveal) {
|
|
526
|
-
throw usageError('Unknown flag: --reveal (only valid with: cc env <profile> show)');
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return envLib.runConfigure(name, dir, provided);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
672
|
async function run(argv) {
|
|
533
673
|
const [action, ...rest] = argv;
|
|
534
674
|
if (!action || action === '--help' || action === '-h') {
|
|
@@ -544,6 +684,16 @@ async function run(argv) {
|
|
|
544
684
|
printHelp(rest[0]);
|
|
545
685
|
return 0;
|
|
546
686
|
}
|
|
687
|
+
if (action === 'env') {
|
|
688
|
+
// `cc env` was removed in v0.3.1 — point upgraders at the new commands.
|
|
689
|
+
process.stderr.write(
|
|
690
|
+
color.stderr.red(
|
|
691
|
+
"'cc env' was removed in v0.3.1. Configure a custom endpoint with " +
|
|
692
|
+
"'cc add <profile> --custom', and view it with 'cc list <profile>'.",
|
|
693
|
+
) + '\n',
|
|
694
|
+
);
|
|
695
|
+
return 2;
|
|
696
|
+
}
|
|
547
697
|
switch (action) {
|
|
548
698
|
case 'add':
|
|
549
699
|
return await cmdAdd(rest);
|
|
@@ -551,8 +701,6 @@ async function run(argv) {
|
|
|
551
701
|
return await cmdUse(rest);
|
|
552
702
|
case 'list':
|
|
553
703
|
return cmdList(rest);
|
|
554
|
-
case 'env':
|
|
555
|
-
return await cmdEnv(rest);
|
|
556
704
|
case 'migrate':
|
|
557
705
|
return await cmdMigrate(rest);
|
|
558
706
|
case 'link':
|
package/lib/env.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Per-profile custom API endpoints
|
|
3
|
+
// Per-profile custom API endpoints.
|
|
4
4
|
//
|
|
5
5
|
// A profile may carry a `.cc-env.json` file that routes Claude Code through an
|
|
6
6
|
// Anthropic-compatible third-party provider (Moonshot/Kimi, OpenRouter, vLLM,
|
|
7
7
|
// corporate proxies) instead of Anthropic's subscription OAuth. The file holds
|
|
8
8
|
// an API key and is treated with the same care as `.credentials.json`: it is
|
|
9
9
|
// never migrated, never linked, and written with user-only file permissions.
|
|
10
|
+
//
|
|
11
|
+
// Endpoint profiles are created and edited through `cc add <profile> --custom`;
|
|
12
|
+
// this module supplies the configuration logic that command delegates to.
|
|
10
13
|
|
|
11
14
|
const fs = require('fs');
|
|
12
15
|
const path = require('path');
|
|
@@ -338,7 +341,7 @@ async function runWizard(name, existing, io) {
|
|
|
338
341
|
}
|
|
339
342
|
}
|
|
340
343
|
|
|
341
|
-
io.write(`\ncchost endpoint configuration for profile '${name}'\n\n`);
|
|
344
|
+
io.write(`\ncchost endpoint configuration for profile '${name}'${isEdit ? ' (editing)' : ''}\n\n`);
|
|
342
345
|
for (const field of WIZARD_FIELDS) {
|
|
343
346
|
await promptOne(field, config, io, isEdit);
|
|
344
347
|
}
|
|
@@ -360,20 +363,23 @@ async function runWizard(name, existing, io) {
|
|
|
360
363
|
// Command orchestration (called from cli.js)
|
|
361
364
|
// ---------------------------------------------------------------------------
|
|
362
365
|
|
|
363
|
-
//
|
|
366
|
+
// Configures a profile's custom endpoint. Called by `cc add <profile> --custom`
|
|
367
|
+
// (both for first-time creation and for editing an existing endpoint profile).
|
|
364
368
|
// `provided` is a partial config of fields supplied via flags.
|
|
365
369
|
// `opts.io` injects wizard IO (tests); `opts.isTTY` overrides TTY detection.
|
|
370
|
+
//
|
|
371
|
+
// Returns `{ code, written }`: `written` is false only when the interactive
|
|
372
|
+
// wizard was cancelled, so the caller can clean up a freshly-created profile.
|
|
366
373
|
async function runConfigure(name, profileDir, provided, opts = {}) {
|
|
367
374
|
if (!fs.existsSync(profileDir)) {
|
|
368
375
|
throw stateError(`Profile "${name}" does not exist. Create it with: cc add ${name}`);
|
|
369
376
|
}
|
|
370
377
|
if (hasOAuthState(profileDir)) {
|
|
371
378
|
throw stateError(
|
|
372
|
-
`Profile '${name}' has OAuth credentials.\n` +
|
|
373
|
-
'
|
|
374
|
-
'
|
|
375
|
-
' cc add <new-name
|
|
376
|
-
' cc env <new-name>',
|
|
379
|
+
`Profile '${name}' has OAuth credentials and cannot be converted to a custom endpoint.\n` +
|
|
380
|
+
'A profile is born subscription or endpoint and stays that way.\n' +
|
|
381
|
+
'Create a fresh endpoint profile instead:\n' +
|
|
382
|
+
' cc add <new-name> --custom',
|
|
377
383
|
);
|
|
378
384
|
}
|
|
379
385
|
|
|
@@ -400,7 +406,7 @@ async function runConfigure(name, profileDir, provided, opts = {}) {
|
|
|
400
406
|
console.log(
|
|
401
407
|
`${editing ? 'Updated' : 'Created'} endpoint config for profile '${name}' → ${hostOf(merged.base_url)}.`,
|
|
402
408
|
);
|
|
403
|
-
return 0;
|
|
409
|
+
return { code: 0, written: true };
|
|
404
410
|
}
|
|
405
411
|
|
|
406
412
|
// Interactive wizard.
|
|
@@ -408,52 +414,22 @@ async function runConfigure(name, profileDir, provided, opts = {}) {
|
|
|
408
414
|
if (!io) {
|
|
409
415
|
const isTTY = opts.isTTY != null ? opts.isTTY : process.stdin.isTTY;
|
|
410
416
|
if (!isTTY) {
|
|
411
|
-
throw usageError(
|
|
417
|
+
throw usageError(
|
|
418
|
+
'cc add --custom requires either endpoint flags or an interactive terminal.',
|
|
419
|
+
);
|
|
412
420
|
}
|
|
413
421
|
io = defaultWizardIo();
|
|
414
422
|
}
|
|
415
423
|
|
|
416
424
|
const result = await runWizard(name, existing, io);
|
|
417
425
|
if (result == null) {
|
|
418
|
-
|
|
419
|
-
return 0;
|
|
426
|
+
return { code: 0, written: false };
|
|
420
427
|
}
|
|
421
428
|
writeEnvConfig(profileDir, result);
|
|
422
429
|
console.log(
|
|
423
430
|
`${editing ? 'Updated' : 'Created'} endpoint config for profile '${name}' → ${hostOf(result.base_url)}.`,
|
|
424
431
|
);
|
|
425
|
-
return 0;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// `cc env <profile> show [--reveal]`.
|
|
429
|
-
function runShow(name, profileDir, { reveal = false, write } = {}) {
|
|
430
|
-
const w = write || ((s) => process.stdout.write(s + '\n'));
|
|
431
|
-
if (!fs.existsSync(profileDir)) {
|
|
432
|
-
throw stateError(`Profile "${name}" does not exist.`);
|
|
433
|
-
}
|
|
434
|
-
const config = readEnvConfig(profileDir); // throws on malformed file
|
|
435
|
-
if (config == null) {
|
|
436
|
-
throw stateError(`Profile '${name}' is not an endpoint profile (no ${ENV_FILE}).`);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
w(`Endpoint configuration for profile '${name}':`);
|
|
440
|
-
w(` Base URL: ${config.base_url || '-'}`);
|
|
441
|
-
if (reveal) {
|
|
442
|
-
w(` Token: ${config.auth_token || '-'}`);
|
|
443
|
-
} else {
|
|
444
|
-
w(` Token: ${config.auth_token ? maskToken(config.auth_token) : '-'}`);
|
|
445
|
-
}
|
|
446
|
-
w(` Main model: ${config.model || '-'}`);
|
|
447
|
-
w(` Opus: ${config.opus || config.model || '-'}`);
|
|
448
|
-
w(` Sonnet: ${config.sonnet || config.model || '-'}`);
|
|
449
|
-
w(` Haiku: ${config.haiku || config.model || '-'}`);
|
|
450
|
-
w(` Subagent: ${config.subagent || config.model || '-'}`);
|
|
451
|
-
if (reveal) {
|
|
452
|
-
w('');
|
|
453
|
-
w('WARNING: the full API key is printed above. Do not paste this output');
|
|
454
|
-
w('into chats, issues, screenshots, or logs.');
|
|
455
|
-
}
|
|
456
|
-
return 0;
|
|
432
|
+
return { code: 0, written: true };
|
|
457
433
|
}
|
|
458
434
|
|
|
459
435
|
module.exports = {
|
|
@@ -475,6 +451,5 @@ module.exports = {
|
|
|
475
451
|
validateField,
|
|
476
452
|
runWizard,
|
|
477
453
|
runConfigure,
|
|
478
|
-
runShow,
|
|
479
454
|
defaultWizardIo,
|
|
480
455
|
};
|