@agentreel/agent 0.1.2 → 0.1.5

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
@@ -121,23 +121,15 @@ var init_install = __esm({
121
121
  }
122
122
  });
123
123
 
124
- // src/cli.ts
125
- import { Command } from "commander";
126
-
127
- // src/commands/init.ts
128
- init_install();
129
- import { realpathSync } from "fs";
130
- import { fileURLToPath } from "url";
131
- import { sep } from "path";
132
- import pc from "picocolors";
133
-
134
124
  // src/config.ts
135
- init_paths();
136
125
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, chmodSync } from "fs";
137
- var DEFAULT = {
138
- apiBaseUrl: "https://api.agentreel.dev",
139
- schemaVersion: 1
140
- };
126
+ function isCaptureDisabledByEnv() {
127
+ const v = process.env.AGENTREEL_DISABLE;
128
+ return v === "1" || v === "true" || v === "yes";
129
+ }
130
+ function isCapturePaused(cfg = readConfig()) {
131
+ return isCaptureDisabledByEnv() || cfg.paused === true;
132
+ }
141
133
  function readConfig() {
142
134
  if (!existsSync2(CONFIG_PATH)) return { ...DEFAULT };
143
135
  try {
@@ -158,11 +150,20 @@ function writeConfig(cfg) {
158
150
  } catch {
159
151
  }
160
152
  }
153
+ var DEFAULT;
154
+ var init_config = __esm({
155
+ "src/config.ts"() {
156
+ "use strict";
157
+ init_paths();
158
+ DEFAULT = {
159
+ apiBaseUrl: "https://api.agentreel.dev",
160
+ schemaVersion: 1
161
+ };
162
+ }
163
+ });
161
164
 
162
165
  // src/db.ts
163
- init_paths();
164
166
  import Database from "better-sqlite3";
165
- var _db = null;
166
167
  function getDb() {
167
168
  if (_db) return _db;
168
169
  ensureAgentreelDir();
@@ -266,79 +267,16 @@ function listRecentSessions(limit = 10) {
266
267
  FROM sessions ORDER BY started_at DESC LIMIT ?`
267
268
  ).all(limit);
268
269
  }
269
-
270
- // src/commands/init.ts
271
- init_paths();
272
- var PACKAGE_NAME = "@agentreel/agent";
273
- function resolveAgentBinary() {
274
- const here = fileURLToPath(import.meta.url);
275
- const real = realpathSync(here);
276
- const isEphemeral = real.includes(`${sep}_npx${sep}`) || real.includes("/_npx/");
277
- return { absolutePath: real, isEphemeral };
278
- }
279
- function chooseHookPrefix(bin) {
280
- if (process.env.AGENTREEL_HOOK_COMMAND) {
281
- return { prefix: process.env.AGENTREEL_HOOK_COMMAND, mode: "absolute" };
282
- }
283
- if (bin.isEphemeral) {
284
- return { prefix: `npx -y ${PACKAGE_NAME}`, mode: "npx" };
285
- }
286
- return { prefix: quotePath(bin.absolutePath), mode: "absolute" };
287
- }
288
- async function initCommand() {
289
- ensureAgentreelDir();
290
- getDb();
291
- const cfg = readConfig();
292
- if (!cfg.installedAt) cfg.installedAt = Date.now();
293
- cfg.hooksInstalled = true;
294
- writeConfig(cfg);
295
- const binary = resolveAgentBinary();
296
- const { prefix, mode } = chooseHookPrefix(binary);
297
- const result = installClaudeCodeHooks(prefix);
298
- console.log(pc.bold(pc.cyan("\n AgentReel ")) + pc.dim("Loom for AI coding sessions\n"));
299
- console.log(pc.green("\u2713") + " Created ~/.agentreel/sessions.db");
300
- console.log(pc.green("\u2713") + " Wrote ~/.agentreel/config.json");
301
- console.log(
302
- pc.green("\u2713") + ` Installed ${result.installedEvents.length} Claude Code hooks \u2192 ~/.claude/settings.json`
303
- );
304
- if (result.backup) {
305
- console.log(pc.dim(` (backup: ${result.backup})`));
306
- }
307
- console.log();
308
- console.log(pc.dim(" Hook command: ") + pc.dim(`${prefix} hook <Event>`));
309
- if (mode === "npx") {
310
- console.log(
311
- pc.dim(" ") + pc.yellow("\u2022") + pc.dim(
312
- ` Hooks resolve via npx every time Claude Code fires an event.
313
- For faster cold starts, run: npm i -g ${PACKAGE_NAME}`
314
- )
315
- );
270
+ var _db;
271
+ var init_db = __esm({
272
+ "src/db.ts"() {
273
+ "use strict";
274
+ init_paths();
275
+ _db = null;
316
276
  }
317
- console.log();
318
- console.log(pc.bold("Next:") + " open Claude Code and run a prompt.");
319
- console.log(" then " + pc.cyan("agentreel status") + " to see captured events.\n");
320
- }
321
-
322
- // src/commands/status.ts
323
- init_paths();
324
- import pc2 from "picocolors";
325
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
277
+ });
326
278
 
327
279
  // src/upload/client.ts
328
- var IngestError = class extends Error {
329
- constructor(message, status, body) {
330
- super(message);
331
- this.status = status;
332
- this.body = body;
333
- this.name = "IngestError";
334
- }
335
- status;
336
- body;
337
- /** 4xx (except 408/429) — payload-shaped problem the agent can't fix by retrying. */
338
- get isPermanent() {
339
- return this.status >= 400 && this.status < 500 && this.status !== 408 && this.status !== 429;
340
- }
341
- };
342
280
  async function postIngest(cfg, body) {
343
281
  if (!cfg.apiKey) {
344
282
  throw new Error("Not linked. Run: agentreel link <key>");
@@ -364,10 +302,28 @@ async function postIngest(cfg, body) {
364
302
  }
365
303
  return data;
366
304
  }
305
+ var IngestError;
306
+ var init_client = __esm({
307
+ "src/upload/client.ts"() {
308
+ "use strict";
309
+ IngestError = class extends Error {
310
+ constructor(message, status, body) {
311
+ super(message);
312
+ this.status = status;
313
+ this.body = body;
314
+ this.name = "IngestError";
315
+ }
316
+ status;
317
+ body;
318
+ /** 4xx (except 408/429) — payload-shaped problem the agent can't fix by retrying. */
319
+ get isPermanent() {
320
+ return this.status >= 400 && this.status < 500 && this.status !== 408 && this.status !== 429;
321
+ }
322
+ };
323
+ }
324
+ });
367
325
 
368
326
  // src/upload/queue.ts
369
- var MAX_BATCH_BYTES = 1024 * 1024;
370
- var MAX_BATCH_EVENTS = 500;
371
327
  function takePendingBatch(maxEvents = MAX_BATCH_EVENTS, maxBytes = MAX_BATCH_BYTES) {
372
328
  const db = getDb();
373
329
  const candidateRows = db.prepare(
@@ -429,15 +385,17 @@ function safeParse(s) {
429
385
  return s;
430
386
  }
431
387
  }
388
+ var MAX_BATCH_BYTES, MAX_BATCH_EVENTS;
389
+ var init_queue = __esm({
390
+ "src/upload/queue.ts"() {
391
+ "use strict";
392
+ init_db();
393
+ MAX_BATCH_BYTES = 1024 * 1024;
394
+ MAX_BATCH_EVENTS = 500;
395
+ }
396
+ });
432
397
 
433
398
  // src/upload/flush.ts
434
- var META_LAST_SYNC_AT = "last_sync_at";
435
- var META_LAST_ERROR_AT = "last_error_at";
436
- var META_LAST_ERROR_MSG = "last_error_msg";
437
- var META_CONSECUTIVE_FAILS = "consecutive_failures";
438
- var META_NEXT_ATTEMPT_AT = "next_attempt_at";
439
- var BACKOFF_BASE_MS = 1e3;
440
- var BACKOFF_MAX_MS = 5 * 6e4;
441
399
  function nextAttemptAt() {
442
400
  const raw = getMeta(META_NEXT_ATTEMPT_AT);
443
401
  return raw ? Number(raw) : 0;
@@ -495,8 +453,326 @@ async function flushOnce() {
495
453
  throw err;
496
454
  }
497
455
  }
456
+ 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;
457
+ var init_flush = __esm({
458
+ "src/upload/flush.ts"() {
459
+ "use strict";
460
+ init_db();
461
+ init_config();
462
+ init_client();
463
+ init_queue();
464
+ META_LAST_SYNC_AT = "last_sync_at";
465
+ META_LAST_ERROR_AT = "last_error_at";
466
+ META_LAST_ERROR_MSG = "last_error_msg";
467
+ META_CONSECUTIVE_FAILS = "consecutive_failures";
468
+ META_NEXT_ATTEMPT_AT = "next_attempt_at";
469
+ BACKOFF_BASE_MS = 1e3;
470
+ BACKOFF_MAX_MS = 5 * 6e4;
471
+ }
472
+ });
473
+
474
+ // src/commands/daemon.ts
475
+ var daemon_exports = {};
476
+ __export(daemon_exports, {
477
+ daemonCommand: () => daemonCommand,
478
+ stopCommand: () => stopCommand
479
+ });
480
+ import pc3 from "picocolors";
481
+ import { spawn } from "child_process";
482
+ import {
483
+ existsSync as existsSync4,
484
+ openSync,
485
+ readFileSync as readFileSync4,
486
+ unlinkSync,
487
+ writeFileSync as writeFileSync3
488
+ } from "fs";
489
+ async function daemonCommand(opts = {}) {
490
+ const cfg = readConfig();
491
+ if (!cfg.apiKey) {
492
+ console.error(pc3.red("\u2717 Not linked. Run ") + pc3.cyan("agentreel link <api-key>"));
493
+ process.exit(1);
494
+ }
495
+ if (opts.detach) {
496
+ spawnDetached();
497
+ return;
498
+ }
499
+ if (!claimPidFile()) {
500
+ process.exit(1);
501
+ }
502
+ const release = () => {
503
+ try {
504
+ unlinkSync(DAEMON_PID_PATH);
505
+ } catch {
506
+ }
507
+ };
508
+ let stopping = false;
509
+ const stop = () => {
510
+ if (stopping) return;
511
+ stopping = true;
512
+ console.log(pc3.dim("\n daemon stopping\u2026"));
513
+ release();
514
+ process.exit(0);
515
+ };
516
+ process.on("SIGINT", stop);
517
+ process.on("SIGTERM", stop);
518
+ process.on("beforeExit", release);
519
+ console.log(pc3.green("\u25CF") + " agentreel daemon running");
520
+ console.log(pc3.dim(` api: ${cfg.apiBaseUrl}`));
521
+ console.log(pc3.dim(` tick: every ${TICK_INTERVAL_MS / 1e3}s, early flush at ${(EARLY_FLUSH_BYTES / 1024).toFixed(0)} KB`));
522
+ console.log(pc3.dim(" Ctrl-C to stop"));
523
+ for (; ; ) {
524
+ if (stopping) return;
525
+ const nextAt = nextAttemptAt();
526
+ const now = Date.now();
527
+ if (nextAt > now) {
528
+ await sleepInterruptible(nextAt - now, () => stopping);
529
+ continue;
530
+ }
531
+ try {
532
+ for (; ; ) {
533
+ const res = await flushOnce();
534
+ if (res.uploadedEvents > 0) {
535
+ console.log(
536
+ pc3.dim(` [${ts()}]`) + pc3.green(" \u2713") + ` ${res.uploadedEvents} events \xB7 ${res.uploadedSessions} sessions`
537
+ );
538
+ }
539
+ if (!res.moreAvailable) break;
540
+ }
541
+ } catch (err) {
542
+ const e = err;
543
+ if (err instanceof IngestError && err.isPermanent) {
544
+ console.error(
545
+ pc3.dim(` [${ts()}]`) + pc3.red(" \u2717 ") + e.message + pc3.dim(" (5min backoff)")
546
+ );
547
+ } else {
548
+ console.error(pc3.dim(` [${ts()}]`) + pc3.yellow(" \xB7 ") + e.message);
549
+ }
550
+ }
551
+ await waitForTickOrPressure(TICK_INTERVAL_MS, EARLY_FLUSH_BYTES, () => stopping);
552
+ }
553
+ }
554
+ function claimPidFile() {
555
+ ensureAgentreelDir();
556
+ if (existsSync4(DAEMON_PID_PATH)) {
557
+ const raw = readFileSync4(DAEMON_PID_PATH, "utf8").trim();
558
+ const pid = Number(raw);
559
+ if (Number.isFinite(pid) && pid > 0 && isAlive(pid)) {
560
+ console.error(pc3.red(`\u2717 daemon already running (pid ${pid})`));
561
+ console.error(pc3.dim(` if this is wrong, remove ${DAEMON_PID_PATH}`));
562
+ return false;
563
+ }
564
+ }
565
+ writeFileSync3(DAEMON_PID_PATH, String(process.pid), { encoding: "utf8", mode: 384 });
566
+ return true;
567
+ }
568
+ function isAlive(pid) {
569
+ try {
570
+ process.kill(pid, 0);
571
+ return true;
572
+ } catch {
573
+ return false;
574
+ }
575
+ }
576
+ function sleepInterruptible(ms, shouldStop) {
577
+ return new Promise((resolve2) => {
578
+ if (ms <= 0) return resolve2();
579
+ const start = Date.now();
580
+ const t = setInterval(() => {
581
+ if (shouldStop() || Date.now() - start >= ms) {
582
+ clearInterval(t);
583
+ resolve2();
584
+ }
585
+ }, 250);
586
+ });
587
+ }
588
+ function spawnDetached() {
589
+ ensureAgentreelDir();
590
+ if (existsSync4(DAEMON_PID_PATH)) {
591
+ const raw = readFileSync4(DAEMON_PID_PATH, "utf8").trim();
592
+ const pid = Number(raw);
593
+ if (Number.isFinite(pid) && pid > 0 && isAlive(pid)) {
594
+ console.error(pc3.red(`\u2717 daemon already running (pid ${pid})`));
595
+ console.error(pc3.dim(" use ") + pc3.cyan("agentreel stop") + pc3.dim(" first"));
596
+ process.exit(1);
597
+ }
598
+ }
599
+ const out = openSync(DAEMON_LOG_PATH, "a");
600
+ const err = openSync(DAEMON_LOG_PATH, "a");
601
+ const child = spawn(process.execPath, [process.argv[1], "daemon"], {
602
+ detached: true,
603
+ stdio: ["ignore", out, err],
604
+ env: process.env
605
+ });
606
+ child.unref();
607
+ console.log(pc3.green("\u25CF") + ` daemon started (pid ${child.pid})`);
608
+ console.log(pc3.dim(` log: ${DAEMON_LOG_PATH}`));
609
+ console.log(pc3.dim(` stop: agentreel stop`));
610
+ }
611
+ function stopCommand() {
612
+ if (!existsSync4(DAEMON_PID_PATH)) {
613
+ console.log(pc3.dim("\xB7 daemon not running"));
614
+ return;
615
+ }
616
+ const raw = readFileSync4(DAEMON_PID_PATH, "utf8").trim();
617
+ const pid = Number(raw);
618
+ if (!Number.isFinite(pid) || pid <= 0) {
619
+ try {
620
+ unlinkSync(DAEMON_PID_PATH);
621
+ } catch {
622
+ }
623
+ console.log(pc3.dim("\xB7 cleared stale pid file"));
624
+ return;
625
+ }
626
+ if (!isAlive(pid)) {
627
+ try {
628
+ unlinkSync(DAEMON_PID_PATH);
629
+ } catch {
630
+ }
631
+ console.log(pc3.dim(`\xB7 no live daemon for pid ${pid} \u2014 cleared stale pid file`));
632
+ return;
633
+ }
634
+ try {
635
+ process.kill(pid, "SIGTERM");
636
+ console.log(pc3.green("\u2713") + ` sent SIGTERM to pid ${pid}`);
637
+ } catch (err) {
638
+ console.error(pc3.red("\u2717 ") + err.message);
639
+ process.exit(1);
640
+ }
641
+ }
642
+ function waitForTickOrPressure(tickMs, pressureBytes, shouldStop) {
643
+ return new Promise((resolve2) => {
644
+ const start = Date.now();
645
+ const t = setInterval(() => {
646
+ if (shouldStop()) {
647
+ clearInterval(t);
648
+ return resolve2();
649
+ }
650
+ if (Date.now() - start >= tickMs) {
651
+ clearInterval(t);
652
+ return resolve2();
653
+ }
654
+ try {
655
+ if (pendingByteSize() >= pressureBytes) {
656
+ clearInterval(t);
657
+ return resolve2();
658
+ }
659
+ } catch {
660
+ }
661
+ }, 1e3);
662
+ });
663
+ }
664
+ function ts() {
665
+ const d = /* @__PURE__ */ new Date();
666
+ return d.toTimeString().slice(0, 8);
667
+ }
668
+ var TICK_INTERVAL_MS, EARLY_FLUSH_BYTES;
669
+ var init_daemon = __esm({
670
+ "src/commands/daemon.ts"() {
671
+ "use strict";
672
+ init_paths();
673
+ init_db();
674
+ init_flush();
675
+ init_queue();
676
+ init_client();
677
+ init_config();
678
+ TICK_INTERVAL_MS = 3e4;
679
+ EARLY_FLUSH_BYTES = MAX_BATCH_BYTES;
680
+ }
681
+ });
682
+
683
+ // src/cli.ts
684
+ import { Command } from "commander";
685
+
686
+ // src/commands/init.ts
687
+ init_install();
688
+ init_config();
689
+ init_db();
690
+ init_paths();
691
+ import { realpathSync } from "fs";
692
+ import { fileURLToPath } from "url";
693
+ import { sep } from "path";
694
+ import pc from "picocolors";
695
+ var PACKAGE_NAME = "@agentreel/agent";
696
+ function resolveAgentBinary() {
697
+ const here = fileURLToPath(import.meta.url);
698
+ const real = realpathSync(here);
699
+ const isEphemeral = real.includes(`${sep}_npx${sep}`) || real.includes("/_npx/");
700
+ return { absolutePath: real, isEphemeral };
701
+ }
702
+ function chooseHookPrefix(bin) {
703
+ if (process.env.AGENTREEL_HOOK_COMMAND) {
704
+ return { prefix: process.env.AGENTREEL_HOOK_COMMAND, mode: "absolute" };
705
+ }
706
+ if (bin.isEphemeral) {
707
+ return { prefix: `npx -y ${PACKAGE_NAME}`, mode: "npx" };
708
+ }
709
+ return { prefix: quotePath(bin.absolutePath), mode: "absolute" };
710
+ }
711
+ async function initCommand() {
712
+ ensureAgentreelDir();
713
+ getDb();
714
+ const cfg = readConfig();
715
+ if (!cfg.installedAt) cfg.installedAt = Date.now();
716
+ cfg.hooksInstalled = true;
717
+ writeConfig(cfg);
718
+ const binary = resolveAgentBinary();
719
+ const { prefix, mode } = chooseHookPrefix(binary);
720
+ const result = installClaudeCodeHooks(prefix);
721
+ console.log(pc.bold(pc.cyan("\n AgentReel ")) + pc.dim("Loom for AI coding sessions\n"));
722
+ console.log(pc.green("\u2713") + " Created ~/.agentreel/sessions.db");
723
+ console.log(pc.green("\u2713") + " Wrote ~/.agentreel/config.json");
724
+ console.log(
725
+ pc.green("\u2713") + ` Installed ${result.installedEvents.length} Claude Code hooks \u2192 ~/.claude/settings.json`
726
+ );
727
+ if (result.backup) {
728
+ console.log(pc.dim(` (backup: ${result.backup})`));
729
+ }
730
+ console.log();
731
+ console.log(pc.dim(" Hook command: ") + pc.dim(`${prefix} hook <Event>`));
732
+ if (mode === "npx") {
733
+ console.log(
734
+ pc.dim(" ") + pc.yellow("\u2022") + pc.dim(
735
+ ` Hooks resolve via npx every time Claude Code fires an event.
736
+ For faster cold starts, run: npm i -g ${PACKAGE_NAME}`
737
+ )
738
+ );
739
+ }
740
+ console.log();
741
+ if (!cfg.apiKey) {
742
+ console.log(pc.bold("Next steps:"));
743
+ console.log(
744
+ " " + pc.dim("1.") + " " + pc.cyan("agentreel link <key>") + pc.dim(" \u2014 get a key from https://agentreel.dev/dashboard/settings")
745
+ );
746
+ console.log(
747
+ " " + pc.dim("2.") + " " + pc.cyan("agentreel daemon --detach") + pc.dim(" \u2014 start the background uploader")
748
+ );
749
+ console.log(
750
+ " " + pc.dim("3.") + " open Claude Code and work as normal."
751
+ );
752
+ } else {
753
+ console.log(pc.bold("Next:"));
754
+ console.log(
755
+ " " + pc.cyan("agentreel daemon --detach") + pc.dim(" \u2014 start the background uploader (already linked)")
756
+ );
757
+ }
758
+ console.log(
759
+ "\n" + pc.dim("Anything: ") + pc.cyan("agentreel status") + pc.dim(" shows queue, last sync, last error.")
760
+ );
761
+ console.log(
762
+ pc.dim("Privacy: ") + pc.cyan("agentreel pause") + pc.dim(" to stop capturing temporarily, ") + pc.cyan("agentreel forget --all") + pc.dim(" to wipe local sessions.")
763
+ );
764
+ console.log(
765
+ pc.dim(" Or set ") + pc.cyan("AGENTREEL_DISABLE=1") + pc.dim(" before launching Claude Code to skip a single session.\n")
766
+ );
767
+ }
498
768
 
499
769
  // src/commands/status.ts
770
+ init_paths();
771
+ init_db();
772
+ init_config();
773
+ init_flush();
774
+ import pc2 from "picocolors";
775
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
500
776
  function fmtDuration(ms) {
501
777
  const s = Math.round(ms / 1e3);
502
778
  if (s < 60) return `${s}s`;
@@ -534,6 +810,15 @@ async function statusCommand() {
534
810
  );
535
811
  console.log(" api base: " + cfg.apiBaseUrl);
536
812
  console.log(" authenticated: " + (cfg.apiKey ? pc2.green("yes") : pc2.yellow("no")));
813
+ if (cfg.paused) {
814
+ const since = cfg.pausedAt ? new Date(cfg.pausedAt).toLocaleString() : "?";
815
+ console.log(" capture: " + pc2.yellow(`paused (since ${since})`));
816
+ console.log(pc2.dim(" run `agentreel resume` to capture again"));
817
+ } else if (isCaptureDisabledByEnv()) {
818
+ console.log(" capture: " + pc2.yellow("disabled (AGENTREEL_DISABLE in env)"));
819
+ } else {
820
+ console.log(" capture: " + pc2.green("active"));
821
+ }
537
822
  const d = daemonStatus();
538
823
  console.log(
539
824
  " daemon: " + (d.running ? pc2.green(`running (pid ${d.pid})`) : pc2.dim("stopped"))
@@ -587,44 +872,48 @@ function numMeta(key) {
587
872
  }
588
873
 
589
874
  // src/commands/auth.ts
590
- import pc3 from "picocolors";
875
+ init_config();
876
+ init_client();
877
+ import pc4 from "picocolors";
591
878
  async function logoutCommand() {
592
879
  const cfg = readConfig();
593
880
  cfg.apiKey = void 0;
594
881
  cfg.workspaceId = void 0;
595
882
  writeConfig(cfg);
596
- console.log(pc3.green("\u2713") + " Cleared local credentials.");
883
+ console.log(pc4.green("\u2713") + " Cleared local credentials.");
597
884
  }
598
885
  async function linkCommand(rawKey, opts) {
599
886
  const key = (rawKey ?? await promptHidden("Paste your AgentReel API key: ")).trim();
600
887
  if (!key) {
601
- console.error(pc3.red("\u2717 No key provided."));
888
+ console.error(pc4.red("\u2717 No key provided."));
602
889
  process.exit(1);
603
890
  }
604
891
  if (!key.startsWith("ar_live_")) {
605
- console.error(pc3.red("\u2717 Keys start with `ar_live_`. Did you paste the right value?"));
892
+ console.error(pc4.red("\u2717 Keys start with `ar_live_`. Did you paste the right value?"));
606
893
  process.exit(1);
607
894
  }
608
895
  const cfg = readConfig();
609
896
  cfg.apiKey = key;
610
897
  if (opts.api) cfg.apiBaseUrl = opts.api;
611
- console.log(pc3.dim(` validating against ${cfg.apiBaseUrl}\u2026`));
898
+ console.log(pc4.dim(` validating against ${cfg.apiBaseUrl}\u2026`));
612
899
  try {
613
900
  await postIngest(cfg, {});
614
901
  } catch (err) {
615
- console.error(pc3.red("\u2717 Validation failed: ") + err.message);
902
+ console.error(pc4.red("\u2717 Validation failed: ") + err.message);
616
903
  process.exit(1);
617
904
  }
618
905
  writeConfig(cfg);
619
- console.log(pc3.green("\u2713") + " Linked.");
620
- console.log(pc3.dim(" api: ") + cfg.apiBaseUrl);
621
- console.log(pc3.dim(" key: ") + key.slice(0, 12) + "\u2026");
906
+ console.log(pc4.green("\u2713") + " Linked.");
907
+ console.log(pc4.dim(" api: ") + cfg.apiBaseUrl);
908
+ console.log(pc4.dim(" key: ") + key.slice(0, 12) + "\u2026");
622
909
  }
623
910
  async function uninstallCommand() {
624
911
  const { uninstallClaudeCodeHooks: uninstallClaudeCodeHooks2 } = await Promise.resolve().then(() => (init_install(), install_exports));
912
+ const { stopCommand: stopCommand2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
913
+ stopCommand2();
625
914
  const { backup } = uninstallClaudeCodeHooks2();
626
- console.log(pc3.green("\u2713") + " Removed AgentReel hooks from ~/.claude/settings.json");
627
- if (backup) console.log(pc3.dim(` (backup: ${backup})`));
915
+ console.log(pc4.green("\u2713") + " Removed AgentReel hooks from ~/.claude/settings.json");
916
+ if (backup) console.log(pc4.dim(` (backup: ${backup})`));
628
917
  }
629
918
  var CTRL_C = 3;
630
919
  var BACKSPACE = 127;
@@ -665,8 +954,8 @@ function promptHidden(prompt) {
665
954
  }
666
955
 
667
956
  // src/commands/watch.ts
668
- import pc4 from "picocolors";
669
- import { existsSync as existsSync7 } from "fs";
957
+ import pc5 from "picocolors";
958
+ import { existsSync as existsSync8 } from "fs";
670
959
  import { basename as basename2 } from "path";
671
960
 
672
961
  // src/cursor/paths.ts
@@ -692,17 +981,17 @@ function cursorHistoryDir() {
692
981
  // src/cursor/watcher.ts
693
982
  import chokidar from "chokidar";
694
983
  import { readFile as readFile2 } from "fs/promises";
695
- import { existsSync as existsSync6 } from "fs";
984
+ import { existsSync as existsSync7 } from "fs";
696
985
  import { basename, dirname as dirname3, join as join5 } from "path";
697
986
 
698
987
  // src/cursor/entries.ts
699
988
  import { readFile } from "fs/promises";
700
- import { existsSync as existsSync4, statSync } from "fs";
989
+ import { existsSync as existsSync5, statSync } from "fs";
701
990
  import { join as join3, dirname as dirname2, resolve, sep as sep2 } from "path";
702
991
  import { fileURLToPath as fileURLToPath2 } from "url";
703
992
  async function readEntries(historyFolder) {
704
993
  const path = join3(historyFolder, "entries.json");
705
- if (!existsSync4(path)) return null;
994
+ if (!existsSync5(path)) return null;
706
995
  try {
707
996
  const raw = await readFile(path, "utf8");
708
997
  return JSON.parse(raw);
@@ -723,7 +1012,7 @@ function findWorkspaceRoot(path) {
723
1012
  while (dir && dir !== sep2) {
724
1013
  const git = join3(dir, ".git");
725
1014
  try {
726
- if (existsSync4(git)) return dir;
1015
+ if (existsSync5(git)) return dir;
727
1016
  } catch {
728
1017
  }
729
1018
  const parent = dirname2(dir);
@@ -762,7 +1051,7 @@ function computeDiff(before, after) {
762
1051
  }
763
1052
 
764
1053
  // src/redact/ignore.ts
765
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
1054
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
766
1055
  import { join as join4, relative, sep as sep3 } from "path";
767
1056
  import ignore from "ignore";
768
1057
  var FILENAME = ".agentreelignore";
@@ -770,9 +1059,9 @@ var TTL_MS = 5e3;
770
1059
  var cache = /* @__PURE__ */ new Map();
771
1060
  function loadFor(workspace) {
772
1061
  const path = join4(workspace, FILENAME);
773
- if (!existsSync5(path)) return null;
1062
+ if (!existsSync6(path)) return null;
774
1063
  try {
775
- const raw = readFileSync4(path, "utf8");
1064
+ const raw = readFileSync5(path, "utf8");
776
1065
  return ignore({ allowRelativePaths: true }).add(raw);
777
1066
  } catch {
778
1067
  return null;
@@ -783,7 +1072,7 @@ function getCached(workspace) {
783
1072
  const path = join4(workspace, FILENAME);
784
1073
  let mtime = 0;
785
1074
  try {
786
- mtime = existsSync5(path) ? statSync2(path).mtimeMs : 0;
1075
+ mtime = existsSync6(path) ? statSync2(path).mtimeMs : 0;
787
1076
  } catch {
788
1077
  mtime = 0;
789
1078
  }
@@ -1006,11 +1295,11 @@ async function processSnapshot(folder, ctx, skipFirst, onSnapshot) {
1006
1295
  let after = "";
1007
1296
  if (ctx.previous) {
1008
1297
  const prevPath = join5(folder, ctx.previous.id);
1009
- if (existsSync6(prevPath)) {
1298
+ if (existsSync7(prevPath)) {
1010
1299
  before = await safeRead(prevPath);
1011
1300
  }
1012
1301
  }
1013
- if (existsSync6(newSnapshot)) {
1302
+ if (existsSync7(newSnapshot)) {
1014
1303
  after = await safeRead(newSnapshot);
1015
1304
  }
1016
1305
  if (before === after) return;
@@ -1057,12 +1346,17 @@ function sleep(ms) {
1057
1346
  var NOISY_PATH = /[\\/](node_modules|\.next|\.turbo|dist|build|\.git|coverage|\.cache|\.venv|venv|target|out)[\\/]/;
1058
1347
 
1059
1348
  // src/cursor/session.ts
1349
+ init_db();
1350
+ init_config();
1060
1351
  import { nanoid } from "nanoid";
1061
1352
  var IDLE_MS = 5 * 60 * 1e3;
1062
1353
  var GLOBAL_KEY = "__global__";
1063
1354
  var CursorSessionManager = class {
1064
1355
  open = /* @__PURE__ */ new Map();
1065
1356
  ingest(snapshot) {
1357
+ if (isCapturePaused()) {
1358
+ return { sessionId: "__paused__", isNew: false };
1359
+ }
1066
1360
  const key = snapshot.workspace ?? GLOBAL_KEY;
1067
1361
  const ts2 = snapshot.timestamp;
1068
1362
  const cwd = snapshot.workspace ?? "";
@@ -1126,38 +1420,39 @@ var CursorSessionManager = class {
1126
1420
 
1127
1421
  // src/commands/watch.ts
1128
1422
  init_paths();
1423
+ init_db();
1129
1424
  async function watchCommand() {
1130
1425
  ensureAgentreelDir();
1131
1426
  getDb();
1132
1427
  const dir = cursorHistoryDir();
1133
- if (!existsSync7(dir)) {
1134
- console.error(pc4.red("\u2717 Cursor history directory not found:"));
1428
+ if (!existsSync8(dir)) {
1429
+ console.error(pc5.red("\u2717 Cursor history directory not found:"));
1135
1430
  console.error(" " + dir);
1136
1431
  console.error();
1137
- console.error(pc4.dim("Open Cursor at least once, edit a file, then re-run."));
1138
- console.error(pc4.dim("(Or set AGENTREEL_CURSOR_HISTORY_DIR to a custom path.)"));
1432
+ console.error(pc5.dim("Open Cursor at least once, edit a file, then re-run."));
1433
+ console.error(pc5.dim("(Or set AGENTREEL_CURSOR_HISTORY_DIR to a custom path.)"));
1139
1434
  process.exit(1);
1140
1435
  }
1141
- console.log(pc4.bold(pc4.cyan("AgentReel \xB7 Cursor watcher\n")));
1142
- console.log(pc4.dim(" watching ") + dir);
1143
- console.log(pc4.dim(" press Ctrl+C to stop\n"));
1436
+ console.log(pc5.bold(pc5.cyan("AgentReel \xB7 Cursor watcher\n")));
1437
+ console.log(pc5.dim(" watching ") + dir);
1438
+ console.log(pc5.dim(" press Ctrl+C to stop\n"));
1144
1439
  const sessions = new CursorSessionManager();
1145
1440
  const watcher = startCursorWatcher(dir, async (snap) => {
1146
1441
  const { sessionId, isNew } = sessions.ingest(snap);
1147
1442
  const ts2 = new Date(snap.timestamp).toLocaleTimeString();
1148
- const ws = snap.workspace ? basename2(snap.workspace) : pc4.dim("no-workspace");
1443
+ const ws = snap.workspace ? basename2(snap.workspace) : pc5.dim("no-workspace");
1149
1444
  const file = snap.filePath.split("/").slice(-2).join("/");
1150
- const sourceLabel = snap.source === "cursor-ai" ? pc4.magenta("ai") : snap.source === "cursor-manual" ? pc4.cyan("man") : pc4.dim("?");
1151
- const stats = snap.binary ? pc4.dim("binary") : `${pc4.green("+" + snap.added)} ${pc4.red("-" + snap.removed)}`;
1445
+ const sourceLabel = snap.source === "cursor-ai" ? pc5.magenta("ai") : snap.source === "cursor-manual" ? pc5.cyan("man") : pc5.dim("?");
1446
+ const stats = snap.binary ? pc5.dim("binary") : `${pc5.green("+" + snap.added)} ${pc5.red("-" + snap.removed)}`;
1152
1447
  if (isNew) {
1153
1448
  console.log(
1154
- `${pc4.dim(ts2)} ${pc4.yellow("session")} ${pc4.dim(sessionId)} ${ws}`
1449
+ `${pc5.dim(ts2)} ${pc5.yellow("session")} ${pc5.dim(sessionId)} ${ws}`
1155
1450
  );
1156
1451
  }
1157
- console.log(`${pc4.dim(ts2)} edit ${sourceLabel} ${file.padEnd(36)} ${stats}`);
1452
+ console.log(`${pc5.dim(ts2)} edit ${sourceLabel} ${file.padEnd(36)} ${stats}`);
1158
1453
  });
1159
1454
  const shutdown = async () => {
1160
- console.log(pc4.dim("\n closing sessions\u2026"));
1455
+ console.log(pc5.dim("\n closing sessions\u2026"));
1161
1456
  sessions.closeAll();
1162
1457
  await watcher.close();
1163
1458
  process.exit(0);
@@ -1167,11 +1462,14 @@ async function watchCommand() {
1167
1462
  }
1168
1463
 
1169
1464
  // src/commands/push.ts
1170
- import pc5 from "picocolors";
1465
+ init_config();
1466
+ init_flush();
1467
+ init_client();
1468
+ import pc6 from "picocolors";
1171
1469
  async function pushCommand() {
1172
1470
  const cfg = readConfig();
1173
1471
  if (!cfg.apiKey) {
1174
- console.error(pc5.red("\u2717 Not linked. Run ") + pc5.cyan("agentreel link <api-key>"));
1472
+ console.error(pc6.red("\u2717 Not linked. Run ") + pc6.cyan("agentreel link <api-key>"));
1175
1473
  process.exit(1);
1176
1474
  }
1177
1475
  let totalSessions = 0;
@@ -1183,14 +1481,14 @@ async function pushCommand() {
1183
1481
  } catch (err) {
1184
1482
  const e = err;
1185
1483
  if (err instanceof IngestError) {
1186
- console.error(pc5.red("\u2717 ") + e.message);
1484
+ console.error(pc6.red("\u2717 ") + e.message);
1187
1485
  if (err.isPermanent) {
1188
1486
  console.error(
1189
- pc5.dim(" not retrying \u2014 fix the cause (re-link, upgrade plan, etc.) and run push again.")
1487
+ pc6.dim(" not retrying \u2014 fix the cause (re-link, upgrade plan, etc.) and run push again.")
1190
1488
  );
1191
1489
  }
1192
1490
  } else {
1193
- console.error(pc5.red("\u2717 ") + e.message);
1491
+ console.error(pc6.red("\u2717 ") + e.message);
1194
1492
  }
1195
1493
  process.exit(1);
1196
1494
  }
@@ -1199,146 +1497,253 @@ async function pushCommand() {
1199
1497
  if (!res.moreAvailable) break;
1200
1498
  }
1201
1499
  if (totalEvents === 0) {
1202
- console.log(pc5.green("\u2713") + " queue is empty \u2014 nothing to upload.");
1500
+ console.log(pc6.green("\u2713") + " queue is empty \u2014 nothing to upload.");
1203
1501
  return;
1204
1502
  }
1205
1503
  console.log(
1206
- pc5.green("\u2713") + ` uploaded ${totalEvents} events \xB7 ${totalSessions} session rows touched`
1504
+ pc6.green("\u2713") + ` uploaded ${totalEvents} events \xB7 ${totalSessions} session rows touched`
1207
1505
  );
1208
1506
  }
1209
1507
 
1210
- // src/commands/daemon.ts
1211
- init_paths();
1212
- import pc6 from "picocolors";
1213
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3, unlinkSync } from "fs";
1214
- var TICK_INTERVAL_MS = 3e4;
1215
- var EARLY_FLUSH_BYTES = MAX_BATCH_BYTES;
1216
- async function daemonCommand() {
1508
+ // src/cli.ts
1509
+ init_daemon();
1510
+
1511
+ // src/commands/privacy.ts
1512
+ init_config();
1513
+ init_db();
1514
+ import pc7 from "picocolors";
1515
+ import { createInterface } from "readline";
1516
+ function pauseCommand() {
1217
1517
  const cfg = readConfig();
1218
- if (!cfg.apiKey) {
1219
- console.error(pc6.red("\u2717 Not linked. Run ") + pc6.cyan("agentreel link <api-key>"));
1220
- process.exit(1);
1518
+ if (cfg.paused) {
1519
+ console.log(pc7.dim("\xB7 already paused"));
1520
+ if (cfg.pausedAt) {
1521
+ console.log(pc7.dim(` since ${new Date(cfg.pausedAt).toLocaleString()}`));
1522
+ }
1523
+ return;
1221
1524
  }
1222
- if (!claimPidFile()) {
1223
- process.exit(1);
1525
+ cfg.paused = true;
1526
+ cfg.pausedAt = Date.now();
1527
+ writeConfig(cfg);
1528
+ console.log(pc7.yellow("\u23F8 capture paused"));
1529
+ console.log(
1530
+ pc7.dim(
1531
+ " Claude Code hooks and the Cursor watcher will no-op until you " + pc7.cyan("agentreel resume") + pc7.dim(".")
1532
+ )
1533
+ );
1534
+ console.log(
1535
+ pc7.dim(
1536
+ " Existing local events stay put. They won't upload while paused (the daemon still runs, but the queue won't grow)."
1537
+ )
1538
+ );
1539
+ }
1540
+ function resumeCommand() {
1541
+ const cfg = readConfig();
1542
+ if (!cfg.paused) {
1543
+ console.log(pc7.dim("\xB7 capture is already active"));
1544
+ return;
1224
1545
  }
1225
- const release = () => {
1226
- try {
1227
- unlinkSync(DAEMON_PID_PATH);
1228
- } catch {
1229
- }
1230
- };
1231
- let stopping = false;
1232
- const stop = () => {
1233
- if (stopping) return;
1234
- stopping = true;
1235
- console.log(pc6.dim("\n daemon stopping\u2026"));
1236
- release();
1237
- process.exit(0);
1238
- };
1239
- process.on("SIGINT", stop);
1240
- process.on("SIGTERM", stop);
1241
- process.on("beforeExit", release);
1242
- console.log(pc6.green("\u25CF") + " agentreel daemon running");
1243
- console.log(pc6.dim(` api: ${cfg.apiBaseUrl}`));
1244
- console.log(pc6.dim(` tick: every ${TICK_INTERVAL_MS / 1e3}s, early flush at ${(EARLY_FLUSH_BYTES / 1024).toFixed(0)} KB`));
1245
- console.log(pc6.dim(" Ctrl-C to stop"));
1246
- for (; ; ) {
1247
- if (stopping) return;
1248
- const nextAt = nextAttemptAt();
1249
- const now = Date.now();
1250
- if (nextAt > now) {
1251
- await sleepInterruptible(nextAt - now, () => stopping);
1252
- continue;
1546
+ const pausedAt = cfg.pausedAt;
1547
+ cfg.paused = false;
1548
+ cfg.pausedAt = void 0;
1549
+ writeConfig(cfg);
1550
+ const duration = pausedAt ? fmtDuration2(Date.now() - pausedAt) : null;
1551
+ console.log(pc7.green("\u25B6 capture resumed"));
1552
+ if (duration) {
1553
+ console.log(pc7.dim(` paused for ${duration}`));
1554
+ }
1555
+ }
1556
+ async function forgetCommand(ids, opts) {
1557
+ const db = getDb();
1558
+ if (opts.all) {
1559
+ const total = db.prepare(`SELECT COUNT(*) AS c FROM sessions`).get().c;
1560
+ if (total === 0) {
1561
+ console.log(pc7.dim("\xB7 nothing to forget \u2014 local DB is empty"));
1562
+ return;
1253
1563
  }
1254
- try {
1255
- for (; ; ) {
1256
- const res = await flushOnce();
1257
- if (res.uploadedEvents > 0) {
1258
- console.log(
1259
- pc6.dim(` [${ts()}]`) + pc6.green(" \u2713") + ` ${res.uploadedEvents} events \xB7 ${res.uploadedSessions} sessions`
1260
- );
1261
- }
1262
- if (!res.moreAvailable) break;
1263
- }
1264
- } catch (err) {
1265
- const e = err;
1266
- if (err instanceof IngestError && err.isPermanent) {
1267
- console.error(
1268
- pc6.dim(` [${ts()}]`) + pc6.red(" \u2717 ") + e.message + pc6.dim(" (5min backoff)")
1269
- );
1270
- } else {
1271
- console.error(pc6.dim(` [${ts()}]`) + pc6.yellow(" \xB7 ") + e.message);
1272
- }
1564
+ if (!opts.force && !await confirm(`Delete all ${total} local sessions? `)) {
1565
+ console.log(pc7.dim("\xB7 cancelled"));
1566
+ return;
1273
1567
  }
1274
- await waitForTickOrPressure(TICK_INTERVAL_MS, EARLY_FLUSH_BYTES, () => stopping);
1568
+ db.transaction(() => {
1569
+ db.exec(`DELETE FROM events`);
1570
+ db.exec(`DELETE FROM sessions`);
1571
+ db.exec(`DELETE FROM meta WHERE key LIKE 'cost_%'`);
1572
+ })();
1573
+ console.log(pc7.green("\u2713") + ` forgot ${total} sessions and their events`);
1574
+ console.log(
1575
+ pc7.dim(
1576
+ " Cloud-side rows are untouched. Delete them from the dashboard if you also want them gone from agentreel.dev."
1577
+ )
1578
+ );
1579
+ return;
1275
1580
  }
1276
- }
1277
- function claimPidFile() {
1278
- ensureAgentreelDir();
1279
- if (existsSync8(DAEMON_PID_PATH)) {
1280
- const raw = readFileSync5(DAEMON_PID_PATH, "utf8").trim();
1281
- const pid = Number(raw);
1282
- if (Number.isFinite(pid) && pid > 0 && isAlive(pid)) {
1283
- console.error(pc6.red(`\u2717 daemon already running (pid ${pid})`));
1284
- console.error(pc6.dim(` if this is wrong, remove ${DAEMON_PID_PATH}`));
1285
- return false;
1581
+ if (ids.length === 0) {
1582
+ console.error(
1583
+ pc7.red("\u2717 provide one or more session ids, or --all to wipe everything")
1584
+ );
1585
+ console.error(pc7.dim(` example: agentreel forget abcd1234`));
1586
+ process.exit(1);
1587
+ }
1588
+ const stmt = db.prepare(
1589
+ `SELECT id FROM sessions WHERE id = ? OR id LIKE ?`
1590
+ );
1591
+ const delEvents = db.prepare(`DELETE FROM events WHERE session_id = ?`);
1592
+ const delSession = db.prepare(`DELETE FROM sessions WHERE id = ?`);
1593
+ const delMeta = db.prepare(`DELETE FROM meta WHERE key LIKE ?`);
1594
+ const matched = [];
1595
+ for (const raw of ids) {
1596
+ const rows = stmt.all(raw, `${raw}%`);
1597
+ if (rows.length === 0) {
1598
+ console.error(pc7.yellow(`\xB7 no session matched "${raw}"`));
1599
+ continue;
1286
1600
  }
1601
+ for (const r of rows) matched.push(r.id);
1287
1602
  }
1288
- writeFileSync3(DAEMON_PID_PATH, String(process.pid), { encoding: "utf8", mode: 384 });
1289
- return true;
1290
- }
1291
- function isAlive(pid) {
1292
- try {
1293
- process.kill(pid, 0);
1294
- return true;
1295
- } catch {
1296
- return false;
1603
+ if (matched.length === 0) {
1604
+ console.error(pc7.red("\u2717 nothing matched, nothing deleted"));
1605
+ process.exit(1);
1297
1606
  }
1607
+ if (!opts.force && !await confirm(`Delete ${matched.length} session(s) and their events? `)) {
1608
+ console.log(pc7.dim("\xB7 cancelled"));
1609
+ return;
1610
+ }
1611
+ db.transaction(() => {
1612
+ for (const id of matched) {
1613
+ delEvents.run(id);
1614
+ delSession.run(id);
1615
+ delMeta.run(`cost_%:${id}`);
1616
+ }
1617
+ })();
1618
+ console.log(pc7.green("\u2713") + ` forgot ${matched.length} session(s)`);
1298
1619
  }
1299
- function sleepInterruptible(ms, shouldStop) {
1300
- return new Promise((resolve2) => {
1301
- if (ms <= 0) return resolve2();
1302
- const start = Date.now();
1303
- const t = setInterval(() => {
1304
- if (shouldStop() || Date.now() - start >= ms) {
1305
- clearInterval(t);
1306
- resolve2();
1307
- }
1308
- }, 250);
1309
- });
1310
- }
1311
- function waitForTickOrPressure(tickMs, pressureBytes, shouldStop) {
1620
+ function confirm(prompt) {
1312
1621
  return new Promise((resolve2) => {
1313
- const start = Date.now();
1314
- const t = setInterval(() => {
1315
- if (shouldStop()) {
1316
- clearInterval(t);
1317
- return resolve2();
1318
- }
1319
- if (Date.now() - start >= tickMs) {
1320
- clearInterval(t);
1321
- return resolve2();
1322
- }
1323
- try {
1324
- if (pendingByteSize() >= pressureBytes) {
1325
- clearInterval(t);
1326
- return resolve2();
1327
- }
1328
- } catch {
1329
- }
1330
- }, 1e3);
1622
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1623
+ rl.question(prompt + "[y/N] ", (answer) => {
1624
+ rl.close();
1625
+ resolve2(/^y(es)?$/i.test(answer.trim()));
1626
+ });
1331
1627
  });
1332
1628
  }
1333
- function ts() {
1334
- const d = /* @__PURE__ */ new Date();
1335
- return d.toTimeString().slice(0, 8);
1629
+ function fmtDuration2(ms) {
1630
+ const s = Math.round(ms / 1e3);
1631
+ if (s < 60) return `${s}s`;
1632
+ const m = Math.floor(s / 60);
1633
+ if (m < 60) return `${m}m ${s % 60}s`;
1634
+ const h = Math.floor(m / 60);
1635
+ if (h < 24) return `${h}h ${m % 60}m`;
1636
+ const d = Math.floor(h / 24);
1637
+ return `${d}d ${h % 24}h`;
1336
1638
  }
1337
1639
 
1338
1640
  // src/hooks/handler.ts
1641
+ init_db();
1642
+ init_paths();
1339
1643
  import { nanoid as nanoid2 } from "nanoid";
1340
1644
  import { appendFileSync } from "fs";
1341
- init_paths();
1645
+
1646
+ // src/cost/transcript.ts
1647
+ init_db();
1648
+ import { existsSync as existsSync9, statSync as statSync3, openSync as openSync2, readSync, closeSync } from "fs";
1649
+
1650
+ // src/cost/pricing.ts
1651
+ var MODEL_RATES = {
1652
+ // Claude 4.x
1653
+ "claude-opus-4-7": { input: 1500, output: 7500, cacheRead: 150, cacheCreation: 1875 },
1654
+ "claude-opus-4-6": { input: 1500, output: 7500, cacheRead: 150, cacheCreation: 1875 },
1655
+ "claude-sonnet-4-6": { input: 300, output: 1500, cacheRead: 30, cacheCreation: 375 },
1656
+ "claude-sonnet-4-5": { input: 300, output: 1500, cacheRead: 30, cacheCreation: 375 },
1657
+ "claude-haiku-4-5": { input: 80, output: 400, cacheRead: 8, cacheCreation: 100 },
1658
+ // Legacy 3.x (still seen in older transcripts)
1659
+ "claude-3-5-sonnet": { input: 300, output: 1500, cacheRead: 30, cacheCreation: 375 },
1660
+ "claude-3-5-haiku": { input: 80, output: 400, cacheRead: 8, cacheCreation: 100 },
1661
+ "claude-3-opus": { input: 1500, output: 7500, cacheRead: 150, cacheCreation: 1875 }
1662
+ };
1663
+ var FALLBACK = MODEL_RATES["claude-sonnet-4-6"];
1664
+ function ratesFor(model) {
1665
+ if (!model) return FALLBACK;
1666
+ if (MODEL_RATES[model]) return MODEL_RATES[model];
1667
+ const stripped = model.replace(/-\d{8}$/, "");
1668
+ if (MODEL_RATES[stripped]) return MODEL_RATES[stripped];
1669
+ for (const key of Object.keys(MODEL_RATES)) {
1670
+ if (model.startsWith(key)) return MODEL_RATES[key];
1671
+ }
1672
+ return FALLBACK;
1673
+ }
1674
+ function costCentsFor(usage, rates) {
1675
+ const c = (usage.inputTokens * rates.input + usage.outputTokens * rates.output + usage.cacheReadTokens * rates.cacheRead + usage.cacheCreationTokens * rates.cacheCreation) / 1e6;
1676
+ return Math.ceil(c);
1677
+ }
1678
+
1679
+ // src/cost/transcript.ts
1680
+ var KEY_OFFSET = (sid) => `cost_offset:${sid}`;
1681
+ var KEY_TOKENS = (sid) => `cost_tokens:${sid}`;
1682
+ var KEY_CENTS = (sid) => `cost_cents:${sid}`;
1683
+ var KEY_PATH = (sid) => `cost_path:${sid}`;
1684
+ function recomputeSessionCost(sessionId, transcriptPath) {
1685
+ if (!transcriptPath) {
1686
+ transcriptPath = getMeta(KEY_PATH(sessionId)) ?? void 0;
1687
+ } else {
1688
+ setMeta(KEY_PATH(sessionId), transcriptPath);
1689
+ }
1690
+ if (!transcriptPath || !existsSync9(transcriptPath)) return;
1691
+ const stat = statSync3(transcriptPath);
1692
+ const totalSize = stat.size;
1693
+ const offset = Number(getMeta(KEY_OFFSET(sessionId)) ?? "0");
1694
+ if (offset >= totalSize) return;
1695
+ const buf = Buffer.alloc(totalSize - offset);
1696
+ const fd = openSync2(transcriptPath, "r");
1697
+ try {
1698
+ readSync(fd, buf, 0, buf.length, offset);
1699
+ } finally {
1700
+ closeSync(fd);
1701
+ }
1702
+ const text = buf.toString("utf8");
1703
+ const lastNewline = text.lastIndexOf("\n");
1704
+ if (lastNewline === -1) return;
1705
+ const consumable = text.slice(0, lastNewline);
1706
+ const consumedBytes = Buffer.byteLength(consumable, "utf8") + 1;
1707
+ let addedTokens = 0;
1708
+ let addedCents = 0;
1709
+ for (const raw of consumable.split("\n")) {
1710
+ if (!raw) continue;
1711
+ let line;
1712
+ try {
1713
+ line = JSON.parse(raw);
1714
+ } catch {
1715
+ continue;
1716
+ }
1717
+ if (line.type !== "assistant") continue;
1718
+ const usage = line.message?.usage;
1719
+ if (!usage) continue;
1720
+ const u = {
1721
+ inputTokens: usage.input_tokens ?? 0,
1722
+ outputTokens: usage.output_tokens ?? 0,
1723
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
1724
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0
1725
+ };
1726
+ const rates = ratesFor(line.message?.model);
1727
+ addedCents += costCentsFor(u, rates);
1728
+ addedTokens += u.inputTokens + u.outputTokens + u.cacheReadTokens + u.cacheCreationTokens;
1729
+ }
1730
+ const prevTokens = Number(getMeta(KEY_TOKENS(sessionId)) ?? "0");
1731
+ const prevCents = Number(getMeta(KEY_CENTS(sessionId)) ?? "0");
1732
+ const newTokens = prevTokens + addedTokens;
1733
+ const newCents = prevCents + addedCents;
1734
+ setMeta(KEY_OFFSET(sessionId), String(offset + consumedBytes));
1735
+ setMeta(KEY_TOKENS(sessionId), String(newTokens));
1736
+ setMeta(KEY_CENTS(sessionId), String(newCents));
1737
+ const db = getDb();
1738
+ db.prepare(
1739
+ `UPDATE sessions
1740
+ SET total_cost_cents = ?, total_tokens = ?
1741
+ WHERE id = ?`
1742
+ ).run(newCents, newTokens, sessionId);
1743
+ }
1744
+
1745
+ // src/hooks/handler.ts
1746
+ init_config();
1342
1747
  var HOOK_EVENT_TO_TYPE = {
1343
1748
  SessionStart: "session_start",
1344
1749
  SessionEnd: "session_end",
@@ -1369,6 +1774,10 @@ function logError(err) {
1369
1774
  }
1370
1775
  async function runHook(eventArg) {
1371
1776
  try {
1777
+ if (isCapturePaused()) {
1778
+ await readStdin().catch(() => "");
1779
+ return;
1780
+ }
1372
1781
  const raw = await readStdin();
1373
1782
  if (!raw.trim()) return;
1374
1783
  const input = JSON.parse(raw);
@@ -1410,13 +1819,18 @@ async function runHook(eventArg) {
1410
1819
  payload: safePayload
1411
1820
  };
1412
1821
  insertEvent(event);
1822
+ try {
1823
+ recomputeSessionCost(sessionId, input.transcript_path);
1824
+ } catch (err) {
1825
+ logError(err);
1826
+ }
1413
1827
  } catch (err) {
1414
1828
  logError(err);
1415
1829
  }
1416
1830
  }
1417
1831
 
1418
1832
  // src/cli.ts
1419
- var VERSION = true ? "0.1.2" : "dev";
1833
+ var VERSION = true ? "0.1.5" : "dev";
1420
1834
  var program = new Command();
1421
1835
  program.name("agentreel").description("AgentReel \u2014 capture Claude Code and Cursor sessions locally").version(VERSION);
1422
1836
  program.command("init").description("install Claude Code hooks and create the local SQLite buffer").action(async () => {
@@ -1434,8 +1848,20 @@ program.command("link [api-key]").description("authenticate the local agent with
1434
1848
  program.command("push").description("upload pending events to agentreel.dev").action(async () => {
1435
1849
  await pushCommand();
1436
1850
  });
1437
- program.command("daemon").description("run the background uploader (30s ticks, 1MB early-flush, exponential backoff)").action(async () => {
1438
- await daemonCommand();
1851
+ 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) => {
1852
+ await daemonCommand({ detach: opts.detach });
1853
+ });
1854
+ program.command("stop").description("stop the running background uploader").action(() => {
1855
+ stopCommand();
1856
+ });
1857
+ program.command("pause").description("pause local capture \u2014 hooks and watcher no-op until `resume`").action(() => {
1858
+ pauseCommand();
1859
+ });
1860
+ program.command("resume").description("resume local capture after `pause`").action(() => {
1861
+ resumeCommand();
1862
+ });
1863
+ program.command("forget [session-ids...]").description("delete captured sessions from the local SQLite buffer").option("--all", "wipe every session (asks for confirmation)").option("--force", "skip the confirmation prompt (use with care)").action(async (ids, opts) => {
1864
+ await forgetCommand(ids ?? [], opts);
1439
1865
  });
1440
1866
  program.command("logout").description("clear local credentials").action(async () => {
1441
1867
  await logoutCommand();