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