brakit 0.10.0 → 0.10.2

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.
@@ -10,7 +10,7 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/constants/config.ts
13
- var PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_OBJECT_SCAN_DEPTH, ISSUE_PRUNE_TTL_MS, OVERFETCH_UNWRAP_MIN_SIZE, STALE_ISSUE_TTL_MS, METRICS_DIR, PORT_FILE, VALID_ISSUE_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES, TELEMETRY_EVENT_CLI_INVOKED, TELEMETRY_EVENT_CLI_UNINSTALL, DETAIL_PREVIEW_LENGTH;
13
+ var PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_OBJECT_SCAN_DEPTH, ISSUE_PRUNE_TTL_MS, OVERFETCH_UNWRAP_MIN_SIZE, STALE_ISSUE_TTL_MS, METRICS_DIR, PORT_FILE, VALID_ISSUE_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES, DETAIL_PREVIEW_LENGTH;
14
14
  var init_config = __esm({
15
15
  "src/constants/config.ts"() {
16
16
  "use strict";
@@ -29,8 +29,6 @@ var init_config = __esm({
29
29
  VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
30
30
  VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
31
31
  VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
32
- TELEMETRY_EVENT_CLI_INVOKED = "cli_invoked";
33
- TELEMETRY_EVENT_CLI_UNINSTALL = "cli_uninstall";
34
32
  DETAIL_PREVIEW_LENGTH = 120;
35
33
  }
36
34
  });
@@ -72,8 +70,91 @@ var init_type_guards = __esm({
72
70
  }
73
71
  });
74
72
 
73
+ // src/constants/detection.ts
74
+ var KNOWN_DEPENDENCY_NAMES, KNOWN_DEPENDENCY_SET;
75
+ var init_detection = __esm({
76
+ "src/constants/detection.ts"() {
77
+ "use strict";
78
+ KNOWN_DEPENDENCY_NAMES = [
79
+ // -- Frameworks (meta) --
80
+ "next",
81
+ "@remix-run/dev",
82
+ "nuxt",
83
+ "astro",
84
+ // -- Frameworks (backend) --
85
+ "@nestjs/core",
86
+ "@adonisjs/core",
87
+ "sails",
88
+ "express",
89
+ "fastify",
90
+ "hono",
91
+ "koa",
92
+ "@hapi/hapi",
93
+ "elysia",
94
+ "h3",
95
+ "nitro",
96
+ "@trpc/server",
97
+ // -- Bundlers --
98
+ "vite",
99
+ // -- ORM / query builders --
100
+ "prisma",
101
+ "@prisma/client",
102
+ "drizzle-orm",
103
+ "typeorm",
104
+ "sequelize",
105
+ "mongoose",
106
+ "kysely",
107
+ "knex",
108
+ "@mikro-orm/core",
109
+ "objection",
110
+ // -- DB drivers --
111
+ "pg",
112
+ "mysql2",
113
+ "mongodb",
114
+ "better-sqlite3",
115
+ "@libsql/client",
116
+ "@planetscale/database",
117
+ "ioredis",
118
+ "redis",
119
+ // -- Auth --
120
+ "lucia",
121
+ "next-auth",
122
+ "@auth/core",
123
+ "passport",
124
+ // -- Queues / messaging --
125
+ "bullmq",
126
+ "amqplib",
127
+ "kafkajs",
128
+ // -- Validation --
129
+ "zod",
130
+ "joi",
131
+ "yup",
132
+ "arktype",
133
+ "valibot",
134
+ // -- HTTP clients --
135
+ "axios",
136
+ "got",
137
+ "ky",
138
+ "undici",
139
+ // -- Realtime --
140
+ "socket.io",
141
+ "ws",
142
+ // -- CSS / styling --
143
+ "tailwindcss",
144
+ // -- Testing --
145
+ "vitest",
146
+ "jest",
147
+ "mocha",
148
+ // -- Runtime indicators --
149
+ "bun-types",
150
+ "@types/bun"
151
+ ];
152
+ KNOWN_DEPENDENCY_SET = new Set(KNOWN_DEPENDENCY_NAMES);
153
+ }
154
+ });
155
+
75
156
  // src/constants/labels.ts
76
- var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, DASHBOARD_API_GRAPH, VALID_TABS_TUPLE, VALID_TABS, POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS;
157
+ var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, DASHBOARD_API_GRAPH, VALID_TABS_TUPLE, VALID_TABS;
77
158
  var init_labels = __esm({
78
159
  "src/constants/labels.ts"() {
79
160
  "use strict";
@@ -99,19 +180,12 @@ var init_labels = __esm({
99
180
  VALID_TABS_TUPLE = [
100
181
  "overview",
101
182
  "actions",
102
- "requests",
103
- "fetches",
104
- "queries",
105
- "errors",
106
- "logs",
183
+ "insights",
107
184
  "performance",
108
- "security",
109
- "graph"
185
+ "graph",
186
+ "explorer"
110
187
  ];
111
188
  VALID_TABS = new Set(VALID_TABS_TUPLE);
112
- POSTHOG_HOST = "https://us.i.posthog.com";
113
- POSTHOG_CAPTURE_PATH = "/i/v0/e/";
114
- POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
115
189
  }
116
190
  });
117
191
 
@@ -140,7 +214,7 @@ var init_features = __esm({
140
214
  MAX_TIMELINE_EVENTS = 20;
141
215
  MAX_RESOLVED_DISPLAY = 5;
142
216
  ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
143
- MCP_SERVER_VERSION = "0.10.0";
217
+ MCP_SERVER_VERSION = "0.10.2";
144
218
  RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
145
219
  PORT_MIN = 1;
146
220
  PORT_MAX = 65535;
@@ -149,13 +223,32 @@ var init_features = __esm({
149
223
  }
150
224
  });
151
225
 
226
+ // src/constants/telemetry.ts
227
+ var POSTHOG_HOST, POSTHOG_CAPTURE_PATH, POSTHOG_REQUEST_TIMEOUT_MS, TELEMETRY_EVENT_CLI_INVOKED, TELEMETRY_EVENT_CLI_UNINSTALL, TELEMETRY_EVENT_DASHBOARD_VIEWED, TELEMETRY_EVENT_SESSION, TELEMETRY_SDK_NAME, SPEED_BUCKET_THRESHOLDS;
228
+ var init_telemetry = __esm({
229
+ "src/constants/telemetry.ts"() {
230
+ "use strict";
231
+ POSTHOG_HOST = "https://us.i.posthog.com";
232
+ POSTHOG_CAPTURE_PATH = "/i/v0/e/";
233
+ POSTHOG_REQUEST_TIMEOUT_MS = 3e3;
234
+ TELEMETRY_EVENT_CLI_INVOKED = "cli_invoked";
235
+ TELEMETRY_EVENT_CLI_UNINSTALL = "cli_uninstall";
236
+ TELEMETRY_EVENT_DASHBOARD_VIEWED = "dashboard_viewed";
237
+ TELEMETRY_EVENT_SESSION = "session";
238
+ TELEMETRY_SDK_NAME = "node";
239
+ SPEED_BUCKET_THRESHOLDS = [200, 500, 1e3, 2e3, 5e3];
240
+ }
241
+ });
242
+
152
243
  // src/constants/index.ts
153
244
  var init_constants = __esm({
154
245
  "src/constants/index.ts"() {
155
246
  "use strict";
156
247
  init_config();
248
+ init_detection();
157
249
  init_labels();
158
250
  init_features();
251
+ init_telemetry();
159
252
  }
160
253
  });
161
254
 
@@ -366,10 +459,27 @@ async function enrichFindings(client) {
366
459
  if (reqData.requests.length > 0) {
367
460
  const req = reqData.requests[0];
368
461
  if (req.id) {
369
- const activity = await client.getActivity(req.id);
370
- const queryCount = activity.counts?.queries ?? 0;
371
- const fetchCount = activity.counts?.fetches ?? 0;
372
- return `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
462
+ const [activity, queries, fetches] = await Promise.all([
463
+ client.getActivity(req.id),
464
+ client.getQueries(req.id),
465
+ client.getFetches(req.id)
466
+ ]);
467
+ const lines = [`Request took ${req.durationMs}ms.`];
468
+ if (queries.entries.length > 0) {
469
+ lines.push(`DB Queries (${queries.entries.length}):`);
470
+ for (const q of queries.entries.slice(0, 5)) {
471
+ const sql = q.sql ?? `${q.operation ?? ""} ${q.table ?? q.model ?? ""}`;
472
+ lines.push(` [${q.durationMs}ms] ${sql}`);
473
+ }
474
+ if (queries.entries.length > 5) lines.push(` ... and ${queries.entries.length - 5} more`);
475
+ }
476
+ if (fetches.entries.length > 0) {
477
+ lines.push(`Fetches (${fetches.entries.length}):`);
478
+ for (const f of fetches.entries.slice(0, 3)) {
479
+ lines.push(` [${f.durationMs}ms] ${f.method} ${f.url} \u2192 ${f.statusCode}`);
480
+ }
481
+ }
482
+ return lines.join("\n");
373
483
  }
374
484
  }
375
485
  } catch {
@@ -488,6 +598,9 @@ var init_get_findings = __esm({
488
598
  return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved, stale, regressed.` }], isError: true };
489
599
  }
490
600
  let findings = await enrichFindings(client);
601
+ if (!state) {
602
+ findings = findings.filter((f) => f.aiStatus !== "wont_fix");
603
+ }
491
604
  if (severity) {
492
605
  findings = findings.filter((f) => f.severity === severity);
493
606
  }
@@ -499,21 +612,25 @@ var init_get_findings = __esm({
499
612
  if (findings.length === 0) {
500
613
  return { content: [{ type: "text", text: "No findings detected. The application looks healthy." }] };
501
614
  }
502
- const lines = [`Found ${findings.length} issue(s):
503
- `];
615
+ const lines = [
616
+ `Found ${findings.length} issue(s). Full context included below \u2014 no need to call get_request_detail separately.`,
617
+ `After fixing, call report_fixes ONCE with all results.
618
+ `
619
+ ];
504
620
  for (const f of findings) {
505
621
  lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
506
622
  lines.push(` ID: ${f.findingId}`);
507
623
  lines.push(` Endpoint: ${f.endpoint}`);
508
624
  lines.push(` Issue: ${f.description}`);
509
- if (f.context) lines.push(` Context: ${f.context}`);
625
+ if (f.context) {
626
+ for (const ctxLine of f.context.split("\n")) {
627
+ lines.push(` ${ctxLine}`);
628
+ }
629
+ }
510
630
  lines.push(` Fix: ${f.hint}`);
511
631
  if (f.aiStatus === "fixed") {
512
632
  lines.push(` AI Status: fixed (awaiting verification)`);
513
633
  if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
514
- } else if (f.aiStatus === "wont_fix") {
515
- lines.push(` AI Status: won't fix`);
516
- if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
517
634
  }
518
635
  lines.push("");
519
636
  }
@@ -901,6 +1018,69 @@ var init_report_fix = __esm({
901
1018
  }
902
1019
  });
903
1020
 
1021
+ // src/mcp/tools/report-fixes.ts
1022
+ var reportFixes;
1023
+ var init_report_fixes = __esm({
1024
+ "src/mcp/tools/report-fixes.ts"() {
1025
+ "use strict";
1026
+ init_type_guards();
1027
+ reportFixes = {
1028
+ name: "report_fixes",
1029
+ description: "Report results for multiple findings in a single call. Use this instead of calling report_fix repeatedly \u2014 it's faster and requires only one confirmation. Pass a JSON array string where each item has finding_id, status ('fixed' or 'wont_fix'), and summary.",
1030
+ inputSchema: {
1031
+ type: "object",
1032
+ properties: {
1033
+ fixes: {
1034
+ type: "string",
1035
+ description: 'JSON array of fix reports. Example: [{"finding_id":"abc123","status":"fixed","summary":"Added input validation"}]'
1036
+ }
1037
+ },
1038
+ required: ["fixes"]
1039
+ },
1040
+ async handler(client, args) {
1041
+ let fixes;
1042
+ try {
1043
+ const raw = typeof args.fixes === "string" ? JSON.parse(args.fixes) : args.fixes;
1044
+ if (!Array.isArray(raw) || raw.length === 0) {
1045
+ return { content: [{ type: "text", text: "fixes must be a non-empty JSON array." }], isError: true };
1046
+ }
1047
+ fixes = raw.filter(
1048
+ (item) => typeof item === "object" && item !== null && typeof item.finding_id === "string" && typeof item.status === "string" && typeof item.summary === "string"
1049
+ );
1050
+ if (fixes.length === 0) {
1051
+ return { content: [{ type: "text", text: "No valid fix entries found. Each entry needs finding_id, status, and summary." }], isError: true };
1052
+ }
1053
+ } catch {
1054
+ return { content: [{ type: "text", text: "fixes must be valid JSON." }], isError: true };
1055
+ }
1056
+ const results = [];
1057
+ let errors = 0;
1058
+ for (const fix of fixes) {
1059
+ if (!fix.finding_id || !isValidAiFixStatus(fix.status) || !fix.summary) {
1060
+ results.push(`\u2717 Invalid entry: ${fix.finding_id || "missing ID"}`);
1061
+ errors++;
1062
+ continue;
1063
+ }
1064
+ const ok = await client.reportFix(fix.finding_id, fix.status, fix.summary);
1065
+ if (ok) {
1066
+ const label = fix.status === "fixed" ? "fixed" : "won't fix";
1067
+ results.push(`\u2713 ${fix.finding_id} \u2192 ${label}`);
1068
+ } else {
1069
+ results.push(`\u2717 ${fix.finding_id} \u2192 not found`);
1070
+ errors++;
1071
+ }
1072
+ }
1073
+ const errorSuffix = errors > 0 ? ` (${errors} error${errors !== 1 ? "s" : ""})` : "";
1074
+ const summary = `Processed ${fixes.length} finding(s)${errorSuffix}. Dashboard updated.`;
1075
+ return {
1076
+ content: [{ type: "text", text: summary + "\n\n" + results.join("\n") }],
1077
+ ...errors > 0 ? { isError: true } : {}
1078
+ };
1079
+ }
1080
+ };
1081
+ }
1082
+ });
1083
+
904
1084
  // src/mcp/tools/index.ts
905
1085
  function getToolDefinitions() {
906
1086
  return [...TOOL_MAP.values()].map((t) => ({
@@ -930,8 +1110,9 @@ var init_tools = __esm({
930
1110
  init_get_report();
931
1111
  init_clear_findings();
932
1112
  init_report_fix();
1113
+ init_report_fixes();
933
1114
  TOOL_MAP = new Map(
934
- [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings, reportFix].map((t) => [t.name, t])
1115
+ [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings, reportFix, reportFixes].map((t) => [t.name, t])
935
1116
  );
936
1117
  }
937
1118
  });
@@ -941,7 +1122,7 @@ var SERVER_INSTRUCTIONS, PROMPTS, PROMPT_MESSAGES;
941
1122
  var init_prompts = __esm({
942
1123
  "src/mcp/prompts.ts"() {
943
1124
  "use strict";
944
- SERVER_INSTRUCTIONS = "Brakit is a dev tool that monitors your running app for security vulnerabilities, performance issues, and query problems. IMPORTANT: After fixing any brakit finding, always call the report_fix tool with the finding_id, status ('fixed' or 'wont_fix'), and a brief summary of what you did. This updates the dashboard in real-time so the developer can track AI-driven fixes.";
1125
+ SERVER_INSTRUCTIONS = "Brakit is a dev tool that monitors your running app for security vulnerabilities, performance issues, and query problems. IMPORTANT: After fixing brakit findings, call report_fixes (batch) with all results at once instead of calling report_fix individually. This is faster and requires only one confirmation.";
945
1126
  PROMPTS = [
946
1127
  {
947
1128
  name: "check-app",
@@ -953,8 +1134,8 @@ var init_prompts = __esm({
953
1134
  }
954
1135
  ];
955
1136
  PROMPT_MESSAGES = {
956
- "check-app": "Check my running app for security and performance issues using brakit. First get all findings, then get the endpoint summary. For any critical or warning findings, get the request detail to understand the root cause. Give me a clear report of what's wrong and offer to fix each issue. After fixing any issue, call report_fix with the finding_id, status, and summary.",
957
- "fix-findings": "Get all open brakit findings. For each finding:\n1. Get the request detail to understand the exact issue\n2. Find the source code responsible and fix it\n3. Call report_fix with the finding_id, status ('fixed' or 'wont_fix'), and a brief summary of what you did\n4. Move to the next finding\n\nAfter all findings are processed, ask me to re-trigger the endpoints so brakit can verify the fixes."
1137
+ "check-app": "Check my running app for security and performance issues using brakit. First get all findings, then get the endpoint summary. For any critical or warning findings, get the request detail to understand the root cause. Give me a clear report of what's wrong and offer to fix each issue. After fixing issues, call report_fixes once with all results.",
1138
+ "fix-findings": "Get all open brakit findings. For each finding:\n1. Get the request detail to understand the exact issue\n2. Find the source code responsible and fix it\n3. Track the finding_id, status ('fixed' or 'wont_fix'), and a brief summary\n\nAfter processing ALL findings, call report_fixes ONCE with the full array of results. Do not call report_fix individually for each finding.\n\nAfter reporting, ask me to re-trigger the endpoints so brakit can verify the fixes."
958
1139
  };
959
1140
  }
960
1141
  });
@@ -1122,12 +1303,24 @@ import { createHash as createHash2 } from "crypto";
1122
1303
  import { readFile as readFile3, readdir } from "fs/promises";
1123
1304
  import { existsSync as existsSync4 } from "fs";
1124
1305
  import { join as join2, relative } from "path";
1306
+ init_detection();
1125
1307
  var FRAMEWORKS = [
1308
+ // Meta-frameworks first (they bundle Express/Vite internally)
1126
1309
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
1127
1310
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
1128
1311
  { name: "nuxt", dep: "nuxt", devCmd: "nuxt dev", bin: "nuxt", defaultPort: 3e3, devArgs: ["dev", "--port"] },
1129
- { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] },
1130
- { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
1312
+ { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] },
1313
+ { name: "nestjs", dep: "@nestjs/core", devCmd: "nest start", bin: "nest", defaultPort: 3e3, devArgs: ["--watch"] },
1314
+ { name: "adonis", dep: "@adonisjs/core", devCmd: "node ace serve", bin: "ace", defaultPort: 3333, devArgs: ["serve", "--watch"] },
1315
+ { name: "sails", dep: "sails", devCmd: "sails lift", bin: "sails", defaultPort: 1337, devArgs: ["lift"] },
1316
+ // Server frameworks
1317
+ { name: "hono", dep: "hono", devCmd: "node", bin: "node", defaultPort: 3e3 },
1318
+ { name: "fastify", dep: "fastify", devCmd: "node", bin: "node", defaultPort: 3e3 },
1319
+ { name: "koa", dep: "koa", devCmd: "node", bin: "node", defaultPort: 3e3 },
1320
+ { name: "hapi", dep: "@hapi/hapi", devCmd: "node", bin: "node", defaultPort: 3e3 },
1321
+ { name: "express", dep: "express", devCmd: "node", bin: "node", defaultPort: 3e3 },
1322
+ // Bundlers (last — likely used alongside a framework above)
1323
+ { name: "vite", dep: "vite", devCmd: "vite", bin: "vite", defaultPort: 5173, devArgs: ["--port"] }
1131
1324
  ];
1132
1325
  async function detectProject(rootDir) {
1133
1326
  const pkgPath = join2(rootDir, "package.json");
@@ -1139,23 +1332,22 @@ async function detectProject(rootDir) {
1139
1332
  const devCommand = matched?.devCmd ?? "";
1140
1333
  const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
1141
1334
  const defaultPort = matched?.defaultPort ?? 3e3;
1142
- const packageManager = await detectPackageManager(rootDir);
1335
+ const packageManager = detectPackageManager(rootDir);
1143
1336
  return { framework, devCommand, devBin, defaultPort, packageManager };
1144
1337
  }
1145
- async function detectPackageManager(rootDir) {
1146
- if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
1147
- if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
1148
- if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1149
- if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
1150
- if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
1151
- return "unknown";
1152
- }
1153
1338
  function detectFrameworkFromDeps(allDeps) {
1154
1339
  for (const f of FRAMEWORKS) {
1155
1340
  if (allDeps[f.dep]) return f.name;
1156
1341
  }
1157
1342
  return "unknown";
1158
1343
  }
1344
+ function detectPackageManager(rootDir) {
1345
+ if (existsSync4(join2(rootDir, "bun.lockb")) || existsSync4(join2(rootDir, "bun.lock"))) return "bun";
1346
+ if (existsSync4(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1347
+ if (existsSync4(join2(rootDir, "yarn.lock"))) return "yarn";
1348
+ if (existsSync4(join2(rootDir, "package-lock.json"))) return "npm";
1349
+ return "unknown";
1350
+ }
1159
1351
  var PYTHON_ENTRY_CANDIDATES = [
1160
1352
  "app.py",
1161
1353
  "main.py",
@@ -1910,7 +2102,7 @@ init_constants();
1910
2102
  init_endpoint();
1911
2103
 
1912
2104
  // src/index.ts
1913
- var VERSION = "0.10.0";
2105
+ var VERSION = "0.10.2";
1914
2106
 
1915
2107
  // src/cli/commands/install.ts
1916
2108
  init_constants();
@@ -2251,10 +2443,6 @@ init_constants();
2251
2443
  init_log();
2252
2444
  init_type_guards();
2253
2445
 
2254
- // src/telemetry/index.ts
2255
- import { platform as platform2, release, arch } from "os";
2256
- import { spawn } from "child_process";
2257
-
2258
2446
  // src/telemetry/config.ts
2259
2447
  init_features();
2260
2448
  import { homedir as homedir2, platform } from "os";
@@ -2264,10 +2452,14 @@ import { randomUUID as randomUUID2 } from "crypto";
2264
2452
  var IS_WINDOWS = platform() === "win32";
2265
2453
  var CONFIG_DIR = join4(homedir2(), ".brakit");
2266
2454
  var CONFIG_PATH = join4(CONFIG_DIR, "config.json");
2455
+ function isValidTelemetryConfig(value) {
2456
+ return typeof value === "object" && value !== null && typeof value.telemetry === "boolean" && typeof value.anonymousId === "string" && value.anonymousId.length > 0;
2457
+ }
2267
2458
  function readConfig() {
2268
2459
  try {
2269
2460
  if (!existsSync6(CONFIG_PATH)) return null;
2270
- return JSON.parse(readFileSync4(CONFIG_PATH, "utf-8"));
2461
+ const parsed = JSON.parse(readFileSync4(CONFIG_PATH, "utf-8"));
2462
+ return isValidTelemetryConfig(parsed) ? parsed : null;
2271
2463
  } catch {
2272
2464
  return null;
2273
2465
  }
@@ -2290,9 +2482,7 @@ function writeConfig(config) {
2290
2482
  }
2291
2483
  function getOrCreateConfig() {
2292
2484
  const existing = readConfig();
2293
- if (existing && typeof existing.telemetry === "boolean" && existing.anonymousId) {
2294
- return existing;
2295
- }
2485
+ if (existing) return existing;
2296
2486
  const config = { telemetry: true, anonymousId: randomUUID2() };
2297
2487
  writeConfig(config);
2298
2488
  return config;
@@ -2302,16 +2492,19 @@ function isTelemetryEnabled() {
2302
2492
  if (cachedEnabled !== null) return cachedEnabled;
2303
2493
  const env = process.env.BRAKIT_TELEMETRY;
2304
2494
  if (env !== void 0) {
2305
- cachedEnabled = env !== "false" && env !== "0" && env !== "off";
2495
+ const normalized = env.toLowerCase().trim();
2496
+ cachedEnabled = normalized !== "false" && normalized !== "0" && normalized !== "off";
2306
2497
  return cachedEnabled;
2307
2498
  }
2308
2499
  cachedEnabled = readConfig()?.telemetry ?? true;
2309
2500
  return cachedEnabled;
2310
2501
  }
2311
2502
 
2312
- // src/telemetry/index.ts
2313
- init_labels();
2314
- init_config();
2503
+ // src/telemetry/transport.ts
2504
+ import { platform as platform2, release, arch } from "os";
2505
+ import { spawn } from "child_process";
2506
+ init_telemetry();
2507
+ init_log();
2315
2508
  var POSTHOG_KEY = "phc_E9TwydCGnSfPLIUhNxChpeg32TSowjk31KiPhnLPP0x";
2316
2509
  function commonProperties() {
2317
2510
  return {
@@ -2325,7 +2518,7 @@ function commonProperties() {
2325
2518
  };
2326
2519
  }
2327
2520
  function sendToPosthog(event, properties) {
2328
- if (!isTelemetryEnabled()) return;
2521
+ if (!isTelemetryEnabled() || !POSTHOG_KEY) return;
2329
2522
  const config = getOrCreateConfig();
2330
2523
  const payload = {
2331
2524
  api_key: POSTHOG_KEY,
@@ -2335,26 +2528,198 @@ function sendToPosthog(event, properties) {
2335
2528
  properties: { ...commonProperties(), ...properties }
2336
2529
  };
2337
2530
  try {
2338
- const body = JSON.stringify(payload);
2531
+ const serializedPayload = JSON.stringify(payload);
2339
2532
  const url = `${POSTHOG_HOST}${POSTHOG_CAPTURE_PATH}`;
2340
2533
  const child = spawn(
2341
2534
  process.execPath,
2342
2535
  [
2343
2536
  "-e",
2344
- `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(body)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
2537
+ `fetch(${JSON.stringify(url)},{method:"POST",headers:{"content-type":"application/json"},body:${JSON.stringify(serializedPayload)},signal:AbortSignal.timeout(${POSTHOG_REQUEST_TIMEOUT_MS})}).catch(()=>{})`
2345
2538
  ],
2346
2539
  { detached: true, stdio: "ignore" }
2347
2540
  );
2348
2541
  child.unref();
2349
- } catch {
2542
+ } catch (err) {
2543
+ brakitDebug(`telemetry send failed: ${err}`);
2350
2544
  }
2351
2545
  }
2352
2546
  function trackEvent(event, properties) {
2353
- sendToPosthog(event, { sdk: "node", ...properties });
2547
+ sendToPosthog(event, { sdk: TELEMETRY_SDK_NAME, ...properties });
2548
+ }
2549
+
2550
+ // src/telemetry/session.ts
2551
+ init_telemetry();
2552
+ var defaultTransport = { send: sendToPosthog, track: trackEvent };
2553
+ var Session = class {
2554
+ constructor(transport = defaultTransport) {
2555
+ // ── Setup phase ──
2556
+ this.startTime = 0;
2557
+ this.framework = "unknown";
2558
+ this.packageManager = "";
2559
+ this.isCustomCommand = false;
2560
+ this.adapters = [];
2561
+ this.setupDurationMs = 0;
2562
+ this.setupSucceeded = false;
2563
+ // ── Detection ──
2564
+ this.detectedDependencies = [];
2565
+ this.adaptersFailed = [];
2566
+ this.configFilesDetected = [];
2567
+ this.jsRuntime = "node";
2568
+ this.loadedPackages = [];
2569
+ // ── Python SDK ──
2570
+ this.pythonConnected = false;
2571
+ this.pythonFramework = "unknown";
2572
+ this.pythonAdapters = [];
2573
+ this.pythonVersion = "";
2574
+ // ── Runtime accumulation ──
2575
+ this.requestCount = 0;
2576
+ this.insightTypes = /* @__PURE__ */ new Set();
2577
+ this.rulesTriggered = /* @__PURE__ */ new Set();
2578
+ this.tabsViewed = /* @__PURE__ */ new Set();
2579
+ this.dashboardOpened = false;
2580
+ this.explainUsed = false;
2581
+ this.firstRequestAt = 0;
2582
+ this.dashboardOpenedAt = 0;
2583
+ this.exitReason = "unknown";
2584
+ this.transport = transport;
2585
+ }
2586
+ // ── Setup phase ──
2587
+ init(framework, packageManager, isCustomCommand, adapters) {
2588
+ getOrCreateConfig();
2589
+ this.startTime = Date.now();
2590
+ this.framework = framework;
2591
+ this.packageManager = packageManager;
2592
+ this.isCustomCommand = isCustomCommand;
2593
+ this.adapters = adapters;
2594
+ }
2595
+ recordSetup(detection, durationMs) {
2596
+ this.detectedDependencies = detection.detectedDependencies;
2597
+ this.adaptersFailed = detection.adaptersFailed;
2598
+ this.configFilesDetected = detection.configFilesDetected;
2599
+ this.jsRuntime = detection.jsRuntime;
2600
+ this.setupDurationMs = durationMs;
2601
+ this.setupSucceeded = true;
2602
+ }
2603
+ // ── Runtime events ──
2604
+ recordFirstRequest(loadedPackages) {
2605
+ if (!this.firstRequestAt) this.firstRequestAt = Date.now();
2606
+ this.loadedPackages = loadedPackages;
2607
+ }
2608
+ recordPythonStack(info) {
2609
+ this.pythonConnected = true;
2610
+ this.pythonFramework = info.framework;
2611
+ this.pythonAdapters = info.adapters;
2612
+ this.pythonVersion = info.pythonVersion;
2613
+ }
2614
+ recordDashboardOpened() {
2615
+ if (this.dashboardOpened) return;
2616
+ this.dashboardOpened = true;
2617
+ this.dashboardOpenedAt = Date.now();
2618
+ this.transport.track(TELEMETRY_EVENT_DASHBOARD_VIEWED, {
2619
+ time_to_dashboard_ms: this.startTime > 0 ? Date.now() - this.startTime : null,
2620
+ request_count_at_open: this.requestCount
2621
+ });
2622
+ }
2623
+ recordTabViewed(tab) {
2624
+ this.tabsViewed.add(tab);
2625
+ }
2626
+ recordExplainUsed() {
2627
+ this.explainUsed = true;
2628
+ }
2629
+ recordExitReason(reason) {
2630
+ if (this.exitReason === "unknown") this.exitReason = reason;
2631
+ }
2632
+ // ── Pre-flush snapshot from services ──
2633
+ recordCounts(requestCount, insightTypes, rulesTriggered) {
2634
+ this.requestCount = requestCount;
2635
+ for (const t of insightTypes) this.insightTypes.add(t);
2636
+ for (const r of rulesTriggered) this.rulesTriggered.add(r);
2637
+ }
2638
+ // ── Serialization ──
2639
+ /** Build the full PostHog session payload. Single source of truth for all fields. */
2640
+ toPostHogPayload(services) {
2641
+ const metricsStore = services.metricsStore;
2642
+ const analysisEngine = services.analysisEngine;
2643
+ const live = metricsStore.getLiveEndpoints();
2644
+ const insights = analysisEngine.getInsights();
2645
+ const findings = analysisEngine.getFindings();
2646
+ let totalRequests = 0;
2647
+ let totalDuration = 0;
2648
+ let slowestP95 = 0;
2649
+ for (const ep of live) {
2650
+ totalRequests += ep.summary.totalRequests;
2651
+ totalDuration += ep.summary.p95Ms * ep.summary.totalRequests;
2652
+ if (ep.summary.p95Ms > slowestP95) slowestP95 = ep.summary.p95Ms;
2653
+ }
2654
+ const now = Date.now();
2655
+ return {
2656
+ sdk: TELEMETRY_SDK_NAME,
2657
+ // Stack detection
2658
+ framework: this.framework,
2659
+ package_manager: this.packageManager,
2660
+ js_runtime: this.jsRuntime,
2661
+ framework_detection_candidates: this.detectedDependencies,
2662
+ config_files_detected: this.configFilesDetected,
2663
+ loaded_packages: this.loadedPackages,
2664
+ loaded_package_count: this.loadedPackages.length,
2665
+ adapters_detected: this.adapters,
2666
+ adapters_failed: this.adaptersFailed,
2667
+ // Python SDK
2668
+ python_connected: this.pythonConnected,
2669
+ python_framework: this.pythonFramework,
2670
+ python_adapters: this.pythonAdapters,
2671
+ python_version: this.pythonVersion,
2672
+ // Session metadata
2673
+ is_custom_command: this.isCustomCommand,
2674
+ first_session: readConfig() === null,
2675
+ setup_succeeded: this.setupSucceeded,
2676
+ setup_duration_ms: this.setupDurationMs,
2677
+ session_duration_s: Math.ceil((now - this.startTime) / 1e3),
2678
+ session_duration_ms: now - this.startTime,
2679
+ exit_reason: this.exitReason,
2680
+ // Usage
2681
+ request_count: this.requestCount,
2682
+ error_count: services.errorStore.getAll().length,
2683
+ query_count: services.queryStore.getAll().length,
2684
+ fetch_count: services.fetchStore.getAll().length,
2685
+ endpoint_count: live.length,
2686
+ avg_duration_ms: totalRequests > 0 ? Math.round(totalDuration / totalRequests) : 0,
2687
+ slowest_endpoint_bucket: speedBucket(slowestP95),
2688
+ // Analysis
2689
+ insight_count: insights.length,
2690
+ finding_count: findings.length,
2691
+ insight_types: [...this.insightTypes],
2692
+ rules_triggered: [...this.rulesTriggered],
2693
+ // Dashboard engagement
2694
+ tabs_viewed: [...this.tabsViewed],
2695
+ dashboard_opened: this.dashboardOpened,
2696
+ explain_used: this.explainUsed,
2697
+ // Timing
2698
+ time_to_first_request_ms: this.firstRequestAt ? this.firstRequestAt - this.startTime : null,
2699
+ time_to_dashboard_ms: this.dashboardOpenedAt ? this.dashboardOpenedAt - this.startTime : null
2700
+ };
2701
+ }
2702
+ flush(services) {
2703
+ this.transport.send(TELEMETRY_EVENT_SESSION, this.toPostHogPayload(services));
2704
+ getOrCreateConfig();
2705
+ }
2706
+ };
2707
+ function speedBucket(ms) {
2708
+ if (ms === 0) return "none";
2709
+ const t = SPEED_BUCKET_THRESHOLDS;
2710
+ if (ms < t[0]) return `<${t[0]}ms`;
2711
+ for (let i = 1; i < t.length; i++) {
2712
+ if (ms < t[i]) return `${t[i - 1]}-${t[i]}ms`;
2713
+ }
2714
+ return `>${t[t.length - 1]}ms`;
2354
2715
  }
2355
2716
 
2717
+ // src/telemetry/index.ts
2718
+ init_telemetry();
2719
+ var session = new Session();
2720
+
2356
2721
  // src/cli/commands/uninstall.ts
2357
- init_config();
2722
+ init_telemetry();
2358
2723
  var PREPENDED_FILES = [
2359
2724
  "app/entry.server.tsx",
2360
2725
  "app/entry.server.ts",
@@ -2613,7 +2978,7 @@ async function clearBuildCaches(rootDir) {
2613
2978
  }
2614
2979
 
2615
2980
  // bin/brakit.ts
2616
- init_config();
2981
+ init_telemetry();
2617
2982
  var sub = process.argv[2];
2618
2983
  var command = sub === "uninstall" ? "uninstall" : sub === "mcp" ? "mcp" : "install";
2619
2984
  var cwd = process.cwd();