@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.
- package/bin/ccm.js +208 -116
- 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,
|
|
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${
|
|
542
|
+
console.log(` \x1B[36m\u276F\x1B[0m \x1B[1m${options[i]}\x1B[0m`);
|
|
513
543
|
} else {
|
|
514
|
-
console.log(` \x1B[90m${
|
|
544
|
+
console.log(` \x1B[90m${options[i]}\x1B[0m`);
|
|
515
545
|
}
|
|
516
546
|
}
|
|
517
547
|
process.stdout.write("\x1B[2K");
|
|
518
|
-
if (
|
|
519
|
-
console.log(` \x1B[90m${selected + 1}/${
|
|
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 + (
|
|
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(
|
|
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,
|
|
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${
|
|
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
|
-
|
|
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
|
|
589
|
-
let models =
|
|
590
|
-
if (
|
|
591
|
-
models
|
|
592
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
|
1036
|
-
import
|
|
1059
|
+
import fs5 from "fs";
|
|
1060
|
+
import path5 from "path";
|
|
1037
1061
|
import os2 from "os";
|
|
1038
|
-
var CLAUDE_DIR =
|
|
1039
|
-
var HISTORY_FILE =
|
|
1040
|
-
var PROJECTS_DIR =
|
|
1041
|
-
var TRASH_DIR =
|
|
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 (!
|
|
1045
|
-
const lines =
|
|
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
|
-
|
|
1058
|
-
|
|
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 (!
|
|
1097
|
-
const projectDirs =
|
|
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 =
|
|
1100
|
-
if (
|
|
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
|
-
|
|
1193
|
+
fs5.mkdirSync(TRASH_DIR, { recursive: true });
|
|
1170
1194
|
}
|
|
1171
1195
|
function extractHistoryForSession(sessionId) {
|
|
1172
|
-
if (!
|
|
1173
|
-
const lines =
|
|
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 (!
|
|
1186
|
-
const lines =
|
|
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 =
|
|
1205
|
-
const destJsonl =
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const sessionDir =
|
|
1209
|
-
if (
|
|
1210
|
-
const destDir =
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
-
|
|
1222
|
-
|
|
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 =
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const sessionDir =
|
|
1239
|
-
if (
|
|
1240
|
-
const destDir =
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
-
|
|
1252
|
-
|
|
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 (
|
|
1281
|
+
if (fs5.existsSync(HISTORY_FILE)) {
|
|
1258
1282
|
atomicWriteJsonl(HISTORY_FILE, []);
|
|
1259
1283
|
}
|
|
1260
1284
|
}
|
|
1261
1285
|
function restoreSession(sessionId) {
|
|
1262
|
-
const metaPath =
|
|
1263
|
-
if (!
|
|
1264
|
-
const meta = JSON.parse(
|
|
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 =
|
|
1267
|
-
if (
|
|
1268
|
-
|
|
1269
|
-
|
|
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 =
|
|
1272
|
-
if (
|
|
1273
|
-
const restoredDir =
|
|
1274
|
-
|
|
1275
|
-
|
|
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 (
|
|
1280
|
-
const existing =
|
|
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
|
-
|
|
1311
|
+
fs5.unlinkSync(metaPath);
|
|
1288
1312
|
return true;
|
|
1289
1313
|
}
|
|
1290
1314
|
function findProjectDirForRestore(projectName) {
|
|
1291
|
-
if (!
|
|
1292
|
-
const dirs =
|
|
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 =
|
|
1296
|
-
if (
|
|
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 =
|
|
1301
|
-
if (
|
|
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 =
|
|
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(
|
|
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 =
|
|
1347
|
+
const files = fs5.readdirSync(TRASH_DIR);
|
|
1324
1348
|
for (const file of files) {
|
|
1325
|
-
const fullPath =
|
|
1326
|
-
if (
|
|
1327
|
-
|
|
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
|
-
|
|
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 =
|
|
1341
|
-
if (
|
|
1342
|
-
const sessionDir =
|
|
1343
|
-
if (
|
|
1344
|
-
const metaPath =
|
|
1345
|
-
if (
|
|
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 =
|
|
1536
|
-
|
|
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
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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;
|