@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/cli.js CHANGED
@@ -52,8 +52,12 @@ var SyncClient = class {
52
52
  this.devices = [];
53
53
  /** Latest snapshot payload per `${deviceId}::${toolId}`. */
54
54
  this.snapshots = /* @__PURE__ */ new Map();
55
- /** Tools we've asked the broker to keep streaming, as `${deviceId}::${toolId}`. */
56
- this.watching = /* @__PURE__ */ new Set();
55
+ /**
56
+ * Capture-mode watches this client holds, keyed by snapKey. Stored with
57
+ * their addressing because they have to be RE-SENT on every reconnect: the
58
+ * broker keys watch state by socket id, and a reconnect gets a new one.
59
+ */
60
+ this.watching = /* @__PURE__ */ new Map();
57
61
  /** In-flight actions awaiting a result, keyed by requestId. */
58
62
  this.pendingActions = /* @__PURE__ */ new Map();
59
63
  /** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
@@ -109,6 +113,7 @@ var SyncClient = class {
109
113
  resolve();
110
114
  };
111
115
  socket.on("connect", onConnect);
116
+ socket.on("connect", () => this.rearmWatches());
112
117
  socket.once("connect_error", (err) => {
113
118
  if (settled) return;
114
119
  settled = true;
@@ -219,11 +224,50 @@ var SyncClient = class {
219
224
  // ----------------------------------------------------------------
220
225
  // Watch + snapshots
221
226
  // ----------------------------------------------------------------
227
+ /**
228
+ * Keep the device RECORDING this tool, without asking it to stream.
229
+ *
230
+ * A dashboard watches for as long as its panel is open; this client is
231
+ * request/response, so it used to watch and unwatch around every single
232
+ * read. That works for a tool whose state just IS (the query cache, the env
233
+ * table) and is useless for one that records a timeline: the unwatch drops
234
+ * the device's subscriber count to zero, which stops capture, so nothing is
235
+ * ever recorded between two reads. The list comes back empty and reads as
236
+ * "the app made no requests" — a wrong conclusion that is very expensive to
237
+ * chase, because everything about the app looks fine.
238
+ *
239
+ * Capture-mode watches are held for the life of the connection and released
240
+ * on disconnect (the broker also drops them for us). They cost the device's
241
+ * ring buffer, not the socket: no snapshot is pushed, and reads still go
242
+ * through `request-devtool-state`.
243
+ */
222
244
  ensureWatching(toolId, targetDeviceId) {
223
245
  const key = snapKey(targetDeviceId, toolId);
224
246
  if (this.watching.has(key)) return;
225
- this.watching.add(key);
226
- this.socket?.emit("devtool-watch", { toolId, targetDeviceId, watching: true });
247
+ this.watching.set(key, { toolId, targetDeviceId });
248
+ this.sendWatch(toolId, targetDeviceId);
249
+ }
250
+ sendWatch(toolId, targetDeviceId) {
251
+ this.socket?.emit("devtool-watch", {
252
+ toolId,
253
+ targetDeviceId,
254
+ watching: true,
255
+ stream: false
256
+ });
257
+ }
258
+ /**
259
+ * Re-arm every held watch after a reconnect.
260
+ *
261
+ * The broker tracks watches per SOCKET, so a reconnect (broker restart, a
262
+ * dropped wifi link) silently discards all of ours while `watching` still
263
+ * says we hold them — `ensureWatching` would then short-circuit forever and
264
+ * the device would never record again. Silent and permanent: the exact
265
+ * failure this whole capture-mode change exists to remove.
266
+ */
267
+ rearmWatches() {
268
+ for (const { toolId, targetDeviceId } of this.watching.values()) {
269
+ this.sendWatch(toolId, targetDeviceId);
270
+ }
227
271
  }
228
272
  /** Stop streaming a tool (releases the device-side backpressure). */
229
273
  unwatch(toolId, targetDeviceId) {
@@ -236,26 +280,25 @@ var SyncClient = class {
236
280
  return this.snapshots.get(snapKey(deviceId, toolId));
237
281
  }
238
282
  /**
239
- * Get a tool's current state. Starts watching (so the device begins emitting),
240
- * explicitly requests state, and resolves with the next snapshot. Falls back
241
- * to the last cached snapshot if the request times out.
283
+ * Get a tool's current state: arm capture (once, for the connection), ask
284
+ * for state explicitly, and resolve with the snapshot that comes back.
285
+ * Falls back to the last cached snapshot if the request times out.
286
+ *
287
+ * The capture watch is deliberately NOT released afterwards — see
288
+ * `ensureWatching`. It costs the device nothing on the socket, and dropping
289
+ * it is what made every timeline tool look permanently empty from here.
242
290
  */
243
291
  async getSnapshot(toolId, deviceId, opts = {}) {
244
292
  const target = this.resolveDevice(deviceId);
245
293
  const key = snapKey(target, toolId);
246
- const wasWatching = this.watching.has(key);
247
294
  this.ensureWatching(toolId, target);
248
- try {
249
- const next = this.waitForSnapshot(key, opts.timeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS);
250
- this.socket?.emit("request-devtool-state", { toolId, targetDeviceId: target });
251
- const payload = await next;
252
- if (payload !== void 0) return { deviceId: target, payload };
253
- const cached = this.snapshots.get(key);
254
- if (cached !== void 0) return { deviceId: target, payload: cached };
255
- throw new Error(`Timed out waiting for "${toolId}" state from ${target} (is the tool active?)`);
256
- } finally {
257
- if (!wasWatching) this.unwatch(toolId, target);
258
- }
295
+ const next = this.waitForSnapshot(key, opts.timeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS);
296
+ this.socket?.emit("request-devtool-state", { toolId, targetDeviceId: target });
297
+ const payload = await next;
298
+ if (payload !== void 0) return { deviceId: target, payload };
299
+ const cached = this.snapshots.get(key);
300
+ if (cached !== void 0) return { deviceId: target, payload: cached };
301
+ throw new Error(`Timed out waiting for "${toolId}" state from ${target} (is the tool active?)`);
259
302
  }
260
303
  waitForSnapshot(key, timeoutMs) {
261
304
  return new Promise((resolve) => {
@@ -330,7 +373,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
330
373
 
331
374
  // src/server.ts
332
375
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
333
- var import_zod18 = require("zod");
376
+ var import_zod19 = require("zod");
334
377
 
335
378
  // src/tools/generic.ts
336
379
  var import_zod = require("zod");
@@ -341,7 +384,7 @@ var import_node_os = __toESM(require("os"));
341
384
  var import_node_path = __toESM(require("path"));
342
385
 
343
386
  // src/version.ts
344
- var VERSION = "7.0.16" ? "7.0.16" : "0.0.0-dev";
387
+ var VERSION = "7.0.18" ? "7.0.18" : "0.0.0-dev";
345
388
  var PACKAGE_NAME = "@buoy-gg/mcp";
346
389
 
347
390
  // src/update/checkForUpdate.ts
@@ -1342,6 +1385,60 @@ function truncate(v, n = 600) {
1342
1385
  const s = typeof v === "string" ? v : JSON.stringify(v);
1343
1386
  return s.length > n ? `${s.slice(0, n)}\u2026(+${s.length - n} chars)` : s;
1344
1387
  }
1388
+ var MAX_BODY_FETCHES = 8;
1389
+ async function resolveBodies(sync, rows, target, max = MAX_BODY_FETCHES) {
1390
+ const needed = rows.filter(
1391
+ (e) => e.id && (e.requestBodyOmitted === true && e.requestData === void 0 || e.responseBodyOmitted === true && e.responseData === void 0)
1392
+ );
1393
+ const bodies = /* @__PURE__ */ new Map();
1394
+ if (needed.length === 0) return { bodies, skipped: 0 };
1395
+ const take = needed.slice(0, max);
1396
+ const settled = await Promise.allSettled(
1397
+ take.map(
1398
+ (e) => sync.callAction(NETWORK_TOOL_ID, "getEventBody", { id: e.id }, target)
1399
+ )
1400
+ );
1401
+ settled.forEach((r, i) => {
1402
+ const id = take[i].id;
1403
+ const body = r.status === "fulfilled" ? r.value : null;
1404
+ bodies.set(id, body ? { ...body } : { unresolved: true });
1405
+ });
1406
+ return { bodies, skipped: needed.length - take.length };
1407
+ }
1408
+ async function captureDiagnosis(sync, target) {
1409
+ let status = null;
1410
+ try {
1411
+ status = await sync.callAction(
1412
+ NETWORK_TOOL_ID,
1413
+ "getCaptureStatus",
1414
+ {},
1415
+ target
1416
+ );
1417
+ } catch {
1418
+ return "The network tool has captured nothing yet.";
1419
+ }
1420
+ if (!status || status.capturing !== false) {
1421
+ return "The device IS capturing, so nothing has been requested since it started.";
1422
+ }
1423
+ 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.)";
1424
+ }
1425
+ function skippedNote(skipped) {
1426
+ return skipped > 0 ? `
1427
+
1428
+ _${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._` : "";
1429
+ }
1430
+ function bodyLines(e, fetched) {
1431
+ const got = e.id ? fetched.get(e.id) : void 0;
1432
+ const label = (inline, resolved, omitted) => {
1433
+ const value = inline ?? resolved;
1434
+ if (value !== void 0 && value !== null) return truncate(value);
1435
+ if (!omitted) return "(none)";
1436
+ return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
1437
+ };
1438
+ return `
1439
+ \u2514\u2500 request: ${label(e.requestData, got?.requestData, e.requestBodyOmitted)}
1440
+ \u2514\u2500 response: ${label(e.responseData, got?.responseData, e.responseBodyOmitted)}`;
1441
+ }
1345
1442
  function formatRow(index, e, markers) {
1346
1443
  const where = e.path || e.url;
1347
1444
  const bits = [
@@ -1395,6 +1492,7 @@ function registerNetworkTools(server, sync) {
1395
1492
  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.`
1396
1493
  );
1397
1494
  }
1495
+ const kept = args.includeBodies ? await resolveBodies(sync, rows2.map((r) => r.event), target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
1398
1496
  const body2 = rows2.map((r, i) => {
1399
1497
  const marks = [r.pinned ? "\u{1F4CC} pinned" : "", r.saved ? "\u{1F516} saved" : ""].filter(Boolean).join(" ");
1400
1498
  const stale = r.event.id?.startsWith("saved:") ? " \xB7 from an earlier app run" : "";
@@ -1402,12 +1500,7 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
1402
1500
  let out = formatRow(i + 1, r.event, marks);
1403
1501
  out += `
1404
1502
  \u2514\u2500 key \`${r.key}\`${stale}${trunc}`;
1405
- if (args.includeBodies) {
1406
- out += `
1407
- \u2514\u2500 request: ${truncate(r.event.requestData)}`;
1408
- out += `
1409
- \u2514\u2500 response: ${truncate(r.event.responseData)}`;
1410
- }
1503
+ if (args.includeBodies) out += bodyLines(r.event, kept.bodies);
1411
1504
  return out;
1412
1505
  }).join("\n");
1413
1506
  return text3(
@@ -1415,23 +1508,19 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
1415
1508
 
1416
1509
  ${body2}
1417
1510
 
1418
- _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._`
1511
+ _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)}`
1419
1512
  );
1420
1513
  }
1421
1514
  const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
1422
1515
  if (rows.length === 0) {
1423
- 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.";
1516
+ const why = events.length === 0 ? await captureDiagnosis(sync, target) : "No request matched the filter.";
1424
1517
  return text3(`No requests on ${target}. ${why}`);
1425
1518
  }
1519
+ const live = args.includeBodies ? await resolveBodies(sync, rows, target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
1426
1520
  const body = rows.map((e, i) => {
1427
1521
  const marks = [pinnedIds.has(e.id) ? "\u{1F4CC}" : "", savedIds.has(e.id) ? "\u{1F516}" : ""].filter(Boolean).join("");
1428
1522
  let out = formatRow(i + 1, e, marks);
1429
- if (args.includeBodies) {
1430
- out += `
1431
- \u2514\u2500 request: ${truncate(e.requestData)}`;
1432
- out += `
1433
- \u2514\u2500 response: ${truncate(e.responseData)}`;
1434
- }
1523
+ if (args.includeBodies) out += bodyLines(e, live.bodies);
1435
1524
  return out;
1436
1525
  }).join("\n");
1437
1526
  const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
@@ -1440,7 +1529,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
1440
1529
 
1441
1530
  ${body}
1442
1531
 
1443
- _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._`
1532
+ _\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)}`
1444
1533
  );
1445
1534
  } catch (e) {
1446
1535
  return fail4(errMsg4(e));
@@ -2326,8 +2415,147 @@ _Tail of recent logs. Use get_snapshot('console') for full args/stacks._`);
2326
2415
  );
2327
2416
  }
2328
2417
 
2329
- // src/tools/packs/screenshot.ts
2418
+ // src/tools/packs/timeMachine.ts
2330
2419
  var import_zod12 = require("zod");
2420
+ var TIME_MACHINE_TOOL_ID = "time-machine";
2421
+ var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2422
+ var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2423
+ var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
2424
+ function kb(bytes) {
2425
+ return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
2426
+ }
2427
+ function formatOutcome(outcome) {
2428
+ const lines = Object.entries(outcome.results).map(([sid, r]) => {
2429
+ const skips = r.skipped.slice(0, 3).map((s) => `
2430
+ \xB7 ${s.id}: ${s.reason}`).join("");
2431
+ const warns = r.warnings.slice(0, 3).map((w) => `
2432
+ \u26A0 ${w}`).join("");
2433
+ return ` ${r.ok ? "\u2713" : "\u2715"} ${sid} \u2014 applied ${r.applied}${r.skipped.length ? `, skipped ${r.skipped.length}` : ""}${skips}${warns}`;
2434
+ });
2435
+ return `Restored "${outcome.snapshotName}" (${outcome.mode}${outcome.willReload ? ", app reloading" : ""}):
2436
+ ${lines.join("\n")}`;
2437
+ }
2438
+ function registerTimeMachineTools(server, sync) {
2439
+ server.registerTool(
2440
+ "get_time_machine",
2441
+ {
2442
+ 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.",
2443
+ inputSchema: {
2444
+ deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2445
+ }
2446
+ },
2447
+ async (args) => {
2448
+ try {
2449
+ const target = sync.requireProDevice(args.deviceId);
2450
+ const { payload } = await sync.getSnapshot(TIME_MACHINE_TOOL_ID, target);
2451
+ const p = payload ?? {};
2452
+ const snapshots = p.snapshots ?? [];
2453
+ const providers = p.providers ?? [];
2454
+ const providerLines = providers.map((pr) => {
2455
+ const cap = pr.canCapture ? pr.canRestore ? "capture+restore" : "capture only" : "unavailable";
2456
+ return `- ${pr.label} (${pr.id}): ${cap}${pr.detail ? ` \u2014 ${pr.detail}` : ""}`;
2457
+ });
2458
+ const snapLines = snapshots.map((s) => {
2459
+ const warnings = Object.values(s.sources).reduce((n, src) => n + src.warnings.length, 0);
2460
+ const desc = s.baseline ? "baseline \u2014 restore wipes to fresh install (always reloads)" : `${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}`;
2461
+ return `- \`${s.id}\` **${s.name}** \u2014 ${new Date(s.createdAt).toISOString()} \xB7 ${desc}${warnings ? ` \xB7 \u26A0 ${warnings}` : ""}`;
2462
+ });
2463
+ const parts = [
2464
+ `**Time Machine \u2014 ${snapshots.length} snapshot(s):**`,
2465
+ snapLines.length ? snapLines.join("\n") : "_none yet \u2014 capture one with time_machine_action_",
2466
+ `**Sources:**
2467
+ ${providerLines.join("\n") || "_no providers registered_"}`
2468
+ ];
2469
+ if (p.lastOutcome) parts.push(`**Last restore:**
2470
+ ${formatOutcome(p.lastOutcome)}`);
2471
+ return text11(parts.join("\n\n"));
2472
+ } catch (e) {
2473
+ return fail12(errMsg12(e));
2474
+ }
2475
+ }
2476
+ );
2477
+ server.registerTool(
2478
+ "time_machine_action",
2479
+ {
2480
+ 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.",
2481
+ inputSchema: {
2482
+ action: import_zod12.z.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
2483
+ id: import_zod12.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
2484
+ name: import_zod12.z.string().optional().describe("Snapshot name (for capture and rename)."),
2485
+ mode: import_zod12.z.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
2486
+ 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").'),
2487
+ sourceIds: import_zod12.z.array(import_zod12.z.string()).optional().describe("Capture/restore only these source ids (default all)."),
2488
+ deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2489
+ }
2490
+ },
2491
+ async (args) => {
2492
+ try {
2493
+ const target = sync.requireProDevice(args.deviceId);
2494
+ const call = (action, params) => sync.callAction(TIME_MACHINE_TOOL_ID, action, params, target);
2495
+ switch (args.action) {
2496
+ case "capture": {
2497
+ const result = await call("capture", { name: args.name ?? "", sourceIds: args.sourceIds });
2498
+ const s = result.snapshot;
2499
+ const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
2500
+ \u26A0 ${sid}: ${w}`)).join("");
2501
+ return text11(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
2502
+ }
2503
+ case "capture_baseline": {
2504
+ const result = await call("captureBaseline", {});
2505
+ return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
2506
+ }
2507
+ case "preview": {
2508
+ if (!args.id) return fail12("preview requires `id` (from get_time_machine).");
2509
+ const result = await call("preview", { id: args.id });
2510
+ const lines = [];
2511
+ let totalApplies = 0;
2512
+ for (const src of result.sources) {
2513
+ const counts = {};
2514
+ for (const item of src.items) {
2515
+ counts[item.verdict] = (counts[item.verdict] ?? 0) + 1;
2516
+ }
2517
+ const applies = (counts.added ?? 0) + (counts.removed ?? 0) + (counts.changed ?? 0) + (counts["too-large"] ?? 0);
2518
+ totalApplies += applies;
2519
+ const parts = Object.entries(counts).filter(([v]) => v !== "unchanged").map(([v, n]) => `${n} ${v}`);
2520
+ lines.push(`- **${src.label}**: ${parts.length ? parts.join(", ") : "no changes"}${src.error ? ` \u2014 \u26A0 ${src.error}` : ""}`);
2521
+ for (const item of src.items.filter((i) => i.verdict !== "unchanged").slice(0, 8)) {
2522
+ lines.push(` ${item.verdict === "added" ? "+" : item.verdict === "removed" ? "\u2212" : item.verdict === "wont-apply" ? "\u2298" : "\u2248"} ${item.label}${item.reason ? ` (${item.reason})` : ""}`);
2523
+ }
2524
+ for (const note of src.notes) lines.push(` \xB7 ${note}`);
2525
+ }
2526
+ const head = totalApplies === 0 ? "**Restore would change NOTHING** (states identical or nothing applicable)." : `**Restore would apply ${totalApplies} change(s):**`;
2527
+ return text11(`${head}
2528
+ ${lines.join("\n")}`);
2529
+ }
2530
+ case "restore": {
2531
+ if (!args.id) return fail12("restore requires `id` (from get_time_machine).");
2532
+ const outcome = await call("restore", {
2533
+ id: args.id,
2534
+ mode: args.mode ?? "live",
2535
+ excludeKeys: args.excludeKeys
2536
+ });
2537
+ return text11(formatOutcome(outcome));
2538
+ }
2539
+ case "delete": {
2540
+ if (!args.id) return fail12("delete requires `id`.");
2541
+ await call("delete", { id: args.id });
2542
+ return text11(`Deleted snapshot \`${args.id}\`.`);
2543
+ }
2544
+ case "rename": {
2545
+ if (!args.id || !args.name) return fail12("rename requires `id` and `name`.");
2546
+ await call("rename", { id: args.id, name: args.name });
2547
+ return text11(`Renamed \`${args.id}\` to "${args.name}".`);
2548
+ }
2549
+ }
2550
+ } catch (e) {
2551
+ return fail12(errMsg12(e));
2552
+ }
2553
+ }
2554
+ );
2555
+ }
2556
+
2557
+ // src/tools/packs/screenshot.ts
2558
+ var import_zod13 = require("zod");
2331
2559
 
2332
2560
  // src/capture/simctl.ts
2333
2561
  var import_node_child_process = require("child_process");
@@ -2407,8 +2635,8 @@ async function cropToBuffer(srcPath, full, region) {
2407
2635
  var TOOL_ID = "highlight-updates";
2408
2636
  var MARGIN_PT = 12;
2409
2637
  var CAPTURE_PADDING_PT = 6;
2410
- var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2411
- var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
2638
+ var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2639
+ var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2412
2640
  function reasonMessage(res, query) {
2413
2641
  switch (res.reason) {
2414
2642
  case "no-renders":
@@ -2438,10 +2666,10 @@ function registerScreenshotTools(server, sync) {
2438
2666
  {
2439
2667
  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.",
2440
2668
  inputSchema: {
2441
- query: import_zod12.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
2442
- nativeTag: import_zod12.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
2443
- scrollIntoView: import_zod12.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
2444
- deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2669
+ query: import_zod13.z.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
2670
+ nativeTag: import_zod13.z.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
2671
+ scrollIntoView: import_zod13.z.boolean().optional().describe("Scroll the component into view before capturing, if it's off-screen. Default true."),
2672
+ deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2445
2673
  }
2446
2674
  },
2447
2675
  async (args) => {
@@ -2461,11 +2689,11 @@ function registerScreenshotTools(server, sync) {
2461
2689
  target
2462
2690
  );
2463
2691
  if (!res.matched || !res.rect || !res.screen) {
2464
- return fail12(reasonMessage(res, args.query) + candidatesText(res.candidates));
2692
+ return fail13(reasonMessage(res, args.query) + candidatesText(res.candidates));
2465
2693
  }
2466
2694
  const shot = await captureIosFramebuffer();
2467
2695
  if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
2468
- return fail12(shot.error ?? "Screenshot capture failed.");
2696
+ return fail13(shot.error ?? "Screenshot capture failed.");
2469
2697
  }
2470
2698
  const pxScale = shot.width / res.screen.width;
2471
2699
  const r = res.rect;
@@ -2481,7 +2709,7 @@ function registerScreenshotTools(server, sync) {
2481
2709
  }
2482
2710
  );
2483
2711
  if (!crop.ok || !crop.buffer) {
2484
- return fail12(crop.error ?? "Crop failed.");
2712
+ return fail13(crop.error ?? "Crop failed.");
2485
2713
  }
2486
2714
  const label = res.componentName || "component";
2487
2715
  const idPart = res.testID ? ` (testID: "${res.testID}")` : "";
@@ -2494,21 +2722,21 @@ function registerScreenshotTools(server, sync) {
2494
2722
  ]
2495
2723
  };
2496
2724
  } catch (e) {
2497
- return fail12(errMsg12(e));
2725
+ return fail13(errMsg13(e));
2498
2726
  }
2499
2727
  }
2500
2728
  );
2501
2729
  }
2502
2730
 
2503
2731
  // src/tools/packs/interact.ts
2504
- var import_zod13 = require("zod");
2732
+ var import_zod14 = require("zod");
2505
2733
  var TOOL_ID2 = "highlight-updates";
2506
- var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2507
- var fail13 = (message) => ({
2734
+ var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2735
+ var fail14 = (message) => ({
2508
2736
  isError: true,
2509
2737
  content: [{ type: "text", text: message }]
2510
2738
  });
2511
- var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2739
+ var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
2512
2740
  function formatElement(e) {
2513
2741
  let line = e.interactive ? "\u2022 " : " ";
2514
2742
  line += e.name;
@@ -2528,8 +2756,8 @@ function registerInteractTools(server, sync) {
2528
2756
  {
2529
2757
  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.",
2530
2758
  inputSchema: {
2531
- interactiveOnly: import_zod13.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
2532
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2759
+ interactiveOnly: import_zod14.z.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
2760
+ deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2533
2761
  }
2534
2762
  },
2535
2763
  async (args) => {
@@ -2539,16 +2767,16 @@ function registerInteractTools(server, sync) {
2539
2767
  const all = res?.elements ?? [];
2540
2768
  const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
2541
2769
  if (elements.length === 0) {
2542
- return text11(
2770
+ return text12(
2543
2771
  "No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
2544
2772
  );
2545
2773
  }
2546
2774
  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 }).`;
2547
- return text11(`${header}
2775
+ return text12(`${header}
2548
2776
 
2549
2777
  ${elements.map(formatElement).join("\n")}`);
2550
2778
  } catch (e) {
2551
- return fail13(errMsg13(e));
2779
+ return fail14(errMsg14(e));
2552
2780
  }
2553
2781
  }
2554
2782
  );
@@ -2557,20 +2785,20 @@ ${elements.map(formatElement).join("\n")}`);
2557
2785
  {
2558
2786
  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.",
2559
2787
  inputSchema: {
2560
- nativeTag: import_zod13.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
2561
- testID: import_zod13.z.string().optional().describe("Exact testID of the element."),
2562
- query: import_zod13.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
2563
- 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)."),
2564
- text: import_zod13.z.string().optional().describe("For a text input: the string to set via onChangeText."),
2565
- 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."),
2566
- scrollIntoView: import_zod13.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
2567
- deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2788
+ nativeTag: import_zod14.z.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
2789
+ testID: import_zod14.z.string().optional().describe("Exact testID of the element."),
2790
+ query: import_zod14.z.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
2791
+ 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)."),
2792
+ text: import_zod14.z.string().optional().describe("For a text input: the string to set via onChangeText."),
2793
+ 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."),
2794
+ scrollIntoView: import_zod14.z.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
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) => {
2571
2799
  try {
2572
2800
  if (args.nativeTag == null && !args.testID && !args.query) {
2573
- return fail13("Provide one of: nativeTag, testID, or query (from describe_screen).");
2801
+ return fail14("Provide one of: nativeTag, testID, or query (from describe_screen).");
2574
2802
  }
2575
2803
  const target = sync.requireProDevice(args.deviceId);
2576
2804
  const res = await sync.callAction(
@@ -2593,26 +2821,26 @@ ${elements.map(formatElement).join("\n")}`);
2593
2821
  const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
2594
2822
  const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
2595
2823
  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}`;
2596
- return text11(`${verb}${scrolledNote}.`);
2824
+ return text12(`${verb}${scrolledNote}.`);
2597
2825
  }
2598
2826
  let msg = res?.reason ?? "Tap failed.";
2599
2827
  if (res?.candidates?.length) {
2600
2828
  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");
2601
2829
  }
2602
- return fail13(msg);
2830
+ return fail14(msg);
2603
2831
  } catch (e) {
2604
- return fail13(errMsg13(e));
2832
+ return fail14(errMsg14(e));
2605
2833
  }
2606
2834
  }
2607
2835
  );
2608
2836
  }
2609
2837
 
2610
2838
  // src/tools/packs/jsThread.ts
2611
- var import_zod14 = require("zod");
2839
+ var import_zod15 = require("zod");
2612
2840
  var JS_TOP_TOOL_ID = "js-top";
2613
- var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2614
- var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2615
- var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
2841
+ var text13 = (s) => ({ content: [{ type: "text", text: s }] });
2842
+ var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2843
+ var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
2616
2844
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2617
2845
  function sparkline(values) {
2618
2846
  const max = Math.max(1, ...values);
@@ -2683,9 +2911,9 @@ function registerJsThreadTools(server, sync) {
2683
2911
  {
2684
2912
  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.",
2685
2913
  inputSchema: {
2686
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2687
- durationMs: import_zod14.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
2688
- clearFirst: import_zod14.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
2914
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2915
+ durationMs: import_zod15.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
2916
+ clearFirst: import_zod15.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
2689
2917
  }
2690
2918
  },
2691
2919
  async ({ deviceId, durationMs, clearFirst }) => {
@@ -2698,7 +2926,7 @@ function registerJsThreadTools(server, sync) {
2698
2926
  { durationMs: duration, clearFirst: clearFirst === true },
2699
2927
  target
2700
2928
  );
2701
- if (!snap) return fail14("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
2929
+ if (!snap) return fail15("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
2702
2930
  const md = formatTopMarkdown(snap, duration);
2703
2931
  const slim = {
2704
2932
  busyPct: Math.round(snap.busyPct * 10) / 10,
@@ -2717,13 +2945,13 @@ function registerJsThreadTools(server, sync) {
2717
2945
  worstFreezeMs: r.worstFreezeMs
2718
2946
  }))
2719
2947
  };
2720
- return text12(`${md}
2948
+ return text13(`${md}
2721
2949
 
2722
2950
  \`\`\`json
2723
2951
  ${JSON.stringify(slim)}
2724
2952
  \`\`\``);
2725
2953
  } catch (e) {
2726
- return fail14(errMsg14(e));
2954
+ return fail15(errMsg15(e));
2727
2955
  }
2728
2956
  }
2729
2957
  );
@@ -2732,8 +2960,8 @@ ${JSON.stringify(slim)}
2732
2960
  {
2733
2961
  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).',
2734
2962
  inputSchema: {
2735
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2736
- key: import_zod14.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
2963
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2964
+ key: import_zod15.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
2737
2965
  }
2738
2966
  },
2739
2967
  async ({ deviceId, key }) => {
@@ -2746,18 +2974,18 @@ ${JSON.stringify(slim)}
2746
2974
  target
2747
2975
  );
2748
2976
  if (!detail) {
2749
- return fail14(
2977
+ return fail15(
2750
2978
  `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
2751
2979
  );
2752
2980
  }
2753
2981
  const md = formatDetailMarkdown(detail);
2754
- return text12(`${md}
2982
+ return text13(`${md}
2755
2983
 
2756
2984
  \`\`\`json
2757
2985
  ${JSON.stringify(detail)}
2758
2986
  \`\`\``);
2759
2987
  } catch (e) {
2760
- return fail14(errMsg14(e));
2988
+ return fail15(errMsg15(e));
2761
2989
  }
2762
2990
  }
2763
2991
  );
@@ -2766,27 +2994,27 @@ ${JSON.stringify(detail)}
2766
2994
  {
2767
2995
  description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
2768
2996
  inputSchema: {
2769
- deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2997
+ deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected).")
2770
2998
  }
2771
2999
  },
2772
3000
  async ({ deviceId }) => {
2773
3001
  try {
2774
3002
  const target = sync.requireProDevice(deviceId);
2775
3003
  await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
2776
- return text12("Cleared js-top stats.");
3004
+ return text13("Cleared js-top stats.");
2777
3005
  } catch (e) {
2778
- return fail14(errMsg14(e));
3006
+ return fail15(errMsg15(e));
2779
3007
  }
2780
3008
  }
2781
3009
  );
2782
3010
  }
2783
3011
 
2784
3012
  // src/tools/packs/images.ts
2785
- var import_zod15 = require("zod");
3013
+ var import_zod16 = require("zod");
2786
3014
  var IMAGES_TOOL_ID = "images";
2787
- var text13 = (s) => ({ content: [{ type: "text", text: s }] });
2788
- var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2789
- var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
3015
+ var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3016
+ var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3017
+ var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
2790
3018
  function uriTail(uri) {
2791
3019
  if (!uri) return "(no source)";
2792
3020
  const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
@@ -2874,9 +3102,9 @@ function registerImagesTools(server, sync) {
2874
3102
  {
2875
3103
  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.",
2876
3104
  inputSchema: {
2877
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2878
- limit: import_zod15.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
2879
- status: import_zod15.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
3105
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3106
+ limit: import_zod16.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
3107
+ status: import_zod16.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
2880
3108
  }
2881
3109
  },
2882
3110
  async ({ deviceId, limit, status }) => {
@@ -2889,7 +3117,7 @@ function registerImagesTools(server, sync) {
2889
3117
  { limit: capped, status },
2890
3118
  target
2891
3119
  );
2892
- if (!list) return fail15("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
3120
+ if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
2893
3121
  const md = formatListMarkdown(list, capped);
2894
3122
  const slim = {
2895
3123
  stats: list.stats,
@@ -2916,13 +3144,13 @@ function registerImagesTools(server, sync) {
2916
3144
  layoutShifts: r.layoutShifts
2917
3145
  }))
2918
3146
  };
2919
- return text13(`${md}
3147
+ return text14(`${md}
2920
3148
 
2921
3149
  \`\`\`json
2922
3150
  ${JSON.stringify(slim)}
2923
3151
  \`\`\``);
2924
3152
  } catch (e) {
2925
- return fail15(errMsg15(e));
3153
+ return fail16(errMsg16(e));
2926
3154
  }
2927
3155
  }
2928
3156
  );
@@ -2931,9 +3159,9 @@ ${JSON.stringify(slim)}
2931
3159
  {
2932
3160
  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.",
2933
3161
  inputSchema: {
2934
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
2935
- id: import_zod15.z.number().describe("Record id from get_images."),
2936
- action: import_zod15.z.enum([
3162
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3163
+ id: import_zod16.z.number().describe("Record id from get_images."),
3164
+ action: import_zod16.z.enum([
2937
3165
  "hard_reload",
2938
3166
  "retry",
2939
3167
  "evict_disk",
@@ -2945,7 +3173,7 @@ ${JSON.stringify(slim)}
2945
3173
  "swap_url",
2946
3174
  "restore"
2947
3175
  ]).describe("What to do."),
2948
- url: import_zod15.z.string().optional().describe("Replacement URL (swap_url only).")
3176
+ url: import_zod16.z.string().optional().describe("Replacement URL (swap_url only).")
2949
3177
  }
2950
3178
  },
2951
3179
  async ({ deviceId, id, action, url }) => {
@@ -2953,8 +3181,8 @@ ${JSON.stringify(slim)}
2953
3181
  const target = sync.requireProDevice(deviceId);
2954
3182
  if (action === "prove_savings") {
2955
3183
  const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
2956
- if (!savings) return fail15("Device returned nothing for proveSavings.");
2957
- if (!savings.ok) return fail15(savings.message);
3184
+ if (!savings) return fail16("Device returned nothing for proveSavings.");
3185
+ if (!savings.ok) return fail16(savings.message);
2958
3186
  const parts = [`**prove_savings** on #${id}: ${savings.message}`];
2959
3187
  if (savings.originalBytes != null)
2960
3188
  parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
@@ -2966,11 +3194,11 @@ ${JSON.stringify(slim)}
2966
3194
  parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
2967
3195
  if (savings.optimizedUri)
2968
3196
  parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
2969
- return text13(parts.join("\n"));
3197
+ return text14(parts.join("\n"));
2970
3198
  }
2971
3199
  const entry = ACTION_MAP[action];
2972
- if (!entry) return fail15(`Unknown action: ${action}`);
2973
- if (action === "swap_url" && !url) return fail15("swap_url requires the `url` param.");
3200
+ if (!entry) return fail16(`Unknown action: ${action}`);
3201
+ if (action === "swap_url" && !url) return fail16("swap_url requires the `url` param.");
2974
3202
  const result = await sync.callAction(
2975
3203
  IMAGES_TOOL_ID,
2976
3204
  entry.action,
@@ -2990,9 +3218,9 @@ ${JSON.stringify(slim)}
2990
3218
  );
2991
3219
  if (detail.error) lines.push(`Error: ${detail.error}`);
2992
3220
  }
2993
- return text13(lines.join("\n"));
3221
+ return text14(lines.join("\n"));
2994
3222
  } catch (e) {
2995
- return fail15(errMsg15(e));
3223
+ return fail16(errMsg16(e));
2996
3224
  }
2997
3225
  }
2998
3226
  );
@@ -3001,12 +3229,12 @@ ${JSON.stringify(slim)}
3001
3229
  {
3002
3230
  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.",
3003
3231
  inputSchema: {
3004
- deviceId: import_zod15.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3005
- network: import_zod15.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
3006
- blankImages: import_zod15.z.boolean().optional().describe("Render all images without a source."),
3007
- 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."),
3008
- clearRegistry: import_zod15.z.boolean().optional().describe("Clear the captured image log."),
3009
- clearExpoCaches: import_zod15.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
3232
+ deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3233
+ network: import_zod16.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
3234
+ blankImages: import_zod16.z.boolean().optional().describe("Render all images without a source."),
3235
+ 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."),
3236
+ clearRegistry: import_zod16.z.boolean().optional().describe("Clear the captured image log."),
3237
+ clearExpoCaches: import_zod16.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
3010
3238
  }
3011
3239
  },
3012
3240
  async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
@@ -3033,22 +3261,22 @@ ${JSON.stringify(slim)}
3033
3261
  const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
3034
3262
  done.push(r?.message ?? "expo caches cleared");
3035
3263
  }
3036
- if (done.length === 0) return fail15("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
3037
- return text13(`Applied: ${done.join(" \xB7 ")}`);
3264
+ if (done.length === 0) return fail16("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
3265
+ return text14(`Applied: ${done.join(" \xB7 ")}`);
3038
3266
  } catch (e) {
3039
- return fail15(errMsg15(e));
3267
+ return fail16(errMsg16(e));
3040
3268
  }
3041
3269
  }
3042
3270
  );
3043
3271
  }
3044
3272
 
3045
3273
  // src/tools/packs/assets.ts
3046
- var import_zod16 = require("zod");
3274
+ var import_zod17 = require("zod");
3047
3275
  var ASSETS_TOOL_ID = "assets";
3048
- var text14 = (s) => ({ content: [{ type: "text", text: s }] });
3049
- var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3050
- var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
3051
- function kb(bytes) {
3276
+ var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3277
+ var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3278
+ var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3279
+ function kb2(bytes) {
3052
3280
  if (bytes == null || bytes <= 0) return "\u2013";
3053
3281
  if (bytes < 1024) return `${bytes}B`;
3054
3282
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
@@ -3056,7 +3284,7 @@ function kb(bytes) {
3056
3284
  }
3057
3285
  function sizeLabel(r) {
3058
3286
  if (r.sizeBytes == null) return "\u2013";
3059
- return r.sizeSource === "measured" ? kb(r.sizeBytes) : `~${kb(r.sizeBytes)}`;
3287
+ return r.sizeSource === "measured" ? kb2(r.sizeBytes) : `~${kb2(r.sizeBytes)}`;
3060
3288
  }
3061
3289
  function scalesLabel(scales) {
3062
3290
  if (!scales.length) return "\u2013";
@@ -3079,9 +3307,9 @@ function formatListMarkdown2(list, limit) {
3079
3307
  const ss = list.scanStatus;
3080
3308
  lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
3081
3309
  lines.push("");
3082
- const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb(v.bytes)})` : ""}`);
3310
+ const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb2(v.bytes)})` : ""}`);
3083
3311
  lines.push(
3084
- `${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 ")}`
3312
+ `${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 ")}`
3085
3313
  );
3086
3314
  if (ss.fontFamilies.length > 0) {
3087
3315
  lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
@@ -3097,7 +3325,7 @@ function formatListMarkdown2(list, limit) {
3097
3325
  if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
3098
3326
  if (ins.duplicates.length) {
3099
3327
  const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
3100
- chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb(wasted)} wasted)` : ""}`);
3328
+ chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb2(wasted)} wasted)` : ""}`);
3101
3329
  }
3102
3330
  if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
3103
3331
  if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
@@ -3111,7 +3339,7 @@ function formatListMarkdown2(list, limit) {
3111
3339
  }
3112
3340
  const diff = list.diff;
3113
3341
  if (diff) {
3114
- const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb(Math.abs(diff.netBytes))} net`;
3342
+ const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb2(Math.abs(diff.netBytes))} net`;
3115
3343
  lines.push("");
3116
3344
  lines.push(
3117
3345
  `**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" : ""}` : "")
@@ -3138,10 +3366,10 @@ function registerAssetsTools(server, sync) {
3138
3366
  {
3139
3367
  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.",
3140
3368
  inputSchema: {
3141
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3142
- limit: import_zod16.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
3143
- kind: import_zod16.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
3144
- unusedOnly: import_zod16.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
3369
+ deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3370
+ limit: import_zod17.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
3371
+ kind: import_zod17.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
3372
+ unusedOnly: import_zod17.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
3145
3373
  }
3146
3374
  },
3147
3375
  async ({ deviceId, limit, kind, unusedOnly }) => {
@@ -3154,7 +3382,7 @@ function registerAssetsTools(server, sync) {
3154
3382
  { limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
3155
3383
  target
3156
3384
  );
3157
- if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3385
+ if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
3158
3386
  if (unusedOnly) {
3159
3387
  list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
3160
3388
  }
@@ -3178,13 +3406,13 @@ function registerAssetsTools(server, sync) {
3178
3406
  sizeSource: r.sizeSource
3179
3407
  }))
3180
3408
  };
3181
- return text14(`${md}
3409
+ return text15(`${md}
3182
3410
 
3183
3411
  \`\`\`json
3184
3412
  ${JSON.stringify(slim)}
3185
3413
  \`\`\``);
3186
3414
  } catch (e) {
3187
- return fail16(errMsg16(e));
3415
+ return fail17(errMsg17(e));
3188
3416
  }
3189
3417
  }
3190
3418
  );
@@ -3193,9 +3421,9 @@ ${JSON.stringify(slim)}
3193
3421
  {
3194
3422
  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).",
3195
3423
  inputSchema: {
3196
- deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3197
- action: import_zod16.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
3198
- id: import_zod16.z.number().optional().describe("Asset id from get_assets (detail only).")
3424
+ deviceId: import_zod17.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
3425
+ action: import_zod17.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
3426
+ id: import_zod17.z.number().optional().describe("Asset id from get_assets (detail only).")
3199
3427
  }
3200
3428
  },
3201
3429
  async ({ deviceId, action, id }) => {
@@ -3205,19 +3433,19 @@ ${JSON.stringify(slim)}
3205
3433
  case "rescan": {
3206
3434
  const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
3207
3435
  const ss = result?.scanStatus;
3208
- if (!ss) return fail16("Device returned nothing for rescan.");
3209
- return text14(
3436
+ if (!ss) return fail17("Device returned nothing for rescan.");
3437
+ return text15(
3210
3438
  `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" : "")
3211
3439
  );
3212
3440
  }
3213
3441
  case "measure_sizes": {
3214
3442
  const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
3215
- return text14(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3443
+ return text15(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
3216
3444
  }
3217
3445
  case "detail": {
3218
- if (id == null) return fail16("detail requires the `id` param (from get_assets).");
3446
+ if (id == null) return fail17("detail requires the `id` param (from get_assets).");
3219
3447
  const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
3220
- if (!detail) return fail16(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3448
+ if (!detail) return fail17(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
3221
3449
  const lines = [
3222
3450
  `## ${detail.name}.${detail.type} (#${detail.id})`,
3223
3451
  "",
@@ -3226,50 +3454,50 @@ ${JSON.stringify(slim)}
3226
3454
  if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
3227
3455
  if (detail.sizesByScale) {
3228
3456
  for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
3229
- lines.push(`- Size @${scale}x: ${kb(bytes)}`);
3457
+ lines.push(`- Size @${scale}x: ${kb2(bytes)}`);
3230
3458
  }
3231
3459
  }
3232
3460
  if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
3233
- if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb(detail.estDecodedBytes)} (RGBA)`);
3461
+ if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb2(detail.estDecodedBytes)} (RGBA)`);
3234
3462
  if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
3235
3463
  if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
3236
3464
  if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
3237
3465
  if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
3238
- return text14(lines.join("\n"));
3466
+ return text15(lines.join("\n"));
3239
3467
  }
3240
3468
  case "save_baseline": {
3241
3469
  const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
3242
- return text14(
3470
+ return text15(
3243
3471
  `Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
3244
3472
  );
3245
3473
  }
3246
3474
  case "clear_baseline": {
3247
3475
  await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
3248
- return text14("Baseline cleared.");
3476
+ return text15("Baseline cleared.");
3249
3477
  }
3250
3478
  case "clear_records": {
3251
3479
  const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
3252
- return text14(result?.message ?? "Inventory cleared.");
3480
+ return text15(result?.message ?? "Inventory cleared.");
3253
3481
  }
3254
3482
  }
3255
3483
  } catch (e) {
3256
- return fail16(errMsg16(e));
3484
+ return fail17(errMsg17(e));
3257
3485
  }
3258
3486
  }
3259
3487
  );
3260
3488
  }
3261
3489
 
3262
3490
  // src/tools/packs/reload.ts
3263
- var import_zod17 = require("zod");
3491
+ var import_zod18 = require("zod");
3264
3492
  var APP_TOOL_ID = "app";
3265
3493
  var RELOAD_ACTION = "reloadApp";
3266
3494
  var DEFAULT_WAIT_MS = 3e4;
3267
3495
  var TEARDOWN_GRACE_MS = 1200;
3268
3496
  var PROBE_TIMEOUT_MS = 1500;
3269
3497
  var PROBE_INTERVAL_MS = 400;
3270
- var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3271
- var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3272
- var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
3498
+ var text16 = (s) => ({ content: [{ type: "text", text: s }] });
3499
+ var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
3500
+ var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
3273
3501
  var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
3274
3502
  async function ping(sync, deviceId, timeoutMs) {
3275
3503
  try {
@@ -3304,12 +3532,12 @@ function registerReloadTools(server, sync) {
3304
3532
  {
3305
3533
  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.",
3306
3534
  inputSchema: {
3307
- strategy: import_zod17.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3535
+ strategy: import_zod18.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
3308
3536
  "Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
3309
3537
  ),
3310
- 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."),
3311
- timeoutMs: import_zod17.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3312
- deviceId: import_zod17.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3538
+ 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."),
3539
+ timeoutMs: import_zod18.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
3540
+ deviceId: import_zod18.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
3313
3541
  }
3314
3542
  },
3315
3543
  async (args) => {
@@ -3325,19 +3553,19 @@ function registerReloadTools(server, sync) {
3325
3553
  );
3326
3554
  const via = ack?.strategy ? ` via ${ack.strategy}` : "";
3327
3555
  if (args.wait === false) {
3328
- return text15(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
3556
+ return text16(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
3329
3557
  }
3330
3558
  const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
3331
3559
  if (elapsed == null) {
3332
- return fail17(
3560
+ return fail18(
3333
3561
  `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.`
3334
3562
  );
3335
3563
  }
3336
- return text15(
3564
+ return text16(
3337
3565
  `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.`
3338
3566
  );
3339
3567
  } catch (e) {
3340
- return fail17(errMsg17(e));
3568
+ return fail18(errMsg18(e));
3341
3569
  }
3342
3570
  }
3343
3571
  );
@@ -3392,6 +3620,14 @@ measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 De
3392
3620
  builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
3393
3621
  re-read anything you cached.
3394
3622
 
3623
+ Testing a multi-step flow repeatedly (cart \u2192 checkout, login states, a bug that needs specific
3624
+ state)? \`time_machine_action\` captures the app's COMPLETE client state (storage, redux, zustand,
3625
+ jotai, react-query cache) as a named restore point and restores it in one call \u2014 capture once,
3626
+ drive the flow with \`tap_element\`, restore, repeat. \`get_time_machine\` lists restore points and
3627
+ which sources are restorable. Restore mode 'live' swaps state in place; 'reload' restores
3628
+ persisted storage then reloads the bundle (bulletproof; baselines = wipe to fresh install).
3629
+ Check the per-source applied/skipped results it returns \u2014 a partial restore is reported, not hidden.
3630
+
3395
3631
  For everything else:
3396
3632
  - \`list_tool_actions\` to see what a tool can do,
3397
3633
  - \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
@@ -3419,6 +3655,7 @@ function createMcpServer(sync) {
3419
3655
  registerStorageTools(server, sync);
3420
3656
  registerRoutesTools(server, sync);
3421
3657
  registerConsoleTools(server, sync);
3658
+ registerTimeMachineTools(server, sync);
3422
3659
  registerScreenshotTools(server, sync);
3423
3660
  registerInteractTools(server, sync);
3424
3661
  registerJsThreadTools(server, sync);
@@ -3428,7 +3665,7 @@ function createMcpServer(sync) {
3428
3665
  return server;
3429
3666
  }
3430
3667
  var deviceIdField = {
3431
- deviceId: import_zod18.z.string().optional().describe(
3668
+ deviceId: import_zod19.z.string().optional().describe(
3432
3669
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
3433
3670
  )
3434
3671
  };