@adithya-13/cc-switch 1.0.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/README.md +88 -0
- package/bin/cc-switch.js +73 -0
- package/bin/cclaude.js +77 -0
- package/package.json +41 -0
- package/src/commands/add.js +68 -0
- package/src/commands/doctor.js +38 -0
- package/src/commands/list.js +29 -0
- package/src/commands/status.js +19 -0
- package/src/commands/use.js +47 -0
- package/src/presets.js +100 -0
- package/src/profiles.js +36 -0
- package/src/settings.js +31 -0
- package/src/utils.js +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# cc-switch
|
|
2
|
+
|
|
3
|
+
> Switch Claude Code between providers instantly. No manual config editing.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cc-switch use zai # → z.ai (GLM-4.7)
|
|
7
|
+
cc-switch use pro # → Claude Pro/Max
|
|
8
|
+
cc-switch use kimi # → Kimi K2
|
|
9
|
+
cc-switch use openrouter # → OpenRouter (320+ models)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g cc-switch
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or via curl:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
curl -fsSL https://raw.githubusercontent.com/adithya-13/cc-switch/main/install.sh | bash
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
Claude Code's usage limits hit mid-session. Switching providers manually means editing `~/.claude/settings.json`, managing API keys, and restarting. `cc-switch` makes it one command.
|
|
27
|
+
|
|
28
|
+
## Supported Providers
|
|
29
|
+
|
|
30
|
+
| Provider | Command | Models |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Claude Pro/Max | `cc-switch use pro` | Claude Sonnet/Opus (OAuth) |
|
|
33
|
+
| z.ai | `cc-switch use zai` | GLM-4.7, GLM-5 |
|
|
34
|
+
| Kimi (Moonshot) | `cc-switch use kimi` | Kimi K2.5 |
|
|
35
|
+
| OpenRouter | `cc-switch use openrouter` | 320+ models |
|
|
36
|
+
| DeepSeek | `cc-switch use deepseek` | DeepSeek V3, R1 |
|
|
37
|
+
| Qwen (Alibaba) | `cc-switch use qwen` | Qwen3.5 |
|
|
38
|
+
| Ollama (local) | `cc-switch use ollama` | Any local model |
|
|
39
|
+
| Custom | `cc-switch add myprofile` | Anything |
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cc-switch use <provider> # switch to provider
|
|
45
|
+
cc-switch list # list all providers + key status
|
|
46
|
+
cc-switch status # show current active provider
|
|
47
|
+
cc-switch add <name> # add a custom provider
|
|
48
|
+
cc-switch doctor # check setup and saved keys
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Rate Limit Detection (cclaude)
|
|
52
|
+
|
|
53
|
+
Instead of `claude`, use `cclaude` — it wraps Claude Code and notifies you when a rate limit is hit, with quick-switch suggestions:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cclaude # same as claude, but with rate limit detection
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
When a limit is hit:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
63
|
+
⚠ Claude usage limit reached
|
|
64
|
+
|
|
65
|
+
Available fallbacks:
|
|
66
|
+
|
|
67
|
+
→ z.ai (GLM) cc-switch use zai
|
|
68
|
+
→ Kimi K2 cc-switch use kimi
|
|
69
|
+
|
|
70
|
+
Quick switch: cc-switch use zai
|
|
71
|
+
Then restart: cclaude
|
|
72
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Key Storage
|
|
76
|
+
|
|
77
|
+
API keys are saved in `~/.cc-switch/keys.json` (chmod 600). Never hardcoded or exposed.
|
|
78
|
+
|
|
79
|
+
## Add Custom Provider
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
cc-switch add myprovider
|
|
83
|
+
# interactive wizard → asks for base URL, API key, model names
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/bin/cc-switch.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { useCommand } from "../src/commands/use.js";
|
|
5
|
+
import { listCommand } from "../src/commands/list.js";
|
|
6
|
+
import { statusCommand } from "../src/commands/status.js";
|
|
7
|
+
import { addCommand } from "../src/commands/add.js";
|
|
8
|
+
import { doctorCommand } from "../src/commands/doctor.js";
|
|
9
|
+
import { readSettings, getCurrentProvider } from "../src/settings.js";
|
|
10
|
+
import { PRESETS } from "../src/presets.js";
|
|
11
|
+
import { loadProfiles } from "../src/profiles.js";
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name("cc-switch")
|
|
17
|
+
.description("Switch Claude Code between providers")
|
|
18
|
+
.version("1.0.0");
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command("use <provider>")
|
|
22
|
+
.description("Switch to a provider (pro, zai, kimi, openrouter, deepseek, qwen, ollama)")
|
|
23
|
+
.action(async (provider) => {
|
|
24
|
+
await useCommand(provider);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command("list")
|
|
29
|
+
.alias("ls")
|
|
30
|
+
.description("List all providers and their status")
|
|
31
|
+
.action(() => listCommand());
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.command("status")
|
|
35
|
+
.description("Show current active provider")
|
|
36
|
+
.action(() => statusCommand());
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command("add <name>")
|
|
40
|
+
.description("Add a custom provider profile")
|
|
41
|
+
.action(async (name) => {
|
|
42
|
+
await addCommand(name);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command("doctor")
|
|
47
|
+
.description("Check setup and saved keys")
|
|
48
|
+
.action(async () => {
|
|
49
|
+
await doctorCommand();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// default: no args → show status + hint
|
|
53
|
+
program.action(() => {
|
|
54
|
+
try {
|
|
55
|
+
const settings = readSettings();
|
|
56
|
+
const current = getCurrentProvider(settings);
|
|
57
|
+
const customProfiles = loadProfiles();
|
|
58
|
+
const preset = PRESETS[current] || customProfiles[current];
|
|
59
|
+
const name = preset?.name || current;
|
|
60
|
+
|
|
61
|
+
console.log(`\n ${chalk.bold("cc-switch")} — Claude Code provider switcher`);
|
|
62
|
+
console.log(` Active: ${chalk.green.bold(name)}\n`);
|
|
63
|
+
console.log(` ${chalk.gray("cc-switch list")} list all providers`);
|
|
64
|
+
console.log(` ${chalk.gray("cc-switch use <name>")} switch provider`);
|
|
65
|
+
console.log(` ${chalk.gray("cc-switch status")} show current`);
|
|
66
|
+
console.log(` ${chalk.gray("cc-switch add <name>")} add custom provider`);
|
|
67
|
+
console.log(` ${chalk.gray("cc-switch doctor")} check setup\n`);
|
|
68
|
+
} catch {
|
|
69
|
+
program.help();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
program.parse();
|
package/bin/cclaude.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { readSettings, getCurrentProvider } from "../src/settings.js";
|
|
5
|
+
import { PRESETS } from "../src/presets.js";
|
|
6
|
+
import { loadKeys, loadProfiles } from "../src/profiles.js";
|
|
7
|
+
|
|
8
|
+
const RATE_LIMIT_PATTERNS = [
|
|
9
|
+
/usage limit reached/i,
|
|
10
|
+
/rate limit/i,
|
|
11
|
+
/Claude\.ai usage limit/i,
|
|
12
|
+
/exceeded.*limit/i,
|
|
13
|
+
/quota exceeded/i,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function getAvailableFallbacks(current) {
|
|
17
|
+
const keys = loadKeys();
|
|
18
|
+
const customProfiles = loadProfiles();
|
|
19
|
+
const allProviders = { ...PRESETS, ...customProfiles };
|
|
20
|
+
|
|
21
|
+
return Object.entries(allProviders)
|
|
22
|
+
.filter(([name, preset]) => {
|
|
23
|
+
if (name === current) return false;
|
|
24
|
+
if (preset.requiresKey === false) return true; // pro (OAuth) always "available"
|
|
25
|
+
return !!keys[name]; // only if key is saved
|
|
26
|
+
})
|
|
27
|
+
.map(([name, preset]) => ({ name, label: preset.name || name }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function notifyRateLimit(current, fallbacks) {
|
|
31
|
+
console.error("\n" + chalk.yellow("━".repeat(50)));
|
|
32
|
+
console.error(chalk.yellow.bold(" ⚠ Claude usage limit reached"));
|
|
33
|
+
console.error(chalk.yellow("━".repeat(50)));
|
|
34
|
+
|
|
35
|
+
if (fallbacks.length === 0) {
|
|
36
|
+
console.error(chalk.gray("\n No saved fallback providers."));
|
|
37
|
+
console.error(chalk.gray(` Add one: ${chalk.cyan("cc-switch add <provider>")}\n`));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.error(chalk.bold("\n Available fallbacks:\n"));
|
|
42
|
+
fallbacks.forEach(({ name, label }) => {
|
|
43
|
+
console.error(` ${chalk.cyan("→")} ${label.padEnd(20)} ${chalk.gray(`cc-switch use ${name}`)}`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const top = fallbacks[0];
|
|
47
|
+
console.error(
|
|
48
|
+
`\n ${chalk.bold("Quick switch:")} ${chalk.cyan(`cc-switch use ${top.name}`)}\n` +
|
|
49
|
+
` Then restart: ${chalk.cyan("cclaude")}\n`
|
|
50
|
+
);
|
|
51
|
+
console.error(chalk.yellow("━".repeat(50)) + "\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// run claude, intercept output for rate limit signals
|
|
55
|
+
const settings = readSettings();
|
|
56
|
+
const current = getCurrentProvider(settings);
|
|
57
|
+
|
|
58
|
+
const child = spawn("claude", process.argv.slice(2), {
|
|
59
|
+
stdio: ["inherit", "inherit", "pipe"], // intercept stderr
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
let stderrBuffer = "";
|
|
63
|
+
|
|
64
|
+
child.stderr.on("data", (data) => {
|
|
65
|
+
const text = data.toString();
|
|
66
|
+
stderrBuffer += text;
|
|
67
|
+
process.stderr.write(data); // still show to user
|
|
68
|
+
|
|
69
|
+
const isRateLimit = RATE_LIMIT_PATTERNS.some((p) => p.test(stderrBuffer));
|
|
70
|
+
if (isRateLimit) {
|
|
71
|
+
const fallbacks = getAvailableFallbacks(current);
|
|
72
|
+
notifyRateLimit(current, fallbacks);
|
|
73
|
+
stderrBuffer = ""; // reset to avoid re-triggering
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adithya-13/cc-switch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Switch Claude Code between providers (Claude Pro, z.ai, Kimi, OpenRouter, DeepSeek, and more)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"anthropic",
|
|
9
|
+
"zai",
|
|
10
|
+
"openrouter",
|
|
11
|
+
"kimi",
|
|
12
|
+
"provider",
|
|
13
|
+
"switch"
|
|
14
|
+
],
|
|
15
|
+
"author": "adithya-13",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://github.com/adithya-13/cc-switch#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/adithya-13/cc-switch.git"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"cc-switch": "./bin/cc-switch.js",
|
|
25
|
+
"cclaude": "./bin/cclaude.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"bin",
|
|
29
|
+
"src",
|
|
30
|
+
"install.sh",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"chalk": "^5.3.0",
|
|
38
|
+
"commander": "^12.0.0",
|
|
39
|
+
"inquirer": "^9.2.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import inquirer from 'inquirer'
|
|
3
|
+
import { saveProfile, saveKey } from '../profiles.js'
|
|
4
|
+
|
|
5
|
+
export async function addCommand(name) {
|
|
6
|
+
console.log(chalk.bold(`\nAdding custom provider: ${name}\n`))
|
|
7
|
+
|
|
8
|
+
const answers = await inquirer.prompt([
|
|
9
|
+
{
|
|
10
|
+
type: 'input',
|
|
11
|
+
name: 'displayName',
|
|
12
|
+
message: 'Display name:',
|
|
13
|
+
default: name,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
type: 'input',
|
|
17
|
+
name: 'baseUrl',
|
|
18
|
+
message: 'Base URL (e.g. https://api.example.com):',
|
|
19
|
+
validate: (v) => v.startsWith('http') || 'Must start with http',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: 'password',
|
|
23
|
+
name: 'key',
|
|
24
|
+
message: 'API key (leave blank if not required):',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'input',
|
|
28
|
+
name: 'sonnetModel',
|
|
29
|
+
message: 'Default sonnet model (blank to skip):',
|
|
30
|
+
default: '',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
type: 'input',
|
|
34
|
+
name: 'haikusModel',
|
|
35
|
+
message: 'Default haiku model (blank to skip):',
|
|
36
|
+
default: '',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'input',
|
|
40
|
+
name: 'opusModel',
|
|
41
|
+
message: 'Default opus model (blank to skip):',
|
|
42
|
+
default: '',
|
|
43
|
+
},
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
const env = {
|
|
47
|
+
ANTHROPIC_BASE_URL: answers.baseUrl,
|
|
48
|
+
API_TIMEOUT_MS: '600000',
|
|
49
|
+
}
|
|
50
|
+
if (answers.sonnetModel) env.ANTHROPIC_DEFAULT_SONNET_MODEL = answers.sonnetModel
|
|
51
|
+
if (answers.haikusModel) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = answers.haikusModel
|
|
52
|
+
if (answers.opusModel) env.ANTHROPIC_DEFAULT_OPUS_MODEL = answers.opusModel
|
|
53
|
+
|
|
54
|
+
const profile = {
|
|
55
|
+
name: answers.displayName,
|
|
56
|
+
description: `Custom: ${answers.baseUrl}`,
|
|
57
|
+
baseUrl: answers.baseUrl,
|
|
58
|
+
env,
|
|
59
|
+
requiresKey: !!answers.key,
|
|
60
|
+
keyEnv: answers.key ? 'ANTHROPIC_AUTH_TOKEN' : undefined,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
saveProfile(name, profile)
|
|
64
|
+
if (answers.key) saveKey(name, answers.key)
|
|
65
|
+
|
|
66
|
+
console.log(chalk.green(`\n✓ Added ${answers.displayName}`))
|
|
67
|
+
console.log(chalk.gray(` Switch: cc-switch use ${name}\n`))
|
|
68
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import { homedir } from 'os'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import { PRESETS } from '../presets.js'
|
|
6
|
+
import { loadKeys, loadProfiles } from '../profiles.js'
|
|
7
|
+
import { readSettings, getCurrentProvider } from '../settings.js'
|
|
8
|
+
import { maskKey } from '../utils.js'
|
|
9
|
+
|
|
10
|
+
export function doctorCommand() {
|
|
11
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json')
|
|
12
|
+
const keysPath = join(homedir(), '.cc-switch', 'keys.json')
|
|
13
|
+
|
|
14
|
+
console.log(chalk.bold('\n cc-switch doctor\n'))
|
|
15
|
+
|
|
16
|
+
const settingsOk = existsSync(settingsPath)
|
|
17
|
+
console.log(` ${settingsOk ? chalk.green('✓') : chalk.red('✗')} ~/.claude/settings.json`)
|
|
18
|
+
|
|
19
|
+
const settings = readSettings()
|
|
20
|
+
const current = getCurrentProvider(settings)
|
|
21
|
+
const all = { ...PRESETS, ...loadProfiles() }
|
|
22
|
+
console.log(` ${chalk.green('●')} Active: ${chalk.bold(all[current]?.name || current)}`)
|
|
23
|
+
|
|
24
|
+
const keys = loadKeys()
|
|
25
|
+
const keysExist = existsSync(keysPath)
|
|
26
|
+
console.log(` ${keysExist ? chalk.green('✓') : chalk.gray('○')} ~/.cc-switch/keys.json`)
|
|
27
|
+
|
|
28
|
+
console.log(`\n Saved keys:`)
|
|
29
|
+
const needsKey = Object.entries(all).filter(([, p]) => p.requiresKey !== false)
|
|
30
|
+
for (const [name, preset] of needsKey) {
|
|
31
|
+
const key = keys[name]
|
|
32
|
+
const status = key ? chalk.green(`saved (${maskKey(key)})`) : chalk.gray('not saved')
|
|
33
|
+
const icon = key ? chalk.green('✓') : chalk.gray('○')
|
|
34
|
+
console.log(` ${icon} ${(preset.name || name).padEnd(22)} ${status}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(`\n ${chalk.green('✓')} Node.js ${process.version}\n`)
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { PRESETS } from '../presets.js'
|
|
3
|
+
import { loadProfiles, loadKeys } from '../profiles.js'
|
|
4
|
+
import { readSettings, getCurrentProvider } from '../settings.js'
|
|
5
|
+
|
|
6
|
+
export function listCommand() {
|
|
7
|
+
const settings = readSettings()
|
|
8
|
+
const current = getCurrentProvider(settings)
|
|
9
|
+
const customProfiles = loadProfiles()
|
|
10
|
+
const keys = loadKeys()
|
|
11
|
+
const all = { ...PRESETS, ...customProfiles }
|
|
12
|
+
|
|
13
|
+
console.log()
|
|
14
|
+
for (const [name, preset] of Object.entries(all)) {
|
|
15
|
+
const isActive = name === current
|
|
16
|
+
const hasKey = preset.requiresKey === false || !!keys[name]
|
|
17
|
+
const marker = isActive ? chalk.green('●') : chalk.gray('○')
|
|
18
|
+
const label = isActive ? chalk.green.bold(preset.name) : chalk.white(preset.name)
|
|
19
|
+
const keyStatus = preset.requiresKey === false
|
|
20
|
+
? chalk.gray('OAuth')
|
|
21
|
+
: hasKey ? chalk.green('key ✓') : chalk.yellow('no key')
|
|
22
|
+
|
|
23
|
+
console.log(` ${marker} ${label.padEnd(isActive ? 33 : 25)} ${keyStatus}`)
|
|
24
|
+
if (preset.description) {
|
|
25
|
+
console.log(` ${chalk.gray(preset.description)}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
console.log()
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { PRESETS } from '../presets.js'
|
|
3
|
+
import { loadProfiles } from '../profiles.js'
|
|
4
|
+
import { readSettings, getCurrentProvider } from '../settings.js'
|
|
5
|
+
|
|
6
|
+
export function statusCommand() {
|
|
7
|
+
const settings = readSettings()
|
|
8
|
+
const current = getCurrentProvider(settings)
|
|
9
|
+
const all = { ...PRESETS, ...loadProfiles() }
|
|
10
|
+
const preset = all[current]
|
|
11
|
+
|
|
12
|
+
console.log()
|
|
13
|
+
console.log(` Active: ${chalk.green.bold(preset?.name || current)}`)
|
|
14
|
+
if (preset?.description) console.log(` ${chalk.gray(preset.description)}`)
|
|
15
|
+
if (settings.env?.ANTHROPIC_BASE_URL) {
|
|
16
|
+
console.log(` URL: ${chalk.gray(settings.env.ANTHROPIC_BASE_URL)}`)
|
|
17
|
+
}
|
|
18
|
+
console.log()
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import inquirer from 'inquirer'
|
|
3
|
+
import { PRESETS } from '../presets.js'
|
|
4
|
+
import { loadProfiles, loadKeys, saveKey } from '../profiles.js'
|
|
5
|
+
import { readSettings, writeSettings } from '../settings.js'
|
|
6
|
+
|
|
7
|
+
export async function useCommand(provider) {
|
|
8
|
+
const allProviders = { ...PRESETS, ...loadProfiles() }
|
|
9
|
+
const preset = allProviders[provider]
|
|
10
|
+
|
|
11
|
+
if (!preset) {
|
|
12
|
+
console.log(chalk.red(`Unknown provider: ${provider}`))
|
|
13
|
+
console.log(chalk.gray('Run: cc-switch list'))
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let key = null
|
|
18
|
+
if (preset.requiresKey) {
|
|
19
|
+
const keys = loadKeys()
|
|
20
|
+
if (keys[provider]) {
|
|
21
|
+
key = keys[provider]
|
|
22
|
+
} else {
|
|
23
|
+
const { key: input } = await inquirer.prompt([{
|
|
24
|
+
type: 'password',
|
|
25
|
+
name: 'key',
|
|
26
|
+
message: `API key for ${preset.name}? (${preset.keyHint})`,
|
|
27
|
+
validate: (v) => v.length > 0 || 'Key required',
|
|
28
|
+
}])
|
|
29
|
+
key = input
|
|
30
|
+
saveKey(provider, key)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const settings = readSettings()
|
|
35
|
+
|
|
36
|
+
if (preset.env === null) {
|
|
37
|
+
delete settings.env
|
|
38
|
+
} else {
|
|
39
|
+
const env = { ...preset.env }
|
|
40
|
+
if (key && preset.keyEnv) env[preset.keyEnv] = key
|
|
41
|
+
settings.env = env
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
writeSettings(settings)
|
|
45
|
+
console.log(chalk.green(`✓ Switched to ${preset.name}`))
|
|
46
|
+
console.log(chalk.gray('Restart Claude Code to apply'))
|
|
47
|
+
}
|
package/src/presets.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Built-in provider presets
|
|
2
|
+
// env: null means "no env block" (use Claude OAuth)
|
|
3
|
+
|
|
4
|
+
export const PRESETS = {
|
|
5
|
+
pro: {
|
|
6
|
+
name: "Claude Pro/Max",
|
|
7
|
+
description: "Claude subscription (Pro, Max, Team)",
|
|
8
|
+
env: null, // removes env block, falls back to OAuth
|
|
9
|
+
requiresKey: false,
|
|
10
|
+
},
|
|
11
|
+
zai: {
|
|
12
|
+
name: "z.ai (GLM)",
|
|
13
|
+
description: "Z.AI GLM-4.7 / GLM-5 — 3x value vs Pro",
|
|
14
|
+
baseUrl: "https://api.z.ai/api/anthropic",
|
|
15
|
+
env: {
|
|
16
|
+
ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
|
|
17
|
+
API_TIMEOUT_MS: "3000000",
|
|
18
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.5-air",
|
|
19
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.7",
|
|
20
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.7",
|
|
21
|
+
},
|
|
22
|
+
requiresKey: true,
|
|
23
|
+
keyEnv: "ANTHROPIC_AUTH_TOKEN",
|
|
24
|
+
keyHint: "Get key at: z.ai/manage-apikey/apikey-list",
|
|
25
|
+
},
|
|
26
|
+
kimi: {
|
|
27
|
+
name: "Kimi K2 (Moonshot)",
|
|
28
|
+
description: "Kimi K2.5 — vision support, fast inference",
|
|
29
|
+
baseUrl: "https://api.moonshot.ai/anthropic",
|
|
30
|
+
env: {
|
|
31
|
+
ANTHROPIC_BASE_URL: "https://api.moonshot.ai/anthropic",
|
|
32
|
+
API_TIMEOUT_MS: "600000",
|
|
33
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: "kimi-k2.5",
|
|
34
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: "kimi-k2",
|
|
35
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: "kimi-k2.5",
|
|
36
|
+
},
|
|
37
|
+
requiresKey: true,
|
|
38
|
+
keyEnv: "ANTHROPIC_AUTH_TOKEN",
|
|
39
|
+
keyHint: "Get key at: platform.moonshot.ai",
|
|
40
|
+
},
|
|
41
|
+
openrouter: {
|
|
42
|
+
name: "OpenRouter",
|
|
43
|
+
description: "320+ models via one API",
|
|
44
|
+
baseUrl: "https://openrouter.ai/api",
|
|
45
|
+
env: {
|
|
46
|
+
ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
|
|
47
|
+
ANTHROPIC_API_KEY: "",
|
|
48
|
+
API_TIMEOUT_MS: "600000",
|
|
49
|
+
},
|
|
50
|
+
requiresKey: true,
|
|
51
|
+
keyEnv: "ANTHROPIC_AUTH_TOKEN",
|
|
52
|
+
keyHint: "Get key at: openrouter.ai/keys",
|
|
53
|
+
},
|
|
54
|
+
deepseek: {
|
|
55
|
+
name: "DeepSeek",
|
|
56
|
+
description: "DeepSeek V3 — cheapest reasoning model",
|
|
57
|
+
baseUrl: "https://api.deepseek.com",
|
|
58
|
+
env: {
|
|
59
|
+
ANTHROPIC_BASE_URL: "https://api.deepseek.com",
|
|
60
|
+
API_TIMEOUT_MS: "600000",
|
|
61
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: "deepseek-chat",
|
|
62
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: "deepseek-chat",
|
|
63
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: "deepseek-reasoner",
|
|
64
|
+
},
|
|
65
|
+
requiresKey: true,
|
|
66
|
+
keyEnv: "ANTHROPIC_AUTH_TOKEN",
|
|
67
|
+
keyHint: "Get key at: platform.deepseek.com",
|
|
68
|
+
},
|
|
69
|
+
qwen: {
|
|
70
|
+
name: "Qwen (Alibaba)",
|
|
71
|
+
description: "Qwen3.5 — strong coding, 256K context",
|
|
72
|
+
baseUrl: "https://dashscope-intl.aliyuncs.com/apps/anthropic",
|
|
73
|
+
env: {
|
|
74
|
+
ANTHROPIC_BASE_URL: "https://dashscope-intl.aliyuncs.com/apps/anthropic",
|
|
75
|
+
API_TIMEOUT_MS: "600000",
|
|
76
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: "qwen3.5-plus",
|
|
77
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: "qwen3.5-coder",
|
|
78
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: "qwen3.5-plus",
|
|
79
|
+
},
|
|
80
|
+
requiresKey: true,
|
|
81
|
+
keyEnv: "ANTHROPIC_AUTH_TOKEN",
|
|
82
|
+
keyHint: "Get key at: dashscope.aliyuncs.com",
|
|
83
|
+
},
|
|
84
|
+
ollama: {
|
|
85
|
+
name: "Ollama (Local)",
|
|
86
|
+
description: "Local models — private, free, offline",
|
|
87
|
+
baseUrl: "http://localhost:11434/api",
|
|
88
|
+
env: {
|
|
89
|
+
ANTHROPIC_BASE_URL: "http://localhost:11434/api",
|
|
90
|
+
API_TIMEOUT_MS: "600000",
|
|
91
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: "qwen2.5-coder:latest",
|
|
92
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: "qwen2.5-coder:latest",
|
|
93
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: "qwen2.5-coder:latest",
|
|
94
|
+
},
|
|
95
|
+
requiresKey: false,
|
|
96
|
+
keyHint: "Install Ollama at: ollama.ai",
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const PRESET_NAMES = Object.keys(PRESETS);
|
package/src/profiles.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
|
|
2
|
+
import { homedir } from 'os'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
|
|
5
|
+
const DIR = join(homedir(), '.cc-switch')
|
|
6
|
+
const KEYS_PATH = join(DIR, 'keys.json')
|
|
7
|
+
const PROFILES_PATH = join(DIR, 'profiles.json')
|
|
8
|
+
|
|
9
|
+
function ensureDir() {
|
|
10
|
+
if (!existsSync(DIR)) mkdirSync(DIR, { recursive: true })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function loadKeys() {
|
|
14
|
+
if (!existsSync(KEYS_PATH)) return {}
|
|
15
|
+
return JSON.parse(readFileSync(KEYS_PATH, 'utf8'))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function saveKey(name, key) {
|
|
19
|
+
ensureDir()
|
|
20
|
+
const keys = loadKeys()
|
|
21
|
+
keys[name] = key
|
|
22
|
+
writeFileSync(KEYS_PATH, JSON.stringify(keys, null, 2))
|
|
23
|
+
chmodSync(KEYS_PATH, 0o600)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadProfiles() {
|
|
27
|
+
if (!existsSync(PROFILES_PATH)) return {}
|
|
28
|
+
return JSON.parse(readFileSync(PROFILES_PATH, 'utf8'))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function saveProfile(name, profile) {
|
|
32
|
+
ensureDir()
|
|
33
|
+
const profiles = loadProfiles()
|
|
34
|
+
profiles[name] = profile
|
|
35
|
+
writeFileSync(PROFILES_PATH, JSON.stringify(profiles, null, 2))
|
|
36
|
+
}
|
package/src/settings.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, copyFileSync, existsSync, renameSync } from 'fs'
|
|
2
|
+
import { homedir } from 'os'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { PRESETS } from './presets.js'
|
|
5
|
+
|
|
6
|
+
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json')
|
|
7
|
+
|
|
8
|
+
export function readSettings() {
|
|
9
|
+
if (!existsSync(SETTINGS_PATH)) return {}
|
|
10
|
+
return JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function writeSettings(settings) {
|
|
14
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
15
|
+
copyFileSync(SETTINGS_PATH, SETTINGS_PATH + '.bak')
|
|
16
|
+
}
|
|
17
|
+
const tmp = SETTINGS_PATH + '.tmp'
|
|
18
|
+
writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n')
|
|
19
|
+
renameSync(tmp, SETTINGS_PATH)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getCurrentProvider(settings) {
|
|
23
|
+
const env = settings?.env
|
|
24
|
+
if (!env || Object.keys(env).length === 0) return 'pro'
|
|
25
|
+
const baseUrl = env.ANTHROPIC_BASE_URL
|
|
26
|
+
if (!baseUrl) return 'pro'
|
|
27
|
+
for (const [name, preset] of Object.entries(PRESETS)) {
|
|
28
|
+
if (preset.baseUrl && baseUrl === preset.baseUrl) return name
|
|
29
|
+
}
|
|
30
|
+
return 'unknown'
|
|
31
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
export function maskKey(key) {
|
|
4
|
+
if (!key || key.length < 8) return '***'
|
|
5
|
+
return key.slice(0, 4) + '...' + key.slice(-4)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const icons = {
|
|
9
|
+
ok: chalk.green('✓'),
|
|
10
|
+
warn: chalk.yellow('⚠'),
|
|
11
|
+
error: chalk.red('✗'),
|
|
12
|
+
arrow: chalk.cyan('→'),
|
|
13
|
+
active: chalk.green('●'),
|
|
14
|
+
inactive: chalk.gray('○'),
|
|
15
|
+
}
|