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.
@@ -9,20 +9,39 @@ import {
9
9
  } from "@modelcontextprotocol/sdk/types.js";
10
10
 
11
11
  // src/constants/routes.ts
12
- var DASHBOARD_API_REQUESTS = "/__brakit/api/requests";
13
- var DASHBOARD_API_CLEAR = "/__brakit/api/clear";
14
- var DASHBOARD_API_FETCHES = "/__brakit/api/fetches";
15
- var DASHBOARD_API_ERRORS = "/__brakit/api/errors";
16
- var DASHBOARD_API_QUERIES = "/__brakit/api/queries";
17
- var DASHBOARD_API_ACTIVITY = "/__brakit/api/activity";
18
- var DASHBOARD_API_METRICS_LIVE = "/__brakit/api/metrics/live";
19
- var DASHBOARD_API_INSIGHTS = "/__brakit/api/insights";
20
- var DASHBOARD_API_SECURITY = "/__brakit/api/security";
21
- var DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
12
+ var DASHBOARD_PREFIX = "/__brakit";
13
+ var DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
14
+ var DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
15
+ var DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
16
+ var DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
17
+ var DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
18
+ var DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
19
+ var DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
20
+ var DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
21
+ var DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
22
+ var DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
23
+ var DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
24
+ var DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
25
+ var DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
26
+ var DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
27
+ var DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
28
+ var DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
29
+ var DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
30
+ var VALID_TABS_TUPLE = [
31
+ "overview",
32
+ "actions",
33
+ "requests",
34
+ "fetches",
35
+ "queries",
36
+ "errors",
37
+ "logs",
38
+ "performance",
39
+ "security"
40
+ ];
41
+ var VALID_TABS = new Set(VALID_TABS_TUPLE);
22
42
 
23
43
  // src/constants/mcp.ts
24
44
  var MCP_SERVER_NAME = "brakit";
25
- var MCP_SERVER_VERSION = "0.8.4";
26
45
  var INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
27
46
  var LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
28
47
  var CLIENT_FETCH_TIMEOUT_MS = 1e4;
@@ -32,6 +51,7 @@ var MAX_DISCOVERY_DEPTH = 5;
32
51
  var MAX_TIMELINE_EVENTS = 20;
33
52
  var MAX_RESOLVED_DISPLAY = 5;
34
53
  var ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
54
+ var MCP_SERVER_VERSION = "0.8.5";
35
55
 
36
56
  // src/mcp/client.ts
37
57
  var BrakitClient = class {
@@ -79,6 +99,19 @@ var BrakitClient = class {
79
99
  if (state) url.searchParams.set("state", state);
80
100
  return this.fetchJson(url);
81
101
  }
102
+ async reportFix(findingId, status, notes) {
103
+ const res = await fetch(`${this.baseUrl}${DASHBOARD_API_FINDINGS_REPORT}`, {
104
+ method: "POST",
105
+ headers: { "Content-Type": "application/json" },
106
+ body: JSON.stringify({ findingId, status, notes }),
107
+ signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
108
+ });
109
+ if (!res.ok) return false;
110
+ const contentType = res.headers.get("content-type") ?? "";
111
+ if (!contentType.includes("application/json")) return false;
112
+ const body = await res.json();
113
+ return body.ok === true;
114
+ }
82
115
  async clearAll() {
83
116
  const res = await fetch(`${this.baseUrl}${DASHBOARD_API_CLEAR}`, {
84
117
  method: "POST",
@@ -108,59 +141,74 @@ var BrakitClient = class {
108
141
  };
109
142
 
110
143
  // src/mcp/discovery.ts
111
- import { readFileSync, existsSync, readdirSync, statSync } from "fs";
144
+ import { readFile, readdir, stat } from "fs/promises";
112
145
  import { resolve, dirname } from "path";
113
146
 
114
147
  // src/constants/limits.ts
115
- var MAX_INGEST_BYTES = 10 * 1024 * 1024;
148
+ var FINDING_ID_HASH_LENGTH = 16;
116
149
 
117
150
  // src/constants/metrics.ts
118
151
  var PORT_FILE = ".brakit/port";
119
152
 
120
- // src/constants/severity.ts
121
- var SEVERITY_CRITICAL = "critical";
122
- var SEVERITY_WARNING = "warning";
123
- var SEVERITY_INFO = "info";
124
- var SEVERITY_ICON_MAP = {
125
- [SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
126
- [SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
127
- [SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
128
- };
153
+ // src/constants/network.ts
154
+ var RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
155
+ var PORT_MIN = 1;
156
+ var PORT_MAX = 65535;
157
+
158
+ // src/constants/lifecycle.ts
159
+ var VALID_FINDING_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
160
+ var VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
161
+ var VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
162
+
163
+ // src/utils/log.ts
164
+ var PREFIX = "[brakit]";
165
+ function brakitDebug(message) {
166
+ if (process.env.DEBUG_BRAKIT) {
167
+ process.stderr.write(`${PREFIX}:debug ${message}
168
+ `);
169
+ }
170
+ }
129
171
 
130
172
  // src/mcp/discovery.ts
131
- function readPort(portPath) {
132
- if (!existsSync(portPath)) return null;
133
- const raw = readFileSync(portPath, "utf-8").trim();
134
- const port = parseInt(raw, 10);
135
- return isNaN(port) || port < 1 || port > 65535 ? null : port;
173
+ async function readPort(portPath) {
174
+ try {
175
+ const raw = (await readFile(portPath, "utf-8")).trim();
176
+ const port = parseInt(raw, 10);
177
+ return isNaN(port) || port < PORT_MIN || port > PORT_MAX ? null : port;
178
+ } catch {
179
+ return null;
180
+ }
136
181
  }
137
- function portInDir(dir) {
182
+ async function portInDir(dir) {
138
183
  return readPort(resolve(dir, PORT_FILE));
139
184
  }
140
- function portInChildren(dir) {
185
+ async function portInChildren(dir) {
141
186
  try {
142
- for (const entry of readdirSync(dir)) {
187
+ const entries = await readdir(dir);
188
+ for (const entry of entries) {
143
189
  if (entry.startsWith(".") || entry === "node_modules") continue;
144
190
  const child = resolve(dir, entry);
145
191
  try {
146
- if (!statSync(child).isDirectory()) continue;
147
- } catch {
192
+ if (!(await stat(child)).isDirectory()) continue;
193
+ } catch (err) {
194
+ brakitDebug(`discovery: stat failed for ${child}: ${err}`);
148
195
  continue;
149
196
  }
150
- const port = portInDir(child);
197
+ const port = await portInDir(child);
151
198
  if (port) return port;
152
199
  }
153
- } catch {
200
+ } catch (err) {
201
+ brakitDebug(`discovery: readdir failed for ${dir}: ${err}`);
154
202
  }
155
203
  return null;
156
204
  }
157
- function searchForPort(startDir) {
205
+ async function searchForPort(startDir) {
158
206
  const start = resolve(startDir);
159
- const initial = portInDir(start) ?? portInChildren(start);
207
+ const initial = await portInDir(start) ?? await portInChildren(start);
160
208
  if (initial) return initial;
161
209
  let dir = dirname(start);
162
210
  for (let depth = 0; depth < MAX_DISCOVERY_DEPTH; depth++) {
163
- const port = portInDir(dir);
211
+ const port = await portInDir(dir) ?? await portInChildren(dir);
164
212
  if (port) return port;
165
213
  const parent = dirname(dir);
166
214
  if (parent === dir) break;
@@ -168,8 +216,8 @@ function searchForPort(startDir) {
168
216
  }
169
217
  return null;
170
218
  }
171
- function discoverBrakitPort(cwd) {
172
- const port = searchForPort(cwd ?? process.cwd());
219
+ async function discoverBrakitPort(cwd) {
220
+ const port = await searchForPort(cwd ?? process.cwd());
173
221
  if (!port) {
174
222
  throw new Error(
175
223
  "Brakit is not running. Start your app with brakit enabled first."
@@ -181,7 +229,7 @@ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTER
181
229
  const deadline = Date.now() + timeoutMs;
182
230
  while (Date.now() < deadline) {
183
231
  try {
184
- const result = discoverBrakitPort(cwd);
232
+ const result = await discoverBrakitPort(cwd);
185
233
  const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
186
234
  if (res.ok) return result;
187
235
  } catch {
@@ -193,14 +241,11 @@ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTER
193
241
  );
194
242
  }
195
243
 
196
- // src/mcp/enrichment.ts
197
- import { createHash as createHash2 } from "crypto";
198
-
199
244
  // src/store/finding-id.ts
200
245
  import { createHash } from "crypto";
201
- function computeFindingId(finding) {
202
- const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
203
- return createHash("sha256").update(key).digest("hex").slice(0, 16);
246
+ function computeInsightId(type, endpoint, desc) {
247
+ const key = `${type}:${endpoint}:${desc}`;
248
+ return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
204
249
  }
205
250
 
206
251
  // src/utils/endpoint.ts
@@ -213,44 +258,46 @@ function parseEndpointKey(endpoint) {
213
258
  }
214
259
 
215
260
  // src/mcp/enrichment.ts
216
- function computeInsightId(type, endpoint, desc) {
217
- const key = `${type}:${endpoint}:${desc}`;
218
- return createHash2("sha256").update(key).digest("hex").slice(0, 16);
219
- }
220
261
  async function enrichFindings(client) {
221
262
  const [securityData, insightsData] = await Promise.all([
222
263
  client.getSecurityFindings(),
223
264
  client.getInsights()
224
265
  ]);
225
- const enriched = [];
226
- for (const f of securityData.findings) {
227
- let context = "";
228
- try {
229
- const { path } = parseEndpointKey(f.endpoint);
230
- const reqData = await client.getRequests({ search: path, limit: 1 });
231
- if (reqData.requests.length > 0) {
232
- const req = reqData.requests[0];
233
- if (req.id) {
234
- const activity = await client.getActivity(req.id);
235
- const queryCount = activity.counts?.queries ?? 0;
236
- const fetchCount = activity.counts?.fetches ?? 0;
237
- context = `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
266
+ const contexts = await Promise.all(
267
+ securityData.findings.map(async (sf) => {
268
+ try {
269
+ const { path } = parseEndpointKey(sf.finding.endpoint);
270
+ const reqData = await client.getRequests({ search: path, limit: 1 });
271
+ if (reqData.requests.length > 0) {
272
+ const req = reqData.requests[0];
273
+ if (req.id) {
274
+ const activity = await client.getActivity(req.id);
275
+ const queryCount = activity.counts?.queries ?? 0;
276
+ const fetchCount = activity.counts?.fetches ?? 0;
277
+ return `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
278
+ }
238
279
  }
280
+ } catch {
281
+ return "(context unavailable)";
239
282
  }
240
- } catch {
241
- context = "(context unavailable)";
242
- }
243
- enriched.push({
244
- findingId: computeFindingId(f),
283
+ return "";
284
+ })
285
+ );
286
+ const enriched = securityData.findings.map((sf, i) => {
287
+ const f = sf.finding;
288
+ return {
289
+ findingId: sf.findingId,
245
290
  severity: f.severity,
246
291
  title: f.title,
247
292
  endpoint: f.endpoint,
248
293
  description: f.desc,
249
294
  hint: f.hint,
250
295
  occurrences: f.count,
251
- context
252
- });
253
- }
296
+ context: contexts[i],
297
+ aiStatus: sf.aiStatus,
298
+ aiNotes: sf.aiNotes
299
+ };
300
+ });
254
301
  for (const si of insightsData.insights) {
255
302
  if (si.state === "resolved") continue;
256
303
  const i = si.insight;
@@ -264,7 +311,9 @@ async function enrichFindings(client) {
264
311
  description: i.desc,
265
312
  hint: i.hint,
266
313
  occurrences: 1,
267
- context: i.detail ?? ""
314
+ context: i.detail ?? "",
315
+ aiStatus: si.aiStatus,
316
+ aiNotes: si.aiNotes
268
317
  });
269
318
  }
270
319
  return enriched;
@@ -317,9 +366,18 @@ async function buildRequestDetail(client, req) {
317
366
  };
318
367
  }
319
368
 
369
+ // src/utils/type-guards.ts
370
+ function isNonEmptyString(val) {
371
+ return typeof val === "string" && val.trim().length > 0;
372
+ }
373
+ function isValidFindingState(val) {
374
+ return typeof val === "string" && VALID_FINDING_STATES.has(val);
375
+ }
376
+ function isValidAiFixStatus(val) {
377
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
378
+ }
379
+
320
380
  // src/mcp/tools/get-findings.ts
321
- var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
322
- var VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
323
381
  var getFindings = {
324
382
  name: "get_findings",
325
383
  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.",
@@ -341,10 +399,10 @@ var getFindings = {
341
399
  async handler(client, args) {
342
400
  const severity = args.severity;
343
401
  const state = args.state;
344
- if (severity && !VALID_SEVERITIES.has(severity)) {
402
+ if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
345
403
  return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
346
404
  }
347
- if (state && !VALID_STATES.has(state)) {
405
+ if (state && !isValidFindingState(state)) {
348
406
  return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
349
407
  }
350
408
  let findings = await enrichFindings(client);
@@ -363,10 +421,18 @@ var getFindings = {
363
421
  `];
364
422
  for (const f of findings) {
365
423
  lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
424
+ lines.push(` ID: ${f.findingId}`);
366
425
  lines.push(` Endpoint: ${f.endpoint}`);
367
426
  lines.push(` Issue: ${f.description}`);
368
427
  if (f.context) lines.push(` Context: ${f.context}`);
369
428
  lines.push(` Fix: ${f.hint}`);
429
+ if (f.aiStatus === "fixed") {
430
+ lines.push(` AI Status: fixed (awaiting verification)`);
431
+ if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
432
+ } else if (f.aiStatus === "wont_fix") {
433
+ lines.push(` AI Status: won't fix`);
434
+ if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
435
+ }
370
436
  lines.push("");
371
437
  }
372
438
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -659,9 +725,57 @@ var clearFindings = {
659
725
  }
660
726
  };
661
727
 
728
+ // src/mcp/tools/report-fix.ts
729
+ var reportFix = {
730
+ name: "report_fix",
731
+ 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).",
732
+ inputSchema: {
733
+ type: "object",
734
+ properties: {
735
+ finding_id: {
736
+ type: "string",
737
+ description: "The finding ID to report on"
738
+ },
739
+ status: {
740
+ type: "string",
741
+ description: "Whether the fix was applied or can't be fixed",
742
+ enum: ["fixed", "wont_fix"]
743
+ },
744
+ summary: {
745
+ type: "string",
746
+ description: "Brief description of what was done or why it can't be fixed"
747
+ }
748
+ },
749
+ required: ["finding_id", "status", "summary"]
750
+ },
751
+ async handler(client, args) {
752
+ const { finding_id, status, summary } = args;
753
+ if (!isNonEmptyString(finding_id)) {
754
+ return { content: [{ type: "text", text: "finding_id is required." }], isError: true };
755
+ }
756
+ if (!isValidAiFixStatus(status)) {
757
+ return { content: [{ type: "text", text: "status must be 'fixed' or 'wont_fix'." }], isError: true };
758
+ }
759
+ if (!isNonEmptyString(summary)) {
760
+ return { content: [{ type: "text", text: "summary is required." }], isError: true };
761
+ }
762
+ const ok = await client.reportFix(finding_id, status, summary);
763
+ if (!ok) {
764
+ return {
765
+ content: [{ type: "text", text: `Finding ${finding_id} not found. It may have already been resolved.` }],
766
+ isError: true
767
+ };
768
+ }
769
+ const label = status === "fixed" ? "marked as fixed (awaiting verification)" : "marked as won't fix";
770
+ return {
771
+ content: [{ type: "text", text: `Finding ${finding_id} ${label}. Dashboard updated.` }]
772
+ };
773
+ }
774
+ };
775
+
662
776
  // src/mcp/tools/index.ts
663
777
  var TOOL_MAP = new Map(
664
- [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings].map((t) => [t.name, t])
778
+ [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings, reportFix].map((t) => [t.name, t])
665
779
  );
666
780
  function getToolDefinitions() {
667
781
  return [...TOOL_MAP.values()].map((t) => ({
@@ -682,6 +796,7 @@ function handleToolCall(client, name, args) {
682
796
  }
683
797
 
684
798
  // src/mcp/prompts.ts
799
+ var 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.";
685
800
  var PROMPTS = [
686
801
  {
687
802
  name: "check-app",
@@ -693,18 +808,8 @@ var PROMPTS = [
693
808
  }
694
809
  ];
695
810
  var PROMPT_MESSAGES = {
696
- "check-app": [
697
- "Check my running app for security and performance issues using brakit.",
698
- "First get all findings, then get the endpoint summary.",
699
- "For any critical or warning findings, get the request detail to understand the root cause.",
700
- "Give me a clear report of what's wrong and offer to fix each issue."
701
- ].join(" "),
702
- "fix-findings": [
703
- "Get all open brakit findings.",
704
- "For each finding, get the request detail to understand the exact issue.",
705
- "Then find the source code responsible and fix it.",
706
- "After fixing, ask me to re-trigger the endpoint so you can verify the fix with brakit."
707
- ].join(" ")
811
+ "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.",
812
+ "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."
708
813
  };
709
814
 
710
815
  // src/mcp/server.ts
@@ -718,7 +823,7 @@ async function startMcpServer() {
718
823
  let cachedClient = discovery ? new BrakitClient(discovery.baseUrl) : null;
719
824
  const server = new Server(
720
825
  { name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
721
- { capabilities: { tools: {}, prompts: {} } }
826
+ { capabilities: { tools: {}, prompts: {} }, instructions: SERVER_INSTRUCTIONS }
722
827
  );
723
828
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
724
829
  prompts: [...PROMPTS]