@misha_misha/agentwatch 0.1.1 → 0.1.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 (2) hide show
  1. package/dist/index.js +154 -39
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -190,6 +190,40 @@ var init_version = __esm({
190
190
  }
191
191
  });
192
192
 
193
+ // src/util/backfill.ts
194
+ var backfill_exports = {};
195
+ __export(backfill_exports, {
196
+ BACKFILL_MAX_AGE_MS: () => BACKFILL_MAX_AGE_MS,
197
+ backfillStartOffset: () => backfillStartOffset,
198
+ setStaleSkipEnabled: () => setStaleSkipEnabled
199
+ });
200
+ import { statSync } from "fs";
201
+ function mtimeMs(file) {
202
+ try {
203
+ return statSync(file).mtimeMs;
204
+ } catch {
205
+ return 0;
206
+ }
207
+ }
208
+ function setStaleSkipEnabled(enabled) {
209
+ staleSkipEnabled = enabled;
210
+ }
211
+ function backfillStartOffset(file, size, isInitialAdd, backfillBytes) {
212
+ if (!isInitialAdd) return size;
213
+ if (staleSkipEnabled && mtimeMs(file) < Date.now() - BACKFILL_MAX_AGE_MS) {
214
+ return size;
215
+ }
216
+ return Math.max(0, size - backfillBytes);
217
+ }
218
+ var BACKFILL_MAX_AGE_MS, staleSkipEnabled;
219
+ var init_backfill = __esm({
220
+ "src/util/backfill.ts"() {
221
+ "use strict";
222
+ BACKFILL_MAX_AGE_MS = 48 * 60 * 60 * 1e3;
223
+ staleSkipEnabled = false;
224
+ }
225
+ });
226
+
193
227
  // src/util/compaction.ts
194
228
  function contextWindow() {
195
229
  const env = process.env.AGENTWATCH_CONTEXT_WINDOW;
@@ -412,7 +446,7 @@ __export(workspace_exports, {
412
446
  claudeProjectsDir: () => claudeProjectsDir,
413
447
  detectWorkspaceRoot: () => detectWorkspaceRoot
414
448
  });
415
- import { existsSync as existsSync3, statSync } from "fs";
449
+ import { existsSync as existsSync3, statSync as statSync2 } from "fs";
416
450
  import { homedir as homedir3 } from "os";
417
451
  import { join as join4 } from "path";
418
452
  function detectWorkspaceRoot() {
@@ -436,7 +470,7 @@ function claudeProjectsDir() {
436
470
  }
437
471
  function isDir(p) {
438
472
  try {
439
- return existsSync3(p) && statSync(p).isDirectory();
473
+ return existsSync3(p) && statSync2(p).isDirectory();
440
474
  } catch {
441
475
  return false;
442
476
  }
@@ -764,6 +798,38 @@ var init_jsonl_stream = __esm({
764
798
  }
765
799
  });
766
800
 
801
+ // src/util/backfill-queue.ts
802
+ function createBackfillQueue(processFile) {
803
+ const queue = [];
804
+ let draining2 = false;
805
+ const drain = () => {
806
+ const file = queue.shift();
807
+ if (file === void 0) {
808
+ draining2 = false;
809
+ return;
810
+ }
811
+ try {
812
+ processFile(file);
813
+ } catch {
814
+ }
815
+ setImmediate(drain);
816
+ };
817
+ return {
818
+ enqueue(file) {
819
+ queue.push(file);
820
+ if (!draining2) {
821
+ draining2 = true;
822
+ setImmediate(drain);
823
+ }
824
+ }
825
+ };
826
+ }
827
+ var init_backfill_queue = __esm({
828
+ "src/util/backfill-queue.ts"() {
829
+ "use strict";
830
+ }
831
+ });
832
+
767
833
  // src/util/parse-errors.ts
768
834
  function createParseErrorTracker(agent, sink, options = {}) {
769
835
  const entries = /* @__PURE__ */ new Map();
@@ -818,7 +884,7 @@ var init_parse_errors = __esm({
818
884
 
819
885
  // src/adapters/claude-code.ts
820
886
  import chokidar2 from "chokidar";
821
- import { existsSync as existsSync4, statSync as statSync2 } from "fs";
887
+ import { existsSync as existsSync4, statSync as statSync3 } from "fs";
822
888
  import { basename as basename2, sep } from "path";
823
889
  function capMap(m, max) {
824
890
  while (m.size > max) {
@@ -850,7 +916,7 @@ function startClaudeAdapter(sink) {
850
916
  const size = safeSize(file);
851
917
  let cursor = cursors.get(file);
852
918
  if (!cursor) {
853
- const start2 = isInitialAdd ? Math.max(0, size - BACKFILL_BYTES) : size;
919
+ const start2 = backfillStartOffset(file, size, isInitialAdd, BACKFILL_BYTES);
854
920
  cursor = { offset: start2 };
855
921
  cursors.set(file, cursor);
856
922
  }
@@ -913,8 +979,16 @@ function startClaudeAdapter(sink) {
913
979
  }
914
980
  }
915
981
  };
916
- watcher2.on("add", (f) => process2(f, true));
982
+ let scanReady = false;
983
+ const backfillQueue = createBackfillQueue((f) => process2(f, true));
984
+ watcher2.on("add", (f) => {
985
+ if (scanReady) process2(f, true);
986
+ else backfillQueue.enqueue(f);
987
+ });
917
988
  watcher2.on("change", (f) => process2(f, false));
989
+ watcher2.on("ready", () => {
990
+ scanReady = true;
991
+ });
918
992
  watcher2.on("error", (err) => {
919
993
  if (typeof err === "object" && err !== null) {
920
994
  const code = err.code;
@@ -1012,7 +1086,7 @@ function capBytes(s, max = MAX_TOOL_RESULT_BYTES) {
1012
1086
  }
1013
1087
  function safeSize(file) {
1014
1088
  try {
1015
- return statSync2(file).size;
1089
+ return statSync3(file).size;
1016
1090
  } catch {
1017
1091
  return 0;
1018
1092
  }
@@ -1214,6 +1288,8 @@ var init_claude_code = __esm({
1214
1288
  init_cost();
1215
1289
  init_recent_writes();
1216
1290
  init_jsonl_stream();
1291
+ init_backfill();
1292
+ init_backfill_queue();
1217
1293
  init_parse_errors();
1218
1294
  MAX_PENDING_TOOL_USES = 5e3;
1219
1295
  pendingToolUses = /* @__PURE__ */ new Map();
@@ -1323,7 +1399,7 @@ var init_openclaw_cron = __esm({
1323
1399
 
1324
1400
  // src/adapters/openclaw.ts
1325
1401
  import chokidar3 from "chokidar";
1326
- import { existsSync as existsSync5, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
1402
+ import { existsSync as existsSync5, readFileSync as readFileSync3, statSync as statSync4 } from "fs";
1327
1403
  import { basename as basename3, join as join5, sep as sep2 } from "path";
1328
1404
  import { homedir as homedir4 } from "os";
1329
1405
  function capMap2(m, max) {
@@ -1383,8 +1459,16 @@ function startOpenClawAdapter(sink) {
1383
1459
  if (!sessionRe.test(f)) return;
1384
1460
  processSession(f, initial, cursors, normalized, parseErrors);
1385
1461
  };
1386
- sessionsWatcher.on("add", (f) => handleSession(f, true));
1462
+ let sessionsReady = false;
1463
+ const sessionBackfillQueue = createBackfillQueue((f) => handleSession(f, true));
1464
+ sessionsWatcher.on("add", (f) => {
1465
+ if (sessionsReady) handleSession(f, true);
1466
+ else sessionBackfillQueue.enqueue(f);
1467
+ });
1387
1468
  sessionsWatcher.on("change", (f) => handleSession(f, false));
1469
+ sessionsWatcher.on("ready", () => {
1470
+ sessionsReady = true;
1471
+ });
1388
1472
  sessionsWatcher.on("error", swallow);
1389
1473
  stoppers.push(() => {
1390
1474
  void sessionsWatcher.close();
@@ -1524,8 +1608,7 @@ function streamLines(file, isInitialAdd, cursors, onLine) {
1524
1608
  const size = safeSize2(file);
1525
1609
  let cursor = cursors.get(file);
1526
1610
  if (!cursor) {
1527
- const backfillStart = Math.max(0, size - BACKFILL_BYTES2);
1528
- cursor = { offset: isInitialAdd ? backfillStart : size };
1611
+ cursor = { offset: backfillStartOffset(file, size, isInitialAdd, BACKFILL_BYTES2) };
1529
1612
  cursors.set(file, cursor);
1530
1613
  }
1531
1614
  if (size <= cursor.offset) return;
@@ -1550,7 +1633,7 @@ function swallow(err) {
1550
1633
  }
1551
1634
  function safeSize2(file) {
1552
1635
  try {
1553
- return statSync3(file).size;
1636
+ return statSync4(file).size;
1554
1637
  } catch {
1555
1638
  return 0;
1556
1639
  }
@@ -1738,6 +1821,8 @@ var init_openclaw = __esm({
1738
1821
  init_schema();
1739
1822
  init_ids();
1740
1823
  init_jsonl_stream();
1824
+ init_backfill();
1825
+ init_backfill_queue();
1741
1826
  init_parse_errors();
1742
1827
  init_openclaw_cron();
1743
1828
  sessionCwd = /* @__PURE__ */ new Map();
@@ -1757,7 +1842,7 @@ import {
1757
1842
  readFileSync as readFileSync4,
1758
1843
  existsSync as existsSync6,
1759
1844
  readdirSync,
1760
- statSync as statSync4
1845
+ statSync as statSync5
1761
1846
  } from "fs";
1762
1847
  import { homedir as homedir5 } from "os";
1763
1848
  import { join as join6 } from "path";
@@ -1939,7 +2024,7 @@ function discoverCursorrules(workspace) {
1939
2024
  if (name === "node_modules") continue;
1940
2025
  const dir = join6(workspace, name);
1941
2026
  try {
1942
- if (!statSync4(dir).isDirectory()) continue;
2027
+ if (!statSync5(dir).isDirectory()) continue;
1943
2028
  } catch {
1944
2029
  continue;
1945
2030
  }
@@ -2220,7 +2305,7 @@ var init_gemini = __esm({
2220
2305
 
2221
2306
  // src/adapters/codex.ts
2222
2307
  import chokidar6 from "chokidar";
2223
- import { existsSync as existsSync8, statSync as statSync5 } from "fs";
2308
+ import { existsSync as existsSync8, statSync as statSync6 } from "fs";
2224
2309
  import { basename as basename5, join as join8, sep as sep4 } from "path";
2225
2310
  import os5 from "os";
2226
2311
  function codexSessionsDir(home = os5.homedir()) {
@@ -2243,7 +2328,7 @@ function startCodexAdapter(sink) {
2243
2328
  const size = safeSize3(file);
2244
2329
  let cursor = cursors.get(file);
2245
2330
  if (!cursor) {
2246
- const start2 = isInitialAdd ? Math.max(0, size - BACKFILL_BYTES3) : size;
2331
+ const start2 = backfillStartOffset(file, size, isInitialAdd, BACKFILL_BYTES3);
2247
2332
  cursor = {
2248
2333
  offset: start2,
2249
2334
  project: "",
@@ -2364,8 +2449,16 @@ function startCodexAdapter(sink) {
2364
2449
  }
2365
2450
  }
2366
2451
  };
2367
- watcher2.on("add", (f) => handle(f, true));
2452
+ let scanReady = false;
2453
+ const backfillQueue = createBackfillQueue((f) => handle(f, true));
2454
+ watcher2.on("add", (f) => {
2455
+ if (scanReady) handle(f, true);
2456
+ else backfillQueue.enqueue(f);
2457
+ });
2368
2458
  watcher2.on("change", (f) => handle(f, false));
2459
+ watcher2.on("ready", () => {
2460
+ scanReady = true;
2461
+ });
2369
2462
  watcher2.on("error", (err) => {
2370
2463
  if (typeof err === "object" && err !== null) {
2371
2464
  const code = err.code;
@@ -2501,7 +2594,7 @@ function truncate5(s, n) {
2501
2594
  }
2502
2595
  function safeSize3(file) {
2503
2596
  try {
2504
- return statSync5(file).size;
2597
+ return statSync6(file).size;
2505
2598
  } catch {
2506
2599
  return 0;
2507
2600
  }
@@ -2515,6 +2608,8 @@ var init_codex = __esm({
2515
2608
  init_cost();
2516
2609
  init_spawn_tracker();
2517
2610
  init_jsonl_stream();
2611
+ init_backfill();
2612
+ init_backfill_queue();
2518
2613
  init_parse_errors();
2519
2614
  BACKFILL_BYTES3 = 512 * 1024;
2520
2615
  MAX_PENDING = 2e3;
@@ -4781,7 +4876,7 @@ var init_claude_hooks = __esm({
4781
4876
 
4782
4877
  // src/git/correlate.ts
4783
4878
  import { spawnSync as spawnSync3 } from "child_process";
4784
- import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync6 } from "fs";
4879
+ import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync7 } from "fs";
4785
4880
  import { join as join12, resolve } from "path";
4786
4881
  function runGit(args, opts = {}) {
4787
4882
  const verb = args[0];
@@ -4814,7 +4909,7 @@ function findProjectGitRoot(workspaceRoot, projectName) {
4814
4909
  if (entry !== projectName) continue;
4815
4910
  const candidate = join12(workspaceRoot, entry);
4816
4911
  try {
4817
- const s = statSync6(candidate);
4912
+ const s = statSync7(candidate);
4818
4913
  if (!s.isDirectory()) continue;
4819
4914
  } catch {
4820
4915
  continue;
@@ -6024,6 +6119,19 @@ function buildStore(db2) {
6024
6119
  const rows = sessionEventsStmt.all(sessionId);
6025
6120
  return rows.map(rowToEvent);
6026
6121
  },
6122
+ budgetRollup() {
6123
+ const todayStart = /* @__PURE__ */ new Date();
6124
+ todayStart.setUTCHours(0, 0, 0, 0);
6125
+ const monthAgo = new Date(Date.now() - 30 * 864e5).toISOString();
6126
+ const day = db2.prepare("SELECT SUM(cost_usd) AS c FROM events WHERE ts >= ?").get(todayStart.toISOString());
6127
+ const top = db2.prepare(
6128
+ "SELECT session_id, cost_usd FROM sessions WHERE last_ts >= ? ORDER BY cost_usd DESC LIMIT 1"
6129
+ ).get(monthAgo);
6130
+ return {
6131
+ dayCost: day.c ?? 0,
6132
+ maxSession: top ? { id: top.session_id, cost: top.cost_usd } : { id: "", cost: 0 }
6133
+ };
6134
+ },
6027
6135
  listRecentEvents(opts = {}) {
6028
6136
  const limit = clamp4(opts.limit ?? 1e3, 1, 5e4);
6029
6137
  const order = opts.order === "asc" ? "ASC" : "DESC";
@@ -6677,7 +6785,7 @@ __export(server_exports2, {
6677
6785
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6678
6786
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6679
6787
  import { z } from "zod";
6680
- import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync7 } from "fs";
6788
+ import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync8 } from "fs";
6681
6789
  import { homedir as homedir12 } from "os";
6682
6790
  import { join as join16 } from "path";
6683
6791
  import Database4 from "better-sqlite3";
@@ -7000,7 +7108,7 @@ function listAllSessions() {
7000
7108
  for (const f of readdirSync3(projPath)) {
7001
7109
  if (!f.endsWith(".jsonl")) continue;
7002
7110
  const full = join16(projPath, f);
7003
- const s = statSync7(full);
7111
+ const s = statSync8(full);
7004
7112
  out.push({
7005
7113
  agent: "claude-code",
7006
7114
  sessionId: f.replace(/\.jsonl$/, ""),
@@ -7068,7 +7176,7 @@ function walkOpenClaw(root, out) {
7068
7176
  const full = join16(sessionsDir, name);
7069
7177
  let st;
7070
7178
  try {
7071
- st = statSync7(full);
7179
+ st = statSync8(full);
7072
7180
  } catch {
7073
7181
  continue;
7074
7182
  }
@@ -7088,7 +7196,7 @@ function walkHermes(dbPath, out) {
7088
7196
  const db2 = openHermesDb(dbPath);
7089
7197
  if (!db2) return;
7090
7198
  try {
7091
- const st = statSync7(dbPath);
7199
+ const st = statSync8(dbPath);
7092
7200
  const rows = db2.prepare(
7093
7201
  "SELECT id, source, message_count, started_at, ended_at FROM sessions"
7094
7202
  ).all();
@@ -7131,7 +7239,7 @@ function walkGemini(dir, out) {
7131
7239
  const full = join16(chatsDir, name);
7132
7240
  let st;
7133
7241
  try {
7134
- st = statSync7(full);
7242
+ st = statSync8(full);
7135
7243
  } catch {
7136
7244
  continue;
7137
7245
  }
@@ -7159,7 +7267,7 @@ function walkCodex(dir, out) {
7159
7267
  const full = join16(dir, name);
7160
7268
  let st;
7161
7269
  try {
7162
- st = statSync7(full);
7270
+ st = statSync8(full);
7163
7271
  } catch {
7164
7272
  continue;
7165
7273
  }
@@ -7313,7 +7421,7 @@ var init_install = __esm({
7313
7421
  });
7314
7422
 
7315
7423
  // src/daemon/log-rotate.ts
7316
- import { closeSync as closeSync2, openSync as openSync2, renameSync, statSync as statSync8, writeSync } from "fs";
7424
+ import { closeSync as closeSync2, openSync as openSync2, renameSync, statSync as statSync9, writeSync } from "fs";
7317
7425
  import { dirname as dirname5 } from "path";
7318
7426
  import { mkdirSync as mkdirSync4 } from "fs";
7319
7427
  var DEFAULT_MAX_BYTES, RotatingLogStream;
@@ -7332,7 +7440,7 @@ var init_log_rotate = __esm({
7332
7440
  mkdirSync4(dirname5(this.path), { recursive: true });
7333
7441
  this.fd = openSync2(this.path, "a");
7334
7442
  try {
7335
- this.bytes = statSync8(this.path).size;
7443
+ this.bytes = statSync9(this.path).size;
7336
7444
  } catch {
7337
7445
  this.bytes = 0;
7338
7446
  }
@@ -8191,6 +8299,9 @@ function computeBudgetStatus(events, budgets = loadBudgets(), now = /* @__PURE__
8191
8299
  const t = new Date(e.ts).getTime();
8192
8300
  if (t >= todayMs) dayCost += c;
8193
8301
  }
8302
+ return budgetStatusFromTotals(dayCost, maxSession, budgets);
8303
+ }
8304
+ function budgetStatusFromTotals(dayCost, maxSession, budgets = loadBudgets()) {
8194
8305
  const status = {
8195
8306
  sessionCost: maxSession.cost,
8196
8307
  dayCost,
@@ -8204,6 +8315,9 @@ function computeBudgetStatus(events, budgets = loadBudgets(), now = /* @__PURE__
8204
8315
  return status;
8205
8316
  }
8206
8317
 
8318
+ // src/ui/App.tsx
8319
+ init_backfill();
8320
+
8207
8321
  // src/util/otel.ts
8208
8322
  init_compaction();
8209
8323
  init_version();
@@ -9266,6 +9380,7 @@ function App() {
9266
9380
  try {
9267
9381
  const seed = store.listRecentEvents({ limit: 500, order: "desc" });
9268
9382
  if (seed.length > 0) dispatch({ type: "events-batch", events: seed });
9383
+ setStaleSkipEnabled(seed.length > 0);
9269
9384
  } catch {
9270
9385
  }
9271
9386
  }
@@ -9337,19 +9452,17 @@ function App() {
9337
9452
  const agentFiltered = state.filterAgent ? state.events.filter((e) => e.agent === state.filterAgent) : state.events;
9338
9453
  const filtered = state.searchQuery ? agentFiltered.filter((e) => matchesQuery(e, state.searchQuery)) : agentFiltered;
9339
9454
  const eventsRef = state.events;
9455
+ const [rollupTick, setRollupTick] = useState(0);
9456
+ useEffect(() => {
9457
+ if (!store) return;
9458
+ const id = setInterval(() => setRollupTick((t) => t + 1), 2500);
9459
+ return () => clearInterval(id);
9460
+ }, [store]);
9340
9461
  const budgetStatus = useMemo(() => {
9341
9462
  if (!store) return computeBudgetStatus(eventsRef);
9342
- const todayStart = /* @__PURE__ */ new Date();
9343
- todayStart.setUTCHours(0, 0, 0, 0);
9344
- const since = todayStart.toISOString();
9345
- const monthAgo = new Date(Date.now() - 30 * 864e5).toISOString();
9346
- const events = store.listRecentEvents({
9347
- sinceTs: monthAgo < since ? monthAgo : since,
9348
- limit: 5e4,
9349
- order: "asc"
9350
- });
9351
- return computeBudgetStatus(events);
9352
- }, [eventsRef, store]);
9463
+ const { dayCost, maxSession } = store.budgetRollup();
9464
+ return budgetStatusFromTotals(dayCost, maxSession);
9465
+ }, [store ? rollupTick : eventsRef, store]);
9353
9466
  const anomalies = useMemo(() => {
9354
9467
  const source = store ? store.listRecentEvents({ limit: 5e3, order: "desc" }) : eventsRef;
9355
9468
  const out = /* @__PURE__ */ new Map();
@@ -9390,7 +9503,7 @@ function App() {
9390
9503
  }
9391
9504
  }
9392
9505
  return out;
9393
- }, [eventsRef, store]);
9506
+ }, [store ? rollupTick : eventsRef, store]);
9394
9507
  const budgetBreachKey = [
9395
9508
  budgetStatus.breachedSession ?? "",
9396
9509
  budgetStatus.dayBreach ? "day" : ""
@@ -9842,6 +9955,8 @@ if (arg === "serve") {
9842
9955
  try {
9843
9956
  const seed = store.listRecentEvents({ limit: 5e3, order: "desc" });
9844
9957
  for (let i = seed.length - 1; i >= 0; i--) addEventToServer2(server, seed[i]);
9958
+ const { setStaleSkipEnabled: setStaleSkipEnabled2 } = await Promise.resolve().then(() => (init_backfill(), backfill_exports));
9959
+ setStaleSkipEnabled2(seed.length > 0);
9845
9960
  } catch (err) {
9846
9961
  process.stderr.write(`[agentwatch] ring seed skipped: ${String(err)}
9847
9962
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misha_misha/agentwatch",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Local-only observability + control plane for every AI coding agent on your machine. TUI live tail + browser dashboard on localhost. Unified timeline across Claude Code, Codex, Gemini CLI, Cursor, Hermes, OpenClaw — token + cost accounting, compaction + anomaly detection, SVG call graphs, diff attribution, agent-aware replay, MCP server mode, OpenTelemetry exporter. No cloud, no telemetry, no sign-in.",
5
5
  "type": "module",
6
6
  "author": "Misha Nefedov",