brakit 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <h1 align="center"><img src="docs/images/icon.png" height="24" alt="" />&nbsp;&nbsp;Brakit</h1>
2
2
 
3
3
  <p align="center">
4
- <b>See what your app is actually doing.</b> <br />
5
- Every request, query, and security issue before you ship. <br />
6
- <b>Open source · Local first · Zero config · 2 dependencies</b>
4
+ <b>AI writes your code. Brakit watches what it does.</b> <br />
5
+ Every request, query, and security issue, caught before you ship. <br />
6
+ <b>Open source · Local first · Zero config · AI-native via MCP</b>
7
7
  </p>
8
8
 
9
9
  <h3 align="center">
@@ -22,6 +22,9 @@
22
22
  <a href="https://typescriptlang.org">
23
23
  <img src="https://img.shields.io/badge/built%20with-TypeScript-3178c6.svg" alt="TypeScript" />
24
24
  </a>
25
+ <a href="https://www.npmjs.com/package/brakit">
26
+ <img src="https://img.shields.io/npm/v/brakit" alt="npm version" />
27
+ </a>
25
28
  <a href="CONTRIBUTING.md">
26
29
  <img src="https://img.shields.io/badge/PRs-Welcome-brightgreen" alt="PRs welcome!" />
27
30
  </a>
@@ -30,7 +33,15 @@
30
33
  ---
31
34
 
32
35
  <p align="center">
33
- <img width="700" src="docs/images/dashboard.png" alt="Brakit Dashboard" />
36
+ <img width="700" src="docs/images/dashboard.png" alt="Brakit Dashboard — issues surfaced automatically" />
37
+ <br />
38
+ <sub>Brakit catches N+1 queries, PII leaks, and slow endpoints as you develop</sub>
39
+ </p>
40
+
41
+ <p align="center">
42
+ <img width="700" src="docs/images/vscode.png" alt="Claude reading Brakit findings in VS Code" />
43
+ <br />
44
+ <sub>Claude reads findings via MCP and fixes your code</sub>
34
45
  </p>
35
46
 
36
47
  ## Quick Start
@@ -63,7 +74,7 @@ Dashboard at `http://localhost:<port>/__brakit`. Insights in the terminal.
63
74
  - **Full server tracing** — fetch calls, DB queries, console logs, errors — zero code changes
64
75
  - **Response overfetch** — large JSON responses with many fields your client doesn't use
65
76
  - **Performance tracking** — health grades and p95 trends across dev sessions
66
- - **AI-native via MCP** — Claude, Cursor, and other AI tools can query findings, inspect endpoints, and verify fixes directly
77
+ - **AI-native via MCP** — Claude Code and Cursor can query findings, inspect endpoints, and verify fixes directly
67
78
 
68
79
  ---
69
80
 
@@ -177,7 +188,7 @@ npm run typecheck # Type-check without emitting
177
188
  npm test # Run tests with vitest
178
189
  ```
179
190
 
180
- Only 2 production dependencies: `citty` (CLI) and `picocolors` (terminal colors). Everything else is Node.js built-ins.
191
+ Minimal production dependencies. Everything else is Node.js built-ins.
181
192
 
182
193
  ### Architecture
183
194
 
package/dist/api.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { IncomingHttpHeaders } from 'node:http';
2
+
1
3
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | (string & {});
2
4
  type FlatHeaders = Record<string, string>;
3
5
  interface TracedRequest {
@@ -117,10 +119,6 @@ interface EndpointMetrics {
117
119
  sessions: SessionMetric[];
118
120
  dataPoints?: LiveRequestPoint[];
119
121
  }
120
- interface MetricsData {
121
- version: 1;
122
- endpoints: EndpointMetrics[];
123
- }
124
122
  interface LiveRequestPoint {
125
123
  timestamp: number;
126
124
  durationMs: number;
@@ -149,7 +147,9 @@ interface RequestMetrics {
149
147
  fetchTimeMs: number;
150
148
  }
151
149
 
152
- type SecuritySeverity = "critical" | "warning" | "info";
150
+ /** Shared severity levels used by both security findings and insights. */
151
+ type Severity = "critical" | "warning" | "info";
152
+ type SecuritySeverity = Severity;
153
153
  interface SecurityFinding {
154
154
  severity: SecuritySeverity;
155
155
  rule: string;
@@ -178,20 +178,81 @@ interface FindingsData {
178
178
  findings: StatefulFinding[];
179
179
  }
180
180
 
181
+ type InsightSeverity = Severity;
182
+ type InsightType = "n1" | "cross-endpoint" | "redundant-query" | "error" | "error-hotspot" | "duplicate" | "slow" | "query-heavy" | "select-star" | "high-rows" | "large-response" | "response-overfetch" | "security" | "regression";
183
+ interface Insight {
184
+ severity: InsightSeverity;
185
+ type: InsightType;
186
+ title: string;
187
+ desc: string;
188
+ hint: string;
189
+ detail?: string;
190
+ nav?: string;
191
+ }
192
+ interface InsightContext {
193
+ requests: readonly TracedRequest[];
194
+ queries: readonly TracedQuery[];
195
+ errors: readonly TracedError[];
196
+ flows: readonly RequestFlow[];
197
+ fetches: readonly TracedFetch[];
198
+ previousMetrics?: readonly EndpointMetrics[];
199
+ securityFindings?: readonly SecurityFinding[];
200
+ }
201
+ interface EndpointGroup {
202
+ total: number;
203
+ errors: number;
204
+ totalDuration: number;
205
+ queryCount: number;
206
+ totalSize: number;
207
+ totalQueryTimeMs: number;
208
+ totalFetchTimeMs: number;
209
+ queryShapeDurations: Map<string, {
210
+ totalMs: number;
211
+ count: number;
212
+ label: string;
213
+ }>;
214
+ }
215
+ interface PreparedInsightContext extends InsightContext {
216
+ nonStatic: readonly TracedRequest[];
217
+ queriesByReq: ReadonlyMap<string, TracedQuery[]>;
218
+ fetchesByReq: ReadonlyMap<string, TracedFetch[]>;
219
+ reqById: ReadonlyMap<string, TracedRequest>;
220
+ endpointGroups: ReadonlyMap<string, EndpointGroup>;
221
+ }
222
+
223
+ type InsightState = "open" | "resolved";
224
+ interface StatefulInsight {
225
+ key: string;
226
+ state: InsightState;
227
+ insight: Insight;
228
+ firstSeenAt: number;
229
+ lastSeenAt: number;
230
+ resolvedAt: number | null;
231
+ /** Consecutive recompute cycles where the insight was not detected. */
232
+ consecutiveAbsences: number;
233
+ }
234
+
181
235
  declare class FindingStore {
182
236
  private rootDir;
183
237
  private findings;
184
238
  private flushTimer;
185
239
  private dirty;
186
- private writing;
240
+ private readonly writer;
187
241
  private readonly findingsPath;
188
- private readonly tmpPath;
189
- private readonly metricsDir;
190
242
  constructor(rootDir: string);
191
243
  start(): void;
192
244
  stop(): void;
193
245
  upsert(finding: SecurityFinding, source: FindingSource): StatefulFinding;
194
246
  transition(findingId: string, state: FindingState): boolean;
247
+ /**
248
+ * Reconcile passive findings against the current analysis results.
249
+ *
250
+ * Passive findings are detected by continuous scanning (not user-triggered).
251
+ * When a previously-seen finding is absent from the current results, it means
252
+ * the issue has been fixed — transition it to "resolved" automatically.
253
+ * Active findings (from MCP verify-fix) are not auto-resolved because they
254
+ * require explicit verification.
255
+ */
195
256
  reconcilePassive(currentFindings: readonly SecurityFinding[]): void;
196
257
  getAll(): readonly StatefulFinding[];
197
258
  getByState(state: FindingState): readonly StatefulFinding[];
@@ -200,8 +261,7 @@ declare class FindingStore {
200
261
  private load;
201
262
  private flush;
202
263
  private flushSync;
203
- private writeAsync;
204
- private ensureDir;
264
+ private serialize;
205
265
  }
206
266
 
207
267
  interface BrakitAdapter {
@@ -231,48 +291,6 @@ declare class SecurityScanner {
231
291
  }
232
292
  declare function createDefaultScanner(): SecurityScanner;
233
293
 
234
- type InsightSeverity = "critical" | "warning" | "info";
235
- type InsightType = "n1" | "cross-endpoint" | "redundant-query" | "error" | "error-hotspot" | "duplicate" | "slow" | "query-heavy" | "select-star" | "high-rows" | "large-response" | "response-overfetch" | "security" | "regression";
236
- interface Insight {
237
- severity: InsightSeverity;
238
- type: InsightType;
239
- title: string;
240
- desc: string;
241
- hint: string;
242
- detail?: string;
243
- nav?: string;
244
- }
245
- interface InsightContext {
246
- requests: readonly TracedRequest[];
247
- queries: readonly TracedQuery[];
248
- errors: readonly TracedError[];
249
- flows: readonly RequestFlow[];
250
- fetches: readonly TracedFetch[];
251
- previousMetrics?: readonly EndpointMetrics[];
252
- securityFindings?: readonly SecurityFinding[];
253
- }
254
- interface EndpointGroup {
255
- total: number;
256
- errors: number;
257
- totalDuration: number;
258
- queryCount: number;
259
- totalSize: number;
260
- totalQueryTimeMs: number;
261
- totalFetchTimeMs: number;
262
- queryShapeDurations: Map<string, {
263
- totalMs: number;
264
- count: number;
265
- label: string;
266
- }>;
267
- }
268
- interface PreparedInsightContext extends InsightContext {
269
- nonStatic: readonly TracedRequest[];
270
- queriesByReq: ReadonlyMap<string, TracedQuery[]>;
271
- fetchesByReq: ReadonlyMap<string, TracedFetch[]>;
272
- reqById: ReadonlyMap<string, TracedRequest>;
273
- endpointGroups: ReadonlyMap<string, EndpointGroup>;
274
- }
275
-
276
294
  interface InsightRule {
277
295
  id: InsightType;
278
296
  check(ctx: PreparedInsightContext): Insight[];
@@ -298,55 +316,120 @@ declare class AdapterRegistry {
298
316
  getActive(): readonly BrakitAdapter[];
299
317
  }
300
318
 
301
- interface MetricsPersistence {
302
- load(): MetricsData;
303
- save(data: MetricsData): void;
304
- saveSync(data: MetricsData): void;
305
- remove(): void;
319
+ interface AnalysisUpdate {
320
+ insights: Insight[];
321
+ findings: SecurityFinding[];
322
+ statefulFindings: readonly StatefulFinding[];
323
+ statefulInsights: readonly StatefulInsight[];
324
+ }
325
+ interface ChannelMap {
326
+ "telemetry:fetch": Omit<TracedFetch, "id">;
327
+ "telemetry:query": Omit<TracedQuery, "id">;
328
+ "telemetry:log": Omit<TracedLog, "id">;
329
+ "telemetry:error": Omit<TracedError, "id">;
330
+ "request:completed": TracedRequest;
331
+ "analysis:updated": AnalysisUpdate;
332
+ "store:cleared": void;
333
+ }
334
+ type Listener<T> = (data: T) => void;
335
+ declare class EventBus {
336
+ private listeners;
337
+ emit<K extends keyof ChannelMap>(channel: K, data: ChannelMap[K]): void;
338
+ on<K extends keyof ChannelMap>(channel: K, fn: Listener<ChannelMap[K]>): () => void;
339
+ off<K extends keyof ChannelMap>(channel: K, fn: Listener<ChannelMap[K]>): void;
306
340
  }
307
341
 
308
- declare class MetricsStore {
309
- private persistence;
310
- private data;
311
- private endpointIndex;
312
- private sessionId;
313
- private sessionStart;
314
- private flushTimer;
315
- private accumulators;
316
- private pendingPoints;
317
- constructor(persistence: MetricsPersistence);
318
- start(): void;
319
- stop(): void;
342
+ interface CaptureInput {
343
+ requestId: string;
344
+ method: string;
345
+ url: string;
346
+ requestHeaders: IncomingHttpHeaders;
347
+ requestBody: Buffer | null;
348
+ statusCode: number;
349
+ responseHeaders: IncomingHttpHeaders;
350
+ responseBody: Buffer | null;
351
+ responseContentType: string;
352
+ startTime: number;
353
+ endTime?: number;
354
+ config: Pick<BrakitConfig, "maxBodyCapture">;
355
+ }
356
+
357
+ interface TelemetryStoreInterface<T extends TelemetryEntry> {
358
+ add(data: Omit<T, "id">): T;
359
+ getAll(): readonly T[];
360
+ getByRequest(requestId: string): T[];
361
+ clear(): void;
362
+ }
363
+ interface RequestStoreInterface {
364
+ capture(input: CaptureInput): TracedRequest;
365
+ getAll(): readonly TracedRequest[];
366
+ clear(): void;
367
+ }
368
+ interface MetricsStoreInterface {
320
369
  recordRequest(req: TracedRequest, metrics: RequestMetrics): void;
321
370
  getAll(): readonly EndpointMetrics[];
322
371
  getEndpoint(endpoint: string): EndpointMetrics | undefined;
323
372
  getLiveEndpoints(): LiveEndpointData[];
324
373
  reset(): void;
325
- flush(sync?: boolean): void;
326
- private getOrCreateEndpoint;
374
+ start(): void;
375
+ stop(): void;
376
+ }
377
+ interface FindingStoreInterface {
378
+ upsert(finding: SecurityFinding, source: FindingSource): StatefulFinding;
379
+ transition(findingId: string, state: FindingState): boolean;
380
+ reconcilePassive(findings: readonly SecurityFinding[]): void;
381
+ getAll(): readonly StatefulFinding[];
382
+ getByState(state: FindingState): readonly StatefulFinding[];
383
+ get(findingId: string): StatefulFinding | undefined;
384
+ clear(): void;
385
+ start(): void;
386
+ stop(): void;
387
+ }
388
+ interface AnalysisEngineInterface {
389
+ start(): void;
390
+ stop(): void;
391
+ recompute(): void;
392
+ getInsights(): readonly Insight[];
393
+ getFindings(): readonly SecurityFinding[];
394
+ getStatefulInsights(): readonly StatefulInsight[];
395
+ getStatefulFindings(): readonly StatefulFinding[];
396
+ }
397
+
398
+ interface ServiceMap {
399
+ "event-bus": EventBus;
400
+ "request-store": RequestStoreInterface;
401
+ "query-store": TelemetryStoreInterface<TracedQuery>;
402
+ "fetch-store": TelemetryStoreInterface<TracedFetch>;
403
+ "log-store": TelemetryStoreInterface<TracedLog>;
404
+ "error-store": TelemetryStoreInterface<TracedError>;
405
+ "metrics-store": MetricsStoreInterface;
406
+ "finding-store": FindingStoreInterface;
407
+ "analysis-engine": AnalysisEngineInterface;
408
+ }
409
+ declare class ServiceRegistry {
410
+ private services;
411
+ register<K extends keyof ServiceMap>(name: K, service: ServiceMap[K]): void;
412
+ get<K extends keyof ServiceMap>(name: K): ServiceMap[K];
413
+ has<K extends keyof ServiceMap>(name: K): boolean;
327
414
  }
328
415
 
329
- type AnalysisListener = (insights: Insight[], findings: SecurityFinding[]) => void;
330
416
  declare class AnalysisEngine {
331
- private metricsStore;
332
- private findingStore?;
417
+ private registry;
333
418
  private debounceMs;
334
419
  private scanner;
420
+ private insightTracker;
335
421
  private cachedInsights;
336
422
  private cachedFindings;
423
+ private cachedStatefulInsights;
337
424
  private debounceTimer;
338
- private listeners;
339
- private boundRequestListener;
340
- private boundQueryListener;
341
- private boundErrorListener;
342
- private boundLogListener;
343
- constructor(metricsStore: MetricsStore, findingStore?: FindingStore | undefined, debounceMs?: number);
425
+ private subs;
426
+ constructor(registry: ServiceRegistry, debounceMs?: number);
344
427
  start(): void;
345
428
  stop(): void;
346
- onUpdate(fn: AnalysisListener): void;
347
- offUpdate(fn: AnalysisListener): void;
348
429
  getInsights(): readonly Insight[];
349
430
  getFindings(): readonly SecurityFinding[];
431
+ getStatefulFindings(): readonly StatefulFinding[];
432
+ getStatefulInsights(): readonly StatefulInsight[];
350
433
  private scheduleRecompute;
351
434
  recompute(): void;
352
435
  }