agent-insights 0.0.10 → 0.0.13

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
@@ -21,6 +21,9 @@ function configFile() {
21
21
  function authFile() {
22
22
  return join(configRoot(), "auth.json");
23
23
  }
24
+ function bypassSecretFile() {
25
+ return join(configRoot(), "bypass-secret");
26
+ }
24
27
  function sessionCacheDir() {
25
28
  return join(configRoot(), "session-cache");
26
29
  }
@@ -219,11 +222,15 @@ __export(store_exports, {
219
222
  clearAuth: () => clearAuth,
220
223
  getValidToken: () => getValidToken,
221
224
  loadAuth: () => loadAuth,
225
+ loadEmail: () => loadEmail,
222
226
  saveAuth: () => saveAuth
223
227
  });
224
228
  import { chmod, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
225
229
  import { dirname as dirname4 } from "path";
226
230
  import { mkdir as mkdir4 } from "fs/promises";
231
+ async function loadEmail() {
232
+ return (await loadAuth())?.email;
233
+ }
227
234
  async function loadAuth() {
228
235
  try {
229
236
  const raw = await readFile4(authFile(), "utf8");
@@ -251,9 +258,9 @@ async function saveAuth(state) {
251
258
  }
252
259
  }
253
260
  async function clearAuth() {
254
- const { unlink } = await import("fs/promises");
261
+ const { unlink: unlink2 } = await import("fs/promises");
255
262
  try {
256
- await unlink(authFile());
263
+ await unlink2(authFile());
257
264
  } catch (err) {
258
265
  if (err.code !== "ENOENT") throw err;
259
266
  }
@@ -272,7 +279,8 @@ async function getValidToken() {
272
279
  const refreshed = {
273
280
  accessToken: tokens.access_token,
274
281
  refreshToken: tokens.refresh_token ?? auth.refreshToken,
275
- expiresAt: Date.now() + tokens.expires_in * 1e3
282
+ expiresAt: Date.now() + tokens.expires_in * 1e3,
283
+ ...auth.email ? { email: auth.email } : {}
276
284
  };
277
285
  await saveAuth(refreshed);
278
286
  return refreshed.accessToken;
@@ -299,9 +307,17 @@ import { spawn } from "child_process";
299
307
  import { randomUUID } from "crypto";
300
308
 
301
309
  // src/config/config.ts
302
- init_paths();
303
310
  import { mkdir, readFile, writeFile } from "fs/promises";
304
311
  import { dirname } from "path";
312
+
313
+ // src/config/analyzer.ts
314
+ var DEFAULT_ANALYZER_URL = "https://agent-insights.labs.vercel.dev";
315
+ function getAnalyzerUrl() {
316
+ return process.env.AGENT_INSIGHTS_ANALYZER_URL || DEFAULT_ANALYZER_URL;
317
+ }
318
+
319
+ // src/config/config.ts
320
+ init_paths();
305
321
  function defaultConfig() {
306
322
  return {
307
323
  version: 1,
@@ -330,7 +346,7 @@ function defaultConfig() {
330
346
  },
331
347
  sessionAnalysis: {
332
348
  enabled: false,
333
- analyzerUrl: process.env.AGENT_INSIGHTS_ANALYZER_URL ?? "",
349
+ analyzerUrl: getAnalyzerUrl(),
334
350
  githubIssueRepo: "vercel-labs/agent-insights"
335
351
  },
336
352
  agents: {
@@ -647,16 +663,15 @@ var claudeCodeAdapter = {
647
663
  const path = resolveClaudePath(repoRoot, scope);
648
664
  const settings = await readJson(path);
649
665
  const next = { ...settings ?? {} };
650
- const hooks = { ...next.hooks ?? {} };
666
+ const hooks = {};
667
+ for (const [event, raw] of Object.entries(next.hooks ?? {})) {
668
+ hooks[event] = normalizeEventHooks(raw);
669
+ }
651
670
  for (const event of CLAUDE_HOOK_EVENTS) {
652
- const existing = (hooks[event] ?? []).filter(
653
- (h) => !isAgentInsightsCommand(h.command)
671
+ const withoutInsights = removeAgentInsightsMatchers(
672
+ hooks[event] ?? []
654
673
  );
655
- existing.push({
656
- type: "command",
657
- command: `${HOOK_COMMAND_NAME} hook ${event}`
658
- });
659
- hooks[event] = existing;
674
+ hooks[event] = addAgentInsightsHook(withoutInsights, event);
660
675
  }
661
676
  next.hooks = hooks;
662
677
  await mkdir2(dirname2(path), { recursive: true });
@@ -668,18 +683,16 @@ var claudeCodeAdapter = {
668
683
  const path = resolveClaudePath(repoRoot, scope);
669
684
  const settings = await readJson(path);
670
685
  if (!settings?.hooks) return { removed: false, path };
671
- const hooks = { ...settings.hooks };
686
+ const hooks = {};
672
687
  let touched = false;
673
- for (const [event, entries] of Object.entries(hooks)) {
674
- const filtered = entries.filter(
675
- (h) => !isAgentInsightsCommand(h.command)
676
- );
677
- if (filtered.length !== entries.length) touched = true;
678
- if (filtered.length === 0) {
679
- delete hooks[event];
680
- } else {
681
- hooks[event] = filtered;
688
+ for (const [event, raw] of Object.entries(settings.hooks)) {
689
+ const before = normalizeEventHooks(raw);
690
+ const after = removeAgentInsightsMatchers(before);
691
+ if (after.length !== before.length) touched = true;
692
+ else if (JSON.stringify(after) !== JSON.stringify(before)) {
693
+ touched = true;
682
694
  }
695
+ if (after.length > 0) hooks[event] = after;
683
696
  }
684
697
  settings.hooks = hooks;
685
698
  await writeFile2(path, `${JSON.stringify(settings, null, 2)}
@@ -692,7 +705,9 @@ var claudeCodeAdapter = {
692
705
  );
693
706
  if (!settings?.hooks) return false;
694
707
  return Object.values(settings.hooks).some(
695
- (entries) => entries.some((h) => isAgentInsightsCommand(h.command))
708
+ (raw) => normalizeEventHooks(raw).some(
709
+ (group) => group.hooks.some((h) => isAgentInsightsCommand(h.command))
710
+ )
696
711
  );
697
712
  }
698
713
  };
@@ -703,6 +718,59 @@ function isAgentInsightsCommand(cmd) {
703
718
  if (!cmd) return false;
704
719
  return cmd.startsWith(`${HOOK_COMMAND_NAME} hook`);
705
720
  }
721
+ function isHookCommand(value) {
722
+ if (!value || typeof value !== "object") return false;
723
+ const entry = value;
724
+ return entry.type === "command" && typeof entry.command === "string";
725
+ }
726
+ function normalizeEventHooks(raw) {
727
+ const groups = [];
728
+ for (const entry of raw) {
729
+ if (!entry || typeof entry !== "object") continue;
730
+ const record = entry;
731
+ if (Array.isArray(record.hooks)) {
732
+ const commands = record.hooks.filter(isHookCommand);
733
+ if (commands.length === 0) continue;
734
+ groups.push({
735
+ matcher: typeof record.matcher === "string" ? record.matcher : "",
736
+ hooks: commands
737
+ });
738
+ continue;
739
+ }
740
+ if (isHookCommand(record)) {
741
+ groups.push({
742
+ matcher: "",
743
+ hooks: [record]
744
+ });
745
+ }
746
+ }
747
+ return groups;
748
+ }
749
+ function removeAgentInsightsMatchers(matchers) {
750
+ const result = [];
751
+ for (const group of matchers) {
752
+ const hooks = group.hooks.filter(
753
+ (h) => !isAgentInsightsCommand(h.command)
754
+ );
755
+ if (hooks.length > 0) {
756
+ result.push({ ...group, hooks });
757
+ }
758
+ }
759
+ return result;
760
+ }
761
+ function addAgentInsightsHook(matchers, event) {
762
+ const command = `${HOOK_COMMAND_NAME} hook ${event}`;
763
+ const entry = { type: "command", command };
764
+ for (const group of matchers) {
765
+ if (group.matcher === "") {
766
+ if (!group.hooks.some((h) => h.command === command)) {
767
+ group.hooks.push(entry);
768
+ }
769
+ return matchers;
770
+ }
771
+ }
772
+ return [...matchers, { matcher: "", hooks: [entry] }];
773
+ }
706
774
  async function readJson(path) {
707
775
  try {
708
776
  const raw = await readFile2(path, "utf8");
@@ -878,8 +946,49 @@ init_store();
878
946
 
879
947
  // src/transcript/store.ts
880
948
  init_store();
881
- import { readFile as readFile5 } from "fs/promises";
949
+ import { readFile as readFile6 } from "fs/promises";
882
950
  import { put } from "@vercel/blob";
951
+
952
+ // src/config/bypass.ts
953
+ init_paths();
954
+ import { chmod as chmod2, mkdir as mkdir5, readFile as readFile5, unlink, writeFile as writeFile5 } from "fs/promises";
955
+ import { dirname as dirname5 } from "path";
956
+ async function loadBypassSecret() {
957
+ const fromEnv = process.env.AGENT_INSIGHTS_BYPASS_SECRET || process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
958
+ if (fromEnv) return fromEnv;
959
+ try {
960
+ const raw = (await readFile5(bypassSecretFile(), "utf8")).trim();
961
+ return raw || void 0;
962
+ } catch (err) {
963
+ if (err.code === "ENOENT") return void 0;
964
+ throw err;
965
+ }
966
+ }
967
+ async function saveBypassSecret(secret) {
968
+ const path = bypassSecretFile();
969
+ await mkdir5(dirname5(path), { recursive: true });
970
+ await writeFile5(path, secret.trim(), { encoding: "utf8", mode: 384 });
971
+ try {
972
+ await chmod2(path, 384);
973
+ } catch {
974
+ }
975
+ }
976
+ async function clearBypassSecret() {
977
+ try {
978
+ await unlink(bypassSecretFile());
979
+ } catch (err) {
980
+ if (err.code !== "ENOENT") throw err;
981
+ }
982
+ }
983
+ async function withBypassHeader(headers) {
984
+ const secret = await loadBypassSecret();
985
+ if (secret) {
986
+ headers["x-vercel-protection-bypass"] = secret;
987
+ }
988
+ return headers;
989
+ }
990
+
991
+ // src/transcript/store.ts
883
992
  var NoopTranscriptStore = class {
884
993
  async upload() {
885
994
  throw new Error("transcript store disabled (transcriptStore.type=none)");
@@ -896,7 +1005,7 @@ var BlobTranscriptStore = class {
896
1005
  token;
897
1006
  prefix;
898
1007
  async upload(input) {
899
- const body = await readFile5(input.transcriptPath);
1008
+ const body = await readFile6(input.transcriptPath);
900
1009
  const requestedPath = buildPathname(
901
1010
  this.prefix,
902
1011
  input.userHash,
@@ -927,19 +1036,17 @@ var AnalyzerTranscriptStore = class {
927
1036
  "transcript store: not logged in \u2014 run `agent-insights login`"
928
1037
  );
929
1038
  }
930
- const body = await readFile5(input.transcriptPath);
1039
+ const body = await readFile6(input.transcriptPath);
931
1040
  const url = new URL("/api/sessions/upload", this.analyzerUrl).toString();
932
- const res = await fetch(url, {
933
- method: "POST",
934
- headers: {
935
- authorization: `Bearer ${token}`,
936
- "content-type": "application/jsonl",
937
- "x-session-id": input.sessionId,
938
- "x-agent": input.agent,
939
- "x-user-hash": input.userHash
940
- },
941
- body
1041
+ const headers = await withBypassHeader({
1042
+ authorization: `Bearer ${token}`,
1043
+ "content-type": "application/jsonl",
1044
+ "x-session-id": input.sessionId,
1045
+ "x-agent": input.agent,
1046
+ "x-user-hash": input.userHash,
1047
+ ...input.userEmail ? { "x-user-email": input.userEmail } : {}
942
1048
  });
1049
+ const res = await fetch(url, { method: "POST", headers, body });
943
1050
  if (!res.ok) {
944
1051
  const text = await res.text().catch(() => "");
945
1052
  throw new Error(
@@ -957,27 +1064,43 @@ var AnalyzerTranscriptStore = class {
957
1064
  };
958
1065
  }
959
1066
  async ping() {
960
- if (!this.analyzerUrl) {
1067
+ const token = await getValidToken();
1068
+ if (!token) {
961
1069
  return {
962
1070
  ok: false,
963
- reason: "analyzer URL not set (AGENT_INSIGHTS_ANALYZER_URL)"
1071
+ reason: "not logged in \u2014 run `agent-insights login`"
964
1072
  };
965
1073
  }
966
- const token = await getValidToken();
967
- if (!token) {
1074
+ try {
1075
+ const url = new URL("/api/sessions/upload", this.analyzerUrl).toString();
1076
+ const headers = await withBypassHeader({
1077
+ authorization: `Bearer ${token}`
1078
+ });
1079
+ const res = await fetch(url, { method: "POST", headers });
1080
+ if (res.status === 400) return { ok: true };
1081
+ if (res.status === 401 || res.status === 403) {
1082
+ const ct = res.headers.get("content-type") ?? "";
1083
+ if (ct.includes("text/html")) {
1084
+ return {
1085
+ ok: false,
1086
+ reason: "blocked by Vercel deployment protection \u2014 set a bypass secret via `agent-insights init --bypass-secret <secret>`"
1087
+ };
1088
+ }
1089
+ return { ok: false, reason: `analyzer rejected token (${res.status})` };
1090
+ }
1091
+ return { ok: false, reason: `unexpected status ${res.status}` };
1092
+ } catch (err) {
968
1093
  return {
969
1094
  ok: false,
970
- reason: "not logged in \u2014 run `agent-insights login`"
1095
+ reason: `analyzer unreachable: ${err.message}`
971
1096
  };
972
1097
  }
973
- return { ok: true };
974
1098
  }
975
1099
  };
976
1100
  function createTranscriptStore(cfg) {
977
1101
  if (cfg.type === "none") return new NoopTranscriptStore();
978
1102
  if (cfg.type === "analyzer") {
979
- const analyzerUrl = process.env.AGENT_INSIGHTS_ANALYZER_URL ?? "";
980
- return new AnalyzerTranscriptStore(analyzerUrl);
1103
+ return new AnalyzerTranscriptStore(getAnalyzerUrl());
981
1104
  }
982
1105
  const token = process.env[cfg.tokenEnv];
983
1106
  if (!token) {
@@ -1055,6 +1178,7 @@ async function runDoctor(opts) {
1055
1178
  }
1056
1179
 
1057
1180
  // src/commands/hook.ts
1181
+ init_store();
1058
1182
  async function runHook(eventName, opts) {
1059
1183
  const cfg = await loadConfig();
1060
1184
  if (!cfg || !cfg.enabled) {
@@ -1083,19 +1207,21 @@ async function runHook(eventName, opts) {
1083
1207
  if (mapped.type === "session.end" && cfg.userConsent.transcriptSync && mapped.transcriptPath) {
1084
1208
  try {
1085
1209
  const store = createTranscriptStore(cfg.transcriptStore);
1210
+ const userEmail = await loadEmail();
1086
1211
  const result = await store.upload({
1087
1212
  transcriptPath: mapped.transcriptPath,
1088
1213
  userHash: event.userHash ?? userHash(),
1089
1214
  sessionId: mapped.sessionId ?? "unknown",
1090
- agent: adapter.id
1215
+ agent: adapter.id,
1216
+ ...userEmail ? { userEmail } : {}
1091
1217
  });
1092
1218
  debug("transcript uploaded", {
1093
1219
  size: result.size,
1094
1220
  pathname: result.pathname
1095
1221
  });
1096
- if (cfg.userConsent.sessionAnalysis && cfg.sessionAnalysis.analyzerUrl) {
1222
+ if (cfg.userConsent.sessionAnalysis) {
1097
1223
  await notifyAnalyzer({
1098
- analyzerUrl: cfg.sessionAnalysis.analyzerUrl,
1224
+ analyzerUrl: getAnalyzerUrl(),
1099
1225
  payload: {
1100
1226
  sessionId: mapped.sessionId ?? "unknown",
1101
1227
  agent: adapter.id,
@@ -1115,10 +1241,11 @@ async function notifyAnalyzer(opts) {
1115
1241
  try {
1116
1242
  const { getValidToken: getValidToken2 } = await Promise.resolve().then(() => (init_store(), store_exports));
1117
1243
  const token = await getValidToken2();
1118
- const headers = {
1244
+ const baseHeaders = {
1119
1245
  "content-type": "application/json"
1120
1246
  };
1121
- if (token) headers["authorization"] = `Bearer ${token}`;
1247
+ if (token) baseHeaders["authorization"] = `Bearer ${token}`;
1248
+ const headers = await withBypassHeader(baseHeaders);
1122
1249
  const url = new URL("/api/sessions", opts.analyzerUrl).toString();
1123
1250
  const res = await fetch(url, {
1124
1251
  method: "POST",
@@ -1207,7 +1334,7 @@ function parseJsonObject(raw) {
1207
1334
  }
1208
1335
 
1209
1336
  // src/commands/init.ts
1210
- import { checkbox, confirm } from "@inquirer/prompts";
1337
+ import { checkbox, confirm, password } from "@inquirer/prompts";
1211
1338
  import kleur3 from "kleur";
1212
1339
  init_paths();
1213
1340
  init_store();
@@ -1241,7 +1368,7 @@ async function runInit(opts) {
1241
1368
  }
1242
1369
  log.warn(`Config already exists at ${configFile()}.`);
1243
1370
  const overwrite = await safePrompt(
1244
- () => confirm({ message: "Overwrite existing config?", default: false })
1371
+ () => confirm({ message: "Overwrite existing config?", default: true })
1245
1372
  );
1246
1373
  if (overwrite === void 0) return;
1247
1374
  if (!overwrite) {
@@ -1259,8 +1386,9 @@ async function runInit(opts) {
1259
1386
  choices = {
1260
1387
  agents: parseAgents(opts.agents),
1261
1388
  transcripts: opts.transcripts ?? true,
1262
- analysis: opts.analysis ?? false,
1263
- telemetry: opts.telemetry ?? false
1389
+ analysis: opts.analysis ?? true,
1390
+ telemetry: opts.telemetry ?? true,
1391
+ bypassSecret: opts.bypassSecret
1264
1392
  };
1265
1393
  }
1266
1394
  if (choices.agents.size === 0) {
@@ -1275,6 +1403,15 @@ async function runInit(opts) {
1275
1403
  cfg.agents.cursor.enabled = choices.agents.has("cursor");
1276
1404
  await saveConfig(cfg);
1277
1405
  log.ok(`Wrote config to ${configFile()}`);
1406
+ if (choices.bypassSecret !== void 0) {
1407
+ if (choices.bypassSecret === "") {
1408
+ await clearBypassSecret();
1409
+ log.ok("Cleared stored deployment-protection bypass secret.");
1410
+ } else {
1411
+ await saveBypassSecret(choices.bypassSecret);
1412
+ log.ok("Saved deployment-protection bypass secret.");
1413
+ }
1414
+ }
1278
1415
  const installHooks = opts.hooks ?? true;
1279
1416
  const repoRoot = process.cwd();
1280
1417
  if (installHooks) {
@@ -1313,22 +1450,40 @@ async function promptChoices(opts) {
1313
1450
  const analysis = await safePrompt(
1314
1451
  () => confirm({
1315
1452
  message: "Enable SessionEnd analysis (internal LLM review)?",
1316
- default: opts.analysis ?? false
1453
+ default: opts.analysis ?? true
1317
1454
  })
1318
1455
  );
1319
1456
  if (analysis === void 0) return void 0;
1320
1457
  const telemetry = await safePrompt(
1321
1458
  () => confirm({
1322
1459
  message: "Send OTLP event telemetry?",
1323
- default: opts.telemetry ?? false
1460
+ default: opts.telemetry ?? true
1324
1461
  })
1325
1462
  );
1326
1463
  if (telemetry === void 0) return void 0;
1464
+ let bypassSecret;
1465
+ if (opts.bypassSecret !== void 0) {
1466
+ bypassSecret = opts.bypassSecret;
1467
+ } else {
1468
+ const existing = await loadBypassSecret();
1469
+ const answer = await safePrompt(
1470
+ () => password({
1471
+ message: existing ? "Vercel deployment-protection bypass secret (Enter to keep existing, '-' to clear):" : "Vercel deployment-protection bypass secret (Enter to skip):",
1472
+ mask: true
1473
+ })
1474
+ );
1475
+ if (answer === void 0) return void 0;
1476
+ const trimmed = answer.trim();
1477
+ if (trimmed === "") bypassSecret = void 0;
1478
+ else if (trimmed === "-") bypassSecret = "";
1479
+ else bypassSecret = trimmed;
1480
+ }
1327
1481
  return {
1328
1482
  agents: new Set(selected),
1329
1483
  transcripts,
1330
1484
  analysis,
1331
- telemetry
1485
+ telemetry,
1486
+ bypassSecret
1332
1487
  };
1333
1488
  }
1334
1489
  async function safePrompt(run) {
@@ -1399,7 +1554,8 @@ async function runLogin() {
1399
1554
  await saveAuth({
1400
1555
  accessToken: tokens.access_token,
1401
1556
  ...tokens.refresh_token !== void 0 ? { refreshToken: tokens.refresh_token } : {},
1402
- expiresAt: Date.now() + tokens.expires_in * 1e3
1557
+ expiresAt: Date.now() + tokens.expires_in * 1e3,
1558
+ email: userInfo2.email
1403
1559
  });
1404
1560
  const name = userInfo2.preferred_username ?? userInfo2.email;
1405
1561
  log.ok(`Logged in as ${kleur4.bold(name)} (${userInfo2.email})`);
@@ -1475,7 +1631,7 @@ function yn(v) {
1475
1631
  var program = new Command();
1476
1632
  program.name("agent-insights").description(
1477
1633
  "Internal CLI for AI coding agent observability (Claude Code, Cursor)."
1478
- ).version("0.0.10");
1634
+ ).version("0.0.13");
1479
1635
  program.command("login").description("Authenticate with Vercel (Sign in with Vercel).").action(async () => {
1480
1636
  await runLogin();
1481
1637
  });
@@ -1487,7 +1643,10 @@ program.command("init").description(
1487
1643
  ).option(
1488
1644
  "-a, --agents <list>",
1489
1645
  "Comma-separated agent ids (claude-code,cursor); use '*' for all"
1490
- ).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").option("-y, --yes", "Non-interactive \u2014 accept defaults / passed flags").action(async (opts) => {
1646
+ ).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").option("-y, --yes", "Non-interactive \u2014 accept defaults / passed flags").option(
1647
+ "--bypass-secret <secret>",
1648
+ "Vercel deployment-protection bypass secret (use '' to clear)"
1649
+ ).action(async (opts) => {
1491
1650
  await runInit(opts);
1492
1651
  });
1493
1652
  program.command("status").description("Show current config, consent, and adapter state.").option("--json", "Emit machine-readable JSON").action(async (opts) => {