@agentreel/agent 0.1.2 → 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
@@ -121,23 +121,8 @@ 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
- };
141
126
  function readConfig() {
142
127
  if (!existsSync2(CONFIG_PATH)) return { ...DEFAULT };
143
128
  try {
@@ -158,11 +143,20 @@ function writeConfig(cfg) {
158
143
  } catch {
159
144
  }
160
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
+ });
161
157
 
162
158
  // src/db.ts
163
- init_paths();
164
159
  import Database from "better-sqlite3";
165
- var _db = null;
166
160
  function getDb() {
167
161
  if (_db) return _db;
168
162
  ensureAgentreelDir();
@@ -266,79 +260,16 @@ function listRecentSessions(limit = 10) {
266
260
  FROM sessions ORDER BY started_at DESC LIMIT ?`
267
261
  ).all(limit);
268
262
  }
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
- );
263
+ var _db;
264
+ var init_db = __esm({
265
+ "src/db.ts"() {
266
+ "use strict";
267
+ init_paths();
268
+ _db = null;
316
269
  }
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";
270
+ });
326
271
 
327
272
  // 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
273
  async function postIngest(cfg, body) {
343
274
  if (!cfg.apiKey) {
344
275
  throw new Error("Not linked. Run: agentreel link <key>");
@@ -364,10 +295,28 @@ async function postIngest(cfg, body) {
364
295
  }
365
296
  return data;
366
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
+ });
367
318
 
368
319
  // src/upload/queue.ts
369
- var MAX_BATCH_BYTES = 1024 * 1024;
370
- var MAX_BATCH_EVENTS = 500;
371
320
  function takePendingBatch(maxEvents = MAX_BATCH_EVENTS, maxBytes = MAX_BATCH_BYTES) {
372
321
  const db = getDb();
373
322
  const candidateRows = db.prepare(
@@ -429,15 +378,17 @@ function safeParse(s) {
429
378
  return s;
430
379
  }
431
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
+ });
432
390
 
433
391
  // 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
392
  function nextAttemptAt() {
442
393
  const raw = getMeta(META_NEXT_ATTEMPT_AT);
443
394
  return raw ? Number(raw) : 0;
@@ -495,8 +446,320 @@ async function flushOnce() {
495
446
  throw err;
496
447
  }
497
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";
678
+
679
+ // src/commands/init.ts
680
+ init_install();
681
+ init_config();
682
+ init_db();
683
+ init_paths();
684
+ import { realpathSync } from "fs";
685
+ import { fileURLToPath } from "url";
686
+ import { sep } from "path";
687
+ import pc from "picocolors";
688
+ var PACKAGE_NAME = "@agentreel/agent";
689
+ function resolveAgentBinary() {
690
+ const here = fileURLToPath(import.meta.url);
691
+ const real = realpathSync(here);
692
+ const isEphemeral = real.includes(`${sep}_npx${sep}`) || real.includes("/_npx/");
693
+ return { absolutePath: real, isEphemeral };
694
+ }
695
+ function chooseHookPrefix(bin) {
696
+ if (process.env.AGENTREEL_HOOK_COMMAND) {
697
+ return { prefix: process.env.AGENTREEL_HOOK_COMMAND, mode: "absolute" };
698
+ }
699
+ if (bin.isEphemeral) {
700
+ return { prefix: `npx -y ${PACKAGE_NAME}`, mode: "npx" };
701
+ }
702
+ return { prefix: quotePath(bin.absolutePath), mode: "absolute" };
703
+ }
704
+ async function initCommand() {
705
+ ensureAgentreelDir();
706
+ getDb();
707
+ const cfg = readConfig();
708
+ if (!cfg.installedAt) cfg.installedAt = Date.now();
709
+ cfg.hooksInstalled = true;
710
+ writeConfig(cfg);
711
+ const binary = resolveAgentBinary();
712
+ const { prefix, mode } = chooseHookPrefix(binary);
713
+ const result = installClaudeCodeHooks(prefix);
714
+ console.log(pc.bold(pc.cyan("\n AgentReel ")) + pc.dim("Loom for AI coding sessions\n"));
715
+ console.log(pc.green("\u2713") + " Created ~/.agentreel/sessions.db");
716
+ console.log(pc.green("\u2713") + " Wrote ~/.agentreel/config.json");
717
+ console.log(
718
+ pc.green("\u2713") + ` Installed ${result.installedEvents.length} Claude Code hooks \u2192 ~/.claude/settings.json`
719
+ );
720
+ if (result.backup) {
721
+ console.log(pc.dim(` (backup: ${result.backup})`));
722
+ }
723
+ console.log();
724
+ console.log(pc.dim(" Hook command: ") + pc.dim(`${prefix} hook <Event>`));
725
+ if (mode === "npx") {
726
+ console.log(
727
+ pc.dim(" ") + pc.yellow("\u2022") + pc.dim(
728
+ ` Hooks resolve via npx every time Claude Code fires an event.
729
+ For faster cold starts, run: npm i -g ${PACKAGE_NAME}`
730
+ )
731
+ );
732
+ }
733
+ console.log();
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
+ );
754
+ }
498
755
 
499
756
  // src/commands/status.ts
757
+ init_paths();
758
+ init_db();
759
+ init_config();
760
+ init_flush();
761
+ import pc2 from "picocolors";
762
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
500
763
  function fmtDuration(ms) {
501
764
  const s = Math.round(ms / 1e3);
502
765
  if (s < 60) return `${s}s`;
@@ -587,44 +850,48 @@ function numMeta(key) {
587
850
  }
588
851
 
589
852
  // src/commands/auth.ts
590
- import pc3 from "picocolors";
853
+ init_config();
854
+ init_client();
855
+ import pc4 from "picocolors";
591
856
  async function logoutCommand() {
592
857
  const cfg = readConfig();
593
858
  cfg.apiKey = void 0;
594
859
  cfg.workspaceId = void 0;
595
860
  writeConfig(cfg);
596
- console.log(pc3.green("\u2713") + " Cleared local credentials.");
861
+ console.log(pc4.green("\u2713") + " Cleared local credentials.");
597
862
  }
598
863
  async function linkCommand(rawKey, opts) {
599
864
  const key = (rawKey ?? await promptHidden("Paste your AgentReel API key: ")).trim();
600
865
  if (!key) {
601
- console.error(pc3.red("\u2717 No key provided."));
866
+ console.error(pc4.red("\u2717 No key provided."));
602
867
  process.exit(1);
603
868
  }
604
869
  if (!key.startsWith("ar_live_")) {
605
- 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?"));
606
871
  process.exit(1);
607
872
  }
608
873
  const cfg = readConfig();
609
874
  cfg.apiKey = key;
610
875
  if (opts.api) cfg.apiBaseUrl = opts.api;
611
- console.log(pc3.dim(` validating against ${cfg.apiBaseUrl}\u2026`));
876
+ console.log(pc4.dim(` validating against ${cfg.apiBaseUrl}\u2026`));
612
877
  try {
613
878
  await postIngest(cfg, {});
614
879
  } catch (err) {
615
- console.error(pc3.red("\u2717 Validation failed: ") + err.message);
880
+ console.error(pc4.red("\u2717 Validation failed: ") + err.message);
616
881
  process.exit(1);
617
882
  }
618
883
  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");
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");
622
887
  }
623
888
  async function uninstallCommand() {
624
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();
625
892
  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})`));
893
+ console.log(pc4.green("\u2713") + " Removed AgentReel hooks from ~/.claude/settings.json");
894
+ if (backup) console.log(pc4.dim(` (backup: ${backup})`));
628
895
  }
629
896
  var CTRL_C = 3;
630
897
  var BACKSPACE = 127;
@@ -665,8 +932,8 @@ function promptHidden(prompt) {
665
932
  }
666
933
 
667
934
  // src/commands/watch.ts
668
- import pc4 from "picocolors";
669
- import { existsSync as existsSync7 } from "fs";
935
+ import pc5 from "picocolors";
936
+ import { existsSync as existsSync8 } from "fs";
670
937
  import { basename as basename2 } from "path";
671
938
 
672
939
  // src/cursor/paths.ts
@@ -692,17 +959,17 @@ function cursorHistoryDir() {
692
959
  // src/cursor/watcher.ts
693
960
  import chokidar from "chokidar";
694
961
  import { readFile as readFile2 } from "fs/promises";
695
- import { existsSync as existsSync6 } from "fs";
962
+ import { existsSync as existsSync7 } from "fs";
696
963
  import { basename, dirname as dirname3, join as join5 } from "path";
697
964
 
698
965
  // src/cursor/entries.ts
699
966
  import { readFile } from "fs/promises";
700
- import { existsSync as existsSync4, statSync } from "fs";
967
+ import { existsSync as existsSync5, statSync } from "fs";
701
968
  import { join as join3, dirname as dirname2, resolve, sep as sep2 } from "path";
702
969
  import { fileURLToPath as fileURLToPath2 } from "url";
703
970
  async function readEntries(historyFolder) {
704
971
  const path = join3(historyFolder, "entries.json");
705
- if (!existsSync4(path)) return null;
972
+ if (!existsSync5(path)) return null;
706
973
  try {
707
974
  const raw = await readFile(path, "utf8");
708
975
  return JSON.parse(raw);
@@ -723,7 +990,7 @@ function findWorkspaceRoot(path) {
723
990
  while (dir && dir !== sep2) {
724
991
  const git = join3(dir, ".git");
725
992
  try {
726
- if (existsSync4(git)) return dir;
993
+ if (existsSync5(git)) return dir;
727
994
  } catch {
728
995
  }
729
996
  const parent = dirname2(dir);
@@ -762,7 +1029,7 @@ function computeDiff(before, after) {
762
1029
  }
763
1030
 
764
1031
  // src/redact/ignore.ts
765
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
1032
+ import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
766
1033
  import { join as join4, relative, sep as sep3 } from "path";
767
1034
  import ignore from "ignore";
768
1035
  var FILENAME = ".agentreelignore";
@@ -770,9 +1037,9 @@ var TTL_MS = 5e3;
770
1037
  var cache = /* @__PURE__ */ new Map();
771
1038
  function loadFor(workspace) {
772
1039
  const path = join4(workspace, FILENAME);
773
- if (!existsSync5(path)) return null;
1040
+ if (!existsSync6(path)) return null;
774
1041
  try {
775
- const raw = readFileSync4(path, "utf8");
1042
+ const raw = readFileSync5(path, "utf8");
776
1043
  return ignore({ allowRelativePaths: true }).add(raw);
777
1044
  } catch {
778
1045
  return null;
@@ -783,7 +1050,7 @@ function getCached(workspace) {
783
1050
  const path = join4(workspace, FILENAME);
784
1051
  let mtime = 0;
785
1052
  try {
786
- mtime = existsSync5(path) ? statSync2(path).mtimeMs : 0;
1053
+ mtime = existsSync6(path) ? statSync2(path).mtimeMs : 0;
787
1054
  } catch {
788
1055
  mtime = 0;
789
1056
  }
@@ -1006,11 +1273,11 @@ async function processSnapshot(folder, ctx, skipFirst, onSnapshot) {
1006
1273
  let after = "";
1007
1274
  if (ctx.previous) {
1008
1275
  const prevPath = join5(folder, ctx.previous.id);
1009
- if (existsSync6(prevPath)) {
1276
+ if (existsSync7(prevPath)) {
1010
1277
  before = await safeRead(prevPath);
1011
1278
  }
1012
1279
  }
1013
- if (existsSync6(newSnapshot)) {
1280
+ if (existsSync7(newSnapshot)) {
1014
1281
  after = await safeRead(newSnapshot);
1015
1282
  }
1016
1283
  if (before === after) return;
@@ -1057,6 +1324,7 @@ function sleep(ms) {
1057
1324
  var NOISY_PATH = /[\\/](node_modules|\.next|\.turbo|dist|build|\.git|coverage|\.cache|\.venv|venv|target|out)[\\/]/;
1058
1325
 
1059
1326
  // src/cursor/session.ts
1327
+ init_db();
1060
1328
  import { nanoid } from "nanoid";
1061
1329
  var IDLE_MS = 5 * 60 * 1e3;
1062
1330
  var GLOBAL_KEY = "__global__";
@@ -1126,38 +1394,39 @@ var CursorSessionManager = class {
1126
1394
 
1127
1395
  // src/commands/watch.ts
1128
1396
  init_paths();
1397
+ init_db();
1129
1398
  async function watchCommand() {
1130
1399
  ensureAgentreelDir();
1131
1400
  getDb();
1132
1401
  const dir = cursorHistoryDir();
1133
- if (!existsSync7(dir)) {
1134
- 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:"));
1135
1404
  console.error(" " + dir);
1136
1405
  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.)"));
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.)"));
1139
1408
  process.exit(1);
1140
1409
  }
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"));
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"));
1144
1413
  const sessions = new CursorSessionManager();
1145
1414
  const watcher = startCursorWatcher(dir, async (snap) => {
1146
1415
  const { sessionId, isNew } = sessions.ingest(snap);
1147
1416
  const ts2 = new Date(snap.timestamp).toLocaleTimeString();
1148
- const ws = snap.workspace ? basename2(snap.workspace) : pc4.dim("no-workspace");
1417
+ const ws = snap.workspace ? basename2(snap.workspace) : pc5.dim("no-workspace");
1149
1418
  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)}`;
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)}`;
1152
1421
  if (isNew) {
1153
1422
  console.log(
1154
- `${pc4.dim(ts2)} ${pc4.yellow("session")} ${pc4.dim(sessionId)} ${ws}`
1423
+ `${pc5.dim(ts2)} ${pc5.yellow("session")} ${pc5.dim(sessionId)} ${ws}`
1155
1424
  );
1156
1425
  }
1157
- console.log(`${pc4.dim(ts2)} edit ${sourceLabel} ${file.padEnd(36)} ${stats}`);
1426
+ console.log(`${pc5.dim(ts2)} edit ${sourceLabel} ${file.padEnd(36)} ${stats}`);
1158
1427
  });
1159
1428
  const shutdown = async () => {
1160
- console.log(pc4.dim("\n closing sessions\u2026"));
1429
+ console.log(pc5.dim("\n closing sessions\u2026"));
1161
1430
  sessions.closeAll();
1162
1431
  await watcher.close();
1163
1432
  process.exit(0);
@@ -1167,11 +1436,14 @@ async function watchCommand() {
1167
1436
  }
1168
1437
 
1169
1438
  // src/commands/push.ts
1170
- import pc5 from "picocolors";
1439
+ init_config();
1440
+ init_flush();
1441
+ init_client();
1442
+ import pc6 from "picocolors";
1171
1443
  async function pushCommand() {
1172
1444
  const cfg = readConfig();
1173
1445
  if (!cfg.apiKey) {
1174
- 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>"));
1175
1447
  process.exit(1);
1176
1448
  }
1177
1449
  let totalSessions = 0;
@@ -1183,14 +1455,14 @@ async function pushCommand() {
1183
1455
  } catch (err) {
1184
1456
  const e = err;
1185
1457
  if (err instanceof IngestError) {
1186
- console.error(pc5.red("\u2717 ") + e.message);
1458
+ console.error(pc6.red("\u2717 ") + e.message);
1187
1459
  if (err.isPermanent) {
1188
1460
  console.error(
1189
- pc5.dim(" not retrying \u2014 fix the cause (re-link, upgrade plan, etc.) and run push again.")
1461
+ pc6.dim(" not retrying \u2014 fix the cause (re-link, upgrade plan, etc.) and run push again.")
1190
1462
  );
1191
1463
  }
1192
1464
  } else {
1193
- console.error(pc5.red("\u2717 ") + e.message);
1465
+ console.error(pc6.red("\u2717 ") + e.message);
1194
1466
  }
1195
1467
  process.exit(1);
1196
1468
  }
@@ -1199,146 +1471,123 @@ async function pushCommand() {
1199
1471
  if (!res.moreAvailable) break;
1200
1472
  }
1201
1473
  if (totalEvents === 0) {
1202
- 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.");
1203
1475
  return;
1204
1476
  }
1205
1477
  console.log(
1206
- pc5.green("\u2713") + ` uploaded ${totalEvents} events \xB7 ${totalSessions} session rows touched`
1478
+ pc6.green("\u2713") + ` uploaded ${totalEvents} events \xB7 ${totalSessions} session rows touched`
1207
1479
  );
1208
1480
  }
1209
1481
 
1210
- // src/commands/daemon.ts
1482
+ // src/cli.ts
1483
+ init_daemon();
1484
+
1485
+ // src/hooks/handler.ts
1486
+ init_db();
1211
1487
  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() {
1217
- 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);
1221
- }
1222
- if (!claimPidFile()) {
1223
- process.exit(1);
1488
+ import { nanoid as nanoid2 } from "nanoid";
1489
+ import { appendFileSync } from "fs";
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];
1224
1516
  }
1225
- const release = () => {
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;
1226
1557
  try {
1227
- unlinkSync(DAEMON_PID_PATH);
1558
+ line = JSON.parse(raw);
1228
1559
  } 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
1560
  continue;
1253
1561
  }
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
- }
1273
- }
1274
- await waitForTickOrPressure(TICK_INTERVAL_MS, EARLY_FLUSH_BYTES, () => stopping);
1275
- }
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;
1286
- }
1287
- }
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;
1297
- }
1298
- }
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) {
1312
- 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);
1331
- });
1332
- }
1333
- function ts() {
1334
- const d = /* @__PURE__ */ new Date();
1335
- return d.toTimeString().slice(0, 8);
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);
1336
1588
  }
1337
1589
 
1338
1590
  // src/hooks/handler.ts
1339
- import { nanoid as nanoid2 } from "nanoid";
1340
- import { appendFileSync } from "fs";
1341
- init_paths();
1342
1591
  var HOOK_EVENT_TO_TYPE = {
1343
1592
  SessionStart: "session_start",
1344
1593
  SessionEnd: "session_end",
@@ -1410,13 +1659,18 @@ async function runHook(eventArg) {
1410
1659
  payload: safePayload
1411
1660
  };
1412
1661
  insertEvent(event);
1662
+ try {
1663
+ recomputeSessionCost(sessionId, input.transcript_path);
1664
+ } catch (err) {
1665
+ logError(err);
1666
+ }
1413
1667
  } catch (err) {
1414
1668
  logError(err);
1415
1669
  }
1416
1670
  }
1417
1671
 
1418
1672
  // src/cli.ts
1419
- var VERSION = true ? "0.1.2" : "dev";
1673
+ var VERSION = true ? "0.1.4" : "dev";
1420
1674
  var program = new Command();
1421
1675
  program.name("agentreel").description("AgentReel \u2014 capture Claude Code and Cursor sessions locally").version(VERSION);
1422
1676
  program.command("init").description("install Claude Code hooks and create the local SQLite buffer").action(async () => {
@@ -1434,8 +1688,11 @@ program.command("link [api-key]").description("authenticate the local agent with
1434
1688
  program.command("push").description("upload pending events to agentreel.dev").action(async () => {
1435
1689
  await pushCommand();
1436
1690
  });
1437
- program.command("daemon").description("run the background uploader (30s ticks, 1MB early-flush, exponential backoff)").action(async () => {
1438
- await daemonCommand();
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();
1439
1696
  });
1440
1697
  program.command("logout").description("clear local credentials").action(async () => {
1441
1698
  await logoutCommand();