@leeandrew94/ccm 0.2.1 → 0.3.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 CHANGED
@@ -35,14 +35,14 @@ rm -rf ~/.ccm
35
35
  ## Quick Start
36
36
 
37
37
  ```bash
38
- # Add a profile (interactive prompts for Base URL / Token / Model)
39
- ccm add mimo
38
+ # Interactive setup wizard (recommended)
39
+ ccm init
40
40
 
41
41
  # Launch
42
42
  ccm mimo
43
43
 
44
44
  # Open another terminal, switch to a different model
45
- ccm add deepseek
45
+ ccm init
46
46
  ccm deepseek
47
47
 
48
48
  # See what's running
@@ -53,6 +53,7 @@ ccm ps
53
53
 
54
54
  | Command | Description |
55
55
  |---|---|
56
+ | `ccm init` | Interactive setup wizard |
56
57
  | `ccm <name>` | Load profile and launch claude |
57
58
  | `ccm add <name>` | Add a new profile |
58
59
  | `ccm edit <name>` | Edit an existing profile |
package/README.zh.md CHANGED
@@ -35,14 +35,14 @@ rm -rf ~/.ccm
35
35
  ## 快速开始
36
36
 
37
37
  ```bash
38
- # 添加 profile(交互式输入 Base URL / Token / Model)
39
- ccm add mimo
38
+ # 交互式初始化(推荐)
39
+ ccm init
40
40
 
41
41
  # 启动
42
42
  ccm mimo
43
43
 
44
44
  # 新开终端,换个模型
45
- ccm add deepseek
45
+ ccm init
46
46
  ccm deepseek
47
47
 
48
48
  # 查看谁在跑
@@ -53,6 +53,7 @@ ccm ps
53
53
 
54
54
  | 命令 | 说明 |
55
55
  |---|---|
56
+ | `ccm init` | 交互式初始化向导 |
56
57
  | `ccm <name>` | 加载 profile 并启动 claude |
57
58
  | `ccm add <name>` | 新增 profile |
58
59
  | `ccm edit <name>` | 修改 profile |
package/bin/ccm.js CHANGED
@@ -443,6 +443,213 @@ function cmdList() {
443
443
  `);
444
444
  }
445
445
 
446
+ // src/commands/init.ts
447
+ import https from "https";
448
+ import http from "http";
449
+ function httpRequest(url, token, timeout = 8e3) {
450
+ return new Promise((resolve) => {
451
+ const parsed = new URL(url);
452
+ const mod = parsed.protocol === "https:" ? https : http;
453
+ const req = mod.request(url, {
454
+ method: "GET",
455
+ headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
456
+ timeout
457
+ }, (res) => {
458
+ let body = "";
459
+ res.on("data", (chunk) => {
460
+ body += chunk;
461
+ });
462
+ res.on("end", () => {
463
+ try {
464
+ resolve({ status: res.statusCode || 0, body: JSON.parse(body) });
465
+ } catch {
466
+ resolve({ status: res.statusCode || 0, body });
467
+ }
468
+ });
469
+ });
470
+ req.on("error", () => resolve({ status: 0, body: {} }));
471
+ req.on("timeout", () => {
472
+ req.destroy();
473
+ resolve({ status: 0, body: {} });
474
+ });
475
+ req.end();
476
+ });
477
+ }
478
+ async function fetchModels(baseUrl, token) {
479
+ const url = baseUrl.replace(/\/+$/, "");
480
+ for (const suffix of ["/anthropic", "/v1/chat/completions"]) {
481
+ if (url.endsWith(suffix)) return [];
482
+ }
483
+ const endpoints = [`${url}/models`, `${url}/v1/models`];
484
+ for (const ep of endpoints) {
485
+ const result = await httpRequest(ep, token);
486
+ if (result.status === 200 && result.body) {
487
+ const data = result.body.data || result.body.models;
488
+ if (Array.isArray(data)) {
489
+ return data.map((m) => m.id || m.name).filter(Boolean).sort();
490
+ }
491
+ }
492
+ }
493
+ return [];
494
+ }
495
+ async function selectModel(models, label) {
496
+ if (models.length === 0) {
497
+ return await ask("Model name");
498
+ }
499
+ console.log(`
500
+ \x1B[1m${label}\x1B[0m (\u2191\u2193 navigate, Enter to select):
501
+ `);
502
+ const PAGE = 12;
503
+ let offset = 0;
504
+ let selected = 0;
505
+ function render() {
506
+ process.stdout.write("\x1B[?25l");
507
+ const start = offset;
508
+ const end = Math.min(offset + PAGE, models.length);
509
+ for (let i = start; i < end; i++) {
510
+ process.stdout.write("\x1B[2K");
511
+ if (i === selected) {
512
+ console.log(` \x1B[36m\u276F\x1B[0m \x1B[1m${models[i]}\x1B[0m`);
513
+ } else {
514
+ console.log(` \x1B[90m${models[i]}\x1B[0m`);
515
+ }
516
+ }
517
+ process.stdout.write("\x1B[2K");
518
+ if (models.length > PAGE) {
519
+ console.log(` \x1B[90m${selected + 1}/${models.length} \u2014 \u2191\u2193 scroll, Enter select\x1B[0m`);
520
+ }
521
+ const lines = end - start + (models.length > PAGE ? 1 : 0);
522
+ process.stdout.write(`\x1B[${lines}A`);
523
+ }
524
+ return new Promise((resolve) => {
525
+ process.stdin.setRawMode(true);
526
+ process.stdin.resume();
527
+ process.stdin.setEncoding("utf-8");
528
+ render();
529
+ let buf = "";
530
+ process.stdin.on("data", function onData(key) {
531
+ if (key === "\x1B[A") {
532
+ selected = Math.max(0, selected - 1);
533
+ if (selected < offset) offset = selected;
534
+ render();
535
+ } else if (key === "\x1B[B") {
536
+ selected = Math.min(models.length - 1, selected + 1);
537
+ if (selected >= offset + PAGE) offset = selected - PAGE + 1;
538
+ render();
539
+ } else if (key === "\r" || key === "\n") {
540
+ const lines = Math.min(PAGE, models.length - offset) + (models.length > PAGE ? 1 : 0);
541
+ for (let i = 0; i < lines; i++) {
542
+ process.stdout.write("\x1B[2K\n");
543
+ }
544
+ process.stdout.write(`\x1B[${lines}A`);
545
+ for (let i = 0; i < lines; i++) {
546
+ process.stdout.write("\x1B[2K\n");
547
+ }
548
+ process.stdout.write(`\x1B[${lines}A`);
549
+ process.stdout.write(` \x1B[36m\u276F\x1B[0m \x1B[1m${models[selected]}\x1B[0m
550
+ `);
551
+ process.stdout.write("\x1B[?25h");
552
+ process.stdin.setRawMode(false);
553
+ process.stdin.removeListener("data", onData);
554
+ process.stdin.pause();
555
+ resolve(models[selected]);
556
+ } else if (key === "") {
557
+ process.stdout.write("\x1B[?25h");
558
+ process.exit(0);
559
+ }
560
+ });
561
+ });
562
+ }
563
+ async function cmdInit() {
564
+ console.log();
565
+ console.log(" \x1B[1;36m\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\x1B[0m");
566
+ console.log(" \x1B[1;36m\u2502\x1B[0m ccm - Claude Code Model Manager \x1B[1;36m\u2502\x1B[0m");
567
+ console.log(" \x1B[1;36m\u2502\x1B[0m \x1B[90mSet up your model profile\x1B[0m \x1B[1;36m\u2502\x1B[0m");
568
+ console.log(" \x1B[1;36m\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\x1B[0m");
569
+ console.log();
570
+ if (!whichClaude()) {
571
+ warn("Claude Code is not installed.");
572
+ info("Install it first: npm install -g @anthropic-ai/claude-code\n");
573
+ const proceed = await ask("Continue anyway? [y/N]");
574
+ if (proceed.toLowerCase() !== "y") return;
575
+ }
576
+ const baseUrl = await ask("API Base URL (*)");
577
+ if (!baseUrl) {
578
+ err("Base URL is required.");
579
+ return;
580
+ }
581
+ const token = await ask("API Key (*)");
582
+ if (!token) {
583
+ err("API Key is required.");
584
+ return;
585
+ }
586
+ console.log();
587
+ info("Validating connection...");
588
+ const isAnthropic = baseUrl.toLowerCase().includes("anthropic");
589
+ let models = [];
590
+ if (!isAnthropic) {
591
+ models = await fetchModels(baseUrl, token);
592
+ if (models.length > 0) {
593
+ ok(`Found \x1B[1m${models.length}\x1B[0m models`);
594
+ } else {
595
+ info("Could not fetch model list (enter manually)");
596
+ }
597
+ }
598
+ let model;
599
+ if (isAnthropic) {
600
+ const anthropicModels = [
601
+ "claude-sonnet-4-20250514",
602
+ "claude-opus-4-20250514",
603
+ "claude-haiku-4-20250501"
604
+ ];
605
+ model = await selectModel(anthropicModels, "Select a model");
606
+ } else if (models.length > 0) {
607
+ model = await selectModel(models, "Select a model");
608
+ } else {
609
+ model = await ask("Model name (*)");
610
+ if (!model) {
611
+ err("Model name is required.");
612
+ return;
613
+ }
614
+ }
615
+ console.log();
616
+ info("Optional: model mappings for Haiku/Sonnet/Opus (Enter to skip)\n");
617
+ const haiku = await ask("Haiku model");
618
+ const sonnet = await ask("Sonnet model");
619
+ const opus = await ask("Opus model");
620
+ console.log();
621
+ const name = await ask("Profile name (*)");
622
+ if (!name) {
623
+ err("Profile name is required.");
624
+ return;
625
+ }
626
+ if (profileExists(name)) {
627
+ warn(`Profile '${name}' already exists.`);
628
+ const overwrite = await ask("Overwrite? [y/N]");
629
+ if (overwrite.toLowerCase() !== "y") {
630
+ info("Cancelled.");
631
+ return;
632
+ }
633
+ }
634
+ const profile = {
635
+ ANTHROPIC_BASE_URL: baseUrl,
636
+ ANTHROPIC_AUTH_TOKEN: token,
637
+ ANTHROPIC_MODEL: model
638
+ };
639
+ if (haiku) profile.ANTHROPIC_DEFAULT_HAIKU_MODEL = haiku;
640
+ if (sonnet) profile.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnet;
641
+ if (opus) profile.ANTHROPIC_DEFAULT_OPUS_MODEL = opus;
642
+ addProfile(name, profile);
643
+ console.log();
644
+ ok(`Profile '\x1B[1m${name}\x1B[0m' saved!
645
+ `);
646
+ console.log(` \x1B[36mccm ${name}\x1B[0m launch with this profile`);
647
+ console.log(` \x1B[36mccm edit ${name}\x1B[0m edit configuration`);
648
+ console.log(` \x1B[36mccm test ${name}\x1B[0m test connection`);
649
+ console.log(` \x1B[36mccm balance ${name}\x1B[0m check credits`);
650
+ console.log();
651
+ }
652
+
446
653
  // src/commands/runtime.ts
447
654
  function cmdPs() {
448
655
  const runs = getAllRuns();
@@ -494,12 +701,12 @@ function cmdCheck() {
494
701
  }
495
702
 
496
703
  // src/commands/diagnose.ts
497
- import https from "https";
498
- import http from "http";
499
- function httpRequest(url, token, method = "GET", timeout = 1e4, headers = {}) {
704
+ import https2 from "https";
705
+ import http2 from "http";
706
+ function httpRequest2(url, token, method = "GET", timeout = 1e4, headers = {}) {
500
707
  return new Promise((resolve) => {
501
708
  const parsed = new URL(url);
502
- const mod = parsed.protocol === "https:" ? https : http;
709
+ const mod = parsed.protocol === "https:" ? https2 : http2;
503
710
  const reqHeaders = {
504
711
  "Authorization": `Bearer ${token}`,
505
712
  "x-api-key": token,
@@ -536,7 +743,7 @@ function isAnthropicCompat(baseUrl) {
536
743
  async function testConnection(baseUrl, token) {
537
744
  const url = baseUrl.replace(/\/+$/, "");
538
745
  if (isAnthropicCompat(baseUrl)) {
539
- const result = await httpRequest(`${url}/v1/messages`, token, "POST", 1e4, {
746
+ const result = await httpRequest2(`${url}/v1/messages`, token, "POST", 1e4, {
540
747
  "anthropic-version": "2023-06-01"
541
748
  });
542
749
  if (result.status === 200 || result.status === 400) return { ok: true, endpoint: "/v1/messages" };
@@ -544,14 +751,14 @@ async function testConnection(baseUrl, token) {
544
751
  return { ok: false, error: result.error || `HTTP ${result.status}` };
545
752
  }
546
753
  for (const endpoint of ["/v1/models", "/models"]) {
547
- const result = await httpRequest(`${url}${endpoint}`, token);
754
+ const result = await httpRequest2(`${url}${endpoint}`, token);
548
755
  if (result.status === 200) return { ok: true, endpoint };
549
756
  if (result.status === 401 || result.status === 403) return { ok: false, error: "Authentication failed (invalid token)" };
550
757
  }
551
758
  try {
552
759
  const baseParts = url.split("//");
553
760
  const hostUrl = `${baseParts[0]}//${baseParts[1].split("/")[0]}/`;
554
- const result = await httpRequest(hostUrl, token);
761
+ const result = await httpRequest2(hostUrl, token);
555
762
  if (result.status > 0) return { ok: true, endpoint: "(host reachable)" };
556
763
  } catch {
557
764
  }
@@ -568,10 +775,11 @@ async function queryBalance(baseUrl, token) {
568
775
  `${url}/dashboard/billing/usage`,
569
776
  `${url}/api/user/balance`,
570
777
  `${url}/billing/usage`,
571
- `${url}/v1/dashboard/billing/usage`
778
+ `${url}/v1/dashboard/billing/usage`,
779
+ `${url}/auth/key`
572
780
  ];
573
781
  for (const ep of endpoints) {
574
- const result = await httpRequest(ep, token);
782
+ const result = await httpRequest2(ep, token);
575
783
  if (result.status === 200) return result.body;
576
784
  }
577
785
  return null;
@@ -636,6 +844,23 @@ function formatBalanceCell(data, statusWidth) {
636
844
  const bal = fmtAmount(data.total_available, currency);
637
845
  return [`\x1B[1;32m${bal}\x1B[0m`, "\x1B[32m\u25CF active\x1B[0m"];
638
846
  }
847
+ if ("data" in data && typeof data.data === "object" && data.data !== null && "usage" in data.data && "limit" in data.data) {
848
+ const d = data.data;
849
+ const used = parseFloat(d.usage);
850
+ const limit = parseFloat(d.limit);
851
+ const remaining = limit - used;
852
+ const balanceStr = `\x1B[1;32m${fmtAmount(remaining.toFixed(4))}\x1B[0m`;
853
+ let statusStr;
854
+ if (limit > 0) {
855
+ const ratio = used / limit;
856
+ const pct = ratio * 100;
857
+ const barStr = bar(ratio, 12);
858
+ statusStr = `${barStr} ${pct.toFixed(1)}% used`;
859
+ } else {
860
+ statusStr = d.is_free_tier ? "\x1B[32m\u25CF free tier\x1B[0m" : "\x1B[32m\u25CF active\x1B[0m";
861
+ }
862
+ return [balanceStr, statusStr];
863
+ }
639
864
  if ("data" in data && typeof data.data === "object" && data.data !== null) {
640
865
  const d = data.data;
641
866
  const granted = d.total_granted;
@@ -756,7 +981,7 @@ function padAnsi2(text, width) {
756
981
  }
757
982
 
758
983
  // src/completions.ts
759
- var COMMANDS = ["add", "edit", "rm", "list", "ls", "ps", "kill", "check", "test", "balance", "bal", "config", "completions"];
984
+ var COMMANDS = ["init", "add", "edit", "rm", "list", "ls", "ps", "kill", "check", "test", "balance", "bal", "config", "completions", "sessions"];
760
985
  function cmdCompletions(args) {
761
986
  const shell = args.shell || detectShell();
762
987
  if (shell === "zsh") {
@@ -1126,7 +1351,7 @@ function cleanupOldTrash() {
1126
1351
 
1127
1352
  // src/session-server.ts
1128
1353
  import { exec } from "child_process";
1129
- import http2 from "http";
1354
+ import http3 from "http";
1130
1355
  function openBrowser(url) {
1131
1356
  const cmd = process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
1132
1357
  exec(cmd);
@@ -1137,7 +1362,7 @@ function jsonResponse(res, data, status = 200) {
1137
1362
  }
1138
1363
  function startSessionServer(port = 13501) {
1139
1364
  return new Promise((resolve) => {
1140
- const server = http2.createServer((req, res) => {
1365
+ const server = http3.createServer((req, res) => {
1141
1366
  const url = new URL(req.url || "/", `http://localhost:${port}`);
1142
1367
  if (req.method === "GET" && (url.pathname === "/" || /^\/session\/[0-9a-f-]+$/.test(url.pathname))) {
1143
1368
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
@@ -1903,6 +2128,7 @@ var VERSION = pkg.version;
1903
2128
  var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
1904
2129
  "_launch",
1905
2130
  "_register",
2131
+ "init",
1906
2132
  "add",
1907
2133
  "edit",
1908
2134
  "rm",
@@ -1923,6 +2149,7 @@ function printHelp() {
1923
2149
  Usage: ccm <command> [options]
1924
2150
 
1925
2151
  Commands:
2152
+ init Interactive setup wizard
1926
2153
  add <name> Add a new model profile
1927
2154
  edit <name> Edit an existing profile
1928
2155
  rm <name> Delete a profile
@@ -2004,6 +2231,9 @@ async function main() {
2004
2231
  case "_register":
2005
2232
  cmdRegister({ name: args.name, pid: args.pid, tty: args.tty });
2006
2233
  break;
2234
+ case "init":
2235
+ await cmdInit();
2236
+ break;
2007
2237
  case "add":
2008
2238
  await cmdAdd({ name: args.name });
2009
2239
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leeandrew94/ccm",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Claude Code Model Manager - switch between AI models per terminal",
5
5
  "type": "module",
6
6
  "bin": {