agent-insights 0.0.1
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/README.md +63 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1017 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.js +484 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/cursor.ts
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
|
|
10
|
+
// src/config/config.ts
|
|
11
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
12
|
+
import { dirname } from "path";
|
|
13
|
+
|
|
14
|
+
// src/config/paths.ts
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
function configRoot() {
|
|
18
|
+
return process.env.AGENT_INSIGHTS_HOME ?? join(homedir(), ".agent-insights");
|
|
19
|
+
}
|
|
20
|
+
function configFile() {
|
|
21
|
+
return join(configRoot(), "config.json");
|
|
22
|
+
}
|
|
23
|
+
function sessionCacheDir() {
|
|
24
|
+
return join(configRoot(), "session-cache");
|
|
25
|
+
}
|
|
26
|
+
function logsDir() {
|
|
27
|
+
return join(configRoot(), "logs");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/config/config.ts
|
|
31
|
+
function defaultConfig() {
|
|
32
|
+
return {
|
|
33
|
+
version: 1,
|
|
34
|
+
enabled: true,
|
|
35
|
+
vercel: {
|
|
36
|
+
team: "vercel-labs",
|
|
37
|
+
project: "agent-insights"
|
|
38
|
+
},
|
|
39
|
+
userConsent: {
|
|
40
|
+
eventTelemetry: false,
|
|
41
|
+
transcriptSync: false,
|
|
42
|
+
sessionAnalysis: false
|
|
43
|
+
},
|
|
44
|
+
exporter: {
|
|
45
|
+
type: "otlp",
|
|
46
|
+
endpoint: process.env.AGENT_INSIGHTS_OTEL_ENDPOINT ?? "",
|
|
47
|
+
headers: {},
|
|
48
|
+
dataset: "agent-insights"
|
|
49
|
+
},
|
|
50
|
+
transcriptStore: {
|
|
51
|
+
type: "blob",
|
|
52
|
+
tokenEnv: "BLOB_READ_WRITE_TOKEN",
|
|
53
|
+
prefix: "sessions/"
|
|
54
|
+
},
|
|
55
|
+
sessionAnalysis: {
|
|
56
|
+
enabled: false,
|
|
57
|
+
analyzerUrl: process.env.AGENT_INSIGHTS_ANALYZER_URL ?? "",
|
|
58
|
+
secretEnv: "AGENT_INSIGHTS_INGEST_SECRET",
|
|
59
|
+
githubIssueRepo: "vercel-labs/agent-insights"
|
|
60
|
+
},
|
|
61
|
+
agents: {
|
|
62
|
+
claudeCode: { enabled: false },
|
|
63
|
+
cursor: { enabled: false }
|
|
64
|
+
},
|
|
65
|
+
privacy: {
|
|
66
|
+
capturePrompts: false,
|
|
67
|
+
captureMessages: false,
|
|
68
|
+
captureToolInputs: false,
|
|
69
|
+
captureFileContents: false
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function loadConfig() {
|
|
74
|
+
try {
|
|
75
|
+
const raw = await readFile(configFile(), "utf8");
|
|
76
|
+
const parsed = JSON.parse(raw);
|
|
77
|
+
return mergeConfig(defaultConfig(), parsed);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (err.code === "ENOENT") return void 0;
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function saveConfig(cfg) {
|
|
84
|
+
await ensureDirs();
|
|
85
|
+
await writeFile(configFile(), `${JSON.stringify(cfg, null, 2)}
|
|
86
|
+
`, "utf8");
|
|
87
|
+
}
|
|
88
|
+
async function ensureDirs() {
|
|
89
|
+
await mkdir(dirname(configFile()), { recursive: true });
|
|
90
|
+
await mkdir(sessionCacheDir(), { recursive: true });
|
|
91
|
+
await mkdir(logsDir(), { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
function mergeConfig(base, patch) {
|
|
94
|
+
return {
|
|
95
|
+
...base,
|
|
96
|
+
...patch,
|
|
97
|
+
vercel: { ...base.vercel, ...patch.vercel ?? {} },
|
|
98
|
+
userConsent: { ...base.userConsent, ...patch.userConsent ?? {} },
|
|
99
|
+
exporter: { ...base.exporter, ...patch.exporter ?? {} },
|
|
100
|
+
transcriptStore: patch.transcriptStore ?? base.transcriptStore,
|
|
101
|
+
sessionAnalysis: {
|
|
102
|
+
...base.sessionAnalysis,
|
|
103
|
+
...patch.sessionAnalysis ?? {}
|
|
104
|
+
},
|
|
105
|
+
agents: {
|
|
106
|
+
claudeCode: {
|
|
107
|
+
...base.agents.claudeCode,
|
|
108
|
+
...patch.agents?.claudeCode ?? {}
|
|
109
|
+
},
|
|
110
|
+
cursor: { ...base.agents.cursor, ...patch.agents?.cursor ?? {} }
|
|
111
|
+
},
|
|
112
|
+
privacy: base.privacy
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/privacy/sanitize.ts
|
|
117
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
|
|
118
|
+
"prompt",
|
|
119
|
+
"prompts",
|
|
120
|
+
"message",
|
|
121
|
+
"messages",
|
|
122
|
+
"content",
|
|
123
|
+
"text",
|
|
124
|
+
"transcript",
|
|
125
|
+
"toolinput",
|
|
126
|
+
"input",
|
|
127
|
+
"arguments",
|
|
128
|
+
"args",
|
|
129
|
+
"params",
|
|
130
|
+
"file",
|
|
131
|
+
"filecontent",
|
|
132
|
+
"filecontents",
|
|
133
|
+
"diff",
|
|
134
|
+
"patch",
|
|
135
|
+
"secret",
|
|
136
|
+
"secrets",
|
|
137
|
+
"token",
|
|
138
|
+
"apikey",
|
|
139
|
+
"authorization",
|
|
140
|
+
"password",
|
|
141
|
+
"credentials"
|
|
142
|
+
]);
|
|
143
|
+
var MAX_STRING_LENGTH = 256;
|
|
144
|
+
function normalizeKey(key) {
|
|
145
|
+
return key.toLowerCase().replace(/[_\-\s]/g, "");
|
|
146
|
+
}
|
|
147
|
+
function truncate(value) {
|
|
148
|
+
if (value.length <= MAX_STRING_LENGTH) return value;
|
|
149
|
+
return `${value.slice(0, MAX_STRING_LENGTH)}\u2026[truncated]`;
|
|
150
|
+
}
|
|
151
|
+
function sanitizePrimitive(value) {
|
|
152
|
+
if (value === null) return null;
|
|
153
|
+
switch (typeof value) {
|
|
154
|
+
case "string":
|
|
155
|
+
return truncate(value);
|
|
156
|
+
case "number":
|
|
157
|
+
return Number.isFinite(value) ? value : null;
|
|
158
|
+
case "boolean":
|
|
159
|
+
return value;
|
|
160
|
+
default:
|
|
161
|
+
return void 0;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function sanitize(input) {
|
|
165
|
+
return sanitizeInner(input);
|
|
166
|
+
}
|
|
167
|
+
function sanitizeInner(value) {
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const item of value) {
|
|
171
|
+
const sanitizedItem = sanitizeInner(item);
|
|
172
|
+
if (sanitizedItem !== void 0) {
|
|
173
|
+
out.push(sanitizedItem);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
if (value && typeof value === "object") {
|
|
179
|
+
const out = {};
|
|
180
|
+
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
181
|
+
if (FORBIDDEN_KEYS.has(normalizeKey(rawKey))) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (rawValue && typeof rawValue === "object") {
|
|
185
|
+
out[rawKey] = sanitizeInner(rawValue);
|
|
186
|
+
} else {
|
|
187
|
+
const primitive = sanitizePrimitive(rawValue);
|
|
188
|
+
if (primitive !== void 0) {
|
|
189
|
+
out[rawKey] = primitive;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
return sanitizePrimitive(value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/util/identity.ts
|
|
199
|
+
import { execSync } from "child_process";
|
|
200
|
+
import { hostname, userInfo } from "os";
|
|
201
|
+
|
|
202
|
+
// src/privacy/hash.ts
|
|
203
|
+
import { createHash } from "crypto";
|
|
204
|
+
function sha256(value) {
|
|
205
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/util/identity.ts
|
|
209
|
+
function userHash() {
|
|
210
|
+
try {
|
|
211
|
+
const user = userInfo().username;
|
|
212
|
+
return sha256(`${user}@${hostname()}`);
|
|
213
|
+
} catch {
|
|
214
|
+
return sha256("unknown");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function repoHash(cwd = process.cwd()) {
|
|
218
|
+
const remote = tryGit(["config", "--get", "remote.origin.url"], cwd);
|
|
219
|
+
return sha256(remote ?? cwd);
|
|
220
|
+
}
|
|
221
|
+
function workspaceHash(cwd = process.cwd()) {
|
|
222
|
+
return sha256(cwd);
|
|
223
|
+
}
|
|
224
|
+
function tryGit(args, cwd) {
|
|
225
|
+
try {
|
|
226
|
+
const out = execSync(`git ${args.join(" ")}`, {
|
|
227
|
+
cwd,
|
|
228
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
229
|
+
encoding: "utf8",
|
|
230
|
+
timeout: 1500
|
|
231
|
+
});
|
|
232
|
+
const trimmed = out.trim();
|
|
233
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
234
|
+
} catch {
|
|
235
|
+
return void 0;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/util/log.ts
|
|
240
|
+
import kleur from "kleur";
|
|
241
|
+
var log = {
|
|
242
|
+
info: (msg) => {
|
|
243
|
+
process.stdout.write(`${msg}
|
|
244
|
+
`);
|
|
245
|
+
},
|
|
246
|
+
ok: (msg) => {
|
|
247
|
+
process.stdout.write(`${kleur.green("\u2713")} ${msg}
|
|
248
|
+
`);
|
|
249
|
+
},
|
|
250
|
+
warn: (msg) => {
|
|
251
|
+
process.stdout.write(`${kleur.yellow("!")} ${msg}
|
|
252
|
+
`);
|
|
253
|
+
},
|
|
254
|
+
fail: (msg) => {
|
|
255
|
+
process.stderr.write(`${kleur.red("\u2717")} ${msg}
|
|
256
|
+
`);
|
|
257
|
+
},
|
|
258
|
+
hint: (msg) => {
|
|
259
|
+
process.stdout.write(`${kleur.dim(msg)}
|
|
260
|
+
`);
|
|
261
|
+
},
|
|
262
|
+
json: (obj) => {
|
|
263
|
+
process.stdout.write(`${JSON.stringify(obj, null, 2)}
|
|
264
|
+
`);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
function debug(msg, extra) {
|
|
268
|
+
if (process.env.AGENT_INSIGHTS_DEBUG) {
|
|
269
|
+
const payload = extra ? ` ${JSON.stringify(extra)}` : "";
|
|
270
|
+
process.stderr.write(`[agent-insights] ${msg}${payload}
|
|
271
|
+
`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/commands/cursor.ts
|
|
276
|
+
async function runCursorWrapper(opts) {
|
|
277
|
+
const cfg = await loadConfig();
|
|
278
|
+
if (!cfg) {
|
|
279
|
+
log.fail("agent-insights is not initialized \u2014 run `agent-insights init`");
|
|
280
|
+
process.exitCode = 1;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const path = opts.path ?? process.cwd();
|
|
284
|
+
const sessionId = randomUUID();
|
|
285
|
+
const baseEvent = {
|
|
286
|
+
agent: "cursor",
|
|
287
|
+
sessionId,
|
|
288
|
+
userHash: userHash(),
|
|
289
|
+
repoHash: repoHash(),
|
|
290
|
+
workspaceHash: workspaceHash()
|
|
291
|
+
};
|
|
292
|
+
emit({
|
|
293
|
+
...baseEvent,
|
|
294
|
+
type: "session.start",
|
|
295
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
296
|
+
});
|
|
297
|
+
const code = await spawnCursor(path);
|
|
298
|
+
emit({
|
|
299
|
+
...baseEvent,
|
|
300
|
+
type: "session.end",
|
|
301
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
302
|
+
outcome: code === 0 ? "success" : "error"
|
|
303
|
+
});
|
|
304
|
+
process.exitCode = code;
|
|
305
|
+
}
|
|
306
|
+
function spawnCursor(path) {
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
const child = spawn("cursor", [path], {
|
|
309
|
+
stdio: "inherit",
|
|
310
|
+
env: process.env
|
|
311
|
+
});
|
|
312
|
+
child.on("error", (err) => {
|
|
313
|
+
log.fail(`failed to launch cursor: ${err.message}`);
|
|
314
|
+
resolve(127);
|
|
315
|
+
});
|
|
316
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function emit(event) {
|
|
320
|
+
debug("cursor wrapper event", {
|
|
321
|
+
type: event.type,
|
|
322
|
+
sessionId: event.sessionId
|
|
323
|
+
});
|
|
324
|
+
void sanitize(event);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/commands/disable.ts
|
|
328
|
+
import { rm } from "fs/promises";
|
|
329
|
+
|
|
330
|
+
// src/adapters/claude-code.ts
|
|
331
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
332
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
333
|
+
var HOOK_COMMAND_NAME = "agent-insights";
|
|
334
|
+
var HOOK_MAP = {
|
|
335
|
+
SessionStart: "session.start",
|
|
336
|
+
SessionEnd: "session.end",
|
|
337
|
+
UserPromptSubmit: "user.prompt.submit",
|
|
338
|
+
PreToolUse: "tool.start",
|
|
339
|
+
PostToolUse: "tool.end",
|
|
340
|
+
PostToolUseFailure: "tool.failure",
|
|
341
|
+
PermissionRequest: "permission.request",
|
|
342
|
+
PermissionDenied: "permission.denied",
|
|
343
|
+
SubagentStart: "subagent.start",
|
|
344
|
+
SubagentStop: "subagent.end",
|
|
345
|
+
Stop: "agent.stop",
|
|
346
|
+
Notification: "notification",
|
|
347
|
+
PreCompact: "context.compact.start",
|
|
348
|
+
PostCompact: "context.compact.end"
|
|
349
|
+
};
|
|
350
|
+
var CLAUDE_HOOK_EVENTS = Object.keys(HOOK_MAP);
|
|
351
|
+
var claudeCodeAdapter = {
|
|
352
|
+
id: "claude-code",
|
|
353
|
+
agent: "claude-code",
|
|
354
|
+
label: "Claude Code",
|
|
355
|
+
settingsFile: ".claude/settings.json",
|
|
356
|
+
map(payload) {
|
|
357
|
+
const type = HOOK_MAP[payload.event];
|
|
358
|
+
if (!type) return void 0;
|
|
359
|
+
const data = payload.data ?? {};
|
|
360
|
+
const sessionId = asString(data["session_id"]) ?? asString(data["sessionId"]);
|
|
361
|
+
const toolName = asString(data["tool_name"]) ?? asString(data["toolName"]) ?? asString(data["tool"]?.["name"]);
|
|
362
|
+
const transcriptPath = asString(data["transcript_path"]) ?? asString(data["transcriptPath"]);
|
|
363
|
+
return {
|
|
364
|
+
type,
|
|
365
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
366
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
367
|
+
...transcriptPath !== void 0 ? { transcriptPath } : {}
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
async install(repoRoot) {
|
|
371
|
+
const path = join2(repoRoot, ".claude/settings.json");
|
|
372
|
+
const settings = await readJson(path);
|
|
373
|
+
const next = { ...settings ?? {} };
|
|
374
|
+
const hooks = { ...next.hooks ?? {} };
|
|
375
|
+
for (const event of CLAUDE_HOOK_EVENTS) {
|
|
376
|
+
const existing = (hooks[event] ?? []).filter(
|
|
377
|
+
(h) => !isAgentInsightsCommand(h.command)
|
|
378
|
+
);
|
|
379
|
+
existing.push({
|
|
380
|
+
type: "command",
|
|
381
|
+
command: `${HOOK_COMMAND_NAME} hook ${event}`
|
|
382
|
+
});
|
|
383
|
+
hooks[event] = existing;
|
|
384
|
+
}
|
|
385
|
+
next.hooks = hooks;
|
|
386
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
387
|
+
await writeFile2(path, `${JSON.stringify(next, null, 2)}
|
|
388
|
+
`, "utf8");
|
|
389
|
+
return { written: true, path };
|
|
390
|
+
},
|
|
391
|
+
async uninstall(repoRoot) {
|
|
392
|
+
const path = join2(repoRoot, ".claude/settings.json");
|
|
393
|
+
const settings = await readJson(path);
|
|
394
|
+
if (!settings?.hooks) return { removed: false, path };
|
|
395
|
+
const hooks = { ...settings.hooks };
|
|
396
|
+
let touched = false;
|
|
397
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
398
|
+
const filtered = entries.filter(
|
|
399
|
+
(h) => !isAgentInsightsCommand(h.command)
|
|
400
|
+
);
|
|
401
|
+
if (filtered.length !== entries.length) touched = true;
|
|
402
|
+
if (filtered.length === 0) {
|
|
403
|
+
delete hooks[event];
|
|
404
|
+
} else {
|
|
405
|
+
hooks[event] = filtered;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
settings.hooks = hooks;
|
|
409
|
+
await writeFile2(path, `${JSON.stringify(settings, null, 2)}
|
|
410
|
+
`, "utf8");
|
|
411
|
+
return { removed: touched, path };
|
|
412
|
+
},
|
|
413
|
+
async isInstalled(repoRoot) {
|
|
414
|
+
const settings = await readJson(
|
|
415
|
+
join2(repoRoot, ".claude/settings.json")
|
|
416
|
+
);
|
|
417
|
+
if (!settings?.hooks) return false;
|
|
418
|
+
return Object.values(settings.hooks).some(
|
|
419
|
+
(entries) => entries.some((h) => isAgentInsightsCommand(h.command))
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
function isAgentInsightsCommand(cmd) {
|
|
424
|
+
if (!cmd) return false;
|
|
425
|
+
return cmd.startsWith(`${HOOK_COMMAND_NAME} hook`);
|
|
426
|
+
}
|
|
427
|
+
async function readJson(path) {
|
|
428
|
+
try {
|
|
429
|
+
const raw = await readFile2(path, "utf8");
|
|
430
|
+
return JSON.parse(raw);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
if (err.code === "ENOENT") return void 0;
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function asString(v) {
|
|
437
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/adapters/cursor.ts
|
|
441
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
442
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
443
|
+
var HOOK_COMMAND_NAME2 = "agent-insights";
|
|
444
|
+
var HOOK_MAP2 = {
|
|
445
|
+
SessionStart: "session.start",
|
|
446
|
+
SessionEnd: "session.end",
|
|
447
|
+
UserPromptSubmit: "user.prompt.submit",
|
|
448
|
+
PreToolUse: "tool.start",
|
|
449
|
+
PostToolUse: "tool.end",
|
|
450
|
+
Stop: "agent.stop"
|
|
451
|
+
};
|
|
452
|
+
var CURSOR_HOOK_EVENTS = Object.keys(HOOK_MAP2);
|
|
453
|
+
var cursorAdapter = {
|
|
454
|
+
id: "cursor",
|
|
455
|
+
agent: "cursor",
|
|
456
|
+
label: "Cursor",
|
|
457
|
+
settingsFile: ".cursor/hooks.json",
|
|
458
|
+
map(payload) {
|
|
459
|
+
const type = HOOK_MAP2[payload.event];
|
|
460
|
+
if (!type) return void 0;
|
|
461
|
+
const data = payload.data ?? {};
|
|
462
|
+
const sessionId = asString2(data["session_id"]) ?? asString2(data["sessionId"]);
|
|
463
|
+
const toolName = asString2(data["tool_name"]) ?? asString2(data["toolName"]);
|
|
464
|
+
const transcriptPath = asString2(data["transcript_path"]) ?? asString2(data["transcriptPath"]);
|
|
465
|
+
return {
|
|
466
|
+
type,
|
|
467
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
468
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
469
|
+
...transcriptPath !== void 0 ? { transcriptPath } : {}
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
async install(repoRoot) {
|
|
473
|
+
const path = join3(repoRoot, ".cursor/hooks.json");
|
|
474
|
+
const settings = await readJson2(path);
|
|
475
|
+
const next = { ...settings ?? {} };
|
|
476
|
+
const hooks = { ...next.hooks ?? {} };
|
|
477
|
+
for (const event of CURSOR_HOOK_EVENTS) {
|
|
478
|
+
const existing = (hooks[event] ?? []).filter(
|
|
479
|
+
(h) => !isAgentInsightsCommand2(h.command)
|
|
480
|
+
);
|
|
481
|
+
existing.push({ command: `${HOOK_COMMAND_NAME2} hook ${event}` });
|
|
482
|
+
hooks[event] = existing;
|
|
483
|
+
}
|
|
484
|
+
next.hooks = hooks;
|
|
485
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
486
|
+
await writeFile3(path, `${JSON.stringify(next, null, 2)}
|
|
487
|
+
`, "utf8");
|
|
488
|
+
return { written: true, path };
|
|
489
|
+
},
|
|
490
|
+
async uninstall(repoRoot) {
|
|
491
|
+
const path = join3(repoRoot, ".cursor/hooks.json");
|
|
492
|
+
const settings = await readJson2(path);
|
|
493
|
+
if (!settings?.hooks) return { removed: false, path };
|
|
494
|
+
const hooks = { ...settings.hooks };
|
|
495
|
+
let touched = false;
|
|
496
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
497
|
+
const filtered = entries.filter(
|
|
498
|
+
(h) => !isAgentInsightsCommand2(h.command)
|
|
499
|
+
);
|
|
500
|
+
if (filtered.length !== entries.length) touched = true;
|
|
501
|
+
if (filtered.length === 0) {
|
|
502
|
+
delete hooks[event];
|
|
503
|
+
} else {
|
|
504
|
+
hooks[event] = filtered;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
settings.hooks = hooks;
|
|
508
|
+
await writeFile3(path, `${JSON.stringify(settings, null, 2)}
|
|
509
|
+
`, "utf8");
|
|
510
|
+
return { removed: touched, path };
|
|
511
|
+
},
|
|
512
|
+
async isInstalled(repoRoot) {
|
|
513
|
+
const settings = await readJson2(
|
|
514
|
+
join3(repoRoot, ".cursor/hooks.json")
|
|
515
|
+
);
|
|
516
|
+
if (!settings?.hooks) return false;
|
|
517
|
+
return Object.values(settings.hooks).some(
|
|
518
|
+
(entries) => entries.some((h) => isAgentInsightsCommand2(h.command))
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
function isAgentInsightsCommand2(cmd) {
|
|
523
|
+
if (!cmd) return false;
|
|
524
|
+
return cmd.startsWith(`${HOOK_COMMAND_NAME2} hook`);
|
|
525
|
+
}
|
|
526
|
+
async function readJson2(path) {
|
|
527
|
+
try {
|
|
528
|
+
const raw = await readFile3(path, "utf8");
|
|
529
|
+
return JSON.parse(raw);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
if (err.code === "ENOENT") return void 0;
|
|
532
|
+
throw err;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function asString2(v) {
|
|
536
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/adapters/registry.ts
|
|
540
|
+
var adapters = {
|
|
541
|
+
"claude-code": claudeCodeAdapter,
|
|
542
|
+
cursor: cursorAdapter
|
|
543
|
+
};
|
|
544
|
+
var adapterList = Object.values(adapters);
|
|
545
|
+
|
|
546
|
+
// src/commands/disable.ts
|
|
547
|
+
async function runDisable(opts) {
|
|
548
|
+
const repoRoot = process.cwd();
|
|
549
|
+
const targets = opts.agent ? [opts.agent] : adapterList.map((a) => a.id);
|
|
550
|
+
for (const id of targets) {
|
|
551
|
+
if (id !== "claude-code" && id !== "cursor") {
|
|
552
|
+
log.warn(`Unknown agent id "${id}"`);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const adapter = adapterList.find((a) => a.id === id);
|
|
556
|
+
if (!adapter) continue;
|
|
557
|
+
const { removed, path } = await adapter.uninstall(repoRoot);
|
|
558
|
+
if (removed) log.ok(`Removed ${adapter.label} hooks from ${path}`);
|
|
559
|
+
else log.hint(`${adapter.label}: no hooks to remove (${path})`);
|
|
560
|
+
}
|
|
561
|
+
const cfg = await loadConfig();
|
|
562
|
+
if (cfg) {
|
|
563
|
+
if (!opts.agent || opts.agent === "claude-code") {
|
|
564
|
+
cfg.agents.claudeCode.enabled = false;
|
|
565
|
+
}
|
|
566
|
+
if (!opts.agent || opts.agent === "cursor") {
|
|
567
|
+
cfg.agents.cursor.enabled = false;
|
|
568
|
+
}
|
|
569
|
+
if (!opts.agent) cfg.enabled = false;
|
|
570
|
+
await saveConfig(cfg);
|
|
571
|
+
}
|
|
572
|
+
if (opts.purge) {
|
|
573
|
+
await rm(configRoot(), { recursive: true, force: true });
|
|
574
|
+
log.ok(`Purged ${configRoot()}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/commands/doctor.ts
|
|
579
|
+
import { existsSync } from "fs";
|
|
580
|
+
import { join as join4 } from "path";
|
|
581
|
+
import kleur2 from "kleur";
|
|
582
|
+
|
|
583
|
+
// src/transcript/store.ts
|
|
584
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
585
|
+
import { put } from "@vercel/blob";
|
|
586
|
+
var NoopTranscriptStore = class {
|
|
587
|
+
async upload() {
|
|
588
|
+
throw new Error("transcript store disabled (transcriptStore.type=none)");
|
|
589
|
+
}
|
|
590
|
+
async ping() {
|
|
591
|
+
return { ok: false, reason: "transcript store disabled" };
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
var BlobTranscriptStore = class {
|
|
595
|
+
constructor(token, prefix) {
|
|
596
|
+
this.token = token;
|
|
597
|
+
this.prefix = prefix;
|
|
598
|
+
}
|
|
599
|
+
token;
|
|
600
|
+
prefix;
|
|
601
|
+
async upload(input) {
|
|
602
|
+
const body = await readFile4(input.transcriptPath);
|
|
603
|
+
const requestedPath = buildPathname(
|
|
604
|
+
this.prefix,
|
|
605
|
+
input.userHash,
|
|
606
|
+
input.sessionId
|
|
607
|
+
);
|
|
608
|
+
const blob = await put(requestedPath, body, {
|
|
609
|
+
access: "public",
|
|
610
|
+
token: this.token,
|
|
611
|
+
contentType: "application/jsonl",
|
|
612
|
+
addRandomSuffix: true
|
|
613
|
+
});
|
|
614
|
+
return { pathname: blob.pathname, url: blob.url, size: body.byteLength };
|
|
615
|
+
}
|
|
616
|
+
async ping() {
|
|
617
|
+
if (!this.token) return { ok: false, reason: "missing token" };
|
|
618
|
+
return { ok: true };
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
function createTranscriptStore(cfg) {
|
|
622
|
+
if (cfg.type === "none") return new NoopTranscriptStore();
|
|
623
|
+
const token = process.env[cfg.tokenEnv];
|
|
624
|
+
if (!token) {
|
|
625
|
+
throw new Error(
|
|
626
|
+
`transcript store: env ${cfg.tokenEnv} is not set (run 'vercel env pull .env.local')`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return new BlobTranscriptStore(token, cfg.prefix);
|
|
630
|
+
}
|
|
631
|
+
function buildPathname(prefix, userHash2, sessionId) {
|
|
632
|
+
const safePrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
633
|
+
const safeSession = encodeURIComponent(sessionId);
|
|
634
|
+
return `${safePrefix}${userHash2}/${safeSession}/transcript.jsonl`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/commands/doctor.ts
|
|
638
|
+
async function runDoctor(opts) {
|
|
639
|
+
const repoRoot = process.cwd();
|
|
640
|
+
const checks = [];
|
|
641
|
+
const cfg = await loadConfig();
|
|
642
|
+
checks.push({
|
|
643
|
+
name: "config found",
|
|
644
|
+
ok: cfg != null,
|
|
645
|
+
detail: cfg ? configFile() : `missing ${configFile()}`
|
|
646
|
+
});
|
|
647
|
+
if (cfg) {
|
|
648
|
+
checks.push({
|
|
649
|
+
name: "vercel project linked",
|
|
650
|
+
ok: existsSync(join4(repoRoot, ".vercel/project.json")),
|
|
651
|
+
detail: "run `vercel link --scope vercel-labs --project agent-insights`"
|
|
652
|
+
});
|
|
653
|
+
if (cfg.userConsent.transcriptSync) {
|
|
654
|
+
try {
|
|
655
|
+
const store = createTranscriptStore(cfg.transcriptStore);
|
|
656
|
+
const ping = await store.ping();
|
|
657
|
+
if (ping.ok) {
|
|
658
|
+
checks.push({ name: "transcript store reachable", ok: true });
|
|
659
|
+
} else {
|
|
660
|
+
checks.push({
|
|
661
|
+
name: "transcript store reachable",
|
|
662
|
+
ok: false,
|
|
663
|
+
detail: ping.reason
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
} catch (err) {
|
|
667
|
+
checks.push({
|
|
668
|
+
name: "transcript store reachable",
|
|
669
|
+
ok: false,
|
|
670
|
+
detail: err.message
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
for (const adapter of adapterList) {
|
|
675
|
+
const enabled = adapter.id === "claude-code" ? cfg.agents.claudeCode.enabled : cfg.agents.cursor.enabled;
|
|
676
|
+
if (!enabled) continue;
|
|
677
|
+
const installed = await adapter.isInstalled(repoRoot);
|
|
678
|
+
checks.push({
|
|
679
|
+
name: `${adapter.label} hooks installed`,
|
|
680
|
+
ok: installed,
|
|
681
|
+
detail: installed ? join4(repoRoot, adapter.settingsFile) : "run `agent-insights init`"
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (opts.json) {
|
|
686
|
+
log.json({ ok: checks.every((c) => c.ok), checks });
|
|
687
|
+
} else {
|
|
688
|
+
for (const c of checks) {
|
|
689
|
+
const tag = c.ok ? kleur2.green("\u2713") : kleur2.red("\u2717");
|
|
690
|
+
const detail = c.detail ? kleur2.dim(` (${c.detail})`) : "";
|
|
691
|
+
log.info(`${tag} ${c.name}${detail}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
process.exitCode = checks.every((c) => c.ok) ? 0 : 1;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/commands/hook.ts
|
|
698
|
+
async function runHook(eventName, opts) {
|
|
699
|
+
const cfg = await loadConfig();
|
|
700
|
+
if (!cfg || !cfg.enabled) {
|
|
701
|
+
debug("hook skipped \u2014 agent-insights not enabled");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const stdin = await readStdinSafely();
|
|
705
|
+
const data = parseJsonObject(stdin);
|
|
706
|
+
const payload = { event: eventName, data };
|
|
707
|
+
const adapter = pickAdapter(opts.agent, eventName);
|
|
708
|
+
if (!adapter) {
|
|
709
|
+
debug(`no adapter resolved for event ${eventName}`);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const mapped = adapter.map(payload);
|
|
713
|
+
if (!mapped) {
|
|
714
|
+
debug(`adapter ${adapter.id} did not map event ${eventName}`);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const event = buildEvent(adapter, mapped);
|
|
718
|
+
debug("hook event", {
|
|
719
|
+
agent: event.agent,
|
|
720
|
+
type: event.type,
|
|
721
|
+
sessionId: event.sessionId
|
|
722
|
+
});
|
|
723
|
+
if (mapped.type === "session.end" && cfg.userConsent.transcriptSync && mapped.transcriptPath) {
|
|
724
|
+
try {
|
|
725
|
+
const store = createTranscriptStore(cfg.transcriptStore);
|
|
726
|
+
const result = await store.upload({
|
|
727
|
+
transcriptPath: mapped.transcriptPath,
|
|
728
|
+
userHash: event.userHash ?? userHash(),
|
|
729
|
+
sessionId: mapped.sessionId ?? "unknown",
|
|
730
|
+
agent: adapter.id
|
|
731
|
+
});
|
|
732
|
+
debug("transcript uploaded", {
|
|
733
|
+
size: result.size,
|
|
734
|
+
pathname: result.pathname
|
|
735
|
+
});
|
|
736
|
+
if (cfg.userConsent.sessionAnalysis && cfg.sessionAnalysis.analyzerUrl) {
|
|
737
|
+
await notifyAnalyzer({
|
|
738
|
+
analyzerUrl: cfg.sessionAnalysis.analyzerUrl,
|
|
739
|
+
secretEnv: cfg.sessionAnalysis.secretEnv,
|
|
740
|
+
payload: {
|
|
741
|
+
sessionId: mapped.sessionId ?? "unknown",
|
|
742
|
+
agent: adapter.id,
|
|
743
|
+
userHash: event.userHash ?? userHash(),
|
|
744
|
+
blobUrl: result.url,
|
|
745
|
+
timestamp: event.timestamp
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
} catch (err) {
|
|
750
|
+
log.fail(`transcript upload failed: ${err.message}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
void sanitize(event);
|
|
754
|
+
}
|
|
755
|
+
async function notifyAnalyzer(opts) {
|
|
756
|
+
try {
|
|
757
|
+
const headers = {
|
|
758
|
+
"content-type": "application/json"
|
|
759
|
+
};
|
|
760
|
+
const secret = process.env[opts.secretEnv];
|
|
761
|
+
if (secret) headers["x-agent-insights-secret"] = secret;
|
|
762
|
+
const url = new URL("/api/sessions", opts.analyzerUrl).toString();
|
|
763
|
+
const res = await fetch(url, {
|
|
764
|
+
method: "POST",
|
|
765
|
+
headers,
|
|
766
|
+
body: JSON.stringify(opts.payload)
|
|
767
|
+
});
|
|
768
|
+
if (!res.ok) {
|
|
769
|
+
debug(`analyzer notify failed: ${res.status} ${res.statusText}`);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
debug("analyzer notified", { sessionId: opts.payload.sessionId });
|
|
773
|
+
} catch (err) {
|
|
774
|
+
debug(`analyzer notify error: ${err.message}`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function pickAdapter(override, eventName) {
|
|
778
|
+
if (override) {
|
|
779
|
+
return adapterList.find((a) => a.id === override);
|
|
780
|
+
}
|
|
781
|
+
for (const a of adapterList) {
|
|
782
|
+
if (a.map({ event: eventName })) return a;
|
|
783
|
+
}
|
|
784
|
+
return void 0;
|
|
785
|
+
}
|
|
786
|
+
function buildEvent(adapter, mapped) {
|
|
787
|
+
return {
|
|
788
|
+
type: mapped.type,
|
|
789
|
+
agent: adapter.agent,
|
|
790
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
791
|
+
...mapped.sessionId !== void 0 ? { sessionId: mapped.sessionId } : {},
|
|
792
|
+
...mapped.toolName !== void 0 ? { toolName: mapped.toolName } : {},
|
|
793
|
+
...mapped.permissionType !== void 0 ? { permissionType: mapped.permissionType } : {},
|
|
794
|
+
...mapped.outcome !== void 0 ? { outcome: mapped.outcome } : {},
|
|
795
|
+
userHash: userHash(),
|
|
796
|
+
repoHash: repoHash(),
|
|
797
|
+
workspaceHash: workspaceHash()
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
var STDIN_TIMEOUT_MS = 200;
|
|
801
|
+
async function readStdinSafely() {
|
|
802
|
+
if (process.stdin.isTTY) return "";
|
|
803
|
+
return new Promise((resolve) => {
|
|
804
|
+
let buf = "";
|
|
805
|
+
let settled = false;
|
|
806
|
+
const finish = () => {
|
|
807
|
+
if (settled) return;
|
|
808
|
+
settled = true;
|
|
809
|
+
process.stdin.removeAllListeners("data");
|
|
810
|
+
process.stdin.removeAllListeners("end");
|
|
811
|
+
process.stdin.removeAllListeners("error");
|
|
812
|
+
try {
|
|
813
|
+
process.stdin.pause();
|
|
814
|
+
process.stdin.unref();
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
resolve(buf);
|
|
818
|
+
};
|
|
819
|
+
const idle = setTimeout(finish, STDIN_TIMEOUT_MS);
|
|
820
|
+
idle.unref();
|
|
821
|
+
process.stdin.setEncoding("utf8");
|
|
822
|
+
process.stdin.on("data", (chunk) => {
|
|
823
|
+
buf += chunk;
|
|
824
|
+
idle.refresh();
|
|
825
|
+
});
|
|
826
|
+
process.stdin.on("end", () => {
|
|
827
|
+
clearTimeout(idle);
|
|
828
|
+
finish();
|
|
829
|
+
});
|
|
830
|
+
process.stdin.on("error", () => {
|
|
831
|
+
clearTimeout(idle);
|
|
832
|
+
finish();
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
function parseJsonObject(raw) {
|
|
837
|
+
if (!raw) return void 0;
|
|
838
|
+
try {
|
|
839
|
+
const parsed = JSON.parse(raw);
|
|
840
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
841
|
+
return parsed;
|
|
842
|
+
}
|
|
843
|
+
return void 0;
|
|
844
|
+
} catch {
|
|
845
|
+
return void 0;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/commands/init.ts
|
|
850
|
+
import { existsSync as existsSync2 } from "fs";
|
|
851
|
+
import { join as join5 } from "path";
|
|
852
|
+
import kleur3 from "kleur";
|
|
853
|
+
var CONSENT_TEXT = `Agent Insights is opt-in (internal).
|
|
854
|
+
|
|
855
|
+
OTLP / lifecycle metrics: event names, timestamps, tool names, outcomes,
|
|
856
|
+
hashed user/repo/workspace IDs \u2014 never prompts, messages, or tool inputs.
|
|
857
|
+
|
|
858
|
+
Vercel Blob (optional): full session transcripts for internal analysis;
|
|
859
|
+
username stored as SHA256 only.
|
|
860
|
+
|
|
861
|
+
SessionEnd analysis (optional): an internal LLM reviews transcript for
|
|
862
|
+
tooling gaps; may post to Slack with a GitHub issue link.`;
|
|
863
|
+
async function runInit(opts) {
|
|
864
|
+
log.info(kleur3.bold("Agent Insights setup\n"));
|
|
865
|
+
log.hint(CONSENT_TEXT);
|
|
866
|
+
log.info("");
|
|
867
|
+
const existing = await loadConfig();
|
|
868
|
+
if (existing && !opts.force) {
|
|
869
|
+
log.warn(
|
|
870
|
+
`Config already exists at ${configFile()} \u2014 re-run with --force to overwrite.`
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
const requested = parseAgents(opts.agents);
|
|
874
|
+
const cfg = existing ?? defaultConfig();
|
|
875
|
+
cfg.userConsent.transcriptSync = opts.transcripts ?? true;
|
|
876
|
+
cfg.userConsent.sessionAnalysis = opts.analysis ?? false;
|
|
877
|
+
cfg.userConsent.eventTelemetry = opts.telemetry ?? false;
|
|
878
|
+
cfg.agents.claudeCode.enabled = requested.has("claude-code");
|
|
879
|
+
cfg.agents.cursor.enabled = requested.has("cursor");
|
|
880
|
+
await saveConfig(cfg);
|
|
881
|
+
log.ok(`Wrote config to ${configFile()}`);
|
|
882
|
+
const installHooks = opts.hooks ?? true;
|
|
883
|
+
const repoRoot = process.cwd();
|
|
884
|
+
if (installHooks) {
|
|
885
|
+
for (const adapter of adapterList) {
|
|
886
|
+
const enabled = adapter.id === "claude-code" && cfg.agents.claudeCode.enabled || adapter.id === "cursor" && cfg.agents.cursor.enabled;
|
|
887
|
+
if (!enabled) continue;
|
|
888
|
+
const { path } = await adapter.install(repoRoot);
|
|
889
|
+
log.ok(`Installed ${adapter.label} hooks \u2192 ${rel(path)}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
printVercelHint(repoRoot);
|
|
893
|
+
log.info("");
|
|
894
|
+
log.info(`Config root: ${configRoot()}`);
|
|
895
|
+
log.hint("Run `agent-insights doctor` to verify the setup.");
|
|
896
|
+
}
|
|
897
|
+
function parseAgents(value) {
|
|
898
|
+
const out = /* @__PURE__ */ new Set();
|
|
899
|
+
const list = (value ?? "claude-code,cursor").split(",").map((s) => s.trim()).filter(Boolean);
|
|
900
|
+
for (const id of list) {
|
|
901
|
+
if (id === "claude-code" || id === "cursor") out.add(id);
|
|
902
|
+
else log.warn(`Unknown agent id "${id}" \u2014 skipping.`);
|
|
903
|
+
}
|
|
904
|
+
return out;
|
|
905
|
+
}
|
|
906
|
+
function printVercelHint(repoRoot) {
|
|
907
|
+
const linked = existsSync2(join5(repoRoot, ".vercel/project.json"));
|
|
908
|
+
const envFile = existsSync2(join5(repoRoot, ".env.local"));
|
|
909
|
+
if (!linked) {
|
|
910
|
+
log.hint(
|
|
911
|
+
"Vercel project not linked. Run `vercel link --scope vercel-labs --project agent-insights`."
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
if (!envFile) {
|
|
915
|
+
log.hint(
|
|
916
|
+
"No .env.local found. Run `vercel env pull .env.local` to fetch BLOB_READ_WRITE_TOKEN."
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function rel(p) {
|
|
921
|
+
const cwd = process.cwd();
|
|
922
|
+
return p.startsWith(cwd) ? p.slice(cwd.length + 1) : p;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/commands/status.ts
|
|
926
|
+
import { existsSync as existsSync3 } from "fs";
|
|
927
|
+
import { join as join6 } from "path";
|
|
928
|
+
import kleur4 from "kleur";
|
|
929
|
+
async function runStatus(opts) {
|
|
930
|
+
const cfg = await loadConfig();
|
|
931
|
+
const repoRoot = process.cwd();
|
|
932
|
+
if (!cfg) {
|
|
933
|
+
if (opts.json) log.json({ initialized: false });
|
|
934
|
+
else log.warn(`No config found at ${configFile()} \u2014 run \`agent-insights init\`.`);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const adapterStatus = await Promise.all(
|
|
938
|
+
adapterList.map(async (a) => ({
|
|
939
|
+
id: a.id,
|
|
940
|
+
label: a.label,
|
|
941
|
+
enabled: a.id === "claude-code" ? cfg.agents.claudeCode.enabled : cfg.agents.cursor.enabled,
|
|
942
|
+
installed: await a.isInstalled(repoRoot)
|
|
943
|
+
}))
|
|
944
|
+
);
|
|
945
|
+
if (opts.json) {
|
|
946
|
+
log.json({
|
|
947
|
+
initialized: true,
|
|
948
|
+
configPath: configFile(),
|
|
949
|
+
enabled: cfg.enabled,
|
|
950
|
+
consent: cfg.userConsent,
|
|
951
|
+
vercel: {
|
|
952
|
+
linked: existsSync3(join6(repoRoot, ".vercel/project.json")),
|
|
953
|
+
envFile: existsSync3(join6(repoRoot, ".env.local"))
|
|
954
|
+
},
|
|
955
|
+
adapters: adapterStatus
|
|
956
|
+
});
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
log.info(kleur4.bold("Agent Insights status\n"));
|
|
960
|
+
log.info(`config ${configFile()}`);
|
|
961
|
+
log.info(`enabled ${cfg.enabled ? "yes" : "no"}`);
|
|
962
|
+
log.info("");
|
|
963
|
+
log.info(kleur4.bold("Consent"));
|
|
964
|
+
log.info(` event telemetry ${yn(cfg.userConsent.eventTelemetry)}`);
|
|
965
|
+
log.info(` transcript sync ${yn(cfg.userConsent.transcriptSync)}`);
|
|
966
|
+
log.info(` session analysis ${yn(cfg.userConsent.sessionAnalysis)}`);
|
|
967
|
+
log.info("");
|
|
968
|
+
log.info(kleur4.bold("Adapters"));
|
|
969
|
+
for (const a of adapterStatus) {
|
|
970
|
+
log.info(
|
|
971
|
+
` ${a.label.padEnd(12)} ${a.enabled ? "enabled" : "disabled"} hooks ${a.installed ? "installed" : "not installed"}`
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
log.info("");
|
|
975
|
+
log.info(kleur4.bold("Vercel"));
|
|
976
|
+
log.info(
|
|
977
|
+
` linked ${yn(existsSync3(join6(repoRoot, ".vercel/project.json")))}`
|
|
978
|
+
);
|
|
979
|
+
log.info(` .env.local ${yn(existsSync3(join6(repoRoot, ".env.local")))}`);
|
|
980
|
+
}
|
|
981
|
+
function yn(v) {
|
|
982
|
+
return v ? kleur4.green("yes") : kleur4.dim("no");
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/cli.ts
|
|
986
|
+
var VERSION = "0.0.1";
|
|
987
|
+
var program = new Command();
|
|
988
|
+
program.name("agent-insights").description(
|
|
989
|
+
"Internal CLI for AI coding agent observability (Claude Code, Cursor)."
|
|
990
|
+
).version(VERSION);
|
|
991
|
+
program.command("init").description("Set up agent-insights in this repo.").option(
|
|
992
|
+
"--agents <list>",
|
|
993
|
+
"Comma-separated agent ids (claude-code,cursor)",
|
|
994
|
+
"claude-code,cursor"
|
|
995
|
+
).option("--transcripts", "Opt into transcript sync to Vercel Blob").option("--no-transcripts", "Skip transcript sync").option("--analysis", "Opt into SessionEnd analysis (Phase 2)").option("--telemetry", "Opt into OTLP event telemetry (Phase 3)").option("--no-hooks", "Skip installing per-agent hooks").option("--force", "Overwrite existing config").action(async (opts) => {
|
|
996
|
+
await runInit(opts);
|
|
997
|
+
});
|
|
998
|
+
program.command("status").description("Show current config, consent, and adapter state.").option("--json", "Emit machine-readable JSON").action(async (opts) => {
|
|
999
|
+
await runStatus(opts);
|
|
1000
|
+
});
|
|
1001
|
+
program.command("doctor").description("Verify config, Vercel link, Blob, and per-agent hook install.").option("--json", "Emit machine-readable JSON").action(async (opts) => {
|
|
1002
|
+
await runDoctor(opts);
|
|
1003
|
+
});
|
|
1004
|
+
program.command("disable").description("Disable agent-insights and/or remove installed hooks.").option("--agent <name>", "Disable a specific adapter (claude-code|cursor)").option("--purge", "Also delete the local config and caches").action(async (opts) => {
|
|
1005
|
+
await runDisable(opts);
|
|
1006
|
+
});
|
|
1007
|
+
program.command("hook <eventName>").description("Internal: invoked by an agent's hook configuration.").option("--agent <name>", "Override agent inference (claude-code|cursor)").action(async (eventName, opts) => {
|
|
1008
|
+
await runHook(eventName, opts);
|
|
1009
|
+
});
|
|
1010
|
+
program.command("cursor [path]").description("Launch Cursor with session start/end emitted (fallback).").action(async (path) => {
|
|
1011
|
+
await runCursorWrapper({ ...path !== void 0 ? { path } : {} });
|
|
1012
|
+
});
|
|
1013
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1014
|
+
log.fail(err.message ?? String(err));
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
});
|
|
1017
|
+
//# sourceMappingURL=cli.js.map
|