@leeandrew94/ccm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jarrod Watts
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # ccm - Claude Code Model Manager
2
+
3
+ [English](#english) | [中文](README.zh.md)
4
+
5
+ ## English
6
+
7
+ Run Claude Code with different AI models in separate terminals. One command per terminal, zero config conflicts.
8
+
9
+ ccm (Claude Code Model Manager) is a CLI tool that lets you run Claude Code with different AI models in different terminal windows at the same time. Each terminal gets its own model, API endpoint, and credentials — no shared config files, no conflicts. Switch models by name, track running instances, and manage profiles from the command line.
10
+
11
+ ## Prerequisites
12
+
13
+ - **Node.js** >= 18.0.0
14
+ - **Claude Code** installed globally: `npm i -g @anthropic-ai/claude-code`
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm i -g @leeandrew94/ccm
20
+ ```
21
+
22
+ ## Uninstall
23
+
24
+ ```bash
25
+ npm uninstall -g @leeandrew94/ccm
26
+
27
+ # (Optional) Remove config and runtime data
28
+ rm -rf ~/.ccm
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```bash
34
+ # Add a profile (interactive prompts for Base URL / Token / Model)
35
+ ccm add mimo
36
+
37
+ # Launch
38
+ ccm mimo
39
+
40
+ # Open another terminal, switch to a different model
41
+ ccm add deepseek
42
+ ccm deepseek
43
+
44
+ # See what's running
45
+ ccm ps
46
+ ```
47
+
48
+ ## Commands
49
+
50
+ | Command | Description |
51
+ |---|---|
52
+ | `ccm <name>` | Load profile and launch claude |
53
+ | `ccm add <name>` | Add a new profile |
54
+ | `ccm edit <name>` | Edit an existing profile |
55
+ | `ccm rm <name>` | Delete a profile |
56
+ | `ccm list` | List all profiles |
57
+ | `ccm config <name>` | Show profile env vars (no launch) |
58
+ | `ccm ps` | Show running instances |
59
+ | `ccm kill <name>` | Kill a running instance |
60
+ | `ccm kill --all` | Kill all running instances |
61
+ | `ccm check` | Check if claude is installed |
62
+ | `ccm test [name]` | Test API connection (omit name to test all) |
63
+ | `ccm balance [name]` | Query model balance/credits (omit name to query all) |
64
+
65
+ ## Shell Completions
66
+
67
+ ```bash
68
+ # zsh — add to ~/.zshrc
69
+ source <(ccm completions zsh)
70
+
71
+ # bash — add to ~/.bashrc
72
+ source <(ccm completions bash)
73
+ ```
74
+
75
+ ## Configuration
76
+
77
+ Profile config file `~/.ccm/profiles.json` (managed via `ccm add` / `ccm edit`):
78
+
79
+ ```json
80
+ {
81
+ "mimo": {
82
+ "ANTHROPIC_BASE_URL": "https://api.example.com",
83
+ "ANTHROPIC_AUTH_TOKEN": "sk-xxx",
84
+ "ANTHROPIC_MODEL": "model-name",
85
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL": "model-name",
86
+ "ANTHROPIC_DEFAULT_SONNET_MODEL": "model-name",
87
+ "ANTHROPIC_DEFAULT_OPUS_MODEL": "model-name"
88
+ }
89
+ }
90
+ ```
91
+
92
+ | Variable | Required | Description |
93
+ |---|---|---|
94
+ | `ANTHROPIC_BASE_URL` | Yes | API endpoint |
95
+ | `ANTHROPIC_AUTH_TOKEN` | Yes | API key |
96
+ | `ANTHROPIC_MODEL` | Yes | Model name |
97
+ | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | No | Haiku model mapping |
98
+ | `ANTHROPIC_DEFAULT_SONNET_MODEL` | No | Sonnet model mapping |
99
+ | `ANTHROPIC_DEFAULT_OPUS_MODEL` | No | Opus model mapping |
100
+
101
+ ## License
102
+
103
+ MIT
package/README.zh.md ADDED
@@ -0,0 +1,103 @@
1
+ # ccm - Claude Code Model Manager
2
+
3
+ [English](README.md) | [中文](#中文)
4
+
5
+ ## 中文
6
+
7
+ 在不同终端窗口用不同 AI 模型运行 Claude Code,一行命令切换,互不干扰。
8
+
9
+ ccm(Claude Code Model Manager)是一个命令行工具,让你在不同终端窗口同时使用不同的 AI 模型运行 Claude Code。每个终端独立绑定模型、API 地址和密钥,不修改全局配置,终端之间完全隔离。通过命令行按名称切换模型、查看运行实例、管理配置。
10
+
11
+ ## 环境要求
12
+
13
+ - **Node.js** >= 18.0.0
14
+ - **Claude Code** 已全局安装:`npm i -g @anthropic-ai/claude-code`
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ npm i -g @leeandrew94/ccm
20
+ ```
21
+
22
+ ## 卸载
23
+
24
+ ```bash
25
+ npm uninstall -g @leeandrew94/ccm
26
+
27
+ # (可选)删除配置和运行数据
28
+ rm -rf ~/.ccm
29
+ ```
30
+
31
+ ## 快速开始
32
+
33
+ ```bash
34
+ # 添加 profile(交互式输入 Base URL / Token / Model)
35
+ ccm add mimo
36
+
37
+ # 启动
38
+ ccm mimo
39
+
40
+ # 新开终端,换个模型
41
+ ccm add deepseek
42
+ ccm deepseek
43
+
44
+ # 查看谁在跑
45
+ ccm ps
46
+ ```
47
+
48
+ ## 命令
49
+
50
+ | 命令 | 说明 |
51
+ |---|---|
52
+ | `ccm <name>` | 加载 profile 并启动 claude |
53
+ | `ccm add <name>` | 新增 profile |
54
+ | `ccm edit <name>` | 修改 profile |
55
+ | `ccm rm <name>` | 删除 profile |
56
+ | `ccm list` | 列出所有 profile |
57
+ | `ccm config <name>` | 查看 profile 配置(不启动) |
58
+ | `ccm ps` | 查看运行中的实例 |
59
+ | `ccm kill <name>` | 停止指定实例 |
60
+ | `ccm kill --all` | 停止所有实例 |
61
+ | `ccm check` | 检查 claude 是否安装 |
62
+ | `ccm test [name]` | 测试 API 连接(不指定则测试全部) |
63
+ | `ccm balance [name]` | 查询模型余额(不指定则查询全部) |
64
+
65
+ ## Shell 补全
66
+
67
+ ```bash
68
+ # zsh — 添加到 ~/.zshrc
69
+ source <(ccm completions zsh)
70
+
71
+ # bash — 添加到 ~/.bashrc
72
+ source <(ccm completions bash)
73
+ ```
74
+
75
+ ## 配置
76
+
77
+ Profile 配置文件 `~/.ccm/profiles.json`(通过 `ccm add` / `ccm edit` 管理):
78
+
79
+ ```json
80
+ {
81
+ "mimo": {
82
+ "ANTHROPIC_BASE_URL": "https://api.example.com",
83
+ "ANTHROPIC_AUTH_TOKEN": "sk-xxx",
84
+ "ANTHROPIC_MODEL": "model-name",
85
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL": "model-name",
86
+ "ANTHROPIC_DEFAULT_SONNET_MODEL": "model-name",
87
+ "ANTHROPIC_DEFAULT_OPUS_MODEL": "model-name"
88
+ }
89
+ }
90
+ ```
91
+
92
+ | 变量 | 必填 | 说明 |
93
+ |---|---|---|
94
+ | `ANTHROPIC_BASE_URL` | 是 | API 端点 |
95
+ | `ANTHROPIC_AUTH_TOKEN` | 是 | API 密钥 |
96
+ | `ANTHROPIC_MODEL` | 是 | 模型名称 |
97
+ | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | 否 | Haiku 映射 |
98
+ | `ANTHROPIC_DEFAULT_SONNET_MODEL` | 否 | Sonnet 映射 |
99
+ | `ANTHROPIC_DEFAULT_OPUS_MODEL` | 否 | Opus 映射 |
100
+
101
+ ## License
102
+
103
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,948 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config.ts
4
+ import fs from "fs";
5
+
6
+ // src/constants.ts
7
+ import path from "path";
8
+ import os from "os";
9
+ var CONFIG_DIR = path.join(os.homedir(), ".ccm");
10
+ var PROFILES_FILE = path.join(CONFIG_DIR, "profiles.json");
11
+ var RUNS_DIR = path.join(CONFIG_DIR, "runs");
12
+ var SETTINGS_DIR = path.join(CONFIG_DIR, "settings");
13
+ var ANTHROPIC_KEYS = [
14
+ "ANTHROPIC_BASE_URL",
15
+ "ANTHROPIC_AUTH_TOKEN",
16
+ "ANTHROPIC_MODEL",
17
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
18
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
19
+ "ANTHROPIC_DEFAULT_OPUS_MODEL"
20
+ ];
21
+
22
+ // src/config.ts
23
+ function ensureConfigDir() {
24
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
25
+ fs.mkdirSync(RUNS_DIR, { recursive: true });
26
+ }
27
+ function loadProfiles() {
28
+ ensureConfigDir();
29
+ if (!fs.existsSync(PROFILES_FILE)) return {};
30
+ try {
31
+ const data = JSON.parse(fs.readFileSync(PROFILES_FILE, "utf-8"));
32
+ return typeof data === "object" && data !== null ? data : {};
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+ function saveProfiles(profiles) {
38
+ ensureConfigDir();
39
+ fs.writeFileSync(PROFILES_FILE, JSON.stringify(profiles, null, 2) + "\n", "utf-8");
40
+ }
41
+ function getProfile(name) {
42
+ return loadProfiles()[name];
43
+ }
44
+ function profileExists(name) {
45
+ return name in loadProfiles();
46
+ }
47
+ function addProfile(name, profile) {
48
+ const profiles = loadProfiles();
49
+ profiles[name] = profile;
50
+ saveProfiles(profiles);
51
+ }
52
+ function updateProfile(name, profile) {
53
+ const profiles = loadProfiles();
54
+ if (!(name in profiles)) throw new Error(`Profile '${name}' not found`);
55
+ profiles[name] = profile;
56
+ saveProfiles(profiles);
57
+ }
58
+ function deleteProfile(name) {
59
+ const profiles = loadProfiles();
60
+ if (!(name in profiles)) throw new Error(`Profile '${name}' not found`);
61
+ delete profiles[name];
62
+ saveProfiles(profiles);
63
+ }
64
+
65
+ // src/commands/launch.ts
66
+ import fs3 from "fs";
67
+ import path3 from "path";
68
+ import { spawn } from "child_process";
69
+
70
+ // src/output.ts
71
+ import readline from "readline";
72
+ function ok(msg) {
73
+ console.log(`\x1B[32m\u2713\x1B[0m ${msg}`);
74
+ }
75
+ function err(msg) {
76
+ console.error(`\x1B[31m\u2717\x1B[0m ${msg}`);
77
+ }
78
+ function info(msg) {
79
+ console.log(`\x1B[34m\u2192\x1B[0m ${msg}`);
80
+ }
81
+ function warn(msg) {
82
+ console.log(`\x1B[33m!\x1B[0m ${msg}`);
83
+ }
84
+ async function ask(prompt, defaultValue = "") {
85
+ const suffix = defaultValue ? ` [${defaultValue}]` : "";
86
+ const rl = readline.createInterface({
87
+ input: process.stdin,
88
+ output: process.stdout
89
+ });
90
+ return new Promise((resolve) => {
91
+ rl.question(` ${prompt}${suffix}: `, (answer) => {
92
+ rl.close();
93
+ resolve(answer.trim() || defaultValue);
94
+ });
95
+ });
96
+ }
97
+
98
+ // src/process.ts
99
+ import fs2 from "fs";
100
+ import path2 from "path";
101
+ function writeRun(pid, profileName, tty = "") {
102
+ fs2.mkdirSync(RUNS_DIR, { recursive: true });
103
+ if (!tty) {
104
+ try {
105
+ tty = fs2.readlinkSync(`/dev/fd/0`);
106
+ } catch {
107
+ tty = "";
108
+ }
109
+ }
110
+ const data = {
111
+ pid,
112
+ profile: profileName,
113
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
114
+ tty
115
+ };
116
+ fs2.writeFileSync(path2.join(RUNS_DIR, `${pid}.json`), JSON.stringify(data, null, 2) + "\n", "utf-8");
117
+ }
118
+ function removeRun(pid) {
119
+ const f = path2.join(RUNS_DIR, `${pid}.json`);
120
+ if (fs2.existsSync(f)) fs2.unlinkSync(f);
121
+ }
122
+ function isAlive(pid) {
123
+ try {
124
+ process.kill(pid, 0);
125
+ return true;
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+ function cleanupStaleRuns() {
131
+ if (!fs2.existsSync(RUNS_DIR)) return;
132
+ for (const f of fs2.readdirSync(RUNS_DIR)) {
133
+ if (!f.endsWith(".json")) continue;
134
+ try {
135
+ const pid = parseInt(path2.basename(f, ".json"), 10);
136
+ if (!isAlive(pid)) fs2.unlinkSync(path2.join(RUNS_DIR, f));
137
+ } catch {
138
+ }
139
+ }
140
+ }
141
+ function getAllRuns() {
142
+ cleanupStaleRuns();
143
+ if (!fs2.existsSync(RUNS_DIR)) return [];
144
+ const runs = [];
145
+ for (const f of fs2.readdirSync(RUNS_DIR).sort()) {
146
+ if (!f.endsWith(".json")) continue;
147
+ try {
148
+ const data = JSON.parse(fs2.readFileSync(path2.join(RUNS_DIR, f), "utf-8"));
149
+ data.uptime = calcUptime(data.started_at || "");
150
+ runs.push(data);
151
+ } catch {
152
+ }
153
+ }
154
+ return runs;
155
+ }
156
+ function getRunningProfiles() {
157
+ return new Set(getAllRuns().map((r) => r.profile));
158
+ }
159
+ function findRunByProfile(name) {
160
+ return getAllRuns().find((r) => r.profile === name);
161
+ }
162
+ function sleep(ms) {
163
+ const sab = new SharedArrayBuffer(4);
164
+ const int32 = new Int32Array(sab);
165
+ Atomics.wait(int32, 0, 0, ms);
166
+ }
167
+ function killProcess(pid) {
168
+ try {
169
+ process.kill(pid, "SIGTERM");
170
+ sleep(500);
171
+ if (isAlive(pid)) {
172
+ process.kill(pid, "SIGKILL");
173
+ sleep(300);
174
+ }
175
+ removeRun(pid);
176
+ return true;
177
+ } catch {
178
+ removeRun(pid);
179
+ return false;
180
+ }
181
+ }
182
+ function killByProfile(name) {
183
+ const run = findRunByProfile(name);
184
+ if (!run) return [false, `No running instance found for '${name}'`];
185
+ const pid = run.pid;
186
+ if (killProcess(pid)) return [true, `Killed ${name} (PID ${pid})`];
187
+ return [false, `Failed to kill PID ${pid}`];
188
+ }
189
+ function killAll() {
190
+ const runs = getAllRuns();
191
+ let success = 0;
192
+ for (const r of runs) {
193
+ if (killProcess(r.pid)) success++;
194
+ }
195
+ return [success, runs.length];
196
+ }
197
+ function calcUptime(startedAt) {
198
+ if (!startedAt) return "-";
199
+ try {
200
+ const delta = Date.now() - new Date(startedAt).getTime();
201
+ const total = Math.floor(delta / 1e3);
202
+ if (total < 60) return `${total}s`;
203
+ const h = Math.floor(total / 3600);
204
+ const m = Math.floor(total % 3600 / 60);
205
+ return h ? `${h}h ${m}m` : `${m}m`;
206
+ } catch {
207
+ return "-";
208
+ }
209
+ }
210
+
211
+ // src/commands/launch.ts
212
+ function whichClaude() {
213
+ const pathDirs = (process.env.PATH || "").split(path3.delimiter);
214
+ for (const dir of pathDirs) {
215
+ const full = path3.join(dir, "claude");
216
+ if (fs3.existsSync(full) && isExecutable(full)) return full;
217
+ }
218
+ return null;
219
+ }
220
+ function isExecutable(filePath) {
221
+ try {
222
+ fs3.accessSync(filePath, fs3.constants.X_OK);
223
+ return true;
224
+ } catch {
225
+ return false;
226
+ }
227
+ }
228
+ function writeSettings(name, profile) {
229
+ fs3.mkdirSync(SETTINGS_DIR, { recursive: true });
230
+ const settings = { env: {} };
231
+ for (const key of ANTHROPIC_KEYS) {
232
+ if (key in profile) settings.env[key] = profile[key];
233
+ }
234
+ const filePath = path3.join(SETTINGS_DIR, `${name}.json`);
235
+ fs3.writeFileSync(filePath, JSON.stringify(settings, null, 2) + "\n");
236
+ return filePath;
237
+ }
238
+ function cmdLaunch(args) {
239
+ const profile = getProfile(args.name);
240
+ if (!profile) {
241
+ err(`Profile '${args.name}' not found.`);
242
+ process.exit(1);
243
+ }
244
+ if (!whichClaude()) {
245
+ err("claude not found in PATH.");
246
+ info("Install: npm install -g @anthropic-ai/claude-code");
247
+ process.exit(1);
248
+ }
249
+ const settingsPath = writeSettings(args.name, profile);
250
+ writeRun(process.pid, args.name, "");
251
+ info(`Launching claude with profile '${args.name}'...`);
252
+ console.log();
253
+ const child = spawn("claude", ["--settings", settingsPath], {
254
+ stdio: "inherit"
255
+ });
256
+ child.on("exit", (code) => {
257
+ process.exit(code ?? 0);
258
+ });
259
+ }
260
+ function cmdConfig(args) {
261
+ const profile = getProfile(args.name);
262
+ if (!profile) {
263
+ err(`Profile '${args.name}' not found.`);
264
+ process.exit(1);
265
+ }
266
+ for (const key of ANTHROPIC_KEYS) {
267
+ if (key in profile) {
268
+ const val = String(profile[key]).replace(/'/g, "'\\''");
269
+ console.log(`export ${key}='${val}'`);
270
+ }
271
+ }
272
+ }
273
+ function cmdRegister(args) {
274
+ writeRun(args.pid, args.name, args.tty || "");
275
+ }
276
+
277
+ // src/commands/profile.ts
278
+ async function cmdAdd(args) {
279
+ const { name } = args;
280
+ if (profileExists(name)) {
281
+ warn(`Profile '${name}' already exists. Use 'ccm edit' to modify.`);
282
+ return;
283
+ }
284
+ info(`Creating profile '${name}'
285
+ `);
286
+ const baseUrl = await ask("Base URL");
287
+ if (!baseUrl) {
288
+ err("Base URL is required.");
289
+ process.exit(1);
290
+ }
291
+ const authToken = await ask("Auth Token");
292
+ if (!authToken) {
293
+ err("Auth Token is required.");
294
+ process.exit(1);
295
+ }
296
+ const model = await ask("Model name");
297
+ if (!model) {
298
+ err("Model name is required.");
299
+ process.exit(1);
300
+ }
301
+ const haiku = await ask("Default Haiku model", "(skip)");
302
+ const sonnet = await ask("Default Sonnet model", "(skip)");
303
+ const opus = await ask("Default Opus model", "(skip)");
304
+ const profile = {
305
+ ANTHROPIC_BASE_URL: baseUrl,
306
+ ANTHROPIC_AUTH_TOKEN: authToken,
307
+ ANTHROPIC_MODEL: model
308
+ };
309
+ if (haiku && haiku !== "(skip)") profile.ANTHROPIC_DEFAULT_HAIKU_MODEL = haiku;
310
+ if (sonnet && sonnet !== "(skip)") profile.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnet;
311
+ if (opus && opus !== "(skip)") profile.ANTHROPIC_DEFAULT_OPUS_MODEL = opus;
312
+ addProfile(name, profile);
313
+ ok(`Profile '${name}' saved.`);
314
+ }
315
+ async function cmdEdit(args) {
316
+ const { name } = args;
317
+ const profile = getProfile(name);
318
+ if (!profile) {
319
+ err(`Profile '${name}' not found.`);
320
+ process.exit(1);
321
+ }
322
+ const running = getRunningProfiles();
323
+ if (running.has(name)) {
324
+ warn(`Profile '${name}' is currently running. Changes apply on next launch.`);
325
+ }
326
+ info(`Editing '${name}' (Enter = keep current)
327
+ `);
328
+ const baseUrl = await ask("Base URL", profile.ANTHROPIC_BASE_URL || "");
329
+ const authToken = await ask("Auth Token", profile.ANTHROPIC_AUTH_TOKEN || "");
330
+ const model = await ask("Model name", profile.ANTHROPIC_MODEL || "");
331
+ const haiku = await ask("Default Haiku model", profile.ANTHROPIC_DEFAULT_HAIKU_MODEL || "");
332
+ const sonnet = await ask("Default Sonnet model", profile.ANTHROPIC_DEFAULT_SONNET_MODEL || "");
333
+ const opus = await ask("Default Opus model", profile.ANTHROPIC_DEFAULT_OPUS_MODEL || "");
334
+ const updated = {
335
+ ANTHROPIC_BASE_URL: baseUrl,
336
+ ANTHROPIC_AUTH_TOKEN: authToken,
337
+ ANTHROPIC_MODEL: model
338
+ };
339
+ if (haiku) updated.ANTHROPIC_DEFAULT_HAIKU_MODEL = haiku;
340
+ if (sonnet) updated.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnet;
341
+ if (opus) updated.ANTHROPIC_DEFAULT_OPUS_MODEL = opus;
342
+ updateProfile(name, updated);
343
+ ok(`Profile '${name}' updated.`);
344
+ }
345
+ async function cmdRm(args) {
346
+ const { name } = args;
347
+ if (!profileExists(name)) {
348
+ err(`Profile '${name}' not found.`);
349
+ process.exit(1);
350
+ }
351
+ const running = getRunningProfiles();
352
+ if (running.has(name)) {
353
+ warn(`Profile '${name}' is currently running.`);
354
+ const answer = await ask("Kill and delete? [y/N]");
355
+ if (answer.toLowerCase() !== "y") {
356
+ info("Cancelled.");
357
+ return;
358
+ }
359
+ const [success, msg] = killByProfile(name);
360
+ if (success) {
361
+ ok(msg);
362
+ } else {
363
+ err(msg);
364
+ process.exit(1);
365
+ }
366
+ }
367
+ deleteProfile(name);
368
+ ok(`Profile '${name}' deleted.`);
369
+ }
370
+ function maskToken(token) {
371
+ if (!token) return "?";
372
+ if (token.length <= 16) return token.slice(0, 4) + "****" + token.slice(-4);
373
+ return token.slice(0, 8) + "*".repeat(token.length - 12) + token.slice(-4);
374
+ }
375
+ function stripAnsi(text) {
376
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
377
+ }
378
+ function padAnsi(text, width) {
379
+ const visible = stripAnsi(text).length;
380
+ return text + " ".repeat(Math.max(0, width - visible));
381
+ }
382
+ function cmdList() {
383
+ const names = Object.keys(loadProfiles()).sort();
384
+ if (!names.length) {
385
+ info("No profiles configured. Use 'ccm add <name>' to create one.");
386
+ return;
387
+ }
388
+ const running = getRunningProfiles();
389
+ const allP = loadProfiles();
390
+ const W_NAME = 16;
391
+ const W_MODEL = 26;
392
+ const W_TOKEN = 28;
393
+ const W_URL = 46;
394
+ const W_STATUS = 12;
395
+ const totalW = W_NAME + W_MODEL + W_TOKEN + W_URL + W_STATUS + 10;
396
+ const line = "\u2500".repeat(totalW);
397
+ console.log();
398
+ console.log(` \x1B[1;36m${line}\x1B[0m`);
399
+ console.log(
400
+ ` \x1B[1;36m\u2502\x1B[0m \x1B[1m${"PROFILE".padStart(Math.floor((W_NAME - 1 + 7) / 2)).padEnd(W_NAME - 1)}\x1B[0m \x1B[1;36m\u2502\x1B[0m \x1B[1m${"MODEL".padStart(Math.floor((W_MODEL - 1 + 5) / 2)).padEnd(W_MODEL - 1)}\x1B[0m \x1B[1;36m\u2502\x1B[0m \x1B[1m${"TOKEN".padStart(Math.floor((W_TOKEN - 1 + 5) / 2)).padEnd(W_TOKEN - 1)}\x1B[0m \x1B[1;36m\u2502\x1B[0m \x1B[1m${"ENDPOINT".padStart(Math.floor((W_URL - 1 + 8) / 2)).padEnd(W_URL - 1)}\x1B[0m \x1B[1;36m\u2502\x1B[0m \x1B[1m${"STATUS".padStart(Math.floor((W_STATUS - 1 + 6) / 2)).padEnd(W_STATUS - 1)}\x1B[0m \x1B[1;36m\u2502\x1B[0m`
401
+ );
402
+ console.log(` \x1B[1;36m${line}\x1B[0m`);
403
+ for (let i = 0; i < names.length; i++) {
404
+ const name = names[i];
405
+ const p = allP[name];
406
+ const model = p.ANTHROPIC_MODEL || "?";
407
+ const token = maskToken(p.ANTHROPIC_AUTH_TOKEN || "");
408
+ let url = p.ANTHROPIC_BASE_URL || "?";
409
+ if (url.length > W_URL - 4) url = url.slice(0, W_URL - 7) + "...";
410
+ let status;
411
+ let nameDisplay;
412
+ if (running.has(name)) {
413
+ status = "\x1B[32m\u25CF running\x1B[0m";
414
+ nameDisplay = `\x1B[32m${name}\x1B[0m`;
415
+ } else {
416
+ status = "\x1B[90m\u25CB idle\x1B[0m";
417
+ nameDisplay = `\x1B[1m${name}\x1B[0m`;
418
+ }
419
+ console.log(
420
+ ` \x1B[36m\u2502\x1B[0m ${padAnsi(nameDisplay, W_NAME - 1)} \x1B[36m\u2502\x1B[0m ${model.padEnd(W_MODEL - 1)} \x1B[36m\u2502\x1B[0m ${token.padEnd(W_TOKEN - 1)} \x1B[36m\u2502\x1B[0m ${url.padEnd(W_URL - 1)} \x1B[36m\u2502\x1B[0m ${padAnsi(status, W_STATUS - 1)} \x1B[36m\u2502\x1B[0m`
421
+ );
422
+ const aliases = [];
423
+ if (p.ANTHROPIC_DEFAULT_HAIKU_MODEL) aliases.push(`haiku=${p.ANTHROPIC_DEFAULT_HAIKU_MODEL}`);
424
+ if (p.ANTHROPIC_DEFAULT_SONNET_MODEL) aliases.push(`sonnet=${p.ANTHROPIC_DEFAULT_SONNET_MODEL}`);
425
+ if (p.ANTHROPIC_DEFAULT_OPUS_MODEL) aliases.push(`opus=${p.ANTHROPIC_DEFAULT_OPUS_MODEL}`);
426
+ if (aliases.length) {
427
+ let aliasStr = aliases.join(", ");
428
+ if (aliasStr.length > totalW - 8) aliasStr = aliasStr.slice(0, totalW - 11) + "...";
429
+ console.log(
430
+ ` \x1B[36m\u2502\x1B[0m \x1B[90m${aliasStr.padEnd(totalW - 4)}\x1B[0m \x1B[36m\u2502\x1B[0m`
431
+ );
432
+ }
433
+ if (i < names.length - 1) console.log(` \x1B[36m${line}\x1B[0m`);
434
+ }
435
+ console.log(` \x1B[1;36m${line}\x1B[0m`);
436
+ const total = names.length;
437
+ const runCount = running.size;
438
+ console.log(` \x1B[90m${total} profile${total !== 1 ? "s" : ""} total, ${runCount} running\x1B[0m
439
+ `);
440
+ }
441
+
442
+ // src/commands/runtime.ts
443
+ function cmdPs() {
444
+ const runs = getAllRuns();
445
+ if (!runs.length) {
446
+ info("No running instances.");
447
+ return;
448
+ }
449
+ console.log(`
450
+ ${"PID".padEnd(8)} ${"PROFILE".padEnd(15)} ${"TTY".padEnd(20)} ${"UPTIME"}`);
451
+ console.log(` ${"\u2500".repeat(8)} ${"\u2500".repeat(15)} ${"\u2500".repeat(20)} ${"\u2500".repeat(10)}`);
452
+ for (const r of runs) {
453
+ console.log(
454
+ ` ${String(r.pid).padEnd(8)} ${r.profile.padEnd(15)} ${(r.tty || "-").padEnd(20)} ${r.uptime || "-"}`
455
+ );
456
+ }
457
+ console.log();
458
+ }
459
+ function cmdKill(args) {
460
+ if (args.all) {
461
+ const [count, total] = killAll();
462
+ if (total === 0) {
463
+ info("No running instances.");
464
+ } else {
465
+ ok(`Killed ${count}/${total} instances.`);
466
+ }
467
+ return;
468
+ }
469
+ if (!args.name) {
470
+ err("Specify a profile name or use --all.");
471
+ process.exit(1);
472
+ }
473
+ const [success, msg] = killByProfile(args.name);
474
+ if (success) {
475
+ ok(msg);
476
+ } else {
477
+ err(msg);
478
+ process.exit(1);
479
+ }
480
+ }
481
+ function cmdCheck() {
482
+ const claudePath = whichClaude();
483
+ if (claudePath) {
484
+ ok(`claude found at ${claudePath}`);
485
+ } else {
486
+ err("claude not found in PATH.");
487
+ info("Install: npm install -g @anthropic-ai/claude-code");
488
+ process.exit(1);
489
+ }
490
+ }
491
+
492
+ // src/commands/diagnose.ts
493
+ import https from "https";
494
+ import http from "http";
495
+ function httpRequest(url, token, method = "GET", timeout = 1e4, headers = {}) {
496
+ return new Promise((resolve) => {
497
+ const parsed = new URL(url);
498
+ const mod = parsed.protocol === "https:" ? https : http;
499
+ const reqHeaders = {
500
+ "Authorization": `Bearer ${token}`,
501
+ "x-api-key": token,
502
+ "Content-Type": "application/json",
503
+ ...headers
504
+ };
505
+ const req = mod.request(url, { method, headers: reqHeaders, timeout }, (res) => {
506
+ let body = "";
507
+ res.on("data", (chunk) => {
508
+ body += chunk;
509
+ });
510
+ res.on("end", () => {
511
+ try {
512
+ resolve({ status: res.statusCode || 0, body: JSON.parse(body) });
513
+ } catch {
514
+ resolve({ status: res.statusCode || 0, body });
515
+ }
516
+ });
517
+ });
518
+ req.on("error", (e) => {
519
+ resolve({ status: 0, body: {}, error: e.message });
520
+ });
521
+ req.on("timeout", () => {
522
+ req.destroy();
523
+ resolve({ status: 0, body: {}, error: "Request timed out" });
524
+ });
525
+ req.end();
526
+ });
527
+ }
528
+ function isAnthropicCompat(baseUrl) {
529
+ const lower = baseUrl.toLowerCase();
530
+ return lower.includes("/anthropic") || lower.includes("anthropic");
531
+ }
532
+ async function testConnection(baseUrl, token) {
533
+ const url = baseUrl.replace(/\/+$/, "");
534
+ if (isAnthropicCompat(baseUrl)) {
535
+ const result = await httpRequest(`${url}/v1/messages`, token, "POST", 1e4, {
536
+ "anthropic-version": "2023-06-01"
537
+ });
538
+ if (result.status === 200 || result.status === 400) return { ok: true, endpoint: "/v1/messages" };
539
+ if (result.status === 401 || result.status === 403) return { ok: false, error: "Authentication failed (invalid token)" };
540
+ return { ok: false, error: result.error || `HTTP ${result.status}` };
541
+ }
542
+ for (const endpoint of ["/v1/models", "/models"]) {
543
+ const result = await httpRequest(`${url}${endpoint}`, token);
544
+ if (result.status === 200) return { ok: true, endpoint };
545
+ if (result.status === 401 || result.status === 403) return { ok: false, error: "Authentication failed (invalid token)" };
546
+ }
547
+ try {
548
+ const baseParts = url.split("//");
549
+ const hostUrl = `${baseParts[0]}//${baseParts[1].split("/")[0]}/`;
550
+ const result = await httpRequest(hostUrl, token);
551
+ if (result.status > 0) return { ok: true, endpoint: "(host reachable)" };
552
+ } catch {
553
+ }
554
+ return { ok: false, error: `HTTP connection failed` };
555
+ }
556
+ async function queryBalance(baseUrl, token) {
557
+ let url = baseUrl.replace(/\/+$/, "");
558
+ for (const suffix of ["/anthropic", "/v1/chat/completions", "/v1"]) {
559
+ if (url.endsWith(suffix)) url = url.slice(0, -suffix.length);
560
+ }
561
+ const endpoints = [
562
+ `${url}/user/balance`,
563
+ `${url}/v1/user/balance`,
564
+ `${url}/dashboard/billing/usage`,
565
+ `${url}/api/user/balance`,
566
+ `${url}/billing/usage`,
567
+ `${url}/v1/dashboard/billing/usage`
568
+ ];
569
+ for (const ep of endpoints) {
570
+ const result = await httpRequest(ep, token);
571
+ if (result.status === 200) return result.body;
572
+ }
573
+ return null;
574
+ }
575
+ function stripAnsi2(text) {
576
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
577
+ }
578
+ function fmtAmount(value, currency = "") {
579
+ let formatted;
580
+ const num = parseFloat(value);
581
+ if (!isNaN(num)) {
582
+ formatted = Number.isInteger(num) ? num.toLocaleString() : num.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
583
+ } else {
584
+ formatted = String(value);
585
+ }
586
+ return currency ? `${formatted} ${currency}` : formatted;
587
+ }
588
+ function bar(ratio, width = 16) {
589
+ const filled = Math.max(0, Math.min(width, Math.floor(ratio * width)));
590
+ const empty = width - filled;
591
+ let color;
592
+ if (ratio > 0.8) color = "\x1B[31m";
593
+ else if (ratio > 0.5) color = "\x1B[33m";
594
+ else color = "\x1B[32m";
595
+ return `${color}${"\u2588".repeat(filled)}\x1B[90m${"\u2591".repeat(empty)}\x1B[0m`;
596
+ }
597
+ function formatBalanceCell(data, statusWidth) {
598
+ const currency = data.currency || "";
599
+ if ("balance" in data && (typeof data.balance === "string" || typeof data.balance === "number")) {
600
+ const bal = fmtAmount(data.balance, currency);
601
+ return [`\x1B[1;32m${bal}\x1B[0m`, "\x1B[32m\u25CF active\x1B[0m"];
602
+ }
603
+ if ("balance_infos" in data && Array.isArray(data.balance_infos)) {
604
+ for (const info2 of data.balance_infos) {
605
+ const cur = info2.currency || currency;
606
+ const balance = info2.balance || info2.total_balance || "";
607
+ const granted = info2.total_granted || "";
608
+ const used = info2.total_used || "";
609
+ const parts = [];
610
+ if (balance) parts.push(`\x1B[1;32m${fmtAmount(balance, cur)}\x1B[0m`);
611
+ const balanceStr = parts.length ? parts.join(" ") : "\x1B[90m\u2014\x1B[0m";
612
+ let statusStr;
613
+ if (granted && used) {
614
+ try {
615
+ const ratio = parseFloat(used) / parseFloat(granted);
616
+ const pct = ratio * 100;
617
+ const barStr = bar(ratio, 12);
618
+ statusStr = `${barStr} ${pct.toFixed(0)}% used`;
619
+ } catch {
620
+ statusStr = "\x1B[32m\u25CF active\x1B[0m";
621
+ }
622
+ } else if (data.is_available) {
623
+ statusStr = "\x1B[32m\u25CF active\x1B[0m";
624
+ } else {
625
+ statusStr = "\x1B[31m\u25CF inactive\x1B[0m";
626
+ }
627
+ return [balanceStr, statusStr];
628
+ }
629
+ return ["\x1B[90m\u2014\x1B[0m", "\x1B[90mno data\x1B[0m"];
630
+ }
631
+ if ("total_available" in data) {
632
+ const bal = fmtAmount(data.total_available, currency);
633
+ return [`\x1B[1;32m${bal}\x1B[0m`, "\x1B[32m\u25CF active\x1B[0m"];
634
+ }
635
+ if ("data" in data && typeof data.data === "object" && data.data !== null) {
636
+ const d = data.data;
637
+ const granted = d.total_granted;
638
+ const used = d.total_used;
639
+ const available = d.total_available || d.total_remain;
640
+ let balanceStr;
641
+ if (available != null) {
642
+ balanceStr = `\x1B[1;32m${fmtAmount(available, currency)}\x1B[0m`;
643
+ } else if (granted != null) {
644
+ balanceStr = `\x1B[1m${fmtAmount(granted, currency)}\x1B[0m`;
645
+ } else {
646
+ balanceStr = "\x1B[90m\u2014\x1B[0m";
647
+ }
648
+ let statusStr;
649
+ if (granted != null && used != null) {
650
+ try {
651
+ const ratio = parseFloat(used) / parseFloat(granted);
652
+ const pct = ratio * 100;
653
+ const barStr = bar(ratio, 12);
654
+ statusStr = `${barStr} ${pct.toFixed(0)}% used`;
655
+ } catch {
656
+ statusStr = "\x1B[32m\u25CF active\x1B[0m";
657
+ }
658
+ } else {
659
+ statusStr = "\x1B[32m\u25CF active\x1B[0m";
660
+ }
661
+ return [balanceStr, statusStr];
662
+ }
663
+ let raw = JSON.stringify(data);
664
+ if (raw.length > statusWidth - 6) raw = raw.slice(0, statusWidth - 9) + "...";
665
+ return ["\x1B[90m\u2014\x1B[0m", `\x1B[90m${raw}\x1B[0m`];
666
+ }
667
+ async function cmdTest(args) {
668
+ const names = args.name ? [args.name] : Object.keys(loadProfiles()).sort();
669
+ if (!names.length) {
670
+ info("No profiles configured.");
671
+ return;
672
+ }
673
+ console.log();
674
+ for (const name of names) {
675
+ const profile = getProfile(name);
676
+ if (!profile) {
677
+ err(`Profile '${name}' not found.`);
678
+ continue;
679
+ }
680
+ const baseUrl = profile.ANTHROPIC_BASE_URL || "";
681
+ const token = profile.ANTHROPIC_AUTH_TOKEN || "";
682
+ const model = profile.ANTHROPIC_MODEL || "?";
683
+ console.log(` \x1B[1m${name}\x1B[0m (${model})`);
684
+ if (!baseUrl || !token) {
685
+ err(" Missing base URL or token");
686
+ console.log();
687
+ continue;
688
+ }
689
+ const result = await testConnection(baseUrl, token);
690
+ if (result.ok) {
691
+ ok(` Connected via ${result.endpoint}`);
692
+ } else {
693
+ err(` Failed: ${result.error}`);
694
+ }
695
+ console.log();
696
+ }
697
+ }
698
+ async function cmdBalance(args) {
699
+ const names = args.name ? [args.name] : Object.keys(loadProfiles()).sort();
700
+ if (!names.length) {
701
+ info("No profiles configured.");
702
+ return;
703
+ }
704
+ const W_NAME = 16;
705
+ const W_MODEL = 22;
706
+ const W_BALANCE = 20;
707
+ const W_STATUS = 36;
708
+ const totalW = W_NAME + W_MODEL + W_BALANCE + W_STATUS + 10;
709
+ const line = "\u2500".repeat(totalW);
710
+ console.log();
711
+ console.log(` \x1B[1;36m${line}\x1B[0m`);
712
+ console.log(
713
+ ` \x1B[1;36m\u2502\x1B[0m \x1B[1m${"PROFILE".padStart(Math.floor((W_NAME - 1 + 7) / 2)).padEnd(W_NAME - 1)}\x1B[0m \x1B[1;36m\u2502\x1B[0m \x1B[1m${"MODEL".padStart(Math.floor((W_MODEL - 1 + 5) / 2)).padEnd(W_MODEL - 1)}\x1B[0m \x1B[1;36m\u2502\x1B[0m \x1B[1m${"BALANCE".padStart(Math.floor((W_BALANCE - 1 + 7) / 2)).padEnd(W_BALANCE - 1)}\x1B[0m \x1B[1;36m\u2502\x1B[0m \x1B[1m${"STATUS".padStart(Math.floor((W_STATUS - 1 + 6) / 2)).padEnd(W_STATUS - 1)}\x1B[0m \x1B[1;36m\u2502\x1B[0m`
714
+ );
715
+ console.log(` \x1B[1;36m${line}\x1B[0m`);
716
+ for (let i = 0; i < names.length; i++) {
717
+ const name = names[i];
718
+ const profile = getProfile(name);
719
+ if (!profile) {
720
+ err(` Profile '${name}' not found.`);
721
+ continue;
722
+ }
723
+ const baseUrl = profile.ANTHROPIC_BASE_URL || "";
724
+ const token = profile.ANTHROPIC_AUTH_TOKEN || "";
725
+ let model = profile.ANTHROPIC_MODEL || "?";
726
+ if (model.length > W_MODEL - 4) model = model.slice(0, W_MODEL - 7) + "...";
727
+ let balanceStr;
728
+ let statusStr;
729
+ if (!baseUrl || !token) {
730
+ balanceStr = "\x1B[31m\u2014\x1B[0m";
731
+ statusStr = "\x1B[31mmissing credentials\x1B[0m";
732
+ } else {
733
+ const balanceData = await queryBalance(baseUrl, token);
734
+ if (balanceData === null) {
735
+ balanceStr = "\x1B[90m\u2014\x1B[0m";
736
+ statusStr = "\x1B[90mapi not available\x1B[0m";
737
+ } else {
738
+ [balanceStr, statusStr] = formatBalanceCell(balanceData, W_STATUS);
739
+ }
740
+ }
741
+ console.log(
742
+ ` \x1B[36m\u2502\x1B[0m \x1B[1m${name.padEnd(W_NAME - 1)}\x1B[0m \x1B[36m\u2502\x1B[0m ${model.padEnd(W_MODEL - 1)} \x1B[36m\u2502\x1B[0m ${padAnsi2(balanceStr, W_BALANCE - 1)} \x1B[36m\u2502\x1B[0m ${padAnsi2(statusStr, W_STATUS - 1)} \x1B[36m\u2502\x1B[0m`
743
+ );
744
+ if (i < names.length - 1) console.log(` \x1B[36m${line}\x1B[0m`);
745
+ }
746
+ console.log(` \x1B[1;36m${line}\x1B[0m
747
+ `);
748
+ }
749
+ function padAnsi2(text, width) {
750
+ const visible = stripAnsi2(text).length;
751
+ return text + " ".repeat(Math.max(0, width - visible));
752
+ }
753
+
754
+ // src/completions.ts
755
+ var COMMANDS = ["add", "edit", "rm", "list", "ls", "ps", "kill", "check", "test", "balance", "bal", "config", "completions"];
756
+ function cmdCompletions(args) {
757
+ const shell = args.shell || detectShell();
758
+ if (shell === "zsh") {
759
+ console.log(ZSH_COMPLETION);
760
+ } else if (shell === "bash") {
761
+ console.log(BASH_COMPLETION);
762
+ } else {
763
+ console.error(`Unsupported shell: ${shell}. Use 'bash' or 'zsh'.`);
764
+ process.exit(1);
765
+ }
766
+ }
767
+ function detectShell() {
768
+ const shell = process.env.SHELL || "";
769
+ if (shell.includes("zsh")) return "zsh";
770
+ if (shell.includes("bash")) return "bash";
771
+ return "zsh";
772
+ }
773
+ var ZSH_COMPLETION = `#compdef ccm
774
+
775
+ _ccm() {
776
+ local -a commands profiles
777
+ commands=(${COMMANDS.join(" ")})
778
+ profiles=("\${(@f)$(ccm list 2>/dev/null | grep -E '^\\s*\u2502' | grep -v 'PROFILE' | grep -v '\u2500' | awk '{print $2}')}")
779
+ _arguments "1: :(\${commands} \${profiles})" "2: :(\${profiles})"
780
+ }
781
+
782
+ _ccm "$@"`;
783
+ var BASH_COMPLETION = `_ccm_completions() {
784
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
785
+ local prev="\${COMP_WORDS[COMP_CWORD-1]}"
786
+
787
+ if [ "$COMP_CWORD" -eq 1 ]; then
788
+ local commands="${COMMANDS.join(" ")}"
789
+ local profiles
790
+ profiles=$(ccm list 2>/dev/null | grep -E '^\\s*\u2502' | grep -v 'PROFILE' | grep -v '\u2500' | awk '{print $2}' | tr '\\n' ' ')
791
+ COMPREPLY=($(compgen -W "$commands $profiles" -- "$cur"))
792
+ elif [ "$COMP_CWORD" -eq 2 ]; then
793
+ case "$prev" in
794
+ edit|rm|kill)
795
+ local profiles
796
+ profiles=$(ccm list 2>/dev/null | grep -E '^\\s*\u2502' | grep -v 'PROFILE' | grep -v '\u2500' | awk '{print $2}' | tr '\\n' ' ')
797
+ COMPREPLY=($(compgen -W "$profiles" -- "$cur"))
798
+ ;;
799
+ esac
800
+ fi
801
+ }
802
+
803
+ complete -F _ccm_completions ccm`;
804
+
805
+ // src/cli.ts
806
+ var VERSION = "0.1.0";
807
+ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
808
+ "_launch",
809
+ "_register",
810
+ "add",
811
+ "edit",
812
+ "rm",
813
+ "list",
814
+ "ls",
815
+ "ps",
816
+ "kill",
817
+ "check",
818
+ "test",
819
+ "balance",
820
+ "bal",
821
+ "config",
822
+ "completions"
823
+ ]);
824
+ function printHelp() {
825
+ console.log(`
826
+ Usage: ccm <command> [options]
827
+
828
+ Commands:
829
+ add <name> Add a new model profile
830
+ edit <name> Edit an existing profile
831
+ rm <name> Delete a profile
832
+ list, ls List all profiles
833
+ ps Show running Claude instances
834
+ kill <name> Kill a running instance
835
+ kill --all Kill all running instances
836
+ check Check if claude is installed
837
+ test [name] Test API connection for profiles
838
+ balance [name] Query model balance/credits
839
+ config <name> Show profile environment variables
840
+ completions Print shell completion script
841
+
842
+ Options:
843
+ -v, --version Show version
844
+ -h, --help Show help
845
+
846
+ Shortcuts:
847
+ ccm <profile> Launch claude with the named profile
848
+ `);
849
+ }
850
+ function parseArgs(argv) {
851
+ const args = { _: [] };
852
+ let i = 0;
853
+ while (i < argv.length) {
854
+ const arg = argv[i];
855
+ if (arg === "--all") {
856
+ args.all = true;
857
+ } else if (arg.startsWith("-")) {
858
+ } else {
859
+ args._.push(arg);
860
+ }
861
+ i++;
862
+ }
863
+ const command = args._[0] || "";
864
+ const positional = args._.slice(1);
865
+ if (positional.length > 0) {
866
+ if (["_launch", "add", "edit", "rm", "config"].includes(command)) {
867
+ args.name = positional[0];
868
+ } else if (command === "_register") {
869
+ args.name = positional[0];
870
+ args.pid = parseInt(positional[1], 10);
871
+ } else if (["test", "balance", "bal", "kill"].includes(command)) {
872
+ args.name = positional[0];
873
+ } else if (command === "completions") {
874
+ args.shell = positional[0];
875
+ }
876
+ }
877
+ return { command, args };
878
+ }
879
+ async function main() {
880
+ const rawArgs = process.argv.slice(2);
881
+ if (rawArgs.includes("-v") || rawArgs.includes("--version")) {
882
+ console.log(`ccm ${VERSION}`);
883
+ return;
884
+ }
885
+ if (rawArgs.includes("-h") || rawArgs.includes("--help") || rawArgs.length === 0) {
886
+ printHelp();
887
+ return;
888
+ }
889
+ const firstArg = rawArgs[0];
890
+ if (firstArg && !KNOWN_COMMANDS.has(firstArg) && !firstArg.startsWith("-")) {
891
+ if (profileExists(firstArg)) {
892
+ cmdLaunch({ name: firstArg });
893
+ return;
894
+ }
895
+ }
896
+ const { command, args } = parseArgs(rawArgs);
897
+ switch (command) {
898
+ case "_launch":
899
+ cmdLaunch({ name: args.name });
900
+ break;
901
+ case "_register":
902
+ cmdRegister({ name: args.name, pid: args.pid, tty: args.tty });
903
+ break;
904
+ case "add":
905
+ await cmdAdd({ name: args.name });
906
+ break;
907
+ case "edit":
908
+ await cmdEdit({ name: args.name });
909
+ break;
910
+ case "rm":
911
+ await cmdRm({ name: args.name });
912
+ break;
913
+ case "list":
914
+ case "ls":
915
+ cmdList();
916
+ break;
917
+ case "ps":
918
+ cmdPs();
919
+ break;
920
+ case "kill":
921
+ cmdKill({ name: args.name, all: args.all });
922
+ break;
923
+ case "check":
924
+ cmdCheck();
925
+ break;
926
+ case "test":
927
+ await cmdTest({ name: args.name });
928
+ break;
929
+ case "balance":
930
+ case "bal":
931
+ await cmdBalance({ name: args.name });
932
+ break;
933
+ case "config":
934
+ cmdConfig({ name: args.name });
935
+ break;
936
+ case "completions":
937
+ cmdCompletions({ shell: args.shell });
938
+ break;
939
+ default:
940
+ console.error(`Unknown command: ${command}`);
941
+ printHelp();
942
+ process.exit(1);
943
+ }
944
+ }
945
+ main().catch((e) => {
946
+ console.error(e);
947
+ process.exit(1);
948
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@leeandrew94/ccm",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code Model Manager - switch between AI models per terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "ccm": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch",
18
+ "typecheck": "tsc --noEmit",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "claude",
23
+ "claude-code",
24
+ "model",
25
+ "manager",
26
+ "ai",
27
+ "cli"
28
+ ],
29
+ "author": "leeandrew94",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/leeandrew94/ccm-cli"
34
+ },
35
+ "devDependencies": {
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.4.0"
38
+ }
39
+ }