@asifkibria/claude-code-toolkit 1.3.1 → 1.4.2
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 +16 -1
- package/dist/__tests__/alerts.test.d.ts +2 -0
- package/dist/__tests__/alerts.test.d.ts.map +1 -0
- package/dist/__tests__/alerts.test.js +195 -0
- package/dist/__tests__/alerts.test.js.map +1 -0
- package/dist/__tests__/bookmarks.test.d.ts +2 -0
- package/dist/__tests__/bookmarks.test.d.ts.map +1 -0
- package/dist/__tests__/bookmarks.test.js +170 -0
- package/dist/__tests__/bookmarks.test.js.map +1 -0
- package/dist/__tests__/bulk.test.d.ts +2 -0
- package/dist/__tests__/bulk.test.d.ts.map +1 -0
- package/dist/__tests__/bulk.test.js +245 -0
- package/dist/__tests__/bulk.test.js.map +1 -0
- package/dist/__tests__/dashboard.test.js +2 -2
- package/dist/__tests__/dashboard.test.js.map +1 -1
- package/dist/__tests__/git.test.d.ts +2 -0
- package/dist/__tests__/git.test.d.ts.map +1 -0
- package/dist/__tests__/git.test.js +216 -0
- package/dist/__tests__/git.test.js.map +1 -0
- package/dist/__tests__/logs.test.d.ts +2 -0
- package/dist/__tests__/logs.test.d.ts.map +1 -0
- package/dist/__tests__/logs.test.js +196 -0
- package/dist/__tests__/logs.test.js.map +1 -0
- package/dist/__tests__/search.test.d.ts +2 -0
- package/dist/__tests__/search.test.d.ts.map +1 -0
- package/dist/__tests__/search.test.js +155 -0
- package/dist/__tests__/search.test.js.map +1 -0
- package/dist/cli.js +269 -12
- package/dist/cli.js.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/bookmarks.d.ts +69 -0
- package/dist/lib/bookmarks.d.ts.map +1 -0
- package/dist/lib/bookmarks.js +225 -0
- package/dist/lib/bookmarks.js.map +1 -0
- package/dist/lib/bulk.d.ts +78 -0
- package/dist/lib/bulk.d.ts.map +1 -0
- package/dist/lib/bulk.js +257 -0
- package/dist/lib/bulk.js.map +1 -0
- package/dist/lib/dashboard-ui.d.ts.map +1 -1
- package/dist/lib/dashboard-ui.js +425 -19
- package/dist/lib/dashboard-ui.js.map +1 -1
- package/dist/lib/dashboard.d.ts.map +1 -1
- package/dist/lib/dashboard.js +398 -20
- package/dist/lib/dashboard.js.map +1 -1
- package/dist/lib/export.d.ts +41 -0
- package/dist/lib/export.d.ts.map +1 -0
- package/dist/lib/export.js +428 -0
- package/dist/lib/export.js.map +1 -0
- package/dist/lib/git.d.ts.map +1 -1
- package/dist/lib/git.js +2 -1
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/mcp-validator.d.ts.map +1 -1
- package/dist/lib/mcp-validator.js +2 -1
- package/dist/lib/mcp-validator.js.map +1 -1
- package/dist/lib/scanner.d.ts.map +1 -1
- package/dist/lib/scanner.js +2 -12
- package/dist/lib/scanner.js.map +1 -1
- package/dist/lib/security.d.ts.map +1 -1
- package/dist/lib/security.js +12 -1
- package/dist/lib/security.js.map +1 -1
- package/dist/lib/session-recovery.d.ts.map +1 -1
- package/dist/lib/session-recovery.js +25 -2
- package/dist/lib/session-recovery.js.map +1 -1
- package/dist/lib/utils.d.ts +15 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +78 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +2 -2
package/dist/lib/dashboard.js
CHANGED
|
@@ -14,6 +14,9 @@ import { checkAlerts, checkQuotas } from "./alerts.js";
|
|
|
14
14
|
import { linkSessionsToGit } from "./git.js";
|
|
15
15
|
import { listLogFiles, parseAllLogs, getLogSummary } from "./logs.js";
|
|
16
16
|
import { findAllJsonlFiles, findBackupFiles, scanFile, fixFile, getConversationStats, estimateContextSize, generateUsageAnalytics, findDuplicates, findArchiveCandidates, archiveConversations, runMaintenance, deleteOldBackups, restoreFromBackup, exportConversation, } from "./scanner.js";
|
|
17
|
+
import { addBookmark, removeBookmark, getSessionBookmarks, renameSession, starSession, tagSession, addTagToSession, removeTagFromSession, getSessionTags, getAllTags, getStarredSessions, getBookmarksSummary, } from "./bookmarks.js";
|
|
18
|
+
import { exportToHtml } from "./export.js";
|
|
19
|
+
import { bulkDelete, bulkArchiveSessions, bulkExportSessions } from "./bulk.js";
|
|
17
20
|
import { saveStorageSnapshot, listStorageSnapshots, loadStorageSnapshot, compareStorageSnapshots, deleteStorageSnapshot, // Missing export in storage.ts? I added it.
|
|
18
21
|
} from "./storage.js";
|
|
19
22
|
const CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
@@ -43,7 +46,7 @@ function parseUrl(url) {
|
|
|
43
46
|
}
|
|
44
47
|
return { pathname, params };
|
|
45
48
|
}
|
|
46
|
-
function extractBearerToken(req,
|
|
49
|
+
function extractBearerToken(req, _params, authToken) {
|
|
47
50
|
if (!authToken)
|
|
48
51
|
return true;
|
|
49
52
|
const header = req.headers["authorization"];
|
|
@@ -52,9 +55,6 @@ function extractBearerToken(req, params, authToken) {
|
|
|
52
55
|
if (provided === authToken)
|
|
53
56
|
return true;
|
|
54
57
|
}
|
|
55
|
-
if (params?.token === authToken) {
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
58
|
return false;
|
|
59
59
|
}
|
|
60
60
|
function rejectUnauthorized(res) {
|
|
@@ -78,9 +78,20 @@ function matchRoute(pathname, pattern) {
|
|
|
78
78
|
return params;
|
|
79
79
|
}
|
|
80
80
|
function readBody(req) {
|
|
81
|
-
|
|
81
|
+
const MAX_BODY_SIZE = 10 * 1024 * 1024;
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
82
83
|
let data = "";
|
|
83
|
-
|
|
84
|
+
let size = 0;
|
|
85
|
+
req.on("data", (chunk) => {
|
|
86
|
+
size += chunk.length;
|
|
87
|
+
if (size > MAX_BODY_SIZE) {
|
|
88
|
+
req.destroy();
|
|
89
|
+
reject(new Error("Request body too large"));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
data += chunk.toString();
|
|
93
|
+
});
|
|
94
|
+
req.on("error", () => resolve({}));
|
|
84
95
|
req.on("end", () => {
|
|
85
96
|
try {
|
|
86
97
|
resolve(JSON.parse(data));
|
|
@@ -214,9 +225,11 @@ function getSecurity() {
|
|
|
214
225
|
}
|
|
215
226
|
function getSecurityFindingPreview(filePath, lineNum) {
|
|
216
227
|
try {
|
|
217
|
-
const fullPath = path.
|
|
218
|
-
|
|
219
|
-
|
|
228
|
+
const fullPath = path.resolve(PROJECTS_DIR, filePath);
|
|
229
|
+
if (!fullPath.startsWith(PROJECTS_DIR + path.sep) && fullPath !== PROJECTS_DIR)
|
|
230
|
+
return null;
|
|
231
|
+
const target = fs.existsSync(fullPath) ? fullPath : null;
|
|
232
|
+
if (!target)
|
|
220
233
|
return null;
|
|
221
234
|
const content = fs.readFileSync(target, "utf-8");
|
|
222
235
|
const lines = content.split("\n");
|
|
@@ -989,6 +1002,43 @@ function actionPreviewTraces(body) {
|
|
|
989
1002
|
});
|
|
990
1003
|
return { success: true, ...preview };
|
|
991
1004
|
}
|
|
1005
|
+
function redactStringValue(text, patterns) {
|
|
1006
|
+
let count = 0;
|
|
1007
|
+
let result = text;
|
|
1008
|
+
for (const pat of patterns) {
|
|
1009
|
+
pat.regex.lastIndex = 0;
|
|
1010
|
+
const before = result;
|
|
1011
|
+
result = result.replace(pat.regex, "[REDACTED]");
|
|
1012
|
+
if (result !== before)
|
|
1013
|
+
count++;
|
|
1014
|
+
}
|
|
1015
|
+
return { result, count };
|
|
1016
|
+
}
|
|
1017
|
+
function redactInObject(obj, patterns) {
|
|
1018
|
+
let totalCount = 0;
|
|
1019
|
+
if (typeof obj === "string") {
|
|
1020
|
+
const { result, count } = redactStringValue(obj, patterns);
|
|
1021
|
+
return { result, count };
|
|
1022
|
+
}
|
|
1023
|
+
if (Array.isArray(obj)) {
|
|
1024
|
+
const newArr = obj.map(item => {
|
|
1025
|
+
const { result, count } = redactInObject(item, patterns);
|
|
1026
|
+
totalCount += count;
|
|
1027
|
+
return result;
|
|
1028
|
+
});
|
|
1029
|
+
return { result: newArr, count: totalCount };
|
|
1030
|
+
}
|
|
1031
|
+
if (obj && typeof obj === "object") {
|
|
1032
|
+
const newObj = {};
|
|
1033
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1034
|
+
const { result, count } = redactInObject(value, patterns);
|
|
1035
|
+
totalCount += count;
|
|
1036
|
+
newObj[key] = result;
|
|
1037
|
+
}
|
|
1038
|
+
return { result: newObj, count: totalCount };
|
|
1039
|
+
}
|
|
1040
|
+
return { result: obj, count: 0 };
|
|
1041
|
+
}
|
|
992
1042
|
function actionRedact(body) {
|
|
993
1043
|
const file = body.file;
|
|
994
1044
|
const lineNum = body.line;
|
|
@@ -1012,11 +1062,24 @@ function actionRedact(body) {
|
|
|
1012
1062
|
const line = lines[lineNum - 1];
|
|
1013
1063
|
if (!line.trim())
|
|
1014
1064
|
return { success: false, error: "Empty line" };
|
|
1015
|
-
let redacted = line;
|
|
1016
|
-
let count = 0;
|
|
1017
1065
|
const patterns = patternType
|
|
1018
1066
|
? SECRET_PATTERNS.filter(p => p.type === patternType || p.name === patternType)
|
|
1019
1067
|
: SECRET_PATTERNS;
|
|
1068
|
+
let parsed;
|
|
1069
|
+
try {
|
|
1070
|
+
parsed = JSON.parse(line);
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
parsed = {};
|
|
1074
|
+
}
|
|
1075
|
+
const { result: redactedObj, count: jsonCount } = redactInObject(parsed, patterns);
|
|
1076
|
+
if (jsonCount > 0) {
|
|
1077
|
+
lines[lineNum - 1] = JSON.stringify(redactedObj);
|
|
1078
|
+
fs.writeFileSync(file, lines.join("\n"), "utf-8");
|
|
1079
|
+
return { success: true, redactedCount: jsonCount, backupPath };
|
|
1080
|
+
}
|
|
1081
|
+
let redacted = line;
|
|
1082
|
+
let count = 0;
|
|
1020
1083
|
for (const pat of patterns) {
|
|
1021
1084
|
pat.regex.lastIndex = 0;
|
|
1022
1085
|
const before = redacted;
|
|
@@ -1044,6 +1107,10 @@ function actionRedactAll() {
|
|
|
1044
1107
|
let secretsRedacted = 0;
|
|
1045
1108
|
const errors = [];
|
|
1046
1109
|
for (const [file, findings] of fileGroups) {
|
|
1110
|
+
if (!fs.existsSync(file)) {
|
|
1111
|
+
errors.push(`${file}: file no longer exists`);
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1047
1114
|
try {
|
|
1048
1115
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1049
1116
|
const backupPath = file.replace(".jsonl", `.backup.${timestamp}.jsonl`);
|
|
@@ -1058,17 +1125,32 @@ function actionRedactAll() {
|
|
|
1058
1125
|
processedLines.add(f.line);
|
|
1059
1126
|
if (f.line < 1 || f.line > lines.length)
|
|
1060
1127
|
continue;
|
|
1061
|
-
|
|
1128
|
+
const line = lines[f.line - 1];
|
|
1129
|
+
let parsed = null;
|
|
1130
|
+
try {
|
|
1131
|
+
parsed = JSON.parse(line);
|
|
1132
|
+
}
|
|
1133
|
+
catch { /* not valid JSON */ }
|
|
1134
|
+
if (parsed) {
|
|
1135
|
+
const { result: redactedObj, count } = redactInObject(parsed, SECRET_PATTERNS);
|
|
1136
|
+
if (count > 0) {
|
|
1137
|
+
lines[f.line - 1] = JSON.stringify(redactedObj);
|
|
1138
|
+
secretsRedacted += count;
|
|
1139
|
+
modified = true;
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
let rawLine = line;
|
|
1062
1144
|
for (const pat of SECRET_PATTERNS) {
|
|
1063
1145
|
pat.regex.lastIndex = 0;
|
|
1064
|
-
const before =
|
|
1065
|
-
|
|
1066
|
-
if (
|
|
1146
|
+
const before = rawLine;
|
|
1147
|
+
rawLine = rawLine.replace(pat.regex, "[REDACTED]");
|
|
1148
|
+
if (rawLine !== before) {
|
|
1067
1149
|
secretsRedacted++;
|
|
1068
1150
|
modified = true;
|
|
1069
1151
|
}
|
|
1070
1152
|
}
|
|
1071
|
-
lines[f.line - 1] =
|
|
1153
|
+
lines[f.line - 1] = rawLine;
|
|
1072
1154
|
}
|
|
1073
1155
|
if (modified) {
|
|
1074
1156
|
fs.writeFileSync(file, lines.join("\n"), "utf-8");
|
|
@@ -1235,7 +1317,7 @@ const getRoutes = {
|
|
|
1235
1317
|
"/api/pii": (params) => {
|
|
1236
1318
|
const limit = parseInt(params?.limit) || 50;
|
|
1237
1319
|
const offset = parseInt(params?.offset) || 0;
|
|
1238
|
-
const result = scanForPII(undefined, { includeFullValues:
|
|
1320
|
+
const result = scanForPII(undefined, { includeFullValues: false });
|
|
1239
1321
|
const paginatedFindings = result.findings.slice(offset, offset + limit);
|
|
1240
1322
|
return {
|
|
1241
1323
|
filesScanned: result.filesScanned,
|
|
@@ -1287,6 +1369,33 @@ const getRoutes = {
|
|
|
1287
1369
|
return { error: "Snapshots not found" };
|
|
1288
1370
|
const diff = compareStorageSnapshots(s1.analysis, s2.analysis);
|
|
1289
1371
|
return { success: true, diff, baseDate: s1.date, currentDate: s2.date };
|
|
1372
|
+
},
|
|
1373
|
+
"/api/bookmarks": () => {
|
|
1374
|
+
const summary = getBookmarksSummary();
|
|
1375
|
+
const allTags = getAllTags();
|
|
1376
|
+
const starred = getStarredSessions();
|
|
1377
|
+
return { ...summary, allTags, starred };
|
|
1378
|
+
},
|
|
1379
|
+
"/api/bookmarks/starred": () => {
|
|
1380
|
+
const starred = getStarredSessions();
|
|
1381
|
+
const sessions = listSessions();
|
|
1382
|
+
return starred.map(s => {
|
|
1383
|
+
const session = sessions.find(sess => sess.id === s.sessionId || sess.id.startsWith(s.sessionId));
|
|
1384
|
+
return {
|
|
1385
|
+
...s,
|
|
1386
|
+
project: session?.project,
|
|
1387
|
+
messageCount: session?.messageCount,
|
|
1388
|
+
sizeBytes: session?.sizeBytes,
|
|
1389
|
+
};
|
|
1390
|
+
});
|
|
1391
|
+
},
|
|
1392
|
+
"/api/bookmarks/tags": () => getAllTags(),
|
|
1393
|
+
"/api/session/:id/tags": (params) => {
|
|
1394
|
+
const sessionId = params?.id;
|
|
1395
|
+
if (!sessionId)
|
|
1396
|
+
return { error: "Session ID required" };
|
|
1397
|
+
const tags = getSessionTags(sessionId);
|
|
1398
|
+
return tags || { sessionId, tags: [], starred: false };
|
|
1290
1399
|
}
|
|
1291
1400
|
};
|
|
1292
1401
|
const postRoutes = {
|
|
@@ -1322,14 +1431,256 @@ const postRoutes = {
|
|
|
1322
1431
|
if (!id)
|
|
1323
1432
|
return { success: false, error: "ID required" };
|
|
1324
1433
|
return { success: deleteStorageSnapshot(id) };
|
|
1434
|
+
},
|
|
1435
|
+
"/api/action/rename-session": (b) => {
|
|
1436
|
+
const sessionId = b.sessionId;
|
|
1437
|
+
const name = b.name;
|
|
1438
|
+
if (!sessionId || !name)
|
|
1439
|
+
return { success: false, error: "sessionId and name required" };
|
|
1440
|
+
const result = renameSession(sessionId, name);
|
|
1441
|
+
return { success: true, ...result };
|
|
1442
|
+
},
|
|
1443
|
+
"/api/action/star-session": (b) => {
|
|
1444
|
+
const sessionId = b.sessionId;
|
|
1445
|
+
const starred = b.starred !== false;
|
|
1446
|
+
if (!sessionId)
|
|
1447
|
+
return { success: false, error: "sessionId required" };
|
|
1448
|
+
const result = starSession(sessionId, starred);
|
|
1449
|
+
return { success: true, ...result };
|
|
1450
|
+
},
|
|
1451
|
+
"/api/action/tag-session": (b) => {
|
|
1452
|
+
const sessionId = b.sessionId;
|
|
1453
|
+
const tags = b.tags;
|
|
1454
|
+
if (!sessionId)
|
|
1455
|
+
return { success: false, error: "sessionId required" };
|
|
1456
|
+
const result = tagSession(sessionId, { tags });
|
|
1457
|
+
return { success: true, ...result };
|
|
1458
|
+
},
|
|
1459
|
+
"/api/action/add-tag": (b) => {
|
|
1460
|
+
const sessionId = b.sessionId;
|
|
1461
|
+
const tag = b.tag;
|
|
1462
|
+
if (!sessionId || !tag)
|
|
1463
|
+
return { success: false, error: "sessionId and tag required" };
|
|
1464
|
+
const result = addTagToSession(sessionId, tag);
|
|
1465
|
+
return { success: true, ...result };
|
|
1466
|
+
},
|
|
1467
|
+
"/api/action/remove-tag": (b) => {
|
|
1468
|
+
const sessionId = b.sessionId;
|
|
1469
|
+
const tag = b.tag;
|
|
1470
|
+
if (!sessionId || !tag)
|
|
1471
|
+
return { success: false, error: "sessionId and tag required" };
|
|
1472
|
+
const result = removeTagFromSession(sessionId, tag);
|
|
1473
|
+
return result ? { success: true, ...result } : { success: false, error: "Session tags not found" };
|
|
1474
|
+
},
|
|
1475
|
+
"/api/action/add-bookmark": (b) => {
|
|
1476
|
+
const sessionId = b.sessionId;
|
|
1477
|
+
const lineNumber = b.lineNumber;
|
|
1478
|
+
const label = b.label;
|
|
1479
|
+
const preview = b.preview;
|
|
1480
|
+
if (!sessionId || !lineNumber)
|
|
1481
|
+
return { success: false, error: "sessionId and lineNumber required" };
|
|
1482
|
+
const result = addBookmark(sessionId, lineNumber, { label, preview });
|
|
1483
|
+
return { success: true, bookmark: result };
|
|
1484
|
+
},
|
|
1485
|
+
"/api/action/remove-bookmark": (b) => {
|
|
1486
|
+
const bookmarkId = b.bookmarkId;
|
|
1487
|
+
if (!bookmarkId)
|
|
1488
|
+
return { success: false, error: "bookmarkId required" };
|
|
1489
|
+
const result = removeBookmark(bookmarkId);
|
|
1490
|
+
return { success: result };
|
|
1491
|
+
},
|
|
1492
|
+
"/api/action/export-html": (b) => {
|
|
1493
|
+
const sessionId = b.sessionId;
|
|
1494
|
+
const theme = b.theme || "light";
|
|
1495
|
+
if (!sessionId)
|
|
1496
|
+
return { success: false, error: "sessionId required" };
|
|
1497
|
+
const sessions = listSessions();
|
|
1498
|
+
const session = sessions.find(s => s.id === sessionId || s.id.startsWith(sessionId));
|
|
1499
|
+
if (!session)
|
|
1500
|
+
return { success: false, error: "Session not found" };
|
|
1501
|
+
const outputPath = session.filePath.replace(".jsonl", ".html");
|
|
1502
|
+
const result = exportToHtml(session.filePath, outputPath, {
|
|
1503
|
+
theme: theme === "dark" ? "dark" : "light",
|
|
1504
|
+
includeTimestamps: true,
|
|
1505
|
+
syntaxHighlighting: true,
|
|
1506
|
+
});
|
|
1507
|
+
return { success: true, file: result.file, messageCount: result.messageCount, size: result.size };
|
|
1508
|
+
},
|
|
1509
|
+
"/api/action/bulk-export": (b) => {
|
|
1510
|
+
const projectFilter = b.projectFilter;
|
|
1511
|
+
const format = b.format || "html";
|
|
1512
|
+
const result = bulkExportSessions({
|
|
1513
|
+
projectFilter,
|
|
1514
|
+
format: format,
|
|
1515
|
+
});
|
|
1516
|
+
return { success: true, exported: result.exported.length, errors: result.errors.length, totalMessages: result.totalMessages };
|
|
1517
|
+
},
|
|
1518
|
+
"/api/action/bulk-delete": (b) => {
|
|
1519
|
+
const days = b.days;
|
|
1520
|
+
const projectFilter = b.projectFilter;
|
|
1521
|
+
const dryRun = b.dryRun !== false;
|
|
1522
|
+
const result = bulkDelete({
|
|
1523
|
+
olderThanDays: days,
|
|
1524
|
+
projectFilter,
|
|
1525
|
+
dryRun,
|
|
1526
|
+
includeStarred: false,
|
|
1527
|
+
});
|
|
1528
|
+
return { success: true, deleted: result.deleted.length, skipped: result.skipped.length, totalFreed: result.totalFreed, dryRun, errors: result.errors };
|
|
1529
|
+
},
|
|
1530
|
+
"/api/action/bulk-archive": (b) => {
|
|
1531
|
+
const days = b.days;
|
|
1532
|
+
const projectFilter = b.projectFilter;
|
|
1533
|
+
const dryRun = b.dryRun !== false;
|
|
1534
|
+
const result = bulkArchiveSessions({
|
|
1535
|
+
olderThanDays: days,
|
|
1536
|
+
projectFilter,
|
|
1537
|
+
dryRun,
|
|
1538
|
+
includeStarred: false,
|
|
1539
|
+
});
|
|
1540
|
+
return { success: true, archived: result.archived.length, skipped: result.skipped.length, totalSize: result.totalSize, dryRun, errors: result.errors };
|
|
1541
|
+
},
|
|
1542
|
+
"/api/action/purge-project": (b) => {
|
|
1543
|
+
const projectFilter = b.projectFilter;
|
|
1544
|
+
if (!projectFilter)
|
|
1545
|
+
return { success: false, error: "projectFilter required" };
|
|
1546
|
+
const deleteResult = bulkDelete({
|
|
1547
|
+
projectFilter,
|
|
1548
|
+
dryRun: false,
|
|
1549
|
+
includeStarred: true,
|
|
1550
|
+
});
|
|
1551
|
+
let dirsRemoved = 0;
|
|
1552
|
+
let dirFreedBytes = 0;
|
|
1553
|
+
const dirErrors = [];
|
|
1554
|
+
try {
|
|
1555
|
+
const projectDirs = fs.readdirSync(PROJECTS_DIR);
|
|
1556
|
+
for (const dir of projectDirs) {
|
|
1557
|
+
if (dir.includes(projectFilter) || projectFilter.includes(dir)) {
|
|
1558
|
+
const dirPath = path.join(PROJECTS_DIR, dir);
|
|
1559
|
+
const stat = fs.statSync(dirPath);
|
|
1560
|
+
if (stat.isDirectory()) {
|
|
1561
|
+
const files = fs.readdirSync(dirPath);
|
|
1562
|
+
for (const file of files) {
|
|
1563
|
+
const filePath = path.join(dirPath, file);
|
|
1564
|
+
try {
|
|
1565
|
+
const fstat = fs.statSync(filePath);
|
|
1566
|
+
dirFreedBytes += fstat.size;
|
|
1567
|
+
fs.unlinkSync(filePath);
|
|
1568
|
+
}
|
|
1569
|
+
catch (e) {
|
|
1570
|
+
dirErrors.push(`${filePath}: ${e}`);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
try {
|
|
1574
|
+
fs.rmdirSync(dirPath);
|
|
1575
|
+
dirsRemoved++;
|
|
1576
|
+
}
|
|
1577
|
+
catch (e) {
|
|
1578
|
+
dirErrors.push(`rmdir ${dirPath}: ${e}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
catch (e) {
|
|
1585
|
+
dirErrors.push(`readdir: ${e}`);
|
|
1586
|
+
}
|
|
1587
|
+
return {
|
|
1588
|
+
success: true,
|
|
1589
|
+
sessionsDeleted: deleteResult.deleted.length,
|
|
1590
|
+
freedBytes: deleteResult.totalFreed + dirFreedBytes,
|
|
1591
|
+
dirsRemoved,
|
|
1592
|
+
errors: [...deleteResult.errors, ...dirErrors],
|
|
1593
|
+
};
|
|
1594
|
+
},
|
|
1595
|
+
"/api/action/delete-files": (b) => {
|
|
1596
|
+
const files = b.files;
|
|
1597
|
+
if (!files || !Array.isArray(files) || files.length === 0) {
|
|
1598
|
+
return { success: false, error: "files array required" };
|
|
1599
|
+
}
|
|
1600
|
+
let deleted = 0;
|
|
1601
|
+
let freedBytes = 0;
|
|
1602
|
+
const errors = [];
|
|
1603
|
+
for (const filePath of files) {
|
|
1604
|
+
try {
|
|
1605
|
+
const resolved = path.resolve(filePath);
|
|
1606
|
+
if (!resolved.startsWith(CLAUDE_DIR + path.sep)) {
|
|
1607
|
+
errors.push(`${filePath}: path outside Claude directory`);
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (fs.existsSync(resolved)) {
|
|
1611
|
+
const stat = fs.statSync(resolved);
|
|
1612
|
+
freedBytes += stat.size;
|
|
1613
|
+
fs.unlinkSync(resolved);
|
|
1614
|
+
deleted++;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
catch (e) {
|
|
1618
|
+
errors.push(`${filePath}: ${e}`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
return { success: true, deleted, freedBytes, errors };
|
|
1622
|
+
},
|
|
1623
|
+
"/api/action/clean-category": (b) => {
|
|
1624
|
+
const category = b.category;
|
|
1625
|
+
if (!category)
|
|
1626
|
+
return { success: false, error: "category required" };
|
|
1627
|
+
const traces = inventoryTraces();
|
|
1628
|
+
const cat = traces.categories.find(c => c.name === category);
|
|
1629
|
+
if (!cat || !cat.items)
|
|
1630
|
+
return { success: false, error: "Category not found" };
|
|
1631
|
+
let deleted = 0;
|
|
1632
|
+
let freedBytes = 0;
|
|
1633
|
+
const errors = [];
|
|
1634
|
+
for (const item of cat.items) {
|
|
1635
|
+
try {
|
|
1636
|
+
if (fs.existsSync(item.path)) {
|
|
1637
|
+
freedBytes += item.size;
|
|
1638
|
+
fs.unlinkSync(item.path);
|
|
1639
|
+
deleted++;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
catch (e) {
|
|
1643
|
+
errors.push(`${item.path}: ${e}`);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return { success: true, deleted, freedBytes, errors };
|
|
1647
|
+
},
|
|
1648
|
+
"/api/action/ignore-pii": (b) => {
|
|
1649
|
+
const finding = b.finding;
|
|
1650
|
+
if (!finding)
|
|
1651
|
+
return { success: false, error: "finding required" };
|
|
1652
|
+
const ignoreFile = path.join(os.homedir(), ".claude", "pii-ignore.json");
|
|
1653
|
+
let ignoreList = [];
|
|
1654
|
+
try {
|
|
1655
|
+
if (fs.existsSync(ignoreFile)) {
|
|
1656
|
+
ignoreList = JSON.parse(fs.readFileSync(ignoreFile, "utf-8"));
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
catch { /* ignore */ }
|
|
1660
|
+
const pattern = finding.value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1661
|
+
if (!ignoreList.some(i => i.pattern === pattern)) {
|
|
1662
|
+
ignoreList.push({ pattern, type: finding.type, reason: "User ignored" });
|
|
1663
|
+
fs.writeFileSync(ignoreFile, JSON.stringify(ignoreList, null, 2));
|
|
1664
|
+
}
|
|
1665
|
+
return { success: true, ignored: pattern };
|
|
1325
1666
|
}
|
|
1326
1667
|
};
|
|
1327
1668
|
export function createDashboardServer(authToken) {
|
|
1328
1669
|
const html = generateDashboardHTML();
|
|
1670
|
+
const CORS_HEADERS = {
|
|
1671
|
+
"Access-Control-Allow-Origin": "http://localhost",
|
|
1672
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
1673
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
1674
|
+
};
|
|
1329
1675
|
const server = http.createServer(async (req, res) => {
|
|
1330
1676
|
const { pathname, params } = parseUrl(req.url || "/");
|
|
1677
|
+
if (req.method === "OPTIONS") {
|
|
1678
|
+
res.writeHead(204, CORS_HEADERS);
|
|
1679
|
+
res.end();
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1331
1682
|
if (req.method === "GET" && pathname === "/") {
|
|
1332
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1683
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", ...CORS_HEADERS });
|
|
1333
1684
|
res.end(html);
|
|
1334
1685
|
return;
|
|
1335
1686
|
}
|
|
@@ -1384,11 +1735,11 @@ export function createDashboardServer(authToken) {
|
|
|
1384
1735
|
if (handler) {
|
|
1385
1736
|
try {
|
|
1386
1737
|
const data = await handler(params);
|
|
1387
|
-
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
|
|
1738
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache", ...CORS_HEADERS });
|
|
1388
1739
|
res.end(JSON.stringify(data));
|
|
1389
1740
|
}
|
|
1390
1741
|
catch (err) {
|
|
1391
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1742
|
+
res.writeHead(500, { "Content-Type": "application/json", ...CORS_HEADERS });
|
|
1392
1743
|
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
1393
1744
|
}
|
|
1394
1745
|
return;
|
|
@@ -1431,6 +1782,33 @@ export function createDashboardServer(authToken) {
|
|
|
1431
1782
|
}
|
|
1432
1783
|
return;
|
|
1433
1784
|
}
|
|
1785
|
+
const tagsMatch = matchRoute(pathname, "/api/session/:id/tags");
|
|
1786
|
+
if (tagsMatch) {
|
|
1787
|
+
try {
|
|
1788
|
+
const tags = getSessionTags(tagsMatch.id);
|
|
1789
|
+
const data = tags || { sessionId: tagsMatch.id, tags: [], starred: false };
|
|
1790
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
|
|
1791
|
+
res.end(JSON.stringify(data));
|
|
1792
|
+
}
|
|
1793
|
+
catch (err) {
|
|
1794
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1795
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
1796
|
+
}
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
const bookmarksMatch = matchRoute(pathname, "/api/session/:id/bookmarks");
|
|
1800
|
+
if (bookmarksMatch) {
|
|
1801
|
+
try {
|
|
1802
|
+
const bookmarks = getSessionBookmarks(bookmarksMatch.id);
|
|
1803
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" });
|
|
1804
|
+
res.end(JSON.stringify({ bookmarks }));
|
|
1805
|
+
}
|
|
1806
|
+
catch (err) {
|
|
1807
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1808
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
1809
|
+
}
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1434
1812
|
const findingMatch = matchRoute(pathname, "/api/security/finding/:file/:line");
|
|
1435
1813
|
if (findingMatch) {
|
|
1436
1814
|
try {
|