@buoy-gg/mcp 7.0.20 → 7.0.22

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/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  SyncClient,
4
4
  createMcpServer,
5
5
  runInit
6
- } from "./chunk-S5NQ6EUN.mjs";
6
+ } from "./chunk-5C6BLMHY.mjs";
7
7
 
8
8
  // src/cli.ts
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/dist/index.d.mts CHANGED
@@ -57,11 +57,31 @@ declare class SyncClient {
57
57
  private pendingActions;
58
58
  /** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
59
59
  private snapshotWaiters;
60
+ /** When each cached snapshot arrived, keyed by snapKey (staleness reporting). */
61
+ private snapshotAt;
62
+ /** Liveness per device — see {@link noteTimeout}. */
63
+ private health;
64
+ /** Dev-server origin per device — see {@link noteDevServerOrigin}. */
65
+ private devServerOrigins;
66
+ /** Devices whose origin probe already ran (a production bundle has none). */
67
+ private originProbed;
68
+ /** Last time each device appeared in the broker's device list. */
69
+ private lastSeen;
60
70
  constructor(opts?: SyncClientOptions);
61
71
  private readonly autoSpawn;
62
72
  /** Connect to a running broker, or spawn one in-process, then handshake. */
63
73
  start(): Promise<BrokerMode>;
64
74
  private connectSocket;
75
+ /**
76
+ * Drop everything remembered about devices that have been gone a while.
77
+ *
78
+ * Deliberately NOT on disconnect: a crashed app often drops off the broker,
79
+ * and its cached console snapshot is the crash evidence — throwing that away
80
+ * the instant it disconnects would delete the answer. But device ids change
81
+ * with every simulator erase and fresh dev client, and each one can hold
82
+ * multi-MB snapshots, so a long-lived server has to forget eventually.
83
+ */
84
+ private pruneStaleDevices;
65
85
  /** Disconnect and tear down a spawned broker, if any. */
66
86
  stop(): Promise<void>;
67
87
  /** All devices the broker knows about, merged with their advertised tools. */
@@ -91,6 +111,61 @@ declare class SyncClient {
91
111
  * is blocked.
92
112
  */
93
113
  requireProDevice(deviceId?: string): string;
114
+ private healthOf;
115
+ /** The device answered — it's alive. */
116
+ private noteOk;
117
+ /**
118
+ * The device didn't answer. A crashed app never answers again, so after the
119
+ * first miss we stop paying the full timeout on every subsequent call.
120
+ */
121
+ private noteTimeout;
122
+ /** True once a device has missed a call and hasn't answered since. */
123
+ isUnresponsive(deviceId: string): boolean;
124
+ /**
125
+ * Full timeout normally; a short one for a device already not answering.
126
+ *
127
+ * With one escape hatch: every RECOVERY_PROBE_MS a flagged device gets the
128
+ * full budget again. Without it the flag is self-sustaining — a device whose
129
+ * work legitimately takes longer than the short budget (a big snapshot, a
130
+ * slow simulator) can never answer, so it can never clear the flag, and every
131
+ * later reply is a confident "most likely CRASHED" about a healthy app.
132
+ */
133
+ private timeoutFor;
134
+ /**
135
+ * Plain-language diagnosis for a device that isn't answering, including the
136
+ * app's last words if the console tool got a crash flush out in time.
137
+ *
138
+ * A silent timeout is the worst possible answer here: the app is on a redbox,
139
+ * the reason is already captured, and without this the caller reads "timed
140
+ * out" and keeps retrying the same dead device.
141
+ */
142
+ unresponsiveNote(deviceId: string): string;
143
+ /**
144
+ * The newest error line the console tool synced for this device, if any.
145
+ * Reads the cached console snapshot rather than the device (which, by the
146
+ * time this matters, is not answering).
147
+ */
148
+ private lastFatalFor;
149
+ /** Age of the cached snapshot for a tool, in ms (null = never received). */
150
+ snapshotAgeMs(toolId: string, deviceId: string): number | null;
151
+ /**
152
+ * Remember where a device's bundle came from, reported by core's `app`
153
+ * adapter while the app is healthy. It is the only way to reload an app that
154
+ * has crashed: the in-app reload needs the React tree that the crash
155
+ * destroyed, but the dev server's reload is handled natively.
156
+ */
157
+ noteDevServerOrigin(deviceId: string, origin: unknown): void;
158
+ /**
159
+ * Ask a freshly-announced device where its bundle came from, once.
160
+ *
161
+ * Deliberately eager: this is the piece of information that can ONLY be
162
+ * collected while the app is healthy, and is only ever needed after it has
163
+ * stopped answering. One cheap round-trip per device buys the ability to
164
+ * recover a crashed app instead of telling the caller to go do it by hand.
165
+ */
166
+ private learnDevServerOrigin;
167
+ /** The dev-server origin last reported by this device, if any. */
168
+ devServerOrigin(deviceId: string): string | null;
94
169
  /**
95
170
  * Keep the device RECORDING this tool, without asking it to stream.
96
171
  *
@@ -135,10 +210,14 @@ declare class SyncClient {
135
210
  */
136
211
  getSnapshot(toolId: string, deviceId?: string, opts?: {
137
212
  timeoutMs?: number;
213
+ silent?: boolean;
138
214
  }): Promise<{
139
215
  deviceId: string;
140
216
  payload: unknown;
217
+ stale?: string;
141
218
  }>;
219
+ /** Header line for data served from cache because the device went quiet. */
220
+ private staleNote;
142
221
  private waitForSnapshot;
143
222
  /**
144
223
  * Invoke a remote action on a device and await its result. Pass deviceId to
@@ -146,14 +225,10 @@ declare class SyncClient {
146
225
  */
147
226
  callAction(toolId: string, action: string, params?: unknown, deviceId?: string, opts?: {
148
227
  timeoutMs?: number;
228
+ silent?: boolean;
149
229
  }): Promise<unknown>;
150
230
  }
151
231
 
152
- /**
153
- * Build the Buoy MCP server and register all tools against a connected
154
- * {@link SyncClient}. Tool packs (perf-monitor, etc.) are layered on top of the
155
- * generic core here.
156
- */
157
232
  declare function createMcpServer(sync: SyncClient): McpServer;
158
233
 
159
234
  /**
@@ -381,10 +456,12 @@ interface IndexEntry {
381
456
  /** Render-capture mirror (absent when capture was unavailable/disabled). */
382
457
  renderCommits?: number;
383
458
  renderTotalMs?: number;
459
+ renderWasted?: number;
384
460
  topRenderers?: {
385
461
  name: string;
386
462
  renders: number;
387
463
  totalMs: number;
464
+ cause?: string;
388
465
  }[];
389
466
  }
390
467
  /** Per-metric min/max/avg/p95 channel. */
@@ -406,12 +483,40 @@ interface BenchmarkReportStats {
406
483
  uiJankFrames: number;
407
484
  deviceMaxRefreshRate: number;
408
485
  }
409
-
410
486
  /**
411
- * Pure helpers for the perf-monitor pack: build an AutomationConfig from
412
- * MCP-friendly input, estimate batch duration, and rank a finished batch's
413
- * results into a winner + comparison table. No network/sync — unit-testable.
487
+ * Why components rendered, counted over a run.
488
+ *
489
+ * `propsUnstable` is the actionable one: props changed by IDENTITY only —
490
+ * a fresh arrow or object literal handed down every render — while every
491
+ * value stayed equal. Reporting that as plain "props" sends you into the
492
+ * child, when the fix is always in the parent.
414
493
  */
494
+ interface RenderCauseCounts {
495
+ mount?: number;
496
+ props?: number;
497
+ propsUnstable?: number;
498
+ hooks?: number;
499
+ parent?: number;
500
+ }
501
+ interface RenderCapture {
502
+ totalCommits: number;
503
+ totalRenders: number;
504
+ capturedDurations: boolean;
505
+ totalRenderMs?: number;
506
+ /** Parent cascades + identity-only prop churn: the removable share. */
507
+ wastedRenders?: number;
508
+ causes?: RenderCauseCounts;
509
+ topComponents: {
510
+ name: string;
511
+ renders: number;
512
+ totalMs: number;
513
+ maxMs: number;
514
+ avgMs: number;
515
+ causes?: RenderCauseCounts;
516
+ changedProps?: string[];
517
+ parentName?: string;
518
+ }[];
519
+ }
415
520
 
416
521
  interface BuildConfigInput {
417
522
  targetRoute: string;
@@ -464,10 +569,12 @@ interface RankRow {
464
569
  /** React commits during the median run (null when capture was off/unavailable). */
465
570
  renderCommits: number | null;
466
571
  /** Top components by total render self-time during the median run. */
572
+ renderWasted?: number;
467
573
  topRenderers: {
468
574
  name: string;
469
575
  renders: number;
470
576
  totalMs: number;
577
+ cause?: string;
471
578
  }[];
472
579
  }
473
580
  interface BatchRanking {
@@ -506,14 +613,44 @@ declare function rankBatchFromIndex(index: IndexEntry[], batchId: string, casesO
506
613
  }[]): BatchRanking;
507
614
  /** Render a batch ranking as a trade-off-aware report (no single false winner). */
508
615
  declare function formatRankingMarkdown(ranking: BatchRanking): string;
509
- /** Side-by-side comparison of two full report stats. */
616
+ /**
617
+ * Side-by-side comparison of two full reports.
618
+ *
619
+ * Renders are part of the comparison, not an afterthought: when someone runs
620
+ * this after a memoization fix, "did the render count actually drop" IS the
621
+ * question, and for a long while this function silently dropped the block
622
+ * that answers it.
623
+ */
510
624
  declare function compareReportStats(a: {
511
625
  name: string;
512
626
  stats: BenchmarkReportStats;
627
+ renders?: RenderCapture;
513
628
  }, b: {
514
629
  name: string;
515
630
  stats: BenchmarkReportStats;
631
+ renders?: RenderCapture;
516
632
  }): {
633
+ renders?: {
634
+ total: {
635
+ a: number;
636
+ b: number;
637
+ delta: number;
638
+ };
639
+ commits: {
640
+ a: number;
641
+ b: number;
642
+ delta: number;
643
+ };
644
+ wasted: {
645
+ a: number;
646
+ b: number;
647
+ delta: number;
648
+ };
649
+ causes: {
650
+ a: RenderCauseCounts;
651
+ b: RenderCauseCounts;
652
+ };
653
+ } | undefined;
517
654
  a: string;
518
655
  b: string;
519
656
  uiFps: {
package/dist/index.d.ts CHANGED
@@ -57,11 +57,31 @@ declare class SyncClient {
57
57
  private pendingActions;
58
58
  /** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
59
59
  private snapshotWaiters;
60
+ /** When each cached snapshot arrived, keyed by snapKey (staleness reporting). */
61
+ private snapshotAt;
62
+ /** Liveness per device — see {@link noteTimeout}. */
63
+ private health;
64
+ /** Dev-server origin per device — see {@link noteDevServerOrigin}. */
65
+ private devServerOrigins;
66
+ /** Devices whose origin probe already ran (a production bundle has none). */
67
+ private originProbed;
68
+ /** Last time each device appeared in the broker's device list. */
69
+ private lastSeen;
60
70
  constructor(opts?: SyncClientOptions);
61
71
  private readonly autoSpawn;
62
72
  /** Connect to a running broker, or spawn one in-process, then handshake. */
63
73
  start(): Promise<BrokerMode>;
64
74
  private connectSocket;
75
+ /**
76
+ * Drop everything remembered about devices that have been gone a while.
77
+ *
78
+ * Deliberately NOT on disconnect: a crashed app often drops off the broker,
79
+ * and its cached console snapshot is the crash evidence — throwing that away
80
+ * the instant it disconnects would delete the answer. But device ids change
81
+ * with every simulator erase and fresh dev client, and each one can hold
82
+ * multi-MB snapshots, so a long-lived server has to forget eventually.
83
+ */
84
+ private pruneStaleDevices;
65
85
  /** Disconnect and tear down a spawned broker, if any. */
66
86
  stop(): Promise<void>;
67
87
  /** All devices the broker knows about, merged with their advertised tools. */
@@ -91,6 +111,61 @@ declare class SyncClient {
91
111
  * is blocked.
92
112
  */
93
113
  requireProDevice(deviceId?: string): string;
114
+ private healthOf;
115
+ /** The device answered — it's alive. */
116
+ private noteOk;
117
+ /**
118
+ * The device didn't answer. A crashed app never answers again, so after the
119
+ * first miss we stop paying the full timeout on every subsequent call.
120
+ */
121
+ private noteTimeout;
122
+ /** True once a device has missed a call and hasn't answered since. */
123
+ isUnresponsive(deviceId: string): boolean;
124
+ /**
125
+ * Full timeout normally; a short one for a device already not answering.
126
+ *
127
+ * With one escape hatch: every RECOVERY_PROBE_MS a flagged device gets the
128
+ * full budget again. Without it the flag is self-sustaining — a device whose
129
+ * work legitimately takes longer than the short budget (a big snapshot, a
130
+ * slow simulator) can never answer, so it can never clear the flag, and every
131
+ * later reply is a confident "most likely CRASHED" about a healthy app.
132
+ */
133
+ private timeoutFor;
134
+ /**
135
+ * Plain-language diagnosis for a device that isn't answering, including the
136
+ * app's last words if the console tool got a crash flush out in time.
137
+ *
138
+ * A silent timeout is the worst possible answer here: the app is on a redbox,
139
+ * the reason is already captured, and without this the caller reads "timed
140
+ * out" and keeps retrying the same dead device.
141
+ */
142
+ unresponsiveNote(deviceId: string): string;
143
+ /**
144
+ * The newest error line the console tool synced for this device, if any.
145
+ * Reads the cached console snapshot rather than the device (which, by the
146
+ * time this matters, is not answering).
147
+ */
148
+ private lastFatalFor;
149
+ /** Age of the cached snapshot for a tool, in ms (null = never received). */
150
+ snapshotAgeMs(toolId: string, deviceId: string): number | null;
151
+ /**
152
+ * Remember where a device's bundle came from, reported by core's `app`
153
+ * adapter while the app is healthy. It is the only way to reload an app that
154
+ * has crashed: the in-app reload needs the React tree that the crash
155
+ * destroyed, but the dev server's reload is handled natively.
156
+ */
157
+ noteDevServerOrigin(deviceId: string, origin: unknown): void;
158
+ /**
159
+ * Ask a freshly-announced device where its bundle came from, once.
160
+ *
161
+ * Deliberately eager: this is the piece of information that can ONLY be
162
+ * collected while the app is healthy, and is only ever needed after it has
163
+ * stopped answering. One cheap round-trip per device buys the ability to
164
+ * recover a crashed app instead of telling the caller to go do it by hand.
165
+ */
166
+ private learnDevServerOrigin;
167
+ /** The dev-server origin last reported by this device, if any. */
168
+ devServerOrigin(deviceId: string): string | null;
94
169
  /**
95
170
  * Keep the device RECORDING this tool, without asking it to stream.
96
171
  *
@@ -135,10 +210,14 @@ declare class SyncClient {
135
210
  */
136
211
  getSnapshot(toolId: string, deviceId?: string, opts?: {
137
212
  timeoutMs?: number;
213
+ silent?: boolean;
138
214
  }): Promise<{
139
215
  deviceId: string;
140
216
  payload: unknown;
217
+ stale?: string;
141
218
  }>;
219
+ /** Header line for data served from cache because the device went quiet. */
220
+ private staleNote;
142
221
  private waitForSnapshot;
143
222
  /**
144
223
  * Invoke a remote action on a device and await its result. Pass deviceId to
@@ -146,14 +225,10 @@ declare class SyncClient {
146
225
  */
147
226
  callAction(toolId: string, action: string, params?: unknown, deviceId?: string, opts?: {
148
227
  timeoutMs?: number;
228
+ silent?: boolean;
149
229
  }): Promise<unknown>;
150
230
  }
151
231
 
152
- /**
153
- * Build the Buoy MCP server and register all tools against a connected
154
- * {@link SyncClient}. Tool packs (perf-monitor, etc.) are layered on top of the
155
- * generic core here.
156
- */
157
232
  declare function createMcpServer(sync: SyncClient): McpServer;
158
233
 
159
234
  /**
@@ -381,10 +456,12 @@ interface IndexEntry {
381
456
  /** Render-capture mirror (absent when capture was unavailable/disabled). */
382
457
  renderCommits?: number;
383
458
  renderTotalMs?: number;
459
+ renderWasted?: number;
384
460
  topRenderers?: {
385
461
  name: string;
386
462
  renders: number;
387
463
  totalMs: number;
464
+ cause?: string;
388
465
  }[];
389
466
  }
390
467
  /** Per-metric min/max/avg/p95 channel. */
@@ -406,12 +483,40 @@ interface BenchmarkReportStats {
406
483
  uiJankFrames: number;
407
484
  deviceMaxRefreshRate: number;
408
485
  }
409
-
410
486
  /**
411
- * Pure helpers for the perf-monitor pack: build an AutomationConfig from
412
- * MCP-friendly input, estimate batch duration, and rank a finished batch's
413
- * results into a winner + comparison table. No network/sync — unit-testable.
487
+ * Why components rendered, counted over a run.
488
+ *
489
+ * `propsUnstable` is the actionable one: props changed by IDENTITY only —
490
+ * a fresh arrow or object literal handed down every render — while every
491
+ * value stayed equal. Reporting that as plain "props" sends you into the
492
+ * child, when the fix is always in the parent.
414
493
  */
494
+ interface RenderCauseCounts {
495
+ mount?: number;
496
+ props?: number;
497
+ propsUnstable?: number;
498
+ hooks?: number;
499
+ parent?: number;
500
+ }
501
+ interface RenderCapture {
502
+ totalCommits: number;
503
+ totalRenders: number;
504
+ capturedDurations: boolean;
505
+ totalRenderMs?: number;
506
+ /** Parent cascades + identity-only prop churn: the removable share. */
507
+ wastedRenders?: number;
508
+ causes?: RenderCauseCounts;
509
+ topComponents: {
510
+ name: string;
511
+ renders: number;
512
+ totalMs: number;
513
+ maxMs: number;
514
+ avgMs: number;
515
+ causes?: RenderCauseCounts;
516
+ changedProps?: string[];
517
+ parentName?: string;
518
+ }[];
519
+ }
415
520
 
416
521
  interface BuildConfigInput {
417
522
  targetRoute: string;
@@ -464,10 +569,12 @@ interface RankRow {
464
569
  /** React commits during the median run (null when capture was off/unavailable). */
465
570
  renderCommits: number | null;
466
571
  /** Top components by total render self-time during the median run. */
572
+ renderWasted?: number;
467
573
  topRenderers: {
468
574
  name: string;
469
575
  renders: number;
470
576
  totalMs: number;
577
+ cause?: string;
471
578
  }[];
472
579
  }
473
580
  interface BatchRanking {
@@ -506,14 +613,44 @@ declare function rankBatchFromIndex(index: IndexEntry[], batchId: string, casesO
506
613
  }[]): BatchRanking;
507
614
  /** Render a batch ranking as a trade-off-aware report (no single false winner). */
508
615
  declare function formatRankingMarkdown(ranking: BatchRanking): string;
509
- /** Side-by-side comparison of two full report stats. */
616
+ /**
617
+ * Side-by-side comparison of two full reports.
618
+ *
619
+ * Renders are part of the comparison, not an afterthought: when someone runs
620
+ * this after a memoization fix, "did the render count actually drop" IS the
621
+ * question, and for a long while this function silently dropped the block
622
+ * that answers it.
623
+ */
510
624
  declare function compareReportStats(a: {
511
625
  name: string;
512
626
  stats: BenchmarkReportStats;
627
+ renders?: RenderCapture;
513
628
  }, b: {
514
629
  name: string;
515
630
  stats: BenchmarkReportStats;
631
+ renders?: RenderCapture;
516
632
  }): {
633
+ renders?: {
634
+ total: {
635
+ a: number;
636
+ b: number;
637
+ delta: number;
638
+ };
639
+ commits: {
640
+ a: number;
641
+ b: number;
642
+ delta: number;
643
+ };
644
+ wasted: {
645
+ a: number;
646
+ b: number;
647
+ delta: number;
648
+ };
649
+ causes: {
650
+ a: RenderCauseCounts;
651
+ b: RenderCauseCounts;
652
+ };
653
+ } | undefined;
517
654
  a: string;
518
655
  b: string;
519
656
  uiFps: {