@leeandrew94/ccm 0.3.0 → 0.4.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.
Files changed (2) hide show
  1. package/bin/ccm.js +208 -116
  2. package/package.json +1 -1
package/bin/ccm.js CHANGED
@@ -446,6 +446,8 @@ function cmdList() {
446
446
  // src/commands/init.ts
447
447
  import https from "https";
448
448
  import http from "http";
449
+ import fs4 from "fs";
450
+ import path4 from "path";
449
451
  function httpRequest(url, token, timeout = 8e3) {
450
452
  return new Promise((resolve) => {
451
453
  const parsed = new URL(url);
@@ -475,6 +477,33 @@ function httpRequest(url, token, timeout = 8e3) {
475
477
  req.end();
476
478
  });
477
479
  }
480
+ var MODEL_CACHE_FILE = path4.join(CONFIG_DIR, "model-cache.json");
481
+ function loadModelCache() {
482
+ try {
483
+ if (fs4.existsSync(MODEL_CACHE_FILE)) {
484
+ return JSON.parse(fs4.readFileSync(MODEL_CACHE_FILE, "utf-8"));
485
+ }
486
+ } catch {
487
+ }
488
+ return {};
489
+ }
490
+ function saveModelCache(cache) {
491
+ fs4.mkdirSync(CONFIG_DIR, { recursive: true });
492
+ fs4.writeFileSync(MODEL_CACHE_FILE, JSON.stringify(cache, null, 2) + "\n", "utf-8");
493
+ }
494
+ function getCachedModels(endpoint) {
495
+ return loadModelCache()[endpoint]?.models;
496
+ }
497
+ function setCachedModels(endpoint, models) {
498
+ const cache = loadModelCache();
499
+ cache[endpoint] = { models, updatedAt: Date.now() };
500
+ saveModelCache(cache);
501
+ }
502
+ var FALLBACK_MODELS = [
503
+ "claude-sonnet-4-20250514",
504
+ "claude-opus-4-20250514",
505
+ "claude-haiku-4-20250501"
506
+ ];
478
507
  async function fetchModels(baseUrl, token) {
479
508
  const url = baseUrl.replace(/\/+$/, "");
480
509
  for (const suffix of ["/anthropic", "/v1/chat/completions"]) {
@@ -496,6 +525,7 @@ async function selectModel(models, label) {
496
525
  if (models.length === 0) {
497
526
  return await ask("Model name");
498
527
  }
528
+ const options = [...models, "Custom model..."];
499
529
  console.log(`
500
530
  \x1B[1m${label}\x1B[0m (\u2191\u2193 navigate, Enter to select):
501
531
  `);
@@ -505,20 +535,20 @@ async function selectModel(models, label) {
505
535
  function render() {
506
536
  process.stdout.write("\x1B[?25l");
507
537
  const start = offset;
508
- const end = Math.min(offset + PAGE, models.length);
538
+ const end = Math.min(offset + PAGE, options.length);
509
539
  for (let i = start; i < end; i++) {
510
540
  process.stdout.write("\x1B[2K");
511
541
  if (i === selected) {
512
- console.log(` \x1B[36m\u276F\x1B[0m \x1B[1m${models[i]}\x1B[0m`);
542
+ console.log(` \x1B[36m\u276F\x1B[0m \x1B[1m${options[i]}\x1B[0m`);
513
543
  } else {
514
- console.log(` \x1B[90m${models[i]}\x1B[0m`);
544
+ console.log(` \x1B[90m${options[i]}\x1B[0m`);
515
545
  }
516
546
  }
517
547
  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`);
548
+ if (options.length > PAGE) {
549
+ console.log(` \x1B[90m${selected + 1}/${options.length} \u2014 \u2191\u2193 scroll, Enter select\x1B[0m`);
520
550
  }
521
- const lines = end - start + (models.length > PAGE ? 1 : 0);
551
+ const lines = end - start + (options.length > PAGE ? 1 : 0);
522
552
  process.stdout.write(`\x1B[${lines}A`);
523
553
  }
524
554
  return new Promise((resolve) => {
@@ -526,18 +556,17 @@ async function selectModel(models, label) {
526
556
  process.stdin.resume();
527
557
  process.stdin.setEncoding("utf-8");
528
558
  render();
529
- let buf = "";
530
559
  process.stdin.on("data", function onData(key) {
531
560
  if (key === "\x1B[A") {
532
561
  selected = Math.max(0, selected - 1);
533
562
  if (selected < offset) offset = selected;
534
563
  render();
535
564
  } else if (key === "\x1B[B") {
536
- selected = Math.min(models.length - 1, selected + 1);
565
+ selected = Math.min(options.length - 1, selected + 1);
537
566
  if (selected >= offset + PAGE) offset = selected - PAGE + 1;
538
567
  render();
539
568
  } else if (key === "\r" || key === "\n") {
540
- const lines = Math.min(PAGE, models.length - offset) + (models.length > PAGE ? 1 : 0);
569
+ const lines = Math.min(PAGE, options.length - offset) + (options.length > PAGE ? 1 : 0);
541
570
  for (let i = 0; i < lines; i++) {
542
571
  process.stdout.write("\x1B[2K\n");
543
572
  }
@@ -546,13 +575,18 @@ async function selectModel(models, label) {
546
575
  process.stdout.write("\x1B[2K\n");
547
576
  }
548
577
  process.stdout.write(`\x1B[${lines}A`);
549
- process.stdout.write(` \x1B[36m\u276F\x1B[0m \x1B[1m${models[selected]}\x1B[0m
578
+ process.stdout.write(` \x1B[36m\u276F\x1B[0m \x1B[1m${options[selected]}\x1B[0m
550
579
  `);
551
580
  process.stdout.write("\x1B[?25h");
552
581
  process.stdin.setRawMode(false);
553
582
  process.stdin.removeListener("data", onData);
554
583
  process.stdin.pause();
555
- resolve(models[selected]);
584
+ const chosen = options[selected];
585
+ if (chosen === "Custom model...") {
586
+ ask("Model name").then(resolve);
587
+ } else {
588
+ resolve(chosen);
589
+ }
556
590
  } else if (key === "") {
557
591
  process.stdout.write("\x1B[?25h");
558
592
  process.exit(0);
@@ -585,33 +619,23 @@ async function cmdInit() {
585
619
  }
586
620
  console.log();
587
621
  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");
622
+ const endpoint = baseUrl.replace(/\/+$/, "");
623
+ let models = await fetchModels(baseUrl, token);
624
+ if (models.length > 0) {
625
+ ok(`Found \x1B[1m${models.length}\x1B[0m models`);
626
+ setCachedModels(endpoint, models);
608
627
  } else {
609
- model = await ask("Model name (*)");
610
- if (!model) {
611
- err("Model name is required.");
612
- return;
628
+ info("Could not fetch model list");
629
+ const cached = getCachedModels(endpoint);
630
+ if (cached && cached.length > 0) {
631
+ info(`Loaded \x1B[1m${cached.length}\x1B[0m models from cache`);
632
+ models = cached;
633
+ } else {
634
+ info("Using built-in model list");
635
+ models = FALLBACK_MODELS;
613
636
  }
614
637
  }
638
+ const model = await selectModel(models, "Select a model");
615
639
  console.log();
616
640
  info("Optional: model mappings for Haiku/Sonnet/Opus (Enter to skip)\n");
617
641
  const haiku = await ask("Haiku model");
@@ -1032,17 +1056,17 @@ var BASH_COMPLETION = `_ccm_completions() {
1032
1056
  complete -F _ccm_completions ccm`;
1033
1057
 
1034
1058
  // src/session-data.ts
1035
- import fs4 from "fs";
1036
- import path4 from "path";
1059
+ import fs5 from "fs";
1060
+ import path5 from "path";
1037
1061
  import os2 from "os";
1038
- var CLAUDE_DIR = path4.join(os2.homedir(), ".claude");
1039
- var HISTORY_FILE = path4.join(CLAUDE_DIR, "history.jsonl");
1040
- var PROJECTS_DIR = path4.join(CLAUDE_DIR, "projects");
1041
- var TRASH_DIR = path4.join(os2.homedir(), ".ccm", "session-trash");
1062
+ var CLAUDE_DIR = path5.join(os2.homedir(), ".claude");
1063
+ var HISTORY_FILE = path5.join(CLAUDE_DIR, "history.jsonl");
1064
+ var PROJECTS_DIR = path5.join(CLAUDE_DIR, "projects");
1065
+ var TRASH_DIR = path5.join(os2.homedir(), ".ccm", "session-trash");
1042
1066
  var TRASH_EXPIRE_DAYS = 30;
1043
1067
  function readJsonl(filePath) {
1044
- if (!fs4.existsSync(filePath)) return [];
1045
- const lines = fs4.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
1068
+ if (!fs5.existsSync(filePath)) return [];
1069
+ const lines = fs5.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
1046
1070
  const results = [];
1047
1071
  for (const line of lines) {
1048
1072
  try {
@@ -1054,8 +1078,8 @@ function readJsonl(filePath) {
1054
1078
  }
1055
1079
  function atomicWriteJsonl(filePath, lines) {
1056
1080
  const tmp = filePath + ".tmp";
1057
- fs4.writeFileSync(tmp, lines.join("\n"), "utf-8");
1058
- fs4.renameSync(tmp, filePath);
1081
+ fs5.writeFileSync(tmp, lines.join("\n"), "utf-8");
1082
+ fs5.renameSync(tmp, filePath);
1059
1083
  }
1060
1084
  function getHistoryEntries() {
1061
1085
  const entries = readJsonl(HISTORY_FILE);
@@ -1093,11 +1117,11 @@ function getSessionsList() {
1093
1117
  return sessions;
1094
1118
  }
1095
1119
  function findSessionJsonl(sessionId) {
1096
- if (!fs4.existsSync(PROJECTS_DIR)) return null;
1097
- const projectDirs = fs4.readdirSync(PROJECTS_DIR);
1120
+ if (!fs5.existsSync(PROJECTS_DIR)) return null;
1121
+ const projectDirs = fs5.readdirSync(PROJECTS_DIR);
1098
1122
  for (const dir of projectDirs) {
1099
- const filePath = path4.join(PROJECTS_DIR, dir, `${sessionId}.jsonl`);
1100
- if (fs4.existsSync(filePath)) return filePath;
1123
+ const filePath = path5.join(PROJECTS_DIR, dir, `${sessionId}.jsonl`);
1124
+ if (fs5.existsSync(filePath)) return filePath;
1101
1125
  }
1102
1126
  return null;
1103
1127
  }
@@ -1166,11 +1190,11 @@ function getSessionTitle(sessionId) {
1166
1190
  return null;
1167
1191
  }
1168
1192
  function ensureTrashDir() {
1169
- fs4.mkdirSync(TRASH_DIR, { recursive: true });
1193
+ fs5.mkdirSync(TRASH_DIR, { recursive: true });
1170
1194
  }
1171
1195
  function extractHistoryForSession(sessionId) {
1172
- if (!fs4.existsSync(HISTORY_FILE)) return [];
1173
- const lines = fs4.readFileSync(HISTORY_FILE, "utf-8").split("\n").filter(Boolean);
1196
+ if (!fs5.existsSync(HISTORY_FILE)) return [];
1197
+ const lines = fs5.readFileSync(HISTORY_FILE, "utf-8").split("\n").filter(Boolean);
1174
1198
  const matched = [];
1175
1199
  for (const line of lines) {
1176
1200
  try {
@@ -1182,8 +1206,8 @@ function extractHistoryForSession(sessionId) {
1182
1206
  return matched;
1183
1207
  }
1184
1208
  function filterHistoryBySession(sessionId) {
1185
- if (!fs4.existsSync(HISTORY_FILE)) return;
1186
- const lines = fs4.readFileSync(HISTORY_FILE, "utf-8").split("\n");
1209
+ if (!fs5.existsSync(HISTORY_FILE)) return;
1210
+ const lines = fs5.readFileSync(HISTORY_FILE, "utf-8").split("\n");
1187
1211
  const filtered = lines.filter((line) => {
1188
1212
  if (!line.trim()) return false;
1189
1213
  try {
@@ -1201,15 +1225,15 @@ function deleteSession(sessionId) {
1201
1225
  const filePath = findSessionJsonl(sessionId);
1202
1226
  let originalProject = "";
1203
1227
  if (filePath) {
1204
- originalProject = path4.basename(path4.dirname(filePath));
1205
- const destJsonl = path4.join(TRASH_DIR, `${sessionId}.jsonl`);
1206
- fs4.copyFileSync(filePath, destJsonl);
1207
- fs4.unlinkSync(filePath);
1208
- const sessionDir = path4.join(path4.dirname(filePath), sessionId);
1209
- if (fs4.existsSync(sessionDir)) {
1210
- const destDir = path4.join(TRASH_DIR, sessionId);
1211
- fs4.cpSync(sessionDir, destDir, { recursive: true });
1212
- fs4.rmSync(sessionDir, { recursive: true, force: true });
1228
+ originalProject = path5.basename(path5.dirname(filePath));
1229
+ const destJsonl = path5.join(TRASH_DIR, `${sessionId}.jsonl`);
1230
+ fs5.copyFileSync(filePath, destJsonl);
1231
+ fs5.unlinkSync(filePath);
1232
+ const sessionDir = path5.join(path5.dirname(filePath), sessionId);
1233
+ if (fs5.existsSync(sessionDir)) {
1234
+ const destDir = path5.join(TRASH_DIR, sessionId);
1235
+ fs5.cpSync(sessionDir, destDir, { recursive: true });
1236
+ fs5.rmSync(sessionDir, { recursive: true, force: true });
1213
1237
  }
1214
1238
  }
1215
1239
  const meta = {
@@ -1218,8 +1242,8 @@ function deleteSession(sessionId) {
1218
1242
  originalProject,
1219
1243
  historyEntries
1220
1244
  };
1221
- fs4.writeFileSync(
1222
- path4.join(TRASH_DIR, `${sessionId}.meta.json`),
1245
+ fs5.writeFileSync(
1246
+ path5.join(TRASH_DIR, `${sessionId}.meta.json`),
1223
1247
  JSON.stringify(meta, null, 2),
1224
1248
  "utf-8"
1225
1249
  );
@@ -1232,14 +1256,14 @@ function deleteAllSessions() {
1232
1256
  const historyEntries = extractHistoryForSession(session.sessionId);
1233
1257
  const filePath = findSessionJsonl(session.sessionId);
1234
1258
  if (filePath) {
1235
- const destJsonl = path4.join(TRASH_DIR, `${session.sessionId}.jsonl`);
1236
- fs4.copyFileSync(filePath, destJsonl);
1237
- fs4.unlinkSync(filePath);
1238
- const sessionDir = path4.join(path4.dirname(filePath), session.sessionId);
1239
- if (fs4.existsSync(sessionDir)) {
1240
- const destDir = path4.join(TRASH_DIR, session.sessionId);
1241
- fs4.cpSync(sessionDir, destDir, { recursive: true });
1242
- fs4.rmSync(sessionDir, { recursive: true, force: true });
1259
+ const destJsonl = path5.join(TRASH_DIR, `${session.sessionId}.jsonl`);
1260
+ fs5.copyFileSync(filePath, destJsonl);
1261
+ fs5.unlinkSync(filePath);
1262
+ const sessionDir = path5.join(path5.dirname(filePath), session.sessionId);
1263
+ if (fs5.existsSync(sessionDir)) {
1264
+ const destDir = path5.join(TRASH_DIR, session.sessionId);
1265
+ fs5.cpSync(sessionDir, destDir, { recursive: true });
1266
+ fs5.rmSync(sessionDir, { recursive: true, force: true });
1243
1267
  }
1244
1268
  }
1245
1269
  const meta = {
@@ -1248,68 +1272,68 @@ function deleteAllSessions() {
1248
1272
  originalProject: session.projectName,
1249
1273
  historyEntries
1250
1274
  };
1251
- fs4.writeFileSync(
1252
- path4.join(TRASH_DIR, `${session.sessionId}.meta.json`),
1275
+ fs5.writeFileSync(
1276
+ path5.join(TRASH_DIR, `${session.sessionId}.meta.json`),
1253
1277
  JSON.stringify(meta, null, 2),
1254
1278
  "utf-8"
1255
1279
  );
1256
1280
  }
1257
- if (fs4.existsSync(HISTORY_FILE)) {
1281
+ if (fs5.existsSync(HISTORY_FILE)) {
1258
1282
  atomicWriteJsonl(HISTORY_FILE, []);
1259
1283
  }
1260
1284
  }
1261
1285
  function restoreSession(sessionId) {
1262
- const metaPath = path4.join(TRASH_DIR, `${sessionId}.meta.json`);
1263
- if (!fs4.existsSync(metaPath)) return false;
1264
- const meta = JSON.parse(fs4.readFileSync(metaPath, "utf-8"));
1286
+ const metaPath = path5.join(TRASH_DIR, `${sessionId}.meta.json`);
1287
+ if (!fs5.existsSync(metaPath)) return false;
1288
+ const meta = JSON.parse(fs5.readFileSync(metaPath, "utf-8"));
1265
1289
  const destDir = findProjectDirForRestore(meta.originalProject);
1266
- const trashJsonl = path4.join(TRASH_DIR, `${sessionId}.jsonl`);
1267
- if (fs4.existsSync(trashJsonl) && destDir) {
1268
- fs4.copyFileSync(trashJsonl, path4.join(destDir, `${sessionId}.jsonl`));
1269
- fs4.unlinkSync(trashJsonl);
1290
+ const trashJsonl = path5.join(TRASH_DIR, `${sessionId}.jsonl`);
1291
+ if (fs5.existsSync(trashJsonl) && destDir) {
1292
+ fs5.copyFileSync(trashJsonl, path5.join(destDir, `${sessionId}.jsonl`));
1293
+ fs5.unlinkSync(trashJsonl);
1270
1294
  }
1271
- const trashSessionDir = path4.join(TRASH_DIR, sessionId);
1272
- if (fs4.existsSync(trashSessionDir) && destDir) {
1273
- const restoredDir = path4.join(destDir, sessionId);
1274
- fs4.cpSync(trashSessionDir, restoredDir, { recursive: true });
1275
- fs4.rmSync(trashSessionDir, { recursive: true, force: true });
1295
+ const trashSessionDir = path5.join(TRASH_DIR, sessionId);
1296
+ if (fs5.existsSync(trashSessionDir) && destDir) {
1297
+ const restoredDir = path5.join(destDir, sessionId);
1298
+ fs5.cpSync(trashSessionDir, restoredDir, { recursive: true });
1299
+ fs5.rmSync(trashSessionDir, { recursive: true, force: true });
1276
1300
  }
1277
1301
  if (meta.historyEntries && meta.historyEntries.length > 0) {
1278
1302
  const newLines = meta.historyEntries.map((e) => JSON.stringify(e));
1279
- if (fs4.existsSync(HISTORY_FILE)) {
1280
- const existing = fs4.readFileSync(HISTORY_FILE, "utf-8");
1303
+ if (fs5.existsSync(HISTORY_FILE)) {
1304
+ const existing = fs5.readFileSync(HISTORY_FILE, "utf-8");
1281
1305
  const combined = existing.trimEnd() + "\n" + newLines.join("\n") + "\n";
1282
1306
  atomicWriteJsonl(HISTORY_FILE, combined.split("\n"));
1283
1307
  } else {
1284
1308
  atomicWriteJsonl(HISTORY_FILE, newLines);
1285
1309
  }
1286
1310
  }
1287
- fs4.unlinkSync(metaPath);
1311
+ fs5.unlinkSync(metaPath);
1288
1312
  return true;
1289
1313
  }
1290
1314
  function findProjectDirForRestore(projectName) {
1291
- if (!fs4.existsSync(PROJECTS_DIR)) return null;
1292
- const dirs = fs4.readdirSync(PROJECTS_DIR);
1315
+ if (!fs5.existsSync(PROJECTS_DIR)) return null;
1316
+ const dirs = fs5.readdirSync(PROJECTS_DIR);
1293
1317
  for (const dir of dirs) {
1294
1318
  if (dir.endsWith(projectName) || dir.includes(projectName)) {
1295
- const fullPath = path4.join(PROJECTS_DIR, dir);
1296
- if (fs4.statSync(fullPath).isDirectory()) return fullPath;
1319
+ const fullPath = path5.join(PROJECTS_DIR, dir);
1320
+ if (fs5.statSync(fullPath).isDirectory()) return fullPath;
1297
1321
  }
1298
1322
  }
1299
1323
  if (dirs.length > 0) {
1300
- const first = path4.join(PROJECTS_DIR, dirs[0]);
1301
- if (fs4.statSync(first).isDirectory()) return first;
1324
+ const first = path5.join(PROJECTS_DIR, dirs[0]);
1325
+ if (fs5.statSync(first).isDirectory()) return first;
1302
1326
  }
1303
1327
  return null;
1304
1328
  }
1305
1329
  function getTrashSessions() {
1306
1330
  ensureTrashDir();
1307
- const files = fs4.readdirSync(TRASH_DIR);
1331
+ const files = fs5.readdirSync(TRASH_DIR);
1308
1332
  const metas = [];
1309
1333
  for (const file of files) {
1310
1334
  if (file.endsWith(".meta.json")) {
1311
1335
  try {
1312
- const meta = JSON.parse(fs4.readFileSync(path4.join(TRASH_DIR, file), "utf-8"));
1336
+ const meta = JSON.parse(fs5.readFileSync(path5.join(TRASH_DIR, file), "utf-8"));
1313
1337
  metas.push(meta);
1314
1338
  } catch {
1315
1339
  }
@@ -1320,13 +1344,13 @@ function getTrashSessions() {
1320
1344
  }
1321
1345
  function purgeTrash() {
1322
1346
  ensureTrashDir();
1323
- const files = fs4.readdirSync(TRASH_DIR);
1347
+ const files = fs5.readdirSync(TRASH_DIR);
1324
1348
  for (const file of files) {
1325
- const fullPath = path4.join(TRASH_DIR, file);
1326
- if (fs4.statSync(fullPath).isDirectory()) {
1327
- fs4.rmSync(fullPath, { recursive: true, force: true });
1349
+ const fullPath = path5.join(TRASH_DIR, file);
1350
+ if (fs5.statSync(fullPath).isDirectory()) {
1351
+ fs5.rmSync(fullPath, { recursive: true, force: true });
1328
1352
  } else {
1329
- fs4.unlinkSync(fullPath);
1353
+ fs5.unlinkSync(fullPath);
1330
1354
  }
1331
1355
  }
1332
1356
  }
@@ -1337,12 +1361,12 @@ function cleanupOldTrash() {
1337
1361
  let cleaned = 0;
1338
1362
  for (const meta of metas) {
1339
1363
  if (meta.deletedAt < cutoff) {
1340
- const jsonlPath = path4.join(TRASH_DIR, `${meta.sessionId}.jsonl`);
1341
- if (fs4.existsSync(jsonlPath)) fs4.unlinkSync(jsonlPath);
1342
- const sessionDir = path4.join(TRASH_DIR, meta.sessionId);
1343
- if (fs4.existsSync(sessionDir)) fs4.rmSync(sessionDir, { recursive: true, force: true });
1344
- const metaPath = path4.join(TRASH_DIR, `${meta.sessionId}.meta.json`);
1345
- if (fs4.existsSync(metaPath)) fs4.unlinkSync(metaPath);
1364
+ const jsonlPath = path5.join(TRASH_DIR, `${meta.sessionId}.jsonl`);
1365
+ if (fs5.existsSync(jsonlPath)) fs5.unlinkSync(jsonlPath);
1366
+ const sessionDir = path5.join(TRASH_DIR, meta.sessionId);
1367
+ if (fs5.existsSync(sessionDir)) fs5.rmSync(sessionDir, { recursive: true, force: true });
1368
+ const metaPath = path5.join(TRASH_DIR, `${meta.sessionId}.meta.json`);
1369
+ if (fs5.existsSync(metaPath)) fs5.unlinkSync(metaPath);
1346
1370
  cleaned++;
1347
1371
  }
1348
1372
  }
@@ -1530,10 +1554,37 @@ function escHtml(s) {
1530
1554
 
1531
1555
  function renderMarkdown(text) {
1532
1556
  if (!text) return '';
1533
- var h = escHtml(text);
1534
1557
  var BT = String.fromCharCode(96);
1535
- 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>'; });
1536
- h = h.replace(new RegExp(BT + '([^\\\\n]+?)' + BT, 'g'), '<code>$1</code>');
1558
+ var h = text;
1559
+
1560
+ // Placeholder delimiters: Unicode PUA (Private Use Area) codepoints.
1561
+ // Control chars (\\x00, \\x01) would be replaced with \uFFFD (\uFFFD) by the
1562
+ // browser's HTML parser, so we use printable PUA chars that survive intact.
1563
+ var PS = '\uE000'; // placeholder start
1564
+ var PE = '\uE001'; // placeholder end
1565
+
1566
+ // Step 1: Extract fenced code blocks from RAW text (before any HTML escaping)
1567
+ // Apply syntax highlighting to raw code so highlightCode regexes work correctly
1568
+ var codeBlocks = [];
1569
+ h = h.replace(new RegExp(BT + BT + BT + '(\\\\w*)\\\\n([\\\\s\\\\S]*?)' + BT + BT + BT, 'g'), function(m, lang, code) {
1570
+ var idx = codeBlocks.length;
1571
+ codeBlocks.push('<pre><code>' + highlightCode(code) + '</code></pre>');
1572
+ return PS + 'CCM_CB' + idx + PE;
1573
+ });
1574
+
1575
+ // Step 2: Extract inline code from RAW text
1576
+ // escHtml the code content so it displays literally
1577
+ var inlineCodes = [];
1578
+ h = h.replace(new RegExp(BT + '([^\\\\n]+?)' + BT, 'g'), function(m, code) {
1579
+ var idx = inlineCodes.length;
1580
+ inlineCodes.push('<code>' + escHtml(code) + '</code>');
1581
+ return PS + 'CCM_IC' + idx + PE;
1582
+ });
1583
+
1584
+ // Step 3: Now HTML-escape the remaining text (blockquotes, etc.)
1585
+ h = escHtml(h);
1586
+
1587
+ // Step 4: Apply markdown formatting
1537
1588
  h = h.replace(/^### (.+)$/gm, '<h3>$1</h3>');
1538
1589
  h = h.replace(/^## (.+)$/gm, '<h2>$1</h2>');
1539
1590
  h = h.replace(/^# (.+)$/gm, '<h1>$1</h1>');
@@ -1565,16 +1616,56 @@ function renderMarkdown(text) {
1565
1616
  h = h.replace(/(<\\/blockquote>)<\\/p>/g, '$1');
1566
1617
  h = h.replace(/<p>(<hr>)/g, '$1');
1567
1618
  h = h.replace(/(<hr>)<\\/p>/g, '$1');
1619
+
1620
+ // Step 5: Restore code blocks and inline code
1621
+ h = h.replace(new RegExp(PS + 'CCM_CB(\\d+)' + PE, 'g'), function(m, idx) { return codeBlocks[parseInt(idx)]; });
1622
+ h = h.replace(new RegExp(PS + 'CCM_IC(\\d+)' + PE, 'g'), function(m, idx) { return inlineCodes[parseInt(idx)]; });
1623
+
1568
1624
  return h;
1569
1625
  }
1570
1626
 
1571
1627
  function highlightCode(code) {
1572
1628
  var h = code;
1573
- h = h.replace(/(\\/\\/.*$)/gm, '<span class="cm">$1</span>');
1574
- h = h.replace(/(#.*$)/gm, '<span class="cm">$1</span>');
1575
- h = h.replace(/("(?:[^"\\\\\\\\]|\\\\\\\\.)*"|'(?:[^'\\\\\\\\]|\\\\\\\\.)*')/g, '<span class="str">$1</span>');
1629
+ var p = [];
1630
+
1631
+ // Same PUA delimiters used in renderMarkdown
1632
+ var PS = '\uE000';
1633
+ var PE = '\uE001';
1634
+
1635
+ // Protect strings with placeholders (before other regexes, to avoid
1636
+ // keywords / comments inside strings being highlighted)
1637
+ h = h.replace(/("[^"]*"|'[^']*')/g, function(m) {
1638
+ var i = p.length;
1639
+ p.push('<span class="str">' + escHtml(m) + '</span>');
1640
+ return PS + 'HL' + i + PE;
1641
+ });
1642
+
1643
+ // Protect // comments
1644
+ h = h.replace(/(\\/\\/.*$)/gm, function(m) {
1645
+ var i = p.length;
1646
+ p.push('<span class="cm">' + escHtml(m) + '</span>');
1647
+ return PS + 'HL' + i + PE;
1648
+ });
1649
+
1650
+ // Protect # comments
1651
+ h = h.replace(/(#.*$)/gm, function(m) {
1652
+ var i = p.length;
1653
+ p.push('<span class="cm">' + escHtml(m) + '</span>');
1654
+ return PS + 'HL' + i + PE;
1655
+ });
1656
+
1657
+ // HTML-escape remaining literal code characters
1658
+ h = escHtml(h);
1659
+
1660
+ // Keywords (safe: placeholders contain no keywords)
1576
1661
  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>');
1662
+
1663
+ // Numbers
1577
1664
  h = h.replace(/\\b(\\d+\\.?\\d*)\\b/g, '<span class="num">$1</span>');
1665
+
1666
+ // Restore protected fragments
1667
+ h = h.replace(new RegExp(PS + 'HL(\\d+)' + PE, 'g'), function(m, i) { return p[parseInt(i)]; });
1668
+
1578
1669
  return h;
1579
1670
  }
1580
1671
 
@@ -1694,6 +1785,7 @@ body {
1694
1785
  .main-header h1 {
1695
1786
  font-size: 16px; font-weight: 600; margin-bottom: 10px;
1696
1787
  line-height: 1.5; color: var(--td-text-primary);
1788
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1697
1789
  }
1698
1790
  .main-header .meta {
1699
1791
  display: flex; gap: 18px; font-size: 12px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leeandrew94/ccm",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Claude Code Model Manager - switch between AI models per terminal",
5
5
  "type": "module",
6
6
  "bin": {