agent-insights 0.0.9 → 0.0.12

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
  }
@@ -251,9 +254,9 @@ async function saveAuth(state) {
251
254
  }
252
255
  }
253
256
  async function clearAuth() {
254
- const { unlink } = await import("fs/promises");
257
+ const { unlink: unlink2 } = await import("fs/promises");
255
258
  try {
256
- await unlink(authFile());
259
+ await unlink2(authFile());
257
260
  } catch (err) {
258
261
  if (err.code !== "ENOENT") throw err;
259
262
  }
@@ -299,9 +302,17 @@ import { spawn } from "child_process";
299
302
  import { randomUUID } from "crypto";
300
303
 
301
304
  // src/config/config.ts
302
- init_paths();
303
305
  import { mkdir, readFile, writeFile } from "fs/promises";
304
306
  import { dirname } from "path";
307
+
308
+ // src/config/analyzer.ts
309
+ var DEFAULT_ANALYZER_URL = "https://agent-insights.labs.vercel.dev";
310
+ function getAnalyzerUrl() {
311
+ return process.env.AGENT_INSIGHTS_ANALYZER_URL || DEFAULT_ANALYZER_URL;
312
+ }
313
+
314
+ // src/config/config.ts
315
+ init_paths();
305
316
  function defaultConfig() {
306
317
  return {
307
318
  version: 1,
@@ -330,7 +341,7 @@ function defaultConfig() {
330
341
  },
331
342
  sessionAnalysis: {
332
343
  enabled: false,
333
- analyzerUrl: process.env.AGENT_INSIGHTS_ANALYZER_URL ?? "",
344
+ analyzerUrl: getAnalyzerUrl(),
334
345
  githubIssueRepo: "vercel-labs/agent-insights"
335
346
  },
336
347
  agents: {
@@ -878,8 +889,49 @@ init_store();
878
889
 
879
890
  // src/transcript/store.ts
880
891
  init_store();
881
- import { readFile as readFile5 } from "fs/promises";
892
+ import { readFile as readFile6 } from "fs/promises";
882
893
  import { put } from "@vercel/blob";
894
+
895
+ // src/config/bypass.ts
896
+ init_paths();
897
+ import { chmod as chmod2, mkdir as mkdir5, readFile as readFile5, unlink, writeFile as writeFile5 } from "fs/promises";
898
+ import { dirname as dirname5 } from "path";
899
+ async function loadBypassSecret() {
900
+ const fromEnv = process.env.AGENT_INSIGHTS_BYPASS_SECRET || process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
901
+ if (fromEnv) return fromEnv;
902
+ try {
903
+ const raw = (await readFile5(bypassSecretFile(), "utf8")).trim();
904
+ return raw || void 0;
905
+ } catch (err) {
906
+ if (err.code === "ENOENT") return void 0;
907
+ throw err;
908
+ }
909
+ }
910
+ async function saveBypassSecret(secret) {
911
+ const path = bypassSecretFile();
912
+ await mkdir5(dirname5(path), { recursive: true });
913
+ await writeFile5(path, secret.trim(), { encoding: "utf8", mode: 384 });
914
+ try {
915
+ await chmod2(path, 384);
916
+ } catch {
917
+ }
918
+ }
919
+ async function clearBypassSecret() {
920
+ try {
921
+ await unlink(bypassSecretFile());
922
+ } catch (err) {
923
+ if (err.code !== "ENOENT") throw err;
924
+ }
925
+ }
926
+ async function withBypassHeader(headers) {
927
+ const secret = await loadBypassSecret();
928
+ if (secret) {
929
+ headers["x-vercel-protection-bypass"] = secret;
930
+ }
931
+ return headers;
932
+ }
933
+
934
+ // src/transcript/store.ts
883
935
  var NoopTranscriptStore = class {
884
936
  async upload() {
885
937
  throw new Error("transcript store disabled (transcriptStore.type=none)");
@@ -896,7 +948,7 @@ var BlobTranscriptStore = class {
896
948
  token;
897
949
  prefix;
898
950
  async upload(input) {
899
- const body = await readFile5(input.transcriptPath);
951
+ const body = await readFile6(input.transcriptPath);
900
952
  const requestedPath = buildPathname(
901
953
  this.prefix,
902
954
  input.userHash,
@@ -927,19 +979,16 @@ var AnalyzerTranscriptStore = class {
927
979
  "transcript store: not logged in \u2014 run `agent-insights login`"
928
980
  );
929
981
  }
930
- const body = await readFile5(input.transcriptPath);
982
+ const body = await readFile6(input.transcriptPath);
931
983
  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
984
+ const headers = await withBypassHeader({
985
+ authorization: `Bearer ${token}`,
986
+ "content-type": "application/jsonl",
987
+ "x-session-id": input.sessionId,
988
+ "x-agent": input.agent,
989
+ "x-user-hash": input.userHash
942
990
  });
991
+ const res = await fetch(url, { method: "POST", headers, body });
943
992
  if (!res.ok) {
944
993
  const text = await res.text().catch(() => "");
945
994
  throw new Error(
@@ -957,27 +1006,43 @@ var AnalyzerTranscriptStore = class {
957
1006
  };
958
1007
  }
959
1008
  async ping() {
960
- if (!this.analyzerUrl) {
1009
+ const token = await getValidToken();
1010
+ if (!token) {
961
1011
  return {
962
1012
  ok: false,
963
- reason: "analyzer URL not set (AGENT_INSIGHTS_ANALYZER_URL)"
1013
+ reason: "not logged in \u2014 run `agent-insights login`"
964
1014
  };
965
1015
  }
966
- const token = await getValidToken();
967
- if (!token) {
1016
+ try {
1017
+ const url = new URL("/api/sessions/upload", this.analyzerUrl).toString();
1018
+ const headers = await withBypassHeader({
1019
+ authorization: `Bearer ${token}`
1020
+ });
1021
+ const res = await fetch(url, { method: "POST", headers });
1022
+ if (res.status === 400) return { ok: true };
1023
+ if (res.status === 401 || res.status === 403) {
1024
+ const ct = res.headers.get("content-type") ?? "";
1025
+ if (ct.includes("text/html")) {
1026
+ return {
1027
+ ok: false,
1028
+ reason: "blocked by Vercel deployment protection \u2014 set a bypass secret via `agent-insights init --bypass-secret <secret>`"
1029
+ };
1030
+ }
1031
+ return { ok: false, reason: `analyzer rejected token (${res.status})` };
1032
+ }
1033
+ return { ok: false, reason: `unexpected status ${res.status}` };
1034
+ } catch (err) {
968
1035
  return {
969
1036
  ok: false,
970
- reason: "not logged in \u2014 run `agent-insights login`"
1037
+ reason: `analyzer unreachable: ${err.message}`
971
1038
  };
972
1039
  }
973
- return { ok: true };
974
1040
  }
975
1041
  };
976
1042
  function createTranscriptStore(cfg) {
977
1043
  if (cfg.type === "none") return new NoopTranscriptStore();
978
1044
  if (cfg.type === "analyzer") {
979
- const analyzerUrl = process.env.AGENT_INSIGHTS_ANALYZER_URL ?? "";
980
- return new AnalyzerTranscriptStore(analyzerUrl);
1045
+ return new AnalyzerTranscriptStore(getAnalyzerUrl());
981
1046
  }
982
1047
  const token = process.env[cfg.tokenEnv];
983
1048
  if (!token) {
@@ -1093,9 +1158,9 @@ async function runHook(eventName, opts) {
1093
1158
  size: result.size,
1094
1159
  pathname: result.pathname
1095
1160
  });
1096
- if (cfg.userConsent.sessionAnalysis && cfg.sessionAnalysis.analyzerUrl) {
1161
+ if (cfg.userConsent.sessionAnalysis) {
1097
1162
  await notifyAnalyzer({
1098
- analyzerUrl: cfg.sessionAnalysis.analyzerUrl,
1163
+ analyzerUrl: getAnalyzerUrl(),
1099
1164
  payload: {
1100
1165
  sessionId: mapped.sessionId ?? "unknown",
1101
1166
  agent: adapter.id,
@@ -1115,10 +1180,11 @@ async function notifyAnalyzer(opts) {
1115
1180
  try {
1116
1181
  const { getValidToken: getValidToken2 } = await Promise.resolve().then(() => (init_store(), store_exports));
1117
1182
  const token = await getValidToken2();
1118
- const headers = {
1183
+ const baseHeaders = {
1119
1184
  "content-type": "application/json"
1120
1185
  };
1121
- if (token) headers["authorization"] = `Bearer ${token}`;
1186
+ if (token) baseHeaders["authorization"] = `Bearer ${token}`;
1187
+ const headers = await withBypassHeader(baseHeaders);
1122
1188
  const url = new URL("/api/sessions", opts.analyzerUrl).toString();
1123
1189
  const res = await fetch(url, {
1124
1190
  method: "POST",
@@ -1207,7 +1273,7 @@ function parseJsonObject(raw) {
1207
1273
  }
1208
1274
 
1209
1275
  // src/commands/init.ts
1210
- import { createInterface } from "readline/promises";
1276
+ import { checkbox, confirm, password } from "@inquirer/prompts";
1211
1277
  import kleur3 from "kleur";
1212
1278
  init_paths();
1213
1279
  init_store();
@@ -1232,33 +1298,43 @@ async function runInit(opts) {
1232
1298
  log.info("");
1233
1299
  const existing = await loadConfig();
1234
1300
  const interactive = !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
1235
- let choices;
1236
- if (interactive) {
1237
- choices = await promptChoices(opts, existing !== null);
1238
- } else {
1239
- if (existing && !opts.force) {
1301
+ if (existing && !opts.force) {
1302
+ if (!interactive) {
1240
1303
  log.warn(
1241
1304
  `Config already exists at ${configFile()} \u2014 re-run with --force to overwrite, or use interactive mode.`
1242
1305
  );
1243
1306
  return;
1244
1307
  }
1308
+ log.warn(`Config already exists at ${configFile()}.`);
1309
+ const overwrite = await safePrompt(
1310
+ () => confirm({ message: "Overwrite existing config?", default: true })
1311
+ );
1312
+ if (overwrite === void 0) return;
1313
+ if (!overwrite) {
1314
+ log.info("Aborted \u2014 existing config left in place.");
1315
+ return;
1316
+ }
1317
+ log.info("");
1318
+ }
1319
+ let choices;
1320
+ if (interactive) {
1321
+ const result = await promptChoices(opts);
1322
+ if (!result) return;
1323
+ choices = result;
1324
+ } else {
1245
1325
  choices = {
1246
1326
  agents: parseAgents(opts.agents),
1247
1327
  transcripts: opts.transcripts ?? true,
1248
- analysis: opts.analysis ?? false,
1249
- telemetry: opts.telemetry ?? false,
1250
- overwrite: true
1328
+ analysis: opts.analysis ?? true,
1329
+ telemetry: opts.telemetry ?? true,
1330
+ bypassSecret: opts.bypassSecret
1251
1331
  };
1252
1332
  }
1253
- if (existing && !choices.overwrite) {
1254
- log.info("Aborted \u2014 existing config left in place.");
1255
- return;
1256
- }
1257
1333
  if (choices.agents.size === 0) {
1258
1334
  log.fail("No agents selected \u2014 nothing to install.");
1259
1335
  process.exit(1);
1260
1336
  }
1261
- const cfg = existing ?? defaultConfig();
1337
+ const cfg = defaultConfig();
1262
1338
  cfg.userConsent.transcriptSync = choices.transcripts;
1263
1339
  cfg.userConsent.sessionAnalysis = choices.analysis;
1264
1340
  cfg.userConsent.eventTelemetry = choices.telemetry;
@@ -1266,6 +1342,15 @@ async function runInit(opts) {
1266
1342
  cfg.agents.cursor.enabled = choices.agents.has("cursor");
1267
1343
  await saveConfig(cfg);
1268
1344
  log.ok(`Wrote config to ${configFile()}`);
1345
+ if (choices.bypassSecret !== void 0) {
1346
+ if (choices.bypassSecret === "") {
1347
+ await clearBypassSecret();
1348
+ log.ok("Cleared stored deployment-protection bypass secret.");
1349
+ } else {
1350
+ await saveBypassSecret(choices.bypassSecret);
1351
+ log.ok("Saved deployment-protection bypass secret.");
1352
+ }
1353
+ }
1269
1354
  const installHooks = opts.hooks ?? true;
1270
1355
  const repoRoot = process.cwd();
1271
1356
  if (installHooks) {
@@ -1280,65 +1365,85 @@ async function runInit(opts) {
1280
1365
  log.info(`Config root: ${configRoot()}`);
1281
1366
  log.hint("Run `agent-insights doctor` to verify the setup.");
1282
1367
  }
1283
- async function promptChoices(opts, hasExisting) {
1284
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1285
- try {
1286
- let overwrite = true;
1287
- if (hasExisting && !opts.force) {
1288
- log.warn(`Config already exists at ${configFile()}.`);
1289
- overwrite = await confirm(rl, "Overwrite existing config?", false);
1290
- if (!overwrite) return emptyChoices();
1291
- }
1292
- log.info("");
1293
- log.info(kleur3.bold("Which agents would you like to install hooks for?"));
1294
- const agents = /* @__PURE__ */ new Set();
1295
- const requested = opts.agents ? parseAgents(opts.agents) : null;
1296
- for (const adapter of adapterList) {
1297
- const defaultYes = requested ? requested.has(adapter.id) : true;
1298
- const enabled = await confirm(rl, ` ${adapter.label}?`, defaultYes);
1299
- if (enabled) agents.add(adapter.id);
1300
- }
1301
- log.info("");
1302
- log.info(kleur3.bold("Consent options:"));
1303
- const transcripts = await confirm(
1304
- rl,
1305
- " Sync session transcripts to Vercel Blob?",
1306
- opts.transcripts ?? true
1307
- );
1308
- const analysis = await confirm(
1309
- rl,
1310
- " Enable SessionEnd analysis (internal LLM review)?",
1311
- opts.analysis ?? false
1312
- );
1313
- const telemetry = await confirm(
1314
- rl,
1315
- " Send OTLP event telemetry?",
1316
- opts.telemetry ?? false
1368
+ async function promptChoices(opts) {
1369
+ const requested = opts.agents ? parseAgents(opts.agents) : null;
1370
+ const selected = await safePrompt(
1371
+ () => checkbox({
1372
+ message: "Which agents would you like to install hooks for?",
1373
+ choices: adapterList.map((adapter) => ({
1374
+ name: adapter.label,
1375
+ value: adapter.id,
1376
+ checked: requested ? requested.has(adapter.id) : true
1377
+ })),
1378
+ required: true
1379
+ })
1380
+ );
1381
+ if (selected === void 0) return void 0;
1382
+ const transcripts = await safePrompt(
1383
+ () => confirm({
1384
+ message: "Sync session transcripts to Vercel Blob?",
1385
+ default: opts.transcripts ?? true
1386
+ })
1387
+ );
1388
+ if (transcripts === void 0) return void 0;
1389
+ const analysis = await safePrompt(
1390
+ () => confirm({
1391
+ message: "Enable SessionEnd analysis (internal LLM review)?",
1392
+ default: opts.analysis ?? true
1393
+ })
1394
+ );
1395
+ if (analysis === void 0) return void 0;
1396
+ const telemetry = await safePrompt(
1397
+ () => confirm({
1398
+ message: "Send OTLP event telemetry?",
1399
+ default: opts.telemetry ?? true
1400
+ })
1401
+ );
1402
+ if (telemetry === void 0) return void 0;
1403
+ let bypassSecret;
1404
+ if (opts.bypassSecret !== void 0) {
1405
+ bypassSecret = opts.bypassSecret;
1406
+ } else {
1407
+ const existing = await loadBypassSecret();
1408
+ const answer = await safePrompt(
1409
+ () => password({
1410
+ message: existing ? "Vercel deployment-protection bypass secret (Enter to keep existing, '-' to clear):" : "Vercel deployment-protection bypass secret (Enter to skip):",
1411
+ mask: true
1412
+ })
1317
1413
  );
1318
- log.info("");
1319
- return { agents, transcripts, analysis, telemetry, overwrite };
1320
- } finally {
1321
- rl.close();
1414
+ if (answer === void 0) return void 0;
1415
+ const trimmed = answer.trim();
1416
+ if (trimmed === "") bypassSecret = void 0;
1417
+ else if (trimmed === "-") bypassSecret = "";
1418
+ else bypassSecret = trimmed;
1322
1419
  }
1323
- }
1324
- function emptyChoices() {
1325
1420
  return {
1326
- agents: /* @__PURE__ */ new Set(),
1327
- transcripts: false,
1328
- analysis: false,
1329
- telemetry: false,
1330
- overwrite: false
1421
+ agents: new Set(selected),
1422
+ transcripts,
1423
+ analysis,
1424
+ telemetry,
1425
+ bypassSecret
1331
1426
  };
1332
1427
  }
1333
- async function confirm(rl, question, defaultYes) {
1334
- const suffix = defaultYes ? "[Y/n]" : "[y/N]";
1335
- const answer = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
1336
- if (!answer) return defaultYes;
1337
- return answer.startsWith("y");
1428
+ async function safePrompt(run) {
1429
+ try {
1430
+ return await run();
1431
+ } catch (err) {
1432
+ if (err instanceof Error && (err.name === "ExitPromptError" || err.name === "AbortPromptError")) {
1433
+ log.info("Aborted.");
1434
+ return void 0;
1435
+ }
1436
+ throw err;
1437
+ }
1338
1438
  }
1339
1439
  function parseAgents(value) {
1340
1440
  const out = /* @__PURE__ */ new Set();
1341
- const list = (value ?? "claude-code,cursor").split(",").map((s) => s.trim()).filter(Boolean);
1441
+ const trimmed = (value ?? "").trim();
1442
+ if (trimmed === "*") {
1443
+ for (const a of adapterList) out.add(a.id);
1444
+ return out;
1445
+ }
1446
+ const list = (trimmed || "claude-code,cursor").split(",").map((s) => s.trim()).filter(Boolean);
1342
1447
  for (const id of list) {
1343
1448
  if (id === "claude-code" || id === "cursor") out.add(id);
1344
1449
  else log.warn(`Unknown agent id "${id}" \u2014 skipping.`);
@@ -1464,7 +1569,7 @@ function yn(v) {
1464
1569
  var program = new Command();
1465
1570
  program.name("agent-insights").description(
1466
1571
  "Internal CLI for AI coding agent observability (Claude Code, Cursor)."
1467
- ).version("0.0.9");
1572
+ ).version("0.0.12");
1468
1573
  program.command("login").description("Authenticate with Vercel (Sign in with Vercel).").action(async () => {
1469
1574
  await runLogin();
1470
1575
  });
@@ -1474,9 +1579,12 @@ program.command("logout").description("Clear stored credentials.").action(async
1474
1579
  program.command("init").description(
1475
1580
  "Install agent hooks globally. Interactive by default; use --yes for non-interactive."
1476
1581
  ).option(
1477
- "--agents <list>",
1478
- "Comma-separated agent ids (claude-code,cursor)"
1479
- ).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) => {
1582
+ "-a, --agents <list>",
1583
+ "Comma-separated agent ids (claude-code,cursor); use '*' for all"
1584
+ ).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(
1585
+ "--bypass-secret <secret>",
1586
+ "Vercel deployment-protection bypass secret (use '' to clear)"
1587
+ ).action(async (opts) => {
1480
1588
  await runInit(opts);
1481
1589
  });
1482
1590
  program.command("status").description("Show current config, consent, and adapter state.").option("--json", "Emit machine-readable JSON").action(async (opts) => {