@buoy-gg/mcp 7.0.16 → 7.0.18

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/index.js CHANGED
@@ -87,8 +87,12 @@ var SyncClient = class {
87
87
  this.devices = [];
88
88
  /** Latest snapshot payload per `${deviceId}::${toolId}`. */
89
89
  this.snapshots = /* @__PURE__ */ new Map();
90
- /** Tools we've asked the broker to keep streaming, as `${deviceId}::${toolId}`. */
91
- this.watching = /* @__PURE__ */ new Set();
90
+ /**
91
+ * Capture-mode watches this client holds, keyed by snapKey. Stored with
92
+ * their addressing because they have to be RE-SENT on every reconnect: the
93
+ * broker keys watch state by socket id, and a reconnect gets a new one.
94
+ */
95
+ this.watching = /* @__PURE__ */ new Map();
92
96
  /** In-flight actions awaiting a result, keyed by requestId. */
93
97
  this.pendingActions = /* @__PURE__ */ new Map();
94
98
  /** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
@@ -144,6 +148,7 @@ var SyncClient = class {
144
148
  resolve();
145
149
  };
146
150
  socket.on("connect", onConnect);
151
+ socket.on("connect", () => this.rearmWatches());
147
152
  socket.once("connect_error", (err) => {
148
153
  if (settled) return;
149
154
  settled = true;
@@ -254,11 +259,50 @@ var SyncClient = class {
254
259
  // ----------------------------------------------------------------
255
260
  // Watch + snapshots
256
261
  // ----------------------------------------------------------------
262
+ /**
263
+ * Keep the device RECORDING this tool, without asking it to stream.
264
+ *
265
+ * A dashboard watches for as long as its panel is open; this client is
266
+ * request/response, so it used to watch and unwatch around every single
267
+ * read. That works for a tool whose state just IS (the query cache, the env
268
+ * table) and is useless for one that records a timeline: the unwatch drops
269
+ * the device's subscriber count to zero, which stops capture, so nothing is
270
+ * ever recorded between two reads. The list comes back empty and reads as
271
+ * "the app made no requests" — a wrong conclusion that is very expensive to
272
+ * chase, because everything about the app looks fine.
273
+ *
274
+ * Capture-mode watches are held for the life of the connection and released
275
+ * on disconnect (the broker also drops them for us). They cost the device's
276
+ * ring buffer, not the socket: no snapshot is pushed, and reads still go
277
+ * through `request-devtool-state`.
278
+ */
257
279
  ensureWatching(toolId, targetDeviceId) {
258
280
  const key = snapKey(targetDeviceId, toolId);
259
281
  if (this.watching.has(key)) return;
260
- this.watching.add(key);
261
- this.socket?.emit("devtool-watch", { toolId, targetDeviceId, watching: true });
282
+ this.watching.set(key, { toolId, targetDeviceId });
283
+ this.sendWatch(toolId, targetDeviceId);
284
+ }
285
+ sendWatch(toolId, targetDeviceId) {
286
+ this.socket?.emit("devtool-watch", {
287
+ toolId,
288
+ targetDeviceId,
289
+ watching: true,
290
+ stream: false
291
+ });
292
+ }
293
+ /**
294
+ * Re-arm every held watch after a reconnect.
295
+ *
296
+ * The broker tracks watches per SOCKET, so a reconnect (broker restart, a
297
+ * dropped wifi link) silently discards all of ours while `watching` still
298
+ * says we hold them — `ensureWatching` would then short-circuit forever and
299
+ * the device would never record again. Silent and permanent: the exact
300
+ * failure this whole capture-mode change exists to remove.
301
+ */
302
+ rearmWatches() {
303
+ for (const { toolId, targetDeviceId } of this.watching.values()) {
304
+ this.sendWatch(toolId, targetDeviceId);
305
+ }
262
306
  }
263
307
  /** Stop streaming a tool (releases the device-side backpressure). */
264
308
  unwatch(toolId, targetDeviceId) {
@@ -271,26 +315,25 @@ var SyncClient = class {
271
315
  return this.snapshots.get(snapKey(deviceId, toolId));
272
316
  }
273
317
  /**
274
- * Get a tool's current state. Starts watching (so the device begins emitting),
275
- * explicitly requests state, and resolves with the next snapshot. Falls back
276
- * to the last cached snapshot if the request times out.
318
+ * Get a tool's current state: arm capture (once, for the connection), ask
319
+ * for state explicitly, and resolve with the snapshot that comes back.
320
+ * Falls back to the last cached snapshot if the request times out.
321
+ *
322
+ * The capture watch is deliberately NOT released afterwards — see
323
+ * `ensureWatching`. It costs the device nothing on the socket, and dropping
324
+ * it is what made every timeline tool look permanently empty from here.
277
325
  */
278
326
  async getSnapshot(toolId, deviceId, opts = {}) {
279
327
  const target = this.resolveDevice(deviceId);
280
328
  const key = snapKey(target, toolId);
281
- const wasWatching = this.watching.has(key);
282
329
  this.ensureWatching(toolId, target);
283
- try {
284
- const next = this.waitForSnapshot(key, opts.timeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS);
285
- this.socket?.emit("request-devtool-state", { toolId, targetDeviceId: target });
286
- const payload = await next;
287
- if (payload !== void 0) return { deviceId: target, payload };
288
- const cached = this.snapshots.get(key);
289
- if (cached !== void 0) return { deviceId: target, payload: cached };
290
- throw new Error(`Timed out waiting for "${toolId}" state from ${target} (is the tool active?)`);
291
- } finally {
292
- if (!wasWatching) this.unwatch(toolId, target);
293
- }
330
+ const next = this.waitForSnapshot(key, opts.timeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS);
331
+ this.socket?.emit("request-devtool-state", { toolId, targetDeviceId: target });
332
+ const payload = await next;
333
+ if (payload !== void 0) return { deviceId: target, payload };
334
+ const cached = this.snapshots.get(key);
335
+ if (cached !== void 0) return { deviceId: target, payload: cached };
336
+ throw new Error(`Timed out waiting for "${toolId}" state from ${target} (is the tool active?)`);
294
337
  }
295
338
  waitForSnapshot(key, timeoutMs) {
296
339
  return new Promise((resolve) => {
@@ -365,7 +408,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
365
408
 
366
409
  // src/server.ts
367
410
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
368
- var import_zod18 = require("zod");
411
+ var import_zod19 = require("zod");
369
412
 
370
413
  // src/tools/generic.ts
371
414
  var import_zod = require("zod");
@@ -376,7 +419,7 @@ var import_node_os = __toESM(require("os"));
376
419
  var import_node_path = __toESM(require("path"));
377
420
 
378
421
  // src/version.ts
379
- var VERSION = "7.0.16" ? "7.0.16" : "0.0.0-dev";
422
+ var VERSION = "7.0.18" ? "7.0.18" : "0.0.0-dev";
380
423
  var PACKAGE_NAME = "@buoy-gg/mcp";
381
424
 
382
425
  // src/update/checkForUpdate.ts
@@ -1377,6 +1420,60 @@ function truncate(v, n = 600) {
1377
1420
  const s = typeof v === "string" ? v : JSON.stringify(v);
1378
1421
  return s.length > n ? `${s.slice(0, n)}\u2026(+${s.length - n} chars)` : s;
1379
1422
  }
1423
+ var MAX_BODY_FETCHES = 8;
1424
+ async function resolveBodies(sync, rows, target, max = MAX_BODY_FETCHES) {
1425
+ const needed = rows.filter(
1426
+ (e) => e.id && (e.requestBodyOmitted === true && e.requestData === void 0 || e.responseBodyOmitted === true && e.responseData === void 0)
1427
+ );
1428
+ const bodies = /* @__PURE__ */ new Map();
1429
+ if (needed.length === 0) return { bodies, skipped: 0 };
1430
+ const take = needed.slice(0, max);
1431
+ const settled = await Promise.allSettled(
1432
+ take.map(
1433
+ (e) => sync.callAction(NETWORK_TOOL_ID, "getEventBody", { id: e.id }, target)
1434
+ )
1435
+ );
1436
+ settled.forEach((r, i) => {
1437
+ const id = take[i].id;
1438
+ const body = r.status === "fulfilled" ? r.value : null;
1439
+ bodies.set(id, body ? { ...body } : { unresolved: true });
1440
+ });
1441
+ return { bodies, skipped: needed.length - take.length };
1442
+ }
1443
+ async function captureDiagnosis(sync, target) {
1444
+ let status = null;
1445
+ try {
1446
+ status = await sync.callAction(
1447
+ NETWORK_TOOL_ID,
1448
+ "getCaptureStatus",
1449
+ {},
1450
+ target
1451
+ );
1452
+ } catch {
1453
+ return "The network tool has captured nothing yet.";
1454
+ }
1455
+ if (!status || status.capturing !== false) {
1456
+ return "The device IS capturing, so nothing has been requested since it started.";
1457
+ }
1458
+ return "The device is NOT capturing. Reading from here arms capture, so the very next read should see traffic \u2014 trigger it and call again. This is not an app or interception problem. (If it stays off, the device is on a Buoy older than the capture-mode watch: open the Network panel on the device or in Buoy Desktop to record.)";
1459
+ }
1460
+ function skippedNote(skipped) {
1461
+ return skipped > 0 ? `
1462
+
1463
+ _${skipped} more withheld ${skipped === 1 ? "body was" : "bodies were"} not fetched (cap ${MAX_BODY_FETCHES} per listing, they are large). Get one in full with its id._` : "";
1464
+ }
1465
+ function bodyLines(e, fetched) {
1466
+ const got = e.id ? fetched.get(e.id) : void 0;
1467
+ const label = (inline, resolved, omitted) => {
1468
+ const value = inline ?? resolved;
1469
+ if (value !== void 0 && value !== null) return truncate(value);
1470
+ if (!omitted) return "(none)";
1471
+ return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
1472
+ };
1473
+ return `
1474
+ \u2514\u2500 request: ${label(e.requestData, got?.requestData, e.requestBodyOmitted)}
1475
+ \u2514\u2500 response: ${label(e.responseData, got?.responseData, e.responseBodyOmitted)}`;
1476
+ }
1380
1477
  function formatRow(index, e, markers) {
1381
1478
  const where = e.path || e.url;
1382
1479
  const bits = [
@@ -1430,6 +1527,7 @@ function registerNetworkTools(server, sync) {
1430
1527
  The developer pins/saves a request from the Network tool (long-press a row, or the pin/bookmark buttons in the request detail header), or you can with network_action.`
1431
1528
  );
1432
1529
  }
1530
+ const kept = args.includeBodies ? await resolveBodies(sync, rows2.map((r) => r.event), target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
1433
1531
  const body2 = rows2.map((r, i) => {
1434
1532
  const marks = [r.pinned ? "\u{1F4CC} pinned" : "", r.saved ? "\u{1F516} saved" : ""].filter(Boolean).join(" ");
1435
1533
  const stale = r.event.id?.startsWith("saved:") ? " \xB7 from an earlier app run" : "";
@@ -1437,12 +1535,7 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
1437
1535
  let out = formatRow(i + 1, r.event, marks);
1438
1536
  out += `
1439
1537
  \u2514\u2500 key \`${r.key}\`${stale}${trunc}`;
1440
- if (args.includeBodies) {
1441
- out += `
1442
- \u2514\u2500 request: ${truncate(r.event.requestData)}`;
1443
- out += `
1444
- \u2514\u2500 response: ${truncate(r.event.responseData)}`;
1445
- }
1538
+ if (args.includeBodies) out += bodyLines(r.event, kept.bodies);
1446
1539
  return out;
1447
1540
  }).join("\n");
1448
1541
  return text3(
@@ -1450,23 +1543,19 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
1450
1543
 
1451
1544
  ${body2}
1452
1545
 
1453
- _These are snapshots: they survive Clear, the 500-request cap and app restarts. Remove one with network_action({action:"remove", key}). Ids starting \`saved:\` are from an earlier run of the app._`
1546
+ _These are snapshots: they survive Clear, the 500-request cap and app restarts. Remove one with network_action({action:"remove", key}). Ids starting \`saved:\` are from an earlier run of the app._${skippedNote(kept.skipped)}`
1454
1547
  );
1455
1548
  }
1456
1549
  const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
1457
1550
  if (rows.length === 0) {
1458
- const why = events.length === 0 ? "The network tool has captured nothing yet (is it enabled, and has the app made a request?)." : "No request matched the filter.";
1551
+ const why = events.length === 0 ? await captureDiagnosis(sync, target) : "No request matched the filter.";
1459
1552
  return text3(`No requests on ${target}. ${why}`);
1460
1553
  }
1554
+ const live = args.includeBodies ? await resolveBodies(sync, rows, target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
1461
1555
  const body = rows.map((e, i) => {
1462
1556
  const marks = [pinnedIds.has(e.id) ? "\u{1F4CC}" : "", savedIds.has(e.id) ? "\u{1F516}" : ""].filter(Boolean).join("");
1463
1557
  let out = formatRow(i + 1, e, marks);
1464
- if (args.includeBodies) {
1465
- out += `
1466
- \u2514\u2500 request: ${truncate(e.requestData)}`;
1467
- out += `
1468
- \u2514\u2500 response: ${truncate(e.responseData)}`;
1469
- }
1558
+ if (args.includeBodies) out += bodyLines(e, live.bodies);
1470
1559
  return out;
1471
1560
  }).join("\n");
1472
1561
  const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
@@ -1475,7 +1564,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
1475
1564
 
1476
1565
  ${body}
1477
1566
 
1478
- _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._`
1567
+ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._${skippedNote(live.skipped)}`
1479
1568
  );
1480
1569
  } catch (e) {
1481
1570
  return fail4(errMsg4(e));
@@ -2361,8 +2450,147 @@ _Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
2361
2450
  );
2362
2451
  }
2363
2452
 
2364
- // src/tools/packs/screenshot.ts
2453
+ // src/tools/packs/timeMachine.ts
2365
2454
  var import_zod12 = require("zod");
2455
+ var TIME_MACHINE_TOOL_ID = "time-machine";
2456
+ var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2457
+ var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2458
+ var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
2459
+ function kb(bytes) {
2460
+ return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
2461
+ }
2462
+ function formatOutcome(outcome) {
2463
+ const lines = Object.entries(outcome.results).map(([sid, r]) => {
2464
+ const skips = r.skipped.slice(0, 3).map((s) => `
2465
+ \xB7 ${s.id}: ${s.reason}`).join("");
2466
+ const warns = r.warnings.slice(0, 3).map((w) => `
2467
+ \u26A0 ${w}`).join("");
2468
+ return ` ${r.ok ? "\u2713" : "\u2715"} ${sid} \u2014 applied ${r.applied}${r.skipped.length ? `, skipped ${r.skipped.length}` : ""}${skips}${warns}`;
2469
+ });
2470
+ return `Restored "${outcome.snapshotName}" (${outcome.mode}${outcome.willReload ? ", app reloading" : ""}):
2471
+ ${lines.join("\n")}`;
2472
+ }
2473
+ function registerTimeMachineTools(server, sync) {
2474
+ server.registerTool(
2475
+ "get_time_machine",
2476
+ {
2477
+ description: "List the app's Time Machine state snapshots (restore points) and which state sources (storage/redux/zustand/jotai/react-query) can currently be captured/restored. Use time_machine_action to capture a snapshot before driving a flow, then restore it to re-run the flow from the same state.",
2478
+ inputSchema: {
2479
+ deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2480
+ }
2481
+ },
2482
+ async (args) => {
2483
+ try {
2484
+ const target = sync.requireProDevice(args.deviceId);
2485
+ const { payload } = await sync.getSnapshot(TIME_MACHINE_TOOL_ID, target);
2486
+ const p = payload ?? {};
2487
+ const snapshots = p.snapshots ?? [];
2488
+ const providers = p.providers ?? [];
2489
+ const providerLines = providers.map((pr) => {
2490
+ const cap = pr.canCapture ? pr.canRestore ? "capture+restore" : "capture only" : "unavailable";
2491
+ return `- ${pr.label} (${pr.id}): ${cap}${pr.detail ? ` \u2014 ${pr.detail}` : ""}`;
2492
+ });
2493
+ const snapLines = snapshots.map((s) => {
2494
+ const warnings = Object.values(s.sources).reduce((n, src) => n + src.warnings.length, 0);
2495
+ const desc = s.baseline ? "baseline \u2014 restore wipes to fresh install (always reloads)" : `${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}`;
2496
+ return `- \`${s.id}\` **${s.name}** \u2014 ${new Date(s.createdAt).toISOString()} \xB7 ${desc}${warnings ? ` \xB7 \u26A0 ${warnings}` : ""}`;
2497
+ });
2498
+ const parts = [
2499
+ `**Time Machine \u2014 ${snapshots.length} snapshot(s):**`,
2500
+ snapLines.length ? snapLines.join("\n") : "_none yet \u2014 capture one with time_machine_action_",
2501
+ `**Sources:**
2502
+ ${providerLines.join("\n") || "_no providers registered_"}`
2503
+ ];
2504
+ if (p.lastOutcome) parts.push(`**Last restore:**
2505
+ ${formatOutcome(p.lastOutcome)}`);
2506
+ return text11(parts.join("\n\n"));
2507
+ } catch (e) {
2508
+ return fail12(errMsg12(e));
2509
+ }
2510
+ }
2511
+ );
2512
+ server.registerTool(
2513
+ "time_machine_action",
2514
+ {
2515
+ description: "Manage Time Machine state snapshots: capture the app's current state as a named restore point, preview what restoring one would change (per-item added/removed/changed/won't-apply verdicts \u2014 check the blast radius BEFORE restoring), restore one (mode 'live' replaces state in place; 'reload' restores persisted storage then reloads the JS bundle \u2014 baselines always reload), capture_baseline (an empty wipe-to-fresh-install restore point), delete, or rename. The restore result reports per-source applied/skipped counts \u2014 check them rather than assuming success.",
2516
+ inputSchema: {
2517
+ action: import_zod12.z.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
2518
+ id: import_zod12.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
2519
+ name: import_zod12.z.string().optional().describe("Snapshot name (for capture and rename)."),
2520
+ mode: import_zod12.z.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
2521
+ excludeKeys: import_zod12.z.array(import_zod12.z.string()).optional().describe('Restore only: item keys to LEAVE UNTOUCHED, as "sourceId::itemKey" (itemKey = the key shown by the preview action, e.g. "query::[\\"pokemon\\"]" or "storage::async:@cart").'),
2522
+ sourceIds: import_zod12.z.array(import_zod12.z.string()).optional().describe("Capture/restore only these source ids (default all)."),
2523
+ deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2524
+ }
2525
+ },
2526
+ async (args) => {
2527
+ try {
2528
+ const target = sync.requireProDevice(args.deviceId);
2529
+ const call = (action, params) => sync.callAction(TIME_MACHINE_TOOL_ID, action, params, target);
2530
+ switch (args.action) {
2531
+ case "capture": {
2532
+ const result = await call("capture", { name: args.name ?? "", sourceIds: args.sourceIds });
2533
+ const s = result.snapshot;
2534
+ const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
2535
+ \u26A0 ${sid}: ${w}`)).join("");
2536
+ return text11(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
2537
+ }
2538
+ case "capture_baseline": {
2539
+ const result = await call("captureBaseline", {});
2540
+ return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
2541
+ }
2542
+ case "preview": {
2543
+ if (!args.id) return fail12("preview requires `id` (from get_time_machine).");
2544
+ const result = await call("preview", { id: args.id });
2545
+ const lines = [];
2546
+ let totalApplies = 0;
2547
+ for (const src of result.sources) {
2548
+ const counts = {};
2549
+ for (const item of src.items) {
2550
+ counts[item.verdict] = (counts[item.verdict] ?? 0) + 1;
2551
+ }
2552
+ const applies = (counts.added ?? 0) + (counts.removed ?? 0) + (counts.changed ?? 0) + (counts["too-large"] ?? 0);
2553
+ totalApplies += applies;
2554
+ const parts = Object.entries(counts).filter(([v]) => v !== "unchanged").map(([v, n]) => `${n} ${v}`);
2555
+ lines.push(`- **${src.label}**: ${parts.length ? parts.join(", ") : "no changes"}${src.error ? ` \u2014 \u26A0 ${src.error}` : ""}`);
2556
+ for (const item of src.items.filter((i) => i.verdict !== "unchanged").slice(0, 8)) {
2557
+ lines.push(` ${item.verdict === "added" ? "+" : item.verdict === "removed" ? "\u2212" : item.verdict === "wont-apply" ? "\u2298" : "\u2248"} ${item.label}${item.reason ? ` (${item.reason})` : ""}`);
2558
+ }
2559
+ for (const note of src.notes) lines.push(` \xB7 ${note}`);
2560
+ }
2561
+ const head = totalApplies === 0 ? "**Restore would change NOTHING** (states identical or nothing applicable)." : `**Restore would apply ${totalApplies} change(s):**`;
2562
+ return text11(`${head}
2563
+ ${lines.join("\n")}`);
2564
+ }
2565
+ case "restore": {
2566
+ if (!args.id) return fail12("restore requires `id` (from get_time_machine).");
2567
+ const outcome = await call("restore", {
2568
+ id: args.id,
2569
+ mode: args.mode ?? "live",
2570
+ excludeKeys: args.excludeKeys
2571
+ });
2572
+ return text11(formatOutcome(outcome));
2573
+ }
2574
+ case "delete": {
2575
+ if (!args.id) return fail12("delete requires `id`.");
2576
+ await call("delete", { id: args.id });
2577
+ return text11(`Deleted snapshot \`${args.id}\`.`);
2578
+ }
2579
+ case "rename": {
2580
+ if (!args.id || !args.name) return fail12("rename requires `id` and `name`.");
2581
+ await call("rename", { id: args.id, name: args.name });
2582
+ return text11(`Renamed \`${args.id}\` to "${args.name}".`);
2583
+ }
2584
+ }
2585
+ } catch (e) {
2586
+ return fail12(errMsg12(e));
2587
+ }
2588
+ }
2589
+ );
2590
+ }
2591
+
2592
+ // src/tools/packs/screenshot.ts
2593
+ var import_zod13 = require("zod");
2366
2594
 
2367
2595
  // src/capture/simctl.ts
2368
2596
  var import_node_child_process = require("child_process");
@@ -2442,8 +2670,8 @@ async function cropToBuffer(srcPath, full, region) {
2442
2670
  var TOOL_ID = "highlight-updates";
2443
2671
  var MARGIN_PT = 12;
2444
2672
  var CAPTURE_PADDING_PT = 6;
2445
- var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2446
- var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
2673
+ var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2674
+ var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2447
2675
  function reasonMessage(res, query) {
2448
2676
  switch (res.reason) {
2449
2677
  case "no-renders":
@@ -2473,10 +2701,10 @@ function registerScreenshotTools(server, sync) {
2473
2701
  {
2474
2702
  description: "Take a tight, cropped screenshot of a single React Native component running in the iOS Simulator \u2014 no manual coordinates needed. Give a testID, nativeID, accessibilityLabel, or component name in `query` (or an exact `nativeTag` from a previous call's candidates) and it locates the live component on-device (scrolling it into view), then captures + crops the Simulator framebuffer to just that component. The component must have rendered at least once since the app opened for it to be trackable \u2014 if nothing matches, navigate to the screen that renders it and retry. iOS Simulator only, and the MCP server must be running on the same Mac as the booted Simulator (uses `xcrun simctl` + `sips`). Requires Buoy Pro.",
2475
2703
  inputSchema: {
2476
- query: import_zod12.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
2477
- nativeTag: import_zod12.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
2478
- scrollIntoView: import_zod12.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
2479
- deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2704
+ query: import_zod13.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
2705
+ nativeTag: import_zod13.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
2706
+ scrollIntoView: import_zod13.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
2707
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2480
2708
  }
2481
2709
  },
2482
2710
  async (args) => {
@@ -2496,11 +2724,11 @@ function registerScreenshotTools(server, sync) {
2496
2724
  target
2497
2725
  );
2498
2726
  if (!res.matched || !res.rect || !res.screen) {
2499
- return fail12(reasonMessage(res, args.query) + candidatesText(res.candidates));
2727
+ return fail13(reasonMessage(res, args.query) + candidatesText(res.candidates));
2500
2728
  }
2501
2729
  const shot = await captureIosFramebuffer();
2502
2730
  if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
2503
- return fail12(shot.error ?? "Screenshot capture failed.");
2731
+ return fail13(shot.error ?? "Screenshot capture failed.");
2504
2732
  }
2505
2733
  const pxScale = shot.width / res.screen.width;
2506
2734
  const r = res.rect;
@@ -2516,7 +2744,7 @@ function registerScreenshotTools(server, sync) {
2516
2744
  }
2517
2745
  );
2518
2746
  if (!crop.ok || !crop.buffer) {
2519
- return fail12(crop.error ?? "Crop failed.");
2747
+ return fail13(crop.error ?? "Crop failed.");
2520
2748
  }
2521
2749
  const label = res.componentName || "component";
2522
2750
  const idPart = res.testID ? ` (testID: "${res.testID}")` : "";
@@ -2529,21 +2757,21 @@ function registerScreenshotTools(server, sync) {
2529
2757
  ]
2530
2758
  };
2531
2759
  } catch (e) {
2532
- return fail12(errMsg12(e));
2760
+ return fail13(errMsg13(e));
2533
2761
  }
2534
2762
  }
2535
2763
  );
2536
2764
  }
2537
2765
 
2538
2766
  // src/tools/packs/interact.ts
2539
- var import_zod13 = require("zod");
2767
+ var import_zod14 = require("zod");
2540
2768
  var TOOL_ID2 = "highlight-updates";
2541
- var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2542
- var fail13 = (message) => ({
2769
+ var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2770
+ var fail14 = (message) => ({
2543
2771
  isError: true,
2544
2772
  content: [{ type: "text", text: message }]
2545
2773
  });
2546
- var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2774
+ var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
2547
2775
  function formatElement(e) {
2548
2776
  let line = e.interactive ? "\u2022 " : " ";
2549
2777
  line += e.name;
@@ -2563,8 +2791,8 @@ function registerInteractTools(server, sync) {
2563
2791
  {
2564
2792
  description: "List the interactive / labeled elements currently on screen \u2014 the app's live accessibility-style tree walked from the React fiber tree. Each row shows the component name, visible text/label, role, testID, a normalized tap point (tap: x,y in 0\u20131), and its #nativeTag. Rows marked with \u2022 are tappable (have an onPress). Use this to see what's on screen and pick something to tap with tap_element \u2014 no screenshot needed. Re-run after navigating or when the screen changes, since positions move. Requires Buoy Pro.",
2565
2793
  inputSchema: {
2566
- interactiveOnly: import_zod13.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
2567
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2794
+ interactiveOnly: import_zod14.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
2795
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2568
2796
  }
2569
2797
  },
2570
2798
  async (args) => {
@@ -2574,16 +2802,16 @@ function registerInteractTools(server, sync) {
2574
2802
  const all = res?.elements ?? [];
2575
2803
  const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
2576
2804
  if (elements.length === 0) {
2577
- return text11(
2805
+ return text12(
2578
2806
  "No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
2579
2807
  );
2580
2808
  }
2581
2809
  const header = `Screen ${res.screen.width}\xD7${res.screen.height} \u2014 ${elements.length} element${elements.length === 1 ? "" : "s"} (\u2022 = tappable). Tap with tap_element({ testID | query | nativeTag }).`;
2582
- return text11(`${header}
2810
+ return text12(`${header}
2583
2811
 
2584
2812
  ${elements.map(formatElement).join("\n")}`);
2585
2813
  } catch (e) {
2586
- return fail13(errMsg13(e));
2814
+ return fail14(errMsg14(e));
2587
2815
  }
2588
2816
  }
2589
2817
  );
@@ -2592,20 +2820,20 @@ ${elements.map(formatElement).join("\n")}`);
2592
2820
  {
2593
2821
  description: "Interact with an on-screen element by invoking its handler directly in JS (no screenshot, no coordinate math, works on real devices). Handles buttons (onPress + onLongPress), toggles/switches and sliders (onValueChange), and text inputs (onChangeText) \u2014 see each element's `control` in describe_screen. Identify the target with an exact `nativeTag` or `testID`, or a fuzzy `query`. Pass `longPress:true` to invoke onLongPress instead of onPress (elements that support it show {long-press} in describe_screen). For a toggle, pass `value:true/false` (omit to flip); for a slider, pass a numeric `value` (omit to jump to the far end); for a text input, pass `text`. If the element is off-screen it's scrolled into view first (scrollIntoView:false to skip). Call describe_screen first to see options + current values. Requires Buoy Pro.",
2594
2822
  inputSchema: {
2595
- nativeTag: import_zod13.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
2596
- testID: import_zod13.z.string().optional().describe("Exact testID of the element."),
2597
- query: import_zod13.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
2598
- value: import_zod13.z.union([import_zod13.z.number(), import_zod13.z.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
2599
- text: import_zod13.z.string().optional().describe("For a text input: the string to set via onChangeText."),
2600
- longPress: import_zod13.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
2601
- scrollIntoView: import_zod13.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
2602
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2823
+ nativeTag: import_zod14.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
2824
+ testID: import_zod14.z.string().optional().describe("Exact testID of the element."),
2825
+ query: import_zod14.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
2826
+ value: import_zod14.z.union([import_zod14.z.number(), import_zod14.z.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
2827
+ text: import_zod14.z.string().optional().describe("For a text input: the string to set via onChangeText."),
2828
+ longPress: import_zod14.z.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
2829
+ scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
2830
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2603
2831
  }
2604
2832
  },
2605
2833
  async (args) => {
2606
2834
  try {
2607
2835
  if (args.nativeTag == null && !args.testID && !args.query) {
2608
- return fail13("Provide one of: nativeTag, testID, or query (from describe_screen).");
2836
+ return fail14("Provide one of: nativeTag, testID, or query (from describe_screen).");
2609
2837
  }
2610
2838
  const target = sync.requireProDevice(args.deviceId);
2611
2839
  const res = await sync.callAction(
@@ -2628,26 +2856,26 @@ ${elements.map(formatElement).join("\n")}`);
2628
2856
  const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
2629
2857
  const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
2630
2858
  const verb = m.via === "onValueChange" ? `Set "${label}"${id} \u2192 ${JSON.stringify(m.value)}` : m.via === "onChangeText" ? `Typed ${JSON.stringify(m.value)} into "${label}"${id}` : m.via === "onLongPress" ? `Long-pressed "${label}"${id}` : `Tapped "${label}"${id}`;
2631
- return text11(`${verb}${scrolledNote}.`);
2859
+ return text12(`${verb}${scrolledNote}.`);
2632
2860
  }
2633
2861
  let msg = res?.reason ?? "Tap failed.";
2634
2862
  if (res?.candidates?.length) {
2635
2863
  msg += "\n\nTappable candidates:\n" + res.candidates.map((c) => `- ${c.name}${c.text ? ` "${c.text}"` : ""}${c.testID ? ` [testID=${c.testID}]` : ""}${c.nativeTag != null ? ` #${c.nativeTag}` : ""}`).join("\n");
2636
2864
  }
2637
- return fail13(msg);
2865
+ return fail14(msg);
2638
2866
  } catch (e) {
2639
- return fail13(errMsg13(e));
2867
+ return fail14(errMsg14(e));
2640
2868
  }
2641
2869
  }
2642
2870
  );
2643
2871
  }
2644
2872
 
2645
2873
  // src/tools/packs/jsThread.ts
2646
- var import_zod14 = require("zod");
2874
+ var import_zod15 = require("zod");
2647
2875
  var JS_TOP_TOOL_ID = "js-top";
2648
- var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2649
- var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2650
- var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
2876
+ var text13 = (s) => ({ content: [{ type: "text", text: s }] });
2877
+ var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2878
+ var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
2651
2879
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2652
2880
  function sparkline(values) {
2653
2881
  const max = Math.max(1, ...values);
@@ -2718,9 +2946,9 @@ function registerJsThreadTools(server, sync) {
2718
2946
  {
2719
2947
  description: "Task Manager for the React Native JS thread: sample for a few seconds and return a ranked table of which task origins (timers, Promise chains, rAF callbacks, bridge call-ins) consumed JS-thread time \u2014 plus thread busy% and freeze (50ms+ block) attribution. Use when JS FPS is low and you need to know WHAT is eating the thread. To profile one specific interaction, pass clearFirst:true, then drive the UI with tap_element while the sample runs. Drill into a row with get_js_thread_origin_detail. Requires @buoy-gg/js-top on the device.",
2720
2948
  inputSchema: {
2721
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2722
- durationMs: import_zod14.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
2723
- clearFirst: import_zod14.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
2949
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2950
+ durationMs: import_zod15.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
2951
+ clearFirst: import_zod15.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
2724
2952
  }
2725
2953
  },
2726
2954
  async ({ deviceId, durationMs, clearFirst }) => {
@@ -2733,7 +2961,7 @@ function registerJsThreadTools(server, sync) {
2733
2961
  { durationMs: duration, clearFirst: clearFirst === true },
2734
2962
  target
2735
2963
  );
2736
- if (!snap) return fail14("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
2964
+ if (!snap) return fail15("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
2737
2965
  const md = formatTopMarkdown(snap, duration);
2738
2966
  const slim = {
2739
2967
  busyPct: Math.round(snap.busyPct * 10) / 10,
@@ -2752,13 +2980,13 @@ function registerJsThreadTools(server, sync) {
2752
2980
  worstFreezeMs: r.worstFreezeMs
2753
2981
  }))
2754
2982
  };
2755
- return text12(`${md}
2983
+ return text13(`${md}
2756
2984
 
2757
2985
  \`\`\`json
2758
2986
  ${JSON.stringify(slim)}
2759
2987
  \`\`\``);
2760
2988
  } catch (e) {
2761
- return fail14(errMsg14(e));
2989
+ return fail15(errMsg15(e));
2762
2990
  }
2763
2991
  }
2764
2992
  );
@@ -2767,8 +2995,8 @@ ${JSON.stringify(slim)}
2767
2995
  {
2768
2996
  description: 'Inspect one js-top task origin in depth: schedules vs runs, avg/max/total exclusive ms, a 5s activity sparkline, freeze attribution, and the captured scheduling site (caller function + stack frames). Pass a row `key` from get_js_thread_top\'s JSON (e.g. "interval|pollFeed" or "unattributed" for the system row).',
2769
2997
  inputSchema: {
2770
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2771
- key: import_zod14.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
2998
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2999
+ key: import_zod15.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
2772
3000
  }
2773
3001
  },
2774
3002
  async ({ deviceId, key }) => {
@@ -2781,18 +3009,18 @@ ${JSON.stringify(slim)}
2781
3009
  target
2782
3010
  );
2783
3011
  if (!detail) {
2784
- return fail14(
3012
+ return fail15(
2785
3013
  `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
2786
3014
  );
2787
3015
  }
2788
3016
  const md = formatDetailMarkdown(detail);
2789
- return text12(`${md}
3017
+ return text13(`${md}
2790
3018
 
2791
3019
  \`\`\`json
2792
3020
  ${JSON.stringify(detail)}
2793
3021
  \`\`\``);
2794
3022
  } catch (e) {
2795
- return fail14(errMsg14(e));
3023
+ return fail15(errMsg15(e));
2796
3024
  }
2797
3025
  }
2798
3026
  );
@@ -2801,27 +3029,27 @@ ${JSON.stringify(detail)}
2801
3029
  {
2802
3030
  description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
2803
3031
  inputSchema: {
2804
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
3032
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2805
3033
  }
2806
3034
  },
2807
3035
  async ({ deviceId }) => {
2808
3036
  try {
2809
3037
  const target = sync.requireProDevice(deviceId);
2810
3038
  await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
2811
- return text12("Cleared js-top stats.");
3039
+ return text13("Cleared js-top stats.");
2812
3040
  } catch (e) {
2813
- return fail14(errMsg14(e));
3041
+ return fail15(errMsg15(e));
2814
3042
  }
2815
3043
  }
2816
3044
  );
2817
3045
  }
2818
3046
 
2819
3047
  // src/tools/packs/images.ts
2820
- var import_zod15 = require("zod");
3048
+ var import_zod16 = require("zod");
2821
3049
  var IMAGES_TOOL_ID = "images";
2822
- var text13 = (s) => ({ content: [{ type: "text", text: s }] });
2823
- var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2824
- var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
3050
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3051
+ var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3052
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
2825
3053
  function uriTail(uri) {
2826
3054
  if (!uri) return "(no source)";
2827
3055
  const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
@@ -2909,9 +3137,9 @@ function registerImagesTools(server, sync) {
2909
3137
  {
2910
3138
  description: "Live image registry from the app: every RN <Image> and expo-image load with cache verdict (memory/disk/network), load ms, decoded vs displayed size with an oversize/upscale verdict and wasted-KB estimate, error details, and any active simulation overrides. Image HTTP never hits the JS network stack, so this is the only visibility into image loading. Requires @buoy-gg/images on the device.",
2911
3139
  inputSchema: {
2912
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2913
- limit: import_zod15.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
2914
- status: import_zod15.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
3140
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3141
+ limit: import_zod16.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
3142
+ status: import_zod16.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
2915
3143
  }
2916
3144
  },
2917
3145
  async ({ deviceId, limit, status }) => {
@@ -2924,7 +3152,7 @@ function registerImagesTools(server, sync) {
2924
3152
  { limit: capped, status },
2925
3153
  target
2926
3154
  );
2927
- if (!list) return fail15("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
3155
+ if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
2928
3156
  const md = formatListMarkdown(list, capped);
2929
3157
  const slim = {
2930
3158
  stats: list.stats,
@@ -2951,13 +3179,13 @@ function registerImagesTools(server, sync) {
2951
3179
  layoutShifts: r.layoutShifts
2952
3180
  }))
2953
3181
  };
2954
- return text13(`${md}
3182
+ return text14(`${md}
2955
3183
 
2956
3184
  \`\`\`json
2957
3185
  ${JSON.stringify(slim)}
2958
3186
  \`\`\``);
2959
3187
  } catch (e) {
2960
- return fail15(errMsg15(e));
3188
+ return fail16(errMsg16(e));
2961
3189
  }
2962
3190
  }
2963
3191
  );
@@ -2966,9 +3194,9 @@ ${JSON.stringify(slim)}
2966
3194
  {
2967
3195
  description: "Act on one image from get_images (by id): hard_reload (bypass caches + refetch), retry, evict_disk (expo disk entry), flash (red border to locate it on screen), prove_savings (re-encode at displayed size as WebP ON DEVICE and return real byte savings + a previewable file:// uri), or simulation overrides \u2014 force_error (instant offline-safe failure), force_loading (request hangs forever), force_blank, swap_url (requires url), restore (remove override). Returns the record's state after the action so the effect is visible.",
2968
3196
  inputSchema: {
2969
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2970
- id: import_zod15.z.number().describe("Record id from get_images."),
2971
- action: import_zod15.z.enum([
3197
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3198
+ id: import_zod16.z.number().describe("Record id from get_images."),
3199
+ action: import_zod16.z.enum([
2972
3200
  "hard_reload",
2973
3201
  "retry",
2974
3202
  "evict_disk",
@@ -2980,7 +3208,7 @@ ${JSON.stringify(slim)}
2980
3208
  "swap_url",
2981
3209
  "restore"
2982
3210
  ]).describe("What to do."),
2983
- url: import_zod15.z.string().optional().describe("Replacement URL (swap_url only).")
3211
+ url: import_zod16.z.string().optional().describe("Replacement URL (swap_url only).")
2984
3212
  }
2985
3213
  },
2986
3214
  async ({ deviceId, id, action, url }) => {
@@ -2988,8 +3216,8 @@ ${JSON.stringify(slim)}
2988
3216
  const target = sync.requireProDevice(deviceId);
2989
3217
  if (action === "prove_savings") {
2990
3218
  const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
2991
- if (!savings) return fail15("Device returned nothing for proveSavings.");
2992
- if (!savings.ok) return fail15(savings.message);
3219
+ if (!savings) return fail16("Device returned nothing for proveSavings.");
3220
+ if (!savings.ok) return fail16(savings.message);
2993
3221
  const parts = [`**prove_savings** on #${id}: ${savings.message}`];
2994
3222
  if (savings.originalBytes != null)
2995
3223
  parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
@@ -3001,11 +3229,11 @@ ${JSON.stringify(slim)}
3001
3229
  parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
3002
3230
  if (savings.optimizedUri)
3003
3231
  parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
3004
- return text13(parts.join("\n"));
3232
+ return text14(parts.join("\n"));
3005
3233
  }
3006
3234
  const entry = ACTION_MAP[action];
3007
- if (!entry) return fail15(`Unknown action: ${action}`);
3008
- if (action === "swap_url" && !url) return fail15("swap_url requires the `url` param.");
3235
+ if (!entry) return fail16(`Unknown action: ${action}`);
3236
+ if (action === "swap_url" && !url) return fail16("swap_url requires the `url` param.");
3009
3237
  const result = await sync.callAction(
3010
3238
  IMAGES_TOOL_ID,
3011
3239
  entry.action,
@@ -3025,9 +3253,9 @@ ${JSON.stringify(slim)}
3025
3253
  );
3026
3254
  if (detail.error) lines.push(`Error: ${detail.error}`);
3027
3255
  }
3028
- return text13(lines.join("\n"));
3256
+ return text14(lines.join("\n"));
3029
3257
  } catch (e) {
3030
- return fail15(errMsg15(e));
3258
+ return fail16(errMsg16(e));
3031
3259
  }
3032
3260
  }
3033
3261
  );
@@ -3036,12 +3264,12 @@ ${JSON.stringify(slim)}
3036
3264
  {
3037
3265
  description: "App-wide image simulation modes: network 'offline' (every NETWORK image load fails immediately, both libs \u2014 bundled assets still load; reload/remount on-screen images to see it), 'cold' (every image bypasses caches \u2014 first-launch behavior, nothing cleared), 'normal' to reset. blankImages renders every image without a source. massAction applies per-image simulations to everything mounted. Also clears the registry or expo caches when asked.",
3038
3266
  inputSchema: {
3039
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3040
- network: import_zod15.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
3041
- blankImages: import_zod15.z.boolean().optional().describe("Render all images without a source."),
3042
- massAction: import_zod15.z.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to EVERY mounted image at once: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
3043
- clearRegistry: import_zod15.z.boolean().optional().describe("Clear the captured image log."),
3044
- clearExpoCaches: import_zod15.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
3267
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3268
+ network: import_zod16.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
3269
+ blankImages: import_zod16.z.boolean().optional().describe("Render all images without a source."),
3270
+ massAction: import_zod16.z.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to EVERY mounted image at once: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
3271
+ clearRegistry: import_zod16.z.boolean().optional().describe("Clear the captured image log."),
3272
+ clearExpoCaches: import_zod16.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
3045
3273
  }
3046
3274
  },
3047
3275
  async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
@@ -3068,22 +3296,22 @@ ${JSON.stringify(slim)}
3068
3296
  const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
3069
3297
  done.push(r?.message ?? "expo caches cleared");
3070
3298
  }
3071
- if (done.length === 0) return fail15("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
3072
- return text13(`Applied: ${done.join(" \xB7 ")}`);
3299
+ if (done.length === 0) return fail16("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
3300
+ return text14(`Applied: ${done.join(" \xB7 ")}`);
3073
3301
  } catch (e) {
3074
- return fail15(errMsg15(e));
3302
+ return fail16(errMsg16(e));
3075
3303
  }
3076
3304
  }
3077
3305
  );
3078
3306
  }
3079
3307
 
3080
3308
  // src/tools/packs/assets.ts
3081
- var import_zod16 = require("zod");
3309
+ var import_zod17 = require("zod");
3082
3310
  var ASSETS_TOOL_ID = "assets";
3083
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3084
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3085
- var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3086
- function kb(bytes) {
3311
+ var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3312
+ var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3313
+ var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3314
+ function kb2(bytes) {
3087
3315
  if (bytes == null || bytes <= 0) return "\u2013";
3088
3316
  if (bytes < 1024) return `${bytes}B`;
3089
3317
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
@@ -3091,7 +3319,7 @@ function kb(bytes) {
3091
3319
  }
3092
3320
  function sizeLabel(r) {
3093
3321
  if (r.sizeBytes == null) return "\u2013";
3094
- return r.sizeSource === "measured" ? kb(r.sizeBytes) : `~${kb(r.sizeBytes)}`;
3322
+ return r.sizeSource === "measured" ? kb2(r.sizeBytes) : `~${kb2(r.sizeBytes)}`;
3095
3323
  }
3096
3324
  function scalesLabel(scales) {
3097
3325
  if (!scales.length) return "\u2013";
@@ -3114,9 +3342,9 @@ function formatListMarkdown2(list, limit) {
3114
3342
  const ss = list.scanStatus;
3115
3343
  lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
3116
3344
  lines.push("");
3117
- const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb(v.bytes)})` : ""}`);
3345
+ const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb2(v.bytes)})` : ""}`);
3118
3346
  lines.push(
3119
- `${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${kb(s.totalBytes)} measured (${s.measuredCount}/${s.total})` : "sizes not measured") + ` \xB7 ${kindBits.join(" \xB7 ")}`
3347
+ `${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${kb2(s.totalBytes)} measured (${s.measuredCount}/${s.total})` : "sizes not measured") + ` \xB7 ${kindBits.join(" \xB7 ")}`
3120
3348
  );
3121
3349
  if (ss.fontFamilies.length > 0) {
3122
3350
  lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
@@ -3132,7 +3360,7 @@ function formatListMarkdown2(list, limit) {
3132
3360
  if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
3133
3361
  if (ins.duplicates.length) {
3134
3362
  const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
3135
- chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb(wasted)} wasted)` : ""}`);
3363
+ chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb2(wasted)} wasted)` : ""}`);
3136
3364
  }
3137
3365
  if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
3138
3366
  if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
@@ -3146,7 +3374,7 @@ function formatListMarkdown2(list, limit) {
3146
3374
  }
3147
3375
  const diff = list.diff;
3148
3376
  if (diff) {
3149
- const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb(Math.abs(diff.netBytes))} net`;
3377
+ const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb2(Math.abs(diff.netBytes))} net`;
3150
3378
  lines.push("");
3151
3379
  lines.push(
3152
3380
  `**Since baseline** (${new Date(diff.baselineSavedAt).toISOString()}): ${diff.added.length} added \xB7 ${diff.removed.length} removed \xB7 ${diff.grown.length} grew \xB7 ${net}` + (diff.removed.length ? ` \u2014 removed: ${diff.removed.slice(0, 5).map((r) => `${r.name}.${r.type}`).join(", ")}${diff.removed.length > 5 ? "\u2026" : ""}` : "")
@@ -3173,10 +3401,10 @@ function registerAssetsTools(server, sync) {
3173
3401
  {
3174
3402
  description: "Bundled-asset inventory from the app: every asset (images, fonts, video, audio) with dimensions, @1x/@2x/@3x scale coverage, byte sizes (measured from the Metro server in dev), loaded vs never-loaded state, and audit findings \u2014 duplicates (by content hash), shipped-but-never-loaded, WebP candidates, images decoding larger than the screen, heavy GIFs/fonts, scale gaps \u2014 plus the change vs a saved baseline. This is what the app SHIPS; use get_images for what it RENDERS. Requires @buoy-gg/assets on the device.",
3175
3403
  inputSchema: {
3176
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3177
- limit: import_zod16.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
3178
- kind: import_zod16.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
3179
- unusedOnly: import_zod16.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
3404
+ deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3405
+ limit: import_zod17.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
3406
+ kind: import_zod17.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
3407
+ unusedOnly: import_zod17.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
3180
3408
  }
3181
3409
  },
3182
3410
  async ({ deviceId, limit, kind, unusedOnly }) => {
@@ -3189,7 +3417,7 @@ function registerAssetsTools(server, sync) {
3189
3417
  { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
3190
3418
  target
3191
3419
  );
3192
- if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3420
+ if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3193
3421
  if (unusedOnly) {
3194
3422
  list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
3195
3423
  }
@@ -3213,13 +3441,13 @@ function registerAssetsTools(server, sync) {
3213
3441
  sizeSource: r.sizeSource
3214
3442
  }))
3215
3443
  };
3216
- return text14(`${md}
3444
+ return text15(`${md}
3217
3445
 
3218
3446
  \`\`\`json
3219
3447
  ${JSON.stringify(slim)}
3220
3448
  \`\`\``);
3221
3449
  } catch (e) {
3222
- return fail16(errMsg16(e));
3450
+ return fail17(errMsg17(e));
3223
3451
  }
3224
3452
  }
3225
3453
  );
@@ -3228,9 +3456,9 @@ ${JSON.stringify(slim)}
3228
3456
  {
3229
3457
  description: "Drive the assets tool: rescan (re-walk the registry + merge the Metro bundle graph + refresh fonts + measure sizes \u2014 run this first if the inventory looks stale or empty), measure_sizes (re-measure byte sizes only), detail (full metadata for one asset by id: source dir, per-scale sizes, resolved URI, per-file graph paths), save_baseline (persist the current inventory for before/after comparison \u2014 save, make your change, then get_assets shows added/removed/grown/net bytes), clear_baseline, clear_records (wipe + rescan rebuilds).",
3230
3458
  inputSchema: {
3231
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3232
- action: import_zod16.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
3233
- id: import_zod16.z.number().optional().describe("Asset id from get_assets (detail only).")
3459
+ deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3460
+ action: import_zod17.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
3461
+ id: import_zod17.z.number().optional().describe("Asset id from get_assets (detail only).")
3234
3462
  }
3235
3463
  },
3236
3464
  async ({ deviceId, action, id }) => {
@@ -3240,19 +3468,19 @@ ${JSON.stringify(slim)}
3240
3468
  case "rescan": {
3241
3469
  const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
3242
3470
  const ss = result?.scanStatus;
3243
- if (!ss) return fail16("Device returned nothing for rescan.");
3244
- return text14(
3471
+ if (!ss) return fail17("Device returned nothing for rescan.");
3472
+ return text15(
3245
3473
  `Rescan done: ${ss.registryCount} registered at runtime` + (ss.graphCount != null ? ` \xB7 ${ss.graphCount} in the bundle graph` : " \xB7 no bundle graph") + (ss.graphError ? ` (graph: ${ss.graphError})` : "") + ` \xB7 ${ss.fontFamilies.length} font families` + (ss.measuring ? " \xB7 size measurement running \u2014 get_assets in a moment" : "")
3246
3474
  );
3247
3475
  }
3248
3476
  case "measure_sizes": {
3249
3477
  const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
3250
- return text14(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3478
+ return text15(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3251
3479
  }
3252
3480
  case "detail": {
3253
- if (id == null) return fail16("detail requires the `id` param (from get_assets).");
3481
+ if (id == null) return fail17("detail requires the `id` param (from get_assets).");
3254
3482
  const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
3255
- if (!detail) return fail16(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3483
+ if (!detail) return fail17(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3256
3484
  const lines = [
3257
3485
  `## ${detail.name}.${detail.type} (#${detail.id})`,
3258
3486
  "",
@@ -3261,50 +3489,50 @@ ${JSON.stringify(slim)}
3261
3489
  if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
3262
3490
  if (detail.sizesByScale) {
3263
3491
  for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
3264
- lines.push(`- Size @${scale}x: ${kb(bytes)}`);
3492
+ lines.push(`- Size @${scale}x: ${kb2(bytes)}`);
3265
3493
  }
3266
3494
  }
3267
3495
  if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
3268
- if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb(detail.estDecodedBytes)} (RGBA)`);
3496
+ if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb2(detail.estDecodedBytes)} (RGBA)`);
3269
3497
  if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
3270
3498
  if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
3271
3499
  if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
3272
3500
  if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
3273
- return text14(lines.join("\n"));
3501
+ return text15(lines.join("\n"));
3274
3502
  }
3275
3503
  case "save_baseline": {
3276
3504
  const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
3277
- return text14(
3505
+ return text15(
3278
3506
  `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
3279
3507
  );
3280
3508
  }
3281
3509
  case "clear_baseline": {
3282
3510
  await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
3283
- return text14("Baseline cleared.");
3511
+ return text15("Baseline cleared.");
3284
3512
  }
3285
3513
  case "clear_records": {
3286
3514
  const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
3287
- return text14(result?.message ?? "Inventory cleared.");
3515
+ return text15(result?.message ?? "Inventory cleared.");
3288
3516
  }
3289
3517
  }
3290
3518
  } catch (e) {
3291
- return fail16(errMsg16(e));
3519
+ return fail17(errMsg17(e));
3292
3520
  }
3293
3521
  }
3294
3522
  );
3295
3523
  }
3296
3524
 
3297
3525
  // src/tools/packs/reload.ts
3298
- var import_zod17 = require("zod");
3526
+ var import_zod18 = require("zod");
3299
3527
  var APP_TOOL_ID = "app";
3300
3528
  var RELOAD_ACTION = "reloadApp";
3301
3529
  var DEFAULT_WAIT_MS = 3e4;
3302
3530
  var TEARDOWN_GRACE_MS = 1200;
3303
3531
  var PROBE_TIMEOUT_MS = 1500;
3304
3532
  var PROBE_INTERVAL_MS = 400;
3305
- var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3306
- var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3307
- var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3533
+ var text16 = (s) => ({ content: [{ type: "text", text: s }] });
3534
+ var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3535
+ var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
3308
3536
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3309
3537
  async function ping(sync, deviceId, timeoutMs) {
3310
3538
  try {
@@ -3339,12 +3567,12 @@ function registerReloadTools(server, sync) {
3339
3567
  {
3340
3568
  description: "Reload the running app's JS bundle (Expo Go, dev client, or RN CLI) \u2014 the equivalent of shaking the device and hitting Reload, but from here. Use it to pick up a change Fast Refresh didn't apply, to clear leaked in-memory state before a measurement, or to re-run app startup. In dev builds it uses React Native's DevSettings.reload(); otherwise it falls back to expo-updates (if the app installs it). All in-memory state is lost. By default this waits for the app to come back up and reports how long the reload took. Works on any app running @buoy-gg/core \u2014 no particular tool package needed. Requires Buoy Pro.",
3341
3569
  inputSchema: {
3342
- strategy: import_zod17.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3570
+ strategy: import_zod18.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3343
3571
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
3344
3572
  ),
3345
- wait: import_zod17.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
3346
- timeoutMs: import_zod17.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3347
- deviceId: import_zod17.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3573
+ wait: import_zod18.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
3574
+ timeoutMs: import_zod18.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3575
+ deviceId: import_zod18.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3348
3576
  }
3349
3577
  },
3350
3578
  async (args) => {
@@ -3360,19 +3588,19 @@ function registerReloadTools(server, sync) {
3360
3588
  );
3361
3589
  const via = ack?.strategy ? ` via ${ack.strategy}` : "";
3362
3590
  if (args.wait === false) {
3363
- return text15(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
3591
+ return text16(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
3364
3592
  }
3365
3593
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
3366
3594
  if (elapsed == null) {
3367
- return fail17(
3595
+ return fail18(
3368
3596
  `Reload was accepted${via}, but the app hasn't reconnected yet. It may still be building the bundle (a cold Metro rebuild can take a while) \u2014 check the device, then run list_devices.`
3369
3597
  );
3370
3598
  }
3371
- return text15(
3599
+ return text16(
3372
3600
  `Reloaded the app${via} \u2014 back and connected after ${(elapsed / 1e3).toFixed(1)}s. All in-memory state was reset, so re-read any state you cached before this.`
3373
3601
  );
3374
3602
  } catch (e) {
3375
- return fail17(errMsg17(e));
3603
+ return fail18(errMsg18(e));
3376
3604
  }
3377
3605
  }
3378
3606
  );
@@ -3427,6 +3655,14 @@ measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 De
3427
3655
  builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
3428
3656
  re-read anything you cached.
3429
3657
 
3658
+ Testing a multi-step flow repeatedly (cart \u2192 checkout, login states, a bug that needs specific
3659
+ state)? \`time_machine_action\` captures the app's COMPLETE client state (storage, redux, zustand,
3660
+ jotai, react-query cache) as a named restore point and restores it in one call \u2014 capture once,
3661
+ drive the flow with \`tap_element\`, restore, repeat. \`get_time_machine\` lists restore points and
3662
+ which sources are restorable. Restore mode 'live' swaps state in place; 'reload' restores
3663
+ persisted storage then reloads the bundle (bulletproof; baselines = wipe to fresh install).
3664
+ Check the per-source applied/skipped results it returns \u2014 a partial restore is reported, not hidden.
3665
+
3430
3666
  For everything else:
3431
3667
  - \`list_tool_actions\` to see what a tool can do,
3432
3668
  - \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
@@ -3454,6 +3690,7 @@ function createMcpServer(sync) {
3454
3690
  registerStorageTools(server, sync);
3455
3691
  registerRoutesTools(server, sync);
3456
3692
  registerConsoleTools(server, sync);
3693
+ registerTimeMachineTools(server, sync);
3457
3694
  registerScreenshotTools(server, sync);
3458
3695
  registerInteractTools(server, sync);
3459
3696
  registerJsThreadTools(server, sync);
@@ -3463,7 +3700,7 @@ function createMcpServer(sync) {
3463
3700
  return server;
3464
3701
  }
3465
3702
  var deviceIdField = {
3466
- deviceId: import_zod18.z.string().optional().describe(
3703
+ deviceId: import_zod19.z.string().optional().describe(
3467
3704
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
3468
3705
  )
3469
3706
  };