@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/{chunk-P2OALRPK.mjs → chunk-ZLTXVKRQ.mjs} +387 -150
- package/dist/cli.js +398 -161
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +40 -4
- package/dist/index.d.ts +40 -4
- package/dist/index.js +398 -161
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -24,8 +24,12 @@ var SyncClient = class {
|
|
|
24
24
|
this.devices = [];
|
|
25
25
|
/** Latest snapshot payload per `${deviceId}::${toolId}`. */
|
|
26
26
|
this.snapshots = /* @__PURE__ */ new Map();
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Capture-mode watches this client holds, keyed by snapKey. Stored with
|
|
29
|
+
* their addressing because they have to be RE-SENT on every reconnect: the
|
|
30
|
+
* broker keys watch state by socket id, and a reconnect gets a new one.
|
|
31
|
+
*/
|
|
32
|
+
this.watching = /* @__PURE__ */ new Map();
|
|
29
33
|
/** In-flight actions awaiting a result, keyed by requestId. */
|
|
30
34
|
this.pendingActions = /* @__PURE__ */ new Map();
|
|
31
35
|
/** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
|
|
@@ -81,6 +85,7 @@ var SyncClient = class {
|
|
|
81
85
|
resolve();
|
|
82
86
|
};
|
|
83
87
|
socket.on("connect", onConnect);
|
|
88
|
+
socket.on("connect", () => this.rearmWatches());
|
|
84
89
|
socket.once("connect_error", (err) => {
|
|
85
90
|
if (settled) return;
|
|
86
91
|
settled = true;
|
|
@@ -191,11 +196,50 @@ var SyncClient = class {
|
|
|
191
196
|
// ----------------------------------------------------------------
|
|
192
197
|
// Watch + snapshots
|
|
193
198
|
// ----------------------------------------------------------------
|
|
199
|
+
/**
|
|
200
|
+
* Keep the device RECORDING this tool, without asking it to stream.
|
|
201
|
+
*
|
|
202
|
+
* A dashboard watches for as long as its panel is open; this client is
|
|
203
|
+
* request/response, so it used to watch and unwatch around every single
|
|
204
|
+
* read. That works for a tool whose state just IS (the query cache, the env
|
|
205
|
+
* table) and is useless for one that records a timeline: the unwatch drops
|
|
206
|
+
* the device's subscriber count to zero, which stops capture, so nothing is
|
|
207
|
+
* ever recorded between two reads. The list comes back empty and reads as
|
|
208
|
+
* "the app made no requests" — a wrong conclusion that is very expensive to
|
|
209
|
+
* chase, because everything about the app looks fine.
|
|
210
|
+
*
|
|
211
|
+
* Capture-mode watches are held for the life of the connection and released
|
|
212
|
+
* on disconnect (the broker also drops them for us). They cost the device's
|
|
213
|
+
* ring buffer, not the socket: no snapshot is pushed, and reads still go
|
|
214
|
+
* through `request-devtool-state`.
|
|
215
|
+
*/
|
|
194
216
|
ensureWatching(toolId, targetDeviceId) {
|
|
195
217
|
const key = snapKey(targetDeviceId, toolId);
|
|
196
218
|
if (this.watching.has(key)) return;
|
|
197
|
-
this.watching.
|
|
198
|
-
this.
|
|
219
|
+
this.watching.set(key, { toolId, targetDeviceId });
|
|
220
|
+
this.sendWatch(toolId, targetDeviceId);
|
|
221
|
+
}
|
|
222
|
+
sendWatch(toolId, targetDeviceId) {
|
|
223
|
+
this.socket?.emit("devtool-watch", {
|
|
224
|
+
toolId,
|
|
225
|
+
targetDeviceId,
|
|
226
|
+
watching: true,
|
|
227
|
+
stream: false
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Re-arm every held watch after a reconnect.
|
|
232
|
+
*
|
|
233
|
+
* The broker tracks watches per SOCKET, so a reconnect (broker restart, a
|
|
234
|
+
* dropped wifi link) silently discards all of ours while `watching` still
|
|
235
|
+
* says we hold them — `ensureWatching` would then short-circuit forever and
|
|
236
|
+
* the device would never record again. Silent and permanent: the exact
|
|
237
|
+
* failure this whole capture-mode change exists to remove.
|
|
238
|
+
*/
|
|
239
|
+
rearmWatches() {
|
|
240
|
+
for (const { toolId, targetDeviceId } of this.watching.values()) {
|
|
241
|
+
this.sendWatch(toolId, targetDeviceId);
|
|
242
|
+
}
|
|
199
243
|
}
|
|
200
244
|
/** Stop streaming a tool (releases the device-side backpressure). */
|
|
201
245
|
unwatch(toolId, targetDeviceId) {
|
|
@@ -208,26 +252,25 @@ var SyncClient = class {
|
|
|
208
252
|
return this.snapshots.get(snapKey(deviceId, toolId));
|
|
209
253
|
}
|
|
210
254
|
/**
|
|
211
|
-
* Get a tool's current state
|
|
212
|
-
*
|
|
213
|
-
* to the last cached snapshot if the request times out.
|
|
255
|
+
* Get a tool's current state: arm capture (once, for the connection), ask
|
|
256
|
+
* for state explicitly, and resolve with the snapshot that comes back.
|
|
257
|
+
* Falls back to the last cached snapshot if the request times out.
|
|
258
|
+
*
|
|
259
|
+
* The capture watch is deliberately NOT released afterwards — see
|
|
260
|
+
* `ensureWatching`. It costs the device nothing on the socket, and dropping
|
|
261
|
+
* it is what made every timeline tool look permanently empty from here.
|
|
214
262
|
*/
|
|
215
263
|
async getSnapshot(toolId, deviceId, opts = {}) {
|
|
216
264
|
const target = this.resolveDevice(deviceId);
|
|
217
265
|
const key = snapKey(target, toolId);
|
|
218
|
-
const wasWatching = this.watching.has(key);
|
|
219
266
|
this.ensureWatching(toolId, target);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
throw new Error(`Timed out waiting for "${toolId}" state from ${target} (is the tool active?)`);
|
|
228
|
-
} finally {
|
|
229
|
-
if (!wasWatching) this.unwatch(toolId, target);
|
|
230
|
-
}
|
|
267
|
+
const next = this.waitForSnapshot(key, opts.timeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS);
|
|
268
|
+
this.socket?.emit("request-devtool-state", { toolId, targetDeviceId: target });
|
|
269
|
+
const payload = await next;
|
|
270
|
+
if (payload !== void 0) return { deviceId: target, payload };
|
|
271
|
+
const cached = this.snapshots.get(key);
|
|
272
|
+
if (cached !== void 0) return { deviceId: target, payload: cached };
|
|
273
|
+
throw new Error(`Timed out waiting for "${toolId}" state from ${target} (is the tool active?)`);
|
|
231
274
|
}
|
|
232
275
|
waitForSnapshot(key, timeoutMs) {
|
|
233
276
|
return new Promise((resolve) => {
|
|
@@ -301,7 +344,7 @@ async function waitForBroker(url, attempts = 20) {
|
|
|
301
344
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
302
345
|
|
|
303
346
|
// src/version.ts
|
|
304
|
-
var VERSION = "7.0.
|
|
347
|
+
var VERSION = "7.0.18" ? "7.0.18" : "0.0.0-dev";
|
|
305
348
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
306
349
|
|
|
307
350
|
// src/update/checkForUpdate.ts
|
|
@@ -2025,7 +2068,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2025
2068
|
|
|
2026
2069
|
// src/server.ts
|
|
2027
2070
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2028
|
-
import { z as
|
|
2071
|
+
import { z as z19 } from "zod";
|
|
2029
2072
|
|
|
2030
2073
|
// src/tools/packs/network.ts
|
|
2031
2074
|
import { z as z12 } from "zod";
|
|
@@ -2079,6 +2122,60 @@ function truncate2(v, n = 600) {
|
|
|
2079
2122
|
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
2080
2123
|
return s.length > n ? `${s.slice(0, n)}\u2026(+${s.length - n} chars)` : s;
|
|
2081
2124
|
}
|
|
2125
|
+
var MAX_BODY_FETCHES = 8;
|
|
2126
|
+
async function resolveBodies(sync, rows, target, max = MAX_BODY_FETCHES) {
|
|
2127
|
+
const needed = rows.filter(
|
|
2128
|
+
(e) => e.id && (e.requestBodyOmitted === true && e.requestData === void 0 || e.responseBodyOmitted === true && e.responseData === void 0)
|
|
2129
|
+
);
|
|
2130
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
2131
|
+
if (needed.length === 0) return { bodies, skipped: 0 };
|
|
2132
|
+
const take = needed.slice(0, max);
|
|
2133
|
+
const settled = await Promise.allSettled(
|
|
2134
|
+
take.map(
|
|
2135
|
+
(e) => sync.callAction(NETWORK_TOOL_ID, "getEventBody", { id: e.id }, target)
|
|
2136
|
+
)
|
|
2137
|
+
);
|
|
2138
|
+
settled.forEach((r, i) => {
|
|
2139
|
+
const id = take[i].id;
|
|
2140
|
+
const body = r.status === "fulfilled" ? r.value : null;
|
|
2141
|
+
bodies.set(id, body ? { ...body } : { unresolved: true });
|
|
2142
|
+
});
|
|
2143
|
+
return { bodies, skipped: needed.length - take.length };
|
|
2144
|
+
}
|
|
2145
|
+
async function captureDiagnosis(sync, target) {
|
|
2146
|
+
let status = null;
|
|
2147
|
+
try {
|
|
2148
|
+
status = await sync.callAction(
|
|
2149
|
+
NETWORK_TOOL_ID,
|
|
2150
|
+
"getCaptureStatus",
|
|
2151
|
+
{},
|
|
2152
|
+
target
|
|
2153
|
+
);
|
|
2154
|
+
} catch {
|
|
2155
|
+
return "The network tool has captured nothing yet.";
|
|
2156
|
+
}
|
|
2157
|
+
if (!status || status.capturing !== false) {
|
|
2158
|
+
return "The device IS capturing, so nothing has been requested since it started.";
|
|
2159
|
+
}
|
|
2160
|
+
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.)";
|
|
2161
|
+
}
|
|
2162
|
+
function skippedNote(skipped) {
|
|
2163
|
+
return skipped > 0 ? `
|
|
2164
|
+
|
|
2165
|
+
_${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._` : "";
|
|
2166
|
+
}
|
|
2167
|
+
function bodyLines(e, fetched) {
|
|
2168
|
+
const got = e.id ? fetched.get(e.id) : void 0;
|
|
2169
|
+
const label = (inline, resolved, omitted) => {
|
|
2170
|
+
const value = inline ?? resolved;
|
|
2171
|
+
if (value !== void 0 && value !== null) return truncate2(value);
|
|
2172
|
+
if (!omitted) return "(none)";
|
|
2173
|
+
return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
|
|
2174
|
+
};
|
|
2175
|
+
return `
|
|
2176
|
+
\u2514\u2500 request: ${label(e.requestData, got?.requestData, e.requestBodyOmitted)}
|
|
2177
|
+
\u2514\u2500 response: ${label(e.responseData, got?.responseData, e.responseBodyOmitted)}`;
|
|
2178
|
+
}
|
|
2082
2179
|
function formatRow(index, e, markers) {
|
|
2083
2180
|
const where = e.path || e.url;
|
|
2084
2181
|
const bits = [
|
|
@@ -2132,6 +2229,7 @@ function registerNetworkTools(server, sync) {
|
|
|
2132
2229
|
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.`
|
|
2133
2230
|
);
|
|
2134
2231
|
}
|
|
2232
|
+
const kept = args.includeBodies ? await resolveBodies(sync, rows2.map((r) => r.event), target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
|
|
2135
2233
|
const body2 = rows2.map((r, i) => {
|
|
2136
2234
|
const marks = [r.pinned ? "\u{1F4CC} pinned" : "", r.saved ? "\u{1F516} saved" : ""].filter(Boolean).join(" ");
|
|
2137
2235
|
const stale = r.event.id?.startsWith("saved:") ? " \xB7 from an earlier app run" : "";
|
|
@@ -2139,12 +2237,7 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
|
|
|
2139
2237
|
let out = formatRow(i + 1, r.event, marks);
|
|
2140
2238
|
out += `
|
|
2141
2239
|
\u2514\u2500 key \`${r.key}\`${stale}${trunc}`;
|
|
2142
|
-
if (args.includeBodies)
|
|
2143
|
-
out += `
|
|
2144
|
-
\u2514\u2500 request: ${truncate2(r.event.requestData)}`;
|
|
2145
|
-
out += `
|
|
2146
|
-
\u2514\u2500 response: ${truncate2(r.event.responseData)}`;
|
|
2147
|
-
}
|
|
2240
|
+
if (args.includeBodies) out += bodyLines(r.event, kept.bodies);
|
|
2148
2241
|
return out;
|
|
2149
2242
|
}).join("\n");
|
|
2150
2243
|
return text10(
|
|
@@ -2152,23 +2245,19 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
|
|
|
2152
2245
|
|
|
2153
2246
|
${body2}
|
|
2154
2247
|
|
|
2155
|
-
_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._`
|
|
2248
|
+
_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)}`
|
|
2156
2249
|
);
|
|
2157
2250
|
}
|
|
2158
2251
|
const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
2159
2252
|
if (rows.length === 0) {
|
|
2160
|
-
const why = events.length === 0 ?
|
|
2253
|
+
const why = events.length === 0 ? await captureDiagnosis(sync, target) : "No request matched the filter.";
|
|
2161
2254
|
return text10(`No requests on ${target}. ${why}`);
|
|
2162
2255
|
}
|
|
2256
|
+
const live = args.includeBodies ? await resolveBodies(sync, rows, target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
|
|
2163
2257
|
const body = rows.map((e, i) => {
|
|
2164
2258
|
const marks = [pinnedIds.has(e.id) ? "\u{1F4CC}" : "", savedIds.has(e.id) ? "\u{1F516}" : ""].filter(Boolean).join("");
|
|
2165
2259
|
let out = formatRow(i + 1, e, marks);
|
|
2166
|
-
if (args.includeBodies)
|
|
2167
|
-
out += `
|
|
2168
|
-
\u2514\u2500 request: ${truncate2(e.requestData)}`;
|
|
2169
|
-
out += `
|
|
2170
|
-
\u2514\u2500 response: ${truncate2(e.responseData)}`;
|
|
2171
|
-
}
|
|
2260
|
+
if (args.includeBodies) out += bodyLines(e, live.bodies);
|
|
2172
2261
|
return out;
|
|
2173
2262
|
}).join("\n");
|
|
2174
2263
|
const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
|
|
@@ -2177,7 +2266,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
|
|
|
2177
2266
|
|
|
2178
2267
|
${body}
|
|
2179
2268
|
|
|
2180
|
-
_\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._`
|
|
2269
|
+
_\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)}`
|
|
2181
2270
|
);
|
|
2182
2271
|
} catch (e) {
|
|
2183
2272
|
return fail12(errMsg12(e));
|
|
@@ -2468,15 +2557,154 @@ A rule that never fires is usually one of: the master switch is off, the interce
|
|
|
2468
2557
|
);
|
|
2469
2558
|
}
|
|
2470
2559
|
|
|
2471
|
-
// src/tools/packs/
|
|
2560
|
+
// src/tools/packs/timeMachine.ts
|
|
2472
2561
|
import { z as z13 } from "zod";
|
|
2473
|
-
var
|
|
2562
|
+
var TIME_MACHINE_TOOL_ID = "time-machine";
|
|
2474
2563
|
var text11 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2475
|
-
var fail13 = (message) => ({
|
|
2564
|
+
var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2565
|
+
var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
2566
|
+
function kb(bytes) {
|
|
2567
|
+
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
2568
|
+
}
|
|
2569
|
+
function formatOutcome(outcome) {
|
|
2570
|
+
const lines = Object.entries(outcome.results).map(([sid, r]) => {
|
|
2571
|
+
const skips = r.skipped.slice(0, 3).map((s) => `
|
|
2572
|
+
\xB7 ${s.id}: ${s.reason}`).join("");
|
|
2573
|
+
const warns = r.warnings.slice(0, 3).map((w) => `
|
|
2574
|
+
\u26A0 ${w}`).join("");
|
|
2575
|
+
return ` ${r.ok ? "\u2713" : "\u2715"} ${sid} \u2014 applied ${r.applied}${r.skipped.length ? `, skipped ${r.skipped.length}` : ""}${skips}${warns}`;
|
|
2576
|
+
});
|
|
2577
|
+
return `Restored "${outcome.snapshotName}" (${outcome.mode}${outcome.willReload ? ", app reloading" : ""}):
|
|
2578
|
+
${lines.join("\n")}`;
|
|
2579
|
+
}
|
|
2580
|
+
function registerTimeMachineTools(server, sync) {
|
|
2581
|
+
server.registerTool(
|
|
2582
|
+
"get_time_machine",
|
|
2583
|
+
{
|
|
2584
|
+
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.",
|
|
2585
|
+
inputSchema: {
|
|
2586
|
+
deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2587
|
+
}
|
|
2588
|
+
},
|
|
2589
|
+
async (args) => {
|
|
2590
|
+
try {
|
|
2591
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2592
|
+
const { payload } = await sync.getSnapshot(TIME_MACHINE_TOOL_ID, target);
|
|
2593
|
+
const p = payload ?? {};
|
|
2594
|
+
const snapshots = p.snapshots ?? [];
|
|
2595
|
+
const providers = p.providers ?? [];
|
|
2596
|
+
const providerLines = providers.map((pr) => {
|
|
2597
|
+
const cap = pr.canCapture ? pr.canRestore ? "capture+restore" : "capture only" : "unavailable";
|
|
2598
|
+
return `- ${pr.label} (${pr.id}): ${cap}${pr.detail ? ` \u2014 ${pr.detail}` : ""}`;
|
|
2599
|
+
});
|
|
2600
|
+
const snapLines = snapshots.map((s) => {
|
|
2601
|
+
const warnings = Object.values(s.sources).reduce((n, src) => n + src.warnings.length, 0);
|
|
2602
|
+
const desc = s.baseline ? "baseline \u2014 restore wipes to fresh install (always reloads)" : `${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}`;
|
|
2603
|
+
return `- \`${s.id}\` **${s.name}** \u2014 ${new Date(s.createdAt).toISOString()} \xB7 ${desc}${warnings ? ` \xB7 \u26A0 ${warnings}` : ""}`;
|
|
2604
|
+
});
|
|
2605
|
+
const parts = [
|
|
2606
|
+
`**Time Machine \u2014 ${snapshots.length} snapshot(s):**`,
|
|
2607
|
+
snapLines.length ? snapLines.join("\n") : "_none yet \u2014 capture one with time_machine_action_",
|
|
2608
|
+
`**Sources:**
|
|
2609
|
+
${providerLines.join("\n") || "_no providers registered_"}`
|
|
2610
|
+
];
|
|
2611
|
+
if (p.lastOutcome) parts.push(`**Last restore:**
|
|
2612
|
+
${formatOutcome(p.lastOutcome)}`);
|
|
2613
|
+
return text11(parts.join("\n\n"));
|
|
2614
|
+
} catch (e) {
|
|
2615
|
+
return fail13(errMsg13(e));
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
);
|
|
2619
|
+
server.registerTool(
|
|
2620
|
+
"time_machine_action",
|
|
2621
|
+
{
|
|
2622
|
+
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.",
|
|
2623
|
+
inputSchema: {
|
|
2624
|
+
action: z13.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
|
|
2625
|
+
id: z13.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
2626
|
+
name: z13.string().optional().describe("Snapshot name (for capture and rename)."),
|
|
2627
|
+
mode: z13.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
|
|
2628
|
+
excludeKeys: z13.array(z13.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").'),
|
|
2629
|
+
sourceIds: z13.array(z13.string()).optional().describe("Capture/restore only these source ids (default all)."),
|
|
2630
|
+
deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2631
|
+
}
|
|
2632
|
+
},
|
|
2633
|
+
async (args) => {
|
|
2634
|
+
try {
|
|
2635
|
+
const target = sync.requireProDevice(args.deviceId);
|
|
2636
|
+
const call = (action, params) => sync.callAction(TIME_MACHINE_TOOL_ID, action, params, target);
|
|
2637
|
+
switch (args.action) {
|
|
2638
|
+
case "capture": {
|
|
2639
|
+
const result = await call("capture", { name: args.name ?? "", sourceIds: args.sourceIds });
|
|
2640
|
+
const s = result.snapshot;
|
|
2641
|
+
const warnings = Object.entries(s.sources).flatMap(([sid, src]) => src.warnings.map((w) => `
|
|
2642
|
+
\u26A0 ${sid}: ${w}`)).join("");
|
|
2643
|
+
return text11(`Captured \`${s.id}\` "${s.name}" \u2014 ${kb(s.sizeBytes)} \xB7 ${Object.keys(s.sources).join(", ")}${warnings}`);
|
|
2644
|
+
}
|
|
2645
|
+
case "capture_baseline": {
|
|
2646
|
+
const result = await call("captureBaseline", {});
|
|
2647
|
+
return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
|
|
2648
|
+
}
|
|
2649
|
+
case "preview": {
|
|
2650
|
+
if (!args.id) return fail13("preview requires `id` (from get_time_machine).");
|
|
2651
|
+
const result = await call("preview", { id: args.id });
|
|
2652
|
+
const lines = [];
|
|
2653
|
+
let totalApplies = 0;
|
|
2654
|
+
for (const src of result.sources) {
|
|
2655
|
+
const counts = {};
|
|
2656
|
+
for (const item of src.items) {
|
|
2657
|
+
counts[item.verdict] = (counts[item.verdict] ?? 0) + 1;
|
|
2658
|
+
}
|
|
2659
|
+
const applies = (counts.added ?? 0) + (counts.removed ?? 0) + (counts.changed ?? 0) + (counts["too-large"] ?? 0);
|
|
2660
|
+
totalApplies += applies;
|
|
2661
|
+
const parts = Object.entries(counts).filter(([v]) => v !== "unchanged").map(([v, n]) => `${n} ${v}`);
|
|
2662
|
+
lines.push(`- **${src.label}**: ${parts.length ? parts.join(", ") : "no changes"}${src.error ? ` \u2014 \u26A0 ${src.error}` : ""}`);
|
|
2663
|
+
for (const item of src.items.filter((i) => i.verdict !== "unchanged").slice(0, 8)) {
|
|
2664
|
+
lines.push(` ${item.verdict === "added" ? "+" : item.verdict === "removed" ? "\u2212" : item.verdict === "wont-apply" ? "\u2298" : "\u2248"} ${item.label}${item.reason ? ` (${item.reason})` : ""}`);
|
|
2665
|
+
}
|
|
2666
|
+
for (const note of src.notes) lines.push(` \xB7 ${note}`);
|
|
2667
|
+
}
|
|
2668
|
+
const head = totalApplies === 0 ? "**Restore would change NOTHING** (states identical or nothing applicable)." : `**Restore would apply ${totalApplies} change(s):**`;
|
|
2669
|
+
return text11(`${head}
|
|
2670
|
+
${lines.join("\n")}`);
|
|
2671
|
+
}
|
|
2672
|
+
case "restore": {
|
|
2673
|
+
if (!args.id) return fail13("restore requires `id` (from get_time_machine).");
|
|
2674
|
+
const outcome = await call("restore", {
|
|
2675
|
+
id: args.id,
|
|
2676
|
+
mode: args.mode ?? "live",
|
|
2677
|
+
excludeKeys: args.excludeKeys
|
|
2678
|
+
});
|
|
2679
|
+
return text11(formatOutcome(outcome));
|
|
2680
|
+
}
|
|
2681
|
+
case "delete": {
|
|
2682
|
+
if (!args.id) return fail13("delete requires `id`.");
|
|
2683
|
+
await call("delete", { id: args.id });
|
|
2684
|
+
return text11(`Deleted snapshot \`${args.id}\`.`);
|
|
2685
|
+
}
|
|
2686
|
+
case "rename": {
|
|
2687
|
+
if (!args.id || !args.name) return fail13("rename requires `id` and `name`.");
|
|
2688
|
+
await call("rename", { id: args.id, name: args.name });
|
|
2689
|
+
return text11(`Renamed \`${args.id}\` to "${args.name}".`);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
} catch (e) {
|
|
2693
|
+
return fail13(errMsg13(e));
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
// src/tools/packs/interact.ts
|
|
2700
|
+
import { z as z14 } from "zod";
|
|
2701
|
+
var TOOL_ID2 = "highlight-updates";
|
|
2702
|
+
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2703
|
+
var fail14 = (message) => ({
|
|
2476
2704
|
isError: true,
|
|
2477
2705
|
content: [{ type: "text", text: message }]
|
|
2478
2706
|
});
|
|
2479
|
-
var
|
|
2707
|
+
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
2480
2708
|
function formatElement(e) {
|
|
2481
2709
|
let line = e.interactive ? "\u2022 " : " ";
|
|
2482
2710
|
line += e.name;
|
|
@@ -2496,8 +2724,8 @@ function registerInteractTools(server, sync) {
|
|
|
2496
2724
|
{
|
|
2497
2725
|
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.",
|
|
2498
2726
|
inputSchema: {
|
|
2499
|
-
interactiveOnly:
|
|
2500
|
-
deviceId:
|
|
2727
|
+
interactiveOnly: z14.boolean().optional().describe("Only return tappable (onPress) elements. Default false (include labels/text for context)."),
|
|
2728
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2501
2729
|
}
|
|
2502
2730
|
},
|
|
2503
2731
|
async (args) => {
|
|
@@ -2507,16 +2735,16 @@ function registerInteractTools(server, sync) {
|
|
|
2507
2735
|
const all = res?.elements ?? [];
|
|
2508
2736
|
const elements = args.interactiveOnly ? all.filter((e) => e.interactive) : all;
|
|
2509
2737
|
if (elements.length === 0) {
|
|
2510
|
-
return
|
|
2738
|
+
return text12(
|
|
2511
2739
|
"No elements found on screen. If the app just navigated, wait a moment and retry; make sure highlight-updates is enabled in the app."
|
|
2512
2740
|
);
|
|
2513
2741
|
}
|
|
2514
2742
|
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 }).`;
|
|
2515
|
-
return
|
|
2743
|
+
return text12(`${header}
|
|
2516
2744
|
|
|
2517
2745
|
${elements.map(formatElement).join("\n")}`);
|
|
2518
2746
|
} catch (e) {
|
|
2519
|
-
return
|
|
2747
|
+
return fail14(errMsg14(e));
|
|
2520
2748
|
}
|
|
2521
2749
|
}
|
|
2522
2750
|
);
|
|
@@ -2525,20 +2753,20 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2525
2753
|
{
|
|
2526
2754
|
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.",
|
|
2527
2755
|
inputSchema: {
|
|
2528
|
-
nativeTag:
|
|
2529
|
-
testID:
|
|
2530
|
-
query:
|
|
2531
|
-
value:
|
|
2532
|
-
text:
|
|
2533
|
-
longPress:
|
|
2534
|
-
scrollIntoView:
|
|
2535
|
-
deviceId:
|
|
2756
|
+
nativeTag: z14.number().optional().describe("Exact nativeTag from describe_screen (most precise)."),
|
|
2757
|
+
testID: z14.string().optional().describe("Exact testID of the element."),
|
|
2758
|
+
query: z14.string().optional().describe('Fuzzy match against testID / label / visible text / component name \u2014 e.g. "Sign in".'),
|
|
2759
|
+
value: z14.union([z14.number(), z14.boolean()]).optional().describe("For a toggle: true/false (omit to flip). For a slider: the numeric value (omit to jump to the far end)."),
|
|
2760
|
+
text: z14.string().optional().describe("For a text input: the string to set via onChangeText."),
|
|
2761
|
+
longPress: z14.boolean().optional().describe("Invoke the element's onLongPress instead of onPress. Fails with a clear reason if the element has no onLongPress handler."),
|
|
2762
|
+
scrollIntoView: z14.boolean().optional().describe("Scroll the target into view first if it's off-screen. Default true."),
|
|
2763
|
+
deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2536
2764
|
}
|
|
2537
2765
|
},
|
|
2538
2766
|
async (args) => {
|
|
2539
2767
|
try {
|
|
2540
2768
|
if (args.nativeTag == null && !args.testID && !args.query) {
|
|
2541
|
-
return
|
|
2769
|
+
return fail14("Provide one of: nativeTag, testID, or query (from describe_screen).");
|
|
2542
2770
|
}
|
|
2543
2771
|
const target = sync.requireProDevice(args.deviceId);
|
|
2544
2772
|
const res = await sync.callAction(
|
|
@@ -2561,26 +2789,26 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2561
2789
|
const id = m.testID ? ` [testID=${m.testID}]` : m.nativeTag != null ? ` #${m.nativeTag}` : "";
|
|
2562
2790
|
const scrolledNote = res.scrolled ? " (scrolled into view)" : "";
|
|
2563
2791
|
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}`;
|
|
2564
|
-
return
|
|
2792
|
+
return text12(`${verb}${scrolledNote}.`);
|
|
2565
2793
|
}
|
|
2566
2794
|
let msg = res?.reason ?? "Tap failed.";
|
|
2567
2795
|
if (res?.candidates?.length) {
|
|
2568
2796
|
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");
|
|
2569
2797
|
}
|
|
2570
|
-
return
|
|
2798
|
+
return fail14(msg);
|
|
2571
2799
|
} catch (e) {
|
|
2572
|
-
return
|
|
2800
|
+
return fail14(errMsg14(e));
|
|
2573
2801
|
}
|
|
2574
2802
|
}
|
|
2575
2803
|
);
|
|
2576
2804
|
}
|
|
2577
2805
|
|
|
2578
2806
|
// src/tools/packs/jsThread.ts
|
|
2579
|
-
import { z as
|
|
2807
|
+
import { z as z15 } from "zod";
|
|
2580
2808
|
var JS_TOP_TOOL_ID = "js-top";
|
|
2581
|
-
var
|
|
2582
|
-
var
|
|
2583
|
-
var
|
|
2809
|
+
var text13 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2810
|
+
var fail15 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2811
|
+
var errMsg15 = (e) => e instanceof Error ? e.message : String(e);
|
|
2584
2812
|
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
2585
2813
|
function sparkline(values) {
|
|
2586
2814
|
const max = Math.max(1, ...values);
|
|
@@ -2651,9 +2879,9 @@ function registerJsThreadTools(server, sync) {
|
|
|
2651
2879
|
{
|
|
2652
2880
|
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.",
|
|
2653
2881
|
inputSchema: {
|
|
2654
|
-
deviceId:
|
|
2655
|
-
durationMs:
|
|
2656
|
-
clearFirst:
|
|
2882
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2883
|
+
durationMs: z15.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
2884
|
+
clearFirst: z15.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
2657
2885
|
}
|
|
2658
2886
|
},
|
|
2659
2887
|
async ({ deviceId, durationMs, clearFirst }) => {
|
|
@@ -2666,7 +2894,7 @@ function registerJsThreadTools(server, sync) {
|
|
|
2666
2894
|
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
2667
2895
|
target
|
|
2668
2896
|
);
|
|
2669
|
-
if (!snap) return
|
|
2897
|
+
if (!snap) return fail15("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
2670
2898
|
const md = formatTopMarkdown(snap, duration);
|
|
2671
2899
|
const slim = {
|
|
2672
2900
|
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
@@ -2685,13 +2913,13 @@ function registerJsThreadTools(server, sync) {
|
|
|
2685
2913
|
worstFreezeMs: r.worstFreezeMs
|
|
2686
2914
|
}))
|
|
2687
2915
|
};
|
|
2688
|
-
return
|
|
2916
|
+
return text13(`${md}
|
|
2689
2917
|
|
|
2690
2918
|
\`\`\`json
|
|
2691
2919
|
${JSON.stringify(slim)}
|
|
2692
2920
|
\`\`\``);
|
|
2693
2921
|
} catch (e) {
|
|
2694
|
-
return
|
|
2922
|
+
return fail15(errMsg15(e));
|
|
2695
2923
|
}
|
|
2696
2924
|
}
|
|
2697
2925
|
);
|
|
@@ -2700,8 +2928,8 @@ ${JSON.stringify(slim)}
|
|
|
2700
2928
|
{
|
|
2701
2929
|
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).',
|
|
2702
2930
|
inputSchema: {
|
|
2703
|
-
deviceId:
|
|
2704
|
-
key:
|
|
2931
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2932
|
+
key: z15.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
2705
2933
|
}
|
|
2706
2934
|
},
|
|
2707
2935
|
async ({ deviceId, key }) => {
|
|
@@ -2714,18 +2942,18 @@ ${JSON.stringify(slim)}
|
|
|
2714
2942
|
target
|
|
2715
2943
|
);
|
|
2716
2944
|
if (!detail) {
|
|
2717
|
-
return
|
|
2945
|
+
return fail15(
|
|
2718
2946
|
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
2719
2947
|
);
|
|
2720
2948
|
}
|
|
2721
2949
|
const md = formatDetailMarkdown(detail);
|
|
2722
|
-
return
|
|
2950
|
+
return text13(`${md}
|
|
2723
2951
|
|
|
2724
2952
|
\`\`\`json
|
|
2725
2953
|
${JSON.stringify(detail)}
|
|
2726
2954
|
\`\`\``);
|
|
2727
2955
|
} catch (e) {
|
|
2728
|
-
return
|
|
2956
|
+
return fail15(errMsg15(e));
|
|
2729
2957
|
}
|
|
2730
2958
|
}
|
|
2731
2959
|
);
|
|
@@ -2734,27 +2962,27 @@ ${JSON.stringify(detail)}
|
|
|
2734
2962
|
{
|
|
2735
2963
|
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
2736
2964
|
inputSchema: {
|
|
2737
|
-
deviceId:
|
|
2965
|
+
deviceId: z15.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2738
2966
|
}
|
|
2739
2967
|
},
|
|
2740
2968
|
async ({ deviceId }) => {
|
|
2741
2969
|
try {
|
|
2742
2970
|
const target = sync.requireProDevice(deviceId);
|
|
2743
2971
|
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
2744
|
-
return
|
|
2972
|
+
return text13("Cleared js-top stats.");
|
|
2745
2973
|
} catch (e) {
|
|
2746
|
-
return
|
|
2974
|
+
return fail15(errMsg15(e));
|
|
2747
2975
|
}
|
|
2748
2976
|
}
|
|
2749
2977
|
);
|
|
2750
2978
|
}
|
|
2751
2979
|
|
|
2752
2980
|
// src/tools/packs/images.ts
|
|
2753
|
-
import { z as
|
|
2981
|
+
import { z as z16 } from "zod";
|
|
2754
2982
|
var IMAGES_TOOL_ID = "images";
|
|
2755
|
-
var
|
|
2756
|
-
var
|
|
2757
|
-
var
|
|
2983
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2984
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2985
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
2758
2986
|
function uriTail(uri) {
|
|
2759
2987
|
if (!uri) return "(no source)";
|
|
2760
2988
|
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
@@ -2842,9 +3070,9 @@ function registerImagesTools(server, sync) {
|
|
|
2842
3070
|
{
|
|
2843
3071
|
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.",
|
|
2844
3072
|
inputSchema: {
|
|
2845
|
-
deviceId:
|
|
2846
|
-
limit:
|
|
2847
|
-
status:
|
|
3073
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3074
|
+
limit: z16.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
3075
|
+
status: z16.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
2848
3076
|
}
|
|
2849
3077
|
},
|
|
2850
3078
|
async ({ deviceId, limit, status }) => {
|
|
@@ -2857,7 +3085,7 @@ function registerImagesTools(server, sync) {
|
|
|
2857
3085
|
{ limit: capped, status },
|
|
2858
3086
|
target
|
|
2859
3087
|
);
|
|
2860
|
-
if (!list) return
|
|
3088
|
+
if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
2861
3089
|
const md = formatListMarkdown(list, capped);
|
|
2862
3090
|
const slim = {
|
|
2863
3091
|
stats: list.stats,
|
|
@@ -2884,13 +3112,13 @@ function registerImagesTools(server, sync) {
|
|
|
2884
3112
|
layoutShifts: r.layoutShifts
|
|
2885
3113
|
}))
|
|
2886
3114
|
};
|
|
2887
|
-
return
|
|
3115
|
+
return text14(`${md}
|
|
2888
3116
|
|
|
2889
3117
|
\`\`\`json
|
|
2890
3118
|
${JSON.stringify(slim)}
|
|
2891
3119
|
\`\`\``);
|
|
2892
3120
|
} catch (e) {
|
|
2893
|
-
return
|
|
3121
|
+
return fail16(errMsg16(e));
|
|
2894
3122
|
}
|
|
2895
3123
|
}
|
|
2896
3124
|
);
|
|
@@ -2899,9 +3127,9 @@ ${JSON.stringify(slim)}
|
|
|
2899
3127
|
{
|
|
2900
3128
|
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.",
|
|
2901
3129
|
inputSchema: {
|
|
2902
|
-
deviceId:
|
|
2903
|
-
id:
|
|
2904
|
-
action:
|
|
3130
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3131
|
+
id: z16.number().describe("Record id from get_images."),
|
|
3132
|
+
action: z16.enum([
|
|
2905
3133
|
"hard_reload",
|
|
2906
3134
|
"retry",
|
|
2907
3135
|
"evict_disk",
|
|
@@ -2913,7 +3141,7 @@ ${JSON.stringify(slim)}
|
|
|
2913
3141
|
"swap_url",
|
|
2914
3142
|
"restore"
|
|
2915
3143
|
]).describe("What to do."),
|
|
2916
|
-
url:
|
|
3144
|
+
url: z16.string().optional().describe("Replacement URL (swap_url only).")
|
|
2917
3145
|
}
|
|
2918
3146
|
},
|
|
2919
3147
|
async ({ deviceId, id, action, url }) => {
|
|
@@ -2921,8 +3149,8 @@ ${JSON.stringify(slim)}
|
|
|
2921
3149
|
const target = sync.requireProDevice(deviceId);
|
|
2922
3150
|
if (action === "prove_savings") {
|
|
2923
3151
|
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
2924
|
-
if (!savings) return
|
|
2925
|
-
if (!savings.ok) return
|
|
3152
|
+
if (!savings) return fail16("Device returned nothing for proveSavings.");
|
|
3153
|
+
if (!savings.ok) return fail16(savings.message);
|
|
2926
3154
|
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
2927
3155
|
if (savings.originalBytes != null)
|
|
2928
3156
|
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
@@ -2934,11 +3162,11 @@ ${JSON.stringify(slim)}
|
|
|
2934
3162
|
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
2935
3163
|
if (savings.optimizedUri)
|
|
2936
3164
|
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
2937
|
-
return
|
|
3165
|
+
return text14(parts.join("\n"));
|
|
2938
3166
|
}
|
|
2939
3167
|
const entry = ACTION_MAP[action];
|
|
2940
|
-
if (!entry) return
|
|
2941
|
-
if (action === "swap_url" && !url) return
|
|
3168
|
+
if (!entry) return fail16(`Unknown action: ${action}`);
|
|
3169
|
+
if (action === "swap_url" && !url) return fail16("swap_url requires the `url` param.");
|
|
2942
3170
|
const result = await sync.callAction(
|
|
2943
3171
|
IMAGES_TOOL_ID,
|
|
2944
3172
|
entry.action,
|
|
@@ -2958,9 +3186,9 @@ ${JSON.stringify(slim)}
|
|
|
2958
3186
|
);
|
|
2959
3187
|
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
2960
3188
|
}
|
|
2961
|
-
return
|
|
3189
|
+
return text14(lines.join("\n"));
|
|
2962
3190
|
} catch (e) {
|
|
2963
|
-
return
|
|
3191
|
+
return fail16(errMsg16(e));
|
|
2964
3192
|
}
|
|
2965
3193
|
}
|
|
2966
3194
|
);
|
|
@@ -2969,12 +3197,12 @@ ${JSON.stringify(slim)}
|
|
|
2969
3197
|
{
|
|
2970
3198
|
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.",
|
|
2971
3199
|
inputSchema: {
|
|
2972
|
-
deviceId:
|
|
2973
|
-
network:
|
|
2974
|
-
blankImages:
|
|
2975
|
-
massAction:
|
|
2976
|
-
clearRegistry:
|
|
2977
|
-
clearExpoCaches:
|
|
3200
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3201
|
+
network: z16.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
3202
|
+
blankImages: z16.boolean().optional().describe("Render all images without a source."),
|
|
3203
|
+
massAction: z16.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."),
|
|
3204
|
+
clearRegistry: z16.boolean().optional().describe("Clear the captured image log."),
|
|
3205
|
+
clearExpoCaches: z16.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
2978
3206
|
}
|
|
2979
3207
|
},
|
|
2980
3208
|
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
@@ -3001,22 +3229,22 @@ ${JSON.stringify(slim)}
|
|
|
3001
3229
|
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
3002
3230
|
done.push(r?.message ?? "expo caches cleared");
|
|
3003
3231
|
}
|
|
3004
|
-
if (done.length === 0) return
|
|
3005
|
-
return
|
|
3232
|
+
if (done.length === 0) return fail16("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
3233
|
+
return text14(`Applied: ${done.join(" \xB7 ")}`);
|
|
3006
3234
|
} catch (e) {
|
|
3007
|
-
return
|
|
3235
|
+
return fail16(errMsg16(e));
|
|
3008
3236
|
}
|
|
3009
3237
|
}
|
|
3010
3238
|
);
|
|
3011
3239
|
}
|
|
3012
3240
|
|
|
3013
3241
|
// src/tools/packs/assets.ts
|
|
3014
|
-
import { z as
|
|
3242
|
+
import { z as z17 } from "zod";
|
|
3015
3243
|
var ASSETS_TOOL_ID = "assets";
|
|
3016
|
-
var
|
|
3017
|
-
var
|
|
3018
|
-
var
|
|
3019
|
-
function
|
|
3244
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3245
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3246
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3247
|
+
function kb2(bytes) {
|
|
3020
3248
|
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3021
3249
|
if (bytes < 1024) return `${bytes}B`;
|
|
3022
3250
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
@@ -3024,7 +3252,7 @@ function kb(bytes) {
|
|
|
3024
3252
|
}
|
|
3025
3253
|
function sizeLabel(r) {
|
|
3026
3254
|
if (r.sizeBytes == null) return "\u2013";
|
|
3027
|
-
return r.sizeSource === "measured" ?
|
|
3255
|
+
return r.sizeSource === "measured" ? kb2(r.sizeBytes) : `~${kb2(r.sizeBytes)}`;
|
|
3028
3256
|
}
|
|
3029
3257
|
function scalesLabel(scales) {
|
|
3030
3258
|
if (!scales.length) return "\u2013";
|
|
@@ -3047,9 +3275,9 @@ function formatListMarkdown2(list, limit) {
|
|
|
3047
3275
|
const ss = list.scanStatus;
|
|
3048
3276
|
lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
|
|
3049
3277
|
lines.push("");
|
|
3050
|
-
const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${
|
|
3278
|
+
const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb2(v.bytes)})` : ""}`);
|
|
3051
3279
|
lines.push(
|
|
3052
|
-
`${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${
|
|
3280
|
+
`${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 ")}`
|
|
3053
3281
|
);
|
|
3054
3282
|
if (ss.fontFamilies.length > 0) {
|
|
3055
3283
|
lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
|
|
@@ -3065,7 +3293,7 @@ function formatListMarkdown2(list, limit) {
|
|
|
3065
3293
|
if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
|
|
3066
3294
|
if (ins.duplicates.length) {
|
|
3067
3295
|
const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
|
|
3068
|
-
chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${
|
|
3296
|
+
chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb2(wasted)} wasted)` : ""}`);
|
|
3069
3297
|
}
|
|
3070
3298
|
if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
|
|
3071
3299
|
if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
|
|
@@ -3079,7 +3307,7 @@ function formatListMarkdown2(list, limit) {
|
|
|
3079
3307
|
}
|
|
3080
3308
|
const diff = list.diff;
|
|
3081
3309
|
if (diff) {
|
|
3082
|
-
const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${
|
|
3310
|
+
const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb2(Math.abs(diff.netBytes))} net`;
|
|
3083
3311
|
lines.push("");
|
|
3084
3312
|
lines.push(
|
|
3085
3313
|
`**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" : ""}` : "")
|
|
@@ -3106,10 +3334,10 @@ function registerAssetsTools(server, sync) {
|
|
|
3106
3334
|
{
|
|
3107
3335
|
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.",
|
|
3108
3336
|
inputSchema: {
|
|
3109
|
-
deviceId:
|
|
3110
|
-
limit:
|
|
3111
|
-
kind:
|
|
3112
|
-
unusedOnly:
|
|
3337
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3338
|
+
limit: z17.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
3339
|
+
kind: z17.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
3340
|
+
unusedOnly: z17.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3113
3341
|
}
|
|
3114
3342
|
},
|
|
3115
3343
|
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
@@ -3122,7 +3350,7 @@ function registerAssetsTools(server, sync) {
|
|
|
3122
3350
|
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3123
3351
|
target
|
|
3124
3352
|
);
|
|
3125
|
-
if (!list) return
|
|
3353
|
+
if (!list) return fail17("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3126
3354
|
if (unusedOnly) {
|
|
3127
3355
|
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3128
3356
|
}
|
|
@@ -3146,13 +3374,13 @@ function registerAssetsTools(server, sync) {
|
|
|
3146
3374
|
sizeSource: r.sizeSource
|
|
3147
3375
|
}))
|
|
3148
3376
|
};
|
|
3149
|
-
return
|
|
3377
|
+
return text15(`${md}
|
|
3150
3378
|
|
|
3151
3379
|
\`\`\`json
|
|
3152
3380
|
${JSON.stringify(slim)}
|
|
3153
3381
|
\`\`\``);
|
|
3154
3382
|
} catch (e) {
|
|
3155
|
-
return
|
|
3383
|
+
return fail17(errMsg17(e));
|
|
3156
3384
|
}
|
|
3157
3385
|
}
|
|
3158
3386
|
);
|
|
@@ -3161,9 +3389,9 @@ ${JSON.stringify(slim)}
|
|
|
3161
3389
|
{
|
|
3162
3390
|
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).",
|
|
3163
3391
|
inputSchema: {
|
|
3164
|
-
deviceId:
|
|
3165
|
-
action:
|
|
3166
|
-
id:
|
|
3392
|
+
deviceId: z17.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3393
|
+
action: z17.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
3394
|
+
id: z17.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3167
3395
|
}
|
|
3168
3396
|
},
|
|
3169
3397
|
async ({ deviceId, action, id }) => {
|
|
@@ -3173,19 +3401,19 @@ ${JSON.stringify(slim)}
|
|
|
3173
3401
|
case "rescan": {
|
|
3174
3402
|
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3175
3403
|
const ss = result?.scanStatus;
|
|
3176
|
-
if (!ss) return
|
|
3177
|
-
return
|
|
3404
|
+
if (!ss) return fail17("Device returned nothing for rescan.");
|
|
3405
|
+
return text15(
|
|
3178
3406
|
`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" : "")
|
|
3179
3407
|
);
|
|
3180
3408
|
}
|
|
3181
3409
|
case "measure_sizes": {
|
|
3182
3410
|
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3183
|
-
return
|
|
3411
|
+
return text15(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3184
3412
|
}
|
|
3185
3413
|
case "detail": {
|
|
3186
|
-
if (id == null) return
|
|
3414
|
+
if (id == null) return fail17("detail requires the `id` param (from get_assets).");
|
|
3187
3415
|
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3188
|
-
if (!detail) return
|
|
3416
|
+
if (!detail) return fail17(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3189
3417
|
const lines = [
|
|
3190
3418
|
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3191
3419
|
"",
|
|
@@ -3194,50 +3422,50 @@ ${JSON.stringify(slim)}
|
|
|
3194
3422
|
if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
|
|
3195
3423
|
if (detail.sizesByScale) {
|
|
3196
3424
|
for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
|
|
3197
|
-
lines.push(`- Size @${scale}x: ${
|
|
3425
|
+
lines.push(`- Size @${scale}x: ${kb2(bytes)}`);
|
|
3198
3426
|
}
|
|
3199
3427
|
}
|
|
3200
3428
|
if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
|
|
3201
|
-
if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${
|
|
3429
|
+
if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb2(detail.estDecodedBytes)} (RGBA)`);
|
|
3202
3430
|
if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
|
|
3203
3431
|
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3204
3432
|
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3205
3433
|
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3206
|
-
return
|
|
3434
|
+
return text15(lines.join("\n"));
|
|
3207
3435
|
}
|
|
3208
3436
|
case "save_baseline": {
|
|
3209
3437
|
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3210
|
-
return
|
|
3438
|
+
return text15(
|
|
3211
3439
|
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3212
3440
|
);
|
|
3213
3441
|
}
|
|
3214
3442
|
case "clear_baseline": {
|
|
3215
3443
|
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3216
|
-
return
|
|
3444
|
+
return text15("Baseline cleared.");
|
|
3217
3445
|
}
|
|
3218
3446
|
case "clear_records": {
|
|
3219
3447
|
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3220
|
-
return
|
|
3448
|
+
return text15(result?.message ?? "Inventory cleared.");
|
|
3221
3449
|
}
|
|
3222
3450
|
}
|
|
3223
3451
|
} catch (e) {
|
|
3224
|
-
return
|
|
3452
|
+
return fail17(errMsg17(e));
|
|
3225
3453
|
}
|
|
3226
3454
|
}
|
|
3227
3455
|
);
|
|
3228
3456
|
}
|
|
3229
3457
|
|
|
3230
3458
|
// src/tools/packs/reload.ts
|
|
3231
|
-
import { z as
|
|
3459
|
+
import { z as z18 } from "zod";
|
|
3232
3460
|
var APP_TOOL_ID = "app";
|
|
3233
3461
|
var RELOAD_ACTION = "reloadApp";
|
|
3234
3462
|
var DEFAULT_WAIT_MS = 3e4;
|
|
3235
3463
|
var TEARDOWN_GRACE_MS = 1200;
|
|
3236
3464
|
var PROBE_TIMEOUT_MS = 1500;
|
|
3237
3465
|
var PROBE_INTERVAL_MS = 400;
|
|
3238
|
-
var
|
|
3239
|
-
var
|
|
3240
|
-
var
|
|
3466
|
+
var text16 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3467
|
+
var fail18 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3468
|
+
var errMsg18 = (e) => e instanceof Error ? e.message : String(e);
|
|
3241
3469
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3242
3470
|
async function ping(sync, deviceId, timeoutMs) {
|
|
3243
3471
|
try {
|
|
@@ -3272,12 +3500,12 @@ function registerReloadTools(server, sync) {
|
|
|
3272
3500
|
{
|
|
3273
3501
|
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.",
|
|
3274
3502
|
inputSchema: {
|
|
3275
|
-
strategy:
|
|
3503
|
+
strategy: z18.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
3276
3504
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
3277
3505
|
),
|
|
3278
|
-
wait:
|
|
3279
|
-
timeoutMs:
|
|
3280
|
-
deviceId:
|
|
3506
|
+
wait: z18.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
3507
|
+
timeoutMs: z18.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
3508
|
+
deviceId: z18.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
3281
3509
|
}
|
|
3282
3510
|
},
|
|
3283
3511
|
async (args) => {
|
|
@@ -3293,19 +3521,19 @@ function registerReloadTools(server, sync) {
|
|
|
3293
3521
|
);
|
|
3294
3522
|
const via = ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
3295
3523
|
if (args.wait === false) {
|
|
3296
|
-
return
|
|
3524
|
+
return text16(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
3297
3525
|
}
|
|
3298
3526
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
3299
3527
|
if (elapsed == null) {
|
|
3300
|
-
return
|
|
3528
|
+
return fail18(
|
|
3301
3529
|
`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.`
|
|
3302
3530
|
);
|
|
3303
3531
|
}
|
|
3304
|
-
return
|
|
3532
|
+
return text16(
|
|
3305
3533
|
`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.`
|
|
3306
3534
|
);
|
|
3307
3535
|
} catch (e) {
|
|
3308
|
-
return
|
|
3536
|
+
return fail18(errMsg18(e));
|
|
3309
3537
|
}
|
|
3310
3538
|
}
|
|
3311
3539
|
);
|
|
@@ -3360,6 +3588,14 @@ measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 De
|
|
|
3360
3588
|
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
3361
3589
|
re-read anything you cached.
|
|
3362
3590
|
|
|
3591
|
+
Testing a multi-step flow repeatedly (cart \u2192 checkout, login states, a bug that needs specific
|
|
3592
|
+
state)? \`time_machine_action\` captures the app's COMPLETE client state (storage, redux, zustand,
|
|
3593
|
+
jotai, react-query cache) as a named restore point and restores it in one call \u2014 capture once,
|
|
3594
|
+
drive the flow with \`tap_element\`, restore, repeat. \`get_time_machine\` lists restore points and
|
|
3595
|
+
which sources are restorable. Restore mode 'live' swaps state in place; 'reload' restores
|
|
3596
|
+
persisted storage then reloads the bundle (bulletproof; baselines = wipe to fresh install).
|
|
3597
|
+
Check the per-source applied/skipped results it returns \u2014 a partial restore is reported, not hidden.
|
|
3598
|
+
|
|
3363
3599
|
For everything else:
|
|
3364
3600
|
- \`list_tool_actions\` to see what a tool can do,
|
|
3365
3601
|
- \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
|
|
@@ -3387,6 +3623,7 @@ function createMcpServer(sync) {
|
|
|
3387
3623
|
registerStorageTools(server, sync);
|
|
3388
3624
|
registerRoutesTools(server, sync);
|
|
3389
3625
|
registerConsoleTools(server, sync);
|
|
3626
|
+
registerTimeMachineTools(server, sync);
|
|
3390
3627
|
registerScreenshotTools(server, sync);
|
|
3391
3628
|
registerInteractTools(server, sync);
|
|
3392
3629
|
registerJsThreadTools(server, sync);
|
|
@@ -3396,7 +3633,7 @@ function createMcpServer(sync) {
|
|
|
3396
3633
|
return server;
|
|
3397
3634
|
}
|
|
3398
3635
|
var deviceIdField = {
|
|
3399
|
-
deviceId:
|
|
3636
|
+
deviceId: z19.string().optional().describe(
|
|
3400
3637
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
3401
3638
|
)
|
|
3402
3639
|
};
|