@buoy-gg/mcp 7.0.17 → 7.0.19
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-7NCVMOMV.mjs → chunk-TZ2C4X6V.mjs} +155 -38
- package/dist/cli.js +155 -38
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +40 -4
- package/dist/index.d.ts +40 -4
- package/dist/index.js +155 -38
- 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.19" ? "7.0.19" : "0.0.0-dev";
|
|
305
348
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
306
349
|
|
|
307
350
|
// src/update/checkForUpdate.ts
|
|
@@ -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));
|
|
@@ -2530,12 +2619,13 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2530
2619
|
server.registerTool(
|
|
2531
2620
|
"time_machine_action",
|
|
2532
2621
|
{
|
|
2533
|
-
description: "Manage Time Machine state snapshots: capture the app's current state as a named restore point, 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.",
|
|
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.",
|
|
2534
2623
|
inputSchema: {
|
|
2535
|
-
action: z13.enum(["capture", "capture_baseline", "restore", "delete", "rename"]).describe("What to do."),
|
|
2624
|
+
action: z13.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
|
|
2536
2625
|
id: z13.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
2537
2626
|
name: z13.string().optional().describe("Snapshot name (for capture and rename)."),
|
|
2538
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").'),
|
|
2539
2629
|
sourceIds: z13.array(z13.string()).optional().describe("Capture/restore only these source ids (default all)."),
|
|
2540
2630
|
deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2541
2631
|
}
|
|
@@ -2556,9 +2646,36 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2556
2646
|
const result = await call("captureBaseline", {});
|
|
2557
2647
|
return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
|
|
2558
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
|
+
}
|
|
2559
2672
|
case "restore": {
|
|
2560
2673
|
if (!args.id) return fail13("restore requires `id` (from get_time_machine).");
|
|
2561
|
-
const outcome = await call("restore", {
|
|
2674
|
+
const outcome = await call("restore", {
|
|
2675
|
+
id: args.id,
|
|
2676
|
+
mode: args.mode ?? "live",
|
|
2677
|
+
excludeKeys: args.excludeKeys
|
|
2678
|
+
});
|
|
2562
2679
|
return text11(formatOutcome(outcome));
|
|
2563
2680
|
}
|
|
2564
2681
|
case "delete": {
|
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
|
-
/**
|
|
56
|
-
|
|
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.
|
|
226
|
-
this.
|
|
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
|
|
240
|
-
*
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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) => {
|
|
@@ -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.
|
|
387
|
+
var VERSION = "7.0.19" ? "7.0.19" : "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 ?
|
|
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));
|
|
@@ -2388,12 +2477,13 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2388
2477
|
server.registerTool(
|
|
2389
2478
|
"time_machine_action",
|
|
2390
2479
|
{
|
|
2391
|
-
description: "Manage Time Machine state snapshots: capture the app's current state as a named restore point, 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.",
|
|
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.",
|
|
2392
2481
|
inputSchema: {
|
|
2393
|
-
action: import_zod12.z.enum(["capture", "capture_baseline", "restore", "delete", "rename"]).describe("What to do."),
|
|
2482
|
+
action: import_zod12.z.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
|
|
2394
2483
|
id: import_zod12.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
2395
2484
|
name: import_zod12.z.string().optional().describe("Snapshot name (for capture and rename)."),
|
|
2396
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").'),
|
|
2397
2487
|
sourceIds: import_zod12.z.array(import_zod12.z.string()).optional().describe("Capture/restore only these source ids (default all)."),
|
|
2398
2488
|
deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2399
2489
|
}
|
|
@@ -2414,9 +2504,36 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2414
2504
|
const result = await call("captureBaseline", {});
|
|
2415
2505
|
return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
|
|
2416
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
|
+
}
|
|
2417
2530
|
case "restore": {
|
|
2418
2531
|
if (!args.id) return fail12("restore requires `id` (from get_time_machine).");
|
|
2419
|
-
const outcome = await call("restore", {
|
|
2532
|
+
const outcome = await call("restore", {
|
|
2533
|
+
id: args.id,
|
|
2534
|
+
mode: args.mode ?? "live",
|
|
2535
|
+
excludeKeys: args.excludeKeys
|
|
2536
|
+
});
|
|
2420
2537
|
return text11(formatOutcome(outcome));
|
|
2421
2538
|
}
|
|
2422
2539
|
case "delete": {
|
package/dist/cli.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -47,7 +47,11 @@ declare class SyncClient {
|
|
|
47
47
|
private devices;
|
|
48
48
|
/** Latest snapshot payload per `${deviceId}::${toolId}`. */
|
|
49
49
|
private snapshots;
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Capture-mode watches this client holds, keyed by snapKey. Stored with
|
|
52
|
+
* their addressing because they have to be RE-SENT on every reconnect: the
|
|
53
|
+
* broker keys watch state by socket id, and a reconnect gets a new one.
|
|
54
|
+
*/
|
|
51
55
|
private watching;
|
|
52
56
|
/** In-flight actions awaiting a result, keyed by requestId. */
|
|
53
57
|
private pendingActions;
|
|
@@ -87,15 +91,47 @@ declare class SyncClient {
|
|
|
87
91
|
* is blocked.
|
|
88
92
|
*/
|
|
89
93
|
requireProDevice(deviceId?: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Keep the device RECORDING this tool, without asking it to stream.
|
|
96
|
+
*
|
|
97
|
+
* A dashboard watches for as long as its panel is open; this client is
|
|
98
|
+
* request/response, so it used to watch and unwatch around every single
|
|
99
|
+
* read. That works for a tool whose state just IS (the query cache, the env
|
|
100
|
+
* table) and is useless for one that records a timeline: the unwatch drops
|
|
101
|
+
* the device's subscriber count to zero, which stops capture, so nothing is
|
|
102
|
+
* ever recorded between two reads. The list comes back empty and reads as
|
|
103
|
+
* "the app made no requests" — a wrong conclusion that is very expensive to
|
|
104
|
+
* chase, because everything about the app looks fine.
|
|
105
|
+
*
|
|
106
|
+
* Capture-mode watches are held for the life of the connection and released
|
|
107
|
+
* on disconnect (the broker also drops them for us). They cost the device's
|
|
108
|
+
* ring buffer, not the socket: no snapshot is pushed, and reads still go
|
|
109
|
+
* through `request-devtool-state`.
|
|
110
|
+
*/
|
|
90
111
|
private ensureWatching;
|
|
112
|
+
private sendWatch;
|
|
113
|
+
/**
|
|
114
|
+
* Re-arm every held watch after a reconnect.
|
|
115
|
+
*
|
|
116
|
+
* The broker tracks watches per SOCKET, so a reconnect (broker restart, a
|
|
117
|
+
* dropped wifi link) silently discards all of ours while `watching` still
|
|
118
|
+
* says we hold them — `ensureWatching` would then short-circuit forever and
|
|
119
|
+
* the device would never record again. Silent and permanent: the exact
|
|
120
|
+
* failure this whole capture-mode change exists to remove.
|
|
121
|
+
*/
|
|
122
|
+
private rearmWatches;
|
|
91
123
|
/** Stop streaming a tool (releases the device-side backpressure). */
|
|
92
124
|
unwatch(toolId: string, targetDeviceId: string): void;
|
|
93
125
|
/** Last snapshot received for a tool/device, if any. */
|
|
94
126
|
cachedSnapshot(toolId: string, deviceId: string): unknown;
|
|
95
127
|
/**
|
|
96
|
-
* Get a tool's current state
|
|
97
|
-
*
|
|
98
|
-
* to the last cached snapshot if the request times out.
|
|
128
|
+
* Get a tool's current state: arm capture (once, for the connection), ask
|
|
129
|
+
* for state explicitly, and resolve with the snapshot that comes back.
|
|
130
|
+
* Falls back to the last cached snapshot if the request times out.
|
|
131
|
+
*
|
|
132
|
+
* The capture watch is deliberately NOT released afterwards — see
|
|
133
|
+
* `ensureWatching`. It costs the device nothing on the socket, and dropping
|
|
134
|
+
* it is what made every timeline tool look permanently empty from here.
|
|
99
135
|
*/
|
|
100
136
|
getSnapshot(toolId: string, deviceId?: string, opts?: {
|
|
101
137
|
timeoutMs?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -47,7 +47,11 @@ declare class SyncClient {
|
|
|
47
47
|
private devices;
|
|
48
48
|
/** Latest snapshot payload per `${deviceId}::${toolId}`. */
|
|
49
49
|
private snapshots;
|
|
50
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Capture-mode watches this client holds, keyed by snapKey. Stored with
|
|
52
|
+
* their addressing because they have to be RE-SENT on every reconnect: the
|
|
53
|
+
* broker keys watch state by socket id, and a reconnect gets a new one.
|
|
54
|
+
*/
|
|
51
55
|
private watching;
|
|
52
56
|
/** In-flight actions awaiting a result, keyed by requestId. */
|
|
53
57
|
private pendingActions;
|
|
@@ -87,15 +91,47 @@ declare class SyncClient {
|
|
|
87
91
|
* is blocked.
|
|
88
92
|
*/
|
|
89
93
|
requireProDevice(deviceId?: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Keep the device RECORDING this tool, without asking it to stream.
|
|
96
|
+
*
|
|
97
|
+
* A dashboard watches for as long as its panel is open; this client is
|
|
98
|
+
* request/response, so it used to watch and unwatch around every single
|
|
99
|
+
* read. That works for a tool whose state just IS (the query cache, the env
|
|
100
|
+
* table) and is useless for one that records a timeline: the unwatch drops
|
|
101
|
+
* the device's subscriber count to zero, which stops capture, so nothing is
|
|
102
|
+
* ever recorded between two reads. The list comes back empty and reads as
|
|
103
|
+
* "the app made no requests" — a wrong conclusion that is very expensive to
|
|
104
|
+
* chase, because everything about the app looks fine.
|
|
105
|
+
*
|
|
106
|
+
* Capture-mode watches are held for the life of the connection and released
|
|
107
|
+
* on disconnect (the broker also drops them for us). They cost the device's
|
|
108
|
+
* ring buffer, not the socket: no snapshot is pushed, and reads still go
|
|
109
|
+
* through `request-devtool-state`.
|
|
110
|
+
*/
|
|
90
111
|
private ensureWatching;
|
|
112
|
+
private sendWatch;
|
|
113
|
+
/**
|
|
114
|
+
* Re-arm every held watch after a reconnect.
|
|
115
|
+
*
|
|
116
|
+
* The broker tracks watches per SOCKET, so a reconnect (broker restart, a
|
|
117
|
+
* dropped wifi link) silently discards all of ours while `watching` still
|
|
118
|
+
* says we hold them — `ensureWatching` would then short-circuit forever and
|
|
119
|
+
* the device would never record again. Silent and permanent: the exact
|
|
120
|
+
* failure this whole capture-mode change exists to remove.
|
|
121
|
+
*/
|
|
122
|
+
private rearmWatches;
|
|
91
123
|
/** Stop streaming a tool (releases the device-side backpressure). */
|
|
92
124
|
unwatch(toolId: string, targetDeviceId: string): void;
|
|
93
125
|
/** Last snapshot received for a tool/device, if any. */
|
|
94
126
|
cachedSnapshot(toolId: string, deviceId: string): unknown;
|
|
95
127
|
/**
|
|
96
|
-
* Get a tool's current state
|
|
97
|
-
*
|
|
98
|
-
* to the last cached snapshot if the request times out.
|
|
128
|
+
* Get a tool's current state: arm capture (once, for the connection), ask
|
|
129
|
+
* for state explicitly, and resolve with the snapshot that comes back.
|
|
130
|
+
* Falls back to the last cached snapshot if the request times out.
|
|
131
|
+
*
|
|
132
|
+
* The capture watch is deliberately NOT released afterwards — see
|
|
133
|
+
* `ensureWatching`. It costs the device nothing on the socket, and dropping
|
|
134
|
+
* it is what made every timeline tool look permanently empty from here.
|
|
99
135
|
*/
|
|
100
136
|
getSnapshot(toolId: string, deviceId?: string, opts?: {
|
|
101
137
|
timeoutMs?: number;
|
package/dist/index.js
CHANGED
|
@@ -87,8 +87,12 @@ var SyncClient = class {
|
|
|
87
87
|
this.devices = [];
|
|
88
88
|
/** Latest snapshot payload per `${deviceId}::${toolId}`. */
|
|
89
89
|
this.snapshots = /* @__PURE__ */ new Map();
|
|
90
|
-
/**
|
|
91
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Capture-mode watches this client holds, keyed by snapKey. Stored with
|
|
92
|
+
* their addressing because they have to be RE-SENT on every reconnect: the
|
|
93
|
+
* broker keys watch state by socket id, and a reconnect gets a new one.
|
|
94
|
+
*/
|
|
95
|
+
this.watching = /* @__PURE__ */ new Map();
|
|
92
96
|
/** In-flight actions awaiting a result, keyed by requestId. */
|
|
93
97
|
this.pendingActions = /* @__PURE__ */ new Map();
|
|
94
98
|
/** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
|
|
@@ -144,6 +148,7 @@ var SyncClient = class {
|
|
|
144
148
|
resolve();
|
|
145
149
|
};
|
|
146
150
|
socket.on("connect", onConnect);
|
|
151
|
+
socket.on("connect", () => this.rearmWatches());
|
|
147
152
|
socket.once("connect_error", (err) => {
|
|
148
153
|
if (settled) return;
|
|
149
154
|
settled = true;
|
|
@@ -254,11 +259,50 @@ var SyncClient = class {
|
|
|
254
259
|
// ----------------------------------------------------------------
|
|
255
260
|
// Watch + snapshots
|
|
256
261
|
// ----------------------------------------------------------------
|
|
262
|
+
/**
|
|
263
|
+
* Keep the device RECORDING this tool, without asking it to stream.
|
|
264
|
+
*
|
|
265
|
+
* A dashboard watches for as long as its panel is open; this client is
|
|
266
|
+
* request/response, so it used to watch and unwatch around every single
|
|
267
|
+
* read. That works for a tool whose state just IS (the query cache, the env
|
|
268
|
+
* table) and is useless for one that records a timeline: the unwatch drops
|
|
269
|
+
* the device's subscriber count to zero, which stops capture, so nothing is
|
|
270
|
+
* ever recorded between two reads. The list comes back empty and reads as
|
|
271
|
+
* "the app made no requests" — a wrong conclusion that is very expensive to
|
|
272
|
+
* chase, because everything about the app looks fine.
|
|
273
|
+
*
|
|
274
|
+
* Capture-mode watches are held for the life of the connection and released
|
|
275
|
+
* on disconnect (the broker also drops them for us). They cost the device's
|
|
276
|
+
* ring buffer, not the socket: no snapshot is pushed, and reads still go
|
|
277
|
+
* through `request-devtool-state`.
|
|
278
|
+
*/
|
|
257
279
|
ensureWatching(toolId, targetDeviceId) {
|
|
258
280
|
const key = snapKey(targetDeviceId, toolId);
|
|
259
281
|
if (this.watching.has(key)) return;
|
|
260
|
-
this.watching.
|
|
261
|
-
this.
|
|
282
|
+
this.watching.set(key, { toolId, targetDeviceId });
|
|
283
|
+
this.sendWatch(toolId, targetDeviceId);
|
|
284
|
+
}
|
|
285
|
+
sendWatch(toolId, targetDeviceId) {
|
|
286
|
+
this.socket?.emit("devtool-watch", {
|
|
287
|
+
toolId,
|
|
288
|
+
targetDeviceId,
|
|
289
|
+
watching: true,
|
|
290
|
+
stream: false
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Re-arm every held watch after a reconnect.
|
|
295
|
+
*
|
|
296
|
+
* The broker tracks watches per SOCKET, so a reconnect (broker restart, a
|
|
297
|
+
* dropped wifi link) silently discards all of ours while `watching` still
|
|
298
|
+
* says we hold them — `ensureWatching` would then short-circuit forever and
|
|
299
|
+
* the device would never record again. Silent and permanent: the exact
|
|
300
|
+
* failure this whole capture-mode change exists to remove.
|
|
301
|
+
*/
|
|
302
|
+
rearmWatches() {
|
|
303
|
+
for (const { toolId, targetDeviceId } of this.watching.values()) {
|
|
304
|
+
this.sendWatch(toolId, targetDeviceId);
|
|
305
|
+
}
|
|
262
306
|
}
|
|
263
307
|
/** Stop streaming a tool (releases the device-side backpressure). */
|
|
264
308
|
unwatch(toolId, targetDeviceId) {
|
|
@@ -271,26 +315,25 @@ var SyncClient = class {
|
|
|
271
315
|
return this.snapshots.get(snapKey(deviceId, toolId));
|
|
272
316
|
}
|
|
273
317
|
/**
|
|
274
|
-
* Get a tool's current state
|
|
275
|
-
*
|
|
276
|
-
* to the last cached snapshot if the request times out.
|
|
318
|
+
* Get a tool's current state: arm capture (once, for the connection), ask
|
|
319
|
+
* for state explicitly, and resolve with the snapshot that comes back.
|
|
320
|
+
* Falls back to the last cached snapshot if the request times out.
|
|
321
|
+
*
|
|
322
|
+
* The capture watch is deliberately NOT released afterwards — see
|
|
323
|
+
* `ensureWatching`. It costs the device nothing on the socket, and dropping
|
|
324
|
+
* it is what made every timeline tool look permanently empty from here.
|
|
277
325
|
*/
|
|
278
326
|
async getSnapshot(toolId, deviceId, opts = {}) {
|
|
279
327
|
const target = this.resolveDevice(deviceId);
|
|
280
328
|
const key = snapKey(target, toolId);
|
|
281
|
-
const wasWatching = this.watching.has(key);
|
|
282
329
|
this.ensureWatching(toolId, target);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
throw new Error(`Timed out waiting for "${toolId}" state from ${target} (is the tool active?)`);
|
|
291
|
-
} finally {
|
|
292
|
-
if (!wasWatching) this.unwatch(toolId, target);
|
|
293
|
-
}
|
|
330
|
+
const next = this.waitForSnapshot(key, opts.timeoutMs ?? DEFAULT_SNAPSHOT_TIMEOUT_MS);
|
|
331
|
+
this.socket?.emit("request-devtool-state", { toolId, targetDeviceId: target });
|
|
332
|
+
const payload = await next;
|
|
333
|
+
if (payload !== void 0) return { deviceId: target, payload };
|
|
334
|
+
const cached = this.snapshots.get(key);
|
|
335
|
+
if (cached !== void 0) return { deviceId: target, payload: cached };
|
|
336
|
+
throw new Error(`Timed out waiting for "${toolId}" state from ${target} (is the tool active?)`);
|
|
294
337
|
}
|
|
295
338
|
waitForSnapshot(key, timeoutMs) {
|
|
296
339
|
return new Promise((resolve) => {
|
|
@@ -376,7 +419,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
376
419
|
var import_node_path = __toESM(require("path"));
|
|
377
420
|
|
|
378
421
|
// src/version.ts
|
|
379
|
-
var VERSION = "7.0.
|
|
422
|
+
var VERSION = "7.0.19" ? "7.0.19" : "0.0.0-dev";
|
|
380
423
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
381
424
|
|
|
382
425
|
// src/update/checkForUpdate.ts
|
|
@@ -1377,6 +1420,60 @@ function truncate(v, n = 600) {
|
|
|
1377
1420
|
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
1378
1421
|
return s.length > n ? `${s.slice(0, n)}\u2026(+${s.length - n} chars)` : s;
|
|
1379
1422
|
}
|
|
1423
|
+
var MAX_BODY_FETCHES = 8;
|
|
1424
|
+
async function resolveBodies(sync, rows, target, max = MAX_BODY_FETCHES) {
|
|
1425
|
+
const needed = rows.filter(
|
|
1426
|
+
(e) => e.id && (e.requestBodyOmitted === true && e.requestData === void 0 || e.responseBodyOmitted === true && e.responseData === void 0)
|
|
1427
|
+
);
|
|
1428
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
1429
|
+
if (needed.length === 0) return { bodies, skipped: 0 };
|
|
1430
|
+
const take = needed.slice(0, max);
|
|
1431
|
+
const settled = await Promise.allSettled(
|
|
1432
|
+
take.map(
|
|
1433
|
+
(e) => sync.callAction(NETWORK_TOOL_ID, "getEventBody", { id: e.id }, target)
|
|
1434
|
+
)
|
|
1435
|
+
);
|
|
1436
|
+
settled.forEach((r, i) => {
|
|
1437
|
+
const id = take[i].id;
|
|
1438
|
+
const body = r.status === "fulfilled" ? r.value : null;
|
|
1439
|
+
bodies.set(id, body ? { ...body } : { unresolved: true });
|
|
1440
|
+
});
|
|
1441
|
+
return { bodies, skipped: needed.length - take.length };
|
|
1442
|
+
}
|
|
1443
|
+
async function captureDiagnosis(sync, target) {
|
|
1444
|
+
let status = null;
|
|
1445
|
+
try {
|
|
1446
|
+
status = await sync.callAction(
|
|
1447
|
+
NETWORK_TOOL_ID,
|
|
1448
|
+
"getCaptureStatus",
|
|
1449
|
+
{},
|
|
1450
|
+
target
|
|
1451
|
+
);
|
|
1452
|
+
} catch {
|
|
1453
|
+
return "The network tool has captured nothing yet.";
|
|
1454
|
+
}
|
|
1455
|
+
if (!status || status.capturing !== false) {
|
|
1456
|
+
return "The device IS capturing, so nothing has been requested since it started.";
|
|
1457
|
+
}
|
|
1458
|
+
return "The device is NOT capturing. Reading from here arms capture, so the very next read should see traffic \u2014 trigger it and call again. This is not an app or interception problem. (If it stays off, the device is on a Buoy older than the capture-mode watch: open the Network panel on the device or in Buoy Desktop to record.)";
|
|
1459
|
+
}
|
|
1460
|
+
function skippedNote(skipped) {
|
|
1461
|
+
return skipped > 0 ? `
|
|
1462
|
+
|
|
1463
|
+
_${skipped} more withheld ${skipped === 1 ? "body was" : "bodies were"} not fetched (cap ${MAX_BODY_FETCHES} per listing, they are large). Get one in full with its id._` : "";
|
|
1464
|
+
}
|
|
1465
|
+
function bodyLines(e, fetched) {
|
|
1466
|
+
const got = e.id ? fetched.get(e.id) : void 0;
|
|
1467
|
+
const label = (inline, resolved, omitted) => {
|
|
1468
|
+
const value = inline ?? resolved;
|
|
1469
|
+
if (value !== void 0 && value !== null) return truncate(value);
|
|
1470
|
+
if (!omitted) return "(none)";
|
|
1471
|
+
return got?.unresolved ? "(withheld \u2014 the device did not answer getEventBody)" : "(none)";
|
|
1472
|
+
};
|
|
1473
|
+
return `
|
|
1474
|
+
\u2514\u2500 request: ${label(e.requestData, got?.requestData, e.requestBodyOmitted)}
|
|
1475
|
+
\u2514\u2500 response: ${label(e.responseData, got?.responseData, e.responseBodyOmitted)}`;
|
|
1476
|
+
}
|
|
1380
1477
|
function formatRow(index, e, markers) {
|
|
1381
1478
|
const where = e.path || e.url;
|
|
1382
1479
|
const bits = [
|
|
@@ -1430,6 +1527,7 @@ function registerNetworkTools(server, sync) {
|
|
|
1430
1527
|
The developer pins/saves a request from the Network tool (long-press a row, or the pin/bookmark buttons in the request detail header), or you can with network_action.`
|
|
1431
1528
|
);
|
|
1432
1529
|
}
|
|
1530
|
+
const kept = args.includeBodies ? await resolveBodies(sync, rows2.map((r) => r.event), target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
|
|
1433
1531
|
const body2 = rows2.map((r, i) => {
|
|
1434
1532
|
const marks = [r.pinned ? "\u{1F4CC} pinned" : "", r.saved ? "\u{1F516} saved" : ""].filter(Boolean).join(" ");
|
|
1435
1533
|
const stale = r.event.id?.startsWith("saved:") ? " \xB7 from an earlier app run" : "";
|
|
@@ -1437,12 +1535,7 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
|
|
|
1437
1535
|
let out = formatRow(i + 1, r.event, marks);
|
|
1438
1536
|
out += `
|
|
1439
1537
|
\u2514\u2500 key \`${r.key}\`${stale}${trunc}`;
|
|
1440
|
-
if (args.includeBodies)
|
|
1441
|
-
out += `
|
|
1442
|
-
\u2514\u2500 request: ${truncate(r.event.requestData)}`;
|
|
1443
|
-
out += `
|
|
1444
|
-
\u2514\u2500 response: ${truncate(r.event.responseData)}`;
|
|
1445
|
-
}
|
|
1538
|
+
if (args.includeBodies) out += bodyLines(r.event, kept.bodies);
|
|
1446
1539
|
return out;
|
|
1447
1540
|
}).join("\n");
|
|
1448
1541
|
return text3(
|
|
@@ -1450,23 +1543,19 @@ The developer pins/saves a request from the Network tool (long-press a row, or t
|
|
|
1450
1543
|
|
|
1451
1544
|
${body2}
|
|
1452
1545
|
|
|
1453
|
-
_These are snapshots: they survive Clear, the 500-request cap and app restarts. Remove one with network_action({action:"remove", key}). Ids starting \`saved:\` are from an earlier run of the app._`
|
|
1546
|
+
_These are snapshots: they survive Clear, the 500-request cap and app restarts. Remove one with network_action({action:"remove", key}). Ids starting \`saved:\` are from an earlier run of the app._${skippedNote(kept.skipped)}`
|
|
1454
1547
|
);
|
|
1455
1548
|
}
|
|
1456
1549
|
const rows = events.filter((e) => matchesStatus(e, args.status)).filter((e) => !pattern || (e.url ?? "").toLowerCase().includes(pattern)).slice(0, limit);
|
|
1457
1550
|
if (rows.length === 0) {
|
|
1458
|
-
const why = events.length === 0 ?
|
|
1551
|
+
const why = events.length === 0 ? await captureDiagnosis(sync, target) : "No request matched the filter.";
|
|
1459
1552
|
return text3(`No requests on ${target}. ${why}`);
|
|
1460
1553
|
}
|
|
1554
|
+
const live = args.includeBodies ? await resolveBodies(sync, rows, target) : { bodies: /* @__PURE__ */ new Map(), skipped: 0 };
|
|
1461
1555
|
const body = rows.map((e, i) => {
|
|
1462
1556
|
const marks = [pinnedIds.has(e.id) ? "\u{1F4CC}" : "", savedIds.has(e.id) ? "\u{1F516}" : ""].filter(Boolean).join("");
|
|
1463
1557
|
let out = formatRow(i + 1, e, marks);
|
|
1464
|
-
if (args.includeBodies)
|
|
1465
|
-
out += `
|
|
1466
|
-
\u2514\u2500 request: ${truncate(e.requestData)}`;
|
|
1467
|
-
out += `
|
|
1468
|
-
\u2514\u2500 response: ${truncate(e.responseData)}`;
|
|
1469
|
-
}
|
|
1558
|
+
if (args.includeBodies) out += bodyLines(e, live.bodies);
|
|
1470
1559
|
return out;
|
|
1471
1560
|
}).join("\n");
|
|
1472
1561
|
const keptNote = saved.length > 0 ? ` \xB7 ${saved.length} kept (flagged:'any' to list)` : "";
|
|
@@ -1475,7 +1564,7 @@ _These are snapshots: they survive Clear, the 500-request cap and app restarts.
|
|
|
1475
1564
|
|
|
1476
1565
|
${body}
|
|
1477
1566
|
|
|
1478
|
-
_\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._`
|
|
1567
|
+
_\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_action({action:"pin", id:"${rows[0].id}"})._${skippedNote(live.skipped)}`
|
|
1479
1568
|
);
|
|
1480
1569
|
} catch (e) {
|
|
1481
1570
|
return fail4(errMsg4(e));
|
|
@@ -2423,12 +2512,13 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2423
2512
|
server.registerTool(
|
|
2424
2513
|
"time_machine_action",
|
|
2425
2514
|
{
|
|
2426
|
-
description: "Manage Time Machine state snapshots: capture the app's current state as a named restore point, 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.",
|
|
2515
|
+
description: "Manage Time Machine state snapshots: capture the app's current state as a named restore point, preview what restoring one would change (per-item added/removed/changed/won't-apply verdicts \u2014 check the blast radius BEFORE restoring), restore one (mode 'live' replaces state in place; 'reload' restores persisted storage then reloads the JS bundle \u2014 baselines always reload), capture_baseline (an empty wipe-to-fresh-install restore point), delete, or rename. The restore result reports per-source applied/skipped counts \u2014 check them rather than assuming success.",
|
|
2427
2516
|
inputSchema: {
|
|
2428
|
-
action: import_zod12.z.enum(["capture", "capture_baseline", "restore", "delete", "rename"]).describe("What to do."),
|
|
2517
|
+
action: import_zod12.z.enum(["capture", "capture_baseline", "preview", "restore", "delete", "rename"]).describe("What to do."),
|
|
2429
2518
|
id: import_zod12.z.string().optional().describe("Snapshot id (required for restore/delete/rename; from get_time_machine)."),
|
|
2430
2519
|
name: import_zod12.z.string().optional().describe("Snapshot name (for capture and rename)."),
|
|
2431
2520
|
mode: import_zod12.z.enum(["live", "reload"]).optional().describe("Restore mode. Default 'live'."),
|
|
2521
|
+
excludeKeys: import_zod12.z.array(import_zod12.z.string()).optional().describe('Restore only: item keys to LEAVE UNTOUCHED, as "sourceId::itemKey" (itemKey = the key shown by the preview action, e.g. "query::[\\"pokemon\\"]" or "storage::async:@cart").'),
|
|
2432
2522
|
sourceIds: import_zod12.z.array(import_zod12.z.string()).optional().describe("Capture/restore only these source ids (default all)."),
|
|
2433
2523
|
deviceId: import_zod12.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2434
2524
|
}
|
|
@@ -2449,9 +2539,36 @@ ${formatOutcome(p.lastOutcome)}`);
|
|
|
2449
2539
|
const result = await call("captureBaseline", {});
|
|
2450
2540
|
return text11(`Captured baseline \`${result.snapshot.id}\` \u2014 restoring it wipes app storage and reloads.`);
|
|
2451
2541
|
}
|
|
2542
|
+
case "preview": {
|
|
2543
|
+
if (!args.id) return fail12("preview requires `id` (from get_time_machine).");
|
|
2544
|
+
const result = await call("preview", { id: args.id });
|
|
2545
|
+
const lines = [];
|
|
2546
|
+
let totalApplies = 0;
|
|
2547
|
+
for (const src of result.sources) {
|
|
2548
|
+
const counts = {};
|
|
2549
|
+
for (const item of src.items) {
|
|
2550
|
+
counts[item.verdict] = (counts[item.verdict] ?? 0) + 1;
|
|
2551
|
+
}
|
|
2552
|
+
const applies = (counts.added ?? 0) + (counts.removed ?? 0) + (counts.changed ?? 0) + (counts["too-large"] ?? 0);
|
|
2553
|
+
totalApplies += applies;
|
|
2554
|
+
const parts = Object.entries(counts).filter(([v]) => v !== "unchanged").map(([v, n]) => `${n} ${v}`);
|
|
2555
|
+
lines.push(`- **${src.label}**: ${parts.length ? parts.join(", ") : "no changes"}${src.error ? ` \u2014 \u26A0 ${src.error}` : ""}`);
|
|
2556
|
+
for (const item of src.items.filter((i) => i.verdict !== "unchanged").slice(0, 8)) {
|
|
2557
|
+
lines.push(` ${item.verdict === "added" ? "+" : item.verdict === "removed" ? "\u2212" : item.verdict === "wont-apply" ? "\u2298" : "\u2248"} ${item.label}${item.reason ? ` (${item.reason})` : ""}`);
|
|
2558
|
+
}
|
|
2559
|
+
for (const note of src.notes) lines.push(` \xB7 ${note}`);
|
|
2560
|
+
}
|
|
2561
|
+
const head = totalApplies === 0 ? "**Restore would change NOTHING** (states identical or nothing applicable)." : `**Restore would apply ${totalApplies} change(s):**`;
|
|
2562
|
+
return text11(`${head}
|
|
2563
|
+
${lines.join("\n")}`);
|
|
2564
|
+
}
|
|
2452
2565
|
case "restore": {
|
|
2453
2566
|
if (!args.id) return fail12("restore requires `id` (from get_time_machine).");
|
|
2454
|
-
const outcome = await call("restore", {
|
|
2567
|
+
const outcome = await call("restore", {
|
|
2568
|
+
id: args.id,
|
|
2569
|
+
mode: args.mode ?? "live",
|
|
2570
|
+
excludeKeys: args.excludeKeys
|
|
2571
|
+
});
|
|
2455
2572
|
return text11(formatOutcome(outcome));
|
|
2456
2573
|
}
|
|
2457
2574
|
case "delete": {
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/mcp",
|
|
3
3
|
"mcpName": "io.github.buoy-gg/mcp",
|
|
4
|
-
"version": "7.0.
|
|
4
|
+
"version": "7.0.19",
|
|
5
5
|
"description": "MCP server for Buoy devtools — lets any LLM copilot drive Buoy tools (perf-monitor benchmarks, etc.) over the devtool sync protocol",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
32
32
|
"socket.io-client": "^4.8.1",
|
|
33
33
|
"zod": "^3.25.0",
|
|
34
|
-
"@buoy-gg/sync-broker": "^7.0.
|
|
34
|
+
"@buoy-gg/sync-broker": "^7.0.19"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|