brakit 0.8.4 → 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.4";
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,9 @@ 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
- };
118
133
  }
119
134
  });
120
135
 
@@ -125,6 +140,17 @@ var init_telemetry = __esm({
125
140
  }
126
141
  });
127
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"]);
151
+ }
152
+ });
153
+
128
154
  // src/constants/index.ts
129
155
  var init_constants = __esm({
130
156
  "src/constants/index.ts"() {
@@ -140,18 +166,52 @@ var init_constants = __esm({
140
166
  init_encoding();
141
167
  init_severity();
142
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();
143
202
  }
144
203
  });
145
204
 
146
205
  // src/store/finding-id.ts
147
206
  import { createHash } from "crypto";
148
- function computeFindingId(finding) {
149
- const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
150
- 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);
151
210
  }
152
211
  var init_finding_id = __esm({
153
212
  "src/store/finding-id.ts"() {
154
213
  "use strict";
214
+ init_limits();
155
215
  }
156
216
  });
157
217
 
@@ -221,6 +281,19 @@ var init_client = __esm({
221
281
  if (state) url.searchParams.set("state", state);
222
282
  return this.fetchJson(url);
223
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
+ }
224
297
  async clearAll() {
225
298
  const res = await fetch(`${this.baseUrl}${DASHBOARD_API_CLEAR}`, {
226
299
  method: "POST",
@@ -252,50 +325,56 @@ var init_client = __esm({
252
325
  });
253
326
 
254
327
  // src/mcp/discovery.ts
255
- import { readFileSync as readFileSync3, existsSync as existsSync5, readdirSync, statSync } from "fs";
256
- import { resolve as resolve5, dirname } from "path";
257
- function readPort(portPath) {
258
- if (!existsSync5(portPath)) return null;
259
- const raw = readFileSync3(portPath, "utf-8").trim();
260
- const port = parseInt(raw, 10);
261
- 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
+ }
262
338
  }
263
- function portInDir(dir) {
339
+ async function portInDir(dir) {
264
340
  return readPort(resolve5(dir, PORT_FILE));
265
341
  }
266
- function portInChildren(dir) {
342
+ async function portInChildren(dir) {
267
343
  try {
268
- for (const entry of readdirSync(dir)) {
344
+ const entries = await readdir2(dir);
345
+ for (const entry of entries) {
269
346
  if (entry.startsWith(".") || entry === "node_modules") continue;
270
347
  const child = resolve5(dir, entry);
271
348
  try {
272
- if (!statSync(child).isDirectory()) continue;
273
- } catch {
349
+ if (!(await stat(child)).isDirectory()) continue;
350
+ } catch (err) {
351
+ brakitDebug(`discovery: stat failed for ${child}: ${err}`);
274
352
  continue;
275
353
  }
276
- const port = portInDir(child);
354
+ const port = await portInDir(child);
277
355
  if (port) return port;
278
356
  }
279
- } catch {
357
+ } catch (err) {
358
+ brakitDebug(`discovery: readdir failed for ${dir}: ${err}`);
280
359
  }
281
360
  return null;
282
361
  }
283
- function searchForPort(startDir) {
362
+ async function searchForPort(startDir) {
284
363
  const start = resolve5(startDir);
285
- const initial = portInDir(start) ?? portInChildren(start);
364
+ const initial = await portInDir(start) ?? await portInChildren(start);
286
365
  if (initial) return initial;
287
- let dir = dirname(start);
366
+ let dir = dirname2(start);
288
367
  for (let depth = 0; depth < MAX_DISCOVERY_DEPTH; depth++) {
289
- const port = portInDir(dir);
368
+ const port = await portInDir(dir) ?? await portInChildren(dir);
290
369
  if (port) return port;
291
- const parent = dirname(dir);
370
+ const parent = dirname2(dir);
292
371
  if (parent === dir) break;
293
372
  dir = parent;
294
373
  }
295
374
  return null;
296
375
  }
297
- function discoverBrakitPort(cwd) {
298
- const port = searchForPort(cwd ?? process.cwd());
376
+ async function discoverBrakitPort(cwd) {
377
+ const port = await searchForPort(cwd ?? process.cwd());
299
378
  if (!port) {
300
379
  throw new Error(
301
380
  "Brakit is not running. Start your app with brakit enabled first."
@@ -307,7 +386,7 @@ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTER
307
386
  const deadline = Date.now() + timeoutMs;
308
387
  while (Date.now() < deadline) {
309
388
  try {
310
- const result = discoverBrakitPort(cwd);
389
+ const result = await discoverBrakitPort(cwd);
311
390
  const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
312
391
  if (res.ok) return result;
313
392
  } catch {
@@ -322,50 +401,52 @@ var init_discovery = __esm({
322
401
  "src/mcp/discovery.ts"() {
323
402
  "use strict";
324
403
  init_constants();
404
+ init_log();
325
405
  init_mcp();
326
406
  }
327
407
  });
328
408
 
329
409
  // src/mcp/enrichment.ts
330
- import { createHash as createHash2 } from "crypto";
331
- function computeInsightId(type, endpoint, desc) {
332
- const key = `${type}:${endpoint}:${desc}`;
333
- return createHash2("sha256").update(key).digest("hex").slice(0, 16);
334
- }
335
410
  async function enrichFindings(client) {
336
411
  const [securityData, insightsData] = await Promise.all([
337
412
  client.getSecurityFindings(),
338
413
  client.getInsights()
339
414
  ]);
340
- const enriched = [];
341
- for (const f of securityData.findings) {
342
- let context = "";
343
- try {
344
- const { path } = parseEndpointKey(f.endpoint);
345
- const reqData = await client.getRequests({ search: path, limit: 1 });
346
- if (reqData.requests.length > 0) {
347
- const req = reqData.requests[0];
348
- if (req.id) {
349
- const activity = await client.getActivity(req.id);
350
- const queryCount = activity.counts?.queries ?? 0;
351
- const fetchCount = activity.counts?.fetches ?? 0;
352
- 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
+ }
353
428
  }
429
+ } catch {
430
+ return "(context unavailable)";
354
431
  }
355
- } catch {
356
- context = "(context unavailable)";
357
- }
358
- enriched.push({
359
- 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,
360
439
  severity: f.severity,
361
440
  title: f.title,
362
441
  endpoint: f.endpoint,
363
442
  description: f.desc,
364
443
  hint: f.hint,
365
444
  occurrences: f.count,
366
- context
367
- });
368
- }
445
+ context: contexts[i],
446
+ aiStatus: sf.aiStatus,
447
+ aiNotes: sf.aiNotes
448
+ };
449
+ });
369
450
  for (const si of insightsData.insights) {
370
451
  if (si.state === "resolved") continue;
371
452
  const i = si.insight;
@@ -379,7 +460,9 @@ async function enrichFindings(client) {
379
460
  description: i.desc,
380
461
  hint: i.hint,
381
462
  occurrences: 1,
382
- context: i.detail ?? ""
463
+ context: i.detail ?? "",
464
+ aiStatus: si.aiStatus,
465
+ aiNotes: si.aiNotes
383
466
  });
384
467
  }
385
468
  return enriched;
@@ -441,13 +524,13 @@ var init_enrichment = __esm({
441
524
  });
442
525
 
443
526
  // src/mcp/tools/get-findings.ts
444
- var VALID_SEVERITIES, VALID_STATES, getFindings;
527
+ var getFindings;
445
528
  var init_get_findings = __esm({
446
529
  "src/mcp/tools/get-findings.ts"() {
447
530
  "use strict";
448
531
  init_enrichment();
449
- VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
450
- VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
532
+ init_lifecycle();
533
+ init_type_guards();
451
534
  getFindings = {
452
535
  name: "get_findings",
453
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.",
@@ -469,10 +552,10 @@ var init_get_findings = __esm({
469
552
  async handler(client, args) {
470
553
  const severity = args.severity;
471
554
  const state = args.state;
472
- if (severity && !VALID_SEVERITIES.has(severity)) {
555
+ if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
473
556
  return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
474
557
  }
475
- if (state && !VALID_STATES.has(state)) {
558
+ if (state && !isValidFindingState(state)) {
476
559
  return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
477
560
  }
478
561
  let findings = await enrichFindings(client);
@@ -491,10 +574,18 @@ var init_get_findings = __esm({
491
574
  `];
492
575
  for (const f of findings) {
493
576
  lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
577
+ lines.push(` ID: ${f.findingId}`);
494
578
  lines.push(` Endpoint: ${f.endpoint}`);
495
579
  lines.push(` Issue: ${f.description}`);
496
580
  if (f.context) lines.push(` Context: ${f.context}`);
497
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
+ }
498
589
  lines.push("");
499
590
  }
500
591
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -823,6 +914,61 @@ var init_clear_findings = __esm({
823
914
  }
824
915
  });
825
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
+
826
972
  // src/mcp/tools/index.ts
827
973
  function getToolDefinitions() {
828
974
  return [...TOOL_MAP.values()].map((t) => ({
@@ -851,17 +997,19 @@ var init_tools = __esm({
851
997
  init_verify_fix();
852
998
  init_get_report();
853
999
  init_clear_findings();
1000
+ init_report_fix();
854
1001
  TOOL_MAP = new Map(
855
- [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings].map((t) => [t.name, t])
1002
+ [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings, reportFix].map((t) => [t.name, t])
856
1003
  );
857
1004
  }
858
1005
  });
859
1006
 
860
1007
  // src/mcp/prompts.ts
861
- var PROMPTS, PROMPT_MESSAGES;
1008
+ var SERVER_INSTRUCTIONS, PROMPTS, PROMPT_MESSAGES;
862
1009
  var init_prompts = __esm({
863
1010
  "src/mcp/prompts.ts"() {
864
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.";
865
1013
  PROMPTS = [
866
1014
  {
867
1015
  name: "check-app",
@@ -873,18 +1021,8 @@ var init_prompts = __esm({
873
1021
  }
874
1022
  ];
875
1023
  PROMPT_MESSAGES = {
876
- "check-app": [
877
- "Check my running app for security and performance issues using brakit.",
878
- "First get all findings, then get the endpoint summary.",
879
- "For any critical or warning findings, get the request detail to understand the root cause.",
880
- "Give me a clear report of what's wrong and offer to fix each issue."
881
- ].join(" "),
882
- "fix-findings": [
883
- "Get all open brakit findings.",
884
- "For each finding, get the request detail to understand the exact issue.",
885
- "Then find the source code responsible and fix it.",
886
- "After fixing, ask me to re-trigger the endpoint so you can verify the fix with brakit."
887
- ].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."
888
1026
  };
889
1027
  }
890
1028
  });
@@ -912,7 +1050,7 @@ async function startMcpServer() {
912
1050
  let cachedClient = discovery ? new BrakitClient(discovery.baseUrl) : null;
913
1051
  const server = new Server(
914
1052
  { name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
915
- { capabilities: { tools: {}, prompts: {} } }
1053
+ { capabilities: { tools: {}, prompts: {} }, instructions: SERVER_INSTRUCTIONS }
916
1054
  );
917
1055
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
918
1056
  prompts: [...PROMPTS]
@@ -990,27 +1128,19 @@ import { runMain } from "citty";
990
1128
 
991
1129
  // src/cli/commands/install.ts
992
1130
  import { defineCommand } from "citty";
993
- import { resolve as resolve3, join as join2 } from "path";
994
- 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";
995
1133
  import { execSync } from "child_process";
1134
+ import { existsSync as existsSync5 } from "fs";
996
1135
  import pc from "picocolors";
997
1136
 
998
1137
  // src/store/finding-store.ts
999
- init_constants();
1138
+ import { readFile as readFile2 } from "fs/promises";
1000
1139
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
1001
1140
  import { resolve as resolve2 } from "path";
1002
1141
 
1003
- // src/utils/atomic-writer.ts
1004
- import {
1005
- writeFileSync as writeFileSync2,
1006
- existsSync as existsSync2,
1007
- mkdirSync as mkdirSync2,
1008
- renameSync
1009
- } from "fs";
1010
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
1011
-
1012
1142
  // src/utils/fs.ts
1013
- import { access } from "fs/promises";
1143
+ import { access, readFile, writeFile } from "fs/promises";
1014
1144
  import { existsSync, readFileSync, writeFileSync } from "fs";
1015
1145
  import { resolve } from "path";
1016
1146
  async function fileExists(path) {
@@ -1023,12 +1153,28 @@ async function fileExists(path) {
1023
1153
  }
1024
1154
 
1025
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();
1026
1172
  init_finding_id();
1027
1173
 
1028
1174
  // src/detect/project.ts
1029
- import { readFile as readFile2 } from "fs/promises";
1175
+ import { readFile as readFile3, readdir } from "fs/promises";
1030
1176
  import { existsSync as existsSync4 } from "fs";
1031
- import { join } from "path";
1177
+ import { join, relative } from "path";
1032
1178
  var FRAMEWORKS = [
1033
1179
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
1034
1180
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
@@ -1038,7 +1184,7 @@ var FRAMEWORKS = [
1038
1184
  ];
1039
1185
  async function detectProject(rootDir) {
1040
1186
  const pkgPath = join(rootDir, "package.json");
1041
- const raw = await readFile2(pkgPath, "utf-8");
1187
+ const raw = await readFile3(pkgPath, "utf-8");
1042
1188
  const pkg = JSON.parse(raw);
1043
1189
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1044
1190
  const framework = detectFrameworkFromDeps(allDeps);
@@ -1063,16 +1209,143 @@ function detectFrameworkFromDeps(allDeps) {
1063
1209
  }
1064
1210
  return "unknown";
1065
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
+ }
1066
1339
 
1067
1340
  // src/analysis/rules/patterns.ts
1068
1341
  var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
1069
1342
  var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
1070
1343
  var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
1071
- 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+/;
1072
1345
  var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
1073
1346
  var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
1074
- var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/;
1075
- 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;
1076
1349
  var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
1077
1350
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
1078
1351
  var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
@@ -1082,9 +1355,9 @@ var RULE_HINTS = {
1082
1355
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
1083
1356
  "stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
1084
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.",
1085
1359
  "sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
1086
1360
  "cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
1087
- "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
1088
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."
1089
1362
  };
1090
1363
 
@@ -1455,48 +1728,47 @@ function hasInternalIds(obj) {
1455
1728
  }
1456
1729
  return false;
1457
1730
  }
1458
- function detectPII(method, reqBody, resBody) {
1459
- const target = unwrapResponse(resBody);
1460
- if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
1461
- const reqEmails = findEmails(reqBody);
1462
- if (reqEmails.length > 0) {
1463
- const resEmails = findEmails(target);
1464
- const echoed = reqEmails.filter((e) => resEmails.includes(e));
1465
- if (echoed.length > 0) {
1466
- const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
1467
- if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
1468
- return { reason: "echo", emailCount: echoed.length };
1469
- }
1470
- }
1471
- }
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 };
1472
1741
  }
1473
- if (target && typeof target === "object" && !Array.isArray(target)) {
1474
- const fields = topLevelFieldCount(target);
1475
- if (fields >= FULL_RECORD_MIN_FIELDS && hasInternalIds(target)) {
1476
- const emails = findEmails(target);
1477
- if (emails.length > 0) {
1478
- return { reason: "full-record", emailCount: emails.length };
1479
- }
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++;
1480
1759
  }
1481
1760
  }
1482
- if (Array.isArray(target) && target.length >= LIST_PII_MIN_ITEMS) {
1483
- let itemsWithEmail = 0;
1484
- for (let i = 0; i < Math.min(target.length, 10); i++) {
1485
- const item = target[i];
1486
- if (item && typeof item === "object") {
1487
- const emails = findEmails(item);
1488
- if (emails.length > 0) itemsWithEmail++;
1489
- }
1490
- }
1491
- if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
1492
- const first = target[0];
1493
- if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
1494
- return { reason: "list-pii", emailCount: itemsWithEmail };
1495
- }
1496
- }
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 };
1497
1765
  }
1498
1766
  return null;
1499
1767
  }
1768
+ function detectPII(method, reqBody, resBody) {
1769
+ const target = unwrapResponse(resBody);
1770
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
1771
+ }
1500
1772
  var REASON_LABELS = {
1501
1773
  echo: "echoes back PII from the request body",
1502
1774
  "full-record": "returns a full record with email and internal IDs",
@@ -1540,6 +1812,9 @@ var responsePiiLeakRule = {
1540
1812
  }
1541
1813
  };
1542
1814
 
1815
+ // src/analysis/engine.ts
1816
+ init_limits();
1817
+
1543
1818
  // src/analysis/group.ts
1544
1819
  init_constants();
1545
1820
  import { randomUUID } from "crypto";
@@ -1597,10 +1872,11 @@ init_constants();
1597
1872
 
1598
1873
  // src/analysis/insight-tracker.ts
1599
1874
  init_endpoint();
1875
+ init_finding_id();
1600
1876
  init_thresholds();
1601
1877
 
1602
1878
  // src/index.ts
1603
- var VERSION = "0.8.4";
1879
+ var VERSION = "0.8.5";
1604
1880
 
1605
1881
  // src/cli/commands/install.ts
1606
1882
  init_constants();
@@ -1628,7 +1904,6 @@ var ENTRY_CANDIDATES = [
1628
1904
  "index.js"
1629
1905
  ];
1630
1906
  var BRAKIT_TEMPLATES = {
1631
- /** Next.js instrumentation.ts — standalone file created by install */
1632
1907
  nextjs: [
1633
1908
  `export async function register() {`,
1634
1909
  ` if (process.env.NODE_ENV !== "production") {`,
@@ -1636,7 +1911,6 @@ var BRAKIT_TEMPLATES = {
1636
1911
  ` }`,
1637
1912
  `}`
1638
1913
  ].join("\n"),
1639
- /** Nuxt server/plugins/brakit.ts — standalone file created by install */
1640
1914
  nuxt: `import "brakit";`
1641
1915
  };
1642
1916
  var ALL_TEMPLATES = Object.values(BRAKIT_TEMPLATES);
@@ -1668,53 +1942,45 @@ var install_default = defineCommand({
1668
1942
  },
1669
1943
  async run({ args }) {
1670
1944
  const rootDir = resolve3(args.dir);
1671
- const pkgPath = join2(rootDir, "package.json");
1672
- if (!await fileExists(pkgPath)) {
1673
- console.error(pc.red(" No project found. Run this from your project directory."));
1674
- process.exit(1);
1675
- }
1676
- let pkg;
1677
- try {
1678
- pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
1679
- } catch {
1680
- console.error(pc.red(" Failed to read package.json."));
1681
- process.exit(1);
1682
- }
1683
- if (!pkg.name || typeof pkg.name !== "string") {
1684
- console.error(pc.red(" No project found. Run this from your project directory."));
1685
- process.exit(1);
1686
- }
1687
- let project;
1688
- try {
1689
- project = await detectProject(rootDir);
1690
- } catch {
1691
- console.error(pc.red(" Failed to read package.json."));
1692
- process.exit(1);
1693
- }
1694
1945
  console.log();
1695
1946
  console.log(pc.bold(" \u25C6 brakit install"));
1696
1947
  console.log();
1697
- const installed = await installPackage(rootDir, project.packageManager);
1698
- if (installed) {
1699
- console.log(pc.green(" \u2713 Added brakit to devDependencies"));
1700
- } else {
1701
- console.log(pc.dim(" \u2713 brakit already in dependencies"));
1702
- }
1703
- const result = await setupInstrumentation(rootDir, project.framework);
1704
- if (result.action === "created") {
1705
- console.log(pc.green(` \u2713 Created ${result.file}`));
1706
- 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:"));
1707
1954
  console.log();
1708
- for (const line of result.content.split("\n")) {
1709
- console.log(pc.dim(` ${line}`));
1710
- }
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."));
1961
+ }
1962
+ process.exit(1);
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);
1711
1983
  }
1712
- } else if (result.action === "prepended") {
1713
- console.log(pc.green(` \u2713 Added import to ${result.file}`));
1714
- } else if (result.action === "exists") {
1715
- console.log(pc.dim(` \u2713 ${result.file} already has brakit import`));
1716
- } else {
1717
- printManualInstructions(project.framework);
1718
1984
  }
1719
1985
  await ensureGitignoreEntry(rootDir, METRICS_DIR);
1720
1986
  const mcpResult = await setupMcp(rootDir);
@@ -1723,14 +1989,30 @@ var install_default = defineCommand({
1723
1989
  } else if (mcpResult === "exists") {
1724
1990
  console.log(pc.dim(" \u2713 MCP already configured"));
1725
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
+ }
1726
1999
  console.log();
2000
+ const port = nodeProjects[0].node?.defaultPort ?? 3e3;
1727
2001
  console.log(pc.dim(" Start your app and visit:"));
1728
- 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
+ }
1729
2011
  console.log();
1730
2012
  }
1731
2013
  });
1732
2014
  async function installPackage(rootDir, pm) {
1733
- const pkgRaw = await readFile3(join2(rootDir, "package.json"), "utf-8");
2015
+ const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
1734
2016
  const pkg = JSON.parse(pkgRaw);
1735
2017
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1736
2018
  if (allDeps["brakit"]) return false;
@@ -1766,7 +2048,7 @@ async function setupNextjs(rootDir) {
1766
2048
  const relPath = hasSrc ? "src/instrumentation.ts" : "instrumentation.ts";
1767
2049
  const absPath = join2(rootDir, relPath);
1768
2050
  if (await fileExists(absPath)) {
1769
- const content2 = await readFile3(absPath, "utf-8");
2051
+ const content2 = await readFile4(absPath, "utf-8");
1770
2052
  if (content2.includes(IMPORT_MARKER)) {
1771
2053
  return { action: "exists", file: relPath };
1772
2054
  }
@@ -1780,7 +2062,7 @@ async function setupNuxt(rootDir) {
1780
2062
  const relPath = "server/plugins/brakit.ts";
1781
2063
  const absPath = join2(rootDir, relPath);
1782
2064
  if (await fileExists(absPath)) {
1783
- const content2 = await readFile3(absPath, "utf-8");
2065
+ const content2 = await readFile4(absPath, "utf-8");
1784
2066
  if (content2.includes(IMPORT_MARKER)) {
1785
2067
  return { action: "exists", file: relPath };
1786
2068
  }
@@ -1797,7 +2079,7 @@ async function setupPrepend(rootDir, ...candidates) {
1797
2079
  for (const relPath of candidates) {
1798
2080
  const absPath = join2(rootDir, relPath);
1799
2081
  if (!await fileExists(absPath)) continue;
1800
- const content = await readFile3(absPath, "utf-8");
2082
+ const content = await readFile4(absPath, "utf-8");
1801
2083
  if (content.includes(IMPORT_MARKER)) {
1802
2084
  return { action: "exists", file: relPath };
1803
2085
  }
@@ -1809,7 +2091,7 @@ ${content}`);
1809
2091
  }
1810
2092
  async function setupGeneric(rootDir) {
1811
2093
  try {
1812
- const pkgRaw = await readFile3(join2(rootDir, "package.json"), "utf-8");
2094
+ const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
1813
2095
  const pkg = JSON.parse(pkgRaw);
1814
2096
  if (pkg.main && typeof pkg.main === "string") {
1815
2097
  const result2 = await setupPrepend(rootDir, pkg.main);
@@ -1829,21 +2111,21 @@ var MCP_CONFIG = {
1829
2111
  }
1830
2112
  }
1831
2113
  };
1832
- async function setupMcp(rootDir) {
2114
+ async function setupMcp(rootDir, config = MCP_CONFIG) {
1833
2115
  const mcpPath = join2(rootDir, ".mcp.json");
1834
2116
  if (await fileExists(mcpPath)) {
1835
- const raw = await readFile3(mcpPath, "utf-8");
2117
+ const raw = await readFile4(mcpPath, "utf-8");
1836
2118
  try {
1837
- const config = JSON.parse(raw);
1838
- if (config?.mcpServers?.brakit) return "exists";
1839
- config.mcpServers = { ...config.mcpServers, ...MCP_CONFIG.mcpServers };
1840
- await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n");
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");
1841
2123
  await ensureGitignoreEntry(rootDir, ".mcp.json");
1842
2124
  return "updated";
1843
2125
  } catch {
1844
2126
  }
1845
2127
  }
1846
- await writeFile3(mcpPath, JSON.stringify(MCP_CONFIG, null, 2) + "\n");
2128
+ await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n");
1847
2129
  await ensureGitignoreEntry(rootDir, ".mcp.json");
1848
2130
  return "created";
1849
2131
  }
@@ -1851,7 +2133,7 @@ async function ensureGitignoreEntry(rootDir, entry) {
1851
2133
  const gitignorePath = join2(rootDir, ".gitignore");
1852
2134
  try {
1853
2135
  if (await fileExists(gitignorePath)) {
1854
- const content = await readFile3(gitignorePath, "utf-8");
2136
+ const content = await readFile4(gitignorePath, "utf-8");
1855
2137
  if (content.split("\n").some((l) => l.trim() === entry)) return;
1856
2138
  await writeFile3(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
1857
2139
  } else {
@@ -1860,6 +2142,15 @@ async function ensureGitignoreEntry(rootDir, entry) {
1860
2142
  } catch {
1861
2143
  }
1862
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
+ }
1863
2154
  function printManualInstructions(framework) {
1864
2155
  console.log(pc.yellow(" \u26A0 Could not auto-detect entry file."));
1865
2156
  console.log();
@@ -1880,7 +2171,7 @@ function printManualInstructions(framework) {
1880
2171
  // src/cli/commands/uninstall.ts
1881
2172
  import { defineCommand as defineCommand2 } from "citty";
1882
2173
  import { resolve as resolve4, join as join3 } from "path";
1883
- 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";
1884
2175
  import { execSync as execSync2 } from "child_process";
1885
2176
  import pc2 from "picocolors";
1886
2177
  init_constants();
@@ -1917,7 +2208,7 @@ var uninstall_default = defineCommand2({
1917
2208
  for (const relPath of CREATED_FILES) {
1918
2209
  const absPath = join3(rootDir, relPath);
1919
2210
  if (!await fileExists(absPath)) continue;
1920
- const content = await readFile4(absPath, "utf-8");
2211
+ const content = await readFile5(absPath, "utf-8");
1921
2212
  if (!content.includes("brakit")) continue;
1922
2213
  if (isExactBrakitTemplate(content)) {
1923
2214
  await unlink(absPath);
@@ -1939,7 +2230,7 @@ var uninstall_default = defineCommand2({
1939
2230
  if (!removed) {
1940
2231
  const candidates = [...PREPENDED_FILES];
1941
2232
  try {
1942
- const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
2233
+ const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
1943
2234
  const pkg = JSON.parse(pkgRaw);
1944
2235
  if (pkg.main) candidates.unshift(pkg.main);
1945
2236
  } catch {
@@ -1947,7 +2238,7 @@ var uninstall_default = defineCommand2({
1947
2238
  for (const relPath of candidates) {
1948
2239
  const absPath = join3(rootDir, relPath);
1949
2240
  if (!await fileExists(absPath)) continue;
1950
- const content = await readFile4(absPath, "utf-8");
2241
+ const content = await readFile5(absPath, "utf-8");
1951
2242
  if (!content.includes(IMPORT_LINE)) continue;
1952
2243
  const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
1953
2244
  await writeFile4(absPath, updated);
@@ -1983,7 +2274,7 @@ async function removeMcpConfig(rootDir) {
1983
2274
  const mcpPath = join3(rootDir, ".mcp.json");
1984
2275
  if (!await fileExists(mcpPath)) return false;
1985
2276
  try {
1986
- const raw = await readFile4(mcpPath, "utf-8");
2277
+ const raw = await readFile5(mcpPath, "utf-8");
1987
2278
  const config = JSON.parse(raw);
1988
2279
  if (!config?.mcpServers?.brakit) return false;
1989
2280
  delete config.mcpServers.brakit;
@@ -1999,7 +2290,7 @@ async function removeMcpConfig(rootDir) {
1999
2290
  }
2000
2291
  async function uninstallPackage(rootDir, pm) {
2001
2292
  try {
2002
- const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
2293
+ const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
2003
2294
  const pkg = JSON.parse(pkgRaw);
2004
2295
  if (!pkg.devDependencies?.brakit && !pkg.dependencies?.brakit) return false;
2005
2296
  } catch {
@@ -2033,7 +2324,7 @@ async function cleanGitignore(rootDir) {
2033
2324
  const gitignorePath = join3(rootDir, ".gitignore");
2034
2325
  if (!await fileExists(gitignorePath)) return false;
2035
2326
  try {
2036
- const content = await readFile4(gitignorePath, "utf-8");
2327
+ const content = await readFile5(gitignorePath, "utf-8");
2037
2328
  const lines = content.split("\n");
2038
2329
  const filtered = lines.filter((line) => line.trim() !== METRICS_DIR);
2039
2330
  if (filtered.length === lines.length) return false;