@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 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
@@ -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);
@@ -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
+ }
@@ -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
+ }