@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 +522 -265
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
880
|
+
console.error(pc4.red("\u2717 Validation failed: ") + err.message);
|
|
616
881
|
process.exit(1);
|
|
617
882
|
}
|
|
618
883
|
writeConfig(cfg);
|
|
619
|
-
console.log(
|
|
620
|
-
console.log(
|
|
621
|
-
console.log(
|
|
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(
|
|
627
|
-
if (backup) console.log(
|
|
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
|
|
669
|
-
import { existsSync as
|
|
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
|
|
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
|
|
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 (!
|
|
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 (
|
|
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
|
|
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 (!
|
|
1040
|
+
if (!existsSync6(path)) return null;
|
|
774
1041
|
try {
|
|
775
|
-
const raw =
|
|
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 =
|
|
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 (
|
|
1276
|
+
if (existsSync7(prevPath)) {
|
|
1010
1277
|
before = await safeRead(prevPath);
|
|
1011
1278
|
}
|
|
1012
1279
|
}
|
|
1013
|
-
if (
|
|
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 (!
|
|
1134
|
-
console.error(
|
|
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(
|
|
1138
|
-
console.error(
|
|
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(
|
|
1142
|
-
console.log(
|
|
1143
|
-
console.log(
|
|
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) :
|
|
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" ?
|
|
1151
|
-
const stats = snap.binary ?
|
|
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
|
-
`${
|
|
1423
|
+
`${pc5.dim(ts2)} ${pc5.yellow("session")} ${pc5.dim(sessionId)} ${ws}`
|
|
1155
1424
|
);
|
|
1156
1425
|
}
|
|
1157
|
-
console.log(`${
|
|
1426
|
+
console.log(`${pc5.dim(ts2)} edit ${sourceLabel} ${file.padEnd(36)} ${stats}`);
|
|
1158
1427
|
});
|
|
1159
1428
|
const shutdown = async () => {
|
|
1160
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1458
|
+
console.error(pc6.red("\u2717 ") + e.message);
|
|
1187
1459
|
if (err.isPermanent) {
|
|
1188
1460
|
console.error(
|
|
1189
|
-
|
|
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(
|
|
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(
|
|
1474
|
+
console.log(pc6.green("\u2713") + " queue is empty \u2014 nothing to upload.");
|
|
1203
1475
|
return;
|
|
1204
1476
|
}
|
|
1205
1477
|
console.log(
|
|
1206
|
-
|
|
1478
|
+
pc6.green("\u2713") + ` uploaded ${totalEvents} events \xB7 ${totalSessions} session rows touched`
|
|
1207
1479
|
);
|
|
1208
1480
|
}
|
|
1209
1481
|
|
|
1210
|
-
// src/
|
|
1482
|
+
// src/cli.ts
|
|
1483
|
+
init_daemon();
|
|
1484
|
+
|
|
1485
|
+
// src/hooks/handler.ts
|
|
1486
|
+
init_db();
|
|
1211
1487
|
init_paths();
|
|
1212
|
-
import
|
|
1213
|
-
import {
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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.
|
|
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();
|