@buoy-gg/agent-core 7.0.40 → 7.0.42

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.
Files changed (202) hide show
  1. package/lib/commonjs/blocks/receipts.js +1 -761
  2. package/lib/commonjs/blocks/runId.js +1 -31
  3. package/lib/commonjs/blocks/types.js +1 -140
  4. package/lib/commonjs/blocks/uiTool.js +6 -1405
  5. package/lib/commonjs/catalog/catalog.g.js +5 -4207
  6. package/lib/commonjs/catalog/catalog.source.json +656 -12
  7. package/lib/commonjs/catalog/catalog.types.g.js +1 -44
  8. package/lib/commonjs/catalog/normalizeParams.js +2 -332
  9. package/lib/commonjs/catalog/signature.js +1 -68
  10. package/lib/commonjs/catalog/snapshotReads.js +1 -263
  11. package/lib/commonjs/catalog/toProviderTools.js +2 -145
  12. package/lib/commonjs/catalog/validateParams.js +1 -129
  13. package/lib/commonjs/context/buildContextPack.js +1 -489
  14. package/lib/commonjs/effects/digest.js +1 -34
  15. package/lib/commonjs/effects/ledger.js +1 -325
  16. package/lib/commonjs/engine/askGate.js +1 -287
  17. package/lib/commonjs/engine/effectFor.js +1 -176
  18. package/lib/commonjs/engine/evidence.js +3 -111
  19. package/lib/commonjs/engine/historyBudget.js +1 -363
  20. package/lib/commonjs/engine/retrieve.js +1 -214
  21. package/lib/commonjs/engine/runAgentTurn.js +8 -1803
  22. package/lib/commonjs/engine/systemPrompt.js +54 -163
  23. package/lib/commonjs/engine/textToolCalls.js +1 -276
  24. package/lib/commonjs/engine/tokenCalibration.js +1 -81
  25. package/lib/commonjs/engine/verify.js +4 -297
  26. package/lib/commonjs/index.js +1 -384
  27. package/lib/commonjs/policy/labels.js +1 -319
  28. package/lib/commonjs/policy/policy.js +1 -148
  29. package/lib/commonjs/policy/redact.js +1 -172
  30. package/lib/commonjs/providers/anthropic.js +3 -445
  31. package/lib/commonjs/providers/openai.js +4 -324
  32. package/lib/commonjs/providers/problem.js +1 -98
  33. package/lib/commonjs/providers/sse.js +1 -240
  34. package/lib/commonjs/providers/streamTimer.js +1 -123
  35. package/lib/commonjs/providers/transport.js +1 -123
  36. package/lib/commonjs/providers/types.js +1 -6
  37. package/lib/commonjs/providers/xhrStream.js +1 -212
  38. package/lib/commonjs/realNow.js +1 -0
  39. package/lib/commonjs/session.js +1 -393
  40. package/lib/commonjs/types.js +0 -1
  41. package/lib/module/blocks/receipts.js +1 -755
  42. package/lib/module/blocks/runId.js +1 -26
  43. package/lib/module/blocks/types.js +1 -134
  44. package/lib/module/blocks/uiTool.js +6 -1396
  45. package/lib/module/catalog/catalog.g.js +5 -4203
  46. package/lib/module/catalog/catalog.source.json +656 -12
  47. package/lib/module/catalog/catalog.types.g.js +1 -40
  48. package/lib/module/catalog/normalizeParams.js +2 -326
  49. package/lib/module/catalog/signature.js +1 -63
  50. package/lib/module/catalog/snapshotReads.js +1 -259
  51. package/lib/module/catalog/toProviderTools.js +2 -139
  52. package/lib/module/catalog/validateParams.js +1 -124
  53. package/lib/module/context/buildContextPack.js +1 -485
  54. package/lib/module/effects/digest.js +1 -29
  55. package/lib/module/effects/ledger.js +1 -319
  56. package/lib/module/engine/askGate.js +1 -280
  57. package/lib/module/engine/effectFor.js +1 -173
  58. package/lib/module/engine/evidence.js +3 -105
  59. package/lib/module/engine/historyBudget.js +1 -355
  60. package/lib/module/engine/retrieve.js +1 -210
  61. package/lib/module/engine/runAgentTurn.js +8 -1794
  62. package/lib/module/engine/systemPrompt.js +54 -157
  63. package/lib/module/engine/textToolCalls.js +1 -270
  64. package/lib/module/engine/tokenCalibration.js +1 -75
  65. package/lib/module/engine/verify.js +4 -289
  66. package/lib/module/index.js +1 -35
  67. package/lib/module/policy/labels.js +1 -312
  68. package/lib/module/policy/policy.js +1 -142
  69. package/lib/module/policy/redact.js +1 -165
  70. package/lib/module/providers/anthropic.js +3 -441
  71. package/lib/module/providers/openai.js +4 -320
  72. package/lib/module/providers/problem.js +1 -92
  73. package/lib/module/providers/sse.js +1 -231
  74. package/lib/module/providers/streamTimer.js +1 -118
  75. package/lib/module/providers/transport.js +1 -119
  76. package/lib/module/providers/types.js +1 -4
  77. package/lib/module/providers/xhrStream.js +1 -207
  78. package/lib/module/realNow.js +1 -0
  79. package/lib/module/session.js +1 -369
  80. package/lib/module/types.js +0 -1
  81. package/lib/typescript/catalog/catalog.g.d.ts +3 -3
  82. package/lib/typescript/catalog/catalog.types.g.d.ts +7 -4
  83. package/lib/typescript/effects/ledger.d.ts +16 -1
  84. package/lib/typescript/providers/problem.d.ts +0 -20
  85. package/lib/typescript/providers/streamTimer.d.ts +0 -24
  86. package/lib/typescript/realNow.d.ts +18 -0
  87. package/lib/web/index.mjs +152 -0
  88. package/package.json +24 -3
  89. package/lib/commonjs/blocks/receipts.js.map +0 -1
  90. package/lib/commonjs/blocks/runId.js.map +0 -1
  91. package/lib/commonjs/blocks/types.js.map +0 -1
  92. package/lib/commonjs/blocks/uiTool.js.map +0 -1
  93. package/lib/commonjs/catalog/catalog.g.js.map +0 -1
  94. package/lib/commonjs/catalog/catalog.types.g.js.map +0 -1
  95. package/lib/commonjs/catalog/normalizeParams.js.map +0 -1
  96. package/lib/commonjs/catalog/signature.js.map +0 -1
  97. package/lib/commonjs/catalog/snapshotReads.js.map +0 -1
  98. package/lib/commonjs/catalog/toProviderTools.js.map +0 -1
  99. package/lib/commonjs/catalog/validateParams.js.map +0 -1
  100. package/lib/commonjs/context/buildContextPack.js.map +0 -1
  101. package/lib/commonjs/effects/digest.js.map +0 -1
  102. package/lib/commonjs/effects/ledger.js.map +0 -1
  103. package/lib/commonjs/engine/askGate.js.map +0 -1
  104. package/lib/commonjs/engine/effectFor.js.map +0 -1
  105. package/lib/commonjs/engine/evidence.js.map +0 -1
  106. package/lib/commonjs/engine/historyBudget.js.map +0 -1
  107. package/lib/commonjs/engine/retrieve.js.map +0 -1
  108. package/lib/commonjs/engine/runAgentTurn.js.map +0 -1
  109. package/lib/commonjs/engine/systemPrompt.js.map +0 -1
  110. package/lib/commonjs/engine/textToolCalls.js.map +0 -1
  111. package/lib/commonjs/engine/tokenCalibration.js.map +0 -1
  112. package/lib/commonjs/engine/verify.js.map +0 -1
  113. package/lib/commonjs/index.js.map +0 -1
  114. package/lib/commonjs/policy/labels.js.map +0 -1
  115. package/lib/commonjs/policy/policy.js.map +0 -1
  116. package/lib/commonjs/policy/redact.js.map +0 -1
  117. package/lib/commonjs/providers/anthropic.js.map +0 -1
  118. package/lib/commonjs/providers/openai.js.map +0 -1
  119. package/lib/commonjs/providers/problem.js.map +0 -1
  120. package/lib/commonjs/providers/sse.js.map +0 -1
  121. package/lib/commonjs/providers/streamTimer.js.map +0 -1
  122. package/lib/commonjs/providers/transport.js.map +0 -1
  123. package/lib/commonjs/providers/types.js.map +0 -1
  124. package/lib/commonjs/providers/xhrStream.js.map +0 -1
  125. package/lib/commonjs/session.js.map +0 -1
  126. package/lib/commonjs/types.js.map +0 -1
  127. package/lib/module/blocks/receipts.js.map +0 -1
  128. package/lib/module/blocks/runId.js.map +0 -1
  129. package/lib/module/blocks/types.js.map +0 -1
  130. package/lib/module/blocks/uiTool.js.map +0 -1
  131. package/lib/module/catalog/catalog.g.js.map +0 -1
  132. package/lib/module/catalog/catalog.types.g.js.map +0 -1
  133. package/lib/module/catalog/normalizeParams.js.map +0 -1
  134. package/lib/module/catalog/signature.js.map +0 -1
  135. package/lib/module/catalog/snapshotReads.js.map +0 -1
  136. package/lib/module/catalog/toProviderTools.js.map +0 -1
  137. package/lib/module/catalog/validateParams.js.map +0 -1
  138. package/lib/module/context/buildContextPack.js.map +0 -1
  139. package/lib/module/effects/digest.js.map +0 -1
  140. package/lib/module/effects/ledger.js.map +0 -1
  141. package/lib/module/engine/askGate.js.map +0 -1
  142. package/lib/module/engine/effectFor.js.map +0 -1
  143. package/lib/module/engine/evidence.js.map +0 -1
  144. package/lib/module/engine/historyBudget.js.map +0 -1
  145. package/lib/module/engine/retrieve.js.map +0 -1
  146. package/lib/module/engine/runAgentTurn.js.map +0 -1
  147. package/lib/module/engine/systemPrompt.js.map +0 -1
  148. package/lib/module/engine/textToolCalls.js.map +0 -1
  149. package/lib/module/engine/tokenCalibration.js.map +0 -1
  150. package/lib/module/engine/verify.js.map +0 -1
  151. package/lib/module/index.js.map +0 -1
  152. package/lib/module/policy/labels.js.map +0 -1
  153. package/lib/module/policy/policy.js.map +0 -1
  154. package/lib/module/policy/redact.js.map +0 -1
  155. package/lib/module/providers/anthropic.js.map +0 -1
  156. package/lib/module/providers/openai.js.map +0 -1
  157. package/lib/module/providers/problem.js.map +0 -1
  158. package/lib/module/providers/sse.js.map +0 -1
  159. package/lib/module/providers/streamTimer.js.map +0 -1
  160. package/lib/module/providers/transport.js.map +0 -1
  161. package/lib/module/providers/types.js.map +0 -1
  162. package/lib/module/providers/xhrStream.js.map +0 -1
  163. package/lib/module/session.js.map +0 -1
  164. package/lib/module/types.js.map +0 -1
  165. package/lib/typescript/blocks/receipts.d.ts.map +0 -1
  166. package/lib/typescript/blocks/runId.d.ts.map +0 -1
  167. package/lib/typescript/blocks/types.d.ts.map +0 -1
  168. package/lib/typescript/blocks/uiTool.d.ts.map +0 -1
  169. package/lib/typescript/catalog/catalog.g.d.ts.map +0 -1
  170. package/lib/typescript/catalog/catalog.types.g.d.ts.map +0 -1
  171. package/lib/typescript/catalog/normalizeParams.d.ts.map +0 -1
  172. package/lib/typescript/catalog/signature.d.ts.map +0 -1
  173. package/lib/typescript/catalog/snapshotReads.d.ts.map +0 -1
  174. package/lib/typescript/catalog/toProviderTools.d.ts.map +0 -1
  175. package/lib/typescript/catalog/validateParams.d.ts.map +0 -1
  176. package/lib/typescript/context/buildContextPack.d.ts.map +0 -1
  177. package/lib/typescript/effects/digest.d.ts.map +0 -1
  178. package/lib/typescript/effects/ledger.d.ts.map +0 -1
  179. package/lib/typescript/engine/askGate.d.ts.map +0 -1
  180. package/lib/typescript/engine/effectFor.d.ts.map +0 -1
  181. package/lib/typescript/engine/evidence.d.ts.map +0 -1
  182. package/lib/typescript/engine/historyBudget.d.ts.map +0 -1
  183. package/lib/typescript/engine/retrieve.d.ts.map +0 -1
  184. package/lib/typescript/engine/runAgentTurn.d.ts.map +0 -1
  185. package/lib/typescript/engine/systemPrompt.d.ts.map +0 -1
  186. package/lib/typescript/engine/textToolCalls.d.ts.map +0 -1
  187. package/lib/typescript/engine/tokenCalibration.d.ts.map +0 -1
  188. package/lib/typescript/engine/verify.d.ts.map +0 -1
  189. package/lib/typescript/index.d.ts.map +0 -1
  190. package/lib/typescript/policy/labels.d.ts.map +0 -1
  191. package/lib/typescript/policy/policy.d.ts.map +0 -1
  192. package/lib/typescript/policy/redact.d.ts.map +0 -1
  193. package/lib/typescript/providers/anthropic.d.ts.map +0 -1
  194. package/lib/typescript/providers/openai.d.ts.map +0 -1
  195. package/lib/typescript/providers/problem.d.ts.map +0 -1
  196. package/lib/typescript/providers/sse.d.ts.map +0 -1
  197. package/lib/typescript/providers/streamTimer.d.ts.map +0 -1
  198. package/lib/typescript/providers/transport.d.ts.map +0 -1
  199. package/lib/typescript/providers/types.d.ts.map +0 -1
  200. package/lib/typescript/providers/xhrStream.d.ts.map +0 -1
  201. package/lib/typescript/session.d.ts.map +0 -1
  202. package/lib/typescript/types.d.ts.map +0 -1
@@ -556,7 +556,7 @@
556
556
  },
557
557
  {
558
558
  "action": "setState",
559
- "summary": "Change a live zustand store. Ask Buoy MERGES by default (replace:false): it shallow-merges the fields you send and KEEPS the store's action functions (setQty, removeLine, …) and untouched keys. Never send replace:true unless you mean to reset the whole store — replacing drops the functions (they can't cross the wire), and the app's buttons that call them then crash. To change a list in the store, ADDRESS ITEMS BY THEIR OWN id instead of resending the list: {\"lines\":{\"seed-1\":{\"qty\":5}}} changes one field of one item, {\"lines\":{\"seed-2\":null}} removes that item, and a key that isn't in the list yet appends a new item (send all its fields). Items you don't name are untouched, so this is the only safe form when you haven't seen the whole list — a capped read means you CANNOT resend it without deleting what you weren't shown. Sending a plain ARRAY still works and still replaces the whole list, which is how you deliberately empty it. On a merge it's a TYPED EDIT: you can only change a field that already exists, to the same type, and any list item you add must match the shape of the ones already there — a wrong type, an unknown field, or a malformed new item is refused with the exact path. force:true bypasses. To change a single value, `path` + `value` is the safest form (path:\"lines[lineId=seed-1].qty\", value:5): it is always a merge and there is no nesting to get wrong.",
559
+ "summary": "Change a live zustand store. Ask Buoy MERGES by default (replace:false): it merges the fields you send, including inside nested objects (so {\"member\":{\"crowns\":5}} changes crowns and keeps member's other fields), and KEEPS the store's action functions (setQty, removeLine, …) and untouched keys. Never send replace:true unless you mean to reset the whole store — replacing drops the functions (they can't cross the wire), and the app's buttons that call them then crash. To change a list in the store, ADDRESS ITEMS BY THEIR OWN id instead of resending the list: {\"lines\":{\"seed-1\":{\"qty\":5}}} changes one field of one item, {\"lines\":{\"seed-2\":null}} removes that item, and a key that isn't in the list yet appends a new item (send all its fields). Items you don't name are untouched, so this is the only safe form when you haven't seen the whole list — a capped read means you CANNOT resend it without deleting what you weren't shown. Sending a plain ARRAY still works and still replaces the whole list, which is how you deliberately empty it. On a merge it's a TYPED EDIT: you can only change a field that already exists, to the same type, and any list item you add must match the shape of the ones already there — a wrong type, an unknown field, or a malformed new item is refused with the exact path. force:true bypasses. To change a single value, `path` + `value` is the safest form (path:\"lines[lineId=seed-1].qty\", value:5): it is always a merge and there is no nesting to get wrong.",
560
560
  "params": {
561
561
  "type": "object",
562
562
  "properties": {
@@ -1289,7 +1289,7 @@
1289
1289
  {
1290
1290
  "toolId": "events",
1291
1291
  "title": "Events",
1292
- "summary": "The cross-tool activity timeline: one chronological ring buffer (max 200, newest-first) that aggregates events from every other installed Buoy tool — network requests, redux/zustand/jotai state changes, react-query query/mutation updates, AsyncStorage/MMKV writes, route navigations, and component renders. Reach for it first for any \"what just happened in the app?\" question, before drilling into a single-tool reader. It exposes 3 actions: exportEvents (formatted read), setEnabledSources (choose what the device records), clearEvents (wipe the timeline). CRITICAL: it only records while something is watching — a cold exportEvents on a freshly connected session usually returns 0 events because nothing has ever armed capture, which is NOT the same as \"the app did nothing\".",
1292
+ "summary": "The cross-tool activity timeline: one chronological ring buffer (max 200, newest-first) that aggregates events from every other installed Buoy tool — network requests, redux/zustand/jotai state changes, react-query query/mutation updates, AsyncStorage/MMKV writes, route navigations, and component renders. Reach for it first for any \"what just happened in the app?\" question, before drilling into a single-tool reader. It exposes 3 actions: exportEvents (formatted read), setEnabledSources (choose what the device records), clearEvents (wipe the timeline). CRITICAL: it only records while something is watching — a cold exportEvents on a freshly connected session usually returns 0 events because nothing has ever armed capture, which is NOT the same as \"the app did nothing\". Swift captures network, storage-async, storage-mmkv, and route. React state and render sources are unavailable. Native export accepts format, includeEventData, includeSource, includeStatus, includeTitle, includeSubtitle, includeSummaryHeader, filterMode, filterSources, dataSizeThreshold, and timestampFormat; other settings are rejected. Native detail inherits source snapshot payload limits.",
1293
1293
  "actions": [
1294
1294
  {
1295
1295
  "action": "exportEvents",
@@ -1441,7 +1441,7 @@
1441
1441
  },
1442
1442
  "effect": "read",
1443
1443
  "release": "works",
1444
- "description": "Runs the device's own \"Copy Settings\" formatter over the unified store and returns {output: string, returned: number, totalAvailable: number, includedData: boolean, format: string}. The store is a 200-event ring, newest-first. Defaults are compact: includeEventData=false, so heavy raw payloads (network bodies, redux state trees, query data) stay on the device — pass settings.includeEventData=true only when you actually need them.\n\nTHREE traps a caller must know:\n(1) COLD READS ARE EMPTY. Sources are only subscribed while a consumer holds a watch on toolId \"events\" (the adapter's subscribe() calls ensureRemoteSourcesDefault) or the on-device Events modal is open. A bare call_action does NOT arm capture — the MCP SyncClient only watches on getSnapshot, never on callAction (packages/mcp/src/tools/packs/events.ts:80). If totalAvailable is 0, say \"nothing was being recorded\", not \"nothing happened\": arm it first (read the events snapshot, or call setEnabledSources), reproduce, then export.\n(2) limit IS APPLIED BEFORE FILTERING. The adapter does all.slice(0, limit) and only then does generateExport filter by settings.filterSources / filterMode (packages/events/src/sync/eventsSyncAdapter.ts:378-381 + eventExportFormatter.ts:616). Filtering to one source with limit:25 can produce an EMPTY output while `returned` still reports 25 — `returned` counts events handed to the formatter, not events in the output. When filtering by source, pass a large limit (or omit it, which uses the whole buffer).\n(3) filterSources takes GRANULAR EventSource values, not friendly names. \"storage\" matches nothing — use [\"storage-async\",\"storage-mmkv\"]; react-query is three values ([\"react-query\",\"react-query-query\",\"react-query-mutation\"]). The MCP get_events wrapper expands those aliases for you; a direct call_action does not.\n\nAn unrecognized preset name silently falls back to DEFAULT_COPY_SETTINGS instead of erroring. settings is shallow-merged OVER the preset.",
1444
+ "description": "Runs the device's own \"Copy Settings\" formatter over the unified store and returns {output: string, returned: number, totalAvailable: number, includedData: boolean, format: string}. The store is a 200-event ring, newest-first. Defaults are compact: includeEventData=false, so heavy raw payloads (network bodies, redux state trees, query data) stay on the device — pass settings.includeEventData=true only when you actually need them.\n\nTHREE traps a caller must know:\n(1) COLD READS ARE EMPTY. Sources are only subscribed while a consumer holds a watch on toolId \"events\" (the adapter's subscribe() calls ensureRemoteSourcesDefault) or the on-device Events modal is open. A bare call_action does NOT arm capture — the MCP SyncClient only watches on getSnapshot, never on callAction (packages/mcp/src/tools/packs/events.ts:80). If totalAvailable is 0, say \"nothing was being recorded\", not \"nothing happened\": arm it first (read the events snapshot, or call setEnabledSources), reproduce, then export.\n(2) limit IS APPLIED BEFORE FILTERING. The adapter does all.slice(0, limit) and only then does generateExport filter by settings.filterSources / filterMode (packages/events/src/sync/eventsSyncAdapter.ts:378-381 + eventExportFormatter.ts:616). Filtering to one source with limit:25 can produce an EMPTY output while `returned` still reports 25 — `returned` counts events handed to the formatter, not events in the output. When filtering by source, pass a large limit (or omit it, which uses the whole buffer).\n(3) filterSources takes GRANULAR EventSource values, not friendly names. \"storage\" matches nothing — use [\"storage-async\",\"storage-mmkv\"]; react-query is three values ([\"react-query\",\"react-query-query\",\"react-query-mutation\"]). The MCP get_events wrapper expands those aliases for you; a direct call_action does not.\n\nAn unrecognized preset name silently falls back to DEFAULT_COPY_SETTINGS instead of erroring. settings is shallow-merged OVER the preset. Swift captures network, storage-async, storage-mmkv, and route. React state and render sources are unavailable. Native export accepts format, includeEventData, includeSource, includeStatus, includeTitle, includeSubtitle, includeSummaryHeader, filterMode, filterSources, dataSizeThreshold, and timestampFormat; other settings are rejected. Native detail inherits source snapshot payload limits.",
1445
1445
  "requires": [
1446
1446
  "@buoy-gg/events installed and auto-discovered by FloatingDevTools",
1447
1447
  "a live watch on toolId \"events\" (an events snapshot read, a desktop Events panel, or a prior setEnabledSources call) before the window you want recorded — otherwise the buffer is empty",
@@ -2171,7 +2171,7 @@
2171
2171
  {
2172
2172
  "toolId": "time-machine",
2173
2173
  "title": "Time Machine",
2174
- "summary": "Save and restore the app's entire client-side state (device storage, redux, zustand, jotai, react-query) as named snapshots (\"restore points\"), plus the route it was captured on. Reach for it to put the app back into a known state before re-running a flow, to preview exactly what a restore would change before committing, or to wipe the app to fresh-install state. Snapshot payloads stay on-device — `list` returns metadata only; use `inspect` for one source's actual captured data and `preview` for item-level diffs.",
2174
+ "summary": "Save and restore the app's entire client-side state (device storage, redux, zustand, jotai, react-query) as named snapshots (\"restore points\"), plus the route it was captured on. Reach for it to put the app back into a known state before re-running a flow, to preview exactly what a restore would change before committing, or to wipe the app to fresh-install state. Snapshot payloads stay on-device — `list` returns metadata only; use `inspect` for one source's actual captured data and `preview` for item-level diffs. Swift supports live restore of registered providers, with UserDefaults and MMKV supplied by default. Keychain is excluded. Read provider detail and restoreModes. Native capabilities omit captureBaseline and wipeAll, and mode reload is rejected before writes. Snapshots include a safety copy before restoration; this is not a full process reset.",
2175
2175
  "actions": [
2176
2176
  {
2177
2177
  "action": "list",
@@ -2183,7 +2183,7 @@
2183
2183
  },
2184
2184
  "effect": "read",
2185
2185
  "release": "works",
2186
- "description": "Same payload as the tool's synced snapshot. Returns { snapshots, busy, lastOutcome, providers, route }. `snapshots[]` = { id (e.g. \"snap_m1x2y3_4\"), name, createdAt, sizeBytes, baseline?, route?:{pathname,href}, restoreRoute?, scope?, excludedKeys?, sources:{ [sourceId]: { warnings:string[] } } } — METADATA ONLY, no captured values. `providers[]` = { id, label, canCapture, canRestore, detail } for the registered sources: \"storage\", \"redux\", \"zustand\", \"jotai\", \"query\" (plus any custom ones the app registered). Read `detail` before reporting a source as broken — e.g. redux says \"Auto-instrumented store cannot be restored\" when the app didn't wrap its root reducer. `route.available` is false on apps without expo-router (those snapshots record no route). `lastOutcome` is the previous restore's per-source result. Call this first — every other action needs an `id` from here.",
2186
+ "description": "Same payload as the tool's synced snapshot. Returns { snapshots, busy, lastOutcome, providers, route }. `snapshots[]` = { id (e.g. \"snap_m1x2y3_4\"), name, createdAt, sizeBytes, baseline?, route?:{pathname,href}, restoreRoute?, scope?, excludedKeys?, sources:{ [sourceId]: { warnings:string[] } } } — METADATA ONLY, no captured values. `providers[]` = { id, label, canCapture, canRestore, detail } for the registered sources: \"storage\", \"redux\", \"zustand\", \"jotai\", \"query\" (plus any custom ones the app registered). Read `detail` before reporting a source as broken — e.g. redux says \"Auto-instrumented store cannot be restored\" when the app didn't wrap its root reducer. `route.available` is false on apps without expo-router (those snapshots record no route). `lastOutcome` is the previous restore's per-source result. Call this first — every other action needs an `id` from here. Swift supports live restore of registered providers, with UserDefaults and MMKV supplied by default. Keychain is excluded. Read provider detail and restoreModes. Native capabilities omit captureBaseline and wipeAll, and mode reload is rejected before writes. Snapshots include a safety copy before restoration; this is not a full process reset.",
2187
2187
  "requires": [
2188
2188
  "@buoy-gg/time-machine registered in FloatingDevTools"
2189
2189
  ]
@@ -2283,7 +2283,7 @@
2283
2283
  },
2284
2284
  "effect": "destructive",
2285
2285
  "release": "works",
2286
- "description": "Requires `id` (throws \"restore requires a snapshot `id`\" without it). mode \"live\" (default) replaces state in place source by source in a fixed order (storage → redux → zustand → jotai → query); mode \"reload\" restores ONLY persisted storage and then reloads the JS bundle so in-memory stores rebuild themselves. A baseline snapshot ALWAYS takes the clear+reload path regardless of mode. Storage restore is a diff — app keys missing from the snapshot are DELETED. Returns a RestoreOutcome: read `results[sourceId].applied` / `.skipped[].reason` / `.warnings` rather than assuming success (a source with canRestore:false comes back skipped with a reason, not an error), and read `willReload` and `route.status` (\"navigated\" | \"deferred\" | \"failed\" | \"unavailable\"). TWO GOTCHAS: (1) omitting `excludeKeys` makes the snapshot's own SAVED excludedKeys apply, and a saved `scope` narrows the restore to only the scoped items — passing an explicit `excludeKeys` array disables that saved scope. (2) omitting `restoreRoute` does NOT mean false: it falls back to the snapshot's own setting, which is ON whenever the snapshot captured a route. Pass restoreRoute:false to keep the app on the current screen.",
2286
+ "description": "Requires `id` (throws \"restore requires a snapshot `id`\" without it). mode \"live\" (default) replaces state in place source by source in a fixed order (storage → redux → zustand → jotai → query); mode \"reload\" restores ONLY persisted storage and then reloads the JS bundle so in-memory stores rebuild themselves. A baseline snapshot ALWAYS takes the clear+reload path regardless of mode. Storage restore is a diff — app keys missing from the snapshot are DELETED. Returns a RestoreOutcome: read `results[sourceId].applied` / `.skipped[].reason` / `.warnings` rather than assuming success (a source with canRestore:false comes back skipped with a reason, not an error), and read `willReload` and `route.status` (\"navigated\" | \"deferred\" | \"failed\" | \"unavailable\"). TWO GOTCHAS: (1) omitting `excludeKeys` makes the snapshot's own SAVED excludedKeys apply, and a saved `scope` narrows the restore to only the scoped items — passing an explicit `excludeKeys` array disables that saved scope. (2) omitting `restoreRoute` does NOT mean false: it falls back to the snapshot's own setting, which is ON whenever the snapshot captured a route. Pass restoreRoute:false to keep the app on the current screen. Swift supports live restore of registered providers, with UserDefaults and MMKV supplied by default. Keychain is excluded. Read provider detail and restoreModes. Native capabilities omit captureBaseline and wipeAll, and mode reload is rejected before writes. Snapshots include a safety copy before restoration; this is not a full process reset.",
2287
2287
  "requires": [
2288
2288
  "a source's provider must report canRestore:true or that source is skipped with a reason",
2289
2289
  "expo-router for the route leg",
@@ -2515,11 +2515,272 @@
2515
2515
  ],
2516
2516
  "unavailableWhen": "The `@buoy-gg/time-machine` tool is not in the app's FloatingDevTools tool list, or the app is a release build that has not opted into `externalSync.enableInRelease` with a real Pro license (packages/devtools-floating-menu/src/floatingMenu/autoExternalSync.tsx:190 — no socket, so no action reaches the device at all). Individual state sources are also absent unless their tool package is installed and instrumented: check `providers[].canCapture`/`canRestore` from `list` before assuming a source is covered."
2517
2517
  },
2518
+ {
2519
+ "toolId": "clock",
2520
+ "title": "Clock",
2521
+ "summary": "Override the app's own clock without touching the device: move it to any date (setTime), jump it forward or back (shift), stop it (freeze/resume), run it faster (setRate), or go back to real time (reset). Reach for it to test anything time-based on the client: expired offers and coupons, countdowns, trial and renewal dates, day or month rollovers, 'last seen' labels, token expiry the app checks itself. What moves: Date.now(), new Date(), Date() and Intl.DateTimeFormat format() with no date; a forward shift also runs the app's setTimeout/setInterval callbacks that come due inside the jump, once each. What stays real: performance.now(), animations, native timers and scheduled notifications, the time zone, and the backend's clock, so a server that checks expiry itself still uses real time. Values the app computed before the change keep their old time until the screen re-reads the clock or the app reloads (the override survives reloads). It also reads the app's sign-in tokens from Network's captured requests (JWTs, PASETO, opaque tokens whose login response gave an expiry, session cookies) and tests the app's token refresh: jumpToTokenExpiry for apps that refresh before expiry, failNextRequest for apps that refresh after a 401. Raw tokens are never returned. Every action returns the same state object as getState: { mode:'real'|'running'|'frozen', active, rate, virtualNow, virtualIso, realNow, offsetMs, timeZone, timers:{total,timeouts,intervals,nextTimeoutInMs}, lastJump:{byMs,firedTimers,at}|null, settings:{persist,showChip,fireTimersOnJump}, tokens:{available, tokens:[{id,label,host,formatLabel,hint,claims,expiresAt,serverExpiresAt,expirySource,lifetimeMs,issuedBy,refresh,uses,refreshes,looping,expiredSends,rejected}], check:{kind:'expiry'|'401',tokenId,status:'waiting'|'refreshed'|'loop',forcedAt,refreshedAt,newLifetimeMs,newIssuedBy,newTokens,staleAfterFailure}|null}, patched:{date,intl,timers,animatedOnRealClock} }. expiresAt is on the app's clock; serverExpiresAt on real time.",
2522
+ "unavailableWhen": "Needs @buoy-gg/clock installed (FloatingDevTools auto-discovers it). In release builds the saved override and timer tracking load when the Clock tool is first opened rather than at app start.",
2523
+ "actions": [
2524
+ {
2525
+ "action": "getState",
2526
+ "summary": "Read what the app's clock says right now, how far it is from real time, and the pending timers.",
2527
+ "description": "Returns the state object described in the tool summary. Read virtualIso (the app's time) against realNow before reporting what 'now' is for the app; offsetMs is app minus real. mode 'real' means no override is on. timers counts the app's pending setTimeout/setInterval callbacks, which is how many a forward shift could run.",
2528
+ "effect": "read",
2529
+ "release": "works",
2530
+ "params": {
2531
+ "type": "object",
2532
+ "properties": {},
2533
+ "additionalProperties": false
2534
+ }
2535
+ },
2536
+ {
2537
+ "action": "setTime",
2538
+ "summary": "Move the app's clock to `time` (ISO 8601, epoch ms, or relative like \"+3d\"); pass `freeze` true to stop it there.",
2539
+ "description": "`time` accepts \"2026-12-31T23:59:50\" (device-local when no zone is given), \"2026-12-31 23:59\", \"2026-12-31T23:59:50Z\", epoch milliseconds, or a signed duration from the app's current time (\"+30d\", \"-2h\"). The clock keeps running from there at the current speed unless `freeze` is true. Unlike shift, setTime never runs timers. Throws a message naming the accepted formats when `time` cannot be read. Returns the new state.",
2540
+ "effect": "write",
2541
+ "release": "works",
2542
+ "undo": {
2543
+ "action": "reset",
2544
+ "note": "reset returns to the device's real clock. If an override was already on before this call, reset does not bring that one back: read getState first and set it again with setTime/setRate."
2545
+ },
2546
+ "params": {
2547
+ "type": "object",
2548
+ "properties": {
2549
+ "time": {
2550
+ "type": [
2551
+ "string",
2552
+ "number"
2553
+ ],
2554
+ "description": "Target time: ISO 8601 (\"2026-12-31T23:59:50\"), \"YYYY-MM-DD HH:MM\", epoch ms, or relative (\"+1d\", \"-2h\")."
2555
+ },
2556
+ "freeze": {
2557
+ "type": "boolean",
2558
+ "description": "Stop the clock at `time` instead of letting it run. Default false."
2559
+ }
2560
+ },
2561
+ "required": [
2562
+ "time"
2563
+ ],
2564
+ "additionalProperties": false
2565
+ }
2566
+ },
2567
+ {
2568
+ "action": "shift",
2569
+ "summary": "Jump the app's clock `by` a duration (\"+1d\", \"-2h30m\", \"90s\" or ms); forward jumps also run app timers that come due unless `fireTimers` is false.",
2570
+ "description": "Moves the clock relative to what it reads now; a frozen clock stays frozen. Units: ms, s, m, h, d, w, mo (30 days), y (365 days). A forward jump runs, once each and soonest first, every app setTimeout/setInterval callback due within the jump (Playwright fastForward rules) and shortens what is left of the others; lastJump.firedTimers reports how many ran. Buoy's own timers are never run. Backward jumps never run timers. Returns the new state.",
2571
+ "effect": "write",
2572
+ "release": "works",
2573
+ "undo": {
2574
+ "action": "reset",
2575
+ "note": "reset returns to the device's real clock. If an override was already on before this call, reset does not bring that one back: read getState first and set it again with setTime/setRate."
2576
+ },
2577
+ "params": {
2578
+ "type": "object",
2579
+ "properties": {
2580
+ "by": {
2581
+ "type": [
2582
+ "string",
2583
+ "number"
2584
+ ],
2585
+ "description": "Signed duration: \"+1d\", \"-2h30m\", \"90s\", \"1.5 hours\", or a number of ms."
2586
+ },
2587
+ "fireTimers": {
2588
+ "type": "boolean",
2589
+ "description": "Run app timers that come due inside a forward jump. Default: the fireTimersOnJump setting (on)."
2590
+ }
2591
+ },
2592
+ "required": [
2593
+ "by"
2594
+ ],
2595
+ "additionalProperties": false
2596
+ }
2597
+ },
2598
+ {
2599
+ "action": "freeze",
2600
+ "summary": "Stop the app's clock where it is, or at `time` if given.",
2601
+ "description": "The app reads the same instant until resume, shift, setTime or reset. Timers keep running in real time and animations keep moving; only reads of the time stop. `time` takes the same formats as setTime. Returns the new state.",
2602
+ "effect": "write",
2603
+ "release": "works",
2604
+ "undo": {
2605
+ "action": "resume",
2606
+ "note": "resume starts the clock again from the frozen instant; it does not return to the time before the freeze. Use reset for real time."
2607
+ },
2608
+ "params": {
2609
+ "type": "object",
2610
+ "properties": {
2611
+ "time": {
2612
+ "type": [
2613
+ "string",
2614
+ "number"
2615
+ ],
2616
+ "description": "Optional instant to freeze at (same formats as setTime). Omit to freeze at the current app time."
2617
+ }
2618
+ },
2619
+ "additionalProperties": false
2620
+ }
2621
+ },
2622
+ {
2623
+ "action": "resume",
2624
+ "summary": "Start a frozen app clock again from the instant it was frozen at.",
2625
+ "description": "No-op when the clock is not frozen. The clock continues at its current speed. Returns the new state.",
2626
+ "effect": "write",
2627
+ "release": "works",
2628
+ "undo": {
2629
+ "action": "freeze",
2630
+ "note": "freeze stops it again, at the current app time."
2631
+ },
2632
+ "params": {
2633
+ "type": "object",
2634
+ "properties": {},
2635
+ "additionalProperties": false
2636
+ }
2637
+ },
2638
+ {
2639
+ "action": "setRate",
2640
+ "summary": "Run the app's clock `rate` times as fast as real time (1 = normal, 60 = a minute per second, 3600 = an hour per second).",
2641
+ "description": "Starts from what the app's clock reads now, so nothing jumps. Only reads of the time speed up: timer delays stay real, so taps, animations and polling behave; code that recomputes from Date.now() on each tick (most countdowns) shows the faster time. Range 0.1 to 86400. Returns the new state.",
2642
+ "effect": "write",
2643
+ "release": "works",
2644
+ "undo": {
2645
+ "action": "setRate",
2646
+ "note": "Call again with rate 1. That keeps the time already gained; use reset for real time."
2647
+ },
2648
+ "params": {
2649
+ "type": "object",
2650
+ "properties": {
2651
+ "rate": {
2652
+ "type": "number",
2653
+ "description": "Speed multiplier, 0.1-86400. 1 is normal speed."
2654
+ }
2655
+ },
2656
+ "required": [
2657
+ "rate"
2658
+ ],
2659
+ "additionalProperties": false
2660
+ }
2661
+ },
2662
+ {
2663
+ "action": "reset",
2664
+ "summary": "Put the app back on the device's real clock.",
2665
+ "description": "Removes the Date and Intl patches entirely, clears the saved override and closes the clock chip. Screens that already computed a time keep it until they re-read the clock or the app reloads. Returns the new state (mode 'real').",
2666
+ "effect": "write",
2667
+ "release": "works",
2668
+ "params": {
2669
+ "type": "object",
2670
+ "properties": {},
2671
+ "additionalProperties": false
2672
+ }
2673
+ },
2674
+ {
2675
+ "action": "updateSettings",
2676
+ "summary": "Change the Clock tool's settings: `persist` (keep the override across reloads), `showChip` (floating reminder), `fireTimersOnJump`.",
2677
+ "description": "Only the booleans given are changed. persist=false still applies the override now but forgets it on the next reload. Returns the new state with settings.",
2678
+ "effect": "write",
2679
+ "release": "works",
2680
+ "params": {
2681
+ "type": "object",
2682
+ "properties": {
2683
+ "persist": {
2684
+ "type": "boolean",
2685
+ "description": "Keep the override across reloads and restarts."
2686
+ },
2687
+ "showChip": {
2688
+ "type": "boolean",
2689
+ "description": "Show the floating clock chip while an override is on."
2690
+ },
2691
+ "fireTimersOnJump": {
2692
+ "type": "boolean",
2693
+ "description": "Whether forward shifts run app timers that come due."
2694
+ }
2695
+ },
2696
+ "additionalProperties": false
2697
+ }
2698
+ },
2699
+ {
2700
+ "action": "jumpToTokenExpiry",
2701
+ "summary": "Move the app's clock to just before a sign-in token expires (default 10 s before), then watch for the app to fetch a new token.",
2702
+ "description": "Tests apps that refresh their token before it expires by comparing its expiry with Date.now(). Shifts the app's clock forward (running timers that come due, like a scheduled refresh) to `leadMs` before the token's expiry, and starts a refresh check. The server keeps real time, so it still accepts the old token: only the app's own refresh logic is exercised. The app has to send a request (or run its refresh timer) before a new token shows up; read getState's tokens.check for the result: 'refreshed' with the new token's lifetime and issuing endpoint, or 'loop' when new tokens keep arriving because they already look expired to the moved clock. Throws when no token has been seen, when the token's expiry is unreadable (then use failNextRequest), or when it has already expired on the app's clock.",
2703
+ "effect": "write",
2704
+ "release": "works",
2705
+ "undo": {
2706
+ "action": "reset",
2707
+ "note": "reset puts the app back on real time. The app keeps any token it fetched meanwhile, which is harmless."
2708
+ },
2709
+ "params": {
2710
+ "type": "object",
2711
+ "properties": {
2712
+ "token": {
2713
+ "type": "string",
2714
+ "description": "Token id from getState's tokens[].id. Default: the most recently used token that has a readable expiry."
2715
+ },
2716
+ "leadMs": {
2717
+ "type": "number",
2718
+ "description": "Land this many ms before expiry. Default 10000."
2719
+ }
2720
+ },
2721
+ "additionalProperties": false
2722
+ }
2723
+ },
2724
+ {
2725
+ "action": "failNextRequest",
2726
+ "summary": "Answer the app's next request that carries a sign-in token with a 401 (or `status` 403), once or `times` times, then watch for the app to fetch a new token.",
2727
+ "description": "Tests apps that refresh their token after the server rejects it. Adds a one-shot Network override on the token's origin, limited to requests that send the token's header, so the 401 never lands on the app's own token request. It answers with `WWW-Authenticate: Bearer error=\"invalid_token\"` and an invalid_token JSON body, and starts a refresh check. Needs @buoy-gg/network. The app has to send a request for the 401 to reach it; getState's tokens.check then shows forcedAt, whether a new token arrived, staleAfterFailure (requests that re-sent the old token after the 401) and failedRefreshes (token requests that failed). Replaces an earlier 401 that has not fired yet.",
2728
+ "effect": "write",
2729
+ "release": "works",
2730
+ "undo": {
2731
+ "action": "stopTokenCheck",
2732
+ "note": "stopTokenCheck withdraws the 401 if it has not reached the app yet. A 401 the app already received cannot be taken back."
2733
+ },
2734
+ "params": {
2735
+ "type": "object",
2736
+ "properties": {
2737
+ "token": {
2738
+ "type": "string",
2739
+ "description": "Token id from getState's tokens[].id. Default: the most recently used token that has a readable expiry."
2740
+ },
2741
+ "status": {
2742
+ "type": "number",
2743
+ "description": "401 (default) or 403."
2744
+ },
2745
+ "times": {
2746
+ "type": "number",
2747
+ "description": "How many requests to fail, 1-10. Default 1."
2748
+ }
2749
+ },
2750
+ "additionalProperties": false
2751
+ }
2752
+ },
2753
+ {
2754
+ "action": "stopTokenCheck",
2755
+ "summary": "End the refresh check, withdrawing a forced 401 that has not reached the app yet.",
2756
+ "description": "Clears tokens.check. When the check was started by failNextRequest and the 401 has not fired, removes that Network override so it cannot fail a later request. Returns the new state.",
2757
+ "effect": "write",
2758
+ "release": "works",
2759
+ "params": {
2760
+ "type": "object",
2761
+ "properties": {},
2762
+ "additionalProperties": false
2763
+ }
2764
+ }
2765
+ ]
2766
+ },
2518
2767
  {
2519
2768
  "toolId": "storage",
2520
2769
  "title": "Storage",
2521
2770
  "summary": "Read and write the app's persisted state across all three backends — AsyncStorage, every registered MMKV instance, and registered Expo SecureStore keys — plus a recorded timeline of storage writes with per-event undo/jump. Reach for this first when a value is wrong, stale, missing, or only broken after a restart/upgrade: the bad value is usually sitting in storage. Buoy's own devtool keys (@react_buoy*, @buoy*, buoy-*) are stripped from every read path, so results are app data only. Nothing here is __DEV__-gated — every action really runs in a release build.",
2522
2771
  "actions": [
2772
+ {
2773
+ "action": "getRequiredKeys",
2774
+ "summary": "List the storage keys the app declares as required, with their expected types and backends.",
2775
+ "params": {
2776
+ "type": "object",
2777
+ "properties": {},
2778
+ "additionalProperties": false
2779
+ },
2780
+ "effect": "read",
2781
+ "release": "works",
2782
+ "description": "Returns the requiredStorageKeys array given to createStorageTool (or the FloatingDevTools requiredStorageKeys prop): each entry is a key, or an object with key, expectedType or expectedValue, description and storageType (async, mmkv or secure). Returns [] when the app declared none. It reports the configuration only; read the keys themselves to see whether they are present and valid. Swift returns the requirements configured through BuoyStorageModule.configure(requiredKeys:)."
2783
+ },
2523
2784
  {
2524
2785
  "action": "async.getAllKeys",
2525
2786
  "summary": "List every AsyncStorage key in the app (Buoy's own devtool keys stripped).",
@@ -3028,7 +3289,7 @@
3028
3289
  {
3029
3290
  "toolId": "highlight-updates",
3030
3291
  "title": "Highlight Updates",
3031
- "summary": "Two different jobs share one tool: (1) DRIVING and READING the live screen — describeScreen lists every on-screen element with tap points, tapElement invokes its real handler in JS (works on physical devices, no screenshots), and locateComponent resolves a component to an on-screen rect; (2) MEASURING re-renders — beginMeasurement/endMeasurement wrap an interaction and return per-component render cost + cause, while toggle/setEnabled draw the user-visible colored highlight boxes on the device. Reach for describeScreen+tapElement to see or operate the app, and beginMeasurement/endMeasurement to answer \"why is this screen slow\". CRITICAL: every action here depends on React's DevTools global hook, which React Native installs ONLY in dev builds — in a release/production build five of these actions return ok:true and silently do nothing, so never report a highlight toggle as applied without confirming it's a dev build.",
3292
+ "summary": "Read and drive the live screen with describeScreen, tapElement, and waitFor. RN also exposes React render tracking; native Swift does not. Inspect the connected device’s actions before calling a platform-specific operation. The touch-capture actions support Scenarios recording. Native capture is restricted to development builds and excludes Buoy controls and secure text inputs.",
3032
3293
  "actions": [
3033
3294
  {
3034
3295
  "action": "describeScreen",
@@ -3371,9 +3632,62 @@
3371
3632
  "requires": [
3372
3633
  "@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>"
3373
3634
  ]
3635
+ },
3636
+ {
3637
+ "action": "startTouchCapture",
3638
+ "summary": "Start recording supported host interactions.",
3639
+ "description": "Returns capture status and a reason if unavailable. RN uses its touch stream; Swift uses UIKit events and native-driver interactions in development builds. Read the status before driving the app. This does not save a scenario.",
3640
+ "params": {
3641
+ "type": "object",
3642
+ "properties": {},
3643
+ "additionalProperties": false
3644
+ },
3645
+ "effect": "write",
3646
+ "release": "empty"
3647
+ },
3648
+ {
3649
+ "action": "stopTouchCapture",
3650
+ "summary": "Stop capturing new interactions and retain recorded data.",
3651
+ "description": "Read retained records with readTouchCapture. Stopping does not save a scenario.",
3652
+ "params": {
3653
+ "type": "object",
3654
+ "properties": {},
3655
+ "additionalProperties": false
3656
+ },
3657
+ "effect": "write",
3658
+ "release": "works"
3659
+ },
3660
+ {
3661
+ "action": "clearTouchCapture",
3662
+ "summary": "Clear retained interactions and reset the sequence number.",
3663
+ "description": "Existing recording data is removed. Start subsequent reads with sinceSeq:0.",
3664
+ "params": {
3665
+ "type": "object",
3666
+ "properties": {},
3667
+ "additionalProperties": false
3668
+ },
3669
+ "effect": "destructive",
3670
+ "release": "works"
3671
+ },
3672
+ {
3673
+ "action": "readTouchCapture",
3674
+ "summary": "Read captured interactions after a sequence number.",
3675
+ "description": "Returns {status, reason, records, seq}. Pass the last returned seq as sinceSeq on the next read. Coverage depends on platform; records do not prove every gesture was captured or replayable.",
3676
+ "params": {
3677
+ "type": "object",
3678
+ "properties": {
3679
+ "sinceSeq": {
3680
+ "type": "number",
3681
+ "description": "Return records with a sequence number greater than this value. Default 0."
3682
+ }
3683
+ },
3684
+ "additionalProperties": false
3685
+ },
3686
+ "effect": "read",
3687
+ "release": "works"
3374
3688
  }
3375
3689
  ],
3376
- "unavailableWhen": "The app doesn't mount <FloatingDevTools> with @buoy-gg/highlight-updates registered. Also effectively inert in any release/production build: React Native installs __REACT_DEVTOOLS_GLOBAL_HOOK__ only under __DEV__, so the fiber walk finds zero roots and the controller's initialize() bails at HighlightUpdatesController.ts:1744 — read each action's release field before claiming it worked."
3690
+ "unavailableWhen": "RN requires @buoy-gg/highlight-updates registered with FloatingDevTools; React inspection and render tracking depend on a development build. Swift exposes native interaction and touch-capture actions under this ID, without React render tracking. Inspect the connected device’s available actions. Swift touch capture refuses production builds."
3377
3691
  },
3378
3692
  {
3379
3693
  "toolId": "scenarios",
@@ -3845,7 +4159,7 @@
3845
4159
  },
3846
4160
  {
3847
4161
  "toolId": "perf-monitor",
3848
- "title": "Perf Monitor",
4162
+ "title": "Bench",
3849
4163
  "summary": "Measures runtime performance on the device: live JS/UI FPS, CPU and memory sampled every 250ms, plus recorded \"benchmark runs\" saved to disk and an automation mode that navigates a screen with different query params and ranks the variants. Reach for it to answer \"is this screen slow / which variant is faster / did my fix land\", not to inspect data — it reads no app state. Two traps: live metrics are all zeros until `setEnabled {enabled:true}` arms sampling, and `startAutomation` swallows every validation error, so a bad config returns ok and simply never runs.",
3850
4164
  "actions": [
3851
4165
  {
@@ -4315,7 +4629,7 @@
4315
4629
  {
4316
4630
  "toolId": "assets",
4317
4631
  "title": "Assets",
4318
- "summary": "Inventory of what the app SHIPS — every bundled asset (images, fonts, video, audio, data) with dimensions, @1x/@2x/@3x scale coverage, content hash, byte size, loaded-vs-never-loaded state, audit findings (duplicates, unused, WebP candidates, over-decode, heavy GIFs/fonts, scale gaps), and a diff vs a saved baseline. Reach for it for bundle-size questions (\"what's making the app big\", \"did this branch add megabytes\", \"which images ship but are never used\"); use the images tool instead for what the app actually RENDERS. Nothing here is __DEV__-gated (zero `__DEV__` in packages/assets/src), but the Metro dev server is what supplies the full-bundle graph and real byte sizes — in an embedded/release bundle the inventory shrinks to registry-only records with estimated sizes.",
4632
+ "summary": "Inventory of what the app SHIPS — every bundled asset (images, fonts, video, audio, data) with dimensions, @1x/@2x/@3x scale coverage, content hash, byte size, loaded-vs-never-loaded state, audit findings (duplicates, unused, WebP candidates, over-decode, heavy GIFs/fonts, scale gaps), and a diff vs a saved baseline. Reach for it for bundle-size questions (\"what's making the app big\", \"did this branch add megabytes\", \"which images ship but are never used\"); use the images tool instead for what the app actually RENDERS. Nothing here is __DEV__-gated (zero `__DEV__` in packages/assets/src), but the Metro dev server is what supplies the full-bundle graph and real byte sizes — in an embedded/release bundle the inventory shrinks to registry-only records with estimated sizes. Swift inventories loose resources in the main bundle and explicitly registered app resource bundles. Buoy resource bundles are excluded. Compiled Assets.car files are aggregate records, not per-image inventory. Native loaded status requires host calls to BuoyAssets.markLoaded; unobserved does not mean unused. Native scale and decoded-memory insight arrays are currently empty. Check scanStatus.coverage and warnings.",
4319
4633
  "actions": [
4320
4634
  {
4321
4635
  "action": "list",
@@ -4349,7 +4663,7 @@
4349
4663
  },
4350
4664
  "effect": "read",
4351
4665
  "release": "works",
4352
- "description": "Returns { stats, scanStatus, insights, diff, total, records }. `records` is sorted by size descending and sliced to `limit`; `total` is the count AFTER kind/loadedOnly filtering but BEFORE the slice. Each record: { id, key, registryId, name, type, kind, hash, location, width, height, scales, uri, loaded, sizeBytes, sizeSource }. `sizeSource` is \"measured\" (real bytes from the Metro dev server) or \"estimate\" (decoded RGBA memory, width*scale*height*scale*4) — never present them as the same number. `insights` carries id lists: duplicates (grouped by content hash, with wastedBytes), neverLoaded, webpCandidates (png/jpg >= 50KB), overDecode (decodes to >2 screenfuls), heavyGifs (>= 100KB), heavyFonts (>= 150KB), scaleGaps, scaleAnomalies. If `scanStatus.lastScanAt` is null or `graphCount` is null, run `rescan` first — the inventory is registry-only until the Metro graph is merged. If `scanStatus.measuring` is true, byte totals are partial; re-run shortly.",
4666
+ "description": "Returns { stats, scanStatus, insights, diff, total, records }. `records` is sorted by size descending and sliced to `limit`; `total` is the count AFTER kind/loadedOnly filtering but BEFORE the slice. Each record: { id, key, registryId, name, type, kind, hash, location, width, height, scales, uri, loaded, sizeBytes, sizeSource }. `sizeSource` is \"measured\" (real bytes from the Metro dev server) or \"estimate\" (decoded RGBA memory, width*scale*height*scale*4) — never present them as the same number. `insights` carries id lists: duplicates (grouped by content hash, with wastedBytes), neverLoaded, webpCandidates (png/jpg >= 50KB), overDecode (decodes to >2 screenfuls), heavyGifs (>= 100KB), heavyFonts (>= 150KB), scaleGaps, scaleAnomalies. If `scanStatus.lastScanAt` is null or `graphCount` is null, run `rescan` first — the inventory is registry-only until the Metro graph is merged. If `scanStatus.measuring` is true, byte totals are partial; re-run shortly. Swift inventories loose resources in the main bundle and explicitly registered app resource bundles. Buoy resource bundles are excluded. Compiled Assets.car files are aggregate records, not per-image inventory. Native loaded status requires host calls to BuoyAssets.markLoaded; unobserved does not mean unused. Native scale and decoded-memory insight arrays are currently empty. Check scanStatus.coverage and warnings.",
4353
4667
  "requires": [
4354
4668
  "@buoy-gg/assets imported in the app",
4355
4669
  "Metro dev server (for full-bundle coverage, never-loaded detection and measured bytes)"
@@ -4465,7 +4779,7 @@
4465
4779
  },
4466
4780
  "effect": "read",
4467
4781
  "release": "works",
4468
- "description": "Returns { registryAvailable, patched, registryCount, graphSupported, graphCount, graphError, lastScanAt, measuring, measuredCount, fontFamilies: string[], localAssetCount }. Use it to explain a thin or surprising inventory before drawing conclusions: registryAvailable:false means the RN asset registry could not be required at all; graphCount:null with a graphError means only runtime-registered assets are known (so 'never loaded' is unanswerable); measuring:true means byte totals are still landing. lastScanAt:null means nothing has scanned yet — call rescan."
4782
+ "description": "Returns { registryAvailable, patched, registryCount, graphSupported, graphCount, graphError, lastScanAt, measuring, measuredCount, fontFamilies: string[], localAssetCount }. Use it to explain a thin or surprising inventory before drawing conclusions: registryAvailable:false means the RN asset registry could not be required at all; graphCount:null with a graphError means only runtime-registered assets are known (so 'never loaded' is unanswerable); measuring:true means byte totals are still landing. lastScanAt:null means nothing has scanned yet — call rescan. Swift inventories loose resources in the main bundle and explicitly registered app resource bundles. Buoy resource bundles are excluded. Compiled Assets.car files are aggregate records, not per-image inventory. Native loaded status requires host calls to BuoyAssets.markLoaded; unobserved does not mean unused. Native scale and decoded-memory insight arrays are currently empty. Check scanStatus.coverage and warnings."
4469
4783
  }
4470
4784
  ],
4471
4785
  "unavailableWhen": "The app doesn't import @buoy-gg/assets (autoExternalSync only registers the adapter when the module resolves — packages/devtools-floating-menu/src/floatingMenu/autoExternalSync.tsx:391), or the app is a release/production JS bundle that didn't opt in with `externalSync.enableInRelease` plus a real Pro license, in which case the device never connects to the broker at all."
@@ -5130,5 +5444,335 @@
5130
5444
  }
5131
5445
  ],
5132
5446
  "unavailableWhen": "Only present while an Ask Buoy session is running — which is exactly when this catalog is in use, so in practice always available to you."
5447
+ },
5448
+ {
5449
+ "toolId": "push-notifications",
5450
+ "title": "Push Notifications",
5451
+ "summary": "Inspect notification receipt, responses, presentation decisions, background-task results and tokens from an explicitly configured Expo capture adapter. Use test IDs to correlate stages. Simulator sending lives in the desktop host and is not a device action. Real-provider sending is not implemented.",
5452
+ "unavailableWhen": "Capture requires @buoy-gg/notifications and early installExpoNotificationCapture setup with the app SDK. Release capture needs an explicit enableInRelease option; release desktop sync separately requires its own opt-in and Pro. Background evidence requires the app task wrapper. Native delivery validation covers Expo 56 on iOS.",
5453
+ "actions": [
5454
+ {
5455
+ "action": "getSnapshot",
5456
+ "summary": "Read captured notification evidence and tokens.",
5457
+ "description": "Returns the bounded journal, session, provider capabilities, permissions and token observations. A missing callback does not prove that no OS alert appeared.",
5458
+ "effect": "read",
5459
+ "release": "works",
5460
+ "params": {
5461
+ "type": "object",
5462
+ "properties": {},
5463
+ "additionalProperties": false
5464
+ }
5465
+ },
5466
+ {
5467
+ "action": "getCapabilities",
5468
+ "summary": "Check notification capture setup.",
5469
+ "description": "Returns provider/version, app ID, platform, supported operations and durable-storage state. No device tokens are registered by this action.",
5470
+ "effect": "read",
5471
+ "release": "works",
5472
+ "params": {
5473
+ "type": "object",
5474
+ "properties": {},
5475
+ "additionalProperties": false
5476
+ }
5477
+ },
5478
+ {
5479
+ "action": "getEvent",
5480
+ "summary": "Read one captured event by id.",
5481
+ "description": "Returns an event by its Buoy record ID, or null after eviction. Native notification ID and test ID are separate fields.",
5482
+ "effect": "read",
5483
+ "release": "works",
5484
+ "params": {
5485
+ "type": "object",
5486
+ "properties": {
5487
+ "id": {
5488
+ "type": "string",
5489
+ "description": "Buoy event record id from getSnapshot.events."
5490
+ }
5491
+ },
5492
+ "additionalProperties": false,
5493
+ "required": [
5494
+ "id"
5495
+ ]
5496
+ }
5497
+ },
5498
+ {
5499
+ "action": "getPermissions",
5500
+ "summary": "Refresh notification permission settings.",
5501
+ "description": "Reads the existing OS settings through Expo. Does not display a permission prompt.",
5502
+ "effect": "read",
5503
+ "release": "works",
5504
+ "params": {
5505
+ "type": "object",
5506
+ "properties": {},
5507
+ "additionalProperties": false
5508
+ }
5509
+ },
5510
+ {
5511
+ "action": "listPresented",
5512
+ "summary": "Inspect notifications currently in the system tray.",
5513
+ "description": "Reads a current snapshot through Expo. This is not a historical record and does not dismiss notifications.",
5514
+ "effect": "read",
5515
+ "release": "works",
5516
+ "params": {
5517
+ "type": "object",
5518
+ "properties": {},
5519
+ "additionalProperties": false
5520
+ }
5521
+ },
5522
+ {
5523
+ "action": "refreshToken",
5524
+ "summary": "Register a token with type \"device\" or \"expo\". Expo registration requires a configured projectId.",
5525
+ "description": "May register with APNs/FCM or contact Expo Push Service. Expo tokens require projectId in app setup. Never invoke merely to open the inspector.",
5526
+ "effect": "write",
5527
+ "release": "works",
5528
+ "params": {
5529
+ "type": "object",
5530
+ "properties": {
5531
+ "type": {
5532
+ "type": "string",
5533
+ "enum": [
5534
+ "device",
5535
+ "expo"
5536
+ ],
5537
+ "description": "device returns APNs on iOS or FCM on Android; expo requests an Expo token."
5538
+ }
5539
+ },
5540
+ "additionalProperties": false,
5541
+ "required": [
5542
+ "type"
5543
+ ]
5544
+ }
5545
+ },
5546
+ {
5547
+ "action": "setCaptureSession",
5548
+ "summary": "Set runId and optional durationMs to arm capture, or runId:null to stop.",
5549
+ "description": "Arm while the app is connected, before backgrounding or stopping it. Await durable:true before relying on cold-launch recovery. Sessions last at most one hour.",
5550
+ "effect": "write",
5551
+ "release": "works",
5552
+ "params": {
5553
+ "type": "object",
5554
+ "properties": {
5555
+ "runId": {
5556
+ "type": [
5557
+ "string",
5558
+ "null"
5559
+ ],
5560
+ "description": "A test-run label of 1 to 128 characters, or null to stop capture."
5561
+ },
5562
+ "durationMs": {
5563
+ "type": "number",
5564
+ "minimum": 1000,
5565
+ "maximum": 3600000,
5566
+ "description": "Capture duration in milliseconds. Default 1800000."
5567
+ }
5568
+ },
5569
+ "additionalProperties": false,
5570
+ "required": [
5571
+ "runId"
5572
+ ]
5573
+ }
5574
+ },
5575
+ {
5576
+ "action": "clearCapturedEvents",
5577
+ "summary": "Clear Buoy notification history.",
5578
+ "description": "Deletes retained captured events. Leaves device tokens, OS notifications and the capture session unchanged.",
5579
+ "effect": "destructive",
5580
+ "release": "works",
5581
+ "params": {
5582
+ "type": "object",
5583
+ "properties": {},
5584
+ "additionalProperties": false
5585
+ }
5586
+ },
5587
+ {
5588
+ "action": "scheduleLocal",
5589
+ "summary": "Schedule a local notification with title, body, optional data and a delay in seconds.",
5590
+ "description": "This tests local notification behavior, not APNs or FCM delivery. The app must be connected before scheduling. Uses the app SDK and notification settings.",
5591
+ "effect": "write",
5592
+ "release": "works",
5593
+ "params": {
5594
+ "type": "object",
5595
+ "properties": {
5596
+ "title": {
5597
+ "type": "string",
5598
+ "description": "Displayed title."
5599
+ },
5600
+ "body": {
5601
+ "type": "string",
5602
+ "description": "Displayed message."
5603
+ },
5604
+ "data": {
5605
+ "type": "object",
5606
+ "description": "Custom notification data. Include __buoyTestId to correlate with a test run."
5607
+ },
5608
+ "seconds": {
5609
+ "type": "number",
5610
+ "minimum": 1,
5611
+ "maximum": 3600,
5612
+ "description": "Delay before local delivery."
5613
+ }
5614
+ },
5615
+ "additionalProperties": false,
5616
+ "required": [
5617
+ "title",
5618
+ "body",
5619
+ "seconds"
5620
+ ]
5621
+ }
5622
+ }
5623
+ ]
5624
+ },
5625
+ {
5626
+ "toolId": "image-overlay",
5627
+ "title": "Image Overlay",
5628
+ "summary": "Control the Swift design-image overlay. Uses the same state as the on-device controls. Read getSnapshot after loadImage to check loading and error. Available on Swift iOS only; check device capabilities.",
5629
+ "actions": [
5630
+ {
5631
+ "action": "getSnapshot",
5632
+ "summary": "Read overlay loading, image presence, placement, selected target and settings.",
5633
+ "params": {
5634
+ "type": "object",
5635
+ "properties": {},
5636
+ "additionalProperties": false
5637
+ },
5638
+ "effect": "read",
5639
+ "release": "works"
5640
+ },
5641
+ {
5642
+ "action": "listTargets",
5643
+ "summary": "Scan visible app image-overlay targets and return their ids, labels and frames.",
5644
+ "params": {
5645
+ "type": "object",
5646
+ "properties": {},
5647
+ "additionalProperties": false
5648
+ },
5649
+ "effect": "read",
5650
+ "release": "works"
5651
+ },
5652
+ {
5653
+ "action": "selectTarget",
5654
+ "summary": "Attach the overlay to a visible target.",
5655
+ "params": {
5656
+ "type": "object",
5657
+ "properties": {
5658
+ "id": {
5659
+ "type": "string",
5660
+ "description": "Target id returned by listTargets."
5661
+ }
5662
+ },
5663
+ "additionalProperties": false,
5664
+ "required": [
5665
+ "id"
5666
+ ]
5667
+ },
5668
+ "effect": "write",
5669
+ "release": "works"
5670
+ },
5671
+ {
5672
+ "action": "loadImage",
5673
+ "summary": "Start loading a design image; returns {scheduled:true}. Check getSnapshot for completion or error.",
5674
+ "params": {
5675
+ "type": "object",
5676
+ "properties": {
5677
+ "url": {
5678
+ "type": "string",
5679
+ "description": "HTTP(S) image URL."
5680
+ }
5681
+ },
5682
+ "additionalProperties": false,
5683
+ "required": [
5684
+ "url"
5685
+ ]
5686
+ },
5687
+ "effect": "write",
5688
+ "release": "works"
5689
+ },
5690
+ {
5691
+ "action": "setSettings",
5692
+ "summary": "Update overlay settings. Invalid batches fail before any settings change.",
5693
+ "params": {
5694
+ "type": "object",
5695
+ "properties": {
5696
+ "visible": {
5697
+ "type": "boolean",
5698
+ "description": "Show the overlay."
5699
+ },
5700
+ "locked": {
5701
+ "type": "boolean",
5702
+ "description": "Lock direct manipulation."
5703
+ },
5704
+ "flipped": {
5705
+ "type": "boolean",
5706
+ "description": "Flip horizontally."
5707
+ },
5708
+ "flippedY": {
5709
+ "type": "boolean",
5710
+ "description": "Flip vertically."
5711
+ },
5712
+ "showOutline": {
5713
+ "type": "boolean",
5714
+ "description": "Show the target outline."
5715
+ },
5716
+ "autoTrack": {
5717
+ "type": "boolean",
5718
+ "description": "Follow the selected target."
5719
+ },
5720
+ "opacity": {
5721
+ "type": "number",
5722
+ "description": "Opacity from 0 to 1."
5723
+ },
5724
+ "scale": {
5725
+ "type": "number",
5726
+ "description": "Positive scale factor."
5727
+ },
5728
+ "offsetX": {
5729
+ "type": "number",
5730
+ "description": "Horizontal offset in points."
5731
+ },
5732
+ "offsetY": {
5733
+ "type": "number",
5734
+ "description": "Vertical offset in points."
5735
+ }
5736
+ },
5737
+ "additionalProperties": false
5738
+ },
5739
+ "effect": "write",
5740
+ "release": "works"
5741
+ },
5742
+ {
5743
+ "action": "fitToScreen",
5744
+ "summary": "Fit the image to the screen width while preserving its aspect ratio.",
5745
+ "params": {
5746
+ "type": "object",
5747
+ "properties": {},
5748
+ "additionalProperties": false
5749
+ },
5750
+ "effect": "write",
5751
+ "release": "works"
5752
+ },
5753
+ {
5754
+ "action": "resetSettings",
5755
+ "summary": "Reset opacity, flips, scale and offsets.",
5756
+ "params": {
5757
+ "type": "object",
5758
+ "properties": {},
5759
+ "additionalProperties": false
5760
+ },
5761
+ "effect": "write",
5762
+ "release": "works"
5763
+ },
5764
+ {
5765
+ "action": "remove",
5766
+ "summary": "Remove the image and target; cancel pending image loading.",
5767
+ "params": {
5768
+ "type": "object",
5769
+ "properties": {},
5770
+ "additionalProperties": false
5771
+ },
5772
+ "effect": "write",
5773
+ "release": "works"
5774
+ }
5775
+ ],
5776
+ "unavailableWhen": "The native Swift image-overlay adapter is not registered. React Native does not expose these actions."
5133
5777
  }
5134
5778
  ]