brakit 0.7.6 → 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.
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/constants/routes.ts
12
- 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;
12
+ 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;
13
13
  var init_routes = __esm({
14
14
  "src/constants/routes.ts"() {
15
15
  "use strict";
@@ -29,6 +29,7 @@ var init_routes = __esm({
29
29
  DASHBOARD_API_INSIGHTS = "/__brakit/api/insights";
30
30
  DASHBOARD_API_SECURITY = "/__brakit/api/security";
31
31
  DASHBOARD_API_TAB = "/__brakit/api/tab";
32
+ DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
32
33
  }
33
34
  });
34
35
 
@@ -93,7 +94,7 @@ var init_transport = __esm({
93
94
  });
94
95
 
95
96
  // src/constants/metrics.ts
96
- var METRICS_DIR, METRICS_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS;
97
+ var METRICS_DIR, METRICS_FILE, METRICS_FLUSH_INTERVAL_MS, METRICS_MAX_SESSIONS, METRICS_MAX_DATA_POINTS, PORT_FILE, FINDINGS_FILE, FINDINGS_FLUSH_INTERVAL_MS;
97
98
  var init_metrics = __esm({
98
99
  "src/constants/metrics.ts"() {
99
100
  "use strict";
@@ -102,6 +103,9 @@ var init_metrics = __esm({
102
103
  METRICS_FLUSH_INTERVAL_MS = 3e4;
103
104
  METRICS_MAX_SESSIONS = 50;
104
105
  METRICS_MAX_DATA_POINTS = 200;
106
+ PORT_FILE = ".brakit/port";
107
+ FINDINGS_FILE = ".brakit/findings.json";
108
+ FINDINGS_FLUSH_INTERVAL_MS = 1e4;
105
109
  }
106
110
  });
107
111
 
@@ -148,6 +152,13 @@ var init_network = __esm({
148
152
  }
149
153
  });
150
154
 
155
+ // src/constants/mcp.ts
156
+ var init_mcp = __esm({
157
+ "src/constants/mcp.ts"() {
158
+ "use strict";
159
+ }
160
+ });
161
+
151
162
  // src/constants/index.ts
152
163
  var init_constants = __esm({
153
164
  "src/constants/index.ts"() {
@@ -159,6 +170,7 @@ var init_constants = __esm({
159
170
  init_metrics();
160
171
  init_headers();
161
172
  init_network();
173
+ init_mcp();
162
174
  }
163
175
  });
164
176
 
@@ -2055,6 +2067,33 @@ var init_insights = __esm({
2055
2067
  }
2056
2068
  });
2057
2069
 
2070
+ // src/dashboard/api/findings.ts
2071
+ function createFindingsHandler(findingStore) {
2072
+ return (req, res) => {
2073
+ if (!requireGet(req, res)) return;
2074
+ const url = new URL(req.url ?? "/", "http://localhost");
2075
+ const stateParam = url.searchParams.get("state");
2076
+ let findings;
2077
+ if (stateParam && VALID_STATES.has(stateParam)) {
2078
+ findings = findingStore.getByState(stateParam);
2079
+ } else {
2080
+ findings = findingStore.getAll();
2081
+ }
2082
+ sendJson(req, res, 200, {
2083
+ total: findings.length,
2084
+ findings
2085
+ });
2086
+ };
2087
+ }
2088
+ var VALID_STATES;
2089
+ var init_findings = __esm({
2090
+ "src/dashboard/api/findings.ts"() {
2091
+ "use strict";
2092
+ init_shared2();
2093
+ VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
2094
+ }
2095
+ });
2096
+
2058
2097
  // src/dashboard/sse.ts
2059
2098
  function createSSEHandler(engine) {
2060
2099
  return (req, res) => {
@@ -2687,6 +2726,197 @@ var init_styles = __esm({
2687
2726
  }
2688
2727
  });
2689
2728
 
2729
+ // src/store/finding-id.ts
2730
+ import { createHash } from "crypto";
2731
+ function computeFindingId(finding) {
2732
+ const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
2733
+ return createHash("sha256").update(key).digest("hex").slice(0, 16);
2734
+ }
2735
+ var init_finding_id = __esm({
2736
+ "src/store/finding-id.ts"() {
2737
+ "use strict";
2738
+ }
2739
+ });
2740
+
2741
+ // src/store/finding-store.ts
2742
+ import {
2743
+ readFileSync as readFileSync3,
2744
+ writeFileSync as writeFileSync3,
2745
+ existsSync as existsSync3,
2746
+ mkdirSync as mkdirSync3,
2747
+ renameSync as renameSync2
2748
+ } from "fs";
2749
+ import { writeFile as writeFile3, mkdir as mkdir2, rename as rename2 } from "fs/promises";
2750
+ import { resolve as resolve3 } from "path";
2751
+ var FindingStore;
2752
+ var init_finding_store = __esm({
2753
+ "src/store/finding-store.ts"() {
2754
+ "use strict";
2755
+ init_constants();
2756
+ init_fs();
2757
+ init_finding_id();
2758
+ FindingStore = class {
2759
+ constructor(rootDir) {
2760
+ this.rootDir = rootDir;
2761
+ this.metricsDir = resolve3(rootDir, METRICS_DIR);
2762
+ this.findingsPath = resolve3(rootDir, FINDINGS_FILE);
2763
+ this.tmpPath = this.findingsPath + ".tmp";
2764
+ this.load();
2765
+ }
2766
+ findings = /* @__PURE__ */ new Map();
2767
+ flushTimer = null;
2768
+ dirty = false;
2769
+ writing = false;
2770
+ findingsPath;
2771
+ tmpPath;
2772
+ metricsDir;
2773
+ start() {
2774
+ this.flushTimer = setInterval(
2775
+ () => this.flush(),
2776
+ FINDINGS_FLUSH_INTERVAL_MS
2777
+ );
2778
+ this.flushTimer.unref();
2779
+ }
2780
+ stop() {
2781
+ if (this.flushTimer) {
2782
+ clearInterval(this.flushTimer);
2783
+ this.flushTimer = null;
2784
+ }
2785
+ this.flushSync();
2786
+ }
2787
+ upsert(finding, source) {
2788
+ const id = computeFindingId(finding);
2789
+ const existing = this.findings.get(id);
2790
+ const now = Date.now();
2791
+ if (existing) {
2792
+ existing.lastSeenAt = now;
2793
+ existing.occurrences++;
2794
+ existing.finding = finding;
2795
+ if (existing.state === "resolved") {
2796
+ existing.state = "open";
2797
+ existing.resolvedAt = null;
2798
+ }
2799
+ this.dirty = true;
2800
+ return existing;
2801
+ }
2802
+ const stateful = {
2803
+ findingId: id,
2804
+ state: "open",
2805
+ source,
2806
+ finding,
2807
+ firstSeenAt: now,
2808
+ lastSeenAt: now,
2809
+ resolvedAt: null,
2810
+ occurrences: 1
2811
+ };
2812
+ this.findings.set(id, stateful);
2813
+ this.dirty = true;
2814
+ return stateful;
2815
+ }
2816
+ transition(findingId, state) {
2817
+ const finding = this.findings.get(findingId);
2818
+ if (!finding) return false;
2819
+ finding.state = state;
2820
+ if (state === "resolved") {
2821
+ finding.resolvedAt = Date.now();
2822
+ }
2823
+ this.dirty = true;
2824
+ return true;
2825
+ }
2826
+ reconcilePassive(currentFindings) {
2827
+ const currentIds = new Set(currentFindings.map(computeFindingId));
2828
+ for (const [id, stateful] of this.findings) {
2829
+ if (stateful.source === "passive" && stateful.state === "open" && !currentIds.has(id)) {
2830
+ stateful.state = "resolved";
2831
+ stateful.resolvedAt = Date.now();
2832
+ this.dirty = true;
2833
+ }
2834
+ }
2835
+ }
2836
+ getAll() {
2837
+ return [...this.findings.values()];
2838
+ }
2839
+ getByState(state) {
2840
+ return [...this.findings.values()].filter((f) => f.state === state);
2841
+ }
2842
+ get(findingId) {
2843
+ return this.findings.get(findingId);
2844
+ }
2845
+ clear() {
2846
+ this.findings.clear();
2847
+ this.dirty = true;
2848
+ }
2849
+ load() {
2850
+ try {
2851
+ if (existsSync3(this.findingsPath)) {
2852
+ const raw = readFileSync3(this.findingsPath, "utf-8");
2853
+ const parsed = JSON.parse(raw);
2854
+ if (parsed?.version === 1 && Array.isArray(parsed.findings)) {
2855
+ for (const f of parsed.findings) {
2856
+ this.findings.set(f.findingId, f);
2857
+ }
2858
+ }
2859
+ }
2860
+ } catch {
2861
+ }
2862
+ }
2863
+ flush() {
2864
+ if (!this.dirty) return;
2865
+ this.writeAsync();
2866
+ }
2867
+ flushSync() {
2868
+ if (!this.dirty) return;
2869
+ try {
2870
+ this.ensureDir();
2871
+ const data = {
2872
+ version: 1,
2873
+ findings: [...this.findings.values()]
2874
+ };
2875
+ writeFileSync3(this.tmpPath, JSON.stringify(data));
2876
+ renameSync2(this.tmpPath, this.findingsPath);
2877
+ this.dirty = false;
2878
+ } catch (err) {
2879
+ process.stderr.write(
2880
+ `[brakit] failed to save findings: ${err.message}
2881
+ `
2882
+ );
2883
+ }
2884
+ }
2885
+ async writeAsync() {
2886
+ if (this.writing) return;
2887
+ this.writing = true;
2888
+ try {
2889
+ if (!existsSync3(this.metricsDir)) {
2890
+ await mkdir2(this.metricsDir, { recursive: true });
2891
+ ensureGitignore(this.metricsDir, METRICS_DIR);
2892
+ }
2893
+ const data = {
2894
+ version: 1,
2895
+ findings: [...this.findings.values()]
2896
+ };
2897
+ await writeFile3(this.tmpPath, JSON.stringify(data));
2898
+ await rename2(this.tmpPath, this.findingsPath);
2899
+ this.dirty = false;
2900
+ } catch (err) {
2901
+ process.stderr.write(
2902
+ `[brakit] failed to save findings: ${err.message}
2903
+ `
2904
+ );
2905
+ } finally {
2906
+ this.writing = false;
2907
+ if (this.dirty) this.writeAsync();
2908
+ }
2909
+ }
2910
+ ensureDir() {
2911
+ if (!existsSync3(this.metricsDir)) {
2912
+ mkdirSync3(this.metricsDir, { recursive: true });
2913
+ ensureGitignore(this.metricsDir, METRICS_DIR);
2914
+ }
2915
+ }
2916
+ };
2917
+ }
2918
+ });
2919
+
2690
2920
  // src/detect/project.ts
2691
2921
  import { readFile as readFile2 } from "fs/promises";
2692
2922
  import { join } from "path";
@@ -4112,8 +4342,9 @@ var init_engine = __esm({
4112
4342
  init_rules();
4113
4343
  init_insights3();
4114
4344
  AnalysisEngine = class {
4115
- constructor(metricsStore, debounceMs = 300) {
4345
+ constructor(metricsStore, findingStore, debounceMs = 300) {
4116
4346
  this.metricsStore = metricsStore;
4347
+ this.findingStore = findingStore;
4117
4348
  this.debounceMs = debounceMs;
4118
4349
  this.scanner = createDefaultScanner();
4119
4350
  this.boundRequestListener = () => this.scheduleRecompute();
@@ -4174,6 +4405,12 @@ var init_engine = __esm({
4174
4405
  const fetches = defaultFetchStore.getAll();
4175
4406
  const flows = groupRequestsIntoFlows(requests);
4176
4407
  this.cachedFindings = this.scanner.scan({ requests, logs });
4408
+ if (this.findingStore) {
4409
+ for (const finding of this.cachedFindings) {
4410
+ this.findingStore.upsert(finding, "passive");
4411
+ }
4412
+ this.findingStore.reconcilePassive(this.cachedFindings);
4413
+ }
4177
4414
  this.cachedInsights = computeInsights({
4178
4415
  requests,
4179
4416
  queries,
@@ -4199,13 +4436,14 @@ var VERSION;
4199
4436
  var init_src = __esm({
4200
4437
  "src/index.ts"() {
4201
4438
  "use strict";
4439
+ init_finding_store();
4202
4440
  init_project();
4203
4441
  init_adapter_registry();
4204
4442
  init_rules();
4205
4443
  init_engine();
4206
4444
  init_insights3();
4207
4445
  init_insights2();
4208
- VERSION = "0.7.6";
4446
+ VERSION = "0.8.0";
4209
4447
  }
4210
4448
  });
4211
4449
 
@@ -6639,12 +6877,12 @@ var init_page = __esm({
6639
6877
  // src/telemetry/config.ts
6640
6878
  import { homedir } from "os";
6641
6879
  import { join as join2 } from "path";
6642
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
6880
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
6643
6881
  import { randomUUID as randomUUID5 } from "crypto";
6644
6882
  function readConfig() {
6645
6883
  try {
6646
- if (!existsSync3(CONFIG_PATH)) return null;
6647
- return JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
6884
+ if (!existsSync4(CONFIG_PATH)) return null;
6885
+ return JSON.parse(readFileSync4(CONFIG_PATH, "utf-8"));
6648
6886
  } catch {
6649
6887
  return null;
6650
6888
  }
@@ -6707,6 +6945,9 @@ function createDashboardHandler(deps) {
6707
6945
  routes[DASHBOARD_API_INSIGHTS] = createInsightsHandler(deps.analysisEngine);
6708
6946
  routes[DASHBOARD_API_SECURITY] = createSecurityHandler(deps.analysisEngine);
6709
6947
  }
6948
+ if (deps.findingStore) {
6949
+ routes[DASHBOARD_API_FINDINGS] = createFindingsHandler(deps.findingStore);
6950
+ }
6710
6951
  routes[DASHBOARD_API_TAB] = (req, res) => {
6711
6952
  const raw = (req.url ?? "").split("tab=")[1];
6712
6953
  if (raw) {
@@ -6739,6 +6980,7 @@ var init_router = __esm({
6739
6980
  init_constants();
6740
6981
  init_api();
6741
6982
  init_insights();
6983
+ init_findings();
6742
6984
  init_sse();
6743
6985
  init_page();
6744
6986
  init_telemetry();
@@ -6779,12 +7021,11 @@ function colorTitle(severity, text) {
6779
7021
  function truncate(s, max = 80) {
6780
7022
  return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
6781
7023
  }
6782
- function formatConsoleLine(insight, dashboardUrl, suffix) {
7024
+ function formatConsoleLine(insight, suffix) {
6783
7025
  const icon = severityIcon(insight.severity);
6784
7026
  const title = colorTitle(insight.severity, insight.title);
6785
7027
  const desc = pc.dim(truncate(insight.desc) + (suffix ?? ""));
6786
- const link = pc.dim(`\u2192 ${dashboardUrl}`);
6787
- let line = ` ${icon} ${title} \u2014 ${desc} ${link}`;
7028
+ let line = ` ${icon} ${title} \u2014 ${desc}`;
6788
7029
  if (insight.detail) {
6789
7030
  line += `
6790
7031
  ${pc.dim("\u2514 " + insight.detail)}`;
@@ -6810,11 +7051,13 @@ function createConsoleInsightListener(proxyPort, metricsStore) {
6810
7051
  suffix = ` (\u2191 from ${prev.p95DurationMs < 1e3 ? prev.p95DurationMs + "ms" : (prev.p95DurationMs / 1e3).toFixed(1) + "s"})`;
6811
7052
  }
6812
7053
  }
6813
- lines.push(formatConsoleLine(insight, dashUrl, suffix));
7054
+ lines.push(formatConsoleLine(insight, suffix));
6814
7055
  }
6815
7056
  if (lines.length > 0) {
6816
7057
  print("");
6817
7058
  for (const line of lines) print(line);
7059
+ print("");
7060
+ print(` ${pc.magenta(pc.bold("brakit"))} ${pc.dim("\u2192")} ${pc.dim("Dashboard:")} ${pc.underline(`http://${dashUrl}`)} ${pc.dim("or ask your AI:")} ${pc.bold('"Fix brakit findings"')}`);
6818
7061
  }
6819
7062
  };
6820
7063
  }
@@ -7050,6 +7293,8 @@ var setup_exports = {};
7050
7293
  __export(setup_exports, {
7051
7294
  setup: () => setup
7052
7295
  });
7296
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
7297
+ import { resolve as resolve4 } from "path";
7053
7298
  function setup() {
7054
7299
  if (initialized) return;
7055
7300
  initialized = true;
@@ -7062,7 +7307,9 @@ function setup() {
7062
7307
  const cwd = process.cwd();
7063
7308
  const metricsStore = new MetricsStore(new FileMetricsPersistence(cwd));
7064
7309
  metricsStore.start();
7065
- const analysisEngine = new AnalysisEngine(metricsStore);
7310
+ const findingStore = new FindingStore(cwd);
7311
+ findingStore.start();
7312
+ const analysisEngine = new AnalysisEngine(metricsStore, findingStore);
7066
7313
  analysisEngine.start();
7067
7314
  const config = {
7068
7315
  proxyPort: 0,
@@ -7070,7 +7317,7 @@ function setup() {
7070
7317
  showStatic: false,
7071
7318
  maxBodyCapture: DEFAULT_MAX_BODY_CAPTURE
7072
7319
  };
7073
- const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine });
7320
+ const handleDashboard = createDashboardHandler({ metricsStore, analysisEngine, findingStore });
7074
7321
  onRequest((req) => {
7075
7322
  const queries = defaultQueryStore.getByRequest(req.id);
7076
7323
  const fetches = defaultFetchStore.getByRequest(req.id);
@@ -7084,6 +7331,9 @@ function setup() {
7084
7331
  handleDashboard,
7085
7332
  config,
7086
7333
  onFirstRequest(port) {
7334
+ const dir = resolve4(cwd, METRICS_DIR);
7335
+ if (!existsSync5(dir)) mkdirSync5(dir, { recursive: true });
7336
+ writeFileSync5(resolve4(cwd, PORT_FILE), String(port));
7087
7337
  analysisEngine.onUpdate(createConsoleInsightListener(port, metricsStore));
7088
7338
  process.stdout.write(` brakit v${VERSION} \u2014 http://localhost:${port}${DASHBOARD_PREFIX}
7089
7339
  `);
@@ -7092,7 +7342,13 @@ function setup() {
7092
7342
  health.setTeardown(() => {
7093
7343
  uninstallInterceptor();
7094
7344
  analysisEngine.stop();
7345
+ findingStore.stop();
7095
7346
  metricsStore.stop();
7347
+ try {
7348
+ const portPath = resolve4(cwd, PORT_FILE);
7349
+ if (existsSync5(portPath)) unlinkSync2(portPath);
7350
+ } catch {
7351
+ }
7096
7352
  });
7097
7353
  }
7098
7354
  function routeEvent2(event) {
@@ -7123,6 +7379,7 @@ var init_setup = __esm({
7123
7379
  init_router();
7124
7380
  init_request_log();
7125
7381
  init_store();
7382
+ init_finding_store();
7126
7383
  init_engine();
7127
7384
  init_terminal();
7128
7385
  init_src();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brakit",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "description": "See what your API is really doing. Security scanning, N+1 detection, duplicate calls, DB queries — one command, zero config.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,6 +28,9 @@
28
28
  "typecheck": "tsc --noEmit",
29
29
  "test": "vitest run",
30
30
  "test:watch": "vitest",
31
+ "test:contracts": "vitest run tests/contracts tests/adapters",
32
+ "test:integration": "vitest run tests/integration",
33
+ "ci": "npm run typecheck && npm test && npm run build",
31
34
  "lint": "tsc --noEmit"
32
35
  },
33
36
  "repository": {
@@ -40,6 +43,7 @@
40
43
  },
41
44
  "author": "Brakit <dev@brakit.ai> (https://brakit.ai)",
42
45
  "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.0.0",
43
47
  "citty": "^0.1.6",
44
48
  "picocolors": "^1.1.1"
45
49
  },