@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 +4 -3
- package/README.zh.md +4 -3
- package/bin/ccm.js +316 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,14 +35,14 @@ rm -rf ~/.ccm
|
|
|
35
35
|
## Quick Start
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
#
|
|
39
|
-
ccm
|
|
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
|
|
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
|
-
#
|
|
39
|
-
ccm
|
|
38
|
+
# 交互式初始化(推荐)
|
|
39
|
+
ccm init
|
|
40
40
|
|
|
41
41
|
# 启动
|
|
42
42
|
ccm mimo
|
|
43
43
|
|
|
44
44
|
# 新开终端,换个模型
|
|
45
|
-
ccm
|
|
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
|
|
498
|
-
import
|
|
499
|
-
function
|
|
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:" ?
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
1311
|
-
|
|
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
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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;
|