agent-insights 0.0.1 → 0.0.7

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 CHANGED
@@ -1,15 +1,13 @@
1
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";
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 = join2(repoRoot, ".claude/settings.json");
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 = join2(repoRoot, ".claude/settings.json");
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
- join2(repoRoot, ".claude/settings.json")
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
- SessionStart: "session.start",
446
- SessionEnd: "session.end",
447
- UserPromptSubmit: "user.prompt.submit",
448
- PreToolUse: "tool.start",
449
- PostToolUse: "tool.end",
450
- Stop: "agent.stop"
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 = join3(repoRoot, ".cursor/hooks.json");
743
+ async install(repoRoot, scope = "global") {
744
+ const path = resolveCursorPath(repoRoot, scope);
474
745
  const settings = await readJson2(path);
475
- const next = { ...settings ?? {} };
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 = join3(repoRoot, ".cursor/hooks.json");
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
- join3(repoRoot, ".cursor/hooks.json")
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 readFile4 } from "fs/promises";
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 readFile4(input.transcriptPath);
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: "public",
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
- detail: installed ? join4(repoRoot, adapter.settingsFile) : "run `agent-insights init`"
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
- const secret = process.env[opts.secretEnv];
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 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
- );
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
- if (!envFile) {
915
- log.hint(
916
- "No .env.local found. Run `vercel env pull .env.local` to fetch BLOB_READ_WRITE_TOKEN."
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 rel(p) {
921
- const cwd = process.cwd();
922
- return p.startsWith(cwd) ? p.slice(cwd.length + 1) : p;
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 as existsSync3 } from "fs";
927
- import { join as join6 } from "path";
928
- import kleur4 from "kleur";
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,51 @@ async function runStatus(opts) {
949
1273
  enabled: cfg.enabled,
950
1274
  consent: cfg.userConsent,
951
1275
  vercel: {
952
- linked: existsSync3(join6(repoRoot, ".vercel/project.json")),
953
- envFile: existsSync3(join6(repoRoot, ".env.local"))
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(kleur4.bold("Agent Insights status\n"));
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(kleur4.bold("Consent"));
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(kleur4.bold("Adapters"));
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(kleur4.bold("Vercel"));
1299
+ log.info(kleur5.bold("Vercel"));
976
1300
  log.info(
977
- ` linked ${yn(existsSync3(join6(repoRoot, ".vercel/project.json")))}`
1301
+ ` linked ${yn(existsSync(join4(repoRoot, ".vercel/project.json")))}`
978
1302
  );
979
- log.info(` .env.local ${yn(existsSync3(join6(repoRoot, ".env.local")))}`);
1303
+ log.info(` .env.local ${yn(existsSync(join4(repoRoot, ".env.local")))}`);
980
1304
  }
981
1305
  function yn(v) {
982
- return v ? kleur4.green("yes") : kleur4.dim("no");
1306
+ return v ? kleur5.green("yes") : kleur5.dim("no");
983
1307
  }
984
1308
 
985
1309
  // src/cli.ts
986
- var VERSION = "0.0.1";
987
1310
  var program = new Command();
988
1311
  program.name("agent-insights").description(
989
1312
  "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(
1313
+ ).version("0.0.7");
1314
+ program.command("login").description("Authenticate with Vercel (Sign in with Vercel).").action(async () => {
1315
+ await runLogin();
1316
+ });
1317
+ program.command("logout").description("Clear stored credentials.").action(async () => {
1318
+ await runLogout();
1319
+ });
1320
+ program.command("init").description("Install agent hooks globally (run once per machine).").option(
992
1321
  "--agents <list>",
993
1322
  "Comma-separated agent ids (claude-code,cursor)",
994
1323
  "claude-code,cursor"