averecion-lite 1.6.4 → 1.7.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.
@@ -1215,6 +1215,11 @@ body {
1215
1215
  color: var(--success);
1216
1216
  }
1217
1217
 
1218
+ .reset-dropdown {
1219
+ position: relative;
1220
+ display: inline-block;
1221
+ }
1222
+
1218
1223
  .reset-btn {
1219
1224
  background: var(--bg-card);
1220
1225
  border: 1px solid var(--border);
@@ -1231,16 +1236,158 @@ body {
1231
1236
  color: var(--text-primary);
1232
1237
  }
1233
1238
 
1234
- .reset-btn.confirming {
1235
- background: rgba(239, 68, 68, 0.15);
1236
- border-color: var(--danger);
1237
- color: var(--danger);
1238
- animation: pulse-border 1s ease-in-out infinite;
1239
+ .reset-menu {
1240
+ position: absolute;
1241
+ top: calc(100% + 6px);
1242
+ right: 0;
1243
+ background: var(--bg-card);
1244
+ border: 1px solid var(--border);
1245
+ border-radius: 12px;
1246
+ padding: 0.5rem;
1247
+ min-width: 280px;
1248
+ z-index: 200;
1249
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
1250
+ animation: dropdownFade 0.15s ease-out;
1251
+ }
1252
+
1253
+ .reset-menu.hidden {
1254
+ display: none;
1255
+ }
1256
+
1257
+ @keyframes dropdownFade {
1258
+ from { opacity: 0; transform: translateY(-4px); }
1259
+ to { opacity: 1; transform: translateY(0); }
1260
+ }
1261
+
1262
+ .reset-option {
1263
+ display: flex;
1264
+ align-items: center;
1265
+ gap: 0.75rem;
1266
+ width: 100%;
1267
+ padding: 0.75rem;
1268
+ background: none;
1269
+ border: 1px solid transparent;
1270
+ border-radius: 8px;
1271
+ cursor: pointer;
1272
+ text-align: left;
1273
+ transition: all 0.15s;
1274
+ color: var(--text-primary);
1275
+ }
1276
+
1277
+ .reset-option:hover {
1278
+ background: rgba(255,255,255,0.05);
1279
+ border-color: var(--border);
1280
+ }
1281
+
1282
+ .reset-option-icon {
1283
+ font-size: 1.3rem;
1284
+ flex-shrink: 0;
1285
+ }
1286
+
1287
+ .reset-option-text {
1288
+ display: flex;
1289
+ flex-direction: column;
1290
+ gap: 2px;
1291
+ }
1292
+
1293
+ .reset-option-title {
1294
+ font-weight: 600;
1295
+ font-size: 0.9rem;
1296
+ }
1297
+
1298
+ .reset-option-desc {
1299
+ font-size: 0.75rem;
1300
+ color: var(--text-secondary);
1301
+ }
1302
+
1303
+ .reset-toast {
1304
+ position: fixed;
1305
+ bottom: 1.5rem;
1306
+ left: 50%;
1307
+ transform: translateX(-50%);
1308
+ background: var(--bg-card);
1309
+ border: 1px solid var(--border);
1310
+ border-radius: 10px;
1311
+ padding: 0.75rem 1.25rem;
1312
+ font-size: 0.9rem;
1313
+ color: var(--text-primary);
1314
+ z-index: 300;
1315
+ box-shadow: 0 4px 24px rgba(0,0,0,0.4);
1316
+ animation: toastSlide 0.3s ease-out;
1317
+ }
1318
+
1319
+ .reset-toast.success { border-color: var(--success); }
1320
+ .reset-toast.info { border-color: var(--accent); }
1321
+
1322
+ @keyframes toastSlide {
1323
+ from { opacity: 0; transform: translateX(-50%) translateY(12px); }
1324
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
1325
+ }
1326
+
1327
+ .reset-confirm-overlay {
1328
+ position: fixed;
1329
+ inset: 0;
1330
+ background: rgba(0,0,0,0.5);
1331
+ display: flex;
1332
+ align-items: center;
1333
+ justify-content: center;
1334
+ z-index: 250;
1335
+ animation: dropdownFade 0.15s ease-out;
1336
+ }
1337
+
1338
+ .reset-confirm-box {
1339
+ background: var(--bg-card);
1340
+ border: 1px solid var(--border);
1341
+ border-radius: 16px;
1342
+ padding: 1.5rem;
1343
+ max-width: 360px;
1344
+ width: 90%;
1345
+ text-align: center;
1346
+ }
1347
+
1348
+ .reset-confirm-box h3 {
1349
+ margin: 0 0 0.5rem;
1350
+ font-size: 1.1rem;
1351
+ }
1352
+
1353
+ .reset-confirm-box p {
1354
+ margin: 0 0 1.25rem;
1355
+ color: var(--text-secondary);
1356
+ font-size: 0.85rem;
1357
+ }
1358
+
1359
+ .reset-confirm-actions {
1360
+ display: flex;
1361
+ gap: 0.75rem;
1362
+ justify-content: center;
1363
+ }
1364
+
1365
+ .reset-confirm-actions button {
1366
+ padding: 0.5rem 1.25rem;
1367
+ border-radius: 8px;
1368
+ font-size: 0.85rem;
1369
+ cursor: pointer;
1370
+ border: 1px solid var(--border);
1371
+ transition: all 0.15s;
1372
+ }
1373
+
1374
+ .btn-confirm-yes {
1375
+ background: var(--accent);
1376
+ color: white;
1377
+ border-color: var(--accent) !important;
1378
+ }
1379
+
1380
+ .btn-confirm-yes:hover {
1381
+ opacity: 0.9;
1382
+ }
1383
+
1384
+ .btn-confirm-cancel {
1385
+ background: var(--bg-card);
1386
+ color: var(--text-secondary);
1239
1387
  }
1240
1388
 
1241
- @keyframes pulse-border {
1242
- 0%, 100% { border-color: var(--danger); }
1243
- 50% { border-color: rgba(239, 68, 68, 0.4); }
1389
+ .btn-confirm-cancel:hover {
1390
+ color: var(--text-primary);
1244
1391
  }
1245
1392
 
1246
1393
  @keyframes slideInLeft {
@@ -1523,3 +1670,126 @@ body {
1523
1670
  margin-top: 0.5rem;
1524
1671
  }
1525
1672
  }
1673
+
1674
+ /* Archives Section */
1675
+ .archives-section {
1676
+ margin-top: 1.5rem;
1677
+ }
1678
+
1679
+ .archives-header {
1680
+ display: flex;
1681
+ align-items: center;
1682
+ justify-content: space-between;
1683
+ margin-bottom: 0.75rem;
1684
+ }
1685
+
1686
+ .archives-header h2 {
1687
+ font-size: 1rem;
1688
+ font-weight: 600;
1689
+ margin: 0;
1690
+ }
1691
+
1692
+ .archives-toggle-btn {
1693
+ background: var(--bg-card);
1694
+ border: 1px solid var(--border);
1695
+ color: var(--text-secondary);
1696
+ padding: 0.35rem 0.75rem;
1697
+ border-radius: 6px;
1698
+ font-size: 0.8rem;
1699
+ cursor: pointer;
1700
+ transition: all 0.15s;
1701
+ }
1702
+
1703
+ .archives-toggle-btn:hover {
1704
+ color: var(--text-primary);
1705
+ border-color: var(--accent);
1706
+ }
1707
+
1708
+ .archives-content.hidden {
1709
+ display: none;
1710
+ }
1711
+
1712
+ .archives-empty {
1713
+ color: var(--text-muted);
1714
+ font-size: 0.85rem;
1715
+ text-align: center;
1716
+ padding: 1rem;
1717
+ }
1718
+
1719
+ .archive-card {
1720
+ background: var(--bg-card);
1721
+ border: 1px solid var(--border);
1722
+ border-radius: 10px;
1723
+ padding: 1rem;
1724
+ margin-bottom: 0.75rem;
1725
+ transition: border-color 0.15s;
1726
+ }
1727
+
1728
+ .archive-card:hover {
1729
+ border-color: var(--accent);
1730
+ }
1731
+
1732
+ .archive-card-header {
1733
+ display: flex;
1734
+ justify-content: space-between;
1735
+ align-items: center;
1736
+ margin-bottom: 0.5rem;
1737
+ }
1738
+
1739
+ .archive-date {
1740
+ font-weight: 600;
1741
+ font-size: 0.9rem;
1742
+ }
1743
+
1744
+ .archive-duration {
1745
+ font-size: 0.8rem;
1746
+ color: var(--text-muted);
1747
+ }
1748
+
1749
+ .archive-stats {
1750
+ display: flex;
1751
+ gap: 1rem;
1752
+ flex-wrap: wrap;
1753
+ }
1754
+
1755
+ .archive-stat {
1756
+ font-size: 0.8rem;
1757
+ display: flex;
1758
+ align-items: center;
1759
+ gap: 0.3rem;
1760
+ }
1761
+
1762
+ .archive-stat .safe { color: var(--success); }
1763
+ .archive-stat .flagged { color: var(--warning); }
1764
+ .archive-stat .attack { color: var(--danger); }
1765
+ .archive-stat .reviewed { color: var(--accent); }
1766
+ .archive-stat .total { color: var(--text-secondary); }
1767
+
1768
+ .archive-actions {
1769
+ display: flex;
1770
+ gap: 0.5rem;
1771
+ margin-top: 0.75rem;
1772
+ padding-top: 0.75rem;
1773
+ border-top: 1px solid var(--border);
1774
+ }
1775
+
1776
+ .archive-actions button {
1777
+ background: var(--bg-card-hover);
1778
+ border: 1px solid var(--border);
1779
+ color: var(--text-secondary);
1780
+ padding: 0.3rem 0.6rem;
1781
+ border-radius: 6px;
1782
+ font-size: 0.75rem;
1783
+ cursor: pointer;
1784
+ transition: all 0.15s;
1785
+ }
1786
+
1787
+ .archive-actions button:hover {
1788
+ color: var(--text-primary);
1789
+ border-color: var(--accent);
1790
+ }
1791
+
1792
+ .archive-actions .btn-delete-archive:hover {
1793
+ border-color: var(--danger);
1794
+ color: var(--danger);
1795
+ }
package/dashboard/dash.js CHANGED
@@ -899,6 +899,7 @@
899
899
  initResetButton();
900
900
  initThreatModal();
901
901
  initSecurityAccordion();
902
+ initArchives();
902
903
 
903
904
  function initNotificationToggle() {
904
905
  const btn = document.getElementById("btn-notifications");
@@ -1069,40 +1070,203 @@
1069
1070
 
1070
1071
  function initResetButton() {
1071
1072
  const btn = document.getElementById("btn-reset");
1072
- if (!btn) return;
1073
+ const menu = document.getElementById("reset-menu");
1074
+ if (!btn || !menu) return;
1073
1075
 
1074
- let confirmTimeout = null;
1076
+ btn.addEventListener("click", (e) => {
1077
+ e.stopPropagation();
1078
+ menu.classList.toggle("hidden");
1079
+ });
1075
1080
 
1076
- btn.addEventListener("click", async () => {
1077
- if (btn.classList.contains("confirming")) {
1078
- clearTimeout(confirmTimeout);
1079
- btn.classList.remove("confirming");
1080
- btn.textContent = "🔄 Resetting...";
1081
- btn.disabled = true;
1081
+ document.addEventListener("click", (e) => {
1082
+ if (!e.target.closest("#reset-dropdown")) {
1083
+ menu.classList.add("hidden");
1084
+ }
1085
+ });
1082
1086
 
1083
- try {
1084
- const headers = { "Content-Type": "application/json" };
1085
- if (SECRET) headers["X-Lite-Secret"] = SECRET;
1086
- await fetch("/lite-reset", { method: "POST", headers });
1087
+ const CONFIRM_MESSAGES = {
1088
+ "new-session": { title: "Start New Session?", desc: "Counters will reset to zero. Only new activity will be shown. Your log files are not deleted." },
1089
+ "rebuild": { title: "Rebuild from Logs?", desc: "All log files will be re-scanned from the beginning and counters recalculated." },
1090
+ "archive-reset": { title: "Archive & Reset?", desc: "The current session will be saved to an archive file, then counters reset to zero." },
1091
+ };
1092
+
1093
+ const TOAST_MESSAGES = {
1094
+ "new-session": "New session started — counters at zero",
1095
+ "rebuild": "All logs re-scanned and rebuilt",
1096
+ "archive-reset": "Session archived and reset",
1097
+ };
1098
+
1099
+ menu.querySelectorAll(".reset-option").forEach(option => {
1100
+ option.addEventListener("click", () => {
1101
+ const mode = option.dataset.mode;
1102
+ menu.classList.add("hidden");
1103
+ showResetConfirm(mode);
1104
+ });
1105
+ });
1106
+
1107
+ function showResetConfirm(mode) {
1108
+ const info = CONFIRM_MESSAGES[mode];
1109
+ const overlay = document.createElement("div");
1110
+ overlay.className = "reset-confirm-overlay";
1111
+ overlay.innerHTML = `
1112
+ <div class="reset-confirm-box">
1113
+ <h3>${info.title}</h3>
1114
+ <p>${info.desc}</p>
1115
+ <div class="reset-confirm-actions">
1116
+ <button class="btn-confirm-cancel" data-testid="btn-confirm-cancel">Cancel</button>
1117
+ <button class="btn-confirm-yes" data-testid="btn-confirm-yes">Confirm</button>
1118
+ </div>
1119
+ </div>
1120
+ `;
1121
+
1122
+ document.body.appendChild(overlay);
1123
+
1124
+ overlay.querySelector(".btn-confirm-cancel").addEventListener("click", () => overlay.remove());
1125
+ overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
1126
+
1127
+ overlay.querySelector(".btn-confirm-yes").addEventListener("click", async () => {
1128
+ overlay.remove();
1129
+ await executeReset(mode);
1130
+ });
1131
+ }
1132
+
1133
+ async function executeReset(mode) {
1134
+ btn.disabled = true;
1135
+ btn.textContent = "🔄 Resetting...";
1136
+
1137
+ try {
1138
+ const headers = { "Content-Type": "application/json" };
1139
+ if (SECRET) headers["X-Lite-Secret"] = SECRET;
1140
+ const res = await fetch("/lite-reset", { method: "POST", headers, body: JSON.stringify({ mode }) });
1141
+ const data = await res.json();
1142
+
1143
+ if (data.success) {
1087
1144
  await loadDashboard();
1088
- btn.textContent = "Reset Done";
1089
- setTimeout(() => {
1090
- btn.textContent = "🔄 Reset";
1091
- btn.disabled = false;
1092
- }, 2000);
1093
- } catch (err) {
1094
- console.error("Reset failed:", err);
1095
- btn.textContent = "🔄 Reset";
1096
- btn.disabled = false;
1145
+ showToast(TOAST_MESSAGES[mode] || "Reset complete", "success");
1146
+ } else {
1147
+ showToast("Reset failed — " + (data.error || "unknown error"), "info");
1097
1148
  }
1149
+ } catch (err) {
1150
+ console.error("Reset failed:", err);
1151
+ showToast("Reset failed — network error", "info");
1152
+ }
1153
+
1154
+ btn.textContent = "🔄 Reset ▾";
1155
+ btn.disabled = false;
1156
+ }
1157
+ }
1158
+
1159
+ function showToast(message, type) {
1160
+ const existing = document.querySelector(".reset-toast");
1161
+ if (existing) existing.remove();
1162
+
1163
+ const toast = document.createElement("div");
1164
+ toast.className = `reset-toast ${type}`;
1165
+ toast.textContent = message;
1166
+ document.body.appendChild(toast);
1167
+ setTimeout(() => toast.remove(), 3000);
1168
+ }
1169
+
1170
+ function initArchives() {
1171
+ const toggleBtn = document.getElementById("btn-toggle-archives");
1172
+ const content = document.getElementById("archives-content");
1173
+ if (!toggleBtn || !content) return;
1174
+
1175
+ toggleBtn.addEventListener("click", () => {
1176
+ const isHidden = content.classList.contains("hidden");
1177
+ if (isHidden) {
1178
+ content.classList.remove("hidden");
1179
+ toggleBtn.textContent = "Hide";
1180
+ loadArchives();
1098
1181
  } else {
1099
- btn.classList.add("confirming");
1100
- btn.textContent = "⚠️ Click again to confirm";
1101
- confirmTimeout = setTimeout(() => {
1102
- btn.classList.remove("confirming");
1103
- btn.textContent = "🔄 Reset";
1104
- }, 3000);
1182
+ content.classList.add("hidden");
1183
+ toggleBtn.textContent = "Show";
1105
1184
  }
1106
1185
  });
1107
1186
  }
1187
+
1188
+ async function loadArchives() {
1189
+ const list = document.getElementById("archives-list");
1190
+ const emptyEl = document.getElementById("archives-empty");
1191
+ if (!list) return;
1192
+
1193
+ try {
1194
+ const headers = {};
1195
+ if (SECRET) headers["X-Lite-Secret"] = SECRET;
1196
+ const res = await fetch("/api/archives", { headers });
1197
+ if (!res.ok) throw new Error("Failed to load archives");
1198
+ const archives = await res.json();
1199
+
1200
+ if (archives.length === 0) {
1201
+ if (emptyEl) emptyEl.style.display = "block";
1202
+ list.innerHTML = emptyEl ? emptyEl.outerHTML : '<p class="archives-empty">No archived sessions yet.</p>';
1203
+ return;
1204
+ }
1205
+
1206
+ if (emptyEl) emptyEl.style.display = "none";
1207
+
1208
+ list.innerHTML = archives.map(a => {
1209
+ const start = new Date(a.startTime);
1210
+ const dateStr = start.toLocaleDateString() + " " + start.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
1211
+ return `
1212
+ <div class="archive-card" data-id="${a.id}" data-testid="archive-${a.id}">
1213
+ <div class="archive-card-header">
1214
+ <span class="archive-date">${dateStr}</span>
1215
+ <span class="archive-duration">${a.duration}</span>
1216
+ </div>
1217
+ <div class="archive-stats">
1218
+ <span class="archive-stat"><span class="safe">${a.totals.safe}</span> safe</span>
1219
+ <span class="archive-stat"><span class="flagged">${a.totals.flagged}</span> flagged</span>
1220
+ <span class="archive-stat"><span class="attack">${a.totals.attacks}</span> attacks</span>
1221
+ <span class="archive-stat"><span class="reviewed">${a.totals.reviewed}</span> reviewed</span>
1222
+ <span class="archive-stat"><span class="total">${a.totals.total}</span> total</span>
1223
+ </div>
1224
+ <div class="archive-actions">
1225
+ <button class="btn-export-archive" data-id="${a.id}" data-testid="btn-export-${a.id}">📥 Export JSON</button>
1226
+ <button class="btn-delete-archive" data-id="${a.id}" data-testid="btn-delete-${a.id}">🗑️ Delete</button>
1227
+ </div>
1228
+ </div>
1229
+ `;
1230
+ }).join("");
1231
+
1232
+ list.querySelectorAll(".btn-export-archive").forEach(btn => {
1233
+ btn.addEventListener("click", async () => {
1234
+ const id = btn.dataset.id;
1235
+ try {
1236
+ const headers = {};
1237
+ if (SECRET) headers["X-Lite-Secret"] = SECRET;
1238
+ const res = await fetch(`/api/archives/${id}`, { headers });
1239
+ const data = await res.json();
1240
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
1241
+ const url = URL.createObjectURL(blob);
1242
+ const a = document.createElement("a");
1243
+ a.href = url;
1244
+ a.download = `${id}.json`;
1245
+ a.click();
1246
+ URL.revokeObjectURL(url);
1247
+ } catch (err) {
1248
+ console.error("Export failed:", err);
1249
+ }
1250
+ });
1251
+ });
1252
+
1253
+ list.querySelectorAll(".btn-delete-archive").forEach(btn => {
1254
+ btn.addEventListener("click", async () => {
1255
+ const id = btn.dataset.id;
1256
+ if (!confirm("Delete this archived session?")) return;
1257
+ try {
1258
+ const headers = {};
1259
+ if (SECRET) headers["X-Lite-Secret"] = SECRET;
1260
+ await fetch(`/api/archives/${id}`, { method: "DELETE", headers });
1261
+ loadArchives();
1262
+ showToast("Archive deleted", "info");
1263
+ } catch (err) {
1264
+ console.error("Delete failed:", err);
1265
+ }
1266
+ });
1267
+ });
1268
+ } catch (err) {
1269
+ console.error("Failed to load archives:", err);
1270
+ }
1271
+ }
1108
1272
  })();
@@ -112,9 +112,34 @@
112
112
  <button id="btn-notifications" class="notification-toggle" data-testid="btn-notifications" title="Enable browser notifications">
113
113
  🔔 Notifications
114
114
  </button>
115
- <button id="btn-reset" class="reset-btn" data-testid="btn-reset" title="Clear all stats and start fresh">
116
- 🔄 Reset
117
- </button>
115
+ <div class="reset-dropdown" id="reset-dropdown">
116
+ <button id="btn-reset" class="reset-btn" data-testid="btn-reset" title="Reset options">
117
+ 🔄 Reset ▾
118
+ </button>
119
+ <div class="reset-menu hidden" id="reset-menu">
120
+ <button class="reset-option" data-mode="new-session" data-testid="btn-reset-new-session">
121
+ <span class="reset-option-icon">🆕</span>
122
+ <div class="reset-option-text">
123
+ <span class="reset-option-title">New Session</span>
124
+ <span class="reset-option-desc">Counters to zero, only new events shown</span>
125
+ </div>
126
+ </button>
127
+ <button class="reset-option" data-mode="rebuild" data-testid="btn-reset-rebuild">
128
+ <span class="reset-option-icon">🔁</span>
129
+ <div class="reset-option-text">
130
+ <span class="reset-option-title">Rebuild</span>
131
+ <span class="reset-option-desc">Re-scan all logs, recalculate everything</span>
132
+ </div>
133
+ </button>
134
+ <button class="reset-option" data-mode="archive-reset" data-testid="btn-reset-archive">
135
+ <span class="reset-option-icon">📦</span>
136
+ <div class="reset-option-text">
137
+ <span class="reset-option-title">Archive & Reset</span>
138
+ <span class="reset-option-desc">Save this session, then start fresh</span>
139
+ </div>
140
+ </button>
141
+ </div>
142
+ </div>
118
143
  <div class="status-badge protected" id="global-status" data-testid="badge-status">
119
144
  ● Monitoring
120
145
  </div>
@@ -319,6 +344,18 @@
319
344
  </div>
320
345
  </div>
321
346
  </section>
347
+
348
+ <section class="archives-section" id="archives-section" data-testid="archives-section">
349
+ <div class="archives-header">
350
+ <h2>📦 Session Archives</h2>
351
+ <button class="archives-toggle-btn" id="btn-toggle-archives" data-testid="btn-toggle-archives">Show</button>
352
+ </div>
353
+ <div class="archives-content hidden" id="archives-content">
354
+ <div class="archives-list" id="archives-list" data-testid="archives-list">
355
+ <p class="archives-empty" id="archives-empty">No archived sessions yet. Use "Archive & Reset" to save a session.</p>
356
+ </div>
357
+ </div>
358
+ </section>
322
359
  </div>
323
360
  </div>
324
361
  </main>
@@ -1,8 +1,6 @@
1
1
  import { EventEmitter } from "events";
2
2
  export declare class LogWatcher extends EventEmitter {
3
- private watchedFile;
4
- private watcher;
5
- private filePosition;
3
+ private watchedFiles;
6
4
  private activeRuns;
7
5
  private pollInterval;
8
6
  private recentContentHashes;
@@ -10,11 +8,11 @@ export declare class LogWatcher extends EventEmitter {
10
8
  constructor();
11
9
  start(): void;
12
10
  reset(): void;
11
+ bookmark(): void;
13
12
  stop(): void;
14
- private getCurrentLogFile;
15
- private checkForLogFile;
13
+ private findAllLogFiles;
14
+ private scanAndWatch;
16
15
  private watchFile;
17
- private scheduleRewatch;
18
16
  private readNewLines;
19
17
  private processLine;
20
18
  private parseLogLine;
@@ -1 +1 @@
1
- {"version":3,"file":"log-watcher.d.ts","sourceRoot":"","sources":["../log-watcher.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAsDtC,qBAAa,UAAW,SAAQ,YAAY;IAC1C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,qBAAqB,CAA+B;;IAY5D,KAAK,IAAI,IAAI;IAUb,KAAK,IAAI,IAAI;IAUb,IAAI,IAAI,IAAI;IAeZ,OAAO,CAAC,iBAAiB;IAsBzB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,SAAS;IAuCjB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,YAAY;IA+BpB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAwMpB,OAAO,CAAC,kBAAkB;IA+D1B,OAAO,CAAC,qBAAqB;IAwE7B,OAAO,CAAC,eAAe;IAuCvB,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,eAAe;IAmCvB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,gBAAgB;CAWzB;AAID,wBAAgB,eAAe,IAAI,UAAU,CAM5C;AAED,wBAAgB,cAAc,IAAI,IAAI,CAKrC;AAED,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD"}
1
+ {"version":3,"file":"log-watcher.d.ts","sourceRoot":"","sources":["../log-watcher.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAuDtC,qBAAa,UAAW,SAAQ,YAAY;IAC1C,OAAO,CAAC,YAAY,CAAyE;IAC7F,OAAO,CAAC,UAAU,CAAgC;IAClD,OAAO,CAAC,YAAY,CAA+B;IACnD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,qBAAqB,CAA+B;;IAY5D,KAAK,IAAI,IAAI;IAKb,KAAK,IAAI,IAAI;IAUb,QAAQ,IAAI,IAAI;IAMhB,IAAI,IAAI,IAAI;IAeZ,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,SAAS;IA4BjB,OAAO,CAAC,YAAY;IAgCpB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,YAAY;IAwMpB,OAAO,CAAC,kBAAkB;IA+D1B,OAAO,CAAC,qBAAqB;IAwE7B,OAAO,CAAC,eAAe;IAuCvB,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,eAAe;IAmCvB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,gBAAgB;CAWzB;AAID,wBAAgB,eAAe,IAAI,UAAU,CAM5C;AAED,wBAAgB,cAAc,IAAI,IAAI,CAKrC;AAED,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD"}
@@ -33,8 +33,9 @@ const os = __importStar(require("os"));
33
33
  const events_1 = require("events");
34
34
  const storage_1 = require("./storage");
35
35
  const CLAWDBOT_LOG_DIRS = [
36
- path.join(os.homedir(), ".clawdbot", "logs"),
37
36
  "/tmp/clawdbot",
37
+ path.join(os.homedir(), ".clawdbot", "logs"),
38
+ path.join(os.homedir(), ".pm2", "logs"),
38
39
  ];
39
40
  const DANGEROUS_PATTERNS = [
40
41
  { pattern: /rm\s+-rf?\s+\//, reason: "Dangerous rm command (root delete)" },
@@ -64,9 +65,7 @@ const INJECTION_PATTERNS = [
64
65
  { pattern: /delete\s+everything/i, reason: "Prompt injection: destructive command" },
65
66
  ];
66
67
  class LogWatcher extends events_1.EventEmitter {
67
- watchedFile = null;
68
- watcher = null;
69
- filePosition = 0;
68
+ watchedFiles = new Map();
70
69
  activeRuns = new Map();
71
70
  pollInterval = null;
72
71
  recentContentHashes = new Map();
@@ -82,28 +81,29 @@ class LogWatcher extends events_1.EventEmitter {
82
81
  }, 30000);
83
82
  }
84
83
  start() {
85
- const logFile = this.getCurrentLogFile();
86
- if (!logFile) {
87
- console.log(`[LogWatcher] No log files found in ${CLAWDBOT_LOG_DIRS.join(", ")} — polling every 5s...`);
88
- this.pollInterval = setInterval(() => this.checkForLogFile(), 5000);
89
- return;
90
- }
91
- this.watchFile(logFile);
84
+ this.scanAndWatch();
85
+ this.pollInterval = setInterval(() => this.scanAndWatch(), 5000);
92
86
  }
93
87
  reset() {
94
- this.filePosition = 0;
95
88
  this.activeRuns.clear();
96
89
  this.recentContentHashes.clear();
97
- if (this.watchedFile) {
98
- console.log(`[LogWatcher] Reset — will re-read ${this.watchedFile} from beginning`);
99
- this.readNewLines();
90
+ for (const [filePath, state] of this.watchedFiles) {
91
+ state.position = 0;
92
+ console.log(`[LogWatcher] Reset — will re-read ${filePath} from beginning`);
93
+ this.readNewLines(filePath);
100
94
  }
101
95
  }
96
+ bookmark() {
97
+ this.activeRuns.clear();
98
+ this.recentContentHashes.clear();
99
+ console.log(`[LogWatcher] Session bookmarked — only new events from now`);
100
+ }
102
101
  stop() {
103
- if (this.watcher) {
104
- this.watcher.close();
105
- this.watcher = null;
102
+ for (const [, state] of this.watchedFiles) {
103
+ if (state.watcher)
104
+ state.watcher.close();
106
105
  }
106
+ this.watchedFiles.clear();
107
107
  if (this.pollInterval) {
108
108
  clearInterval(this.pollInterval);
109
109
  this.pollInterval = null;
@@ -113,95 +113,74 @@ class LogWatcher extends events_1.EventEmitter {
113
113
  this.dedupeCleanupInterval = null;
114
114
  }
115
115
  }
116
- getCurrentLogFile() {
116
+ findAllLogFiles() {
117
+ const files = [];
117
118
  for (const dir of CLAWDBOT_LOG_DIRS) {
118
119
  if (!fs.existsSync(dir))
119
120
  continue;
120
- const commandsLog = path.join(dir, "commands.log");
121
- if (fs.existsSync(commandsLog))
122
- return commandsLog;
123
- const today = new Date().toISOString().split("T")[0];
124
- const dateLog = path.join(dir, `clawdbot-${today}.log`);
125
- if (fs.existsSync(dateLog))
126
- return dateLog;
127
121
  try {
128
- const files = fs.readdirSync(dir)
129
- .filter(f => f.endsWith(".log"))
130
- .map(f => ({ name: f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
131
- .sort((a, b) => b.mtime - a.mtime);
132
- if (files.length > 0)
133
- return path.join(dir, files[0].name);
122
+ const entries = fs.readdirSync(dir).filter(f => f.endsWith(".log"));
123
+ for (const entry of entries) {
124
+ files.push(path.join(dir, entry));
125
+ }
134
126
  }
135
127
  catch { }
136
128
  }
137
- return null;
129
+ return files;
138
130
  }
139
- checkForLogFile() {
140
- const logFile = this.getCurrentLogFile();
141
- if (logFile) {
142
- if (this.pollInterval) {
143
- clearInterval(this.pollInterval);
144
- this.pollInterval = null;
131
+ scanAndWatch() {
132
+ const logFiles = this.findAllLogFiles();
133
+ if (logFiles.length === 0 && this.watchedFiles.size === 0) {
134
+ return;
135
+ }
136
+ for (const filePath of logFiles) {
137
+ if (!this.watchedFiles.has(filePath)) {
138
+ this.watchFile(filePath);
145
139
  }
146
- this.watchFile(logFile);
147
140
  }
148
141
  }
149
142
  watchFile(logFile) {
150
- if (this.watchedFile === logFile)
143
+ if (this.watchedFiles.has(logFile))
151
144
  return;
152
- if (this.watcher) {
153
- this.watcher.close();
154
- }
155
- this.watchedFile = logFile;
156
- this.filePosition = 0;
145
+ const state = { position: 0, watcher: null };
146
+ this.watchedFiles.set(logFile, state);
157
147
  console.log(`[LogWatcher] Watching ${logFile} from beginning`);
158
148
  try {
159
- this.readNewLines();
160
- this.watcher = fs.watch(logFile, (eventType) => {
149
+ this.readNewLines(logFile);
150
+ state.watcher = fs.watch(logFile, (eventType) => {
161
151
  if (eventType === "change") {
162
- this.readNewLines();
152
+ this.readNewLines(logFile);
163
153
  }
164
154
  });
165
- this.watcher.on("error", (err) => {
166
- console.error("[LogWatcher] Watch error:", err);
167
- this.scheduleRewatch();
155
+ state.watcher.on("error", (err) => {
156
+ console.error(`[LogWatcher] Watch error on ${logFile}:`, err);
157
+ if (state.watcher)
158
+ state.watcher.close();
159
+ this.watchedFiles.delete(logFile);
168
160
  });
169
- setInterval(() => {
170
- const currentLogFile = this.getCurrentLogFile();
171
- if (currentLogFile && currentLogFile !== this.watchedFile) {
172
- console.log("[LogWatcher] Log file rotated, switching to new file");
173
- this.watchFile(currentLogFile);
174
- }
175
- }, 60000);
176
161
  }
177
162
  catch (err) {
178
- console.error("[LogWatcher] Failed to watch file:", err);
179
- this.scheduleRewatch();
163
+ console.error(`[LogWatcher] Failed to watch ${logFile}:`, err);
164
+ this.watchedFiles.delete(logFile);
180
165
  }
181
166
  }
182
- scheduleRewatch() {
183
- setTimeout(() => {
184
- const logFile = this.getCurrentLogFile();
185
- if (logFile)
186
- this.watchFile(logFile);
187
- }, 5000);
188
- }
189
- readNewLines() {
190
- if (!this.watchedFile)
167
+ readNewLines(filePath) {
168
+ const state = this.watchedFiles.get(filePath);
169
+ if (!state)
191
170
  return;
192
171
  try {
193
- const stat = fs.statSync(this.watchedFile);
194
- if (stat.size < this.filePosition) {
195
- console.log("[LogWatcher] Log file truncated or rotated, resetting position");
196
- this.filePosition = 0;
172
+ const stat = fs.statSync(filePath);
173
+ if (stat.size < state.position) {
174
+ console.log(`[LogWatcher] ${path.basename(filePath)} truncated, resetting position`);
175
+ state.position = 0;
197
176
  }
198
- if (stat.size <= this.filePosition)
177
+ if (stat.size <= state.position)
199
178
  return;
200
- const fd = fs.openSync(this.watchedFile, "r");
201
- const buffer = Buffer.alloc(stat.size - this.filePosition);
202
- fs.readSync(fd, buffer, 0, buffer.length, this.filePosition);
179
+ const fd = fs.openSync(filePath, "r");
180
+ const buffer = Buffer.alloc(stat.size - state.position);
181
+ fs.readSync(fd, buffer, 0, buffer.length, state.position);
203
182
  fs.closeSync(fd);
204
- this.filePosition = stat.size;
183
+ state.position = stat.size;
205
184
  const lines = buffer.toString("utf-8").split("\n");
206
185
  for (const line of lines) {
207
186
  if (line.trim()) {
@@ -210,7 +189,7 @@ class LogWatcher extends events_1.EventEmitter {
210
189
  }
211
190
  }
212
191
  catch (err) {
213
- console.error("[LogWatcher] Error reading file:", err);
192
+ console.error(`[LogWatcher] Error reading ${filePath}:`, err);
214
193
  }
215
194
  }
216
195
  processLine(line) {
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,WAAW,EAAkC,MAAM,WAAW,CAAC;AAElG,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAC;QAAC,uBAAuB,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;IACzJ,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,QAAQ,EAAE;QAAE,oBAAoB,EAAE,OAAO,CAAC;QAAC,kBAAkB,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;IAClG,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACrE,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,EAAE,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChF,WAAW,EAAE,WAAW,EAAE,CAAC;IAC3B,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,wBAAwB,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClH,gBAAgB,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACrF;AAED,QAAA,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAqG,CAAC;AAC1I,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,OAAO,OAAO,QAAmB;AAE1E,wBAAgB,UAAU,CAAC,SAAS,SAAK,GAAG,WAAW,CA6CtD"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsE,WAAW,EAAkC,MAAM,WAAW,CAAC;AAE5I,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAC;QAAC,uBAAuB,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;IACzJ,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7C,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE,QAAQ,EAAE;QAAE,oBAAoB,EAAE,OAAO,CAAC;QAAC,kBAAkB,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;IAClG,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACrE,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,EAAE,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChF,WAAW,EAAE,WAAW,EAAE,CAAC;IAC3B,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,wBAAwB,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClH,gBAAgB,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACrF;AAED,QAAA,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAqG,CAAC;AAC1I,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,OAAO,OAAO,QAAmB;AAE1E,wBAAgB,UAAU,CAAC,SAAS,SAAK,GAAG,WAAW,CA6CtD"}
package/dist/metrics.js CHANGED
@@ -6,8 +6,8 @@ const storage_1 = require("./storage");
6
6
  const session = { approved: 0, blocked: 0, manualApproved: 0, highRiskIntercepts: 0, promptInjectionDetected: 0 };
7
7
  function incrementMetric(k) { session[k]++; }
8
8
  function getMetrics(hoursBack = 24) {
9
- const allEvents = (0, storage_1.getEvents)(0);
10
- const recentEvents = (0, storage_1.getEvents)(hoursBack);
9
+ const allEvents = (0, storage_1.getFilteredEvents)(0);
10
+ const recentEvents = (0, storage_1.getFilteredEvents)(hoursBack);
11
11
  const kpis = { approved: 0, blocked: 0, manualApproved: 0, highRiskIntercepts: 0, promptInjectionDetected: 0, dangerDetected: 0 };
12
12
  const egressCounts = {};
13
13
  const trusted = new Set(), unknownBlocked = new Set();
@@ -55,7 +55,7 @@ function getMetrics(hoursBack = 24) {
55
55
  skills: { trusted: trusted.size, unknownBlocked: unknownBlocked.size, outdated: 0 },
56
56
  instance: { reverseProxyHardened: true, dashboardLocalOnly: true, secretsEnvOnly: !!process.env.LITE_ADAPTER_SECRET },
57
57
  timeline: { hours, approved, blocked }, cost: { inputTokens, outputTokens, estimatedUSD },
58
- lastActions: (0, storage_1.getLastEvents)(10),
58
+ lastActions: (0, storage_1.getFilteredLastEvents)(10),
59
59
  config: (0, storage_1.getConfig)(),
60
60
  pendingApprovals: (0, storage_1.getPendingApprovals)()
61
61
  };
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AAuCA,wBAAsB,WAAW,CAAC,IAAI,SAAO,EAAE,IAAI,SAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuK9E;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBhD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../server.ts"],"names":[],"mappings":"AAuCA,wBAAsB,WAAW,CAAC,IAAI,SAAO,EAAE,IAAI,SAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiM9E;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBhD"}
package/dist/server.js CHANGED
@@ -95,28 +95,59 @@ async function startServer(port = 4321, host = "0.0.0.0") {
95
95
  const success = (0, storage_1.resolveApproval)(id, approved === true);
96
96
  res.json({ success, id, approved });
97
97
  });
98
- app.post("/lite-reset", localOnly, validateSecret, (_req, res) => {
99
- const fs = require("fs");
100
- const os = require("os");
101
- const logsDir = path.join(os.homedir(), ".clawguard", "logs");
98
+ app.post("/lite-reset", localOnly, validateSecret, (req, res) => {
99
+ const mode = req.body?.mode || "new-session";
100
+ const watcher = (0, log_watcher_1.getLogWatcher)();
102
101
  try {
103
- if (fs.existsSync(logsDir)) {
104
- const files = fs.readdirSync(logsDir).filter((f) => f.endsWith(".jsonl"));
105
- for (const file of files) {
106
- fs.unlinkSync(path.join(logsDir, file));
107
- }
102
+ if (mode === "new-session") {
103
+ (0, storage_1.setSessionStart)(new Date().toISOString());
104
+ if (watcher)
105
+ watcher.bookmark();
106
+ broadcastToClients({ type: "metrics", data: (0, metrics_1.getMetrics)(24) });
107
+ res.json({ success: true, mode, message: "New session started — counters reset to zero" });
108
+ }
109
+ else if (mode === "rebuild") {
110
+ (0, storage_1.setSessionStart)(null);
111
+ if (watcher)
112
+ watcher.reset();
113
+ broadcastToClients({ type: "metrics", data: (0, metrics_1.getMetrics)(24) });
114
+ res.json({ success: true, mode, message: "All logs re-scanned and counters rebuilt" });
115
+ }
116
+ else if (mode === "archive-reset") {
117
+ const archive = (0, storage_1.archiveCurrentSession)();
118
+ (0, storage_1.setSessionStart)(new Date().toISOString());
119
+ if (watcher)
120
+ watcher.bookmark();
121
+ broadcastToClients({ type: "metrics", data: (0, metrics_1.getMetrics)(24) });
122
+ res.json({ success: true, mode, message: "Session archived and reset", archiveId: archive.id });
108
123
  }
109
- const watcher = (0, log_watcher_1.getLogWatcher)();
110
- if (watcher) {
111
- watcher.reset();
124
+ else {
125
+ res.status(400).json({ error: `Unknown reset mode: ${mode}` });
112
126
  }
113
- broadcastToClients({ type: "metrics", data: (0, metrics_1.getMetrics)(24) });
114
- res.json({ success: true, message: "Session reset" });
115
127
  }
116
128
  catch (err) {
117
129
  res.status(500).json({ error: "Failed to reset" });
118
130
  }
119
131
  });
132
+ app.get("/api/archives", localOnly, validateSecret, (_req, res) => {
133
+ res.json((0, storage_1.listArchives)());
134
+ });
135
+ app.get("/api/archives/:id", localOnly, validateSecret, (req, res) => {
136
+ const archive = (0, storage_1.getArchive)(req.params.id);
137
+ if (!archive) {
138
+ res.status(404).json({ error: "Archive not found" });
139
+ return;
140
+ }
141
+ res.json(archive);
142
+ });
143
+ app.delete("/api/archives/:id", localOnly, validateSecret, (req, res) => {
144
+ const success = (0, storage_1.deleteArchive)(req.params.id);
145
+ if (!success) {
146
+ res.status(404).json({ error: "Archive not found" });
147
+ return;
148
+ }
149
+ res.json({ success: true });
150
+ });
120
151
  app.get("/", localOnly, (_req, res) => res.sendFile(path.join(dashboardDir, "landing.html")));
121
152
  app.get("/clawguard", localOnly, (_req, res) => res.sendFile(path.join(dashboardDir, "index.html")));
122
153
  app.get("/lite-dash", localOnly, (_req, res) => res.redirect("/clawguard"));
package/dist/storage.d.ts CHANGED
@@ -25,4 +25,26 @@ export declare function getPendingApprovals(): {
25
25
  createdAt: string;
26
26
  }[];
27
27
  export declare function resolveApproval(id: string, approved: boolean): boolean;
28
+ export declare function setSessionStart(ts: string | null): void;
29
+ export declare function getSessionStart(): string | null;
30
+ export declare function getFilteredEvents(hoursBack?: number): ActionEvent[];
31
+ export declare function getFilteredLastEvents(count?: number): ActionEvent[];
32
+ export interface SessionArchive {
33
+ id: string;
34
+ startTime: string;
35
+ endTime: string;
36
+ duration: string;
37
+ totals: {
38
+ safe: number;
39
+ flagged: number;
40
+ attacks: number;
41
+ reviewed: number;
42
+ total: number;
43
+ };
44
+ events: ActionEvent[];
45
+ }
46
+ export declare function archiveCurrentSession(): SessionArchive;
47
+ export declare function listArchives(): Omit<SessionArchive, "events">[];
48
+ export declare function getArchive(id: string): SessionArchive | null;
49
+ export declare function deleteArchive(id: string): boolean;
28
50
  //# sourceMappingURL=storage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../storage.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA2CD,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,QAW7C;AAED,wBAAgB,SAAS,CAAC,SAAS,SAAK,GAAG,WAAW,EAAE,CAIvD;AAED,wBAAgB,aAAa,CAAC,KAAK,SAAK,GAAG,WAAW,EAAE,CAEvD;AAED,wBAAgB,SAAS,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,wBAAwB,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAOzJ;AAED,wBAAgB,mBAAmB,IAAI;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAAE,CAgBvG;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAYtE"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../storage.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA4CD,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,QAW7C;AAED,wBAAgB,SAAS,CAAC,SAAS,SAAK,GAAG,WAAW,EAAE,CAIvD;AAED,wBAAgB,aAAa,CAAC,KAAK,SAAK,GAAG,WAAW,EAAE,CAEvD;AAED,wBAAgB,SAAS,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,wBAAwB,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAOzJ;AAED,wBAAgB,mBAAmB,IAAI;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAAE,CAgBvG;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAYtE;AAID,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEvD;AAED,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAE/C;AAED,wBAAgB,iBAAiB,CAAC,SAAS,SAAI,GAAG,WAAW,EAAE,CAM9D;AAED,wBAAgB,qBAAqB,CAAC,KAAK,SAAK,GAAG,WAAW,EAAE,CAM/D;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5F,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,wBAAgB,qBAAqB,IAAI,cAAc,CAwCtD;AAED,wBAAgB,YAAY,IAAI,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,EAAE,CAY/D;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAQ5D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CASjD"}
package/dist/storage.js CHANGED
@@ -29,6 +29,14 @@ exports.getLastEvents = getLastEvents;
29
29
  exports.getConfig = getConfig;
30
30
  exports.getPendingApprovals = getPendingApprovals;
31
31
  exports.resolveApproval = resolveApproval;
32
+ exports.setSessionStart = setSessionStart;
33
+ exports.getSessionStart = getSessionStart;
34
+ exports.getFilteredEvents = getFilteredEvents;
35
+ exports.getFilteredLastEvents = getFilteredLastEvents;
36
+ exports.archiveCurrentSession = archiveCurrentSession;
37
+ exports.listArchives = listArchives;
38
+ exports.getArchive = getArchive;
39
+ exports.deleteArchive = deleteArchive;
32
40
  const fs = __importStar(require("fs"));
33
41
  const path = __importStar(require("path"));
34
42
  const os = __importStar(require("os"));
@@ -36,6 +44,7 @@ const CLAWGUARD_DIR = path.join(os.homedir(), ".clawguard");
36
44
  const LOGS_DIR = path.join(CLAWGUARD_DIR, "logs");
37
45
  const CONFIG_FILE = path.join(CLAWGUARD_DIR, "config.json");
38
46
  const PENDING_DIR = path.join(CLAWGUARD_DIR, "pending");
47
+ const ARCHIVES_DIR = path.join(CLAWGUARD_DIR, "archives");
39
48
  function ensureDir() {
40
49
  if (!fs.existsSync(CLAWGUARD_DIR))
41
50
  fs.mkdirSync(CLAWGUARD_DIR, { recursive: true });
@@ -134,3 +143,100 @@ function resolveApproval(id, approved) {
134
143
  catch { }
135
144
  return false;
136
145
  }
146
+ let sessionStartTime = null;
147
+ function setSessionStart(ts) {
148
+ sessionStartTime = ts;
149
+ }
150
+ function getSessionStart() {
151
+ return sessionStartTime;
152
+ }
153
+ function getFilteredEvents(hoursBack = 0) {
154
+ let events = hoursBack <= 0 ? loadLogsFromJsonl() : loadLogsFromJsonl().filter(e => e.ts >= new Date(Date.now() - hoursBack * 3600000).toISOString());
155
+ if (sessionStartTime) {
156
+ events = events.filter(e => e.ts >= sessionStartTime);
157
+ }
158
+ return events;
159
+ }
160
+ function getFilteredLastEvents(count = 10) {
161
+ let events = loadLogsFromJsonl();
162
+ if (sessionStartTime) {
163
+ events = events.filter(e => e.ts >= sessionStartTime);
164
+ }
165
+ return events.slice(-count).reverse();
166
+ }
167
+ function archiveCurrentSession() {
168
+ if (!fs.existsSync(ARCHIVES_DIR))
169
+ fs.mkdirSync(ARCHIVES_DIR, { recursive: true });
170
+ const allEvents = sessionStartTime
171
+ ? loadLogsFromJsonl().filter(e => e.ts >= sessionStartTime)
172
+ : loadLogsFromJsonl();
173
+ const now = new Date().toISOString();
174
+ const start = sessionStartTime || (allEvents.length > 0 ? allEvents[0].ts : now);
175
+ let safe = 0, flagged = 0, attacks = 0, reviewed = 0;
176
+ for (const e of allEvents) {
177
+ if (e.decision === "approved")
178
+ safe++;
179
+ else if (e.decision === "manual")
180
+ reviewed++;
181
+ else if (e.decision === "blocked") {
182
+ const isInjection = e.reason.includes("promptInjection") || e.reason.includes("Prompt injection");
183
+ if (isInjection)
184
+ attacks++;
185
+ else
186
+ flagged++;
187
+ }
188
+ }
189
+ const startDate = new Date(start);
190
+ const endDate = new Date(now);
191
+ const diffMs = endDate.getTime() - startDate.getTime();
192
+ const hours = Math.floor(diffMs / 3600000);
193
+ const mins = Math.floor((diffMs % 3600000) / 60000);
194
+ const duration = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
195
+ const archive = {
196
+ id: `session-${now.replace(/[:.]/g, "-")}`,
197
+ startTime: start,
198
+ endTime: now,
199
+ duration,
200
+ totals: { safe, flagged, attacks, reviewed, total: allEvents.length },
201
+ events: allEvents,
202
+ };
203
+ const fileName = `${archive.id}.json`;
204
+ fs.writeFileSync(path.join(ARCHIVES_DIR, fileName), JSON.stringify(archive, null, 2));
205
+ return archive;
206
+ }
207
+ function listArchives() {
208
+ if (!fs.existsSync(ARCHIVES_DIR))
209
+ return [];
210
+ const files = fs.readdirSync(ARCHIVES_DIR).filter(f => f.endsWith(".json")).sort().reverse();
211
+ const archives = [];
212
+ for (const file of files) {
213
+ try {
214
+ const data = JSON.parse(fs.readFileSync(path.join(ARCHIVES_DIR, file), "utf-8"));
215
+ const { events, ...meta } = data;
216
+ archives.push(meta);
217
+ }
218
+ catch { }
219
+ }
220
+ return archives;
221
+ }
222
+ function getArchive(id) {
223
+ const filePath = path.join(ARCHIVES_DIR, `${id}.json`);
224
+ try {
225
+ if (fs.existsSync(filePath)) {
226
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
227
+ }
228
+ }
229
+ catch { }
230
+ return null;
231
+ }
232
+ function deleteArchive(id) {
233
+ const filePath = path.join(ARCHIVES_DIR, `${id}.json`);
234
+ try {
235
+ if (fs.existsSync(filePath)) {
236
+ fs.unlinkSync(filePath);
237
+ return true;
238
+ }
239
+ }
240
+ catch { }
241
+ return false;
242
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "averecion-lite",
3
- "version": "1.6.4",
3
+ "version": "1.7.0",
4
4
  "description": "Real-time AI agent monitoring - watches logs, detects dangerous commands and prompt injection attempts",
5
5
  "author": "Averecion <hello@averecion.com>",
6
6
  "homepage": "https://github.com/averecion/clawguard#readme",