brakit 0.8.3 → 0.8.5

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,29 +10,49 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/constants/routes.ts
13
- var DASHBOARD_API_REQUESTS, DASHBOARD_API_CLEAR, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_FINDINGS;
13
+ 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, VALID_TABS_TUPLE, VALID_TABS;
14
14
  var init_routes = __esm({
15
15
  "src/constants/routes.ts"() {
16
16
  "use strict";
17
- DASHBOARD_API_REQUESTS = "/__brakit/api/requests";
18
- DASHBOARD_API_CLEAR = "/__brakit/api/clear";
19
- DASHBOARD_API_FETCHES = "/__brakit/api/fetches";
20
- DASHBOARD_API_ERRORS = "/__brakit/api/errors";
21
- DASHBOARD_API_QUERIES = "/__brakit/api/queries";
22
- DASHBOARD_API_ACTIVITY = "/__brakit/api/activity";
23
- DASHBOARD_API_METRICS_LIVE = "/__brakit/api/metrics/live";
24
- DASHBOARD_API_INSIGHTS = "/__brakit/api/insights";
25
- DASHBOARD_API_SECURITY = "/__brakit/api/security";
26
- DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
17
+ DASHBOARD_PREFIX = "/__brakit";
18
+ DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
19
+ DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
20
+ DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
21
+ DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
22
+ DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
23
+ DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
24
+ DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
25
+ DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
26
+ DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
27
+ DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
28
+ DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
29
+ DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
30
+ DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
31
+ DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
32
+ DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
33
+ DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
34
+ DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
35
+ VALID_TABS_TUPLE = [
36
+ "overview",
37
+ "actions",
38
+ "requests",
39
+ "fetches",
40
+ "queries",
41
+ "errors",
42
+ "logs",
43
+ "performance",
44
+ "security"
45
+ ];
46
+ VALID_TABS = new Set(VALID_TABS_TUPLE);
27
47
  }
28
48
  });
29
49
 
30
50
  // src/constants/limits.ts
31
- var MAX_INGEST_BYTES;
51
+ var FINDING_ID_HASH_LENGTH;
32
52
  var init_limits = __esm({
33
53
  "src/constants/limits.ts"() {
34
54
  "use strict";
35
- MAX_INGEST_BYTES = 10 * 1024 * 1024;
55
+ FINDING_ID_HASH_LENGTH = 16;
36
56
  }
37
57
  });
38
58
 
@@ -70,19 +90,22 @@ var init_headers = __esm({
70
90
  });
71
91
 
72
92
  // src/constants/network.ts
93
+ var RECOVERY_WINDOW_MS, PORT_MIN, PORT_MAX;
73
94
  var init_network = __esm({
74
95
  "src/constants/network.ts"() {
75
96
  "use strict";
97
+ RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
98
+ PORT_MIN = 1;
99
+ PORT_MAX = 65535;
76
100
  }
77
101
  });
78
102
 
79
103
  // src/constants/mcp.ts
80
- var MCP_SERVER_NAME, MCP_SERVER_VERSION, INITIAL_DISCOVERY_TIMEOUT_MS, LAZY_DISCOVERY_TIMEOUT_MS, CLIENT_FETCH_TIMEOUT_MS, HEALTH_CHECK_TIMEOUT_MS, DISCOVERY_POLL_INTERVAL_MS, MAX_DISCOVERY_DEPTH, MAX_TIMELINE_EVENTS, MAX_RESOLVED_DISPLAY, ENRICHMENT_SEVERITY_FILTER;
104
+ var MCP_SERVER_NAME, INITIAL_DISCOVERY_TIMEOUT_MS, LAZY_DISCOVERY_TIMEOUT_MS, CLIENT_FETCH_TIMEOUT_MS, HEALTH_CHECK_TIMEOUT_MS, DISCOVERY_POLL_INTERVAL_MS, MAX_DISCOVERY_DEPTH, MAX_TIMELINE_EVENTS, MAX_RESOLVED_DISPLAY, ENRICHMENT_SEVERITY_FILTER, MCP_SERVER_VERSION;
81
105
  var init_mcp = __esm({
82
106
  "src/constants/mcp.ts"() {
83
107
  "use strict";
84
108
  MCP_SERVER_NAME = "brakit";
85
- MCP_SERVER_VERSION = "0.8.3";
86
109
  INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
87
110
  LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
88
111
  CLIENT_FETCH_TIMEOUT_MS = 1e4;
@@ -92,6 +115,7 @@ var init_mcp = __esm({
92
115
  MAX_TIMELINE_EVENTS = 20;
93
116
  MAX_RESOLVED_DISPLAY = 5;
94
117
  ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
118
+ MCP_SERVER_VERSION = "0.8.5";
95
119
  }
96
120
  });
97
121
 
@@ -103,18 +127,27 @@ var init_encoding = __esm({
103
127
  });
104
128
 
105
129
  // src/constants/severity.ts
106
- var SEVERITY_CRITICAL, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_ICON_MAP;
107
130
  var init_severity = __esm({
108
131
  "src/constants/severity.ts"() {
109
132
  "use strict";
110
- SEVERITY_CRITICAL = "critical";
111
- SEVERITY_WARNING = "warning";
112
- SEVERITY_INFO = "info";
113
- SEVERITY_ICON_MAP = {
114
- [SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
115
- [SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
116
- [SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
117
- };
133
+ }
134
+ });
135
+
136
+ // src/constants/telemetry.ts
137
+ var init_telemetry = __esm({
138
+ "src/constants/telemetry.ts"() {
139
+ "use strict";
140
+ }
141
+ });
142
+
143
+ // src/constants/lifecycle.ts
144
+ var VALID_FINDING_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES;
145
+ var init_lifecycle = __esm({
146
+ "src/constants/lifecycle.ts"() {
147
+ "use strict";
148
+ VALID_FINDING_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
149
+ VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
150
+ VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
118
151
  }
119
152
  });
120
153
 
@@ -132,18 +165,53 @@ var init_constants = __esm({
132
165
  init_mcp();
133
166
  init_encoding();
134
167
  init_severity();
168
+ init_telemetry();
169
+ init_lifecycle();
170
+ }
171
+ });
172
+
173
+ // src/utils/log.ts
174
+ function brakitDebug(message) {
175
+ if (process.env.DEBUG_BRAKIT) {
176
+ process.stderr.write(`${PREFIX}:debug ${message}
177
+ `);
178
+ }
179
+ }
180
+ var PREFIX;
181
+ var init_log = __esm({
182
+ "src/utils/log.ts"() {
183
+ "use strict";
184
+ PREFIX = "[brakit]";
185
+ }
186
+ });
187
+
188
+ // src/utils/type-guards.ts
189
+ function isNonEmptyString(val) {
190
+ return typeof val === "string" && val.trim().length > 0;
191
+ }
192
+ function isValidFindingState(val) {
193
+ return typeof val === "string" && VALID_FINDING_STATES.has(val);
194
+ }
195
+ function isValidAiFixStatus(val) {
196
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
197
+ }
198
+ var init_type_guards = __esm({
199
+ "src/utils/type-guards.ts"() {
200
+ "use strict";
201
+ init_lifecycle();
135
202
  }
136
203
  });
137
204
 
138
205
  // src/store/finding-id.ts
139
206
  import { createHash } from "crypto";
140
- function computeFindingId(finding) {
141
- const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
142
- return createHash("sha256").update(key).digest("hex").slice(0, 16);
207
+ function computeInsightId(type, endpoint, desc) {
208
+ const key = `${type}:${endpoint}:${desc}`;
209
+ return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
143
210
  }
144
211
  var init_finding_id = __esm({
145
212
  "src/store/finding-id.ts"() {
146
213
  "use strict";
214
+ init_limits();
147
215
  }
148
216
  });
149
217
 
@@ -213,6 +281,19 @@ var init_client = __esm({
213
281
  if (state) url.searchParams.set("state", state);
214
282
  return this.fetchJson(url);
215
283
  }
284
+ async reportFix(findingId, status, notes) {
285
+ const res = await fetch(`${this.baseUrl}${DASHBOARD_API_FINDINGS_REPORT}`, {
286
+ method: "POST",
287
+ headers: { "Content-Type": "application/json" },
288
+ body: JSON.stringify({ findingId, status, notes }),
289
+ signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
290
+ });
291
+ if (!res.ok) return false;
292
+ const contentType = res.headers.get("content-type") ?? "";
293
+ if (!contentType.includes("application/json")) return false;
294
+ const body = await res.json();
295
+ return body.ok === true;
296
+ }
216
297
  async clearAll() {
217
298
  const res = await fetch(`${this.baseUrl}${DASHBOARD_API_CLEAR}`, {
218
299
  method: "POST",
@@ -244,50 +325,56 @@ var init_client = __esm({
244
325
  });
245
326
 
246
327
  // src/mcp/discovery.ts
247
- import { readFileSync as readFileSync3, existsSync as existsSync4, readdirSync, statSync } from "fs";
248
- import { resolve as resolve5, dirname } from "path";
249
- function readPort(portPath) {
250
- if (!existsSync4(portPath)) return null;
251
- const raw = readFileSync3(portPath, "utf-8").trim();
252
- const port = parseInt(raw, 10);
253
- return isNaN(port) || port < 1 || port > 65535 ? null : port;
328
+ import { readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
329
+ import { resolve as resolve5, dirname as dirname2 } from "path";
330
+ async function readPort(portPath) {
331
+ try {
332
+ const raw = (await readFile6(portPath, "utf-8")).trim();
333
+ const port = parseInt(raw, 10);
334
+ return isNaN(port) || port < PORT_MIN || port > PORT_MAX ? null : port;
335
+ } catch {
336
+ return null;
337
+ }
254
338
  }
255
- function portInDir(dir) {
339
+ async function portInDir(dir) {
256
340
  return readPort(resolve5(dir, PORT_FILE));
257
341
  }
258
- function portInChildren(dir) {
342
+ async function portInChildren(dir) {
259
343
  try {
260
- for (const entry of readdirSync(dir)) {
344
+ const entries = await readdir2(dir);
345
+ for (const entry of entries) {
261
346
  if (entry.startsWith(".") || entry === "node_modules") continue;
262
347
  const child = resolve5(dir, entry);
263
348
  try {
264
- if (!statSync(child).isDirectory()) continue;
265
- } catch {
349
+ if (!(await stat(child)).isDirectory()) continue;
350
+ } catch (err) {
351
+ brakitDebug(`discovery: stat failed for ${child}: ${err}`);
266
352
  continue;
267
353
  }
268
- const port = portInDir(child);
354
+ const port = await portInDir(child);
269
355
  if (port) return port;
270
356
  }
271
- } catch {
357
+ } catch (err) {
358
+ brakitDebug(`discovery: readdir failed for ${dir}: ${err}`);
272
359
  }
273
360
  return null;
274
361
  }
275
- function searchForPort(startDir) {
362
+ async function searchForPort(startDir) {
276
363
  const start = resolve5(startDir);
277
- const initial = portInDir(start) ?? portInChildren(start);
364
+ const initial = await portInDir(start) ?? await portInChildren(start);
278
365
  if (initial) return initial;
279
- let dir = dirname(start);
366
+ let dir = dirname2(start);
280
367
  for (let depth = 0; depth < MAX_DISCOVERY_DEPTH; depth++) {
281
- const port = portInDir(dir);
368
+ const port = await portInDir(dir) ?? await portInChildren(dir);
282
369
  if (port) return port;
283
- const parent = dirname(dir);
370
+ const parent = dirname2(dir);
284
371
  if (parent === dir) break;
285
372
  dir = parent;
286
373
  }
287
374
  return null;
288
375
  }
289
- function discoverBrakitPort(cwd) {
290
- const port = searchForPort(cwd ?? process.cwd());
376
+ async function discoverBrakitPort(cwd) {
377
+ const port = await searchForPort(cwd ?? process.cwd());
291
378
  if (!port) {
292
379
  throw new Error(
293
380
  "Brakit is not running. Start your app with brakit enabled first."
@@ -299,7 +386,7 @@ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTER
299
386
  const deadline = Date.now() + timeoutMs;
300
387
  while (Date.now() < deadline) {
301
388
  try {
302
- const result = discoverBrakitPort(cwd);
389
+ const result = await discoverBrakitPort(cwd);
303
390
  const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
304
391
  if (res.ok) return result;
305
392
  } catch {
@@ -314,50 +401,52 @@ var init_discovery = __esm({
314
401
  "src/mcp/discovery.ts"() {
315
402
  "use strict";
316
403
  init_constants();
404
+ init_log();
317
405
  init_mcp();
318
406
  }
319
407
  });
320
408
 
321
409
  // src/mcp/enrichment.ts
322
- import { createHash as createHash2 } from "crypto";
323
- function computeInsightId(type, endpoint, desc) {
324
- const key = `${type}:${endpoint}:${desc}`;
325
- return createHash2("sha256").update(key).digest("hex").slice(0, 16);
326
- }
327
410
  async function enrichFindings(client) {
328
411
  const [securityData, insightsData] = await Promise.all([
329
412
  client.getSecurityFindings(),
330
413
  client.getInsights()
331
414
  ]);
332
- const enriched = [];
333
- for (const f of securityData.findings) {
334
- let context = "";
335
- try {
336
- const { path } = parseEndpointKey(f.endpoint);
337
- const reqData = await client.getRequests({ search: path, limit: 1 });
338
- if (reqData.requests.length > 0) {
339
- const req = reqData.requests[0];
340
- if (req.id) {
341
- const activity = await client.getActivity(req.id);
342
- const queryCount = activity.counts?.queries ?? 0;
343
- const fetchCount = activity.counts?.fetches ?? 0;
344
- context = `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
415
+ const contexts = await Promise.all(
416
+ securityData.findings.map(async (sf) => {
417
+ try {
418
+ const { path } = parseEndpointKey(sf.finding.endpoint);
419
+ const reqData = await client.getRequests({ search: path, limit: 1 });
420
+ if (reqData.requests.length > 0) {
421
+ const req = reqData.requests[0];
422
+ if (req.id) {
423
+ const activity = await client.getActivity(req.id);
424
+ const queryCount = activity.counts?.queries ?? 0;
425
+ const fetchCount = activity.counts?.fetches ?? 0;
426
+ return `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
427
+ }
345
428
  }
429
+ } catch {
430
+ return "(context unavailable)";
346
431
  }
347
- } catch {
348
- context = "(context unavailable)";
349
- }
350
- enriched.push({
351
- findingId: computeFindingId(f),
432
+ return "";
433
+ })
434
+ );
435
+ const enriched = securityData.findings.map((sf, i) => {
436
+ const f = sf.finding;
437
+ return {
438
+ findingId: sf.findingId,
352
439
  severity: f.severity,
353
440
  title: f.title,
354
441
  endpoint: f.endpoint,
355
442
  description: f.desc,
356
443
  hint: f.hint,
357
444
  occurrences: f.count,
358
- context
359
- });
360
- }
445
+ context: contexts[i],
446
+ aiStatus: sf.aiStatus,
447
+ aiNotes: sf.aiNotes
448
+ };
449
+ });
361
450
  for (const si of insightsData.insights) {
362
451
  if (si.state === "resolved") continue;
363
452
  const i = si.insight;
@@ -371,7 +460,9 @@ async function enrichFindings(client) {
371
460
  description: i.desc,
372
461
  hint: i.hint,
373
462
  occurrences: 1,
374
- context: i.detail ?? ""
463
+ context: i.detail ?? "",
464
+ aiStatus: si.aiStatus,
465
+ aiNotes: si.aiNotes
375
466
  });
376
467
  }
377
468
  return enriched;
@@ -433,13 +524,13 @@ var init_enrichment = __esm({
433
524
  });
434
525
 
435
526
  // src/mcp/tools/get-findings.ts
436
- var VALID_SEVERITIES, VALID_STATES, getFindings;
527
+ var getFindings;
437
528
  var init_get_findings = __esm({
438
529
  "src/mcp/tools/get-findings.ts"() {
439
530
  "use strict";
440
531
  init_enrichment();
441
- VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
442
- VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
532
+ init_lifecycle();
533
+ init_type_guards();
443
534
  getFindings = {
444
535
  name: "get_findings",
445
536
  description: "Get all security findings and performance insights from the running app. Returns enriched findings with actionable fix hints, endpoint context, and evidence. Use this to understand what issues exist in the running application.",
@@ -461,10 +552,10 @@ var init_get_findings = __esm({
461
552
  async handler(client, args) {
462
553
  const severity = args.severity;
463
554
  const state = args.state;
464
- if (severity && !VALID_SEVERITIES.has(severity)) {
555
+ if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
465
556
  return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
466
557
  }
467
- if (state && !VALID_STATES.has(state)) {
558
+ if (state && !isValidFindingState(state)) {
468
559
  return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
469
560
  }
470
561
  let findings = await enrichFindings(client);
@@ -483,10 +574,18 @@ var init_get_findings = __esm({
483
574
  `];
484
575
  for (const f of findings) {
485
576
  lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
577
+ lines.push(` ID: ${f.findingId}`);
486
578
  lines.push(` Endpoint: ${f.endpoint}`);
487
579
  lines.push(` Issue: ${f.description}`);
488
580
  if (f.context) lines.push(` Context: ${f.context}`);
489
581
  lines.push(` Fix: ${f.hint}`);
582
+ if (f.aiStatus === "fixed") {
583
+ lines.push(` AI Status: fixed (awaiting verification)`);
584
+ if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
585
+ } else if (f.aiStatus === "wont_fix") {
586
+ lines.push(` AI Status: won't fix`);
587
+ if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
588
+ }
490
589
  lines.push("");
491
590
  }
492
591
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -815,6 +914,61 @@ var init_clear_findings = __esm({
815
914
  }
816
915
  });
817
916
 
917
+ // src/mcp/tools/report-fix.ts
918
+ var reportFix;
919
+ var init_report_fix = __esm({
920
+ "src/mcp/tools/report-fix.ts"() {
921
+ "use strict";
922
+ init_type_guards();
923
+ reportFix = {
924
+ name: "report_fix",
925
+ description: "Report the result of fixing a brakit finding. Call this after attempting to fix each finding to update the dashboard with the outcome. Use status 'fixed' when you've applied a fix, or 'wont_fix' when the issue can't be resolved (e.g. third-party library, by design).",
926
+ inputSchema: {
927
+ type: "object",
928
+ properties: {
929
+ finding_id: {
930
+ type: "string",
931
+ description: "The finding ID to report on"
932
+ },
933
+ status: {
934
+ type: "string",
935
+ description: "Whether the fix was applied or can't be fixed",
936
+ enum: ["fixed", "wont_fix"]
937
+ },
938
+ summary: {
939
+ type: "string",
940
+ description: "Brief description of what was done or why it can't be fixed"
941
+ }
942
+ },
943
+ required: ["finding_id", "status", "summary"]
944
+ },
945
+ async handler(client, args) {
946
+ const { finding_id, status, summary } = args;
947
+ if (!isNonEmptyString(finding_id)) {
948
+ return { content: [{ type: "text", text: "finding_id is required." }], isError: true };
949
+ }
950
+ if (!isValidAiFixStatus(status)) {
951
+ return { content: [{ type: "text", text: "status must be 'fixed' or 'wont_fix'." }], isError: true };
952
+ }
953
+ if (!isNonEmptyString(summary)) {
954
+ return { content: [{ type: "text", text: "summary is required." }], isError: true };
955
+ }
956
+ const ok = await client.reportFix(finding_id, status, summary);
957
+ if (!ok) {
958
+ return {
959
+ content: [{ type: "text", text: `Finding ${finding_id} not found. It may have already been resolved.` }],
960
+ isError: true
961
+ };
962
+ }
963
+ const label = status === "fixed" ? "marked as fixed (awaiting verification)" : "marked as won't fix";
964
+ return {
965
+ content: [{ type: "text", text: `Finding ${finding_id} ${label}. Dashboard updated.` }]
966
+ };
967
+ }
968
+ };
969
+ }
970
+ });
971
+
818
972
  // src/mcp/tools/index.ts
819
973
  function getToolDefinitions() {
820
974
  return [...TOOL_MAP.values()].map((t) => ({
@@ -843,17 +997,19 @@ var init_tools = __esm({
843
997
  init_verify_fix();
844
998
  init_get_report();
845
999
  init_clear_findings();
1000
+ init_report_fix();
846
1001
  TOOL_MAP = new Map(
847
- [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings].map((t) => [t.name, t])
1002
+ [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings, reportFix].map((t) => [t.name, t])
848
1003
  );
849
1004
  }
850
1005
  });
851
1006
 
852
1007
  // src/mcp/prompts.ts
853
- var PROMPTS, PROMPT_MESSAGES;
1008
+ var SERVER_INSTRUCTIONS, PROMPTS, PROMPT_MESSAGES;
854
1009
  var init_prompts = __esm({
855
1010
  "src/mcp/prompts.ts"() {
856
1011
  "use strict";
1012
+ 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.";
857
1013
  PROMPTS = [
858
1014
  {
859
1015
  name: "check-app",
@@ -865,18 +1021,8 @@ var init_prompts = __esm({
865
1021
  }
866
1022
  ];
867
1023
  PROMPT_MESSAGES = {
868
- "check-app": [
869
- "Check my running app for security and performance issues using brakit.",
870
- "First get all findings, then get the endpoint summary.",
871
- "For any critical or warning findings, get the request detail to understand the root cause.",
872
- "Give me a clear report of what's wrong and offer to fix each issue."
873
- ].join(" "),
874
- "fix-findings": [
875
- "Get all open brakit findings.",
876
- "For each finding, get the request detail to understand the exact issue.",
877
- "Then find the source code responsible and fix it.",
878
- "After fixing, ask me to re-trigger the endpoint so you can verify the fix with brakit."
879
- ].join(" ")
1024
+ "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.",
1025
+ "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."
880
1026
  };
881
1027
  }
882
1028
  });
@@ -904,7 +1050,7 @@ async function startMcpServer() {
904
1050
  let cachedClient = discovery ? new BrakitClient(discovery.baseUrl) : null;
905
1051
  const server = new Server(
906
1052
  { name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
907
- { capabilities: { tools: {}, prompts: {} } }
1053
+ { capabilities: { tools: {}, prompts: {} }, instructions: SERVER_INSTRUCTIONS }
908
1054
  );
909
1055
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
910
1056
  prompts: [...PROMPTS]
@@ -982,27 +1128,19 @@ import { runMain } from "citty";
982
1128
 
983
1129
  // src/cli/commands/install.ts
984
1130
  import { defineCommand } from "citty";
985
- import { resolve as resolve3, join as join2 } from "path";
986
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1131
+ import { resolve as resolve3, join as join2, dirname } from "path";
1132
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
987
1133
  import { execSync } from "child_process";
1134
+ import { existsSync as existsSync5 } from "fs";
988
1135
  import pc from "picocolors";
989
1136
 
990
1137
  // src/store/finding-store.ts
991
- init_constants();
1138
+ import { readFile as readFile2 } from "fs/promises";
992
1139
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
993
1140
  import { resolve as resolve2 } from "path";
994
1141
 
995
- // src/utils/atomic-writer.ts
996
- import {
997
- writeFileSync as writeFileSync2,
998
- existsSync as existsSync2,
999
- mkdirSync as mkdirSync2,
1000
- renameSync
1001
- } from "fs";
1002
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
1003
-
1004
1142
  // src/utils/fs.ts
1005
- import { access } from "fs/promises";
1143
+ import { access, readFile, writeFile } from "fs/promises";
1006
1144
  import { existsSync, readFileSync, writeFileSync } from "fs";
1007
1145
  import { resolve } from "path";
1008
1146
  async function fileExists(path) {
@@ -1015,11 +1153,28 @@ async function fileExists(path) {
1015
1153
  }
1016
1154
 
1017
1155
  // src/store/finding-store.ts
1156
+ init_constants();
1157
+ init_limits();
1158
+
1159
+ // src/utils/atomic-writer.ts
1160
+ import {
1161
+ writeFileSync as writeFileSync2,
1162
+ existsSync as existsSync2,
1163
+ mkdirSync as mkdirSync2,
1164
+ renameSync
1165
+ } from "fs";
1166
+ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
1167
+ init_log();
1168
+ init_type_guards();
1169
+
1170
+ // src/store/finding-store.ts
1171
+ init_log();
1018
1172
  init_finding_id();
1019
1173
 
1020
1174
  // src/detect/project.ts
1021
- import { readFile as readFile2 } from "fs/promises";
1022
- import { join } from "path";
1175
+ import { readFile as readFile3, readdir } from "fs/promises";
1176
+ import { existsSync as existsSync4 } from "fs";
1177
+ import { join, relative } from "path";
1023
1178
  var FRAMEWORKS = [
1024
1179
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
1025
1180
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
@@ -1029,22 +1184,14 @@ var FRAMEWORKS = [
1029
1184
  ];
1030
1185
  async function detectProject(rootDir) {
1031
1186
  const pkgPath = join(rootDir, "package.json");
1032
- const raw = await readFile2(pkgPath, "utf-8");
1187
+ const raw = await readFile3(pkgPath, "utf-8");
1033
1188
  const pkg = JSON.parse(raw);
1034
1189
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1035
- let framework = "unknown";
1036
- let devCommand = "";
1037
- let devBin = "";
1038
- let defaultPort = 3e3;
1039
- for (const f of FRAMEWORKS) {
1040
- if (allDeps[f.dep]) {
1041
- framework = f.name;
1042
- devCommand = f.devCmd;
1043
- devBin = join(rootDir, "node_modules", ".bin", f.bin);
1044
- defaultPort = f.defaultPort;
1045
- break;
1046
- }
1047
- }
1190
+ const framework = detectFrameworkFromDeps(allDeps);
1191
+ const matched = FRAMEWORKS.find((f) => f.name === framework);
1192
+ const devCommand = matched?.devCmd ?? "";
1193
+ const devBin = matched ? join(rootDir, "node_modules", ".bin", matched.bin) : "";
1194
+ const defaultPort = matched?.defaultPort ?? 3e3;
1048
1195
  const packageManager = await detectPackageManager(rootDir);
1049
1196
  return { framework, devCommand, devBin, defaultPort, packageManager };
1050
1197
  }
@@ -1056,16 +1203,149 @@ async function detectPackageManager(rootDir) {
1056
1203
  if (await fileExists(join(rootDir, "package-lock.json"))) return "npm";
1057
1204
  return "unknown";
1058
1205
  }
1206
+ function detectFrameworkFromDeps(allDeps) {
1207
+ for (const f of FRAMEWORKS) {
1208
+ if (allDeps[f.dep]) return f.name;
1209
+ }
1210
+ return "unknown";
1211
+ }
1212
+ var PYTHON_ENTRY_CANDIDATES = [
1213
+ "app.py",
1214
+ "main.py",
1215
+ "wsgi.py",
1216
+ "asgi.py",
1217
+ "server.py",
1218
+ "run.py",
1219
+ "manage.py",
1220
+ "app/__init__.py"
1221
+ ];
1222
+ var PYTHON_FRAMEWORK_MAP = {
1223
+ flask: "flask",
1224
+ fastapi: "fastapi",
1225
+ django: "django"
1226
+ };
1227
+ var PYTHON_DEFAULT_PORTS = {
1228
+ flask: 5e3,
1229
+ fastapi: 8e3,
1230
+ django: 8e3,
1231
+ unknown: 8e3
1232
+ };
1233
+ async function detectPythonProject(rootDir) {
1234
+ const hasPyproject = await fileExists(join(rootDir, "pyproject.toml"));
1235
+ const hasRequirements = await fileExists(join(rootDir, "requirements.txt"));
1236
+ const hasSetupPy = await fileExists(join(rootDir, "setup.py"));
1237
+ if (!hasPyproject && !hasRequirements && !hasSetupPy) return null;
1238
+ const framework = await detectPythonFramework(rootDir, hasPyproject, hasRequirements);
1239
+ const packageManager = await detectPythonPackageManager(rootDir);
1240
+ const entryFile = await detectPythonEntry(rootDir);
1241
+ return {
1242
+ framework,
1243
+ packageManager,
1244
+ entryFile,
1245
+ defaultPort: PYTHON_DEFAULT_PORTS[framework] ?? 8e3
1246
+ };
1247
+ }
1248
+ async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
1249
+ if (hasPyproject) {
1250
+ try {
1251
+ const content = await readFile3(join(rootDir, "pyproject.toml"), "utf-8");
1252
+ for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
1253
+ if (content.includes(`"${dep}"`) || content.includes(`'${dep}'`) || content.includes(`${dep} `)) {
1254
+ return fw;
1255
+ }
1256
+ }
1257
+ } catch {
1258
+ }
1259
+ }
1260
+ if (hasRequirements) {
1261
+ try {
1262
+ const content = await readFile3(join(rootDir, "requirements.txt"), "utf-8");
1263
+ const lines = content.toLowerCase().split("\n");
1264
+ for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
1265
+ if (lines.some((l) => l.startsWith(dep) && (l.length === dep.length || /[=<>~![]/u.test(l[dep.length])))) {
1266
+ return fw;
1267
+ }
1268
+ }
1269
+ } catch {
1270
+ }
1271
+ }
1272
+ return "unknown";
1273
+ }
1274
+ async function detectPythonPackageManager(rootDir) {
1275
+ if (await fileExists(join(rootDir, "uv.lock"))) return "uv";
1276
+ if (await fileExists(join(rootDir, "poetry.lock"))) return "poetry";
1277
+ if (await fileExists(join(rootDir, "Pipfile.lock"))) return "pipenv";
1278
+ if (await fileExists(join(rootDir, "Pipfile"))) return "pipenv";
1279
+ if (await fileExists(join(rootDir, "requirements.txt"))) return "pip";
1280
+ try {
1281
+ const content = await readFile3(join(rootDir, "pyproject.toml"), "utf-8");
1282
+ if (content.includes("[tool.poetry]")) return "poetry";
1283
+ if (content.includes("[tool.uv]")) return "uv";
1284
+ } catch {
1285
+ }
1286
+ return "unknown";
1287
+ }
1288
+ async function detectPythonEntry(rootDir) {
1289
+ for (const candidate of PYTHON_ENTRY_CANDIDATES) {
1290
+ if (await fileExists(join(rootDir, candidate))) {
1291
+ return candidate;
1292
+ }
1293
+ }
1294
+ return null;
1295
+ }
1296
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1297
+ "node_modules",
1298
+ ".git",
1299
+ ".brakit",
1300
+ "dist",
1301
+ "build",
1302
+ "__pycache__",
1303
+ ".venv",
1304
+ "venv",
1305
+ ".next",
1306
+ ".nuxt",
1307
+ ".output",
1308
+ ".cache",
1309
+ "coverage"
1310
+ ]);
1311
+ async function scanForProjects(rootDir) {
1312
+ const projects = [];
1313
+ await detectInDir(rootDir, rootDir, projects);
1314
+ try {
1315
+ const entries = await readdir(rootDir, { withFileTypes: true });
1316
+ for (const entry of entries) {
1317
+ if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
1318
+ const childDir = join(rootDir, entry.name);
1319
+ await detectInDir(childDir, rootDir, projects);
1320
+ }
1321
+ } catch {
1322
+ }
1323
+ return projects;
1324
+ }
1325
+ async function detectInDir(dir, rootDir, projects) {
1326
+ const rel = dir === rootDir ? "." : `./${relative(rootDir, dir)}`;
1327
+ if (await fileExists(join(dir, "package.json"))) {
1328
+ try {
1329
+ const node = await detectProject(dir);
1330
+ projects.push({ dir, relDir: rel, type: "node", node });
1331
+ } catch {
1332
+ }
1333
+ }
1334
+ const python = await detectPythonProject(dir);
1335
+ if (python) {
1336
+ projects.push({ dir, relDir: rel, type: "python", python });
1337
+ }
1338
+ }
1059
1339
 
1060
1340
  // src/analysis/rules/patterns.ts
1061
1341
  var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
1062
1342
  var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
1063
1343
  var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
1064
- var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections/;
1344
+ var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections|Traceback \(most recent call last\)|File ".+", line \d+/;
1065
1345
  var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
1066
1346
  var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
1067
- var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/;
1068
- var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/i;
1347
+ var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
1348
+ var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
1069
1349
  var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
1070
1350
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
1071
1351
  var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
@@ -1075,9 +1355,9 @@ var RULE_HINTS = {
1075
1355
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
1076
1356
  "stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
1077
1357
  "error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
1358
+ "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
1078
1359
  "sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
1079
1360
  "cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
1080
- "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
1081
1361
  "response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
1082
1362
  };
1083
1363
 
@@ -1448,48 +1728,47 @@ function hasInternalIds(obj) {
1448
1728
  }
1449
1729
  return false;
1450
1730
  }
1451
- function detectPII(method, reqBody, resBody) {
1452
- const target = unwrapResponse(resBody);
1453
- if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
1454
- const reqEmails = findEmails(reqBody);
1455
- if (reqEmails.length > 0) {
1456
- const resEmails = findEmails(target);
1457
- const echoed = reqEmails.filter((e) => resEmails.includes(e));
1458
- if (echoed.length > 0) {
1459
- const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
1460
- if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
1461
- return { reason: "echo", emailCount: echoed.length };
1462
- }
1463
- }
1464
- }
1731
+ function detectEchoPII(method, reqBody, target) {
1732
+ if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
1733
+ const reqEmails = findEmails(reqBody);
1734
+ if (reqEmails.length === 0) return null;
1735
+ const resEmails = findEmails(target);
1736
+ const echoed = reqEmails.filter((e) => resEmails.includes(e));
1737
+ if (echoed.length === 0) return null;
1738
+ const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
1739
+ if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
1740
+ return { reason: "echo", emailCount: echoed.length };
1465
1741
  }
1466
- if (target && typeof target === "object" && !Array.isArray(target)) {
1467
- const fields = topLevelFieldCount(target);
1468
- if (fields >= FULL_RECORD_MIN_FIELDS && hasInternalIds(target)) {
1469
- const emails = findEmails(target);
1470
- if (emails.length > 0) {
1471
- return { reason: "full-record", emailCount: emails.length };
1472
- }
1742
+ return null;
1743
+ }
1744
+ function detectFullRecordPII(target) {
1745
+ if (!target || typeof target !== "object" || Array.isArray(target)) return null;
1746
+ const fields = topLevelFieldCount(target);
1747
+ if (fields < FULL_RECORD_MIN_FIELDS || !hasInternalIds(target)) return null;
1748
+ const emails = findEmails(target);
1749
+ if (emails.length === 0) return null;
1750
+ return { reason: "full-record", emailCount: emails.length };
1751
+ }
1752
+ function detectListPII(target) {
1753
+ if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
1754
+ let itemsWithEmail = 0;
1755
+ for (let i = 0; i < Math.min(target.length, 10); i++) {
1756
+ const item = target[i];
1757
+ if (item && typeof item === "object" && findEmails(item).length > 0) {
1758
+ itemsWithEmail++;
1473
1759
  }
1474
1760
  }
1475
- if (Array.isArray(target) && target.length >= LIST_PII_MIN_ITEMS) {
1476
- let itemsWithEmail = 0;
1477
- for (let i = 0; i < Math.min(target.length, 10); i++) {
1478
- const item = target[i];
1479
- if (item && typeof item === "object") {
1480
- const emails = findEmails(item);
1481
- if (emails.length > 0) itemsWithEmail++;
1482
- }
1483
- }
1484
- if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
1485
- const first = target[0];
1486
- if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
1487
- return { reason: "list-pii", emailCount: itemsWithEmail };
1488
- }
1489
- }
1761
+ if (itemsWithEmail < LIST_PII_MIN_ITEMS) return null;
1762
+ const first = target[0];
1763
+ if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
1764
+ return { reason: "list-pii", emailCount: itemsWithEmail };
1490
1765
  }
1491
1766
  return null;
1492
1767
  }
1768
+ function detectPII(method, reqBody, resBody) {
1769
+ const target = unwrapResponse(resBody);
1770
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
1771
+ }
1493
1772
  var REASON_LABELS = {
1494
1773
  echo: "echoes back PII from the request body",
1495
1774
  "full-record": "returns a full record with email and internal IDs",
@@ -1533,6 +1812,9 @@ var responsePiiLeakRule = {
1533
1812
  }
1534
1813
  };
1535
1814
 
1815
+ // src/analysis/engine.ts
1816
+ init_limits();
1817
+
1536
1818
  // src/analysis/group.ts
1537
1819
  init_constants();
1538
1820
  import { randomUUID } from "crypto";
@@ -1590,14 +1872,60 @@ init_constants();
1590
1872
 
1591
1873
  // src/analysis/insight-tracker.ts
1592
1874
  init_endpoint();
1875
+ init_finding_id();
1593
1876
  init_thresholds();
1594
1877
 
1595
1878
  // src/index.ts
1596
- var VERSION = "0.8.3";
1879
+ var VERSION = "0.8.5";
1597
1880
 
1598
1881
  // src/cli/commands/install.ts
1882
+ init_constants();
1883
+
1884
+ // src/cli/templates.ts
1599
1885
  var IMPORT_LINE = `import "brakit";`;
1600
1886
  var IMPORT_MARKER = "brakit";
1887
+ var CREATED_FILES = [
1888
+ "src/instrumentation.ts",
1889
+ "instrumentation.ts",
1890
+ "server/plugins/brakit.ts"
1891
+ ];
1892
+ var ENTRY_CANDIDATES = [
1893
+ "src/index.ts",
1894
+ "src/server.ts",
1895
+ "src/app.ts",
1896
+ "src/index.js",
1897
+ "src/server.js",
1898
+ "src/app.js",
1899
+ "server.ts",
1900
+ "app.ts",
1901
+ "index.ts",
1902
+ "server.js",
1903
+ "app.js",
1904
+ "index.js"
1905
+ ];
1906
+ var BRAKIT_TEMPLATES = {
1907
+ nextjs: [
1908
+ `export async function register() {`,
1909
+ ` if (process.env.NODE_ENV !== "production") {`,
1910
+ ` try { await import("brakit"); } catch {}`,
1911
+ ` }`,
1912
+ `}`
1913
+ ].join("\n"),
1914
+ nuxt: `import "brakit";`
1915
+ };
1916
+ var ALL_TEMPLATES = Object.values(BRAKIT_TEMPLATES);
1917
+ function normalize(content) {
1918
+ return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
1919
+ }
1920
+ function isExactBrakitTemplate(fileContent) {
1921
+ const normalizedFile = normalize(fileContent);
1922
+ if (!normalizedFile) return false;
1923
+ return ALL_TEMPLATES.some(
1924
+ (template) => normalize(template) === normalizedFile
1925
+ );
1926
+ }
1927
+
1928
+ // src/cli/commands/install.ts
1601
1929
  var install_default = defineCommand({
1602
1930
  meta: {
1603
1931
  name: "brakit install",
@@ -1614,68 +1942,77 @@ var install_default = defineCommand({
1614
1942
  },
1615
1943
  async run({ args }) {
1616
1944
  const rootDir = resolve3(args.dir);
1617
- const pkgPath = join2(rootDir, "package.json");
1618
- if (!await fileExists(pkgPath)) {
1619
- console.error(pc.red(" No project found. Run this from your project directory."));
1620
- process.exit(1);
1621
- }
1622
- let pkg;
1623
- try {
1624
- pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
1625
- } catch {
1626
- console.error(pc.red(" Failed to read package.json."));
1627
- process.exit(1);
1628
- }
1629
- if (!pkg.name || typeof pkg.name !== "string") {
1630
- console.error(pc.red(" No project found. Run this from your project directory."));
1631
- process.exit(1);
1632
- }
1633
- let project;
1634
- try {
1635
- project = await detectProject(rootDir);
1636
- } catch {
1637
- console.error(pc.red(" Failed to read package.json."));
1638
- process.exit(1);
1639
- }
1640
1945
  console.log();
1641
1946
  console.log(pc.bold(" \u25C6 brakit install"));
1642
1947
  console.log();
1643
- const installed = await installPackage(rootDir, project.packageManager);
1644
- if (installed) {
1645
- console.log(pc.green(" \u2713 Added brakit to devDependencies"));
1646
- } else {
1647
- console.log(pc.dim(" \u2713 brakit already in dependencies"));
1648
- }
1649
- const result = await setupInstrumentation(rootDir, project.framework);
1650
- if (result.action === "created") {
1651
- console.log(pc.green(` \u2713 Created ${result.file}`));
1652
- if (result.content) {
1948
+ const projects = await scanForProjects(rootDir);
1949
+ const nodeProjects = projects.filter((p) => p.type === "node");
1950
+ const pythonProjects = projects.filter((p) => p.type === "python");
1951
+ if (nodeProjects.length === 0) {
1952
+ if (pythonProjects.length > 0) {
1953
+ console.log(pc.dim(" Python project detected. To add brakit:"));
1653
1954
  console.log();
1654
- for (const line of result.content.split("\n")) {
1655
- console.log(pc.dim(` ${line}`));
1656
- }
1955
+ console.log(pc.bold(" pip install brakit"));
1956
+ console.log(pc.dim(" Then add to the top of your entry file:"));
1957
+ console.log(pc.bold(" import brakit # noqa: F401"));
1958
+ console.log();
1959
+ } else {
1960
+ console.error(pc.red(" No project found. Run this from your project directory."));
1657
1961
  }
1658
- } else if (result.action === "prepended") {
1659
- console.log(pc.green(` \u2713 Added import to ${result.file}`));
1660
- } else if (result.action === "exists") {
1661
- console.log(pc.dim(` \u2713 ${result.file} already has brakit import`));
1662
- } else {
1663
- printManualInstructions(project.framework);
1962
+ process.exit(1);
1664
1963
  }
1964
+ for (const p of nodeProjects) {
1965
+ const node = p.node;
1966
+ const suffix = p.relDir === "." ? "" : ` in ${p.relDir}`;
1967
+ const installed = await installPackage(p.dir, node.packageManager);
1968
+ if (installed) {
1969
+ console.log(pc.green(` \u2713 Added brakit to devDependencies${suffix}`));
1970
+ } else {
1971
+ console.log(pc.dim(` \u2713 brakit already in dependencies${suffix}`));
1972
+ }
1973
+ const result = await setupInstrumentation(p.dir, node.framework);
1974
+ const prefix = p.relDir === "." ? "" : `${p.relDir}/`;
1975
+ if (result.action === "created") {
1976
+ console.log(pc.green(` \u2713 Created ${prefix}${result.file}`));
1977
+ } else if (result.action === "prepended") {
1978
+ console.log(pc.green(` \u2713 Added import to ${prefix}${result.file}`));
1979
+ } else if (result.action === "exists") {
1980
+ console.log(pc.dim(` \u2713 ${prefix}${result.file} already has brakit import`));
1981
+ } else {
1982
+ printManualInstructions(node.framework);
1983
+ }
1984
+ }
1985
+ await ensureGitignoreEntry(rootDir, METRICS_DIR);
1665
1986
  const mcpResult = await setupMcp(rootDir);
1666
1987
  if (mcpResult === "created" || mcpResult === "updated") {
1667
1988
  console.log(pc.green(" \u2713 Configured MCP for Claude Code / Cursor"));
1668
1989
  } else if (mcpResult === "exists") {
1669
1990
  console.log(pc.dim(" \u2713 MCP already configured"));
1670
1991
  }
1992
+ const gitRoot = findGitRoot(rootDir);
1993
+ if (gitRoot && gitRoot !== rootDir) {
1994
+ const parentMcpResult = await setupMcp(gitRoot);
1995
+ if (parentMcpResult === "created" || parentMcpResult === "updated") {
1996
+ console.log(pc.green(" \u2713 Configured MCP at project root"));
1997
+ }
1998
+ }
1671
1999
  console.log();
2000
+ const port = nodeProjects[0].node?.defaultPort ?? 3e3;
1672
2001
  console.log(pc.dim(" Start your app and visit:"));
1673
- console.log(pc.bold(" http://localhost:<port>/__brakit"));
2002
+ console.log(pc.bold(` http://localhost:${port}/__brakit`));
2003
+ if (pythonProjects.length > 0) {
2004
+ const pyLabel = pythonProjects.map((p) => p.relDir).join(", ");
2005
+ console.log();
2006
+ console.log(pc.dim(` Python backend detected (${pyLabel}). To capture telemetry:`));
2007
+ console.log(pc.bold(" pip install brakit"));
2008
+ console.log(pc.dim(" Then add to the top of your entry file:"));
2009
+ console.log(pc.bold(" import brakit # noqa: F401"));
2010
+ }
1674
2011
  console.log();
1675
2012
  }
1676
2013
  });
1677
2014
  async function installPackage(rootDir, pm) {
1678
- const pkgRaw = await readFile3(join2(rootDir, "package.json"), "utf-8");
2015
+ const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
1679
2016
  const pkg = JSON.parse(pkgRaw);
1680
2017
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1681
2018
  if (allDeps["brakit"]) return false;
@@ -1690,6 +2027,7 @@ async function installPackage(rootDir, pm) {
1690
2027
  execSync(cmd, { cwd: rootDir, stdio: "pipe" });
1691
2028
  } catch {
1692
2029
  console.warn(pc.yellow(` \u26A0 Failed to run "${cmd}". Install brakit manually.`));
2030
+ return false;
1693
2031
  }
1694
2032
  return true;
1695
2033
  }
@@ -1710,20 +2048,13 @@ async function setupNextjs(rootDir) {
1710
2048
  const relPath = hasSrc ? "src/instrumentation.ts" : "instrumentation.ts";
1711
2049
  const absPath = join2(rootDir, relPath);
1712
2050
  if (await fileExists(absPath)) {
1713
- const content2 = await readFile3(absPath, "utf-8");
2051
+ const content2 = await readFile4(absPath, "utf-8");
1714
2052
  if (content2.includes(IMPORT_MARKER)) {
1715
2053
  return { action: "exists", file: relPath };
1716
2054
  }
1717
2055
  return { action: "manual", file: relPath };
1718
2056
  }
1719
- const content = [
1720
- `export async function register() {`,
1721
- ` if (process.env.NODE_ENV !== "production") {`,
1722
- ` try { await import("brakit"); } catch {}`,
1723
- ` }`,
1724
- `}`,
1725
- ``
1726
- ].join("\n");
2057
+ const content = BRAKIT_TEMPLATES.nextjs + "\n";
1727
2058
  await writeFile3(absPath, content);
1728
2059
  return { action: "created", file: relPath, content };
1729
2060
  }
@@ -1731,14 +2062,13 @@ async function setupNuxt(rootDir) {
1731
2062
  const relPath = "server/plugins/brakit.ts";
1732
2063
  const absPath = join2(rootDir, relPath);
1733
2064
  if (await fileExists(absPath)) {
1734
- const content2 = await readFile3(absPath, "utf-8");
2065
+ const content2 = await readFile4(absPath, "utf-8");
1735
2066
  if (content2.includes(IMPORT_MARKER)) {
1736
2067
  return { action: "exists", file: relPath };
1737
2068
  }
1738
2069
  return { action: "manual", file: relPath };
1739
2070
  }
1740
- const content = `${IMPORT_LINE}
1741
- `;
2071
+ const content = BRAKIT_TEMPLATES.nuxt + "\n";
1742
2072
  const dir = join2(rootDir, "server/plugins");
1743
2073
  const { mkdirSync: mkdirSync3 } = await import("fs");
1744
2074
  mkdirSync3(dir, { recursive: true });
@@ -1749,7 +2079,7 @@ async function setupPrepend(rootDir, ...candidates) {
1749
2079
  for (const relPath of candidates) {
1750
2080
  const absPath = join2(rootDir, relPath);
1751
2081
  if (!await fileExists(absPath)) continue;
1752
- const content = await readFile3(absPath, "utf-8");
2082
+ const content = await readFile4(absPath, "utf-8");
1753
2083
  if (content.includes(IMPORT_MARKER)) {
1754
2084
  return { action: "exists", file: relPath };
1755
2085
  }
@@ -1759,23 +2089,9 @@ ${content}`);
1759
2089
  }
1760
2090
  return { action: "manual", file: null };
1761
2091
  }
1762
- var ENTRY_CANDIDATES = [
1763
- "src/index.ts",
1764
- "src/server.ts",
1765
- "src/app.ts",
1766
- "src/index.js",
1767
- "src/server.js",
1768
- "src/app.js",
1769
- "server.ts",
1770
- "app.ts",
1771
- "index.ts",
1772
- "server.js",
1773
- "app.js",
1774
- "index.js"
1775
- ];
1776
2092
  async function setupGeneric(rootDir) {
1777
2093
  try {
1778
- const pkgRaw = await readFile3(join2(rootDir, "package.json"), "utf-8");
2094
+ const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
1779
2095
  const pkg = JSON.parse(pkgRaw);
1780
2096
  if (pkg.main && typeof pkg.main === "string") {
1781
2097
  const result2 = await setupPrepend(rootDir, pkg.main);
@@ -1795,37 +2111,46 @@ var MCP_CONFIG = {
1795
2111
  }
1796
2112
  }
1797
2113
  };
1798
- async function setupMcp(rootDir) {
2114
+ async function setupMcp(rootDir, config = MCP_CONFIG) {
1799
2115
  const mcpPath = join2(rootDir, ".mcp.json");
1800
2116
  if (await fileExists(mcpPath)) {
1801
- const raw = await readFile3(mcpPath, "utf-8");
2117
+ const raw = await readFile4(mcpPath, "utf-8");
1802
2118
  try {
1803
- const config = JSON.parse(raw);
1804
- if (config?.mcpServers?.brakit) return "exists";
1805
- config.mcpServers = { ...config.mcpServers, ...MCP_CONFIG.mcpServers };
1806
- await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n");
1807
- await ensureGitignoreMcp(rootDir);
2119
+ const existing = JSON.parse(raw);
2120
+ if (existing?.mcpServers?.brakit) return "exists";
2121
+ existing.mcpServers = { ...existing.mcpServers, ...config.mcpServers };
2122
+ await writeFile3(mcpPath, JSON.stringify(existing, null, 2) + "\n");
2123
+ await ensureGitignoreEntry(rootDir, ".mcp.json");
1808
2124
  return "updated";
1809
2125
  } catch {
1810
2126
  }
1811
2127
  }
1812
- await writeFile3(mcpPath, JSON.stringify(MCP_CONFIG, null, 2) + "\n");
1813
- await ensureGitignoreMcp(rootDir);
2128
+ await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n");
2129
+ await ensureGitignoreEntry(rootDir, ".mcp.json");
1814
2130
  return "created";
1815
2131
  }
1816
- async function ensureGitignoreMcp(rootDir) {
2132
+ async function ensureGitignoreEntry(rootDir, entry) {
1817
2133
  const gitignorePath = join2(rootDir, ".gitignore");
1818
2134
  try {
1819
2135
  if (await fileExists(gitignorePath)) {
1820
- const content = await readFile3(gitignorePath, "utf-8");
1821
- if (content.split("\n").some((l) => l.trim() === ".mcp.json")) return;
1822
- await writeFile3(gitignorePath, content.trimEnd() + "\n.mcp.json\n");
2136
+ const content = await readFile4(gitignorePath, "utf-8");
2137
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
2138
+ await writeFile3(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
1823
2139
  } else {
1824
- await writeFile3(gitignorePath, ".mcp.json\n");
2140
+ await writeFile3(gitignorePath, entry + "\n");
1825
2141
  }
1826
2142
  } catch {
1827
2143
  }
1828
2144
  }
2145
+ function findGitRoot(startDir) {
2146
+ let dir = resolve3(startDir);
2147
+ while (true) {
2148
+ if (existsSync5(join2(dir, ".git"))) return dir;
2149
+ const parent = dirname(dir);
2150
+ if (parent === dir) return null;
2151
+ dir = parent;
2152
+ }
2153
+ }
1829
2154
  function printManualInstructions(framework) {
1830
2155
  console.log(pc.yellow(" \u26A0 Could not auto-detect entry file."));
1831
2156
  console.log();
@@ -1846,31 +2171,14 @@ function printManualInstructions(framework) {
1846
2171
  // src/cli/commands/uninstall.ts
1847
2172
  import { defineCommand as defineCommand2 } from "citty";
1848
2173
  import { resolve as resolve4, join as join3 } from "path";
1849
- import { readFile as readFile4, writeFile as writeFile4, unlink, rm } from "fs/promises";
2174
+ import { readFile as readFile5, writeFile as writeFile4, unlink, rm } from "fs/promises";
1850
2175
  import { execSync as execSync2 } from "child_process";
1851
2176
  import pc2 from "picocolors";
1852
2177
  init_constants();
1853
- var IMPORT_LINE2 = `import "brakit";`;
1854
- var CREATED_FILES = [
1855
- "src/instrumentation.ts",
1856
- "instrumentation.ts",
1857
- "server/plugins/brakit.ts"
1858
- ];
1859
2178
  var PREPENDED_FILES = [
1860
2179
  "app/entry.server.tsx",
1861
2180
  "app/entry.server.ts",
1862
- "src/index.ts",
1863
- "src/server.ts",
1864
- "src/app.ts",
1865
- "src/index.js",
1866
- "src/server.js",
1867
- "src/app.js",
1868
- "server.ts",
1869
- "app.ts",
1870
- "index.ts",
1871
- "server.js",
1872
- "app.js",
1873
- "index.js"
2181
+ ...ENTRY_CANDIDATES
1874
2182
  ];
1875
2183
  var uninstall_default = defineCommand2({
1876
2184
  meta: {
@@ -1900,21 +2208,29 @@ var uninstall_default = defineCommand2({
1900
2208
  for (const relPath of CREATED_FILES) {
1901
2209
  const absPath = join3(rootDir, relPath);
1902
2210
  if (!await fileExists(absPath)) continue;
1903
- const content = await readFile4(absPath, "utf-8");
2211
+ const content = await readFile5(absPath, "utf-8");
1904
2212
  if (!content.includes("brakit")) continue;
1905
- const lines = content.split("\n").filter((l) => l.trim().length > 0);
1906
- const allBrakit = lines.every((l) => l.includes("brakit") || l.includes("register") || l.includes("import") || l.includes("export") || l.includes("try") || l.includes("catch") || l.includes("process.env") || l.includes("{") || l.includes("}"));
1907
- if (allBrakit) {
2213
+ if (isExactBrakitTemplate(content)) {
1908
2214
  await unlink(absPath);
1909
2215
  console.log(pc2.green(` \u2713 Removed ${relPath}`));
1910
2216
  removed = true;
1911
2217
  break;
1912
2218
  }
2219
+ const lines = content.split("\n");
2220
+ const cleaned = lines.filter(
2221
+ (line) => !line.includes('import("brakit")') && !line.includes('import "brakit"')
2222
+ );
2223
+ if (cleaned.length < lines.length) {
2224
+ await writeFile4(absPath, cleaned.join("\n"));
2225
+ console.log(pc2.green(` \u2713 Removed brakit lines from ${relPath}`));
2226
+ removed = true;
2227
+ break;
2228
+ }
1913
2229
  }
1914
2230
  if (!removed) {
1915
2231
  const candidates = [...PREPENDED_FILES];
1916
2232
  try {
1917
- const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
2233
+ const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
1918
2234
  const pkg = JSON.parse(pkgRaw);
1919
2235
  if (pkg.main) candidates.unshift(pkg.main);
1920
2236
  } catch {
@@ -1922,9 +2238,9 @@ var uninstall_default = defineCommand2({
1922
2238
  for (const relPath of candidates) {
1923
2239
  const absPath = join3(rootDir, relPath);
1924
2240
  if (!await fileExists(absPath)) continue;
1925
- const content = await readFile4(absPath, "utf-8");
1926
- if (!content.includes(IMPORT_LINE2)) continue;
1927
- const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE2.trim()).join("\n");
2241
+ const content = await readFile5(absPath, "utf-8");
2242
+ if (!content.includes(IMPORT_LINE)) continue;
2243
+ const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
1928
2244
  await writeFile4(absPath, updated);
1929
2245
  console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
1930
2246
  removed = true;
@@ -1958,7 +2274,7 @@ async function removeMcpConfig(rootDir) {
1958
2274
  const mcpPath = join3(rootDir, ".mcp.json");
1959
2275
  if (!await fileExists(mcpPath)) return false;
1960
2276
  try {
1961
- const raw = await readFile4(mcpPath, "utf-8");
2277
+ const raw = await readFile5(mcpPath, "utf-8");
1962
2278
  const config = JSON.parse(raw);
1963
2279
  if (!config?.mcpServers?.brakit) return false;
1964
2280
  delete config.mcpServers.brakit;
@@ -1974,7 +2290,7 @@ async function removeMcpConfig(rootDir) {
1974
2290
  }
1975
2291
  async function uninstallPackage(rootDir, pm) {
1976
2292
  try {
1977
- const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
2293
+ const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
1978
2294
  const pkg = JSON.parse(pkgRaw);
1979
2295
  if (!pkg.devDependencies?.brakit && !pkg.dependencies?.brakit) return false;
1980
2296
  } catch {
@@ -2008,7 +2324,7 @@ async function cleanGitignore(rootDir) {
2008
2324
  const gitignorePath = join3(rootDir, ".gitignore");
2009
2325
  if (!await fileExists(gitignorePath)) return false;
2010
2326
  try {
2011
- const content = await readFile4(gitignorePath, "utf-8");
2327
+ const content = await readFile5(gitignorePath, "utf-8");
2012
2328
  const lines = content.split("\n");
2013
2329
  const filtered = lines.filter((line) => line.trim() !== METRICS_DIR);
2014
2330
  if (filtered.length === lines.length) return false;