@asifkibria/claude-code-toolkit 1.3.0 → 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.
Files changed (69) hide show
  1. package/README.md +21 -6
  2. package/dist/__tests__/alerts.test.d.ts +2 -0
  3. package/dist/__tests__/alerts.test.d.ts.map +1 -0
  4. package/dist/__tests__/alerts.test.js +195 -0
  5. package/dist/__tests__/alerts.test.js.map +1 -0
  6. package/dist/__tests__/bookmarks.test.d.ts +2 -0
  7. package/dist/__tests__/bookmarks.test.d.ts.map +1 -0
  8. package/dist/__tests__/bookmarks.test.js +170 -0
  9. package/dist/__tests__/bookmarks.test.js.map +1 -0
  10. package/dist/__tests__/bulk.test.d.ts +2 -0
  11. package/dist/__tests__/bulk.test.d.ts.map +1 -0
  12. package/dist/__tests__/bulk.test.js +245 -0
  13. package/dist/__tests__/bulk.test.js.map +1 -0
  14. package/dist/__tests__/dashboard.test.js +2 -2
  15. package/dist/__tests__/dashboard.test.js.map +1 -1
  16. package/dist/__tests__/git.test.d.ts +2 -0
  17. package/dist/__tests__/git.test.d.ts.map +1 -0
  18. package/dist/__tests__/git.test.js +216 -0
  19. package/dist/__tests__/git.test.js.map +1 -0
  20. package/dist/__tests__/logs.test.d.ts +2 -0
  21. package/dist/__tests__/logs.test.d.ts.map +1 -0
  22. package/dist/__tests__/logs.test.js +196 -0
  23. package/dist/__tests__/logs.test.js.map +1 -0
  24. package/dist/__tests__/search.test.d.ts +2 -0
  25. package/dist/__tests__/search.test.d.ts.map +1 -0
  26. package/dist/__tests__/search.test.js +155 -0
  27. package/dist/__tests__/search.test.js.map +1 -0
  28. package/dist/cli.js +269 -12
  29. package/dist/cli.js.map +1 -1
  30. package/dist/index.js +5 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/lib/bookmarks.d.ts +69 -0
  33. package/dist/lib/bookmarks.d.ts.map +1 -0
  34. package/dist/lib/bookmarks.js +225 -0
  35. package/dist/lib/bookmarks.js.map +1 -0
  36. package/dist/lib/bulk.d.ts +78 -0
  37. package/dist/lib/bulk.d.ts.map +1 -0
  38. package/dist/lib/bulk.js +257 -0
  39. package/dist/lib/bulk.js.map +1 -0
  40. package/dist/lib/dashboard-ui.d.ts.map +1 -1
  41. package/dist/lib/dashboard-ui.js +425 -19
  42. package/dist/lib/dashboard-ui.js.map +1 -1
  43. package/dist/lib/dashboard.d.ts.map +1 -1
  44. package/dist/lib/dashboard.js +398 -20
  45. package/dist/lib/dashboard.js.map +1 -1
  46. package/dist/lib/export.d.ts +41 -0
  47. package/dist/lib/export.d.ts.map +1 -0
  48. package/dist/lib/export.js +428 -0
  49. package/dist/lib/export.js.map +1 -0
  50. package/dist/lib/git.d.ts.map +1 -1
  51. package/dist/lib/git.js +2 -1
  52. package/dist/lib/git.js.map +1 -1
  53. package/dist/lib/mcp-validator.d.ts.map +1 -1
  54. package/dist/lib/mcp-validator.js +2 -1
  55. package/dist/lib/mcp-validator.js.map +1 -1
  56. package/dist/lib/scanner.d.ts.map +1 -1
  57. package/dist/lib/scanner.js +2 -12
  58. package/dist/lib/scanner.js.map +1 -1
  59. package/dist/lib/security.d.ts.map +1 -1
  60. package/dist/lib/security.js +12 -1
  61. package/dist/lib/security.js.map +1 -1
  62. package/dist/lib/session-recovery.d.ts.map +1 -1
  63. package/dist/lib/session-recovery.js +25 -2
  64. package/dist/lib/session-recovery.js.map +1 -1
  65. package/dist/lib/utils.d.ts +15 -0
  66. package/dist/lib/utils.d.ts.map +1 -0
  67. package/dist/lib/utils.js +78 -0
  68. package/dist/lib/utils.js.map +1 -0
  69. package/package.json +2 -2
@@ -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, params, authToken) {
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
- return new Promise((resolve) => {
81
+ const MAX_BODY_SIZE = 10 * 1024 * 1024;
82
+ return new Promise((resolve, reject) => {
82
83
  let data = "";
83
- req.on("data", (chunk) => { data += chunk.toString(); });
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.join(PROJECTS_DIR, filePath);
218
- const target = fs.existsSync(fullPath) ? fullPath : filePath;
219
- if (!fs.existsSync(target))
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
- let line = lines[f.line - 1];
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 = line;
1065
- line = line.replace(pat.regex, "[REDACTED]");
1066
- if (line !== before) {
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] = line;
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: true });
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 {