@agentreel/agent 0.1.0
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/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/cli.js +1148 -0
- package/dist/cli.js.map +1 -0
- package/package.json +67 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/paths.ts
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { mkdirSync } from "fs";
|
|
16
|
+
function ensureAgentreelDir() {
|
|
17
|
+
mkdirSync(AGENTREEL_DIR, { recursive: true });
|
|
18
|
+
mkdirSync(QUEUE_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
var HOME, AGENTREEL_DIR, DB_PATH, CONFIG_PATH, QUEUE_DIR, LOG_PATH, CLAUDE_DIR, CLAUDE_SETTINGS_PATH;
|
|
21
|
+
var init_paths = __esm({
|
|
22
|
+
"src/paths.ts"() {
|
|
23
|
+
"use strict";
|
|
24
|
+
HOME = homedir();
|
|
25
|
+
AGENTREEL_DIR = join(HOME, ".agentreel");
|
|
26
|
+
DB_PATH = join(AGENTREEL_DIR, "sessions.db");
|
|
27
|
+
CONFIG_PATH = join(AGENTREEL_DIR, "config.json");
|
|
28
|
+
QUEUE_DIR = join(AGENTREEL_DIR, "queue");
|
|
29
|
+
LOG_PATH = join(AGENTREEL_DIR, "agent.log");
|
|
30
|
+
CLAUDE_DIR = join(HOME, ".claude");
|
|
31
|
+
CLAUDE_SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// src/hooks/install.ts
|
|
36
|
+
var install_exports = {};
|
|
37
|
+
__export(install_exports, {
|
|
38
|
+
installClaudeCodeHooks: () => installClaudeCodeHooks,
|
|
39
|
+
quotePath: () => quotePath,
|
|
40
|
+
uninstallClaudeCodeHooks: () => uninstallClaudeCodeHooks
|
|
41
|
+
});
|
|
42
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
43
|
+
import { dirname } from "path";
|
|
44
|
+
function readSettings() {
|
|
45
|
+
if (!existsSync(CLAUDE_SETTINGS_PATH)) return {};
|
|
46
|
+
const raw = readFileSync(CLAUDE_SETTINGS_PATH, "utf8");
|
|
47
|
+
if (!raw.trim()) return {};
|
|
48
|
+
return JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
function backupSettings() {
|
|
51
|
+
if (!existsSync(CLAUDE_SETTINGS_PATH)) return null;
|
|
52
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
53
|
+
const backup = `${CLAUDE_SETTINGS_PATH}.agentreel-backup-${stamp}`;
|
|
54
|
+
copyFileSync(CLAUDE_SETTINGS_PATH, backup);
|
|
55
|
+
return backup;
|
|
56
|
+
}
|
|
57
|
+
function buildHookEntry(event, hookCommandPrefix) {
|
|
58
|
+
return {
|
|
59
|
+
type: "command",
|
|
60
|
+
command: `${hookCommandPrefix} hook ${event}`,
|
|
61
|
+
timeout: 5
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function quotePath(p) {
|
|
65
|
+
return /[\s'"$`\\]/.test(p) ? `"${p.replace(/"/g, '\\"')}"` : p;
|
|
66
|
+
}
|
|
67
|
+
function installClaudeCodeHooks(hookCommandPrefix) {
|
|
68
|
+
mkdirSync2(CLAUDE_DIR, { recursive: true });
|
|
69
|
+
mkdirSync2(dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
70
|
+
const backup = backupSettings();
|
|
71
|
+
const settings = readSettings();
|
|
72
|
+
settings.hooks ??= {};
|
|
73
|
+
for (const event of HOOK_EVENTS) {
|
|
74
|
+
const groups = settings.hooks[event] ?? [];
|
|
75
|
+
const filtered = groups.filter((g) => g.__agentreel !== HOOK_MARKER);
|
|
76
|
+
filtered.push({
|
|
77
|
+
matcher: ".*",
|
|
78
|
+
__agentreel: HOOK_MARKER,
|
|
79
|
+
hooks: [buildHookEntry(event, hookCommandPrefix)]
|
|
80
|
+
});
|
|
81
|
+
settings.hooks[event] = filtered;
|
|
82
|
+
}
|
|
83
|
+
writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
84
|
+
return { backup, installedEvents: [...HOOK_EVENTS], hookCommandPrefix };
|
|
85
|
+
}
|
|
86
|
+
function uninstallClaudeCodeHooks() {
|
|
87
|
+
if (!existsSync(CLAUDE_SETTINGS_PATH)) return { backup: null };
|
|
88
|
+
const backup = backupSettings();
|
|
89
|
+
const settings = readSettings();
|
|
90
|
+
if (settings.hooks) {
|
|
91
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
92
|
+
const groups = settings.hooks[event] ?? [];
|
|
93
|
+
const remaining = groups.filter((g) => g.__agentreel !== HOOK_MARKER);
|
|
94
|
+
if (remaining.length === 0) delete settings.hooks[event];
|
|
95
|
+
else settings.hooks[event] = remaining;
|
|
96
|
+
}
|
|
97
|
+
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
98
|
+
}
|
|
99
|
+
writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
100
|
+
return { backup };
|
|
101
|
+
}
|
|
102
|
+
var HOOK_MARKER, HOOK_EVENTS;
|
|
103
|
+
var init_install = __esm({
|
|
104
|
+
"src/hooks/install.ts"() {
|
|
105
|
+
"use strict";
|
|
106
|
+
init_paths();
|
|
107
|
+
HOOK_MARKER = "agentreel:v1";
|
|
108
|
+
HOOK_EVENTS = [
|
|
109
|
+
"SessionStart",
|
|
110
|
+
"SessionEnd",
|
|
111
|
+
"UserPromptSubmit",
|
|
112
|
+
"PreToolUse",
|
|
113
|
+
"PostToolUse",
|
|
114
|
+
"Notification",
|
|
115
|
+
"Stop",
|
|
116
|
+
"SubagentStop",
|
|
117
|
+
"PreCompact"
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
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
|
+
// src/config.ts
|
|
133
|
+
init_paths();
|
|
134
|
+
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
|
+
function readConfig() {
|
|
140
|
+
if (!existsSync2(CONFIG_PATH)) return { ...DEFAULT };
|
|
141
|
+
try {
|
|
142
|
+
const raw = readFileSync2(CONFIG_PATH, "utf8");
|
|
143
|
+
return { ...DEFAULT, ...JSON.parse(raw) };
|
|
144
|
+
} catch {
|
|
145
|
+
return { ...DEFAULT };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function writeConfig(cfg) {
|
|
149
|
+
ensureAgentreelDir();
|
|
150
|
+
writeFileSync2(CONFIG_PATH, JSON.stringify(cfg, null, 2) + "\n", {
|
|
151
|
+
encoding: "utf8",
|
|
152
|
+
mode: 384
|
|
153
|
+
});
|
|
154
|
+
try {
|
|
155
|
+
chmodSync(CONFIG_PATH, 384);
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/db.ts
|
|
161
|
+
init_paths();
|
|
162
|
+
import Database from "better-sqlite3";
|
|
163
|
+
var _db = null;
|
|
164
|
+
function getDb() {
|
|
165
|
+
if (_db) return _db;
|
|
166
|
+
ensureAgentreelDir();
|
|
167
|
+
const db = new Database(DB_PATH);
|
|
168
|
+
db.pragma("journal_mode = WAL");
|
|
169
|
+
db.pragma("synchronous = NORMAL");
|
|
170
|
+
migrate(db);
|
|
171
|
+
_db = db;
|
|
172
|
+
return db;
|
|
173
|
+
}
|
|
174
|
+
function migrate(db) {
|
|
175
|
+
db.exec(`
|
|
176
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
177
|
+
id TEXT PRIMARY KEY,
|
|
178
|
+
tool TEXT NOT NULL,
|
|
179
|
+
started_at INTEGER NOT NULL,
|
|
180
|
+
ended_at INTEGER,
|
|
181
|
+
cwd TEXT,
|
|
182
|
+
total_cost_cents INTEGER,
|
|
183
|
+
total_tokens INTEGER
|
|
184
|
+
);
|
|
185
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
186
|
+
id TEXT PRIMARY KEY,
|
|
187
|
+
session_id TEXT NOT NULL,
|
|
188
|
+
tool TEXT NOT NULL,
|
|
189
|
+
type TEXT NOT NULL,
|
|
190
|
+
ts INTEGER NOT NULL,
|
|
191
|
+
cwd TEXT,
|
|
192
|
+
payload TEXT NOT NULL,
|
|
193
|
+
uploaded_at INTEGER
|
|
194
|
+
);
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id, ts);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_events_pending ON events(uploaded_at) WHERE uploaded_at IS NULL;
|
|
197
|
+
`);
|
|
198
|
+
}
|
|
199
|
+
function upsertSession(s) {
|
|
200
|
+
const db = getDb();
|
|
201
|
+
db.prepare(
|
|
202
|
+
`INSERT INTO sessions (id, tool, started_at, ended_at, cwd, total_cost_cents, total_tokens)
|
|
203
|
+
VALUES (@id, @tool, @startedAt, @endedAt, @cwd, @totalCostCents, @totalTokens)
|
|
204
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
205
|
+
ended_at = COALESCE(excluded.ended_at, sessions.ended_at),
|
|
206
|
+
cwd = COALESCE(excluded.cwd, sessions.cwd),
|
|
207
|
+
total_cost_cents = COALESCE(excluded.total_cost_cents, sessions.total_cost_cents),
|
|
208
|
+
total_tokens = COALESCE(excluded.total_tokens, sessions.total_tokens)`
|
|
209
|
+
).run({
|
|
210
|
+
id: s.id,
|
|
211
|
+
tool: s.tool,
|
|
212
|
+
startedAt: s.startedAt,
|
|
213
|
+
endedAt: s.endedAt ?? null,
|
|
214
|
+
cwd: s.cwd ?? null,
|
|
215
|
+
totalCostCents: s.totalCostCents ?? null,
|
|
216
|
+
totalTokens: s.totalTokens ?? null
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function insertEvent(e) {
|
|
220
|
+
const db = getDb();
|
|
221
|
+
db.prepare(
|
|
222
|
+
`INSERT OR IGNORE INTO events (id, session_id, tool, type, ts, cwd, payload)
|
|
223
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
224
|
+
).run(e.id, e.sessionId, e.tool, e.type, e.ts, e.cwd ?? null, JSON.stringify(e.payload ?? {}));
|
|
225
|
+
}
|
|
226
|
+
function countEvents() {
|
|
227
|
+
const db = getDb();
|
|
228
|
+
const total = db.prepare(`SELECT COUNT(*) AS c FROM events`).get();
|
|
229
|
+
const pending = db.prepare(`SELECT COUNT(*) AS c FROM events WHERE uploaded_at IS NULL`).get();
|
|
230
|
+
return { total: total.c, pending: pending.c };
|
|
231
|
+
}
|
|
232
|
+
function listRecentSessions(limit = 10) {
|
|
233
|
+
const db = getDb();
|
|
234
|
+
return db.prepare(
|
|
235
|
+
`SELECT id, tool, started_at, ended_at, cwd
|
|
236
|
+
FROM sessions ORDER BY started_at DESC LIMIT ?`
|
|
237
|
+
).all(limit);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/commands/init.ts
|
|
241
|
+
init_paths();
|
|
242
|
+
var PACKAGE_NAME = "@agentreel/agent";
|
|
243
|
+
function resolveAgentBinary() {
|
|
244
|
+
const here = fileURLToPath(import.meta.url);
|
|
245
|
+
const real = realpathSync(here);
|
|
246
|
+
const isEphemeral = real.includes(`${sep}_npx${sep}`) || real.includes("/_npx/");
|
|
247
|
+
return { absolutePath: real, isEphemeral };
|
|
248
|
+
}
|
|
249
|
+
function chooseHookPrefix(bin) {
|
|
250
|
+
if (process.env.AGENTREEL_HOOK_COMMAND) {
|
|
251
|
+
return { prefix: process.env.AGENTREEL_HOOK_COMMAND, mode: "absolute" };
|
|
252
|
+
}
|
|
253
|
+
if (bin.isEphemeral) {
|
|
254
|
+
return { prefix: `npx -y ${PACKAGE_NAME}`, mode: "npx" };
|
|
255
|
+
}
|
|
256
|
+
return { prefix: quotePath(bin.absolutePath), mode: "absolute" };
|
|
257
|
+
}
|
|
258
|
+
async function initCommand() {
|
|
259
|
+
ensureAgentreelDir();
|
|
260
|
+
getDb();
|
|
261
|
+
const cfg = readConfig();
|
|
262
|
+
if (!cfg.installedAt) cfg.installedAt = Date.now();
|
|
263
|
+
cfg.hooksInstalled = true;
|
|
264
|
+
writeConfig(cfg);
|
|
265
|
+
const binary = resolveAgentBinary();
|
|
266
|
+
const { prefix, mode } = chooseHookPrefix(binary);
|
|
267
|
+
const result = installClaudeCodeHooks(prefix);
|
|
268
|
+
console.log(pc.bold(pc.cyan("\n AgentReel ")) + pc.dim("Loom for AI coding sessions\n"));
|
|
269
|
+
console.log(pc.green("\u2713") + " Created ~/.agentreel/sessions.db");
|
|
270
|
+
console.log(pc.green("\u2713") + " Wrote ~/.agentreel/config.json");
|
|
271
|
+
console.log(
|
|
272
|
+
pc.green("\u2713") + ` Installed ${result.installedEvents.length} Claude Code hooks \u2192 ~/.claude/settings.json`
|
|
273
|
+
);
|
|
274
|
+
if (result.backup) {
|
|
275
|
+
console.log(pc.dim(` (backup: ${result.backup})`));
|
|
276
|
+
}
|
|
277
|
+
console.log();
|
|
278
|
+
console.log(pc.dim(" Hook command: ") + pc.dim(`${prefix} hook <Event>`));
|
|
279
|
+
if (mode === "npx") {
|
|
280
|
+
console.log(
|
|
281
|
+
pc.dim(" ") + pc.yellow("\u2022") + pc.dim(
|
|
282
|
+
` Hooks resolve via npx every time Claude Code fires an event.
|
|
283
|
+
For faster cold starts, run: npm i -g ${PACKAGE_NAME}`
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
console.log();
|
|
288
|
+
console.log(pc.bold("Next:") + " open Claude Code and run a prompt.");
|
|
289
|
+
console.log(" then " + pc.cyan("agentreel status") + " to see captured events.\n");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/commands/status.ts
|
|
293
|
+
init_paths();
|
|
294
|
+
import pc2 from "picocolors";
|
|
295
|
+
import { existsSync as existsSync3 } from "fs";
|
|
296
|
+
function fmtDuration(ms) {
|
|
297
|
+
const s = Math.round(ms / 1e3);
|
|
298
|
+
if (s < 60) return `${s}s`;
|
|
299
|
+
const m = Math.floor(s / 60);
|
|
300
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
301
|
+
const h = Math.floor(m / 60);
|
|
302
|
+
return `${h}h ${m % 60}m`;
|
|
303
|
+
}
|
|
304
|
+
async function statusCommand() {
|
|
305
|
+
const cfg = readConfig();
|
|
306
|
+
console.log(pc2.bold(pc2.cyan("AgentReel status\n")));
|
|
307
|
+
console.log(" config: " + (existsSync3(CONFIG_PATH) ? CONFIG_PATH : pc2.red("missing")));
|
|
308
|
+
console.log(" database: " + (existsSync3(DB_PATH) ? DB_PATH : pc2.red("not initialized")));
|
|
309
|
+
console.log(
|
|
310
|
+
" claude hooks: " + (existsSync3(CLAUDE_SETTINGS_PATH) ? CLAUDE_SETTINGS_PATH : pc2.red("not installed"))
|
|
311
|
+
);
|
|
312
|
+
console.log(" api base: " + cfg.apiBaseUrl);
|
|
313
|
+
console.log(" authenticated: " + (cfg.apiKey ? pc2.green("yes") : pc2.yellow("no")));
|
|
314
|
+
console.log();
|
|
315
|
+
if (!existsSync3(DB_PATH)) {
|
|
316
|
+
console.log(pc2.yellow("Run `agentreel init` to install hooks."));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const { total, pending } = countEvents();
|
|
320
|
+
console.log(` events captured: ${total}`);
|
|
321
|
+
console.log(` pending upload: ${pending}`);
|
|
322
|
+
console.log();
|
|
323
|
+
const sessions = listRecentSessions(5);
|
|
324
|
+
if (sessions.length === 0) {
|
|
325
|
+
console.log(pc2.dim(" no sessions yet \u2014 start a Claude Code session to capture one."));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
console.log(pc2.bold("recent sessions:"));
|
|
329
|
+
for (const s of sessions) {
|
|
330
|
+
const dur = s.ended_at ? fmtDuration(s.ended_at - s.started_at) : pc2.dim("active");
|
|
331
|
+
const ts = new Date(s.started_at).toLocaleString();
|
|
332
|
+
console.log(` ${pc2.dim(s.id.slice(0, 8))} ${s.tool.padEnd(11)} ${ts} ${dur}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/commands/auth.ts
|
|
337
|
+
import pc3 from "picocolors";
|
|
338
|
+
|
|
339
|
+
// src/upload/client.ts
|
|
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;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/commands/auth.ts
|
|
368
|
+
async function logoutCommand() {
|
|
369
|
+
const cfg = readConfig();
|
|
370
|
+
cfg.apiKey = void 0;
|
|
371
|
+
cfg.workspaceId = void 0;
|
|
372
|
+
writeConfig(cfg);
|
|
373
|
+
console.log(pc3.green("\u2713") + " Cleared local credentials.");
|
|
374
|
+
}
|
|
375
|
+
async function linkCommand(rawKey, opts) {
|
|
376
|
+
const key = (rawKey ?? await promptHidden("Paste your AgentReel API key: ")).trim();
|
|
377
|
+
if (!key) {
|
|
378
|
+
console.error(pc3.red("\u2717 No key provided."));
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
if (!key.startsWith("ar_live_")) {
|
|
382
|
+
console.error(pc3.red("\u2717 Keys start with `ar_live_`. Did you paste the right value?"));
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
const cfg = readConfig();
|
|
386
|
+
cfg.apiKey = key;
|
|
387
|
+
if (opts.api) cfg.apiBaseUrl = opts.api;
|
|
388
|
+
console.log(pc3.dim(` validating against ${cfg.apiBaseUrl}\u2026`));
|
|
389
|
+
try {
|
|
390
|
+
await postIngest(cfg, {});
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.error(pc3.red("\u2717 Validation failed: ") + err.message);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
writeConfig(cfg);
|
|
396
|
+
console.log(pc3.green("\u2713") + " Linked.");
|
|
397
|
+
console.log(pc3.dim(" api: ") + cfg.apiBaseUrl);
|
|
398
|
+
console.log(pc3.dim(" key: ") + key.slice(0, 12) + "\u2026");
|
|
399
|
+
}
|
|
400
|
+
async function uninstallCommand() {
|
|
401
|
+
const { uninstallClaudeCodeHooks: uninstallClaudeCodeHooks2 } = await Promise.resolve().then(() => (init_install(), install_exports));
|
|
402
|
+
const { backup } = uninstallClaudeCodeHooks2();
|
|
403
|
+
console.log(pc3.green("\u2713") + " Removed AgentReel hooks from ~/.claude/settings.json");
|
|
404
|
+
if (backup) console.log(pc3.dim(` (backup: ${backup})`));
|
|
405
|
+
}
|
|
406
|
+
var CTRL_C = 3;
|
|
407
|
+
var BACKSPACE = 127;
|
|
408
|
+
var BACKSPACE_ALT = 8;
|
|
409
|
+
var NEWLINE = 10;
|
|
410
|
+
var CARRIAGE = 13;
|
|
411
|
+
function promptHidden(prompt) {
|
|
412
|
+
return new Promise((resolve2) => {
|
|
413
|
+
process.stdout.write(prompt);
|
|
414
|
+
let buf = "";
|
|
415
|
+
const stdin = process.stdin;
|
|
416
|
+
stdin.setRawMode?.(true);
|
|
417
|
+
stdin.resume();
|
|
418
|
+
stdin.setEncoding("utf8");
|
|
419
|
+
const onData = (chunk) => {
|
|
420
|
+
for (const ch of chunk) {
|
|
421
|
+
const code = ch.charCodeAt(0);
|
|
422
|
+
if (code === NEWLINE || code === CARRIAGE) {
|
|
423
|
+
stdin.setRawMode?.(false);
|
|
424
|
+
stdin.pause();
|
|
425
|
+
stdin.removeListener("data", onData);
|
|
426
|
+
process.stdout.write("\n");
|
|
427
|
+
return resolve2(buf);
|
|
428
|
+
}
|
|
429
|
+
if (code === CTRL_C) {
|
|
430
|
+
stdin.setRawMode?.(false);
|
|
431
|
+
process.exit(130);
|
|
432
|
+
}
|
|
433
|
+
if (code === BACKSPACE || code === BACKSPACE_ALT) {
|
|
434
|
+
buf = buf.slice(0, -1);
|
|
435
|
+
} else {
|
|
436
|
+
buf += ch;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
stdin.on("data", onData);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/commands/watch.ts
|
|
445
|
+
import pc4 from "picocolors";
|
|
446
|
+
import { existsSync as existsSync7 } from "fs";
|
|
447
|
+
import { basename as basename2 } from "path";
|
|
448
|
+
|
|
449
|
+
// src/cursor/paths.ts
|
|
450
|
+
import { homedir as homedir2, platform } from "os";
|
|
451
|
+
import { join as join2 } from "path";
|
|
452
|
+
function defaultCursorHistoryDir() {
|
|
453
|
+
const home = homedir2();
|
|
454
|
+
switch (platform()) {
|
|
455
|
+
case "darwin":
|
|
456
|
+
return join2(home, "Library", "Application Support", "Cursor", "User", "History");
|
|
457
|
+
case "win32": {
|
|
458
|
+
const appData = process.env.APPDATA ?? join2(home, "AppData", "Roaming");
|
|
459
|
+
return join2(appData, "Cursor", "User", "History");
|
|
460
|
+
}
|
|
461
|
+
default:
|
|
462
|
+
return join2(home, ".config", "Cursor", "User", "History");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function cursorHistoryDir() {
|
|
466
|
+
return process.env.AGENTREEL_CURSOR_HISTORY_DIR ?? defaultCursorHistoryDir();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/cursor/watcher.ts
|
|
470
|
+
import chokidar from "chokidar";
|
|
471
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
472
|
+
import { existsSync as existsSync6 } from "fs";
|
|
473
|
+
import { basename, dirname as dirname3, join as join5 } from "path";
|
|
474
|
+
|
|
475
|
+
// src/cursor/entries.ts
|
|
476
|
+
import { readFile } from "fs/promises";
|
|
477
|
+
import { existsSync as existsSync4, statSync } from "fs";
|
|
478
|
+
import { join as join3, dirname as dirname2, resolve, sep as sep2 } from "path";
|
|
479
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
480
|
+
async function readEntries(historyFolder) {
|
|
481
|
+
const path = join3(historyFolder, "entries.json");
|
|
482
|
+
if (!existsSync4(path)) return null;
|
|
483
|
+
try {
|
|
484
|
+
const raw = await readFile(path, "utf8");
|
|
485
|
+
return JSON.parse(raw);
|
|
486
|
+
} catch {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function resourceToPath(resource) {
|
|
491
|
+
if (!resource.startsWith("file://")) return null;
|
|
492
|
+
try {
|
|
493
|
+
return fileURLToPath2(resource);
|
|
494
|
+
} catch {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function findWorkspaceRoot(path) {
|
|
499
|
+
let dir = dirname2(resolve(path));
|
|
500
|
+
while (dir && dir !== sep2) {
|
|
501
|
+
const git = join3(dir, ".git");
|
|
502
|
+
try {
|
|
503
|
+
if (existsSync4(git)) return dir;
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
const parent = dirname2(dir);
|
|
507
|
+
if (parent === dir) break;
|
|
508
|
+
dir = parent;
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
function isProbablyBinary(path) {
|
|
513
|
+
try {
|
|
514
|
+
const s = statSync(path);
|
|
515
|
+
if (s.size > 1024 * 1024) return true;
|
|
516
|
+
} catch {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
const text = /\.(ts|tsx|js|jsx|mjs|cjs|json|jsonc|md|mdx|css|scss|html|xml|yaml|yml|toml|sh|bash|zsh|fish|py|rb|go|rs|java|kt|swift|c|cc|cpp|h|hpp|cs|php|sql|prisma|graphql|gql|env|gitignore|dockerfile|tf|hcl|lua|vue|svelte|astro|txt|csv|tsv|log|conf|ini)$/i;
|
|
520
|
+
return !text.test(path);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/cursor/diff.ts
|
|
524
|
+
import { diffLines } from "diff";
|
|
525
|
+
import DiffMatchPatch from "diff-match-patch";
|
|
526
|
+
var dmp = new DiffMatchPatch.diff_match_patch();
|
|
527
|
+
dmp.Diff_Timeout = 1;
|
|
528
|
+
function computeDiff(before, after) {
|
|
529
|
+
const patches = dmp.patch_make(before, after);
|
|
530
|
+
const patch = dmp.patch_toText(patches);
|
|
531
|
+
let added = 0;
|
|
532
|
+
let removed = 0;
|
|
533
|
+
for (const part of diffLines(before, after)) {
|
|
534
|
+
const n = part.count ?? (part.value.match(/\n/g)?.length ?? (part.value.length > 0 ? 1 : 0));
|
|
535
|
+
if (part.added) added += n;
|
|
536
|
+
else if (part.removed) removed += n;
|
|
537
|
+
}
|
|
538
|
+
return { patch, added, removed };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/redact/ignore.ts
|
|
542
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
543
|
+
import { join as join4, relative, sep as sep3 } from "path";
|
|
544
|
+
import ignore from "ignore";
|
|
545
|
+
var FILENAME = ".agentreelignore";
|
|
546
|
+
var TTL_MS = 5e3;
|
|
547
|
+
var cache = /* @__PURE__ */ new Map();
|
|
548
|
+
function loadFor(workspace) {
|
|
549
|
+
const path = join4(workspace, FILENAME);
|
|
550
|
+
if (!existsSync5(path)) return null;
|
|
551
|
+
try {
|
|
552
|
+
const raw = readFileSync3(path, "utf8");
|
|
553
|
+
return ignore({ allowRelativePaths: true }).add(raw);
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function getCached(workspace) {
|
|
559
|
+
const entry = cache.get(workspace);
|
|
560
|
+
const path = join4(workspace, FILENAME);
|
|
561
|
+
let mtime = 0;
|
|
562
|
+
try {
|
|
563
|
+
mtime = existsSync5(path) ? statSync2(path).mtimeMs : 0;
|
|
564
|
+
} catch {
|
|
565
|
+
mtime = 0;
|
|
566
|
+
}
|
|
567
|
+
const now = Date.now();
|
|
568
|
+
if (entry && now - entry.loadedAt < TTL_MS && entry.fileMtime === mtime) {
|
|
569
|
+
return entry.matcher;
|
|
570
|
+
}
|
|
571
|
+
const matcher = loadFor(workspace);
|
|
572
|
+
cache.set(workspace, { matcher, loadedAt: now, fileMtime: mtime });
|
|
573
|
+
return matcher;
|
|
574
|
+
}
|
|
575
|
+
function isIgnored(workspace, filePath) {
|
|
576
|
+
if (!workspace) return false;
|
|
577
|
+
const matcher = getCached(workspace);
|
|
578
|
+
if (!matcher) return false;
|
|
579
|
+
const rel = relative(workspace, filePath);
|
|
580
|
+
if (!rel || rel.startsWith("..") || rel.startsWith(sep3)) return false;
|
|
581
|
+
return matcher.ignores(rel.split(sep3).join("/"));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/redact/patterns.ts
|
|
585
|
+
var PATTERNS = [
|
|
586
|
+
// PEM-encoded private keys (multiline, must run early)
|
|
587
|
+
{
|
|
588
|
+
name: "pem-private-key",
|
|
589
|
+
re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
|
|
590
|
+
replacement: "[REDACTED:private-key]"
|
|
591
|
+
},
|
|
592
|
+
// JWT (header.payload.signature) — eyJ-prefixed base64url segments
|
|
593
|
+
{
|
|
594
|
+
name: "jwt",
|
|
595
|
+
re: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
596
|
+
replacement: "[REDACTED:jwt]"
|
|
597
|
+
},
|
|
598
|
+
// GitHub fine-grained PATs (84 chars after prefix is the official format)
|
|
599
|
+
{
|
|
600
|
+
name: "github-fine-grained-pat",
|
|
601
|
+
re: /\bgithub_pat_[A-Za-z0-9_]{82,}\b/g,
|
|
602
|
+
replacement: "[REDACTED:gh-fine-pat]"
|
|
603
|
+
},
|
|
604
|
+
// GitHub OAuth, PAT, app, server, refresh tokens
|
|
605
|
+
{
|
|
606
|
+
name: "github-token",
|
|
607
|
+
re: /\bgh[pousr]_[A-Za-z0-9]{36,255}\b/g,
|
|
608
|
+
replacement: "[REDACTED:gh-token]"
|
|
609
|
+
},
|
|
610
|
+
// Anthropic
|
|
611
|
+
{
|
|
612
|
+
name: "anthropic-key",
|
|
613
|
+
re: /\bsk-ant-(?:api\d{2}-)?[A-Za-z0-9_-]{40,}\b/g,
|
|
614
|
+
replacement: "[REDACTED:anthropic-key]"
|
|
615
|
+
},
|
|
616
|
+
// OpenAI (sk-proj-..., sk-svcacct-..., legacy sk-...)
|
|
617
|
+
{
|
|
618
|
+
name: "openai-key",
|
|
619
|
+
re: /\bsk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}\b/g,
|
|
620
|
+
replacement: "[REDACTED:openai-key]"
|
|
621
|
+
},
|
|
622
|
+
// Stripe
|
|
623
|
+
{
|
|
624
|
+
name: "stripe-secret",
|
|
625
|
+
re: /\bsk_(?:test|live)_[A-Za-z0-9]{16,}\b/g,
|
|
626
|
+
replacement: "[REDACTED:stripe-secret]"
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "stripe-restricted",
|
|
630
|
+
re: /\brk_(?:test|live)_[A-Za-z0-9]{16,}\b/g,
|
|
631
|
+
replacement: "[REDACTED:stripe-restricted]"
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: "stripe-publishable",
|
|
635
|
+
re: /\bpk_(?:test|live)_[A-Za-z0-9]{16,}\b/g,
|
|
636
|
+
replacement: "[REDACTED:stripe-publishable]"
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: "stripe-webhook",
|
|
640
|
+
re: /\bwhsec_[A-Za-z0-9]{32,}\b/g,
|
|
641
|
+
replacement: "[REDACTED:stripe-webhook]"
|
|
642
|
+
},
|
|
643
|
+
// AWS access key IDs (and STS / temporary forms)
|
|
644
|
+
{
|
|
645
|
+
name: "aws-access-key-id",
|
|
646
|
+
re: /\b(?:AKIA|ASIA|AGPA|AROA|AIDA|ANPA|ANVA|AIPA)[0-9A-Z]{16}\b/g,
|
|
647
|
+
replacement: "[REDACTED:aws-key-id]"
|
|
648
|
+
},
|
|
649
|
+
// Slack tokens
|
|
650
|
+
{
|
|
651
|
+
name: "slack-token",
|
|
652
|
+
re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
653
|
+
replacement: "[REDACTED:slack-token]"
|
|
654
|
+
},
|
|
655
|
+
// Google API keys
|
|
656
|
+
{
|
|
657
|
+
name: "google-api-key",
|
|
658
|
+
re: /\bAIza[A-Za-z0-9_-]{35}\b/g,
|
|
659
|
+
replacement: "[REDACTED:google-api-key]"
|
|
660
|
+
},
|
|
661
|
+
// npm tokens
|
|
662
|
+
{
|
|
663
|
+
name: "npm-token",
|
|
664
|
+
re: /\bnpm_[A-Za-z0-9]{36}\b/g,
|
|
665
|
+
replacement: "[REDACTED:npm-token]"
|
|
666
|
+
},
|
|
667
|
+
// dotenv-style KEY=VALUE on its own line, where the KEY name looks sensitive.
|
|
668
|
+
// This is a fallback for arbitrary secrets that don't match a specific
|
|
669
|
+
// provider pattern. Captures the key, replaces the value.
|
|
670
|
+
{
|
|
671
|
+
name: "dotenv-secret",
|
|
672
|
+
re: /^(\s*(?:export\s+)?[A-Z][A-Z0-9_]*?(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|API|AUTH|CREDENTIAL|PRIVATE|SESSION|COOKIE|BEARER|DSN)[A-Z0-9_]*\s*=\s*)(['"]?)([^\n'"]{4,})\2/gm,
|
|
673
|
+
replacement: (m) => {
|
|
674
|
+
const inner = /^(\s*(?:export\s+)?[A-Z][A-Z0-9_]*\s*=\s*)(['"]?)([^\n'"]{4,})\2/.exec(m);
|
|
675
|
+
if (!inner) return "[REDACTED:dotenv-secret]";
|
|
676
|
+
const [, prefix, quote] = inner;
|
|
677
|
+
return `${prefix ?? ""}${quote ?? ""}[REDACTED:dotenv-secret]${quote ?? ""}`;
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
// Email addresses
|
|
681
|
+
{
|
|
682
|
+
name: "email",
|
|
683
|
+
re: /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/g,
|
|
684
|
+
replacement: "[REDACTED:email]"
|
|
685
|
+
},
|
|
686
|
+
// IPv4 — validates each octet is 0-255 to cut version-string false positives.
|
|
687
|
+
// Skips three benign forms below in the post-filter.
|
|
688
|
+
{
|
|
689
|
+
name: "ipv4",
|
|
690
|
+
re: /\b(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\b/g,
|
|
691
|
+
replacement: (m) => {
|
|
692
|
+
if (m === "0.0.0.0" || m === "127.0.0.1" || m === "255.255.255.255") return m;
|
|
693
|
+
return "[REDACTED:ip]";
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
];
|
|
697
|
+
var SENSITIVE_KEY_RE = /(?:^|[_\-.])(?:api[_-]?key|access[_-]?token|secret|password|passwd|pwd|authorization|bearer|credential|private[_-]?key|client[_-]?secret|webhook[_-]?secret|service[_-]?account|refresh[_-]?token)(?:$|[_\-.])/i;
|
|
698
|
+
|
|
699
|
+
// src/redact/scrubber.ts
|
|
700
|
+
function scrubString(input) {
|
|
701
|
+
if (!input) return input;
|
|
702
|
+
let s = input;
|
|
703
|
+
for (const rule of PATTERNS) {
|
|
704
|
+
if (typeof rule.replacement === "function") {
|
|
705
|
+
s = s.replace(rule.re, rule.replacement);
|
|
706
|
+
} else {
|
|
707
|
+
s = s.replace(rule.re, rule.replacement);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return s;
|
|
711
|
+
}
|
|
712
|
+
function scrubAny(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
713
|
+
if (value == null) return value;
|
|
714
|
+
if (typeof value === "string") return scrubString(value);
|
|
715
|
+
if (typeof value !== "object") return value;
|
|
716
|
+
if (seen.has(value)) return value;
|
|
717
|
+
seen.add(value);
|
|
718
|
+
if (Array.isArray(value)) {
|
|
719
|
+
return value.map((v) => scrubAny(v, seen));
|
|
720
|
+
}
|
|
721
|
+
const out = {};
|
|
722
|
+
for (const [k, v] of Object.entries(value)) {
|
|
723
|
+
if (SENSITIVE_KEY_RE.test(k) && typeof v === "string" && v.length >= 4) {
|
|
724
|
+
out[k] = "[REDACTED:by-key-name]";
|
|
725
|
+
} else {
|
|
726
|
+
out[k] = scrubAny(v, seen);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return out;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/cursor/watcher.ts
|
|
733
|
+
function startCursorWatcher(historyDir, onSnapshot, opts = {}) {
|
|
734
|
+
const skipFirst = opts.skipFirstObservation ?? true;
|
|
735
|
+
const watcher = chokidar.watch(historyDir, {
|
|
736
|
+
ignoreInitial: true,
|
|
737
|
+
depth: 2,
|
|
738
|
+
persistent: true,
|
|
739
|
+
awaitWriteFinish: { stabilityThreshold: 120, pollInterval: 40 }
|
|
740
|
+
});
|
|
741
|
+
watcher.on("add", async (path) => {
|
|
742
|
+
try {
|
|
743
|
+
const folder = dirname3(path);
|
|
744
|
+
const file = basename(path);
|
|
745
|
+
if (file === "entries.json") return;
|
|
746
|
+
const ctx = await waitForEntry(folder, file, 2e3);
|
|
747
|
+
if (!ctx) return;
|
|
748
|
+
await processSnapshot(folder, ctx, skipFirst, onSnapshot);
|
|
749
|
+
} catch (err) {
|
|
750
|
+
console.error("[agentreel] cursor watcher: error processing", path, err);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
return watcher;
|
|
754
|
+
}
|
|
755
|
+
async function waitForEntry(folder, fileId, totalMs) {
|
|
756
|
+
const start = Date.now();
|
|
757
|
+
while (Date.now() - start < totalMs) {
|
|
758
|
+
const file = await readEntries(folder);
|
|
759
|
+
if (file) {
|
|
760
|
+
const idx = file.entries.findIndex((e) => e.id === fileId);
|
|
761
|
+
if (idx >= 0) {
|
|
762
|
+
const current = file.entries[idx];
|
|
763
|
+
if (!current) return null;
|
|
764
|
+
const previous = idx > 0 ? file.entries[idx - 1] ?? null : null;
|
|
765
|
+
return { resource: file.resource, current, previous, currentIdx: idx };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
await sleep(150);
|
|
769
|
+
}
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
async function processSnapshot(folder, ctx, skipFirst, onSnapshot) {
|
|
773
|
+
const filePath = resourceToPath(ctx.resource);
|
|
774
|
+
if (!filePath) return;
|
|
775
|
+
if (NOISY_PATH.test(filePath)) return;
|
|
776
|
+
const workspace = findWorkspaceRoot(filePath);
|
|
777
|
+
if (isIgnored(workspace, filePath)) return;
|
|
778
|
+
if (!ctx.previous) {
|
|
779
|
+
if (skipFirst) return;
|
|
780
|
+
}
|
|
781
|
+
const newSnapshot = join5(folder, ctx.current.id);
|
|
782
|
+
let before = "";
|
|
783
|
+
let after = "";
|
|
784
|
+
if (ctx.previous) {
|
|
785
|
+
const prevPath = join5(folder, ctx.previous.id);
|
|
786
|
+
if (existsSync6(prevPath)) {
|
|
787
|
+
before = await safeRead(prevPath);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (existsSync6(newSnapshot)) {
|
|
791
|
+
after = await safeRead(newSnapshot);
|
|
792
|
+
}
|
|
793
|
+
if (before === after) return;
|
|
794
|
+
const binary = isProbablyBinary(filePath);
|
|
795
|
+
let patch = "";
|
|
796
|
+
let added = 0;
|
|
797
|
+
let removed = 0;
|
|
798
|
+
if (!binary) {
|
|
799
|
+
const beforeSafe = scrubString(before);
|
|
800
|
+
const afterSafe = scrubString(after);
|
|
801
|
+
const result = computeDiff(beforeSafe, afterSafe);
|
|
802
|
+
patch = result.patch;
|
|
803
|
+
added = result.added;
|
|
804
|
+
removed = result.removed;
|
|
805
|
+
}
|
|
806
|
+
const source = classifySource(ctx.current.source);
|
|
807
|
+
onSnapshot({
|
|
808
|
+
filePath,
|
|
809
|
+
workspace,
|
|
810
|
+
source,
|
|
811
|
+
timestamp: ctx.current.timestamp ?? Date.now(),
|
|
812
|
+
patch,
|
|
813
|
+
added,
|
|
814
|
+
removed,
|
|
815
|
+
binary
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function classifySource(raw) {
|
|
819
|
+
if (!raw) return "cursor-manual";
|
|
820
|
+
const s = raw.toLowerCase();
|
|
821
|
+
if (s.includes("composer") || s.includes("ai") || s.includes("chat")) return "cursor-ai";
|
|
822
|
+
return "cursor-manual";
|
|
823
|
+
}
|
|
824
|
+
async function safeRead(path) {
|
|
825
|
+
try {
|
|
826
|
+
return await readFile2(path, "utf8");
|
|
827
|
+
} catch {
|
|
828
|
+
return "";
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function sleep(ms) {
|
|
832
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
833
|
+
}
|
|
834
|
+
var NOISY_PATH = /[\\/](node_modules|\.next|\.turbo|dist|build|\.git|coverage|\.cache|\.venv|venv|target|out)[\\/]/;
|
|
835
|
+
|
|
836
|
+
// src/cursor/session.ts
|
|
837
|
+
import { nanoid } from "nanoid";
|
|
838
|
+
var IDLE_MS = 5 * 60 * 1e3;
|
|
839
|
+
var GLOBAL_KEY = "__global__";
|
|
840
|
+
var CursorSessionManager = class {
|
|
841
|
+
open = /* @__PURE__ */ new Map();
|
|
842
|
+
ingest(snapshot) {
|
|
843
|
+
const key = snapshot.workspace ?? GLOBAL_KEY;
|
|
844
|
+
const ts = snapshot.timestamp;
|
|
845
|
+
const cwd = snapshot.workspace ?? "";
|
|
846
|
+
let session = this.open.get(key);
|
|
847
|
+
let isNew = false;
|
|
848
|
+
if (!session || ts - session.lastTs > IDLE_MS) {
|
|
849
|
+
if (session) this.closeSession(session, session.lastTs);
|
|
850
|
+
session = {
|
|
851
|
+
id: `cur_${nanoid(10)}`,
|
|
852
|
+
startedAt: ts,
|
|
853
|
+
lastTs: ts,
|
|
854
|
+
cwd
|
|
855
|
+
};
|
|
856
|
+
this.open.set(key, session);
|
|
857
|
+
isNew = true;
|
|
858
|
+
upsertSession({
|
|
859
|
+
id: session.id,
|
|
860
|
+
tool: "cursor",
|
|
861
|
+
startedAt: ts,
|
|
862
|
+
cwd
|
|
863
|
+
});
|
|
864
|
+
} else {
|
|
865
|
+
session.lastTs = ts;
|
|
866
|
+
}
|
|
867
|
+
const event = {
|
|
868
|
+
id: nanoid(),
|
|
869
|
+
sessionId: session.id,
|
|
870
|
+
tool: "cursor",
|
|
871
|
+
type: "tool_use_post",
|
|
872
|
+
ts,
|
|
873
|
+
cwd,
|
|
874
|
+
payload: {
|
|
875
|
+
tool_name: "Edit",
|
|
876
|
+
file_path: snapshot.filePath,
|
|
877
|
+
added: snapshot.added,
|
|
878
|
+
removed: snapshot.removed,
|
|
879
|
+
binary: snapshot.binary,
|
|
880
|
+
source: snapshot.source,
|
|
881
|
+
patch: snapshot.patch
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
insertEvent(event);
|
|
885
|
+
return { sessionId: session.id, isNew };
|
|
886
|
+
}
|
|
887
|
+
/** Stamp ended_at on every open session — call on shutdown. */
|
|
888
|
+
closeAll() {
|
|
889
|
+
const now = Date.now();
|
|
890
|
+
for (const s of this.open.values()) this.closeSession(s, Math.max(s.lastTs, now));
|
|
891
|
+
this.open.clear();
|
|
892
|
+
}
|
|
893
|
+
closeSession(s, endedAt) {
|
|
894
|
+
upsertSession({
|
|
895
|
+
id: s.id,
|
|
896
|
+
tool: "cursor",
|
|
897
|
+
startedAt: s.startedAt,
|
|
898
|
+
endedAt,
|
|
899
|
+
cwd: s.cwd
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// src/commands/watch.ts
|
|
905
|
+
init_paths();
|
|
906
|
+
async function watchCommand() {
|
|
907
|
+
ensureAgentreelDir();
|
|
908
|
+
getDb();
|
|
909
|
+
const dir = cursorHistoryDir();
|
|
910
|
+
if (!existsSync7(dir)) {
|
|
911
|
+
console.error(pc4.red("\u2717 Cursor history directory not found:"));
|
|
912
|
+
console.error(" " + dir);
|
|
913
|
+
console.error();
|
|
914
|
+
console.error(pc4.dim("Open Cursor at least once, edit a file, then re-run."));
|
|
915
|
+
console.error(pc4.dim("(Or set AGENTREEL_CURSOR_HISTORY_DIR to a custom path.)"));
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
console.log(pc4.bold(pc4.cyan("AgentReel \xB7 Cursor watcher\n")));
|
|
919
|
+
console.log(pc4.dim(" watching ") + dir);
|
|
920
|
+
console.log(pc4.dim(" press Ctrl+C to stop\n"));
|
|
921
|
+
const sessions = new CursorSessionManager();
|
|
922
|
+
const watcher = startCursorWatcher(dir, async (snap) => {
|
|
923
|
+
const { sessionId, isNew } = sessions.ingest(snap);
|
|
924
|
+
const ts = new Date(snap.timestamp).toLocaleTimeString();
|
|
925
|
+
const ws = snap.workspace ? basename2(snap.workspace) : pc4.dim("no-workspace");
|
|
926
|
+
const file = snap.filePath.split("/").slice(-2).join("/");
|
|
927
|
+
const sourceLabel = snap.source === "cursor-ai" ? pc4.magenta("ai") : snap.source === "cursor-manual" ? pc4.cyan("man") : pc4.dim("?");
|
|
928
|
+
const stats = snap.binary ? pc4.dim("binary") : `${pc4.green("+" + snap.added)} ${pc4.red("-" + snap.removed)}`;
|
|
929
|
+
if (isNew) {
|
|
930
|
+
console.log(
|
|
931
|
+
`${pc4.dim(ts)} ${pc4.yellow("session")} ${pc4.dim(sessionId)} ${ws}`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
console.log(`${pc4.dim(ts)} edit ${sourceLabel} ${file.padEnd(36)} ${stats}`);
|
|
935
|
+
});
|
|
936
|
+
const shutdown = async () => {
|
|
937
|
+
console.log(pc4.dim("\n closing sessions\u2026"));
|
|
938
|
+
sessions.closeAll();
|
|
939
|
+
await watcher.close();
|
|
940
|
+
process.exit(0);
|
|
941
|
+
};
|
|
942
|
+
process.on("SIGINT", shutdown);
|
|
943
|
+
process.on("SIGTERM", shutdown);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/commands/push.ts
|
|
947
|
+
import pc5 from "picocolors";
|
|
948
|
+
|
|
949
|
+
// src/upload/queue.ts
|
|
950
|
+
function takePendingBatch(maxEvents = 500) {
|
|
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
|
|
1005
|
+
async function pushCommand() {
|
|
1006
|
+
const cfg = readConfig();
|
|
1007
|
+
if (!cfg.apiKey) {
|
|
1008
|
+
console.error(pc5.red("\u2717 Not linked. Run ") + pc5.cyan("agentreel link <api-key>"));
|
|
1009
|
+
process.exit(1);
|
|
1010
|
+
}
|
|
1011
|
+
let totalSessions = 0;
|
|
1012
|
+
let totalEvents = 0;
|
|
1013
|
+
while (true) {
|
|
1014
|
+
const batch = takePendingBatch(500);
|
|
1015
|
+
if (batch.events.length === 0) break;
|
|
1016
|
+
console.log(
|
|
1017
|
+
pc5.dim(` uploading ${batch.events.length} events across ${batch.sessions.length} sessions\u2026`)
|
|
1018
|
+
);
|
|
1019
|
+
const res = await postIngest(cfg, { sessions: batch.sessions, events: batch.events });
|
|
1020
|
+
markUploaded(batch.eventIds);
|
|
1021
|
+
totalSessions += res.sessions_written ?? 0;
|
|
1022
|
+
totalEvents += res.events_written ?? 0;
|
|
1023
|
+
}
|
|
1024
|
+
if (totalEvents === 0) {
|
|
1025
|
+
console.log(pc5.green("\u2713") + " queue is empty \u2014 nothing to upload.");
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
console.log(
|
|
1029
|
+
pc5.green("\u2713") + ` uploaded ${totalEvents} events \xB7 ${totalSessions} session rows touched`
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/hooks/handler.ts
|
|
1034
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
1035
|
+
import { appendFileSync } from "fs";
|
|
1036
|
+
init_paths();
|
|
1037
|
+
var HOOK_EVENT_TO_TYPE = {
|
|
1038
|
+
SessionStart: "session_start",
|
|
1039
|
+
SessionEnd: "session_end",
|
|
1040
|
+
UserPromptSubmit: "user_prompt_submit",
|
|
1041
|
+
PreToolUse: "tool_use_pre",
|
|
1042
|
+
PostToolUse: "tool_use_post",
|
|
1043
|
+
Notification: "notification",
|
|
1044
|
+
Stop: "stop",
|
|
1045
|
+
SubagentStop: "subagent_stop",
|
|
1046
|
+
PreCompact: "pre_compact"
|
|
1047
|
+
};
|
|
1048
|
+
async function readStdin() {
|
|
1049
|
+
if (process.stdin.isTTY) return "";
|
|
1050
|
+
const chunks = [];
|
|
1051
|
+
for await (const chunk of process.stdin) {
|
|
1052
|
+
chunks.push(chunk);
|
|
1053
|
+
}
|
|
1054
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1055
|
+
}
|
|
1056
|
+
function logError(err) {
|
|
1057
|
+
try {
|
|
1058
|
+
ensureAgentreelDir();
|
|
1059
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] hook error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
1060
|
+
`;
|
|
1061
|
+
appendFileSync(LOG_PATH, line);
|
|
1062
|
+
} catch {
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async function runHook(eventArg) {
|
|
1066
|
+
try {
|
|
1067
|
+
const raw = await readStdin();
|
|
1068
|
+
if (!raw.trim()) return;
|
|
1069
|
+
const input = JSON.parse(raw);
|
|
1070
|
+
const hookEventName = input.hook_event_name ?? eventArg ?? "Unknown";
|
|
1071
|
+
const type = HOOK_EVENT_TO_TYPE[hookEventName] ?? "unknown";
|
|
1072
|
+
const ts = Date.now();
|
|
1073
|
+
const sessionId = input.session_id ?? "unknown-session";
|
|
1074
|
+
if (type === "session_start") {
|
|
1075
|
+
upsertSession({
|
|
1076
|
+
id: sessionId,
|
|
1077
|
+
tool: "claude-code",
|
|
1078
|
+
startedAt: ts,
|
|
1079
|
+
cwd: input.cwd
|
|
1080
|
+
});
|
|
1081
|
+
} else if (type === "session_end") {
|
|
1082
|
+
upsertSession({
|
|
1083
|
+
id: sessionId,
|
|
1084
|
+
tool: "claude-code",
|
|
1085
|
+
startedAt: ts,
|
|
1086
|
+
endedAt: ts,
|
|
1087
|
+
cwd: input.cwd
|
|
1088
|
+
});
|
|
1089
|
+
} else {
|
|
1090
|
+
upsertSession({
|
|
1091
|
+
id: sessionId,
|
|
1092
|
+
tool: "claude-code",
|
|
1093
|
+
startedAt: ts,
|
|
1094
|
+
cwd: input.cwd
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
const safePayload = scrubAny(input);
|
|
1098
|
+
const event = {
|
|
1099
|
+
id: nanoid2(),
|
|
1100
|
+
sessionId,
|
|
1101
|
+
tool: "claude-code",
|
|
1102
|
+
type,
|
|
1103
|
+
ts,
|
|
1104
|
+
cwd: input.cwd,
|
|
1105
|
+
payload: safePayload
|
|
1106
|
+
};
|
|
1107
|
+
insertEvent(event);
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
logError(err);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/cli.ts
|
|
1114
|
+
var program = new Command();
|
|
1115
|
+
program.name("agentreel").description("AgentReel \u2014 capture Claude Code and Cursor sessions locally").version("0.0.0");
|
|
1116
|
+
program.command("init").description("install Claude Code hooks and create the local SQLite buffer").action(async () => {
|
|
1117
|
+
await initCommand();
|
|
1118
|
+
});
|
|
1119
|
+
program.command("status").description("show local capture status, recent sessions, queue depth").action(async () => {
|
|
1120
|
+
await statusCommand();
|
|
1121
|
+
});
|
|
1122
|
+
program.command("watch").description("watch Cursor's local history and capture edits as events").action(async () => {
|
|
1123
|
+
await watchCommand();
|
|
1124
|
+
});
|
|
1125
|
+
program.command("link [api-key]").description("authenticate the local agent with agentreel.dev").option("--api <url>", "override the API base URL (default https://api.agentreel.dev)").action(async (apiKey, opts) => {
|
|
1126
|
+
await linkCommand(apiKey, opts);
|
|
1127
|
+
});
|
|
1128
|
+
program.command("push").description("upload pending events to agentreel.dev").action(async () => {
|
|
1129
|
+
await pushCommand();
|
|
1130
|
+
});
|
|
1131
|
+
program.command("logout").description("clear local credentials").action(async () => {
|
|
1132
|
+
await logoutCommand();
|
|
1133
|
+
});
|
|
1134
|
+
program.command("uninstall").description("remove AgentReel hooks from ~/.claude/settings.json").action(async () => {
|
|
1135
|
+
await uninstallCommand();
|
|
1136
|
+
});
|
|
1137
|
+
program.command("hook <event>").description("internal: hook handler invoked by Claude Code (reads JSON from stdin)").action(async (event) => {
|
|
1138
|
+
await runHook(event);
|
|
1139
|
+
});
|
|
1140
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1141
|
+
const cmd = process.argv[2];
|
|
1142
|
+
if (cmd === "hook") {
|
|
1143
|
+
process.exit(0);
|
|
1144
|
+
}
|
|
1145
|
+
console.error(err);
|
|
1146
|
+
process.exit(1);
|
|
1147
|
+
});
|
|
1148
|
+
//# sourceMappingURL=cli.js.map
|