agent-insights 0.0.1 → 0.0.6
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 +409 -79
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +44 -39
- package/dist/index.js +50 -25
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
12
|
-
import { dirname } from "path";
|
|
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
|
+
};
|
|
13
11
|
|
|
14
12
|
// src/config/paths.ts
|
|
15
13
|
import { homedir } from "os";
|
|
@@ -20,18 +18,278 @@ function configRoot() {
|
|
|
20
18
|
function configFile() {
|
|
21
19
|
return join(configRoot(), "config.json");
|
|
22
20
|
}
|
|
21
|
+
function authFile() {
|
|
22
|
+
return join(configRoot(), "auth.json");
|
|
23
|
+
}
|
|
23
24
|
function sessionCacheDir() {
|
|
24
25
|
return join(configRoot(), "session-cache");
|
|
25
26
|
}
|
|
26
27
|
function logsDir() {
|
|
27
28
|
return join(configRoot(), "logs");
|
|
28
29
|
}
|
|
30
|
+
var init_paths = __esm({
|
|
31
|
+
"src/config/paths.ts"() {
|
|
32
|
+
"use strict";
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// src/auth/oauth.ts
|
|
37
|
+
import { createHash as createHash2, randomBytes } from "crypto";
|
|
38
|
+
import { exec } from "child_process";
|
|
39
|
+
import { createServer } from "http";
|
|
40
|
+
import { platform } from "os";
|
|
41
|
+
function generateCodeVerifier() {
|
|
42
|
+
return randomBytes(48).toString("base64url");
|
|
43
|
+
}
|
|
44
|
+
function generateCodeChallenge(verifier) {
|
|
45
|
+
return createHash2("sha256").update(verifier).digest("base64url");
|
|
46
|
+
}
|
|
47
|
+
function generateState() {
|
|
48
|
+
return randomBytes(16).toString("hex");
|
|
49
|
+
}
|
|
50
|
+
async function startPkceFlow(timeoutMs = 12e4) {
|
|
51
|
+
const codeVerifier = generateCodeVerifier();
|
|
52
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
53
|
+
const state = generateState();
|
|
54
|
+
const authUrl = new URL(AUTH_ENDPOINT);
|
|
55
|
+
authUrl.searchParams.set("client_id", CLIENT_ID);
|
|
56
|
+
authUrl.searchParams.set("response_type", "code");
|
|
57
|
+
authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
|
|
58
|
+
authUrl.searchParams.set("scope", SCOPES);
|
|
59
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
60
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
61
|
+
authUrl.searchParams.set("state", state);
|
|
62
|
+
const url = authUrl.toString();
|
|
63
|
+
const codePromise = listenForCallback(state, timeoutMs);
|
|
64
|
+
openBrowser(url);
|
|
65
|
+
process.stderr.write(`
|
|
66
|
+
If the browser did not open, visit:
|
|
67
|
+
${url}
|
|
68
|
+
|
|
69
|
+
`);
|
|
70
|
+
const code = await codePromise;
|
|
71
|
+
return { code, codeVerifier };
|
|
72
|
+
}
|
|
73
|
+
function listenForCallback(expectedState, timeoutMs) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const server = createServer((req, res) => {
|
|
76
|
+
const url = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
|
|
77
|
+
if (url.pathname !== "/callback") {
|
|
78
|
+
res.writeHead(404);
|
|
79
|
+
res.end("Not found");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const code = url.searchParams.get("code");
|
|
83
|
+
const returnedState = url.searchParams.get("state");
|
|
84
|
+
const error = url.searchParams.get("error");
|
|
85
|
+
const html = (msg) => `<html><body style="font-family:sans-serif;text-align:center;padding:80px"><h2>${msg}</h2><p>You can close this tab.</p></body></html>`;
|
|
86
|
+
if (error) {
|
|
87
|
+
res.writeHead(400, { "content-type": "text/html" });
|
|
88
|
+
res.end(html(`Login failed: ${error}`));
|
|
89
|
+
server.close();
|
|
90
|
+
reject(new Error(`OAuth error: ${error}`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (returnedState !== expectedState) {
|
|
94
|
+
res.writeHead(400, { "content-type": "text/html" });
|
|
95
|
+
res.end(html("Invalid state parameter \u2014 please try again."));
|
|
96
|
+
server.close();
|
|
97
|
+
reject(new Error("State mismatch \u2014 possible CSRF"));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!code) {
|
|
101
|
+
res.writeHead(400, { "content-type": "text/html" });
|
|
102
|
+
res.end(html("No authorization code received."));
|
|
103
|
+
server.close();
|
|
104
|
+
reject(new Error("No code in callback"));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
108
|
+
res.end(html("\u2713 Logged in! You can close this tab."));
|
|
109
|
+
server.close();
|
|
110
|
+
resolve(code);
|
|
111
|
+
});
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
server.close();
|
|
114
|
+
reject(new Error("Login timed out \u2014 no response within 2 minutes."));
|
|
115
|
+
}, timeoutMs);
|
|
116
|
+
timer.unref();
|
|
117
|
+
server.listen(CALLBACK_PORT, "127.0.0.1", () => {
|
|
118
|
+
});
|
|
119
|
+
server.on("error", (err) => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
if (err.code === "EADDRINUSE") {
|
|
122
|
+
reject(
|
|
123
|
+
new Error(
|
|
124
|
+
`Port ${CALLBACK_PORT} is already in use. Close any other agent-insights login process and try again.`
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
} else {
|
|
128
|
+
reject(err);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function exchangeCodeForToken(code, codeVerifier) {
|
|
134
|
+
const res = await fetch(TOKEN_ENDPOINT, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
137
|
+
body: new URLSearchParams({
|
|
138
|
+
grant_type: "authorization_code",
|
|
139
|
+
client_id: CLIENT_ID,
|
|
140
|
+
code,
|
|
141
|
+
code_verifier: codeVerifier,
|
|
142
|
+
redirect_uri: REDIRECT_URI
|
|
143
|
+
})
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
const body = await res.text();
|
|
147
|
+
throw new Error(`Token exchange failed (${res.status}): ${body}`);
|
|
148
|
+
}
|
|
149
|
+
return res.json();
|
|
150
|
+
}
|
|
151
|
+
async function refreshAccessToken(refreshToken) {
|
|
152
|
+
const res = await fetch(TOKEN_ENDPOINT, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
155
|
+
body: new URLSearchParams({
|
|
156
|
+
grant_type: "refresh_token",
|
|
157
|
+
client_id: CLIENT_ID,
|
|
158
|
+
refresh_token: refreshToken
|
|
159
|
+
})
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const body = await res.text();
|
|
163
|
+
throw new Error(`Token refresh failed (${res.status}): ${body}`);
|
|
164
|
+
}
|
|
165
|
+
return res.json();
|
|
166
|
+
}
|
|
167
|
+
async function fetchUserInfo(accessToken) {
|
|
168
|
+
const res = await fetch(USERINFO_ENDPOINT, {
|
|
169
|
+
headers: { authorization: `Bearer ${accessToken}` }
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
throw new Error(`Failed to fetch user info (${res.status})`);
|
|
173
|
+
}
|
|
174
|
+
return res.json();
|
|
175
|
+
}
|
|
176
|
+
function openBrowser(url) {
|
|
177
|
+
const os = platform();
|
|
178
|
+
let cmd;
|
|
179
|
+
if (os === "darwin") cmd = `open "${url}"`;
|
|
180
|
+
else if (os === "win32") cmd = `start "" "${url}"`;
|
|
181
|
+
else cmd = `xdg-open "${url}"`;
|
|
182
|
+
exec(cmd, () => {
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
var CLIENT_ID, AUTH_ENDPOINT, TOKEN_ENDPOINT, USERINFO_ENDPOINT, CALLBACK_PORT, REDIRECT_URI, SCOPES;
|
|
186
|
+
var init_oauth = __esm({
|
|
187
|
+
"src/auth/oauth.ts"() {
|
|
188
|
+
"use strict";
|
|
189
|
+
CLIENT_ID = process.env.AGENT_INSIGHTS_CLIENT_ID ?? "cl_TxqqNFTHcF9kEtM48LBFsYfwjhJ9T8sz";
|
|
190
|
+
AUTH_ENDPOINT = "https://vercel.com/oauth/authorize";
|
|
191
|
+
TOKEN_ENDPOINT = "https://api.vercel.com/login/oauth/token";
|
|
192
|
+
USERINFO_ENDPOINT = "https://api.vercel.com/login/oauth/userinfo";
|
|
193
|
+
CALLBACK_PORT = 9797;
|
|
194
|
+
REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
195
|
+
SCOPES = "openid email profile offline_access";
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// src/auth/store.ts
|
|
200
|
+
var store_exports = {};
|
|
201
|
+
__export(store_exports, {
|
|
202
|
+
clearAuth: () => clearAuth,
|
|
203
|
+
getValidToken: () => getValidToken,
|
|
204
|
+
loadAuth: () => loadAuth,
|
|
205
|
+
saveAuth: () => saveAuth
|
|
206
|
+
});
|
|
207
|
+
import { chmod, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
208
|
+
import { dirname as dirname4 } from "path";
|
|
209
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
210
|
+
async function loadAuth() {
|
|
211
|
+
try {
|
|
212
|
+
const raw = await readFile4(authFile(), "utf8");
|
|
213
|
+
const parsed = JSON.parse(raw);
|
|
214
|
+
if (typeof parsed.accessToken === "string" && parsed.accessToken.length > 0) {
|
|
215
|
+
return parsed;
|
|
216
|
+
}
|
|
217
|
+
return void 0;
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err.code === "ENOENT") return void 0;
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function saveAuth(state) {
|
|
224
|
+
const path = authFile();
|
|
225
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
226
|
+
await writeFile4(path, `${JSON.stringify(state, null, 2)}
|
|
227
|
+
`, {
|
|
228
|
+
encoding: "utf8",
|
|
229
|
+
mode: 384
|
|
230
|
+
});
|
|
231
|
+
try {
|
|
232
|
+
await chmod(path, 384);
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function clearAuth() {
|
|
237
|
+
const { unlink } = await import("fs/promises");
|
|
238
|
+
try {
|
|
239
|
+
await unlink(authFile());
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (err.code !== "ENOENT") throw err;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function getValidToken() {
|
|
245
|
+
const auth = await loadAuth();
|
|
246
|
+
if (!auth) return void 0;
|
|
247
|
+
const needsRefresh = Date.now() >= auth.expiresAt - REFRESH_BUFFER_MS;
|
|
248
|
+
if (!needsRefresh) return auth.accessToken;
|
|
249
|
+
if (!auth.refreshToken) {
|
|
250
|
+
await clearAuth();
|
|
251
|
+
return void 0;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const tokens = await refreshAccessToken(auth.refreshToken);
|
|
255
|
+
const refreshed = {
|
|
256
|
+
accessToken: tokens.access_token,
|
|
257
|
+
refreshToken: tokens.refresh_token ?? auth.refreshToken,
|
|
258
|
+
expiresAt: Date.now() + tokens.expires_in * 1e3
|
|
259
|
+
};
|
|
260
|
+
await saveAuth(refreshed);
|
|
261
|
+
return refreshed.accessToken;
|
|
262
|
+
} catch {
|
|
263
|
+
await clearAuth();
|
|
264
|
+
return void 0;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
var REFRESH_BUFFER_MS;
|
|
268
|
+
var init_store = __esm({
|
|
269
|
+
"src/auth/store.ts"() {
|
|
270
|
+
"use strict";
|
|
271
|
+
init_paths();
|
|
272
|
+
init_oauth();
|
|
273
|
+
REFRESH_BUFFER_MS = 6e4;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// src/cli.ts
|
|
278
|
+
import { Command } from "commander";
|
|
279
|
+
|
|
280
|
+
// src/commands/cursor.ts
|
|
281
|
+
import { spawn } from "child_process";
|
|
282
|
+
import { randomUUID } from "crypto";
|
|
29
283
|
|
|
30
284
|
// src/config/config.ts
|
|
285
|
+
init_paths();
|
|
286
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
287
|
+
import { dirname } from "path";
|
|
31
288
|
function defaultConfig() {
|
|
32
289
|
return {
|
|
33
290
|
version: 1,
|
|
34
291
|
enabled: true,
|
|
292
|
+
hooksScope: "global",
|
|
35
293
|
vercel: {
|
|
36
294
|
team: "vercel-labs",
|
|
37
295
|
project: "agent-insights"
|
|
@@ -55,7 +313,6 @@ function defaultConfig() {
|
|
|
55
313
|
sessionAnalysis: {
|
|
56
314
|
enabled: false,
|
|
57
315
|
analyzerUrl: process.env.AGENT_INSIGHTS_ANALYZER_URL ?? "",
|
|
58
|
-
secretEnv: "AGENT_INSIGHTS_INGEST_SECRET",
|
|
59
316
|
githubIssueRepo: "vercel-labs/agent-insights"
|
|
60
317
|
},
|
|
61
318
|
agents: {
|
|
@@ -329,6 +586,7 @@ import { rm } from "fs/promises";
|
|
|
329
586
|
|
|
330
587
|
// src/adapters/claude-code.ts
|
|
331
588
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
589
|
+
import { homedir as homedir2 } from "os";
|
|
332
590
|
import { dirname as dirname2, join as join2 } from "path";
|
|
333
591
|
var HOOK_COMMAND_NAME = "agent-insights";
|
|
334
592
|
var HOOK_MAP = {
|
|
@@ -367,8 +625,8 @@ var claudeCodeAdapter = {
|
|
|
367
625
|
...transcriptPath !== void 0 ? { transcriptPath } : {}
|
|
368
626
|
};
|
|
369
627
|
},
|
|
370
|
-
async install(repoRoot) {
|
|
371
|
-
const path =
|
|
628
|
+
async install(repoRoot, scope = "global") {
|
|
629
|
+
const path = resolveClaudePath(repoRoot, scope);
|
|
372
630
|
const settings = await readJson(path);
|
|
373
631
|
const next = { ...settings ?? {} };
|
|
374
632
|
const hooks = { ...next.hooks ?? {} };
|
|
@@ -388,8 +646,8 @@ var claudeCodeAdapter = {
|
|
|
388
646
|
`, "utf8");
|
|
389
647
|
return { written: true, path };
|
|
390
648
|
},
|
|
391
|
-
async uninstall(repoRoot) {
|
|
392
|
-
const path =
|
|
649
|
+
async uninstall(repoRoot, scope = "global") {
|
|
650
|
+
const path = resolveClaudePath(repoRoot, scope);
|
|
393
651
|
const settings = await readJson(path);
|
|
394
652
|
if (!settings?.hooks) return { removed: false, path };
|
|
395
653
|
const hooks = { ...settings.hooks };
|
|
@@ -410,9 +668,9 @@ var claudeCodeAdapter = {
|
|
|
410
668
|
`, "utf8");
|
|
411
669
|
return { removed: touched, path };
|
|
412
670
|
},
|
|
413
|
-
async isInstalled(repoRoot) {
|
|
671
|
+
async isInstalled(repoRoot, scope = "global") {
|
|
414
672
|
const settings = await readJson(
|
|
415
|
-
|
|
673
|
+
resolveClaudePath(repoRoot, scope)
|
|
416
674
|
);
|
|
417
675
|
if (!settings?.hooks) return false;
|
|
418
676
|
return Object.values(settings.hooks).some(
|
|
@@ -420,6 +678,9 @@ var claudeCodeAdapter = {
|
|
|
420
678
|
);
|
|
421
679
|
}
|
|
422
680
|
};
|
|
681
|
+
function resolveClaudePath(repoRoot, scope) {
|
|
682
|
+
return scope === "global" ? join2(homedir2(), ".claude", "settings.json") : join2(repoRoot, ".claude", "settings.json");
|
|
683
|
+
}
|
|
423
684
|
function isAgentInsightsCommand(cmd) {
|
|
424
685
|
if (!cmd) return false;
|
|
425
686
|
return cmd.startsWith(`${HOOK_COMMAND_NAME} hook`);
|
|
@@ -439,15 +700,25 @@ function asString(v) {
|
|
|
439
700
|
|
|
440
701
|
// src/adapters/cursor.ts
|
|
441
702
|
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
703
|
+
import { homedir as homedir3 } from "os";
|
|
442
704
|
import { dirname as dirname3, join as join3 } from "path";
|
|
443
705
|
var HOOK_COMMAND_NAME2 = "agent-insights";
|
|
444
706
|
var HOOK_MAP2 = {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
707
|
+
sessionStart: "session.start",
|
|
708
|
+
sessionEnd: "session.end",
|
|
709
|
+
beforeSubmitPrompt: "user.prompt.submit",
|
|
710
|
+
preToolUse: "tool.start",
|
|
711
|
+
postToolUse: "tool.end",
|
|
712
|
+
postToolUseFailure: "tool.failure",
|
|
713
|
+
subagentStart: "subagent.start",
|
|
714
|
+
subagentStop: "subagent.end",
|
|
715
|
+
stop: "agent.stop",
|
|
716
|
+
preCompact: "context.compact.start",
|
|
717
|
+
// Additional events — mapped to existing schema types
|
|
718
|
+
beforeShellExecution: "tool.start",
|
|
719
|
+
afterShellExecution: "tool.end",
|
|
720
|
+
afterFileEdit: "tool.end",
|
|
721
|
+
workspaceOpen: "session.start"
|
|
451
722
|
};
|
|
452
723
|
var CURSOR_HOOK_EVENTS = Object.keys(HOOK_MAP2);
|
|
453
724
|
var cursorAdapter = {
|
|
@@ -459,9 +730,9 @@ var cursorAdapter = {
|
|
|
459
730
|
const type = HOOK_MAP2[payload.event];
|
|
460
731
|
if (!type) return void 0;
|
|
461
732
|
const data = payload.data ?? {};
|
|
462
|
-
const sessionId = asString2(data["session_id"]) ?? asString2(data["sessionId"]);
|
|
733
|
+
const sessionId = asString2(data["session_id"]) ?? asString2(data["sessionId"]) ?? asString2(data["conversation_id"]);
|
|
463
734
|
const toolName = asString2(data["tool_name"]) ?? asString2(data["toolName"]);
|
|
464
|
-
const transcriptPath = asString2(data["transcript_path"]) ?? asString2(data["transcriptPath"]);
|
|
735
|
+
const transcriptPath = asString2(data["transcript_path"]) ?? asString2(data["transcriptPath"]) ?? asString2(process.env.CURSOR_TRANSCRIPT_PATH);
|
|
465
736
|
return {
|
|
466
737
|
type,
|
|
467
738
|
...sessionId !== void 0 ? { sessionId } : {},
|
|
@@ -469,10 +740,13 @@ var cursorAdapter = {
|
|
|
469
740
|
...transcriptPath !== void 0 ? { transcriptPath } : {}
|
|
470
741
|
};
|
|
471
742
|
},
|
|
472
|
-
async install(repoRoot) {
|
|
473
|
-
const path =
|
|
743
|
+
async install(repoRoot, scope = "global") {
|
|
744
|
+
const path = resolveCursorPath(repoRoot, scope);
|
|
474
745
|
const settings = await readJson2(path);
|
|
475
|
-
const next = {
|
|
746
|
+
const next = {
|
|
747
|
+
version: 1,
|
|
748
|
+
...settings ?? {}
|
|
749
|
+
};
|
|
476
750
|
const hooks = { ...next.hooks ?? {} };
|
|
477
751
|
for (const event of CURSOR_HOOK_EVENTS) {
|
|
478
752
|
const existing = (hooks[event] ?? []).filter(
|
|
@@ -487,8 +761,8 @@ var cursorAdapter = {
|
|
|
487
761
|
`, "utf8");
|
|
488
762
|
return { written: true, path };
|
|
489
763
|
},
|
|
490
|
-
async uninstall(repoRoot) {
|
|
491
|
-
const path =
|
|
764
|
+
async uninstall(repoRoot, scope = "global") {
|
|
765
|
+
const path = resolveCursorPath(repoRoot, scope);
|
|
492
766
|
const settings = await readJson2(path);
|
|
493
767
|
if (!settings?.hooks) return { removed: false, path };
|
|
494
768
|
const hooks = { ...settings.hooks };
|
|
@@ -509,9 +783,9 @@ var cursorAdapter = {
|
|
|
509
783
|
`, "utf8");
|
|
510
784
|
return { removed: touched, path };
|
|
511
785
|
},
|
|
512
|
-
async isInstalled(repoRoot) {
|
|
786
|
+
async isInstalled(repoRoot, scope = "global") {
|
|
513
787
|
const settings = await readJson2(
|
|
514
|
-
|
|
788
|
+
resolveCursorPath(repoRoot, scope)
|
|
515
789
|
);
|
|
516
790
|
if (!settings?.hooks) return false;
|
|
517
791
|
return Object.values(settings.hooks).some(
|
|
@@ -519,6 +793,9 @@ var cursorAdapter = {
|
|
|
519
793
|
);
|
|
520
794
|
}
|
|
521
795
|
};
|
|
796
|
+
function resolveCursorPath(repoRoot, scope) {
|
|
797
|
+
return scope === "global" ? join3(homedir3(), ".cursor", "hooks.json") : join3(repoRoot, ".cursor", "hooks.json");
|
|
798
|
+
}
|
|
522
799
|
function isAgentInsightsCommand2(cmd) {
|
|
523
800
|
if (!cmd) return false;
|
|
524
801
|
return cmd.startsWith(`${HOOK_COMMAND_NAME2} hook`);
|
|
@@ -544,6 +821,7 @@ var adapters = {
|
|
|
544
821
|
var adapterList = Object.values(adapters);
|
|
545
822
|
|
|
546
823
|
// src/commands/disable.ts
|
|
824
|
+
init_paths();
|
|
547
825
|
async function runDisable(opts) {
|
|
548
826
|
const repoRoot = process.cwd();
|
|
549
827
|
const targets = opts.agent ? [opts.agent] : adapterList.map((a) => a.id);
|
|
@@ -576,12 +854,12 @@ async function runDisable(opts) {
|
|
|
576
854
|
}
|
|
577
855
|
|
|
578
856
|
// src/commands/doctor.ts
|
|
579
|
-
import { existsSync } from "fs";
|
|
580
|
-
import { join as join4 } from "path";
|
|
581
857
|
import kleur2 from "kleur";
|
|
858
|
+
init_paths();
|
|
859
|
+
init_store();
|
|
582
860
|
|
|
583
861
|
// src/transcript/store.ts
|
|
584
|
-
import { readFile as
|
|
862
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
585
863
|
import { put } from "@vercel/blob";
|
|
586
864
|
var NoopTranscriptStore = class {
|
|
587
865
|
async upload() {
|
|
@@ -599,14 +877,14 @@ var BlobTranscriptStore = class {
|
|
|
599
877
|
token;
|
|
600
878
|
prefix;
|
|
601
879
|
async upload(input) {
|
|
602
|
-
const body = await
|
|
880
|
+
const body = await readFile5(input.transcriptPath);
|
|
603
881
|
const requestedPath = buildPathname(
|
|
604
882
|
this.prefix,
|
|
605
883
|
input.userHash,
|
|
606
884
|
input.sessionId
|
|
607
885
|
);
|
|
608
886
|
const blob = await put(requestedPath, body, {
|
|
609
|
-
access: "
|
|
887
|
+
access: "private",
|
|
610
888
|
token: this.token,
|
|
611
889
|
contentType: "application/jsonl",
|
|
612
890
|
addRandomSuffix: true
|
|
@@ -620,6 +898,11 @@ var BlobTranscriptStore = class {
|
|
|
620
898
|
};
|
|
621
899
|
function createTranscriptStore(cfg) {
|
|
622
900
|
if (cfg.type === "none") return new NoopTranscriptStore();
|
|
901
|
+
if (cfg.type === "analyzer") {
|
|
902
|
+
throw new Error(
|
|
903
|
+
"transcript store: type=analyzer uploads are handled by the hook command directly"
|
|
904
|
+
);
|
|
905
|
+
}
|
|
623
906
|
const token = process.env[cfg.tokenEnv];
|
|
624
907
|
if (!token) {
|
|
625
908
|
throw new Error(
|
|
@@ -638,6 +921,12 @@ function buildPathname(prefix, userHash2, sessionId) {
|
|
|
638
921
|
async function runDoctor(opts) {
|
|
639
922
|
const repoRoot = process.cwd();
|
|
640
923
|
const checks = [];
|
|
924
|
+
const token = await getValidToken();
|
|
925
|
+
checks.push({
|
|
926
|
+
name: "logged in",
|
|
927
|
+
ok: token != null,
|
|
928
|
+
...token ? {} : { detail: "run `agent-insights login`" }
|
|
929
|
+
});
|
|
641
930
|
const cfg = await loadConfig();
|
|
642
931
|
checks.push({
|
|
643
932
|
name: "config found",
|
|
@@ -645,11 +934,6 @@ async function runDoctor(opts) {
|
|
|
645
934
|
detail: cfg ? configFile() : `missing ${configFile()}`
|
|
646
935
|
});
|
|
647
936
|
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
937
|
if (cfg.userConsent.transcriptSync) {
|
|
654
938
|
try {
|
|
655
939
|
const store = createTranscriptStore(cfg.transcriptStore);
|
|
@@ -674,11 +958,11 @@ async function runDoctor(opts) {
|
|
|
674
958
|
for (const adapter of adapterList) {
|
|
675
959
|
const enabled = adapter.id === "claude-code" ? cfg.agents.claudeCode.enabled : cfg.agents.cursor.enabled;
|
|
676
960
|
if (!enabled) continue;
|
|
677
|
-
const installed = await adapter.isInstalled(repoRoot);
|
|
961
|
+
const installed = await adapter.isInstalled(repoRoot, cfg.hooksScope);
|
|
678
962
|
checks.push({
|
|
679
963
|
name: `${adapter.label} hooks installed`,
|
|
680
964
|
ok: installed,
|
|
681
|
-
|
|
965
|
+
...installed ? {} : { detail: "run `agent-insights init`" }
|
|
682
966
|
});
|
|
683
967
|
}
|
|
684
968
|
}
|
|
@@ -736,7 +1020,6 @@ async function runHook(eventName, opts) {
|
|
|
736
1020
|
if (cfg.userConsent.sessionAnalysis && cfg.sessionAnalysis.analyzerUrl) {
|
|
737
1021
|
await notifyAnalyzer({
|
|
738
1022
|
analyzerUrl: cfg.sessionAnalysis.analyzerUrl,
|
|
739
|
-
secretEnv: cfg.sessionAnalysis.secretEnv,
|
|
740
1023
|
payload: {
|
|
741
1024
|
sessionId: mapped.sessionId ?? "unknown",
|
|
742
1025
|
agent: adapter.id,
|
|
@@ -754,11 +1037,12 @@ async function runHook(eventName, opts) {
|
|
|
754
1037
|
}
|
|
755
1038
|
async function notifyAnalyzer(opts) {
|
|
756
1039
|
try {
|
|
1040
|
+
const { getValidToken: getValidToken2 } = await Promise.resolve().then(() => (init_store(), store_exports));
|
|
1041
|
+
const token = await getValidToken2();
|
|
757
1042
|
const headers = {
|
|
758
1043
|
"content-type": "application/json"
|
|
759
1044
|
};
|
|
760
|
-
|
|
761
|
-
if (secret) headers["x-agent-insights-secret"] = secret;
|
|
1045
|
+
if (token) headers["authorization"] = `Bearer ${token}`;
|
|
762
1046
|
const url = new URL("/api/sessions", opts.analyzerUrl).toString();
|
|
763
1047
|
const res = await fetch(url, {
|
|
764
1048
|
method: "POST",
|
|
@@ -847,9 +1131,9 @@ function parseJsonObject(raw) {
|
|
|
847
1131
|
}
|
|
848
1132
|
|
|
849
1133
|
// src/commands/init.ts
|
|
850
|
-
import { existsSync as existsSync2 } from "fs";
|
|
851
|
-
import { join as join5 } from "path";
|
|
852
1134
|
import kleur3 from "kleur";
|
|
1135
|
+
init_paths();
|
|
1136
|
+
init_store();
|
|
853
1137
|
var CONSENT_TEXT = `Agent Insights is opt-in (internal).
|
|
854
1138
|
|
|
855
1139
|
OTLP / lifecycle metrics: event names, timestamps, tool names, outcomes,
|
|
@@ -861,6 +1145,11 @@ username stored as SHA256 only.
|
|
|
861
1145
|
SessionEnd analysis (optional): an internal LLM reviews transcript for
|
|
862
1146
|
tooling gaps; may post to Slack with a GitHub issue link.`;
|
|
863
1147
|
async function runInit(opts) {
|
|
1148
|
+
const token = await getValidToken();
|
|
1149
|
+
if (!token) {
|
|
1150
|
+
log.fail("Not logged in. Run `agent-insights login` first.");
|
|
1151
|
+
process.exit(1);
|
|
1152
|
+
}
|
|
864
1153
|
log.info(kleur3.bold("Agent Insights setup\n"));
|
|
865
1154
|
log.hint(CONSENT_TEXT);
|
|
866
1155
|
log.info("");
|
|
@@ -885,11 +1174,10 @@ async function runInit(opts) {
|
|
|
885
1174
|
for (const adapter of adapterList) {
|
|
886
1175
|
const enabled = adapter.id === "claude-code" && cfg.agents.claudeCode.enabled || adapter.id === "cursor" && cfg.agents.cursor.enabled;
|
|
887
1176
|
if (!enabled) continue;
|
|
888
|
-
const { path } = await adapter.install(repoRoot);
|
|
1177
|
+
const { path } = await adapter.install(repoRoot, cfg.hooksScope);
|
|
889
1178
|
log.ok(`Installed ${adapter.label} hooks \u2192 ${rel(path)}`);
|
|
890
1179
|
}
|
|
891
1180
|
}
|
|
892
|
-
printVercelHint(repoRoot);
|
|
893
1181
|
log.info("");
|
|
894
1182
|
log.info(`Config root: ${configRoot()}`);
|
|
895
1183
|
log.hint("Run `agent-insights doctor` to verify the setup.");
|
|
@@ -903,29 +1191,65 @@ function parseAgents(value) {
|
|
|
903
1191
|
}
|
|
904
1192
|
return out;
|
|
905
1193
|
}
|
|
906
|
-
function
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1194
|
+
function rel(p) {
|
|
1195
|
+
const cwd = process.cwd();
|
|
1196
|
+
return p.startsWith(cwd) ? p.slice(cwd.length + 1) : p;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/commands/login.ts
|
|
1200
|
+
init_oauth();
|
|
1201
|
+
init_store();
|
|
1202
|
+
import kleur4 from "kleur";
|
|
1203
|
+
var ALLOWED_DOMAIN = "vercel.com";
|
|
1204
|
+
async function runLogin() {
|
|
1205
|
+
log.info(kleur4.bold("Signing in with Vercel...\n"));
|
|
1206
|
+
let pkceResult;
|
|
1207
|
+
try {
|
|
1208
|
+
log.hint(`Listening on ${REDIRECT_URI} \u2014 opening browser...`);
|
|
1209
|
+
pkceResult = await startPkceFlow();
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
log.fail(`Login failed: ${err.message}`);
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
}
|
|
1214
|
+
let tokens;
|
|
1215
|
+
try {
|
|
1216
|
+
tokens = await exchangeCodeForToken(pkceResult.code, pkceResult.codeVerifier);
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
log.fail(`Token exchange failed: ${err.message}`);
|
|
1219
|
+
process.exit(1);
|
|
913
1220
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1221
|
+
let userInfo2;
|
|
1222
|
+
try {
|
|
1223
|
+
userInfo2 = await fetchUserInfo(tokens.access_token);
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
log.fail(`Could not fetch user info: ${err.message}`);
|
|
1226
|
+
process.exit(1);
|
|
1227
|
+
}
|
|
1228
|
+
if (!userInfo2.email?.endsWith(`@${ALLOWED_DOMAIN}`) || !userInfo2.email_verified) {
|
|
1229
|
+
log.fail(
|
|
1230
|
+
`Only @${ALLOWED_DOMAIN} accounts are allowed. Got: ${userInfo2.email ?? "(no email)"}`
|
|
917
1231
|
);
|
|
1232
|
+
process.exit(1);
|
|
918
1233
|
}
|
|
1234
|
+
await saveAuth({
|
|
1235
|
+
accessToken: tokens.access_token,
|
|
1236
|
+
...tokens.refresh_token !== void 0 ? { refreshToken: tokens.refresh_token } : {},
|
|
1237
|
+
expiresAt: Date.now() + tokens.expires_in * 1e3
|
|
1238
|
+
});
|
|
1239
|
+
const name = userInfo2.preferred_username ?? userInfo2.email;
|
|
1240
|
+
log.ok(`Logged in as ${kleur4.bold(name)} (${userInfo2.email})`);
|
|
1241
|
+
log.hint("Run `agent-insights init` to install hooks globally.");
|
|
919
1242
|
}
|
|
920
|
-
function
|
|
921
|
-
|
|
922
|
-
|
|
1243
|
+
async function runLogout() {
|
|
1244
|
+
await clearAuth();
|
|
1245
|
+
log.ok("Logged out.");
|
|
923
1246
|
}
|
|
924
1247
|
|
|
925
1248
|
// src/commands/status.ts
|
|
926
|
-
import { existsSync
|
|
927
|
-
import { join as
|
|
928
|
-
import
|
|
1249
|
+
import { existsSync } from "fs";
|
|
1250
|
+
import { join as join4 } from "path";
|
|
1251
|
+
import kleur5 from "kleur";
|
|
1252
|
+
init_paths();
|
|
929
1253
|
async function runStatus(opts) {
|
|
930
1254
|
const cfg = await loadConfig();
|
|
931
1255
|
const repoRoot = process.cwd();
|
|
@@ -949,46 +1273,52 @@ async function runStatus(opts) {
|
|
|
949
1273
|
enabled: cfg.enabled,
|
|
950
1274
|
consent: cfg.userConsent,
|
|
951
1275
|
vercel: {
|
|
952
|
-
linked:
|
|
953
|
-
envFile:
|
|
1276
|
+
linked: existsSync(join4(repoRoot, ".vercel/project.json")),
|
|
1277
|
+
envFile: existsSync(join4(repoRoot, ".env.local"))
|
|
954
1278
|
},
|
|
955
1279
|
adapters: adapterStatus
|
|
956
1280
|
});
|
|
957
1281
|
return;
|
|
958
1282
|
}
|
|
959
|
-
log.info(
|
|
1283
|
+
log.info(kleur5.bold("Agent Insights status\n"));
|
|
960
1284
|
log.info(`config ${configFile()}`);
|
|
961
1285
|
log.info(`enabled ${cfg.enabled ? "yes" : "no"}`);
|
|
962
1286
|
log.info("");
|
|
963
|
-
log.info(
|
|
1287
|
+
log.info(kleur5.bold("Consent"));
|
|
964
1288
|
log.info(` event telemetry ${yn(cfg.userConsent.eventTelemetry)}`);
|
|
965
1289
|
log.info(` transcript sync ${yn(cfg.userConsent.transcriptSync)}`);
|
|
966
1290
|
log.info(` session analysis ${yn(cfg.userConsent.sessionAnalysis)}`);
|
|
967
1291
|
log.info("");
|
|
968
|
-
log.info(
|
|
1292
|
+
log.info(kleur5.bold("Adapters"));
|
|
969
1293
|
for (const a of adapterStatus) {
|
|
970
1294
|
log.info(
|
|
971
1295
|
` ${a.label.padEnd(12)} ${a.enabled ? "enabled" : "disabled"} hooks ${a.installed ? "installed" : "not installed"}`
|
|
972
1296
|
);
|
|
973
1297
|
}
|
|
974
1298
|
log.info("");
|
|
975
|
-
log.info(
|
|
1299
|
+
log.info(kleur5.bold("Vercel"));
|
|
976
1300
|
log.info(
|
|
977
|
-
` linked ${yn(
|
|
1301
|
+
` linked ${yn(existsSync(join4(repoRoot, ".vercel/project.json")))}`
|
|
978
1302
|
);
|
|
979
|
-
log.info(` .env.local ${yn(
|
|
1303
|
+
log.info(` .env.local ${yn(existsSync(join4(repoRoot, ".env.local")))}`);
|
|
980
1304
|
}
|
|
981
1305
|
function yn(v) {
|
|
982
|
-
return v ?
|
|
1306
|
+
return v ? kleur5.green("yes") : kleur5.dim("no");
|
|
983
1307
|
}
|
|
984
1308
|
|
|
985
1309
|
// src/cli.ts
|
|
986
|
-
var VERSION = "0.0.
|
|
1310
|
+
var VERSION = "0.0.2";
|
|
987
1311
|
var program = new Command();
|
|
988
1312
|
program.name("agent-insights").description(
|
|
989
1313
|
"Internal CLI for AI coding agent observability (Claude Code, Cursor)."
|
|
990
1314
|
).version(VERSION);
|
|
991
|
-
program.command("
|
|
1315
|
+
program.command("login").description("Authenticate with Vercel (Sign in with Vercel).").action(async () => {
|
|
1316
|
+
await runLogin();
|
|
1317
|
+
});
|
|
1318
|
+
program.command("logout").description("Clear stored credentials.").action(async () => {
|
|
1319
|
+
await runLogout();
|
|
1320
|
+
});
|
|
1321
|
+
program.command("init").description("Install agent hooks globally (run once per machine).").option(
|
|
992
1322
|
"--agents <list>",
|
|
993
1323
|
"Comma-separated agent ids (claude-code,cursor)",
|
|
994
1324
|
"claude-code,cursor"
|