@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 +682 -256
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
);
|
|
270
|
+
var _db;
|
|
271
|
+
var init_db = __esm({
|
|
272
|
+
"src/db.ts"() {
|
|
273
|
+
"use strict";
|
|
274
|
+
init_paths();
|
|
275
|
+
_db = null;
|
|
316
276
|
}
|
|
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";
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
902
|
+
console.error(pc4.red("\u2717 Validation failed: ") + err.message);
|
|
616
903
|
process.exit(1);
|
|
617
904
|
}
|
|
618
905
|
writeConfig(cfg);
|
|
619
|
-
console.log(
|
|
620
|
-
console.log(
|
|
621
|
-
console.log(
|
|
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(
|
|
627
|
-
if (backup) console.log(
|
|
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
|
|
669
|
-
import { existsSync as
|
|
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
|
|
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
|
|
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 (!
|
|
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 (
|
|
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
|
|
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 (!
|
|
1062
|
+
if (!existsSync6(path)) return null;
|
|
774
1063
|
try {
|
|
775
|
-
const raw =
|
|
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 =
|
|
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 (
|
|
1298
|
+
if (existsSync7(prevPath)) {
|
|
1010
1299
|
before = await safeRead(prevPath);
|
|
1011
1300
|
}
|
|
1012
1301
|
}
|
|
1013
|
-
if (
|
|
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 (!
|
|
1134
|
-
console.error(
|
|
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(
|
|
1138
|
-
console.error(
|
|
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(
|
|
1142
|
-
console.log(
|
|
1143
|
-
console.log(
|
|
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) :
|
|
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" ?
|
|
1151
|
-
const stats = snap.binary ?
|
|
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
|
-
`${
|
|
1449
|
+
`${pc5.dim(ts2)} ${pc5.yellow("session")} ${pc5.dim(sessionId)} ${ws}`
|
|
1155
1450
|
);
|
|
1156
1451
|
}
|
|
1157
|
-
console.log(`${
|
|
1452
|
+
console.log(`${pc5.dim(ts2)} edit ${sourceLabel} ${file.padEnd(36)} ${stats}`);
|
|
1158
1453
|
});
|
|
1159
1454
|
const shutdown = async () => {
|
|
1160
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1484
|
+
console.error(pc6.red("\u2717 ") + e.message);
|
|
1187
1485
|
if (err.isPermanent) {
|
|
1188
1486
|
console.error(
|
|
1189
|
-
|
|
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(
|
|
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(
|
|
1500
|
+
console.log(pc6.green("\u2713") + " queue is empty \u2014 nothing to upload.");
|
|
1203
1501
|
return;
|
|
1204
1502
|
}
|
|
1205
1503
|
console.log(
|
|
1206
|
-
|
|
1504
|
+
pc6.green("\u2713") + ` uploaded ${totalEvents} events \xB7 ${totalSessions} session rows touched`
|
|
1207
1505
|
);
|
|
1208
1506
|
}
|
|
1209
1507
|
|
|
1210
|
-
// src/
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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 (
|
|
1219
|
-
console.
|
|
1220
|
-
|
|
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
|
-
|
|
1223
|
-
|
|
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
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
-
|
|
1289
|
-
|
|
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
|
|
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
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
|
1334
|
-
const
|
|
1335
|
-
|
|
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
|
-
|
|
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.
|
|
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();
|