@leeandrew94/ccm 0.2.1 → 0.3.1

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" });
@@ -1305,10 +1530,37 @@ function escHtml(s) {
1305
1530
 
1306
1531
  function renderMarkdown(text) {
1307
1532
  if (!text) return '';
1308
- var h = escHtml(text);
1309
1533
  var BT = String.fromCharCode(96);
1310
- h = h.replace(new RegExp(BT + BT + BT + '(\\\\w*)\\\\n([\\\\s\\\\S]*?)' + BT + BT + BT, 'g'), function(m, lang, code) { return '<pre><code>' + highlightCode(code) + '</code></pre>'; });
1311
- h = h.replace(new RegExp(BT + '([^\\\\n]+?)' + BT, 'g'), '<code>$1</code>');
1534
+ var h = text;
1535
+
1536
+ // Placeholder delimiters: Unicode PUA (Private Use Area) codepoints.
1537
+ // Control chars (\\x00, \\x01) would be replaced with \uFFFD (\uFFFD) by the
1538
+ // browser's HTML parser, so we use printable PUA chars that survive intact.
1539
+ var PS = '\uE000'; // placeholder start
1540
+ var PE = '\uE001'; // placeholder end
1541
+
1542
+ // Step 1: Extract fenced code blocks from RAW text (before any HTML escaping)
1543
+ // Apply syntax highlighting to raw code so highlightCode regexes work correctly
1544
+ var codeBlocks = [];
1545
+ h = h.replace(new RegExp(BT + BT + BT + '(\\\\w*)\\\\n([\\\\s\\\\S]*?)' + BT + BT + BT, 'g'), function(m, lang, code) {
1546
+ var idx = codeBlocks.length;
1547
+ codeBlocks.push('<pre><code>' + highlightCode(code) + '</code></pre>');
1548
+ return PS + 'CCM_CB' + idx + PE;
1549
+ });
1550
+
1551
+ // Step 2: Extract inline code from RAW text
1552
+ // escHtml the code content so it displays literally
1553
+ var inlineCodes = [];
1554
+ h = h.replace(new RegExp(BT + '([^\\\\n]+?)' + BT, 'g'), function(m, code) {
1555
+ var idx = inlineCodes.length;
1556
+ inlineCodes.push('<code>' + escHtml(code) + '</code>');
1557
+ return PS + 'CCM_IC' + idx + PE;
1558
+ });
1559
+
1560
+ // Step 3: Now HTML-escape the remaining text (blockquotes, etc.)
1561
+ h = escHtml(h);
1562
+
1563
+ // Step 4: Apply markdown formatting
1312
1564
  h = h.replace(/^### (.+)$/gm, '<h3>$1</h3>');
1313
1565
  h = h.replace(/^## (.+)$/gm, '<h2>$1</h2>');
1314
1566
  h = h.replace(/^# (.+)$/gm, '<h1>$1</h1>');
@@ -1340,16 +1592,56 @@ function renderMarkdown(text) {
1340
1592
  h = h.replace(/(<\\/blockquote>)<\\/p>/g, '$1');
1341
1593
  h = h.replace(/<p>(<hr>)/g, '$1');
1342
1594
  h = h.replace(/(<hr>)<\\/p>/g, '$1');
1595
+
1596
+ // Step 5: Restore code blocks and inline code
1597
+ h = h.replace(new RegExp(PS + 'CCM_CB(\\d+)' + PE, 'g'), function(m, idx) { return codeBlocks[parseInt(idx)]; });
1598
+ h = h.replace(new RegExp(PS + 'CCM_IC(\\d+)' + PE, 'g'), function(m, idx) { return inlineCodes[parseInt(idx)]; });
1599
+
1343
1600
  return h;
1344
1601
  }
1345
1602
 
1346
1603
  function highlightCode(code) {
1347
1604
  var h = code;
1348
- h = h.replace(/(\\/\\/.*$)/gm, '<span class="cm">$1</span>');
1349
- h = h.replace(/(#.*$)/gm, '<span class="cm">$1</span>');
1350
- h = h.replace(/("(?:[^"\\\\\\\\]|\\\\\\\\.)*"|'(?:[^'\\\\\\\\]|\\\\\\\\.)*')/g, '<span class="str">$1</span>');
1605
+ var p = [];
1606
+
1607
+ // Same PUA delimiters used in renderMarkdown
1608
+ var PS = '\uE000';
1609
+ var PE = '\uE001';
1610
+
1611
+ // Protect strings with placeholders (before other regexes, to avoid
1612
+ // keywords / comments inside strings being highlighted)
1613
+ h = h.replace(/("[^"]*"|'[^']*')/g, function(m) {
1614
+ var i = p.length;
1615
+ p.push('<span class="str">' + escHtml(m) + '</span>');
1616
+ return PS + 'HL' + i + PE;
1617
+ });
1618
+
1619
+ // Protect // comments
1620
+ h = h.replace(/(\\/\\/.*$)/gm, function(m) {
1621
+ var i = p.length;
1622
+ p.push('<span class="cm">' + escHtml(m) + '</span>');
1623
+ return PS + 'HL' + i + PE;
1624
+ });
1625
+
1626
+ // Protect # comments
1627
+ h = h.replace(/(#.*$)/gm, function(m) {
1628
+ var i = p.length;
1629
+ p.push('<span class="cm">' + escHtml(m) + '</span>');
1630
+ return PS + 'HL' + i + PE;
1631
+ });
1632
+
1633
+ // HTML-escape remaining literal code characters
1634
+ h = escHtml(h);
1635
+
1636
+ // Keywords (safe: placeholders contain no keywords)
1351
1637
  h = h.replace(/\\b(import|export|from|const|let|var|function|return|if|else|for|while|class|extends|new|async|await|try|catch|throw|switch|case|break|default|typeof|instanceof|void|null|undefined|true|false|def|self|print|raise|with|as|in|not|and|or|is|lambda|yield|assert|del|global|nonlocal|pass|elif|except|finally)\\b/g, '<span class="kw">$1</span>');
1638
+
1639
+ // Numbers
1352
1640
  h = h.replace(/\\b(\\d+\\.?\\d*)\\b/g, '<span class="num">$1</span>');
1641
+
1642
+ // Restore protected fragments
1643
+ h = h.replace(new RegExp(PS + 'HL(\\d+)' + PE, 'g'), function(m, i) { return p[parseInt(i)]; });
1644
+
1353
1645
  return h;
1354
1646
  }
1355
1647
 
@@ -1469,6 +1761,7 @@ body {
1469
1761
  .main-header h1 {
1470
1762
  font-size: 16px; font-weight: 600; margin-bottom: 10px;
1471
1763
  line-height: 1.5; color: var(--td-text-primary);
1764
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1472
1765
  }
1473
1766
  .main-header .meta {
1474
1767
  display: flex; gap: 18px; font-size: 12px;
@@ -1903,6 +2196,7 @@ var VERSION = pkg.version;
1903
2196
  var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
1904
2197
  "_launch",
1905
2198
  "_register",
2199
+ "init",
1906
2200
  "add",
1907
2201
  "edit",
1908
2202
  "rm",
@@ -1923,6 +2217,7 @@ function printHelp() {
1923
2217
  Usage: ccm <command> [options]
1924
2218
 
1925
2219
  Commands:
2220
+ init Interactive setup wizard
1926
2221
  add <name> Add a new model profile
1927
2222
  edit <name> Edit an existing profile
1928
2223
  rm <name> Delete a profile
@@ -2004,6 +2299,9 @@ async function main() {
2004
2299
  case "_register":
2005
2300
  cmdRegister({ name: args.name, pid: args.pid, tty: args.tty });
2006
2301
  break;
2302
+ case "init":
2303
+ await cmdInit();
2304
+ break;
2007
2305
  case "add":
2008
2306
  await cmdAdd({ name: args.name });
2009
2307
  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.1",
4
4
  "description": "Claude Code Model Manager - switch between AI models per terminal",
5
5
  "type": "module",
6
6
  "bin": {