agent-insights 0.0.7 → 0.0.10

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
@@ -75,7 +75,7 @@ function listenForCallback(expectedState, timeoutMs) {
75
75
  const server = createServer((req, res) => {
76
76
  const url = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
77
77
  if (url.pathname !== "/callback") {
78
- res.writeHead(404);
78
+ res.writeHead(404, { connection: "close" });
79
79
  res.end("Not found");
80
80
  return;
81
81
  }
@@ -83,33 +83,48 @@ function listenForCallback(expectedState, timeoutMs) {
83
83
  const returnedState = url.searchParams.get("state");
84
84
  const error = url.searchParams.get("error");
85
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
+ const headers = { "content-type": "text/html", connection: "close" };
87
+ const finish = (next, body, status) => {
88
+ res.writeHead(status, headers);
89
+ res.end(body, () => {
90
+ clearTimeout(timer);
91
+ server.closeAllConnections?.();
92
+ server.close();
93
+ next();
94
+ });
95
+ };
86
96
  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}`));
97
+ finish(
98
+ () => reject(new Error(`OAuth error: ${error}`)),
99
+ html(`Login failed: ${error}`),
100
+ 400
101
+ );
91
102
  return;
92
103
  }
93
104
  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"));
105
+ finish(
106
+ () => reject(new Error("State mismatch \u2014 possible CSRF")),
107
+ html("Invalid state parameter \u2014 please try again."),
108
+ 400
109
+ );
98
110
  return;
99
111
  }
100
112
  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"));
113
+ finish(
114
+ () => reject(new Error("No code in callback")),
115
+ html("No authorization code received."),
116
+ 400
117
+ );
105
118
  return;
106
119
  }
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);
120
+ finish(
121
+ () => resolve(code),
122
+ html("\u2713 Logged in! You can close this tab."),
123
+ 200
124
+ );
111
125
  });
112
126
  const timer = setTimeout(() => {
127
+ server.closeAllConnections?.();
113
128
  server.close();
114
129
  reject(new Error("Login timed out \u2014 no response within 2 minutes."));
115
130
  }, timeoutMs);
@@ -179,8 +194,10 @@ function openBrowser(url) {
179
194
  if (os === "darwin") cmd = `open "${url}"`;
180
195
  else if (os === "win32") cmd = `start "" "${url}"`;
181
196
  else cmd = `xdg-open "${url}"`;
182
- exec(cmd, () => {
197
+ const child = exec(cmd);
198
+ child.on("error", () => {
183
199
  });
200
+ child.unref();
184
201
  }
185
202
  var CLIENT_ID, AUTH_ENDPOINT, TOKEN_ENDPOINT, USERINFO_ENDPOINT, CALLBACK_PORT, REDIRECT_URI, SCOPES;
186
203
  var init_oauth = __esm({
@@ -306,9 +323,10 @@ function defaultConfig() {
306
323
  dataset: "agent-insights"
307
324
  },
308
325
  transcriptStore: {
309
- type: "blob",
310
- tokenEnv: "BLOB_READ_WRITE_TOKEN",
311
- prefix: "sessions/"
326
+ // Upload via the analyzer using the SiwV bearer token — no client-side
327
+ // Blob token required. Set `transcriptStore.type` to "blob" in
328
+ // config.json to switch to direct uploads with BLOB_READ_WRITE_TOKEN.
329
+ type: "analyzer"
312
330
  },
313
331
  sessionAnalysis: {
314
332
  enabled: false,
@@ -859,6 +877,7 @@ init_paths();
859
877
  init_store();
860
878
 
861
879
  // src/transcript/store.ts
880
+ init_store();
862
881
  import { readFile as readFile5 } from "fs/promises";
863
882
  import { put } from "@vercel/blob";
864
883
  var NoopTranscriptStore = class {
@@ -896,17 +915,74 @@ var BlobTranscriptStore = class {
896
915
  return { ok: true };
897
916
  }
898
917
  };
918
+ var AnalyzerTranscriptStore = class {
919
+ constructor(analyzerUrl) {
920
+ this.analyzerUrl = analyzerUrl;
921
+ }
922
+ analyzerUrl;
923
+ async upload(input) {
924
+ const token = await getValidToken();
925
+ if (!token) {
926
+ throw new Error(
927
+ "transcript store: not logged in \u2014 run `agent-insights login`"
928
+ );
929
+ }
930
+ const body = await readFile5(input.transcriptPath);
931
+ 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
942
+ });
943
+ if (!res.ok) {
944
+ const text = await res.text().catch(() => "");
945
+ throw new Error(
946
+ `transcript upload failed (${res.status}): ${text || res.statusText}`
947
+ );
948
+ }
949
+ const json = await res.json();
950
+ if (!json.ok || !json.blobUrl || !json.pathname) {
951
+ throw new Error(`transcript upload error: ${json.error ?? "unknown"}`);
952
+ }
953
+ return {
954
+ pathname: json.pathname,
955
+ url: json.blobUrl,
956
+ size: json.size ?? body.byteLength
957
+ };
958
+ }
959
+ async ping() {
960
+ if (!this.analyzerUrl) {
961
+ return {
962
+ ok: false,
963
+ reason: "analyzer URL not set (AGENT_INSIGHTS_ANALYZER_URL)"
964
+ };
965
+ }
966
+ const token = await getValidToken();
967
+ if (!token) {
968
+ return {
969
+ ok: false,
970
+ reason: "not logged in \u2014 run `agent-insights login`"
971
+ };
972
+ }
973
+ return { ok: true };
974
+ }
975
+ };
899
976
  function createTranscriptStore(cfg) {
900
977
  if (cfg.type === "none") return new NoopTranscriptStore();
901
978
  if (cfg.type === "analyzer") {
902
- throw new Error(
903
- "transcript store: type=analyzer uploads are handled by the hook command directly"
904
- );
979
+ const analyzerUrl = process.env.AGENT_INSIGHTS_ANALYZER_URL ?? "";
980
+ return new AnalyzerTranscriptStore(analyzerUrl);
905
981
  }
906
982
  const token = process.env[cfg.tokenEnv];
907
983
  if (!token) {
908
984
  throw new Error(
909
- `transcript store: env ${cfg.tokenEnv} is not set (run 'vercel env pull .env.local')`
985
+ `transcript store: env ${cfg.tokenEnv} is not set \u2014 run \`agent-insights init --force\` to switch to analyzer-proxied uploads (no token required), or \`vercel env pull .env.local\` to keep direct Blob uploads`
910
986
  );
911
987
  }
912
988
  return new BlobTranscriptStore(token, cfg.prefix);
@@ -1131,6 +1207,7 @@ function parseJsonObject(raw) {
1131
1207
  }
1132
1208
 
1133
1209
  // src/commands/init.ts
1210
+ import { checkbox, confirm } from "@inquirer/prompts";
1134
1211
  import kleur3 from "kleur";
1135
1212
  init_paths();
1136
1213
  init_store();
@@ -1154,18 +1231,48 @@ async function runInit(opts) {
1154
1231
  log.hint(CONSENT_TEXT);
1155
1232
  log.info("");
1156
1233
  const existing = await loadConfig();
1234
+ const interactive = !opts.yes && Boolean(process.stdin.isTTY && process.stdout.isTTY);
1157
1235
  if (existing && !opts.force) {
1158
- log.warn(
1159
- `Config already exists at ${configFile()} \u2014 re-run with --force to overwrite.`
1236
+ if (!interactive) {
1237
+ log.warn(
1238
+ `Config already exists at ${configFile()} \u2014 re-run with --force to overwrite, or use interactive mode.`
1239
+ );
1240
+ return;
1241
+ }
1242
+ log.warn(`Config already exists at ${configFile()}.`);
1243
+ const overwrite = await safePrompt(
1244
+ () => confirm({ message: "Overwrite existing config?", default: false })
1160
1245
  );
1246
+ if (overwrite === void 0) return;
1247
+ if (!overwrite) {
1248
+ log.info("Aborted \u2014 existing config left in place.");
1249
+ return;
1250
+ }
1251
+ log.info("");
1252
+ }
1253
+ let choices;
1254
+ if (interactive) {
1255
+ const result = await promptChoices(opts);
1256
+ if (!result) return;
1257
+ choices = result;
1258
+ } else {
1259
+ choices = {
1260
+ agents: parseAgents(opts.agents),
1261
+ transcripts: opts.transcripts ?? true,
1262
+ analysis: opts.analysis ?? false,
1263
+ telemetry: opts.telemetry ?? false
1264
+ };
1161
1265
  }
1162
- const requested = parseAgents(opts.agents);
1163
- const cfg = existing ?? defaultConfig();
1164
- cfg.userConsent.transcriptSync = opts.transcripts ?? true;
1165
- cfg.userConsent.sessionAnalysis = opts.analysis ?? false;
1166
- cfg.userConsent.eventTelemetry = opts.telemetry ?? false;
1167
- cfg.agents.claudeCode.enabled = requested.has("claude-code");
1168
- cfg.agents.cursor.enabled = requested.has("cursor");
1266
+ if (choices.agents.size === 0) {
1267
+ log.fail("No agents selected \u2014 nothing to install.");
1268
+ process.exit(1);
1269
+ }
1270
+ const cfg = defaultConfig();
1271
+ cfg.userConsent.transcriptSync = choices.transcripts;
1272
+ cfg.userConsent.sessionAnalysis = choices.analysis;
1273
+ cfg.userConsent.eventTelemetry = choices.telemetry;
1274
+ cfg.agents.claudeCode.enabled = choices.agents.has("claude-code");
1275
+ cfg.agents.cursor.enabled = choices.agents.has("cursor");
1169
1276
  await saveConfig(cfg);
1170
1277
  log.ok(`Wrote config to ${configFile()}`);
1171
1278
  const installHooks = opts.hooks ?? true;
@@ -1182,9 +1289,67 @@ async function runInit(opts) {
1182
1289
  log.info(`Config root: ${configRoot()}`);
1183
1290
  log.hint("Run `agent-insights doctor` to verify the setup.");
1184
1291
  }
1292
+ async function promptChoices(opts) {
1293
+ const requested = opts.agents ? parseAgents(opts.agents) : null;
1294
+ const selected = await safePrompt(
1295
+ () => checkbox({
1296
+ message: "Which agents would you like to install hooks for?",
1297
+ choices: adapterList.map((adapter) => ({
1298
+ name: adapter.label,
1299
+ value: adapter.id,
1300
+ checked: requested ? requested.has(adapter.id) : true
1301
+ })),
1302
+ required: true
1303
+ })
1304
+ );
1305
+ if (selected === void 0) return void 0;
1306
+ const transcripts = await safePrompt(
1307
+ () => confirm({
1308
+ message: "Sync session transcripts to Vercel Blob?",
1309
+ default: opts.transcripts ?? true
1310
+ })
1311
+ );
1312
+ if (transcripts === void 0) return void 0;
1313
+ const analysis = await safePrompt(
1314
+ () => confirm({
1315
+ message: "Enable SessionEnd analysis (internal LLM review)?",
1316
+ default: opts.analysis ?? false
1317
+ })
1318
+ );
1319
+ if (analysis === void 0) return void 0;
1320
+ const telemetry = await safePrompt(
1321
+ () => confirm({
1322
+ message: "Send OTLP event telemetry?",
1323
+ default: opts.telemetry ?? false
1324
+ })
1325
+ );
1326
+ if (telemetry === void 0) return void 0;
1327
+ return {
1328
+ agents: new Set(selected),
1329
+ transcripts,
1330
+ analysis,
1331
+ telemetry
1332
+ };
1333
+ }
1334
+ async function safePrompt(run) {
1335
+ try {
1336
+ return await run();
1337
+ } catch (err) {
1338
+ if (err instanceof Error && (err.name === "ExitPromptError" || err.name === "AbortPromptError")) {
1339
+ log.info("Aborted.");
1340
+ return void 0;
1341
+ }
1342
+ throw err;
1343
+ }
1344
+ }
1185
1345
  function parseAgents(value) {
1186
1346
  const out = /* @__PURE__ */ new Set();
1187
- const list = (value ?? "claude-code,cursor").split(",").map((s) => s.trim()).filter(Boolean);
1347
+ const trimmed = (value ?? "").trim();
1348
+ if (trimmed === "*") {
1349
+ for (const a of adapterList) out.add(a.id);
1350
+ return out;
1351
+ }
1352
+ const list = (trimmed || "claude-code,cursor").split(",").map((s) => s.trim()).filter(Boolean);
1188
1353
  for (const id of list) {
1189
1354
  if (id === "claude-code" || id === "cursor") out.add(id);
1190
1355
  else log.warn(`Unknown agent id "${id}" \u2014 skipping.`);
@@ -1310,18 +1475,19 @@ function yn(v) {
1310
1475
  var program = new Command();
1311
1476
  program.name("agent-insights").description(
1312
1477
  "Internal CLI for AI coding agent observability (Claude Code, Cursor)."
1313
- ).version("0.0.7");
1478
+ ).version("0.0.10");
1314
1479
  program.command("login").description("Authenticate with Vercel (Sign in with Vercel).").action(async () => {
1315
1480
  await runLogin();
1316
1481
  });
1317
1482
  program.command("logout").description("Clear stored credentials.").action(async () => {
1318
1483
  await runLogout();
1319
1484
  });
1320
- program.command("init").description("Install agent hooks globally (run once per machine).").option(
1321
- "--agents <list>",
1322
- "Comma-separated agent ids (claude-code,cursor)",
1323
- "claude-code,cursor"
1324
- ).option("--transcripts", "Opt into transcript sync to Vercel Blob").option("--no-transcripts", "Skip transcript sync").option("--analysis", "Opt into SessionEnd analysis (Phase 2)").option("--telemetry", "Opt into OTLP event telemetry (Phase 3)").option("--no-hooks", "Skip installing per-agent hooks").option("--force", "Overwrite existing config").action(async (opts) => {
1485
+ program.command("init").description(
1486
+ "Install agent hooks globally. Interactive by default; use --yes for non-interactive."
1487
+ ).option(
1488
+ "-a, --agents <list>",
1489
+ "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) => {
1325
1491
  await runInit(opts);
1326
1492
  });
1327
1493
  program.command("status").description("Show current config, consent, and adapter state.").option("--json", "Emit machine-readable JSON").action(async (opts) => {