@neuxsbotzz/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 +18 -0
- package/cli.mjs +358 -0
- package/package.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# cc-switch
|
|
2
|
+
|
|
3
|
+
Switch Claude Code and Codex CLI between official endpoints and Token.Me.Uk.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @neuxsbotzz/cc-switch
|
|
7
|
+
|
|
8
|
+
cc-switch use token-me-uk --key sk-your-api-key
|
|
9
|
+
cc-switch status
|
|
10
|
+
cc-switch use official --key sk-your-official-key
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Custom provider:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cc-switch add work --anthropic-url https://example.com --openai-url https://example.com/v1 --key sk-your-api-key
|
|
17
|
+
cc-switch use work
|
|
18
|
+
```
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import readline from 'node:readline/promises'
|
|
7
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
8
|
+
|
|
9
|
+
const markerStart = '# >>> cc-switch config >>>'
|
|
10
|
+
const markerEnd = '# <<< cc-switch config <<<'
|
|
11
|
+
|
|
12
|
+
const builtInProviders = {
|
|
13
|
+
official: {
|
|
14
|
+
name: 'official',
|
|
15
|
+
anthropicUrl: 'https://api.anthropic.com',
|
|
16
|
+
openaiUrl: 'https://api.openai.com/v1',
|
|
17
|
+
key: '',
|
|
18
|
+
},
|
|
19
|
+
'token-me-uk': {
|
|
20
|
+
name: 'token-me-uk',
|
|
21
|
+
anthropicUrl: 'https://www.token.me.uk',
|
|
22
|
+
openaiUrl: 'https://www.token.me.uk/v1',
|
|
23
|
+
key: '',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function printHelp() {
|
|
28
|
+
console.log(`cc-switch - Claude Code / Codex CLI 线路切换工具
|
|
29
|
+
|
|
30
|
+
用法:
|
|
31
|
+
cc-switch use token-me-uk --key sk-your-api-key
|
|
32
|
+
cc-switch use official --key sk-your-official-key
|
|
33
|
+
cc-switch add <name> --anthropic-url <url> --openai-url <url> --key <key>
|
|
34
|
+
cc-switch list
|
|
35
|
+
cc-switch status
|
|
36
|
+
cc-switch remove <name>
|
|
37
|
+
|
|
38
|
+
参数:
|
|
39
|
+
--key <key> API Key;use 时传入会保存到该线路
|
|
40
|
+
--anthropic-url <url> Claude Code 使用的地址,不带 /v1
|
|
41
|
+
--openai-url <url> Codex CLI / OpenAI SDK 使用的地址,通常带 /v1
|
|
42
|
+
--target <all|claude|codex>
|
|
43
|
+
--profile <path> macOS/Linux 写入的 shell 配置文件
|
|
44
|
+
--dry-run 只显示将写入的内容,不修改系统
|
|
45
|
+
`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseOptions(argv) {
|
|
49
|
+
const positional = []
|
|
50
|
+
const options = {
|
|
51
|
+
key: '',
|
|
52
|
+
anthropicUrl: '',
|
|
53
|
+
openaiUrl: '',
|
|
54
|
+
target: 'all',
|
|
55
|
+
profile: '',
|
|
56
|
+
dryRun: false,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
60
|
+
const arg = argv[i]
|
|
61
|
+
const next = argv[i + 1]
|
|
62
|
+
if (arg === '--key' && next) {
|
|
63
|
+
options.key = next
|
|
64
|
+
i += 1
|
|
65
|
+
} else if (arg === '--anthropic-url' && next) {
|
|
66
|
+
options.anthropicUrl = normalizeUrl(next)
|
|
67
|
+
i += 1
|
|
68
|
+
} else if (arg === '--openai-url' && next) {
|
|
69
|
+
options.openaiUrl = normalizeUrl(next)
|
|
70
|
+
i += 1
|
|
71
|
+
} else if (arg === '--target' && next) {
|
|
72
|
+
options.target = next
|
|
73
|
+
i += 1
|
|
74
|
+
} else if (arg === '--profile' && next) {
|
|
75
|
+
options.profile = next
|
|
76
|
+
i += 1
|
|
77
|
+
} else if (arg === '--dry-run') {
|
|
78
|
+
options.dryRun = true
|
|
79
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
80
|
+
printHelp()
|
|
81
|
+
process.exit(0)
|
|
82
|
+
} else {
|
|
83
|
+
positional.push(arg)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { positional, options }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeUrl(value) {
|
|
91
|
+
return String(value || '').trim().replace(/\/+$/, '')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function configPath() {
|
|
95
|
+
if (process.env.CC_SWITCH_CONFIG) return process.env.CC_SWITCH_CONFIG
|
|
96
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
97
|
+
return path.join(process.env.APPDATA, 'cc-switch', 'config.json')
|
|
98
|
+
}
|
|
99
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
|
|
100
|
+
return path.join(base, 'cc-switch', 'config.json')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function loadConfig() {
|
|
104
|
+
const file = configPath()
|
|
105
|
+
if (!fs.existsSync(file)) return { active: '', providers: {} }
|
|
106
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
107
|
+
return {
|
|
108
|
+
active: parsed.active || '',
|
|
109
|
+
providers: parsed.providers || {},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function saveConfig(config) {
|
|
114
|
+
const file = configPath()
|
|
115
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
116
|
+
fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function allProviders(config) {
|
|
120
|
+
return {
|
|
121
|
+
...builtInProviders,
|
|
122
|
+
...config.providers,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertProviderName(name) {
|
|
127
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(name || '')) {
|
|
128
|
+
throw new Error('线路名称只能使用字母、数字、点、下划线和短横线')
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function deriveOpenAiUrl(anthropicUrl) {
|
|
133
|
+
return `${normalizeUrl(anthropicUrl)}/v1`
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function askForKey(provider, current) {
|
|
137
|
+
if (current) return current.trim()
|
|
138
|
+
if (provider.key) return provider.key.trim()
|
|
139
|
+
const envKey = process.env.TOKEN_ME_UK_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || ''
|
|
140
|
+
if (envKey) return envKey.trim()
|
|
141
|
+
|
|
142
|
+
const rl = readline.createInterface({ input, output })
|
|
143
|
+
try {
|
|
144
|
+
const answer = await rl.question('请输入 API Key: ')
|
|
145
|
+
return answer.trim()
|
|
146
|
+
} finally {
|
|
147
|
+
rl.close()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildEnv(provider, target) {
|
|
152
|
+
if (!['all', 'claude', 'codex'].includes(target)) {
|
|
153
|
+
throw new Error('--target 只能是 all、claude 或 codex')
|
|
154
|
+
}
|
|
155
|
+
const env = {}
|
|
156
|
+
if (target === 'all' || target === 'claude') {
|
|
157
|
+
env.ANTHROPIC_BASE_URL = provider.anthropicUrl
|
|
158
|
+
env.ANTHROPIC_API_KEY = provider.key
|
|
159
|
+
}
|
|
160
|
+
if (target === 'all' || target === 'codex') {
|
|
161
|
+
env.OPENAI_BASE_URL = provider.openaiUrl
|
|
162
|
+
env.OPENAI_API_KEY = provider.key
|
|
163
|
+
}
|
|
164
|
+
return env
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function defaultProfilePath() {
|
|
168
|
+
if (process.env.CC_SWITCH_PROFILE) return process.env.CC_SWITCH_PROFILE
|
|
169
|
+
const home = os.homedir()
|
|
170
|
+
const shell = process.env.SHELL || ''
|
|
171
|
+
if (shell.includes('zsh')) return path.join(home, '.zshrc')
|
|
172
|
+
if (shell.includes('bash')) return path.join(home, '.bashrc')
|
|
173
|
+
const zshrc = path.join(home, '.zshrc')
|
|
174
|
+
const bashrc = path.join(home, '.bashrc')
|
|
175
|
+
if (fs.existsSync(zshrc)) return zshrc
|
|
176
|
+
if (fs.existsSync(bashrc)) return bashrc
|
|
177
|
+
return bashrc
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function shellQuote(value) {
|
|
181
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function writeUnixProfile(profile, env, dryRun) {
|
|
185
|
+
const block = [
|
|
186
|
+
markerStart,
|
|
187
|
+
...Object.entries(env).map(([key, value]) => `export ${key}=${shellQuote(value)}`),
|
|
188
|
+
markerEnd,
|
|
189
|
+
'',
|
|
190
|
+
].join('\n')
|
|
191
|
+
|
|
192
|
+
if (dryRun) {
|
|
193
|
+
console.log(`将写入 ${profile}:\n\n${block}`)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fs.mkdirSync(path.dirname(profile), { recursive: true })
|
|
198
|
+
const current = fs.existsSync(profile) ? fs.readFileSync(profile, 'utf8') : ''
|
|
199
|
+
const pattern = new RegExp(`${escapeRegExp(markerStart)}[\\s\\S]*?${escapeRegExp(markerEnd)}\\n?`, 'm')
|
|
200
|
+
const next = pattern.test(current)
|
|
201
|
+
? current.replace(pattern, block)
|
|
202
|
+
: `${current.replace(/\s*$/, '')}\n\n${block}`
|
|
203
|
+
fs.writeFileSync(profile, next, 'utf8')
|
|
204
|
+
console.log(`已写入 ${profile}`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function writeWindowsEnv(env, dryRun) {
|
|
208
|
+
for (const [key, value] of Object.entries(env)) {
|
|
209
|
+
if (dryRun) {
|
|
210
|
+
console.log(`setx ${key} ${value}`)
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
execFileSync('setx', [key, value], { stdio: 'inherit' })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function writeEnv(env, options) {
|
|
218
|
+
const profile = options.profile || process.env.CC_SWITCH_PROFILE || ''
|
|
219
|
+
if (profile || process.platform !== 'win32') {
|
|
220
|
+
writeUnixProfile(profile || defaultProfilePath(), env, options.dryRun)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
writeWindowsEnv(env, options.dryRun)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function escapeRegExp(value) {
|
|
227
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function maskKey(value) {
|
|
231
|
+
if (!value) return '(未设置)'
|
|
232
|
+
if (value.length <= 12) return '***'
|
|
233
|
+
return `${value.slice(0, 7)}...${value.slice(-4)}`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function printProvider(name, provider, active) {
|
|
237
|
+
const mark = active === name ? '*' : ' '
|
|
238
|
+
console.log(`${mark} ${name}`)
|
|
239
|
+
console.log(` Claude: ${provider.anthropicUrl}`)
|
|
240
|
+
console.log(` OpenAI: ${provider.openaiUrl}`)
|
|
241
|
+
console.log(` Key: ${maskKey(provider.key || '')}`)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function addProvider(name, options) {
|
|
245
|
+
assertProviderName(name)
|
|
246
|
+
const anthropicUrl = normalizeUrl(options.anthropicUrl)
|
|
247
|
+
const openaiUrl = normalizeUrl(options.openaiUrl || deriveOpenAiUrl(anthropicUrl))
|
|
248
|
+
if (!anthropicUrl) throw new Error('请提供 --anthropic-url')
|
|
249
|
+
if (!openaiUrl) throw new Error('请提供 --openai-url')
|
|
250
|
+
|
|
251
|
+
const config = loadConfig()
|
|
252
|
+
config.providers[name] = {
|
|
253
|
+
name,
|
|
254
|
+
anthropicUrl,
|
|
255
|
+
openaiUrl,
|
|
256
|
+
key: options.key || config.providers[name]?.key || '',
|
|
257
|
+
}
|
|
258
|
+
saveConfig(config)
|
|
259
|
+
console.log(`已保存线路: ${name}`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function useProvider(name, options) {
|
|
263
|
+
assertProviderName(name)
|
|
264
|
+
const config = loadConfig()
|
|
265
|
+
const providers = allProviders(config)
|
|
266
|
+
const provider = providers[name]
|
|
267
|
+
if (!provider) throw new Error(`找不到线路: ${name}。可先运行 cc-switch add ${name} ...`)
|
|
268
|
+
|
|
269
|
+
const key = await askForKey(provider, options.key)
|
|
270
|
+
if (!key) throw new Error('API Key 不能为空')
|
|
271
|
+
|
|
272
|
+
const resolved = { ...provider, key }
|
|
273
|
+
const env = buildEnv(resolved, options.target)
|
|
274
|
+
writeEnv(env, options)
|
|
275
|
+
|
|
276
|
+
if (!builtInProviders[name]) {
|
|
277
|
+
config.providers[name] = resolved
|
|
278
|
+
} else if (options.key || key) {
|
|
279
|
+
config.providers[name] = resolved
|
|
280
|
+
}
|
|
281
|
+
config.active = name
|
|
282
|
+
if (!options.dryRun) saveConfig(config)
|
|
283
|
+
|
|
284
|
+
console.log(`\n已切换到: ${name}`)
|
|
285
|
+
console.log(`Claude: ${resolved.anthropicUrl}`)
|
|
286
|
+
console.log(`OpenAI: ${resolved.openaiUrl}`)
|
|
287
|
+
console.log(`Key: ${maskKey(resolved.key)}`)
|
|
288
|
+
if (process.platform === 'win32' && !options.profile && !process.env.CC_SWITCH_PROFILE) {
|
|
289
|
+
console.log('请重新打开 PowerShell / CMD 后再运行 claude 或 codex。')
|
|
290
|
+
} else {
|
|
291
|
+
console.log(`执行 source ${options.profile || process.env.CC_SWITCH_PROFILE || defaultProfilePath()},或重新打开终端后生效。`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function listProviders() {
|
|
296
|
+
const config = loadConfig()
|
|
297
|
+
const providers = allProviders(config)
|
|
298
|
+
for (const [name, provider] of Object.entries(providers)) {
|
|
299
|
+
printProvider(name, provider, config.active)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function status() {
|
|
304
|
+
const config = loadConfig()
|
|
305
|
+
const providers = allProviders(config)
|
|
306
|
+
const active = config.active
|
|
307
|
+
if (!active || !providers[active]) {
|
|
308
|
+
console.log('当前没有启用线路。')
|
|
309
|
+
console.log(`配置文件: ${configPath()}`)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
const provider = providers[active]
|
|
313
|
+
console.log(`当前线路: ${active}`)
|
|
314
|
+
console.log(`Claude: ${provider.anthropicUrl}`)
|
|
315
|
+
console.log(`OpenAI: ${provider.openaiUrl}`)
|
|
316
|
+
console.log(`Key: ${maskKey(provider.key || '')}`)
|
|
317
|
+
console.log(`配置文件: ${configPath()}`)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function removeProvider(name) {
|
|
321
|
+
assertProviderName(name)
|
|
322
|
+
if (builtInProviders[name]) throw new Error('内置线路不能删除')
|
|
323
|
+
const config = loadConfig()
|
|
324
|
+
if (!config.providers[name]) throw new Error(`找不到线路: ${name}`)
|
|
325
|
+
delete config.providers[name]
|
|
326
|
+
if (config.active === name) config.active = ''
|
|
327
|
+
saveConfig(config)
|
|
328
|
+
console.log(`已删除线路: ${name}`)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function main() {
|
|
332
|
+
const { positional, options } = parseOptions(process.argv.slice(2))
|
|
333
|
+
const [command, name] = positional
|
|
334
|
+
|
|
335
|
+
if (!command) {
|
|
336
|
+
printHelp()
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (command === 'add') {
|
|
341
|
+
await addProvider(name, options)
|
|
342
|
+
} else if (command === 'use') {
|
|
343
|
+
await useProvider(name, options)
|
|
344
|
+
} else if (command === 'list') {
|
|
345
|
+
listProviders()
|
|
346
|
+
} else if (command === 'status') {
|
|
347
|
+
status()
|
|
348
|
+
} else if (command === 'remove' || command === 'rm') {
|
|
349
|
+
removeProvider(name)
|
|
350
|
+
} else {
|
|
351
|
+
throw new Error(`未知命令: ${command}`)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
main().catch((error) => {
|
|
356
|
+
console.error(`\ncc-switch 失败: ${error.message}`)
|
|
357
|
+
process.exit(1)
|
|
358
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neuxsbotzz/cc-switch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Switch Claude Code and Codex CLI between official and Token.Me.Uk API providers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-switch": "./cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli.mjs",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"license": "UNLICENSED"
|
|
14
|
+
}
|