brakit 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -52
- package/dist/api.d.ts +47 -2
- package/dist/api.js +275 -94
- package/dist/bin/brakit.js +1143 -48
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +737 -0
- package/dist/runtime/index.js +302 -66
- package/package.json +5 -1
package/dist/bin/brakit.js
CHANGED
|
@@ -1,17 +1,944 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
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;
|
|
14
|
+
var init_routes = __esm({
|
|
15
|
+
"src/constants/routes.ts"() {
|
|
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";
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// src/constants/limits.ts
|
|
31
|
+
var MAX_REQUEST_ENTRIES, MAX_TELEMETRY_ENTRIES;
|
|
32
|
+
var init_limits = __esm({
|
|
33
|
+
"src/constants/limits.ts"() {
|
|
34
|
+
"use strict";
|
|
35
|
+
MAX_REQUEST_ENTRIES = 1e3;
|
|
36
|
+
MAX_TELEMETRY_ENTRIES = 1e3;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// src/constants/thresholds.ts
|
|
41
|
+
var OVERFETCH_UNWRAP_MIN_SIZE;
|
|
42
|
+
var init_thresholds = __esm({
|
|
43
|
+
"src/constants/thresholds.ts"() {
|
|
44
|
+
"use strict";
|
|
45
|
+
OVERFETCH_UNWRAP_MIN_SIZE = 3;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/constants/transport.ts
|
|
50
|
+
var init_transport = __esm({
|
|
51
|
+
"src/constants/transport.ts"() {
|
|
52
|
+
"use strict";
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// src/constants/metrics.ts
|
|
57
|
+
var METRICS_DIR, PORT_FILE;
|
|
58
|
+
var init_metrics = __esm({
|
|
59
|
+
"src/constants/metrics.ts"() {
|
|
60
|
+
"use strict";
|
|
61
|
+
METRICS_DIR = ".brakit";
|
|
62
|
+
PORT_FILE = ".brakit/port";
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/constants/headers.ts
|
|
67
|
+
var init_headers = __esm({
|
|
68
|
+
"src/constants/headers.ts"() {
|
|
69
|
+
"use strict";
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// src/constants/network.ts
|
|
74
|
+
var init_network = __esm({
|
|
75
|
+
"src/constants/network.ts"() {
|
|
76
|
+
"use strict";
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// src/constants/mcp.ts
|
|
81
|
+
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_TIMELINE_EVENTS, MAX_RESOLVED_DISPLAY, ENRICHMENT_SEVERITY_FILTER;
|
|
82
|
+
var init_mcp = __esm({
|
|
83
|
+
"src/constants/mcp.ts"() {
|
|
84
|
+
"use strict";
|
|
85
|
+
MCP_SERVER_NAME = "brakit";
|
|
86
|
+
MCP_SERVER_VERSION = "0.8.0";
|
|
87
|
+
INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
|
|
88
|
+
LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
|
|
89
|
+
CLIENT_FETCH_TIMEOUT_MS = 1e4;
|
|
90
|
+
HEALTH_CHECK_TIMEOUT_MS = 3e3;
|
|
91
|
+
DISCOVERY_POLL_INTERVAL_MS = 500;
|
|
92
|
+
MAX_TIMELINE_EVENTS = 20;
|
|
93
|
+
MAX_RESOLVED_DISPLAY = 5;
|
|
94
|
+
ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// src/constants/index.ts
|
|
99
|
+
var init_constants = __esm({
|
|
100
|
+
"src/constants/index.ts"() {
|
|
101
|
+
"use strict";
|
|
102
|
+
init_routes();
|
|
103
|
+
init_limits();
|
|
104
|
+
init_thresholds();
|
|
105
|
+
init_transport();
|
|
106
|
+
init_metrics();
|
|
107
|
+
init_headers();
|
|
108
|
+
init_network();
|
|
109
|
+
init_mcp();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// src/store/finding-id.ts
|
|
114
|
+
import { createHash } from "crypto";
|
|
115
|
+
function computeFindingId(finding) {
|
|
116
|
+
const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
|
|
117
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
118
|
+
}
|
|
119
|
+
var init_finding_id = __esm({
|
|
120
|
+
"src/store/finding-id.ts"() {
|
|
121
|
+
"use strict";
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// src/utils/endpoint.ts
|
|
126
|
+
function parseEndpointKey(endpoint) {
|
|
127
|
+
const spaceIdx = endpoint.indexOf(" ");
|
|
128
|
+
if (spaceIdx > 0) {
|
|
129
|
+
return { method: endpoint.slice(0, spaceIdx), path: endpoint.slice(spaceIdx + 1) };
|
|
130
|
+
}
|
|
131
|
+
return { path: endpoint };
|
|
132
|
+
}
|
|
133
|
+
var init_endpoint = __esm({
|
|
134
|
+
"src/utils/endpoint.ts"() {
|
|
135
|
+
"use strict";
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// src/mcp/client.ts
|
|
140
|
+
var BrakitClient;
|
|
141
|
+
var init_client = __esm({
|
|
142
|
+
"src/mcp/client.ts"() {
|
|
143
|
+
"use strict";
|
|
144
|
+
init_routes();
|
|
145
|
+
init_mcp();
|
|
146
|
+
BrakitClient = class {
|
|
147
|
+
constructor(baseUrl) {
|
|
148
|
+
this.baseUrl = baseUrl;
|
|
149
|
+
}
|
|
150
|
+
async getRequests(params) {
|
|
151
|
+
const url = new URL(`${this.baseUrl}${DASHBOARD_API_REQUESTS}`);
|
|
152
|
+
if (params?.method) url.searchParams.set("method", params.method);
|
|
153
|
+
if (params?.status) url.searchParams.set("status", params.status);
|
|
154
|
+
if (params?.search) url.searchParams.set("search", params.search);
|
|
155
|
+
if (params?.limit) url.searchParams.set("limit", String(params.limit));
|
|
156
|
+
if (params?.offset) url.searchParams.set("offset", String(params.offset));
|
|
157
|
+
return this.fetchJson(url);
|
|
158
|
+
}
|
|
159
|
+
async getSecurityFindings() {
|
|
160
|
+
return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_SECURITY}`);
|
|
161
|
+
}
|
|
162
|
+
async getInsights() {
|
|
163
|
+
return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_INSIGHTS}`);
|
|
164
|
+
}
|
|
165
|
+
async getQueries(requestId) {
|
|
166
|
+
const url = new URL(`${this.baseUrl}${DASHBOARD_API_QUERIES}`);
|
|
167
|
+
if (requestId) url.searchParams.set("requestId", requestId);
|
|
168
|
+
return this.fetchJson(url);
|
|
169
|
+
}
|
|
170
|
+
async getFetches(requestId) {
|
|
171
|
+
const url = new URL(`${this.baseUrl}${DASHBOARD_API_FETCHES}`);
|
|
172
|
+
if (requestId) url.searchParams.set("requestId", requestId);
|
|
173
|
+
return this.fetchJson(url);
|
|
174
|
+
}
|
|
175
|
+
async getErrors() {
|
|
176
|
+
return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_ERRORS}`);
|
|
177
|
+
}
|
|
178
|
+
async getActivity(requestId) {
|
|
179
|
+
const url = new URL(`${this.baseUrl}${DASHBOARD_API_ACTIVITY}`);
|
|
180
|
+
url.searchParams.set("requestId", requestId);
|
|
181
|
+
return this.fetchJson(url);
|
|
182
|
+
}
|
|
183
|
+
async getLiveMetrics() {
|
|
184
|
+
return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_METRICS_LIVE}`);
|
|
185
|
+
}
|
|
186
|
+
async getFindings(state) {
|
|
187
|
+
const url = new URL(`${this.baseUrl}${DASHBOARD_API_FINDINGS}`);
|
|
188
|
+
if (state) url.searchParams.set("state", state);
|
|
189
|
+
return this.fetchJson(url);
|
|
190
|
+
}
|
|
191
|
+
async clearAll() {
|
|
192
|
+
const res = await fetch(`${this.baseUrl}${DASHBOARD_API_CLEAR}`, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
|
|
195
|
+
});
|
|
196
|
+
return res.ok;
|
|
197
|
+
}
|
|
198
|
+
async isAlive() {
|
|
199
|
+
try {
|
|
200
|
+
const res = await fetch(`${this.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`, {
|
|
201
|
+
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS)
|
|
202
|
+
});
|
|
203
|
+
return res.ok;
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async fetchJson(url) {
|
|
209
|
+
const res = await fetch(url.toString(), {
|
|
210
|
+
signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
|
|
211
|
+
});
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
throw new Error(`Brakit API error: ${res.status} ${res.statusText}`);
|
|
214
|
+
}
|
|
215
|
+
return res.json();
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// src/mcp/discovery.ts
|
|
222
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
223
|
+
import { resolve as resolve6 } from "path";
|
|
224
|
+
function discoverBrakitPort(cwd) {
|
|
225
|
+
const root = cwd ?? process.cwd();
|
|
226
|
+
const portPath = resolve6(root, PORT_FILE);
|
|
227
|
+
if (!existsSync4(portPath)) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Brakit is not running. No port file found at ${portPath}.
|
|
230
|
+
Start your app with brakit enabled first.`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
const raw = readFileSync4(portPath, "utf-8").trim();
|
|
234
|
+
const port = parseInt(raw, 10);
|
|
235
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
236
|
+
throw new Error(`Invalid port in ${portPath}: "${raw}"`);
|
|
237
|
+
}
|
|
238
|
+
return { port, baseUrl: `http://localhost:${port}` };
|
|
239
|
+
}
|
|
240
|
+
async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTERVAL_MS) {
|
|
241
|
+
const deadline = Date.now() + timeoutMs;
|
|
242
|
+
while (Date.now() < deadline) {
|
|
243
|
+
try {
|
|
244
|
+
const result = discoverBrakitPort(cwd);
|
|
245
|
+
const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
|
|
246
|
+
if (res.ok) return result;
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
250
|
+
}
|
|
251
|
+
throw new Error(
|
|
252
|
+
"Timed out waiting for Brakit to start. Is your app running with brakit enabled?"
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
var init_discovery = __esm({
|
|
256
|
+
"src/mcp/discovery.ts"() {
|
|
257
|
+
"use strict";
|
|
258
|
+
init_constants();
|
|
259
|
+
init_mcp();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// src/mcp/enrichment.ts
|
|
264
|
+
import { createHash as createHash2 } from "crypto";
|
|
265
|
+
function computeInsightId(type, endpoint, desc) {
|
|
266
|
+
const key = `${type}:${endpoint}:${desc}`;
|
|
267
|
+
return createHash2("sha256").update(key).digest("hex").slice(0, 16);
|
|
268
|
+
}
|
|
269
|
+
async function enrichFindings(client) {
|
|
270
|
+
const [securityData, insightsData] = await Promise.all([
|
|
271
|
+
client.getSecurityFindings(),
|
|
272
|
+
client.getInsights()
|
|
273
|
+
]);
|
|
274
|
+
const enriched = [];
|
|
275
|
+
for (const f of securityData.findings) {
|
|
276
|
+
let context = "";
|
|
277
|
+
try {
|
|
278
|
+
const { path } = parseEndpointKey(f.endpoint);
|
|
279
|
+
const reqData = await client.getRequests({ search: path, limit: 1 });
|
|
280
|
+
if (reqData.requests.length > 0) {
|
|
281
|
+
const req = reqData.requests[0];
|
|
282
|
+
if (req.id) {
|
|
283
|
+
const activity = await client.getActivity(req.id);
|
|
284
|
+
const queryCount = activity.counts?.queries ?? 0;
|
|
285
|
+
const fetchCount = activity.counts?.fetches ?? 0;
|
|
286
|
+
context = `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
context = "(context unavailable)";
|
|
291
|
+
}
|
|
292
|
+
enriched.push({
|
|
293
|
+
findingId: computeFindingId(f),
|
|
294
|
+
severity: f.severity,
|
|
295
|
+
title: f.title,
|
|
296
|
+
endpoint: f.endpoint,
|
|
297
|
+
description: f.desc,
|
|
298
|
+
hint: f.hint,
|
|
299
|
+
occurrences: f.count,
|
|
300
|
+
context
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
for (const i of insightsData.insights) {
|
|
304
|
+
if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
|
|
305
|
+
const endpoint = i.nav ?? "global";
|
|
306
|
+
enriched.push({
|
|
307
|
+
findingId: computeInsightId(i.type, endpoint, i.desc),
|
|
308
|
+
severity: i.severity,
|
|
309
|
+
title: i.title,
|
|
310
|
+
endpoint,
|
|
311
|
+
description: i.desc,
|
|
312
|
+
hint: i.hint,
|
|
313
|
+
occurrences: 1,
|
|
314
|
+
context: i.detail ?? ""
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return enriched;
|
|
318
|
+
}
|
|
319
|
+
async function enrichEndpoints(client, sortBy) {
|
|
320
|
+
const data = await client.getLiveMetrics();
|
|
321
|
+
const endpoints = data.endpoints.map((ep) => ({
|
|
322
|
+
...ep.summary,
|
|
323
|
+
endpoint: ep.endpoint
|
|
324
|
+
}));
|
|
325
|
+
if (sortBy === "error_rate") {
|
|
326
|
+
endpoints.sort((a, b) => b.errorRate - a.errorRate);
|
|
327
|
+
} else if (sortBy === "query_count") {
|
|
328
|
+
endpoints.sort((a, b) => b.avgQueryCount - a.avgQueryCount);
|
|
329
|
+
} else if (sortBy === "requests") {
|
|
330
|
+
endpoints.sort((a, b) => b.totalRequests - a.totalRequests);
|
|
331
|
+
}
|
|
332
|
+
return endpoints;
|
|
333
|
+
}
|
|
334
|
+
async function enrichRequestDetail(client, opts) {
|
|
335
|
+
if (opts.requestId) {
|
|
336
|
+
const data = await client.getRequests({ search: opts.requestId, limit: 1 });
|
|
337
|
+
if (data.requests.length > 0) {
|
|
338
|
+
return buildRequestDetail(client, data.requests[0].id);
|
|
339
|
+
}
|
|
340
|
+
} else if (opts.endpoint) {
|
|
341
|
+
const { method, path } = parseEndpointKey(opts.endpoint);
|
|
342
|
+
const data = await client.getRequests({ method, search: path, limit: 1 });
|
|
343
|
+
if (data.requests.length > 0) {
|
|
344
|
+
return buildRequestDetail(client, data.requests[0].id);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
async function buildRequestDetail(client, requestId) {
|
|
350
|
+
const [reqData, activity, queries, fetches] = await Promise.all([
|
|
351
|
+
client.getRequests({ search: requestId, limit: 1 }),
|
|
352
|
+
client.getActivity(requestId),
|
|
353
|
+
client.getQueries(requestId),
|
|
354
|
+
client.getFetches(requestId)
|
|
355
|
+
]);
|
|
356
|
+
const req = reqData.requests[0];
|
|
357
|
+
return {
|
|
358
|
+
id: requestId,
|
|
359
|
+
method: req.method,
|
|
360
|
+
url: req.url,
|
|
361
|
+
statusCode: req.statusCode,
|
|
362
|
+
durationMs: req.durationMs,
|
|
363
|
+
queries: queries.entries,
|
|
364
|
+
fetches: fetches.entries,
|
|
365
|
+
timeline: activity.timeline
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
var init_enrichment = __esm({
|
|
369
|
+
"src/mcp/enrichment.ts"() {
|
|
370
|
+
"use strict";
|
|
371
|
+
init_mcp();
|
|
372
|
+
init_finding_id();
|
|
373
|
+
init_endpoint();
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// src/mcp/tools/get-findings.ts
|
|
378
|
+
var VALID_SEVERITIES, VALID_STATES, getFindings;
|
|
379
|
+
var init_get_findings = __esm({
|
|
380
|
+
"src/mcp/tools/get-findings.ts"() {
|
|
381
|
+
"use strict";
|
|
382
|
+
init_enrichment();
|
|
383
|
+
VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
|
|
384
|
+
VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
|
|
385
|
+
getFindings = {
|
|
386
|
+
name: "get_findings",
|
|
387
|
+
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.",
|
|
388
|
+
inputSchema: {
|
|
389
|
+
type: "object",
|
|
390
|
+
properties: {
|
|
391
|
+
severity: {
|
|
392
|
+
type: "string",
|
|
393
|
+
enum: ["critical", "warning"],
|
|
394
|
+
description: "Filter by severity level"
|
|
395
|
+
},
|
|
396
|
+
state: {
|
|
397
|
+
type: "string",
|
|
398
|
+
enum: ["open", "fixing", "resolved"],
|
|
399
|
+
description: "Filter by finding state (from finding lifecycle)"
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
async handler(client, args) {
|
|
404
|
+
const severity = args.severity;
|
|
405
|
+
const state = args.state;
|
|
406
|
+
if (severity && !VALID_SEVERITIES.has(severity)) {
|
|
407
|
+
return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
|
|
408
|
+
}
|
|
409
|
+
if (state && !VALID_STATES.has(state)) {
|
|
410
|
+
return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
|
|
411
|
+
}
|
|
412
|
+
let findings = await enrichFindings(client);
|
|
413
|
+
if (severity) {
|
|
414
|
+
findings = findings.filter((f) => f.severity === severity);
|
|
415
|
+
}
|
|
416
|
+
if (state) {
|
|
417
|
+
const stateful = await client.getFindings(state);
|
|
418
|
+
const statefulIds = new Set(stateful.findings.map((f) => f.findingId));
|
|
419
|
+
findings = findings.filter((f) => statefulIds.has(f.findingId));
|
|
420
|
+
}
|
|
421
|
+
if (findings.length === 0) {
|
|
422
|
+
return { content: [{ type: "text", text: "No findings detected. The application looks healthy." }] };
|
|
423
|
+
}
|
|
424
|
+
const lines = [`Found ${findings.length} issue(s):
|
|
425
|
+
`];
|
|
426
|
+
for (const f of findings) {
|
|
427
|
+
lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
|
|
428
|
+
lines.push(` Endpoint: ${f.endpoint}`);
|
|
429
|
+
lines.push(` Issue: ${f.description}`);
|
|
430
|
+
if (f.context) lines.push(` Context: ${f.context}`);
|
|
431
|
+
lines.push(` Fix: ${f.hint}`);
|
|
432
|
+
lines.push("");
|
|
433
|
+
}
|
|
434
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// src/mcp/tools/get-endpoints.ts
|
|
441
|
+
var VALID_SORT_KEYS, getEndpoints;
|
|
442
|
+
var init_get_endpoints = __esm({
|
|
443
|
+
"src/mcp/tools/get-endpoints.ts"() {
|
|
444
|
+
"use strict";
|
|
445
|
+
init_enrichment();
|
|
446
|
+
VALID_SORT_KEYS = /* @__PURE__ */ new Set(["p95", "error_rate", "query_count", "requests"]);
|
|
447
|
+
getEndpoints = {
|
|
448
|
+
name: "get_endpoints",
|
|
449
|
+
description: "Get a summary of all observed API endpoints with performance stats. Shows p95 latency, error rate, query count, and time breakdown for each endpoint. Use this to identify which endpoints need attention.",
|
|
450
|
+
inputSchema: {
|
|
451
|
+
type: "object",
|
|
452
|
+
properties: {
|
|
453
|
+
sort_by: {
|
|
454
|
+
type: "string",
|
|
455
|
+
enum: ["p95", "error_rate", "query_count", "requests"],
|
|
456
|
+
description: "Sort endpoints by this metric (default: p95 latency)"
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
async handler(client, args) {
|
|
461
|
+
const sortBy = args.sort_by;
|
|
462
|
+
if (sortBy && !VALID_SORT_KEYS.has(sortBy)) {
|
|
463
|
+
return { content: [{ type: "text", text: `Invalid sort_by "${sortBy}". Use: p95, error_rate, query_count, requests.` }], isError: true };
|
|
464
|
+
}
|
|
465
|
+
const endpoints = await enrichEndpoints(client, sortBy);
|
|
466
|
+
if (endpoints.length === 0) {
|
|
467
|
+
return {
|
|
468
|
+
content: [{ type: "text", text: "No endpoints observed yet. Make some requests to your app first." }]
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const lines = [`${endpoints.length} endpoint(s) observed:
|
|
472
|
+
`];
|
|
473
|
+
for (const ep of endpoints) {
|
|
474
|
+
lines.push(`${ep.endpoint}`);
|
|
475
|
+
lines.push(` p95: ${ep.p95Ms}ms | Errors: ${(ep.errorRate * 100).toFixed(1)}% | Queries: ${ep.avgQueryCount}/req | Requests: ${ep.totalRequests}`);
|
|
476
|
+
lines.push(` Time breakdown: DB ${ep.avgQueryTimeMs}ms + Fetch ${ep.avgFetchTimeMs}ms + App ${ep.avgAppTimeMs}ms`);
|
|
477
|
+
lines.push("");
|
|
478
|
+
}
|
|
479
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// src/mcp/tools/get-request-detail.ts
|
|
486
|
+
var getRequestDetail;
|
|
487
|
+
var init_get_request_detail = __esm({
|
|
488
|
+
"src/mcp/tools/get-request-detail.ts"() {
|
|
489
|
+
"use strict";
|
|
490
|
+
init_mcp();
|
|
491
|
+
init_enrichment();
|
|
492
|
+
getRequestDetail = {
|
|
493
|
+
name: "get_request_detail",
|
|
494
|
+
description: "Get full details of a specific HTTP request including all DB queries it fired, all fetches it made, the response, and a timeline of events. Use this to deeply understand what happens when a specific endpoint is hit.",
|
|
495
|
+
inputSchema: {
|
|
496
|
+
type: "object",
|
|
497
|
+
properties: {
|
|
498
|
+
request_id: {
|
|
499
|
+
type: "string",
|
|
500
|
+
description: "The specific request ID to look up"
|
|
501
|
+
},
|
|
502
|
+
endpoint: {
|
|
503
|
+
type: "string",
|
|
504
|
+
description: "Alternatively, get the latest request for an endpoint like 'GET /api/users'"
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
async handler(client, args) {
|
|
509
|
+
const requestId = args.request_id;
|
|
510
|
+
const endpoint = args.endpoint;
|
|
511
|
+
if (!requestId && !endpoint) {
|
|
512
|
+
return {
|
|
513
|
+
content: [{ type: "text", text: "Please provide either a request_id or an endpoint (e.g. 'GET /api/users')." }]
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const detail = await enrichRequestDetail(client, { requestId, endpoint });
|
|
517
|
+
if (!detail) {
|
|
518
|
+
return {
|
|
519
|
+
content: [{ type: "text", text: `No request found for ${requestId ?? endpoint}.` }]
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
const lines = [
|
|
523
|
+
`Request: ${detail.method} ${detail.url}`,
|
|
524
|
+
`Status: ${detail.statusCode}`,
|
|
525
|
+
`Duration: ${detail.durationMs}ms`,
|
|
526
|
+
""
|
|
527
|
+
];
|
|
528
|
+
if (detail.queries.length > 0) {
|
|
529
|
+
lines.push(`DB Queries (${detail.queries.length}):`);
|
|
530
|
+
for (const q of detail.queries) {
|
|
531
|
+
const sql = q.sql ?? `${q.operation} ${q.table ?? q.model ?? ""}`;
|
|
532
|
+
lines.push(` [${q.durationMs}ms] ${sql}`);
|
|
533
|
+
}
|
|
534
|
+
lines.push("");
|
|
535
|
+
}
|
|
536
|
+
if (detail.fetches.length > 0) {
|
|
537
|
+
lines.push(`Outgoing Fetches (${detail.fetches.length}):`);
|
|
538
|
+
for (const f of detail.fetches) {
|
|
539
|
+
lines.push(` [${f.durationMs}ms] ${f.method} ${f.url} \u2192 ${f.statusCode}`);
|
|
540
|
+
}
|
|
541
|
+
lines.push("");
|
|
542
|
+
}
|
|
543
|
+
if (detail.timeline.length > 0) {
|
|
544
|
+
lines.push(`Timeline (${detail.timeline.length} events):`);
|
|
545
|
+
for (const event of detail.timeline.slice(0, MAX_TIMELINE_EVENTS)) {
|
|
546
|
+
lines.push(` ${event.type}: ${JSON.stringify(event.data)}`);
|
|
547
|
+
}
|
|
548
|
+
if (detail.timeline.length > MAX_TIMELINE_EVENTS) {
|
|
549
|
+
lines.push(` ... and ${detail.timeline.length - MAX_TIMELINE_EVENTS} more events`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// src/mcp/tools/verify-fix.ts
|
|
559
|
+
var verifyFix;
|
|
560
|
+
var init_verify_fix = __esm({
|
|
561
|
+
"src/mcp/tools/verify-fix.ts"() {
|
|
562
|
+
"use strict";
|
|
563
|
+
verifyFix = {
|
|
564
|
+
name: "verify_fix",
|
|
565
|
+
description: "Verify whether a previously found security or performance issue has been resolved. After you fix code, the user should trigger the endpoint again, then call this tool to check if the finding still appears in Brakit's analysis.",
|
|
566
|
+
inputSchema: {
|
|
567
|
+
type: "object",
|
|
568
|
+
properties: {
|
|
569
|
+
finding_id: {
|
|
570
|
+
type: "string",
|
|
571
|
+
description: "The finding ID to verify"
|
|
572
|
+
},
|
|
573
|
+
endpoint: {
|
|
574
|
+
type: "string",
|
|
575
|
+
description: "Alternatively, check if a specific endpoint still has issues (e.g. 'GET /api/users')"
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
async handler(client, args) {
|
|
580
|
+
const findingId = args.finding_id;
|
|
581
|
+
const endpoint = args.endpoint;
|
|
582
|
+
if (findingId !== void 0 && findingId.trim() === "") {
|
|
583
|
+
return { content: [{ type: "text", text: "finding_id cannot be empty." }], isError: true };
|
|
584
|
+
}
|
|
585
|
+
if (endpoint !== void 0 && endpoint.trim() === "") {
|
|
586
|
+
return { content: [{ type: "text", text: "endpoint cannot be empty." }], isError: true };
|
|
587
|
+
}
|
|
588
|
+
if (findingId) {
|
|
589
|
+
const data = await client.getFindings();
|
|
590
|
+
const finding = data.findings.find((f) => f.findingId === findingId);
|
|
591
|
+
if (!finding) {
|
|
592
|
+
return {
|
|
593
|
+
content: [{
|
|
594
|
+
type: "text",
|
|
595
|
+
text: `Finding ${findingId} not found. It may have already been resolved and cleaned up.`
|
|
596
|
+
}]
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
if (finding.state === "resolved") {
|
|
600
|
+
return {
|
|
601
|
+
content: [{
|
|
602
|
+
type: "text",
|
|
603
|
+
text: `RESOLVED: "${finding.finding.title}" on ${finding.finding.endpoint} is no longer detected. The fix worked.`
|
|
604
|
+
}]
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
content: [{
|
|
609
|
+
type: "text",
|
|
610
|
+
text: [
|
|
611
|
+
`STILL PRESENT: "${finding.finding.title}" on ${finding.finding.endpoint}`,
|
|
612
|
+
` State: ${finding.state}`,
|
|
613
|
+
` Last seen: ${new Date(finding.lastSeenAt).toISOString()}`,
|
|
614
|
+
` Occurrences: ${finding.occurrences}`,
|
|
615
|
+
` Issue: ${finding.finding.desc}`,
|
|
616
|
+
` Hint: ${finding.finding.hint}`,
|
|
617
|
+
"",
|
|
618
|
+
"Make sure the user has triggered the endpoint again after the fix, so Brakit can re-analyze."
|
|
619
|
+
].join("\n")
|
|
620
|
+
}]
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (endpoint) {
|
|
624
|
+
const data = await client.getFindings();
|
|
625
|
+
const endpointFindings = data.findings.filter(
|
|
626
|
+
(f) => f.finding.endpoint === endpoint || f.finding.endpoint.endsWith(` ${endpoint}`)
|
|
627
|
+
);
|
|
628
|
+
if (endpointFindings.length === 0) {
|
|
629
|
+
return {
|
|
630
|
+
content: [{
|
|
631
|
+
type: "text",
|
|
632
|
+
text: `No findings found for endpoint "${endpoint}". Either it's clean or it hasn't been analyzed yet.`
|
|
633
|
+
}]
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const open = endpointFindings.filter((f) => f.state === "open");
|
|
637
|
+
const resolved = endpointFindings.filter((f) => f.state === "resolved");
|
|
638
|
+
const lines = [
|
|
639
|
+
`Endpoint: ${endpoint}`,
|
|
640
|
+
`Open issues: ${open.length}`,
|
|
641
|
+
`Resolved: ${resolved.length}`,
|
|
642
|
+
""
|
|
643
|
+
];
|
|
644
|
+
for (const f of open) {
|
|
645
|
+
lines.push(` [${f.finding.severity}] ${f.finding.title}: ${f.finding.desc}`);
|
|
646
|
+
}
|
|
647
|
+
for (const f of resolved) {
|
|
648
|
+
lines.push(` [resolved] ${f.finding.title}`);
|
|
649
|
+
}
|
|
650
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
content: [{
|
|
654
|
+
type: "text",
|
|
655
|
+
text: "Please provide either a finding_id or an endpoint to verify."
|
|
656
|
+
}]
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// src/mcp/tools/get-report.ts
|
|
664
|
+
var getReport;
|
|
665
|
+
var init_get_report = __esm({
|
|
666
|
+
"src/mcp/tools/get-report.ts"() {
|
|
667
|
+
"use strict";
|
|
668
|
+
init_mcp();
|
|
669
|
+
getReport = {
|
|
670
|
+
name: "get_report",
|
|
671
|
+
description: "Generate a summary report of all findings: total found, open, resolved. Use this to get a high-level overview of the application's health.",
|
|
672
|
+
inputSchema: {
|
|
673
|
+
type: "object",
|
|
674
|
+
properties: {}
|
|
675
|
+
},
|
|
676
|
+
async handler(client, _args) {
|
|
677
|
+
const [findingsData, securityData, insightsData, metricsData] = await Promise.all([
|
|
678
|
+
client.getFindings(),
|
|
679
|
+
client.getSecurityFindings(),
|
|
680
|
+
client.getInsights(),
|
|
681
|
+
client.getLiveMetrics()
|
|
682
|
+
]);
|
|
683
|
+
const findings = findingsData.findings;
|
|
684
|
+
const open = findings.filter((f) => f.state === "open");
|
|
685
|
+
const resolved = findings.filter((f) => f.state === "resolved");
|
|
686
|
+
const fixing = findings.filter((f) => f.state === "fixing");
|
|
687
|
+
const criticalOpen = open.filter((f) => f.finding.severity === "critical");
|
|
688
|
+
const warningOpen = open.filter((f) => f.finding.severity === "warning");
|
|
689
|
+
const totalRequests = metricsData.endpoints.reduce(
|
|
690
|
+
(s, ep) => s + ep.summary.totalRequests,
|
|
691
|
+
0
|
|
692
|
+
);
|
|
693
|
+
const lines = [
|
|
694
|
+
"=== Brakit Report ===",
|
|
695
|
+
"",
|
|
696
|
+
`Endpoints observed: ${metricsData.endpoints.length}`,
|
|
697
|
+
`Total requests captured: ${totalRequests}`,
|
|
698
|
+
`Active security rules: ${securityData.findings.length} finding(s)`,
|
|
699
|
+
`Performance insights: ${insightsData.insights.length} insight(s)`,
|
|
700
|
+
"",
|
|
701
|
+
"--- Finding Summary ---",
|
|
702
|
+
`Total: ${findings.length}`,
|
|
703
|
+
` Open: ${open.length} (${criticalOpen.length} critical, ${warningOpen.length} warning)`,
|
|
704
|
+
` In progress: ${fixing.length}`,
|
|
705
|
+
` Resolved: ${resolved.length}`
|
|
706
|
+
];
|
|
707
|
+
if (criticalOpen.length > 0) {
|
|
708
|
+
lines.push("");
|
|
709
|
+
lines.push("--- Critical Issues (fix first) ---");
|
|
710
|
+
for (const f of criticalOpen) {
|
|
711
|
+
lines.push(` [CRITICAL] ${f.finding.title} \u2014 ${f.finding.endpoint}`);
|
|
712
|
+
lines.push(` ${f.finding.desc}`);
|
|
713
|
+
lines.push(` Fix: ${f.finding.hint}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (resolved.length > 0) {
|
|
717
|
+
lines.push("");
|
|
718
|
+
lines.push("--- Recently Resolved ---");
|
|
719
|
+
for (const f of resolved.slice(0, MAX_RESOLVED_DISPLAY)) {
|
|
720
|
+
lines.push(` \u2713 ${f.finding.title} \u2014 ${f.finding.endpoint}`);
|
|
721
|
+
}
|
|
722
|
+
if (resolved.length > MAX_RESOLVED_DISPLAY) {
|
|
723
|
+
lines.push(` ... and ${resolved.length - MAX_RESOLVED_DISPLAY} more`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// src/mcp/tools/clear-findings.ts
|
|
733
|
+
var clearFindings;
|
|
734
|
+
var init_clear_findings = __esm({
|
|
735
|
+
"src/mcp/tools/clear-findings.ts"() {
|
|
736
|
+
"use strict";
|
|
737
|
+
clearFindings = {
|
|
738
|
+
name: "clear_findings",
|
|
739
|
+
description: "Reset finding history for a fresh session. Use this when you want to start tracking findings from scratch.",
|
|
740
|
+
inputSchema: {
|
|
741
|
+
type: "object",
|
|
742
|
+
properties: {}
|
|
743
|
+
},
|
|
744
|
+
async handler(client, _args) {
|
|
745
|
+
const ok = await client.clearAll();
|
|
746
|
+
if (!ok) {
|
|
747
|
+
return {
|
|
748
|
+
content: [{ type: "text", text: "Failed to clear findings. Is the app still running?" }]
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
content: [{ type: "text", text: "All findings and captured data have been cleared. Start making requests to capture fresh data." }]
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// src/mcp/tools/index.ts
|
|
760
|
+
function getToolDefinitions() {
|
|
761
|
+
return [...TOOL_MAP.values()].map((t) => ({
|
|
762
|
+
name: t.name,
|
|
763
|
+
description: t.description,
|
|
764
|
+
inputSchema: t.inputSchema
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
function handleToolCall(client, name, args) {
|
|
768
|
+
const tool = TOOL_MAP.get(name);
|
|
769
|
+
if (!tool) {
|
|
770
|
+
return Promise.resolve({
|
|
771
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
772
|
+
isError: true
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return tool.handler(client, args);
|
|
776
|
+
}
|
|
777
|
+
var TOOL_MAP;
|
|
778
|
+
var init_tools = __esm({
|
|
779
|
+
"src/mcp/tools/index.ts"() {
|
|
780
|
+
"use strict";
|
|
781
|
+
init_get_findings();
|
|
782
|
+
init_get_endpoints();
|
|
783
|
+
init_get_request_detail();
|
|
784
|
+
init_verify_fix();
|
|
785
|
+
init_get_report();
|
|
786
|
+
init_clear_findings();
|
|
787
|
+
TOOL_MAP = new Map(
|
|
788
|
+
[getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings].map((t) => [t.name, t])
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
// src/mcp/prompts.ts
|
|
794
|
+
var PROMPTS, PROMPT_MESSAGES;
|
|
795
|
+
var init_prompts = __esm({
|
|
796
|
+
"src/mcp/prompts.ts"() {
|
|
797
|
+
"use strict";
|
|
798
|
+
PROMPTS = [
|
|
799
|
+
{
|
|
800
|
+
name: "check-app",
|
|
801
|
+
description: "Check your running app for security vulnerabilities and performance issues"
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
name: "fix-findings",
|
|
805
|
+
description: "Find all open brakit findings and fix them one by one"
|
|
806
|
+
}
|
|
807
|
+
];
|
|
808
|
+
PROMPT_MESSAGES = {
|
|
809
|
+
"check-app": [
|
|
810
|
+
"Check my running app for security and performance issues using brakit.",
|
|
811
|
+
"First get all findings, then get the endpoint summary.",
|
|
812
|
+
"For any critical or warning findings, get the request detail to understand the root cause.",
|
|
813
|
+
"Give me a clear report of what's wrong and offer to fix each issue."
|
|
814
|
+
].join(" "),
|
|
815
|
+
"fix-findings": [
|
|
816
|
+
"Get all open brakit findings.",
|
|
817
|
+
"For each finding, get the request detail to understand the exact issue.",
|
|
818
|
+
"Then find the source code responsible and fix it.",
|
|
819
|
+
"After fixing, ask me to re-trigger the endpoint so you can verify the fix with brakit."
|
|
820
|
+
].join(" ")
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// src/mcp/server.ts
|
|
826
|
+
var server_exports = {};
|
|
827
|
+
__export(server_exports, {
|
|
828
|
+
startMcpServer: () => startMcpServer
|
|
829
|
+
});
|
|
830
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
831
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
832
|
+
import {
|
|
833
|
+
ListToolsRequestSchema,
|
|
834
|
+
CallToolRequestSchema,
|
|
835
|
+
ListPromptsRequestSchema,
|
|
836
|
+
GetPromptRequestSchema
|
|
837
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
838
|
+
async function startMcpServer() {
|
|
839
|
+
let discovery;
|
|
840
|
+
try {
|
|
841
|
+
discovery = await waitForBrakit(void 0, INITIAL_DISCOVERY_TIMEOUT_MS);
|
|
842
|
+
} catch {
|
|
843
|
+
discovery = null;
|
|
844
|
+
}
|
|
845
|
+
let cachedClient = discovery ? new BrakitClient(discovery.baseUrl) : null;
|
|
846
|
+
const server = new Server(
|
|
847
|
+
{ name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
|
|
848
|
+
{ capabilities: { tools: {}, prompts: {} } }
|
|
849
|
+
);
|
|
850
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
851
|
+
prompts: [...PROMPTS]
|
|
852
|
+
}));
|
|
853
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => ({
|
|
854
|
+
description: PROMPTS.find((p) => p.name === request.params.name)?.description,
|
|
855
|
+
messages: [{
|
|
856
|
+
role: "user",
|
|
857
|
+
content: {
|
|
858
|
+
type: "text",
|
|
859
|
+
text: PROMPT_MESSAGES[request.params.name] ?? "Check my app for issues."
|
|
860
|
+
}
|
|
861
|
+
}]
|
|
862
|
+
}));
|
|
863
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
864
|
+
tools: getToolDefinitions()
|
|
865
|
+
}));
|
|
866
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
867
|
+
const { name, arguments: args } = request.params;
|
|
868
|
+
let activeClient = cachedClient;
|
|
869
|
+
if (!activeClient) {
|
|
870
|
+
try {
|
|
871
|
+
const disc = await waitForBrakit(void 0, LAZY_DISCOVERY_TIMEOUT_MS);
|
|
872
|
+
activeClient = new BrakitClient(disc.baseUrl);
|
|
873
|
+
cachedClient = activeClient;
|
|
874
|
+
} catch {
|
|
875
|
+
return {
|
|
876
|
+
content: [{
|
|
877
|
+
type: "text",
|
|
878
|
+
text: "Brakit is not running. Start your app with brakit enabled first."
|
|
879
|
+
}],
|
|
880
|
+
isError: true
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const alive = await activeClient.isAlive();
|
|
885
|
+
if (!alive) {
|
|
886
|
+
return {
|
|
887
|
+
content: [{
|
|
888
|
+
type: "text",
|
|
889
|
+
text: "Brakit appears to be down. Is your app still running?"
|
|
890
|
+
}],
|
|
891
|
+
isError: true
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
return await handleToolCall(activeClient, name, args ?? {});
|
|
896
|
+
} catch (err) {
|
|
897
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
898
|
+
return {
|
|
899
|
+
content: [{
|
|
900
|
+
type: "text",
|
|
901
|
+
text: `Error calling ${name}: ${message}`
|
|
902
|
+
}],
|
|
903
|
+
isError: true
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
const transport = new StdioServerTransport();
|
|
908
|
+
await server.connect(transport);
|
|
909
|
+
}
|
|
910
|
+
var init_server = __esm({
|
|
911
|
+
"src/mcp/server.ts"() {
|
|
912
|
+
"use strict";
|
|
913
|
+
init_client();
|
|
914
|
+
init_discovery();
|
|
915
|
+
init_tools();
|
|
916
|
+
init_mcp();
|
|
917
|
+
init_prompts();
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
|
|
2
921
|
// bin/brakit.ts
|
|
3
922
|
import { runMain } from "citty";
|
|
4
923
|
|
|
5
924
|
// src/cli/commands/install.ts
|
|
6
925
|
import { defineCommand } from "citty";
|
|
7
|
-
import { resolve as
|
|
8
|
-
import { readFile as readFile3, writeFile as
|
|
926
|
+
import { resolve as resolve4, join as join2 } from "path";
|
|
927
|
+
import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
|
|
9
928
|
import { execSync } from "child_process";
|
|
10
929
|
import pc from "picocolors";
|
|
11
930
|
|
|
12
|
-
// src/
|
|
13
|
-
|
|
14
|
-
import {
|
|
931
|
+
// src/store/finding-store.ts
|
|
932
|
+
init_constants();
|
|
933
|
+
import {
|
|
934
|
+
readFileSync as readFileSync2,
|
|
935
|
+
writeFileSync as writeFileSync2,
|
|
936
|
+
existsSync as existsSync2,
|
|
937
|
+
mkdirSync as mkdirSync2,
|
|
938
|
+
renameSync
|
|
939
|
+
} from "fs";
|
|
940
|
+
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
941
|
+
import { resolve as resolve2 } from "path";
|
|
15
942
|
|
|
16
943
|
// src/utils/fs.ts
|
|
17
944
|
import { access } from "fs/promises";
|
|
@@ -26,7 +953,12 @@ async function fileExists(path) {
|
|
|
26
953
|
}
|
|
27
954
|
}
|
|
28
955
|
|
|
956
|
+
// src/store/finding-store.ts
|
|
957
|
+
init_finding_id();
|
|
958
|
+
|
|
29
959
|
// src/detect/project.ts
|
|
960
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
961
|
+
import { join } from "path";
|
|
30
962
|
var FRAMEWORKS = [
|
|
31
963
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
32
964
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -386,6 +1318,31 @@ var corsCredentialsRule = {
|
|
|
386
1318
|
}
|
|
387
1319
|
};
|
|
388
1320
|
|
|
1321
|
+
// src/utils/response.ts
|
|
1322
|
+
init_thresholds();
|
|
1323
|
+
function unwrapResponse(parsed) {
|
|
1324
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
1325
|
+
const obj = parsed;
|
|
1326
|
+
const keys = Object.keys(obj);
|
|
1327
|
+
if (keys.length > 3) return parsed;
|
|
1328
|
+
let best = null;
|
|
1329
|
+
let bestSize = 0;
|
|
1330
|
+
for (const key of keys) {
|
|
1331
|
+
const val = obj[key];
|
|
1332
|
+
if (Array.isArray(val) && val.length > bestSize) {
|
|
1333
|
+
best = val;
|
|
1334
|
+
bestSize = val.length;
|
|
1335
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1336
|
+
const size = Object.keys(val).length;
|
|
1337
|
+
if (size > bestSize) {
|
|
1338
|
+
best = val;
|
|
1339
|
+
bestSize = size;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
389
1346
|
// src/analysis/rules/response-pii-leak.ts
|
|
390
1347
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
|
|
391
1348
|
var FULL_RECORD_MIN_FIELDS = 5;
|
|
@@ -430,28 +1387,6 @@ function hasInternalIds(obj) {
|
|
|
430
1387
|
}
|
|
431
1388
|
return false;
|
|
432
1389
|
}
|
|
433
|
-
function unwrapResponse(parsed) {
|
|
434
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
|
|
435
|
-
const obj = parsed;
|
|
436
|
-
const keys = Object.keys(obj);
|
|
437
|
-
if (keys.length > 3) return parsed;
|
|
438
|
-
let best = null;
|
|
439
|
-
let bestSize = 0;
|
|
440
|
-
for (const key of keys) {
|
|
441
|
-
const val = obj[key];
|
|
442
|
-
if (Array.isArray(val) && val.length > bestSize) {
|
|
443
|
-
best = val;
|
|
444
|
-
bestSize = val.length;
|
|
445
|
-
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
446
|
-
const size = Object.keys(val).length;
|
|
447
|
-
if (size > bestSize) {
|
|
448
|
-
best = val;
|
|
449
|
-
bestSize = size;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
return best && bestSize >= 3 ? best : parsed;
|
|
454
|
-
}
|
|
455
1390
|
function detectPII(method, reqBody, resBody) {
|
|
456
1391
|
const target = unwrapResponse(resBody);
|
|
457
1392
|
if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
|
|
@@ -537,9 +1472,8 @@ var responsePiiLeakRule = {
|
|
|
537
1472
|
}
|
|
538
1473
|
};
|
|
539
1474
|
|
|
540
|
-
// src/
|
|
541
|
-
|
|
542
|
-
var MAX_TELEMETRY_ENTRIES = 1e3;
|
|
1475
|
+
// src/store/request-store.ts
|
|
1476
|
+
init_constants();
|
|
543
1477
|
|
|
544
1478
|
// src/utils/static-patterns.ts
|
|
545
1479
|
var STATIC_PATTERNS = [
|
|
@@ -624,6 +1558,7 @@ var RequestStore = class {
|
|
|
624
1558
|
var defaultStore = new RequestStore();
|
|
625
1559
|
|
|
626
1560
|
// src/store/telemetry-store.ts
|
|
1561
|
+
init_constants();
|
|
627
1562
|
import { randomUUID } from "crypto";
|
|
628
1563
|
var TelemetryStore = class {
|
|
629
1564
|
constructor(maxEntries = MAX_TELEMETRY_ENTRIES) {
|
|
@@ -677,25 +1612,79 @@ var QueryStore = class extends TelemetryStore {
|
|
|
677
1612
|
var defaultQueryStore = new QueryStore();
|
|
678
1613
|
|
|
679
1614
|
// src/store/metrics/metrics-store.ts
|
|
1615
|
+
init_constants();
|
|
680
1616
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1617
|
+
init_endpoint();
|
|
681
1618
|
|
|
682
1619
|
// src/store/metrics/persistence.ts
|
|
1620
|
+
init_constants();
|
|
683
1621
|
import {
|
|
684
|
-
readFileSync as
|
|
685
|
-
writeFileSync as
|
|
686
|
-
mkdirSync as
|
|
687
|
-
existsSync as
|
|
1622
|
+
readFileSync as readFileSync3,
|
|
1623
|
+
writeFileSync as writeFileSync3,
|
|
1624
|
+
mkdirSync as mkdirSync3,
|
|
1625
|
+
existsSync as existsSync3,
|
|
688
1626
|
unlinkSync,
|
|
689
|
-
renameSync
|
|
1627
|
+
renameSync as renameSync2
|
|
690
1628
|
} from "fs";
|
|
691
|
-
import { writeFile as
|
|
692
|
-
import { resolve as
|
|
1629
|
+
import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
|
|
1630
|
+
import { resolve as resolve3 } from "path";
|
|
693
1631
|
|
|
694
1632
|
// src/analysis/group.ts
|
|
1633
|
+
init_constants();
|
|
695
1634
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
696
1635
|
|
|
1636
|
+
// src/analysis/label.ts
|
|
1637
|
+
init_constants();
|
|
1638
|
+
|
|
1639
|
+
// src/analysis/transforms.ts
|
|
1640
|
+
init_constants();
|
|
1641
|
+
|
|
1642
|
+
// src/analysis/insights/prepare.ts
|
|
1643
|
+
init_endpoint();
|
|
1644
|
+
init_constants();
|
|
1645
|
+
|
|
1646
|
+
// src/analysis/insights/rules/n1.ts
|
|
1647
|
+
init_endpoint();
|
|
1648
|
+
init_thresholds();
|
|
1649
|
+
|
|
1650
|
+
// src/analysis/insights/rules/cross-endpoint.ts
|
|
1651
|
+
init_endpoint();
|
|
1652
|
+
init_thresholds();
|
|
1653
|
+
|
|
1654
|
+
// src/analysis/insights/rules/redundant-query.ts
|
|
1655
|
+
init_endpoint();
|
|
1656
|
+
init_thresholds();
|
|
1657
|
+
|
|
1658
|
+
// src/analysis/insights/rules/error-hotspot.ts
|
|
1659
|
+
init_thresholds();
|
|
1660
|
+
|
|
1661
|
+
// src/analysis/insights/rules/duplicate.ts
|
|
1662
|
+
init_thresholds();
|
|
1663
|
+
|
|
1664
|
+
// src/analysis/insights/rules/slow.ts
|
|
1665
|
+
init_thresholds();
|
|
1666
|
+
|
|
1667
|
+
// src/analysis/insights/rules/query-heavy.ts
|
|
1668
|
+
init_thresholds();
|
|
1669
|
+
|
|
1670
|
+
// src/analysis/insights/rules/select-star.ts
|
|
1671
|
+
init_thresholds();
|
|
1672
|
+
|
|
1673
|
+
// src/analysis/insights/rules/high-rows.ts
|
|
1674
|
+
init_thresholds();
|
|
1675
|
+
|
|
1676
|
+
// src/analysis/insights/rules/response-overfetch.ts
|
|
1677
|
+
init_endpoint();
|
|
1678
|
+
init_thresholds();
|
|
1679
|
+
|
|
1680
|
+
// src/analysis/insights/rules/large-response.ts
|
|
1681
|
+
init_thresholds();
|
|
1682
|
+
|
|
1683
|
+
// src/analysis/insights/rules/regression.ts
|
|
1684
|
+
init_thresholds();
|
|
1685
|
+
|
|
697
1686
|
// src/index.ts
|
|
698
|
-
var VERSION = "0.
|
|
1687
|
+
var VERSION = "0.8.0";
|
|
699
1688
|
|
|
700
1689
|
// src/cli/commands/install.ts
|
|
701
1690
|
var IMPORT_LINE = `import "brakit";`;
|
|
@@ -715,7 +1704,7 @@ var install_default = defineCommand({
|
|
|
715
1704
|
}
|
|
716
1705
|
},
|
|
717
1706
|
async run({ args }) {
|
|
718
|
-
const rootDir =
|
|
1707
|
+
const rootDir = resolve4(args.dir);
|
|
719
1708
|
const pkgPath = join2(rootDir, "package.json");
|
|
720
1709
|
if (!await fileExists(pkgPath)) {
|
|
721
1710
|
console.error(pc.red(" No package.json found. Run this from your project root."));
|
|
@@ -753,6 +1742,12 @@ var install_default = defineCommand({
|
|
|
753
1742
|
} else {
|
|
754
1743
|
printManualInstructions(project.framework);
|
|
755
1744
|
}
|
|
1745
|
+
const mcpResult = await setupMcp(rootDir);
|
|
1746
|
+
if (mcpResult === "created" || mcpResult === "updated") {
|
|
1747
|
+
console.log(pc.green(" \u2713 Configured MCP for Claude Code / Cursor"));
|
|
1748
|
+
} else if (mcpResult === "exists") {
|
|
1749
|
+
console.log(pc.dim(" \u2713 MCP already configured"));
|
|
1750
|
+
}
|
|
756
1751
|
console.log();
|
|
757
1752
|
console.log(pc.dim(" Start your app and visit:"));
|
|
758
1753
|
console.log(pc.bold(" http://localhost:<port>/__brakit"));
|
|
@@ -809,7 +1804,7 @@ async function setupNextjs(rootDir) {
|
|
|
809
1804
|
`}`,
|
|
810
1805
|
``
|
|
811
1806
|
].join("\n");
|
|
812
|
-
await
|
|
1807
|
+
await writeFile4(absPath, content);
|
|
813
1808
|
return { action: "created", file: relPath, content };
|
|
814
1809
|
}
|
|
815
1810
|
async function setupNuxt(rootDir) {
|
|
@@ -825,9 +1820,9 @@ async function setupNuxt(rootDir) {
|
|
|
825
1820
|
const content = `${IMPORT_LINE}
|
|
826
1821
|
`;
|
|
827
1822
|
const dir = join2(rootDir, "server/plugins");
|
|
828
|
-
const { mkdirSync:
|
|
829
|
-
|
|
830
|
-
await
|
|
1823
|
+
const { mkdirSync: mkdirSync4 } = await import("fs");
|
|
1824
|
+
mkdirSync4(dir, { recursive: true });
|
|
1825
|
+
await writeFile4(absPath, content);
|
|
831
1826
|
return { action: "created", file: relPath, content };
|
|
832
1827
|
}
|
|
833
1828
|
async function setupPrepend(rootDir, ...candidates) {
|
|
@@ -838,7 +1833,7 @@ async function setupPrepend(rootDir, ...candidates) {
|
|
|
838
1833
|
if (content.includes(IMPORT_MARKER)) {
|
|
839
1834
|
return { action: "exists", file: relPath };
|
|
840
1835
|
}
|
|
841
|
-
await
|
|
1836
|
+
await writeFile4(absPath, `${IMPORT_LINE}
|
|
842
1837
|
${content}`);
|
|
843
1838
|
return { action: "prepended", file: relPath };
|
|
844
1839
|
}
|
|
@@ -872,6 +1867,45 @@ async function setupGeneric(rootDir) {
|
|
|
872
1867
|
if (result.action !== "manual") return result;
|
|
873
1868
|
return { action: "manual", file: null };
|
|
874
1869
|
}
|
|
1870
|
+
var MCP_CONFIG = {
|
|
1871
|
+
mcpServers: {
|
|
1872
|
+
brakit: {
|
|
1873
|
+
command: "npx",
|
|
1874
|
+
args: ["brakit", "mcp"]
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
async function setupMcp(rootDir) {
|
|
1879
|
+
const mcpPath = join2(rootDir, ".mcp.json");
|
|
1880
|
+
if (await fileExists(mcpPath)) {
|
|
1881
|
+
const raw = await readFile3(mcpPath, "utf-8");
|
|
1882
|
+
try {
|
|
1883
|
+
const config = JSON.parse(raw);
|
|
1884
|
+
if (config?.mcpServers?.brakit) return "exists";
|
|
1885
|
+
config.mcpServers = { ...config.mcpServers, ...MCP_CONFIG.mcpServers };
|
|
1886
|
+
await writeFile4(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1887
|
+
await ensureGitignoreMcp(rootDir);
|
|
1888
|
+
return "updated";
|
|
1889
|
+
} catch {
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
await writeFile4(mcpPath, JSON.stringify(MCP_CONFIG, null, 2) + "\n");
|
|
1893
|
+
await ensureGitignoreMcp(rootDir);
|
|
1894
|
+
return "created";
|
|
1895
|
+
}
|
|
1896
|
+
async function ensureGitignoreMcp(rootDir) {
|
|
1897
|
+
const gitignorePath = join2(rootDir, ".gitignore");
|
|
1898
|
+
try {
|
|
1899
|
+
if (await fileExists(gitignorePath)) {
|
|
1900
|
+
const content = await readFile3(gitignorePath, "utf-8");
|
|
1901
|
+
if (content.split("\n").some((l) => l.trim() === ".mcp.json")) return;
|
|
1902
|
+
await writeFile4(gitignorePath, content.trimEnd() + "\n.mcp.json\n");
|
|
1903
|
+
} else {
|
|
1904
|
+
await writeFile4(gitignorePath, ".mcp.json\n");
|
|
1905
|
+
}
|
|
1906
|
+
} catch {
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
875
1909
|
function printManualInstructions(framework) {
|
|
876
1910
|
console.log(pc.yellow(" \u26A0 Could not auto-detect entry file."));
|
|
877
1911
|
console.log();
|
|
@@ -891,10 +1925,11 @@ function printManualInstructions(framework) {
|
|
|
891
1925
|
|
|
892
1926
|
// src/cli/commands/uninstall.ts
|
|
893
1927
|
import { defineCommand as defineCommand2 } from "citty";
|
|
894
|
-
import { resolve as
|
|
895
|
-
import { readFile as readFile4, writeFile as
|
|
1928
|
+
import { resolve as resolve5, join as join3 } from "path";
|
|
1929
|
+
import { readFile as readFile4, writeFile as writeFile5, unlink, rm } from "fs/promises";
|
|
896
1930
|
import { execSync as execSync2 } from "child_process";
|
|
897
1931
|
import pc2 from "picocolors";
|
|
1932
|
+
init_constants();
|
|
898
1933
|
var IMPORT_LINE2 = `import "brakit";`;
|
|
899
1934
|
var CREATED_FILES = [
|
|
900
1935
|
"src/instrumentation.ts",
|
|
@@ -932,7 +1967,7 @@ var uninstall_default = defineCommand2({
|
|
|
932
1967
|
}
|
|
933
1968
|
},
|
|
934
1969
|
async run({ args }) {
|
|
935
|
-
const rootDir =
|
|
1970
|
+
const rootDir = resolve5(args.dir);
|
|
936
1971
|
let project = null;
|
|
937
1972
|
try {
|
|
938
1973
|
project = await detectProject(rootDir);
|
|
@@ -970,7 +2005,7 @@ var uninstall_default = defineCommand2({
|
|
|
970
2005
|
const content = await readFile4(absPath, "utf-8");
|
|
971
2006
|
if (!content.includes(IMPORT_LINE2)) continue;
|
|
972
2007
|
const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE2.trim()).join("\n");
|
|
973
|
-
await
|
|
2008
|
+
await writeFile5(absPath, updated);
|
|
974
2009
|
console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
|
|
975
2010
|
removed = true;
|
|
976
2011
|
break;
|
|
@@ -979,6 +2014,18 @@ var uninstall_default = defineCommand2({
|
|
|
979
2014
|
if (!removed) {
|
|
980
2015
|
console.log(pc2.dim(" No brakit instrumentation files found."));
|
|
981
2016
|
}
|
|
2017
|
+
const mcpRemoved = await removeMcpConfig(rootDir);
|
|
2018
|
+
if (mcpRemoved) {
|
|
2019
|
+
console.log(pc2.green(" \u2713 Removed brakit MCP configuration"));
|
|
2020
|
+
}
|
|
2021
|
+
const dataRemoved = await removeBrakitData(rootDir);
|
|
2022
|
+
if (dataRemoved) {
|
|
2023
|
+
console.log(pc2.green(" \u2713 Removed .brakit directory"));
|
|
2024
|
+
}
|
|
2025
|
+
const gitignoreCleaned = await cleanGitignore(rootDir);
|
|
2026
|
+
if (gitignoreCleaned) {
|
|
2027
|
+
console.log(pc2.green(" \u2713 Removed .brakit from .gitignore"));
|
|
2028
|
+
}
|
|
982
2029
|
const pm = project?.packageManager ?? "npm";
|
|
983
2030
|
const uninstalled = await uninstallPackage(rootDir, pm);
|
|
984
2031
|
if (uninstalled) {
|
|
@@ -987,6 +2034,24 @@ var uninstall_default = defineCommand2({
|
|
|
987
2034
|
console.log();
|
|
988
2035
|
}
|
|
989
2036
|
});
|
|
2037
|
+
async function removeMcpConfig(rootDir) {
|
|
2038
|
+
const mcpPath = join3(rootDir, ".mcp.json");
|
|
2039
|
+
if (!await fileExists(mcpPath)) return false;
|
|
2040
|
+
try {
|
|
2041
|
+
const raw = await readFile4(mcpPath, "utf-8");
|
|
2042
|
+
const config = JSON.parse(raw);
|
|
2043
|
+
if (!config?.mcpServers?.brakit) return false;
|
|
2044
|
+
delete config.mcpServers.brakit;
|
|
2045
|
+
if (Object.keys(config.mcpServers).length === 0) {
|
|
2046
|
+
await unlink(mcpPath);
|
|
2047
|
+
} else {
|
|
2048
|
+
await writeFile5(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
2049
|
+
}
|
|
2050
|
+
return true;
|
|
2051
|
+
} catch {
|
|
2052
|
+
return false;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
990
2055
|
async function uninstallPackage(rootDir, pm) {
|
|
991
2056
|
try {
|
|
992
2057
|
const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
|
|
@@ -1009,12 +2074,42 @@ async function uninstallPackage(rootDir, pm) {
|
|
|
1009
2074
|
}
|
|
1010
2075
|
return true;
|
|
1011
2076
|
}
|
|
2077
|
+
async function removeBrakitData(rootDir) {
|
|
2078
|
+
const dataDir = join3(rootDir, METRICS_DIR);
|
|
2079
|
+
if (!await fileExists(dataDir)) return false;
|
|
2080
|
+
try {
|
|
2081
|
+
await rm(dataDir, { recursive: true, force: true });
|
|
2082
|
+
return true;
|
|
2083
|
+
} catch {
|
|
2084
|
+
return false;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
async function cleanGitignore(rootDir) {
|
|
2088
|
+
const gitignorePath = join3(rootDir, ".gitignore");
|
|
2089
|
+
if (!await fileExists(gitignorePath)) return false;
|
|
2090
|
+
try {
|
|
2091
|
+
const content = await readFile4(gitignorePath, "utf-8");
|
|
2092
|
+
const lines = content.split("\n");
|
|
2093
|
+
const filtered = lines.filter((line) => line.trim() !== METRICS_DIR);
|
|
2094
|
+
if (filtered.length === lines.length) return false;
|
|
2095
|
+
await writeFile5(gitignorePath, filtered.join("\n"));
|
|
2096
|
+
return true;
|
|
2097
|
+
} catch {
|
|
2098
|
+
return false;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
1012
2101
|
|
|
1013
2102
|
// bin/brakit.ts
|
|
1014
2103
|
var sub = process.argv[2];
|
|
1015
2104
|
if (sub === "uninstall") {
|
|
1016
2105
|
process.argv.splice(2, 1);
|
|
1017
2106
|
runMain(uninstall_default);
|
|
2107
|
+
} else if (sub === "mcp") {
|
|
2108
|
+
Promise.resolve().then(() => (init_server(), server_exports)).then(({ startMcpServer: startMcpServer2 }) => startMcpServer2()).catch((err) => {
|
|
2109
|
+
process.stderr.write(`[brakit] MCP server failed: ${err.message}
|
|
2110
|
+
`);
|
|
2111
|
+
process.exitCode = 1;
|
|
2112
|
+
});
|
|
1018
2113
|
} else {
|
|
1019
2114
|
if (sub === "install") process.argv.splice(2, 1);
|
|
1020
2115
|
runMain(install_default);
|