@agentreel/agent 0.1.0 → 0.1.4

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/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ function ensureAgentreelDir() {
17
17
  mkdirSync(AGENTREEL_DIR, { recursive: true });
18
18
  mkdirSync(QUEUE_DIR, { recursive: true });
19
19
  }
20
- var HOME, AGENTREEL_DIR, DB_PATH, CONFIG_PATH, QUEUE_DIR, LOG_PATH, CLAUDE_DIR, CLAUDE_SETTINGS_PATH;
20
+ var HOME, AGENTREEL_DIR, DB_PATH, CONFIG_PATH, QUEUE_DIR, LOG_PATH, DAEMON_PID_PATH, DAEMON_LOG_PATH, CLAUDE_DIR, CLAUDE_SETTINGS_PATH;
21
21
  var init_paths = __esm({
22
22
  "src/paths.ts"() {
23
23
  "use strict";
@@ -27,6 +27,8 @@ var init_paths = __esm({
27
27
  CONFIG_PATH = join(AGENTREEL_DIR, "config.json");
28
28
  QUEUE_DIR = join(AGENTREEL_DIR, "queue");
29
29
  LOG_PATH = join(AGENTREEL_DIR, "agent.log");
30
+ DAEMON_PID_PATH = join(AGENTREEL_DIR, "daemon.pid");
31
+ DAEMON_LOG_PATH = join(AGENTREEL_DIR, "daemon.log");
30
32
  CLAUDE_DIR = join(HOME, ".claude");
31
33
  CLAUDE_SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
32
34
  }
@@ -119,23 +121,8 @@ var init_install = __esm({
119
121
  }
120
122
  });
121
123
 
122
- // src/cli.ts
123
- import { Command } from "commander";
124
-
125
- // src/commands/init.ts
126
- init_install();
127
- import { realpathSync } from "fs";
128
- import { fileURLToPath } from "url";
129
- import { sep } from "path";
130
- import pc from "picocolors";
131
-
132
124
  // src/config.ts
133
- init_paths();
134
125
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, chmodSync } from "fs";
135
- var DEFAULT = {
136
- apiBaseUrl: "https://api.agentreel.dev",
137
- schemaVersion: 1
138
- };
139
126
  function readConfig() {
140
127
  if (!existsSync2(CONFIG_PATH)) return { ...DEFAULT };
141
128
  try {
@@ -156,11 +143,20 @@ function writeConfig(cfg) {
156
143
  } catch {
157
144
  }
158
145
  }
146
+ var DEFAULT;
147
+ var init_config = __esm({
148
+ "src/config.ts"() {
149
+ "use strict";
150
+ init_paths();
151
+ DEFAULT = {
152
+ apiBaseUrl: "https://api.agentreel.dev",
153
+ schemaVersion: 1
154
+ };
155
+ }
156
+ });
159
157
 
160
158
  // src/db.ts
161
- init_paths();
162
159
  import Database from "better-sqlite3";
163
- var _db = null;
164
160
  function getDb() {
165
161
  if (_db) return _db;
166
162
  ensureAgentreelDir();
@@ -194,8 +190,36 @@ function migrate(db) {
194
190
  );
195
191
  CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id, ts);
196
192
  CREATE INDEX IF NOT EXISTS idx_events_pending ON events(uploaded_at) WHERE uploaded_at IS NULL;
193
+ CREATE TABLE IF NOT EXISTS meta (
194
+ key TEXT PRIMARY KEY,
195
+ value TEXT
196
+ );
197
197
  `);
198
198
  }
199
+ function getMeta(key) {
200
+ const db = getDb();
201
+ const row = db.prepare(`SELECT value FROM meta WHERE key = ?`).get(key);
202
+ return row?.value ?? null;
203
+ }
204
+ function setMeta(key, value) {
205
+ const db = getDb();
206
+ if (value == null) {
207
+ db.prepare(`DELETE FROM meta WHERE key = ?`).run(key);
208
+ return;
209
+ }
210
+ db.prepare(
211
+ `INSERT INTO meta (key, value) VALUES (?, ?)
212
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
213
+ ).run(key, value);
214
+ }
215
+ function pendingByteSize() {
216
+ const db = getDb();
217
+ const row = db.prepare(
218
+ `SELECT COALESCE(SUM(LENGTH(payload)), 0) AS bytes
219
+ FROM events WHERE uploaded_at IS NULL`
220
+ ).get();
221
+ return row.bytes;
222
+ }
199
223
  function upsertSession(s) {
200
224
  const db = getDb();
201
225
  db.prepare(
@@ -236,9 +260,431 @@ function listRecentSessions(limit = 10) {
236
260
  FROM sessions ORDER BY started_at DESC LIMIT ?`
237
261
  ).all(limit);
238
262
  }
263
+ var _db;
264
+ var init_db = __esm({
265
+ "src/db.ts"() {
266
+ "use strict";
267
+ init_paths();
268
+ _db = null;
269
+ }
270
+ });
271
+
272
+ // src/upload/client.ts
273
+ async function postIngest(cfg, body) {
274
+ if (!cfg.apiKey) {
275
+ throw new Error("Not linked. Run: agentreel link <key>");
276
+ }
277
+ const url = `${cfg.apiBaseUrl.replace(/\/$/, "")}/api/v1/sessions/ingest`;
278
+ const res = await fetch(url, {
279
+ method: "POST",
280
+ headers: {
281
+ "content-type": "application/json",
282
+ authorization: `Bearer ${cfg.apiKey}`
283
+ },
284
+ body: JSON.stringify(body)
285
+ });
286
+ let data = null;
287
+ try {
288
+ data = await res.json();
289
+ } catch {
290
+ }
291
+ if (!res.ok || !data?.ok) {
292
+ const reason = data?.reason ? ` (${data.reason})` : "";
293
+ const tag = data?.error ?? `http-${res.status}`;
294
+ throw new IngestError(`${tag}${reason}`, res.status, data);
295
+ }
296
+ return data;
297
+ }
298
+ var IngestError;
299
+ var init_client = __esm({
300
+ "src/upload/client.ts"() {
301
+ "use strict";
302
+ IngestError = class extends Error {
303
+ constructor(message, status, body) {
304
+ super(message);
305
+ this.status = status;
306
+ this.body = body;
307
+ this.name = "IngestError";
308
+ }
309
+ status;
310
+ body;
311
+ /** 4xx (except 408/429) — payload-shaped problem the agent can't fix by retrying. */
312
+ get isPermanent() {
313
+ return this.status >= 400 && this.status < 500 && this.status !== 408 && this.status !== 429;
314
+ }
315
+ };
316
+ }
317
+ });
318
+
319
+ // src/upload/queue.ts
320
+ function takePendingBatch(maxEvents = MAX_BATCH_EVENTS, maxBytes = MAX_BATCH_BYTES) {
321
+ const db = getDb();
322
+ const candidateRows = db.prepare(
323
+ `SELECT id, session_id, tool, type, ts, cwd, payload
324
+ FROM events
325
+ WHERE uploaded_at IS NULL
326
+ ORDER BY ts ASC
327
+ LIMIT ?`
328
+ ).all(maxEvents);
329
+ if (candidateRows.length === 0) return { sessions: [], events: [], eventIds: [] };
330
+ const eventRows = [];
331
+ let bytes = 0;
332
+ for (const row of candidateRows) {
333
+ const rowBytes = Buffer.byteLength(row.payload, "utf8");
334
+ if (eventRows.length > 0 && bytes + rowBytes > maxBytes) break;
335
+ eventRows.push(row);
336
+ bytes += rowBytes;
337
+ }
338
+ const sessionIds = [...new Set(eventRows.map((e) => e.session_id))];
339
+ const placeholders = sessionIds.map(() => "?").join(",");
340
+ const sessionRows = db.prepare(
341
+ `SELECT id, tool, started_at, ended_at, cwd, total_cost_cents, total_tokens
342
+ FROM sessions WHERE id IN (${placeholders})`
343
+ ).all(...sessionIds);
344
+ const sessions = sessionRows.map((s) => ({
345
+ id: s.id,
346
+ tool: s.tool,
347
+ started_at: s.started_at,
348
+ ended_at: s.ended_at,
349
+ cwd: s.cwd,
350
+ total_cost_cents: s.total_cost_cents,
351
+ total_tokens: s.total_tokens
352
+ }));
353
+ const events = eventRows.map((e) => ({
354
+ id: e.id,
355
+ session_id: e.session_id,
356
+ ts: e.ts,
357
+ type: e.type,
358
+ tool: e.tool,
359
+ cwd: e.cwd,
360
+ payload: safeParse(e.payload)
361
+ }));
362
+ return { sessions, events, eventIds: eventRows.map((r) => r.id) };
363
+ }
364
+ function markUploaded(eventIds) {
365
+ if (eventIds.length === 0) return;
366
+ const db = getDb();
367
+ const now = Date.now();
368
+ const stmt = db.prepare(`UPDATE events SET uploaded_at = ? WHERE id = ?`);
369
+ const tx = db.transaction((ids) => {
370
+ for (const id of ids) stmt.run(now, id);
371
+ });
372
+ tx(eventIds);
373
+ }
374
+ function safeParse(s) {
375
+ try {
376
+ return JSON.parse(s);
377
+ } catch {
378
+ return s;
379
+ }
380
+ }
381
+ var MAX_BATCH_BYTES, MAX_BATCH_EVENTS;
382
+ var init_queue = __esm({
383
+ "src/upload/queue.ts"() {
384
+ "use strict";
385
+ init_db();
386
+ MAX_BATCH_BYTES = 1024 * 1024;
387
+ MAX_BATCH_EVENTS = 500;
388
+ }
389
+ });
390
+
391
+ // src/upload/flush.ts
392
+ function nextAttemptAt() {
393
+ const raw = getMeta(META_NEXT_ATTEMPT_AT);
394
+ return raw ? Number(raw) : 0;
395
+ }
396
+ function consecutiveFailures() {
397
+ const raw = getMeta(META_CONSECUTIVE_FAILS);
398
+ return raw ? Number(raw) : 0;
399
+ }
400
+ function recordSuccess() {
401
+ const now = Date.now();
402
+ setMeta(META_LAST_SYNC_AT, String(now));
403
+ setMeta(META_LAST_ERROR_AT, null);
404
+ setMeta(META_LAST_ERROR_MSG, null);
405
+ setMeta(META_CONSECUTIVE_FAILS, "0");
406
+ setMeta(META_NEXT_ATTEMPT_AT, "0");
407
+ }
408
+ function recordFailure(err, permanent) {
409
+ const now = Date.now();
410
+ const fails = consecutiveFailures() + 1;
411
+ setMeta(META_LAST_ERROR_AT, String(now));
412
+ setMeta(META_LAST_ERROR_MSG, truncate(err.message, 500));
413
+ setMeta(META_CONSECUTIVE_FAILS, String(fails));
414
+ if (permanent) {
415
+ setMeta(META_NEXT_ATTEMPT_AT, String(now + BACKOFF_MAX_MS));
416
+ return;
417
+ }
418
+ const exp = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** (fails - 1));
419
+ const jitter = exp * 0.25 * Math.random();
420
+ setMeta(META_NEXT_ATTEMPT_AT, String(now + exp + jitter));
421
+ }
422
+ function truncate(s, n) {
423
+ return s.length > n ? s.slice(0, n - 1) + "\u2026" : s;
424
+ }
425
+ async function flushOnce() {
426
+ const cfg = readConfig();
427
+ if (!cfg.apiKey) {
428
+ throw new Error("Not linked. Run: agentreel link <api-key>");
429
+ }
430
+ const batch = takePendingBatch(MAX_BATCH_EVENTS, MAX_BATCH_BYTES);
431
+ if (batch.events.length === 0) {
432
+ return { uploadedEvents: 0, uploadedSessions: 0, moreAvailable: false };
433
+ }
434
+ try {
435
+ const res = await postIngest(cfg, { sessions: batch.sessions, events: batch.events });
436
+ markUploaded(batch.eventIds);
437
+ recordSuccess();
438
+ return {
439
+ uploadedEvents: res.events_written ?? 0,
440
+ uploadedSessions: res.sessions_written ?? 0,
441
+ moreAvailable: batch.events.length === MAX_BATCH_EVENTS
442
+ };
443
+ } catch (err) {
444
+ const permanent = err instanceof IngestError && err.isPermanent;
445
+ recordFailure(err, permanent);
446
+ throw err;
447
+ }
448
+ }
449
+ var META_LAST_SYNC_AT, META_LAST_ERROR_AT, META_LAST_ERROR_MSG, META_CONSECUTIVE_FAILS, META_NEXT_ATTEMPT_AT, BACKOFF_BASE_MS, BACKOFF_MAX_MS;
450
+ var init_flush = __esm({
451
+ "src/upload/flush.ts"() {
452
+ "use strict";
453
+ init_db();
454
+ init_config();
455
+ init_client();
456
+ init_queue();
457
+ META_LAST_SYNC_AT = "last_sync_at";
458
+ META_LAST_ERROR_AT = "last_error_at";
459
+ META_LAST_ERROR_MSG = "last_error_msg";
460
+ META_CONSECUTIVE_FAILS = "consecutive_failures";
461
+ META_NEXT_ATTEMPT_AT = "next_attempt_at";
462
+ BACKOFF_BASE_MS = 1e3;
463
+ BACKOFF_MAX_MS = 5 * 6e4;
464
+ }
465
+ });
466
+
467
+ // src/commands/daemon.ts
468
+ var daemon_exports = {};
469
+ __export(daemon_exports, {
470
+ daemonCommand: () => daemonCommand,
471
+ stopCommand: () => stopCommand
472
+ });
473
+ import pc3 from "picocolors";
474
+ import { spawn } from "child_process";
475
+ import {
476
+ existsSync as existsSync4,
477
+ openSync,
478
+ readFileSync as readFileSync4,
479
+ unlinkSync,
480
+ writeFileSync as writeFileSync3
481
+ } from "fs";
482
+ async function daemonCommand(opts = {}) {
483
+ const cfg = readConfig();
484
+ if (!cfg.apiKey) {
485
+ console.error(pc3.red("\u2717 Not linked. Run ") + pc3.cyan("agentreel link <api-key>"));
486
+ process.exit(1);
487
+ }
488
+ if (opts.detach) {
489
+ spawnDetached();
490
+ return;
491
+ }
492
+ if (!claimPidFile()) {
493
+ process.exit(1);
494
+ }
495
+ const release = () => {
496
+ try {
497
+ unlinkSync(DAEMON_PID_PATH);
498
+ } catch {
499
+ }
500
+ };
501
+ let stopping = false;
502
+ const stop = () => {
503
+ if (stopping) return;
504
+ stopping = true;
505
+ console.log(pc3.dim("\n daemon stopping\u2026"));
506
+ release();
507
+ process.exit(0);
508
+ };
509
+ process.on("SIGINT", stop);
510
+ process.on("SIGTERM", stop);
511
+ process.on("beforeExit", release);
512
+ console.log(pc3.green("\u25CF") + " agentreel daemon running");
513
+ console.log(pc3.dim(` api: ${cfg.apiBaseUrl}`));
514
+ console.log(pc3.dim(` tick: every ${TICK_INTERVAL_MS / 1e3}s, early flush at ${(EARLY_FLUSH_BYTES / 1024).toFixed(0)} KB`));
515
+ console.log(pc3.dim(" Ctrl-C to stop"));
516
+ for (; ; ) {
517
+ if (stopping) return;
518
+ const nextAt = nextAttemptAt();
519
+ const now = Date.now();
520
+ if (nextAt > now) {
521
+ await sleepInterruptible(nextAt - now, () => stopping);
522
+ continue;
523
+ }
524
+ try {
525
+ for (; ; ) {
526
+ const res = await flushOnce();
527
+ if (res.uploadedEvents > 0) {
528
+ console.log(
529
+ pc3.dim(` [${ts()}]`) + pc3.green(" \u2713") + ` ${res.uploadedEvents} events \xB7 ${res.uploadedSessions} sessions`
530
+ );
531
+ }
532
+ if (!res.moreAvailable) break;
533
+ }
534
+ } catch (err) {
535
+ const e = err;
536
+ if (err instanceof IngestError && err.isPermanent) {
537
+ console.error(
538
+ pc3.dim(` [${ts()}]`) + pc3.red(" \u2717 ") + e.message + pc3.dim(" (5min backoff)")
539
+ );
540
+ } else {
541
+ console.error(pc3.dim(` [${ts()}]`) + pc3.yellow(" \xB7 ") + e.message);
542
+ }
543
+ }
544
+ await waitForTickOrPressure(TICK_INTERVAL_MS, EARLY_FLUSH_BYTES, () => stopping);
545
+ }
546
+ }
547
+ function claimPidFile() {
548
+ ensureAgentreelDir();
549
+ if (existsSync4(DAEMON_PID_PATH)) {
550
+ const raw = readFileSync4(DAEMON_PID_PATH, "utf8").trim();
551
+ const pid = Number(raw);
552
+ if (Number.isFinite(pid) && pid > 0 && isAlive(pid)) {
553
+ console.error(pc3.red(`\u2717 daemon already running (pid ${pid})`));
554
+ console.error(pc3.dim(` if this is wrong, remove ${DAEMON_PID_PATH}`));
555
+ return false;
556
+ }
557
+ }
558
+ writeFileSync3(DAEMON_PID_PATH, String(process.pid), { encoding: "utf8", mode: 384 });
559
+ return true;
560
+ }
561
+ function isAlive(pid) {
562
+ try {
563
+ process.kill(pid, 0);
564
+ return true;
565
+ } catch {
566
+ return false;
567
+ }
568
+ }
569
+ function sleepInterruptible(ms, shouldStop) {
570
+ return new Promise((resolve2) => {
571
+ if (ms <= 0) return resolve2();
572
+ const start = Date.now();
573
+ const t = setInterval(() => {
574
+ if (shouldStop() || Date.now() - start >= ms) {
575
+ clearInterval(t);
576
+ resolve2();
577
+ }
578
+ }, 250);
579
+ });
580
+ }
581
+ function spawnDetached() {
582
+ ensureAgentreelDir();
583
+ if (existsSync4(DAEMON_PID_PATH)) {
584
+ const raw = readFileSync4(DAEMON_PID_PATH, "utf8").trim();
585
+ const pid = Number(raw);
586
+ if (Number.isFinite(pid) && pid > 0 && isAlive(pid)) {
587
+ console.error(pc3.red(`\u2717 daemon already running (pid ${pid})`));
588
+ console.error(pc3.dim(" use ") + pc3.cyan("agentreel stop") + pc3.dim(" first"));
589
+ process.exit(1);
590
+ }
591
+ }
592
+ const out = openSync(DAEMON_LOG_PATH, "a");
593
+ const err = openSync(DAEMON_LOG_PATH, "a");
594
+ const child = spawn(process.execPath, [process.argv[1], "daemon"], {
595
+ detached: true,
596
+ stdio: ["ignore", out, err],
597
+ env: process.env
598
+ });
599
+ child.unref();
600
+ console.log(pc3.green("\u25CF") + ` daemon started (pid ${child.pid})`);
601
+ console.log(pc3.dim(` log: ${DAEMON_LOG_PATH}`));
602
+ console.log(pc3.dim(` stop: agentreel stop`));
603
+ }
604
+ function stopCommand() {
605
+ if (!existsSync4(DAEMON_PID_PATH)) {
606
+ console.log(pc3.dim("\xB7 daemon not running"));
607
+ return;
608
+ }
609
+ const raw = readFileSync4(DAEMON_PID_PATH, "utf8").trim();
610
+ const pid = Number(raw);
611
+ if (!Number.isFinite(pid) || pid <= 0) {
612
+ try {
613
+ unlinkSync(DAEMON_PID_PATH);
614
+ } catch {
615
+ }
616
+ console.log(pc3.dim("\xB7 cleared stale pid file"));
617
+ return;
618
+ }
619
+ if (!isAlive(pid)) {
620
+ try {
621
+ unlinkSync(DAEMON_PID_PATH);
622
+ } catch {
623
+ }
624
+ console.log(pc3.dim(`\xB7 no live daemon for pid ${pid} \u2014 cleared stale pid file`));
625
+ return;
626
+ }
627
+ try {
628
+ process.kill(pid, "SIGTERM");
629
+ console.log(pc3.green("\u2713") + ` sent SIGTERM to pid ${pid}`);
630
+ } catch (err) {
631
+ console.error(pc3.red("\u2717 ") + err.message);
632
+ process.exit(1);
633
+ }
634
+ }
635
+ function waitForTickOrPressure(tickMs, pressureBytes, shouldStop) {
636
+ return new Promise((resolve2) => {
637
+ const start = Date.now();
638
+ const t = setInterval(() => {
639
+ if (shouldStop()) {
640
+ clearInterval(t);
641
+ return resolve2();
642
+ }
643
+ if (Date.now() - start >= tickMs) {
644
+ clearInterval(t);
645
+ return resolve2();
646
+ }
647
+ try {
648
+ if (pendingByteSize() >= pressureBytes) {
649
+ clearInterval(t);
650
+ return resolve2();
651
+ }
652
+ } catch {
653
+ }
654
+ }, 1e3);
655
+ });
656
+ }
657
+ function ts() {
658
+ const d = /* @__PURE__ */ new Date();
659
+ return d.toTimeString().slice(0, 8);
660
+ }
661
+ var TICK_INTERVAL_MS, EARLY_FLUSH_BYTES;
662
+ var init_daemon = __esm({
663
+ "src/commands/daemon.ts"() {
664
+ "use strict";
665
+ init_paths();
666
+ init_db();
667
+ init_flush();
668
+ init_queue();
669
+ init_client();
670
+ init_config();
671
+ TICK_INTERVAL_MS = 3e4;
672
+ EARLY_FLUSH_BYTES = MAX_BATCH_BYTES;
673
+ }
674
+ });
675
+
676
+ // src/cli.ts
677
+ import { Command } from "commander";
239
678
 
240
679
  // src/commands/init.ts
680
+ init_install();
681
+ init_config();
682
+ init_db();
241
683
  init_paths();
684
+ import { realpathSync } from "fs";
685
+ import { fileURLToPath } from "url";
686
+ import { sep } from "path";
687
+ import pc from "picocolors";
242
688
  var PACKAGE_NAME = "@agentreel/agent";
243
689
  function resolveAgentBinary() {
244
690
  const here = fileURLToPath(import.meta.url);
@@ -285,14 +731,35 @@ async function initCommand() {
285
731
  );
286
732
  }
287
733
  console.log();
288
- console.log(pc.bold("Next:") + " open Claude Code and run a prompt.");
289
- console.log(" then " + pc.cyan("agentreel status") + " to see captured events.\n");
734
+ if (!cfg.apiKey) {
735
+ console.log(pc.bold("Next steps:"));
736
+ console.log(
737
+ " " + pc.dim("1.") + " " + pc.cyan("agentreel link <key>") + pc.dim(" \u2014 get a key from https://agentreel.dev/dashboard/settings")
738
+ );
739
+ console.log(
740
+ " " + pc.dim("2.") + " " + pc.cyan("agentreel daemon --detach") + pc.dim(" \u2014 start the background uploader")
741
+ );
742
+ console.log(
743
+ " " + pc.dim("3.") + " open Claude Code and work as normal."
744
+ );
745
+ } else {
746
+ console.log(pc.bold("Next:"));
747
+ console.log(
748
+ " " + pc.cyan("agentreel daemon --detach") + pc.dim(" \u2014 start the background uploader (already linked)")
749
+ );
750
+ }
751
+ console.log(
752
+ "\n" + pc.dim("Anything: ") + pc.cyan("agentreel status") + pc.dim(" shows queue, last sync, last error.\n")
753
+ );
290
754
  }
291
755
 
292
756
  // src/commands/status.ts
293
757
  init_paths();
758
+ init_db();
759
+ init_config();
760
+ init_flush();
294
761
  import pc2 from "picocolors";
295
- import { existsSync as existsSync3 } from "fs";
762
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
296
763
  function fmtDuration(ms) {
297
764
  const s = Math.round(ms / 1e3);
298
765
  if (s < 60) return `${s}s`;
@@ -301,6 +768,25 @@ function fmtDuration(ms) {
301
768
  const h = Math.floor(m / 60);
302
769
  return `${h}h ${m % 60}m`;
303
770
  }
771
+ function fmtAgo(ms) {
772
+ return fmtDuration(Date.now() - ms) + " ago";
773
+ }
774
+ function fmtBytes(n) {
775
+ if (n < 1024) return `${n} B`;
776
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
777
+ return `${(n / 1024 / 1024).toFixed(2)} MB`;
778
+ }
779
+ function daemonStatus() {
780
+ if (!existsSync3(DAEMON_PID_PATH)) return { running: false, pid: null };
781
+ try {
782
+ const pid = Number(readFileSync3(DAEMON_PID_PATH, "utf8").trim());
783
+ if (!Number.isFinite(pid) || pid <= 0) return { running: false, pid: null };
784
+ process.kill(pid, 0);
785
+ return { running: true, pid };
786
+ } catch {
787
+ return { running: false, pid: null };
788
+ }
789
+ }
304
790
  async function statusCommand() {
305
791
  const cfg = readConfig();
306
792
  console.log(pc2.bold(pc2.cyan("AgentReel status\n")));
@@ -311,14 +797,38 @@ async function statusCommand() {
311
797
  );
312
798
  console.log(" api base: " + cfg.apiBaseUrl);
313
799
  console.log(" authenticated: " + (cfg.apiKey ? pc2.green("yes") : pc2.yellow("no")));
800
+ const d = daemonStatus();
801
+ console.log(
802
+ " daemon: " + (d.running ? pc2.green(`running (pid ${d.pid})`) : pc2.dim("stopped"))
803
+ );
314
804
  console.log();
315
805
  if (!existsSync3(DB_PATH)) {
316
806
  console.log(pc2.yellow("Run `agentreel init` to install hooks."));
317
807
  return;
318
808
  }
319
809
  const { total, pending } = countEvents();
810
+ const pendingBytes = pendingByteSize();
320
811
  console.log(` events captured: ${total}`);
321
- console.log(` pending upload: ${pending}`);
812
+ console.log(` pending upload: ${pending}` + (pending > 0 ? pc2.dim(` (${fmtBytes(pendingBytes)})`) : ""));
813
+ const lastSync = numMeta(META_LAST_SYNC_AT);
814
+ const lastErr = numMeta(META_LAST_ERROR_AT);
815
+ const lastErrMsg = getMeta(META_LAST_ERROR_MSG);
816
+ const fails = consecutiveFailures();
817
+ const nextAt = numMeta(META_NEXT_ATTEMPT_AT);
818
+ console.log(
819
+ ` last sync: ` + (lastSync ? pc2.green(fmtAgo(lastSync)) : pc2.dim("never"))
820
+ );
821
+ if (lastErr) {
822
+ console.log(
823
+ ` last error: ` + pc2.red(fmtAgo(lastErr)) + pc2.dim(` \xB7 ${fails} consecutive`)
824
+ );
825
+ if (lastErrMsg) console.log(pc2.dim(` ${lastErrMsg}`));
826
+ if (nextAt && nextAt > Date.now()) {
827
+ console.log(
828
+ pc2.dim(` next attempt in ${fmtDuration(nextAt - Date.now())}`)
829
+ );
830
+ }
831
+ }
322
832
  console.log();
323
833
  const sessions = listRecentSessions(5);
324
834
  if (sessions.length === 0) {
@@ -328,80 +838,60 @@ async function statusCommand() {
328
838
  console.log(pc2.bold("recent sessions:"));
329
839
  for (const s of sessions) {
330
840
  const dur = s.ended_at ? fmtDuration(s.ended_at - s.started_at) : pc2.dim("active");
331
- const ts = new Date(s.started_at).toLocaleString();
332
- console.log(` ${pc2.dim(s.id.slice(0, 8))} ${s.tool.padEnd(11)} ${ts} ${dur}`);
841
+ const ts2 = new Date(s.started_at).toLocaleString();
842
+ console.log(` ${pc2.dim(s.id.slice(0, 8))} ${s.tool.padEnd(11)} ${ts2} ${dur}`);
333
843
  }
334
844
  }
335
-
336
- // src/commands/auth.ts
337
- import pc3 from "picocolors";
338
-
339
- // src/upload/client.ts
340
- async function postIngest(cfg, body) {
341
- if (!cfg.apiKey) {
342
- throw new Error("Not linked. Run: agentreel link <key>");
343
- }
344
- const url = `${cfg.apiBaseUrl.replace(/\/$/, "")}/api/v1/sessions/ingest`;
345
- const res = await fetch(url, {
346
- method: "POST",
347
- headers: {
348
- "content-type": "application/json",
349
- authorization: `Bearer ${cfg.apiKey}`
350
- },
351
- body: JSON.stringify(body)
352
- });
353
- let data;
354
- try {
355
- data = await res.json();
356
- } catch {
357
- throw new Error(`Ingest returned ${res.status} with non-JSON body`);
358
- }
359
- if (!res.ok || !data.ok) {
360
- throw new Error(
361
- `Ingest failed: ${res.status} ${data.error ?? ""} ${data.reason ?? ""}`.trim()
362
- );
363
- }
364
- return data;
845
+ function numMeta(key) {
846
+ const v = getMeta(key);
847
+ if (!v) return null;
848
+ const n = Number(v);
849
+ return Number.isFinite(n) && n > 0 ? n : null;
365
850
  }
366
851
 
367
852
  // src/commands/auth.ts
853
+ init_config();
854
+ init_client();
855
+ import pc4 from "picocolors";
368
856
  async function logoutCommand() {
369
857
  const cfg = readConfig();
370
858
  cfg.apiKey = void 0;
371
859
  cfg.workspaceId = void 0;
372
860
  writeConfig(cfg);
373
- console.log(pc3.green("\u2713") + " Cleared local credentials.");
861
+ console.log(pc4.green("\u2713") + " Cleared local credentials.");
374
862
  }
375
863
  async function linkCommand(rawKey, opts) {
376
864
  const key = (rawKey ?? await promptHidden("Paste your AgentReel API key: ")).trim();
377
865
  if (!key) {
378
- console.error(pc3.red("\u2717 No key provided."));
866
+ console.error(pc4.red("\u2717 No key provided."));
379
867
  process.exit(1);
380
868
  }
381
869
  if (!key.startsWith("ar_live_")) {
382
- console.error(pc3.red("\u2717 Keys start with `ar_live_`. Did you paste the right value?"));
870
+ console.error(pc4.red("\u2717 Keys start with `ar_live_`. Did you paste the right value?"));
383
871
  process.exit(1);
384
872
  }
385
873
  const cfg = readConfig();
386
874
  cfg.apiKey = key;
387
875
  if (opts.api) cfg.apiBaseUrl = opts.api;
388
- console.log(pc3.dim(` validating against ${cfg.apiBaseUrl}\u2026`));
876
+ console.log(pc4.dim(` validating against ${cfg.apiBaseUrl}\u2026`));
389
877
  try {
390
878
  await postIngest(cfg, {});
391
879
  } catch (err) {
392
- console.error(pc3.red("\u2717 Validation failed: ") + err.message);
880
+ console.error(pc4.red("\u2717 Validation failed: ") + err.message);
393
881
  process.exit(1);
394
882
  }
395
883
  writeConfig(cfg);
396
- console.log(pc3.green("\u2713") + " Linked.");
397
- console.log(pc3.dim(" api: ") + cfg.apiBaseUrl);
398
- console.log(pc3.dim(" key: ") + key.slice(0, 12) + "\u2026");
884
+ console.log(pc4.green("\u2713") + " Linked.");
885
+ console.log(pc4.dim(" api: ") + cfg.apiBaseUrl);
886
+ console.log(pc4.dim(" key: ") + key.slice(0, 12) + "\u2026");
399
887
  }
400
888
  async function uninstallCommand() {
401
889
  const { uninstallClaudeCodeHooks: uninstallClaudeCodeHooks2 } = await Promise.resolve().then(() => (init_install(), install_exports));
890
+ const { stopCommand: stopCommand2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
891
+ stopCommand2();
402
892
  const { backup } = uninstallClaudeCodeHooks2();
403
- console.log(pc3.green("\u2713") + " Removed AgentReel hooks from ~/.claude/settings.json");
404
- if (backup) console.log(pc3.dim(` (backup: ${backup})`));
893
+ console.log(pc4.green("\u2713") + " Removed AgentReel hooks from ~/.claude/settings.json");
894
+ if (backup) console.log(pc4.dim(` (backup: ${backup})`));
405
895
  }
406
896
  var CTRL_C = 3;
407
897
  var BACKSPACE = 127;
@@ -442,8 +932,8 @@ function promptHidden(prompt) {
442
932
  }
443
933
 
444
934
  // src/commands/watch.ts
445
- import pc4 from "picocolors";
446
- import { existsSync as existsSync7 } from "fs";
935
+ import pc5 from "picocolors";
936
+ import { existsSync as existsSync8 } from "fs";
447
937
  import { basename as basename2 } from "path";
448
938
 
449
939
  // src/cursor/paths.ts
@@ -469,17 +959,17 @@ function cursorHistoryDir() {
469
959
  // src/cursor/watcher.ts
470
960
  import chokidar from "chokidar";
471
961
  import { readFile as readFile2 } from "fs/promises";
472
- import { existsSync as existsSync6 } from "fs";
962
+ import { existsSync as existsSync7 } from "fs";
473
963
  import { basename, dirname as dirname3, join as join5 } from "path";
474
964
 
475
965
  // src/cursor/entries.ts
476
966
  import { readFile } from "fs/promises";
477
- import { existsSync as existsSync4, statSync } from "fs";
967
+ import { existsSync as existsSync5, statSync } from "fs";
478
968
  import { join as join3, dirname as dirname2, resolve, sep as sep2 } from "path";
479
969
  import { fileURLToPath as fileURLToPath2 } from "url";
480
970
  async function readEntries(historyFolder) {
481
971
  const path = join3(historyFolder, "entries.json");
482
- if (!existsSync4(path)) return null;
972
+ if (!existsSync5(path)) return null;
483
973
  try {
484
974
  const raw = await readFile(path, "utf8");
485
975
  return JSON.parse(raw);
@@ -500,7 +990,7 @@ function findWorkspaceRoot(path) {
500
990
  while (dir && dir !== sep2) {
501
991
  const git = join3(dir, ".git");
502
992
  try {
503
- if (existsSync4(git)) return dir;
993
+ if (existsSync5(git)) return dir;
504
994
  } catch {
505
995
  }
506
996
  const parent = dirname2(dir);
@@ -539,7 +1029,7 @@ function computeDiff(before, after) {
539
1029
  }
540
1030
 
541
1031
  // src/redact/ignore.ts
542
- import { existsSync as existsSync5, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
1032
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
543
1033
  import { join as join4, relative, sep as sep3 } from "path";
544
1034
  import ignore from "ignore";
545
1035
  var FILENAME = ".agentreelignore";
@@ -547,9 +1037,9 @@ var TTL_MS = 5e3;
547
1037
  var cache = /* @__PURE__ */ new Map();
548
1038
  function loadFor(workspace) {
549
1039
  const path = join4(workspace, FILENAME);
550
- if (!existsSync5(path)) return null;
1040
+ if (!existsSync6(path)) return null;
551
1041
  try {
552
- const raw = readFileSync3(path, "utf8");
1042
+ const raw = readFileSync5(path, "utf8");
553
1043
  return ignore({ allowRelativePaths: true }).add(raw);
554
1044
  } catch {
555
1045
  return null;
@@ -560,7 +1050,7 @@ function getCached(workspace) {
560
1050
  const path = join4(workspace, FILENAME);
561
1051
  let mtime = 0;
562
1052
  try {
563
- mtime = existsSync5(path) ? statSync2(path).mtimeMs : 0;
1053
+ mtime = existsSync6(path) ? statSync2(path).mtimeMs : 0;
564
1054
  } catch {
565
1055
  mtime = 0;
566
1056
  }
@@ -783,11 +1273,11 @@ async function processSnapshot(folder, ctx, skipFirst, onSnapshot) {
783
1273
  let after = "";
784
1274
  if (ctx.previous) {
785
1275
  const prevPath = join5(folder, ctx.previous.id);
786
- if (existsSync6(prevPath)) {
1276
+ if (existsSync7(prevPath)) {
787
1277
  before = await safeRead(prevPath);
788
1278
  }
789
1279
  }
790
- if (existsSync6(newSnapshot)) {
1280
+ if (existsSync7(newSnapshot)) {
791
1281
  after = await safeRead(newSnapshot);
792
1282
  }
793
1283
  if (before === after) return;
@@ -834,6 +1324,7 @@ function sleep(ms) {
834
1324
  var NOISY_PATH = /[\\/](node_modules|\.next|\.turbo|dist|build|\.git|coverage|\.cache|\.venv|venv|target|out)[\\/]/;
835
1325
 
836
1326
  // src/cursor/session.ts
1327
+ init_db();
837
1328
  import { nanoid } from "nanoid";
838
1329
  var IDLE_MS = 5 * 60 * 1e3;
839
1330
  var GLOBAL_KEY = "__global__";
@@ -841,16 +1332,16 @@ var CursorSessionManager = class {
841
1332
  open = /* @__PURE__ */ new Map();
842
1333
  ingest(snapshot) {
843
1334
  const key = snapshot.workspace ?? GLOBAL_KEY;
844
- const ts = snapshot.timestamp;
1335
+ const ts2 = snapshot.timestamp;
845
1336
  const cwd = snapshot.workspace ?? "";
846
1337
  let session = this.open.get(key);
847
1338
  let isNew = false;
848
- if (!session || ts - session.lastTs > IDLE_MS) {
1339
+ if (!session || ts2 - session.lastTs > IDLE_MS) {
849
1340
  if (session) this.closeSession(session, session.lastTs);
850
1341
  session = {
851
1342
  id: `cur_${nanoid(10)}`,
852
- startedAt: ts,
853
- lastTs: ts,
1343
+ startedAt: ts2,
1344
+ lastTs: ts2,
854
1345
  cwd
855
1346
  };
856
1347
  this.open.set(key, session);
@@ -858,18 +1349,18 @@ var CursorSessionManager = class {
858
1349
  upsertSession({
859
1350
  id: session.id,
860
1351
  tool: "cursor",
861
- startedAt: ts,
1352
+ startedAt: ts2,
862
1353
  cwd
863
1354
  });
864
1355
  } else {
865
- session.lastTs = ts;
1356
+ session.lastTs = ts2;
866
1357
  }
867
1358
  const event = {
868
1359
  id: nanoid(),
869
1360
  sessionId: session.id,
870
1361
  tool: "cursor",
871
1362
  type: "tool_use_post",
872
- ts,
1363
+ ts: ts2,
873
1364
  cwd,
874
1365
  payload: {
875
1366
  tool_name: "Edit",
@@ -903,38 +1394,39 @@ var CursorSessionManager = class {
903
1394
 
904
1395
  // src/commands/watch.ts
905
1396
  init_paths();
1397
+ init_db();
906
1398
  async function watchCommand() {
907
1399
  ensureAgentreelDir();
908
1400
  getDb();
909
1401
  const dir = cursorHistoryDir();
910
- if (!existsSync7(dir)) {
911
- console.error(pc4.red("\u2717 Cursor history directory not found:"));
1402
+ if (!existsSync8(dir)) {
1403
+ console.error(pc5.red("\u2717 Cursor history directory not found:"));
912
1404
  console.error(" " + dir);
913
1405
  console.error();
914
- console.error(pc4.dim("Open Cursor at least once, edit a file, then re-run."));
915
- console.error(pc4.dim("(Or set AGENTREEL_CURSOR_HISTORY_DIR to a custom path.)"));
1406
+ console.error(pc5.dim("Open Cursor at least once, edit a file, then re-run."));
1407
+ console.error(pc5.dim("(Or set AGENTREEL_CURSOR_HISTORY_DIR to a custom path.)"));
916
1408
  process.exit(1);
917
1409
  }
918
- console.log(pc4.bold(pc4.cyan("AgentReel \xB7 Cursor watcher\n")));
919
- console.log(pc4.dim(" watching ") + dir);
920
- console.log(pc4.dim(" press Ctrl+C to stop\n"));
1410
+ console.log(pc5.bold(pc5.cyan("AgentReel \xB7 Cursor watcher\n")));
1411
+ console.log(pc5.dim(" watching ") + dir);
1412
+ console.log(pc5.dim(" press Ctrl+C to stop\n"));
921
1413
  const sessions = new CursorSessionManager();
922
1414
  const watcher = startCursorWatcher(dir, async (snap) => {
923
1415
  const { sessionId, isNew } = sessions.ingest(snap);
924
- const ts = new Date(snap.timestamp).toLocaleTimeString();
925
- const ws = snap.workspace ? basename2(snap.workspace) : pc4.dim("no-workspace");
1416
+ const ts2 = new Date(snap.timestamp).toLocaleTimeString();
1417
+ const ws = snap.workspace ? basename2(snap.workspace) : pc5.dim("no-workspace");
926
1418
  const file = snap.filePath.split("/").slice(-2).join("/");
927
- const sourceLabel = snap.source === "cursor-ai" ? pc4.magenta("ai") : snap.source === "cursor-manual" ? pc4.cyan("man") : pc4.dim("?");
928
- const stats = snap.binary ? pc4.dim("binary") : `${pc4.green("+" + snap.added)} ${pc4.red("-" + snap.removed)}`;
1419
+ const sourceLabel = snap.source === "cursor-ai" ? pc5.magenta("ai") : snap.source === "cursor-manual" ? pc5.cyan("man") : pc5.dim("?");
1420
+ const stats = snap.binary ? pc5.dim("binary") : `${pc5.green("+" + snap.added)} ${pc5.red("-" + snap.removed)}`;
929
1421
  if (isNew) {
930
1422
  console.log(
931
- `${pc4.dim(ts)} ${pc4.yellow("session")} ${pc4.dim(sessionId)} ${ws}`
1423
+ `${pc5.dim(ts2)} ${pc5.yellow("session")} ${pc5.dim(sessionId)} ${ws}`
932
1424
  );
933
1425
  }
934
- console.log(`${pc4.dim(ts)} edit ${sourceLabel} ${file.padEnd(36)} ${stats}`);
1426
+ console.log(`${pc5.dim(ts2)} edit ${sourceLabel} ${file.padEnd(36)} ${stats}`);
935
1427
  });
936
1428
  const shutdown = async () => {
937
- console.log(pc4.dim("\n closing sessions\u2026"));
1429
+ console.log(pc5.dim("\n closing sessions\u2026"));
938
1430
  sessions.closeAll();
939
1431
  await watcher.close();
940
1432
  process.exit(0);
@@ -944,96 +1436,158 @@ async function watchCommand() {
944
1436
  }
945
1437
 
946
1438
  // src/commands/push.ts
947
- import pc5 from "picocolors";
948
-
949
- // src/upload/queue.ts
950
- function takePendingBatch(maxEvents = 500) {
951
- const db = getDb();
952
- const eventRows = db.prepare(
953
- `SELECT id, session_id, tool, type, ts, cwd, payload
954
- FROM events
955
- WHERE uploaded_at IS NULL
956
- ORDER BY ts ASC
957
- LIMIT ?`
958
- ).all(maxEvents);
959
- if (eventRows.length === 0) return { sessions: [], events: [], eventIds: [] };
960
- const sessionIds = [...new Set(eventRows.map((e) => e.session_id))];
961
- const placeholders = sessionIds.map(() => "?").join(",");
962
- const sessionRows = db.prepare(
963
- `SELECT id, tool, started_at, ended_at, cwd, total_cost_cents, total_tokens
964
- FROM sessions WHERE id IN (${placeholders})`
965
- ).all(...sessionIds);
966
- const sessions = sessionRows.map((s) => ({
967
- id: s.id,
968
- tool: s.tool,
969
- started_at: s.started_at,
970
- ended_at: s.ended_at,
971
- cwd: s.cwd,
972
- total_cost_cents: s.total_cost_cents,
973
- total_tokens: s.total_tokens
974
- }));
975
- const events = eventRows.map((e) => ({
976
- id: e.id,
977
- session_id: e.session_id,
978
- ts: e.ts,
979
- type: e.type,
980
- tool: e.tool,
981
- cwd: e.cwd,
982
- payload: safeParse(e.payload)
983
- }));
984
- return { sessions, events, eventIds: eventRows.map((r) => r.id) };
985
- }
986
- function markUploaded(eventIds) {
987
- if (eventIds.length === 0) return;
988
- const db = getDb();
989
- const now = Date.now();
990
- const stmt = db.prepare(`UPDATE events SET uploaded_at = ? WHERE id = ?`);
991
- const tx = db.transaction((ids) => {
992
- for (const id of ids) stmt.run(now, id);
993
- });
994
- tx(eventIds);
995
- }
996
- function safeParse(s) {
997
- try {
998
- return JSON.parse(s);
999
- } catch {
1000
- return s;
1001
- }
1002
- }
1003
-
1004
- // src/commands/push.ts
1439
+ init_config();
1440
+ init_flush();
1441
+ init_client();
1442
+ import pc6 from "picocolors";
1005
1443
  async function pushCommand() {
1006
1444
  const cfg = readConfig();
1007
1445
  if (!cfg.apiKey) {
1008
- console.error(pc5.red("\u2717 Not linked. Run ") + pc5.cyan("agentreel link <api-key>"));
1446
+ console.error(pc6.red("\u2717 Not linked. Run ") + pc6.cyan("agentreel link <api-key>"));
1009
1447
  process.exit(1);
1010
1448
  }
1011
1449
  let totalSessions = 0;
1012
1450
  let totalEvents = 0;
1013
- while (true) {
1014
- const batch = takePendingBatch(500);
1015
- if (batch.events.length === 0) break;
1016
- console.log(
1017
- pc5.dim(` uploading ${batch.events.length} events across ${batch.sessions.length} sessions\u2026`)
1018
- );
1019
- const res = await postIngest(cfg, { sessions: batch.sessions, events: batch.events });
1020
- markUploaded(batch.eventIds);
1021
- totalSessions += res.sessions_written ?? 0;
1022
- totalEvents += res.events_written ?? 0;
1451
+ for (; ; ) {
1452
+ let res;
1453
+ try {
1454
+ res = await flushOnce();
1455
+ } catch (err) {
1456
+ const e = err;
1457
+ if (err instanceof IngestError) {
1458
+ console.error(pc6.red("\u2717 ") + e.message);
1459
+ if (err.isPermanent) {
1460
+ console.error(
1461
+ pc6.dim(" not retrying \u2014 fix the cause (re-link, upgrade plan, etc.) and run push again.")
1462
+ );
1463
+ }
1464
+ } else {
1465
+ console.error(pc6.red("\u2717 ") + e.message);
1466
+ }
1467
+ process.exit(1);
1468
+ }
1469
+ totalSessions += res.uploadedSessions;
1470
+ totalEvents += res.uploadedEvents;
1471
+ if (!res.moreAvailable) break;
1023
1472
  }
1024
1473
  if (totalEvents === 0) {
1025
- console.log(pc5.green("\u2713") + " queue is empty \u2014 nothing to upload.");
1474
+ console.log(pc6.green("\u2713") + " queue is empty \u2014 nothing to upload.");
1026
1475
  return;
1027
1476
  }
1028
1477
  console.log(
1029
- pc5.green("\u2713") + ` uploaded ${totalEvents} events \xB7 ${totalSessions} session rows touched`
1478
+ pc6.green("\u2713") + ` uploaded ${totalEvents} events \xB7 ${totalSessions} session rows touched`
1030
1479
  );
1031
1480
  }
1032
1481
 
1482
+ // src/cli.ts
1483
+ init_daemon();
1484
+
1033
1485
  // src/hooks/handler.ts
1486
+ init_db();
1487
+ init_paths();
1034
1488
  import { nanoid as nanoid2 } from "nanoid";
1035
1489
  import { appendFileSync } from "fs";
1036
- init_paths();
1490
+
1491
+ // src/cost/transcript.ts
1492
+ init_db();
1493
+ import { existsSync as existsSync9, statSync as statSync3, openSync as openSync2, readSync, closeSync } from "fs";
1494
+
1495
+ // src/cost/pricing.ts
1496
+ var MODEL_RATES = {
1497
+ // Claude 4.x
1498
+ "claude-opus-4-7": { input: 1500, output: 7500, cacheRead: 150, cacheCreation: 1875 },
1499
+ "claude-opus-4-6": { input: 1500, output: 7500, cacheRead: 150, cacheCreation: 1875 },
1500
+ "claude-sonnet-4-6": { input: 300, output: 1500, cacheRead: 30, cacheCreation: 375 },
1501
+ "claude-sonnet-4-5": { input: 300, output: 1500, cacheRead: 30, cacheCreation: 375 },
1502
+ "claude-haiku-4-5": { input: 80, output: 400, cacheRead: 8, cacheCreation: 100 },
1503
+ // Legacy 3.x (still seen in older transcripts)
1504
+ "claude-3-5-sonnet": { input: 300, output: 1500, cacheRead: 30, cacheCreation: 375 },
1505
+ "claude-3-5-haiku": { input: 80, output: 400, cacheRead: 8, cacheCreation: 100 },
1506
+ "claude-3-opus": { input: 1500, output: 7500, cacheRead: 150, cacheCreation: 1875 }
1507
+ };
1508
+ var FALLBACK = MODEL_RATES["claude-sonnet-4-6"];
1509
+ function ratesFor(model) {
1510
+ if (!model) return FALLBACK;
1511
+ if (MODEL_RATES[model]) return MODEL_RATES[model];
1512
+ const stripped = model.replace(/-\d{8}$/, "");
1513
+ if (MODEL_RATES[stripped]) return MODEL_RATES[stripped];
1514
+ for (const key of Object.keys(MODEL_RATES)) {
1515
+ if (model.startsWith(key)) return MODEL_RATES[key];
1516
+ }
1517
+ return FALLBACK;
1518
+ }
1519
+ function costCentsFor(usage, rates) {
1520
+ const c = (usage.inputTokens * rates.input + usage.outputTokens * rates.output + usage.cacheReadTokens * rates.cacheRead + usage.cacheCreationTokens * rates.cacheCreation) / 1e6;
1521
+ return Math.ceil(c);
1522
+ }
1523
+
1524
+ // src/cost/transcript.ts
1525
+ var KEY_OFFSET = (sid) => `cost_offset:${sid}`;
1526
+ var KEY_TOKENS = (sid) => `cost_tokens:${sid}`;
1527
+ var KEY_CENTS = (sid) => `cost_cents:${sid}`;
1528
+ var KEY_PATH = (sid) => `cost_path:${sid}`;
1529
+ function recomputeSessionCost(sessionId, transcriptPath) {
1530
+ if (!transcriptPath) {
1531
+ transcriptPath = getMeta(KEY_PATH(sessionId)) ?? void 0;
1532
+ } else {
1533
+ setMeta(KEY_PATH(sessionId), transcriptPath);
1534
+ }
1535
+ if (!transcriptPath || !existsSync9(transcriptPath)) return;
1536
+ const stat = statSync3(transcriptPath);
1537
+ const totalSize = stat.size;
1538
+ const offset = Number(getMeta(KEY_OFFSET(sessionId)) ?? "0");
1539
+ if (offset >= totalSize) return;
1540
+ const buf = Buffer.alloc(totalSize - offset);
1541
+ const fd = openSync2(transcriptPath, "r");
1542
+ try {
1543
+ readSync(fd, buf, 0, buf.length, offset);
1544
+ } finally {
1545
+ closeSync(fd);
1546
+ }
1547
+ const text = buf.toString("utf8");
1548
+ const lastNewline = text.lastIndexOf("\n");
1549
+ if (lastNewline === -1) return;
1550
+ const consumable = text.slice(0, lastNewline);
1551
+ const consumedBytes = Buffer.byteLength(consumable, "utf8") + 1;
1552
+ let addedTokens = 0;
1553
+ let addedCents = 0;
1554
+ for (const raw of consumable.split("\n")) {
1555
+ if (!raw) continue;
1556
+ let line;
1557
+ try {
1558
+ line = JSON.parse(raw);
1559
+ } catch {
1560
+ continue;
1561
+ }
1562
+ if (line.type !== "assistant") continue;
1563
+ const usage = line.message?.usage;
1564
+ if (!usage) continue;
1565
+ const u = {
1566
+ inputTokens: usage.input_tokens ?? 0,
1567
+ outputTokens: usage.output_tokens ?? 0,
1568
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
1569
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0
1570
+ };
1571
+ const rates = ratesFor(line.message?.model);
1572
+ addedCents += costCentsFor(u, rates);
1573
+ addedTokens += u.inputTokens + u.outputTokens + u.cacheReadTokens + u.cacheCreationTokens;
1574
+ }
1575
+ const prevTokens = Number(getMeta(KEY_TOKENS(sessionId)) ?? "0");
1576
+ const prevCents = Number(getMeta(KEY_CENTS(sessionId)) ?? "0");
1577
+ const newTokens = prevTokens + addedTokens;
1578
+ const newCents = prevCents + addedCents;
1579
+ setMeta(KEY_OFFSET(sessionId), String(offset + consumedBytes));
1580
+ setMeta(KEY_TOKENS(sessionId), String(newTokens));
1581
+ setMeta(KEY_CENTS(sessionId), String(newCents));
1582
+ const db = getDb();
1583
+ db.prepare(
1584
+ `UPDATE sessions
1585
+ SET total_cost_cents = ?, total_tokens = ?
1586
+ WHERE id = ?`
1587
+ ).run(newCents, newTokens, sessionId);
1588
+ }
1589
+
1590
+ // src/hooks/handler.ts
1037
1591
  var HOOK_EVENT_TO_TYPE = {
1038
1592
  SessionStart: "session_start",
1039
1593
  SessionEnd: "session_end",
@@ -1069,28 +1623,28 @@ async function runHook(eventArg) {
1069
1623
  const input = JSON.parse(raw);
1070
1624
  const hookEventName = input.hook_event_name ?? eventArg ?? "Unknown";
1071
1625
  const type = HOOK_EVENT_TO_TYPE[hookEventName] ?? "unknown";
1072
- const ts = Date.now();
1626
+ const ts2 = Date.now();
1073
1627
  const sessionId = input.session_id ?? "unknown-session";
1074
1628
  if (type === "session_start") {
1075
1629
  upsertSession({
1076
1630
  id: sessionId,
1077
1631
  tool: "claude-code",
1078
- startedAt: ts,
1632
+ startedAt: ts2,
1079
1633
  cwd: input.cwd
1080
1634
  });
1081
1635
  } else if (type === "session_end") {
1082
1636
  upsertSession({
1083
1637
  id: sessionId,
1084
1638
  tool: "claude-code",
1085
- startedAt: ts,
1086
- endedAt: ts,
1639
+ startedAt: ts2,
1640
+ endedAt: ts2,
1087
1641
  cwd: input.cwd
1088
1642
  });
1089
1643
  } else {
1090
1644
  upsertSession({
1091
1645
  id: sessionId,
1092
1646
  tool: "claude-code",
1093
- startedAt: ts,
1647
+ startedAt: ts2,
1094
1648
  cwd: input.cwd
1095
1649
  });
1096
1650
  }
@@ -1100,19 +1654,25 @@ async function runHook(eventArg) {
1100
1654
  sessionId,
1101
1655
  tool: "claude-code",
1102
1656
  type,
1103
- ts,
1657
+ ts: ts2,
1104
1658
  cwd: input.cwd,
1105
1659
  payload: safePayload
1106
1660
  };
1107
1661
  insertEvent(event);
1662
+ try {
1663
+ recomputeSessionCost(sessionId, input.transcript_path);
1664
+ } catch (err) {
1665
+ logError(err);
1666
+ }
1108
1667
  } catch (err) {
1109
1668
  logError(err);
1110
1669
  }
1111
1670
  }
1112
1671
 
1113
1672
  // src/cli.ts
1673
+ var VERSION = true ? "0.1.4" : "dev";
1114
1674
  var program = new Command();
1115
- program.name("agentreel").description("AgentReel \u2014 capture Claude Code and Cursor sessions locally").version("0.0.0");
1675
+ program.name("agentreel").description("AgentReel \u2014 capture Claude Code and Cursor sessions locally").version(VERSION);
1116
1676
  program.command("init").description("install Claude Code hooks and create the local SQLite buffer").action(async () => {
1117
1677
  await initCommand();
1118
1678
  });
@@ -1128,6 +1688,12 @@ program.command("link [api-key]").description("authenticate the local agent with
1128
1688
  program.command("push").description("upload pending events to agentreel.dev").action(async () => {
1129
1689
  await pushCommand();
1130
1690
  });
1691
+ program.command("daemon").description("run the background uploader (30s ticks, 1MB early-flush, exponential backoff)").option("--detach", "fork into the background and return immediately; logs to ~/.agentreel/daemon.log").action(async (opts) => {
1692
+ await daemonCommand({ detach: opts.detach });
1693
+ });
1694
+ program.command("stop").description("stop the running background uploader").action(() => {
1695
+ stopCommand();
1696
+ });
1131
1697
  program.command("logout").description("clear local credentials").action(async () => {
1132
1698
  await logoutCommand();
1133
1699
  });