@buoy-gg/agent-core 7.0.34 → 7.0.36
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/README.md +10 -7
- package/lib/commonjs/blocks/receipts.js +83 -24
- package/lib/commonjs/blocks/receipts.js.map +1 -1
- package/lib/commonjs/blocks/runId.js +31 -0
- package/lib/commonjs/blocks/runId.js.map +1 -0
- package/lib/commonjs/blocks/types.js +23 -2
- package/lib/commonjs/blocks/types.js.map +1 -1
- package/lib/commonjs/blocks/uiTool.js +426 -12
- package/lib/commonjs/blocks/uiTool.js.map +1 -1
- package/lib/commonjs/catalog/catalog.g.js +121 -43
- package/lib/commonjs/catalog/catalog.g.js.map +1 -1
- package/lib/commonjs/catalog/catalog.source.json +121 -45
- package/lib/commonjs/catalog/catalog.types.g.js +44 -0
- package/lib/commonjs/catalog/catalog.types.g.js.map +1 -0
- package/lib/commonjs/catalog/normalizeParams.js +333 -0
- package/lib/commonjs/catalog/normalizeParams.js.map +1 -0
- package/lib/commonjs/catalog/signature.js +68 -0
- package/lib/commonjs/catalog/signature.js.map +1 -0
- package/lib/commonjs/catalog/snapshotReads.js +13 -6
- package/lib/commonjs/catalog/snapshotReads.js.map +1 -1
- package/lib/commonjs/catalog/toProviderTools.js +41 -5
- package/lib/commonjs/catalog/toProviderTools.js.map +1 -1
- package/lib/commonjs/context/buildContextPack.js +367 -19
- package/lib/commonjs/context/buildContextPack.js.map +1 -1
- package/lib/commonjs/effects/digest.js +34 -0
- package/lib/commonjs/effects/digest.js.map +1 -0
- package/lib/commonjs/effects/ledger.js +85 -1
- package/lib/commonjs/effects/ledger.js.map +1 -1
- package/lib/commonjs/engine/askGate.js +287 -0
- package/lib/commonjs/engine/askGate.js.map +1 -0
- package/lib/commonjs/engine/effectFor.js +82 -1
- package/lib/commonjs/engine/effectFor.js.map +1 -1
- package/lib/commonjs/engine/historyBudget.js +260 -0
- package/lib/commonjs/engine/historyBudget.js.map +1 -0
- package/lib/commonjs/engine/runAgentTurn.js +958 -88
- package/lib/commonjs/engine/runAgentTurn.js.map +1 -1
- package/lib/commonjs/engine/systemPrompt.js +72 -8
- package/lib/commonjs/engine/systemPrompt.js.map +1 -1
- package/lib/commonjs/engine/textToolCalls.js +276 -0
- package/lib/commonjs/engine/textToolCalls.js.map +1 -0
- package/lib/commonjs/index.js +81 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/policy/labels.js +19 -3
- package/lib/commonjs/policy/labels.js.map +1 -1
- package/lib/commonjs/policy/policy.js +6 -1
- package/lib/commonjs/policy/policy.js.map +1 -1
- package/lib/commonjs/policy/redact.js +42 -5
- package/lib/commonjs/policy/redact.js.map +1 -1
- package/lib/commonjs/providers/anthropic.js +340 -174
- package/lib/commonjs/providers/anthropic.js.map +1 -1
- package/lib/commonjs/providers/openai.js +229 -103
- package/lib/commonjs/providers/openai.js.map +1 -1
- package/lib/commonjs/providers/sse.js +164 -29
- package/lib/commonjs/providers/sse.js.map +1 -1
- package/lib/commonjs/providers/streamTimer.js +123 -0
- package/lib/commonjs/providers/streamTimer.js.map +1 -0
- package/lib/commonjs/providers/transport.js +107 -0
- package/lib/commonjs/providers/transport.js.map +1 -0
- package/lib/commonjs/providers/xhrStream.js +209 -0
- package/lib/commonjs/providers/xhrStream.js.map +1 -0
- package/lib/commonjs/session.js +245 -52
- package/lib/commonjs/session.js.map +1 -1
- package/lib/module/blocks/receipts.js +83 -24
- package/lib/module/blocks/receipts.js.map +1 -1
- package/lib/module/blocks/runId.js +26 -0
- package/lib/module/blocks/runId.js.map +1 -0
- package/lib/module/blocks/types.js +23 -2
- package/lib/module/blocks/types.js.map +1 -1
- package/lib/module/blocks/uiTool.js +424 -12
- package/lib/module/blocks/uiTool.js.map +1 -1
- package/lib/module/catalog/catalog.g.js +121 -43
- package/lib/module/catalog/catalog.g.js.map +1 -1
- package/lib/module/catalog/catalog.source.json +121 -45
- package/lib/module/catalog/catalog.types.g.js +40 -0
- package/lib/module/catalog/catalog.types.g.js.map +1 -0
- package/lib/module/catalog/normalizeParams.js +327 -0
- package/lib/module/catalog/normalizeParams.js.map +1 -0
- package/lib/module/catalog/signature.js +63 -0
- package/lib/module/catalog/signature.js.map +1 -0
- package/lib/module/catalog/snapshotReads.js +13 -6
- package/lib/module/catalog/snapshotReads.js.map +1 -1
- package/lib/module/catalog/toProviderTools.js +41 -5
- package/lib/module/catalog/toProviderTools.js.map +1 -1
- package/lib/module/context/buildContextPack.js +366 -19
- package/lib/module/context/buildContextPack.js.map +1 -1
- package/lib/module/effects/digest.js +29 -0
- package/lib/module/effects/digest.js.map +1 -0
- package/lib/module/effects/ledger.js +85 -1
- package/lib/module/effects/ledger.js.map +1 -1
- package/lib/module/engine/askGate.js +280 -0
- package/lib/module/engine/askGate.js.map +1 -0
- package/lib/module/engine/effectFor.js +83 -1
- package/lib/module/engine/effectFor.js.map +1 -1
- package/lib/module/engine/historyBudget.js +251 -0
- package/lib/module/engine/historyBudget.js.map +1 -0
- package/lib/module/engine/runAgentTurn.js +954 -88
- package/lib/module/engine/runAgentTurn.js.map +1 -1
- package/lib/module/engine/systemPrompt.js +71 -8
- package/lib/module/engine/systemPrompt.js.map +1 -1
- package/lib/module/engine/textToolCalls.js +270 -0
- package/lib/module/engine/textToolCalls.js.map +1 -0
- package/lib/module/index.js +7 -4
- package/lib/module/index.js.map +1 -1
- package/lib/module/policy/labels.js +19 -3
- package/lib/module/policy/labels.js.map +1 -1
- package/lib/module/policy/policy.js +6 -1
- package/lib/module/policy/policy.js.map +1 -1
- package/lib/module/policy/redact.js +42 -5
- package/lib/module/policy/redact.js.map +1 -1
- package/lib/module/providers/anthropic.js +341 -175
- package/lib/module/providers/anthropic.js.map +1 -1
- package/lib/module/providers/openai.js +230 -104
- package/lib/module/providers/openai.js.map +1 -1
- package/lib/module/providers/sse.js +160 -29
- package/lib/module/providers/sse.js.map +1 -1
- package/lib/module/providers/streamTimer.js +118 -0
- package/lib/module/providers/streamTimer.js.map +1 -0
- package/lib/module/providers/transport.js +103 -0
- package/lib/module/providers/transport.js.map +1 -0
- package/lib/module/providers/xhrStream.js +204 -0
- package/lib/module/providers/xhrStream.js.map +1 -0
- package/lib/module/session.js +228 -54
- package/lib/module/session.js.map +1 -1
- package/lib/typescript/blocks/receipts.d.ts +5 -10
- package/lib/typescript/blocks/receipts.d.ts.map +1 -1
- package/lib/typescript/blocks/runId.d.ts +20 -0
- package/lib/typescript/blocks/runId.d.ts.map +1 -0
- package/lib/typescript/blocks/types.d.ts +23 -2
- package/lib/typescript/blocks/types.d.ts.map +1 -1
- package/lib/typescript/blocks/uiTool.d.ts +11 -2
- package/lib/typescript/blocks/uiTool.d.ts.map +1 -1
- package/lib/typescript/catalog/catalog.g.d.ts.map +1 -1
- package/lib/typescript/catalog/catalog.types.g.d.ts +40 -0
- package/lib/typescript/catalog/catalog.types.g.d.ts.map +1 -0
- package/lib/typescript/catalog/normalizeParams.d.ts +43 -0
- package/lib/typescript/catalog/normalizeParams.d.ts.map +1 -0
- package/lib/typescript/catalog/signature.d.ts +29 -0
- package/lib/typescript/catalog/signature.d.ts.map +1 -0
- package/lib/typescript/catalog/snapshotReads.d.ts.map +1 -1
- package/lib/typescript/catalog/toProviderTools.d.ts +28 -15
- package/lib/typescript/catalog/toProviderTools.d.ts.map +1 -1
- package/lib/typescript/context/buildContextPack.d.ts +24 -2
- package/lib/typescript/context/buildContextPack.d.ts.map +1 -1
- package/lib/typescript/effects/digest.d.ts +10 -0
- package/lib/typescript/effects/digest.d.ts.map +1 -0
- package/lib/typescript/effects/ledger.d.ts +55 -0
- package/lib/typescript/effects/ledger.d.ts.map +1 -1
- package/lib/typescript/engine/askGate.d.ts +29 -0
- package/lib/typescript/engine/askGate.d.ts.map +1 -0
- package/lib/typescript/engine/effectFor.d.ts +6 -1
- package/lib/typescript/engine/effectFor.d.ts.map +1 -1
- package/lib/typescript/engine/historyBudget.d.ts +92 -0
- package/lib/typescript/engine/historyBudget.d.ts.map +1 -0
- package/lib/typescript/engine/runAgentTurn.d.ts +115 -1
- package/lib/typescript/engine/runAgentTurn.d.ts.map +1 -1
- package/lib/typescript/engine/systemPrompt.d.ts +41 -0
- package/lib/typescript/engine/systemPrompt.d.ts.map +1 -1
- package/lib/typescript/engine/textToolCalls.d.ts +58 -0
- package/lib/typescript/engine/textToolCalls.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +8 -5
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/policy/labels.d.ts.map +1 -1
- package/lib/typescript/policy/policy.d.ts.map +1 -1
- package/lib/typescript/policy/redact.d.ts.map +1 -1
- package/lib/typescript/providers/anthropic.d.ts +5 -0
- package/lib/typescript/providers/anthropic.d.ts.map +1 -1
- package/lib/typescript/providers/openai.d.ts +0 -16
- package/lib/typescript/providers/openai.d.ts.map +1 -1
- package/lib/typescript/providers/sse.d.ts +82 -1
- package/lib/typescript/providers/sse.d.ts.map +1 -1
- package/lib/typescript/providers/streamTimer.d.ts +59 -0
- package/lib/typescript/providers/streamTimer.d.ts.map +1 -0
- package/lib/typescript/providers/transport.d.ts +40 -0
- package/lib/typescript/providers/transport.d.ts.map +1 -0
- package/lib/typescript/providers/types.d.ts +116 -3
- package/lib/typescript/providers/types.d.ts.map +1 -1
- package/lib/typescript/providers/xhrStream.d.ts +43 -0
- package/lib/typescript/providers/xhrStream.d.ts.map +1 -0
- package/lib/typescript/session.d.ts +63 -2
- package/lib/typescript/session.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -120,7 +120,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
120
120
|
"summary": "Reads and writes the app's Jotai atoms: list registered atoms with change counts and writability, fetch one atom's current value, fetch the real prev/next values behind one recorded change, wipe the change timeline, and set a writable atom. Reach for it when on-screen data disagrees with the API or a value looks stale/wrong and the app uses Jotai. Only atoms the app explicitly passed to watchAtoms()/watchDefaultStoreAtoms() exist here — coverage is opt-in, so an empty list means nothing was registered, not that the app has no state. For the HISTORY of atom changes use get_events with sources:['jotai']; this tool's snapshot deliberately ships value-free markers and you fetch values on demand.",
|
|
121
121
|
"actions": [{
|
|
122
122
|
"action": "listAtoms",
|
|
123
|
-
"summary": "
|
|
123
|
+
"summary": "Compact reader for a remote driver: the registered atoms with light metadata. Each atom's value stays on the device unless `includeValues` is set, and `limit` caps how many are returned. `writable` says which can be set via setAtom. Each atom also carries `shape` — the item type of any list it holds — which is the cheap way to learn what an addition must look like.",
|
|
124
124
|
"params": {
|
|
125
125
|
"type": "object",
|
|
126
126
|
"properties": {
|
|
@@ -137,17 +137,21 @@ const CATALOG = exports.CATALOG = [{
|
|
|
137
137
|
},
|
|
138
138
|
"effect": "read",
|
|
139
139
|
"release": "works",
|
|
140
|
-
"description": "
|
|
140
|
+
"description": "Compact reader for a remote driver: the registered atoms with light metadata. Each atom's value stays on the device unless `includeValues` is set, and `limit` caps how many are returned. `writable` says which can be set via setAtom. Each atom also carries `shape` — the item type of any list it holds — which is the cheap way to learn what an addition must look like.",
|
|
141
141
|
"requires": ["@buoy-gg/jotai installed in the app", "watchAtoms(store, atoms) or watchDefaultStoreAtoms(atoms) called at app startup"]
|
|
142
142
|
}, {
|
|
143
143
|
"action": "getAtomValue",
|
|
144
|
-
"summary": "Fetch one atom's current value on demand, by label.",
|
|
144
|
+
"summary": "Fetch one atom's current value on demand, by label. Send `path` to get back ONE value instead of the whole atom (path:\"lines[id=seed-1].qty\") — do that whenever the value is big, because results are cut off at 24,000 characters. Also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. Read it before writing and match it exactly.",
|
|
145
145
|
"params": {
|
|
146
146
|
"type": "object",
|
|
147
147
|
"properties": {
|
|
148
148
|
"label": {
|
|
149
149
|
"type": "string",
|
|
150
150
|
"description": "Atom label exactly as registered — the object key in watchAtoms(store, { countAtom }), e.g. \"countAtom\". Get exact labels from listAtoms."
|
|
151
|
+
},
|
|
152
|
+
"path": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"description": "Return only the value at this path instead of the whole atom value."
|
|
151
155
|
}
|
|
152
156
|
},
|
|
153
157
|
"required": ["label"],
|
|
@@ -155,7 +159,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
155
159
|
},
|
|
156
160
|
"effect": "read",
|
|
157
161
|
"release": "works",
|
|
158
|
-
"description": "
|
|
162
|
+
"description": "Fetch one atom's current value on demand, by label. Send `path` to get back ONE value instead of the whole atom (path:\"lines[id=seed-1].qty\") — do that whenever the value is big, because results are cut off at 24,000 characters. Also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. Read it before writing and match it exactly.",
|
|
159
163
|
"requires": ["The label must already be registered via watchAtoms — call listAtoms first for exact labels"]
|
|
160
164
|
}, {
|
|
161
165
|
"action": "getChangeDetail",
|
|
@@ -177,7 +181,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
177
181
|
"requires": ["A change id from the jotai snapshot or get_events sources:['jotai']"]
|
|
178
182
|
}, {
|
|
179
183
|
"action": "setAtom",
|
|
180
|
-
"summary": "Write
|
|
184
|
+
"summary": "Write to a writable Jotai atom, named by its `label` — this mutates the running app's state. Three forms: `value` alone REPLACES; `value` with `merge:true` merges a patch under the typed-edit rule (change existing fields to the same type; a list may grow or shrink but every item must have the fields the others have); `path`+`value` sets ONE value with no nesting to get wrong (path:\"lines[id=seed-1].qty\"). To change one item of a list, address it by its own id — {\"lines\":{\"seed-1\":{\"qty\":5}}} changes one, {\"lines\":{\"seed-2\":null}} removes one, a new id adds one; items you don't name are untouched. A plain array replaces the whole list. A refused merge names the field and, when the problem was depth, hands back the corrected patch — fix it that way rather than reaching for `force`, which writes raw and can crash the screen.",
|
|
181
185
|
"params": {
|
|
182
186
|
"type": "object",
|
|
183
187
|
"properties": {
|
|
@@ -187,6 +191,18 @@ const CATALOG = exports.CATALOG = [{
|
|
|
187
191
|
},
|
|
188
192
|
"value": {
|
|
189
193
|
"description": "The new value — any JSON (number, string, boolean, object, array, null). Passed straight to store.set(atom, value); it REPLACES the value, no merging."
|
|
194
|
+
},
|
|
195
|
+
"merge": {
|
|
196
|
+
"type": "boolean",
|
|
197
|
+
"description": "Merge `value` into the atom's current value instead of replacing it, under the typed-edit rule. Default false."
|
|
198
|
+
},
|
|
199
|
+
"path": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"description": "Set ONE value inside the atom, named by its path in the CURRENT value — read it first and copy the path from `shape`. Always a merge. A list step is written [field=value] or [0] and resolves to that item's own id."
|
|
202
|
+
},
|
|
203
|
+
"force": {
|
|
204
|
+
"type": "boolean",
|
|
205
|
+
"description": "Bypass the typed-edit safety and write raw. This is what crashes screens; use ONLY to deliberately replace the whole shape."
|
|
190
206
|
}
|
|
191
207
|
},
|
|
192
208
|
"required": ["label", "value"],
|
|
@@ -194,7 +210,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
194
210
|
},
|
|
195
211
|
"effect": "write",
|
|
196
212
|
"release": "works",
|
|
197
|
-
"description": "
|
|
213
|
+
"description": "Write to a writable Jotai atom, named by its `label` — this mutates the running app's state. Three forms: `value` alone REPLACES; `value` with `merge:true` merges a patch under the typed-edit rule (change existing fields to the same type; a list may grow or shrink but every item must have the fields the others have); `path`+`value` sets ONE value with no nesting to get wrong (path:\"lines[id=seed-1].qty\"). To change one item of a list, address it by its own id — {\"lines\":{\"seed-1\":{\"qty\":5}}} changes one, {\"lines\":{\"seed-2\":null}} removes one, a new id adds one; items you don't name are untouched. A plain array replaces the whole list. A refused merge names the field and, when the problem was depth, hands back the corrected patch — fix it that way rather than reaching for `force`, which writes raw and can crash the screen.",
|
|
198
214
|
"requires": ["listAtoms reports writable:true for this label (atom has a write fn AND the store exposes set())"]
|
|
199
215
|
}, {
|
|
200
216
|
"action": "clearEvents",
|
|
@@ -243,7 +259,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
243
259
|
"description": "Returns `{path, params, at, source}` where `source` is \"event\" (the newest navigation event won) or \"stack\" (a freshly launched app that has not navigated yet — the focused stack item answered). Returns `{path:null}` when the app has no <RouteTracker/> and no live navigation stack: a null path means \"unknown\", never \"/\". Prefer this over the full getSnapshot when all you need is where the app is right now."
|
|
244
260
|
}, {
|
|
245
261
|
"action": "navigate",
|
|
246
|
-
"summary": "Navigate the device to a concrete path (pushes by default; replace:true swaps the current screen).",
|
|
262
|
+
"summary": "Navigate the device to a concrete path (pushes by default; replace:true swaps the current screen). ALSO HOW YOU LOAD DATA THE APP HAS NOT FETCHED YET: go to the screen that fetches it, waitFor something on it, then read. Moving the user's screen to go and look is not a change to the app — you do not need to ask, and you do not have to navigate back.",
|
|
247
263
|
"params": {
|
|
248
264
|
"type": "object",
|
|
249
265
|
"properties": {
|
|
@@ -261,7 +277,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
261
277
|
},
|
|
262
278
|
"effect": "write",
|
|
263
279
|
"release": "works",
|
|
264
|
-
"description": "Calls expo-router's router.navigate(path), or router.replace(path) when replace:true. On bare React Navigation (no expo-router) it falls back to navigating by SCREEN NAME through the captured container ref — pass the screen's name ('Settings' or '/Settings'; replace is ignored there), and nested navigators are resolved automatically. The fallback needs <RouteTracker/> mounted inside the NavigationContainer. Pass a CONCRETE path — sitemap entries are templates ('/pokemon/[id]'), so resolve dynamic segments to real values ('/pokemon/25') first; a query string is allowed ('/pokemon/25?tab=stats'). Push is the default so navigate-then-back flows keep working; pass replace:true when you mean 'leave this screen' (e.g. resetting to '/'), otherwise repeated 'go to home' stacks another '/' on top. Throws 'navigate requires a path param' when path is missing/empty, and 'expo-router is not available on this device' on React-Navigation-only apps. Returns { navigated: path, replaced: boolean }. The return only proves the router call was made, not that the screen rendered — re-read the snapshot's stack to confirm.",
|
|
280
|
+
"description": "Calls expo-router's router.navigate(path), or router.replace(path) when replace:true. On bare React Navigation (no expo-router) it falls back to navigating by SCREEN NAME through the captured container ref — pass the screen's name ('Settings' or '/Settings'; replace is ignored there), and nested navigators are resolved automatically. The fallback needs <RouteTracker/> mounted inside the NavigationContainer. Pass a CONCRETE path — sitemap entries are templates ('/pokemon/[id]'), so resolve dynamic segments to real values ('/pokemon/25') first; a query string is allowed ('/pokemon/25?tab=stats'). Push is the default so navigate-then-back flows keep working; pass replace:true when you mean 'leave this screen' (e.g. resetting to '/'), otherwise repeated 'go to home' stacks another '/' on top. Throws 'navigate requires a path param' when path is missing/empty, and 'expo-router is not available on this device' on React-Navigation-only apps. Returns { navigated: path, replaced: boolean }. The return only proves the router call was made, not that the screen rendered — re-read the snapshot's stack to confirm, and use highlight-updates.waitFor before reading anything the new screen has to FETCH. This is the main tool for filling a gap in what you can read: the query cache, the request list and the events timeline only ever hold what the app has ALREADY done, so when the data you were asked about was never loaded, the answer is to go to the screen that loads it rather than to report the gap.",
|
|
265
281
|
"requires": ["expo-router installed and initialized in the app", "<FloatingDevTools> mounted (adapter registered)"]
|
|
266
282
|
}, {
|
|
267
283
|
"action": "stackGoBack",
|
|
@@ -400,17 +416,21 @@ const CATALOG = exports.CATALOG = [{
|
|
|
400
416
|
},
|
|
401
417
|
"effect": "read",
|
|
402
418
|
"release": "works",
|
|
403
|
-
"description": "The entry point — call this first to learn valid storeName values for getStoreState/setState. Returns {stores:[{name, changes, isPersisted, persistName?, keys?|currentState?}], total, returned, includedValues}. Compact by default: `keys` is Object.keys() of the state (undefined when the state isn't a plain object). includeValues:true swaps `keys` for the full `currentState` object and can be very large — the state objects here are NOT wire-budget-capped the way the streaming snapshot is. `changes` is that store's recorded state-change count, reset to 0 by clearEvents. Names come from the app: the object keys passed to watchStores({counterStore: useCounterStore}) or the `name` option of buoyDevTools(). An empty stores array means the app never instrumented its stores, not that it has none.",
|
|
419
|
+
"description": "The entry point — call this first to learn valid storeName values for getStoreState/setState. Returns {stores:[{name, changes, isPersisted, persistName?, keys?|currentState?}], total, returned, includedValues}. Compact by default: `keys` is Object.keys() of the state (undefined when the state isn't a plain object). includeValues:true swaps `keys` for the full `currentState` object and can be very large — the state objects here are NOT wire-budget-capped the way the streaming snapshot is. `changes` is that store's recorded state-change count, reset to 0 by clearEvents. Names come from the app: the object keys passed to watchStores({counterStore: useCounterStore}) or the `name` option of buoyDevTools(). An empty stores array means the app never instrumented its stores, not that it has none. Every read here also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. It is tiny and does not grow with the data, so it survives the 24,000-character cut when the data itself does not — read it before writing, and match it exactly. A list whose `item` is missing is EMPTY, which means nothing in the running app knows what belongs in it; anything you add there cannot be checked and will be accepted as-is, so say so rather than inventing fields.",
|
|
404
420
|
"requires": ["@buoy-gg/zustand installed and the zustand tool registered with FloatingDevTools", "the app calls watchStores({...}) or wraps stores with buoyDevTools() — otherwise the registry is empty"]
|
|
405
421
|
}, {
|
|
406
422
|
"action": "getStoreState",
|
|
407
|
-
"summary": "Fetch one store's full current state object on demand, by store name.",
|
|
423
|
+
"summary": "Fetch one store's full current state object on demand, by store name. Send `path` to get back ONE value instead of the whole store (path:\"lines[lineId=seed-1].qty\") — do that whenever the store is big, because results are cut off at 24,000 characters.",
|
|
408
424
|
"params": {
|
|
409
425
|
"type": "object",
|
|
410
426
|
"properties": {
|
|
411
427
|
"storeName": {
|
|
412
428
|
"type": "string",
|
|
413
429
|
"description": "Registered store name exactly as listStores reports it (e.g. \"counterStore\", \"authStore\", \"cartStore\"). Case-sensitive; no fuzzy matching."
|
|
430
|
+
},
|
|
431
|
+
"path": {
|
|
432
|
+
"type": "string",
|
|
433
|
+
"description": "Return only the value at this path instead of the whole payload. Use it when you only need one field, and ALWAYS when the payload is big: results are cut off at 24,000 characters, and a field you never saw is a field you will guess the shape of. The path must match the CURRENT data — read `shape` first rather than assuming a wrapper. On a bad path it answers with the field names that do exist, and with the path that would have worked if that field lives somewhere else."
|
|
414
434
|
}
|
|
415
435
|
},
|
|
416
436
|
"required": ["storeName"],
|
|
@@ -418,7 +438,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
418
438
|
},
|
|
419
439
|
"effect": "read",
|
|
420
440
|
"release": "works",
|
|
421
|
-
"description": "Use when listStores' compact `keys` view isn't enough, or when the streaming snapshot showed the {__buoyStateOnDevice:true} marker (states over 16KB are withheld from the per-snapshot wire). Returns {found:true, storeName, currentState} — or {found:false, reason:\"missing storeName\"} / {found:false, reason:\"unknown storeName\"}, which is a plain answer, not an error. currentState is read live via store.api.getState(); if that throws it comes back undefined. Over the 8MB detail cap it returns {__buoyTruncated:true, note} instead, or the STATE_ON_DEVICE marker when the state isn't JSON-serializable. Note that action functions living in state (increment, reset, …) do not survive the JSON wire — what you read back is the data half of the store only.",
|
|
441
|
+
"description": "Use when listStores' compact `keys` view isn't enough, or when the streaming snapshot showed the {__buoyStateOnDevice:true} marker (states over 16KB are withheld from the per-snapshot wire). Returns {found:true, storeName, currentState} — or {found:false, reason:\"missing storeName\"} / {found:false, reason:\"unknown storeName\"}, which is a plain answer, not an error. currentState is read live via store.api.getState(); if that throws it comes back undefined. Over the 8MB detail cap it returns {__buoyTruncated:true, note} instead, or the STATE_ON_DEVICE marker when the state isn't JSON-serializable. Note that action functions living in state (increment, reset, …) do not survive the JSON wire — what you read back is the data half of the store only. Every read here also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. It is tiny and does not grow with the data, so it survives the 24,000-character cut when the data itself does not — read it before writing, and match it exactly. A list whose `item` is missing is EMPTY, which means nothing in the running app knows what belongs in it; anything you add there cannot be checked and will be accepted as-is, so say so rather than inventing fields. Send `path` to get back ONE value instead of the whole store (path:\"lines[lineId=seed-1].qty\") — do that whenever the store is big, because results are cut off at 24,000 characters.",
|
|
422
442
|
"requires": ["the store must already be registered — get the exact name from listStores"]
|
|
423
443
|
}, {
|
|
424
444
|
"action": "getChangeDetail",
|
|
@@ -440,7 +460,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
440
460
|
"requires": ["a change id from the zustand change log (the tool snapshot, or get_events sources:['zustand'])"]
|
|
441
461
|
}, {
|
|
442
462
|
"action": "setState",
|
|
443
|
-
"summary": "
|
|
463
|
+
"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.",
|
|
444
464
|
"params": {
|
|
445
465
|
"type": "object",
|
|
446
466
|
"properties": {
|
|
@@ -457,14 +477,25 @@ const CATALOG = exports.CATALOG = [{
|
|
|
457
477
|
"type": "boolean",
|
|
458
478
|
"description": "true replaces the whole state — including the store's action functions, which breaks every button wired to them until the app reloads. Buoy sends false for you unless you explicitly pass true, so a partial merge is the safe path: pass only the fields you intend to change (e.g. {\"count\": 5}). The ADAPTER's own default is true; this is a deliberately safer default for agent calls.",
|
|
459
479
|
"default": false
|
|
480
|
+
},
|
|
481
|
+
"force": {
|
|
482
|
+
"type": "boolean",
|
|
483
|
+
"description": "Bypass the typed-edit safety on a merge and write raw. Default false — a violating merge is refused."
|
|
484
|
+
},
|
|
485
|
+
"path": {
|
|
486
|
+
"type": "string",
|
|
487
|
+
"description": "Set ONE value, named by its full path in the CURRENT data: path:\"<the real path>\", value:<new value>. READ THE DATA FIRST AND COPY THE PATH FROM IT — `shape` on getStoreState/listStores prints it. A path is only safer than a hand-nested `data` if it matches the payload you are actually looking at; if the field is at the top level the path is just \"name\", and inventing a wrapper that is not there is refused. A list step is written [field=value] or [0] and resolves to that item's own id, so it still means the same row if the list changed. Always a merge, and always through the same typed-edit guard as `data`. Requires `value`; send `data` OR `path`+`value`, not both."
|
|
488
|
+
},
|
|
489
|
+
"value": {
|
|
490
|
+
"description": "The value `path` is set to. Required whenever `path` is sent. null is a real value (only allowed if the field is already nullable), not a delete."
|
|
460
491
|
}
|
|
461
492
|
},
|
|
462
|
-
"required": ["storeName"
|
|
493
|
+
"required": ["storeName"],
|
|
463
494
|
"additionalProperties": false
|
|
464
495
|
},
|
|
465
496
|
"effect": "destructive",
|
|
466
497
|
"release": "works",
|
|
467
|
-
"description": "Powers time-travel / reset / 'put the app in this state' from a dashboard. Calls the store's real setState(state, replace ?? true). THE DEFAULT IS DESTRUCTIVE: `replace` defaults to TRUE, so any key missing from `state` is deleted — and Zustand stores conventionally keep their action functions in state (increment, login, addToCart). Functions cannot cross the sync wire, so a JSON `state` object can never carry them back; a default-replace leaves every `useStore(s => s.increment)` call site reading undefined and the app broken until reload. Buoy's own Time Machine restore avoids this by re-grafting the live store's functions onto the snapshot before replacing (packages/zustand/src/zustand/utils/snapshotProvider.ts:8-12) — this raw action does NOT do that. For a QA-facing tweak always pass replace:false to merge just the fields you're changing. Returns {ok:true}, or {ok:false, error:\"Missing storeName.\"} / {ok:false, error:'No store named \"X\".'}. The write also lands in the change log as a normal recorded change.",
|
|
498
|
+
"description": "Powers time-travel / reset / 'put the app in this state' from a dashboard. Calls the store's real setState(state, replace ?? true). THE DEFAULT IS DESTRUCTIVE: `replace` defaults to TRUE, so any key missing from `state` is deleted — and Zustand stores conventionally keep their action functions in state (increment, login, addToCart). Functions cannot cross the sync wire, so a JSON `state` object can never carry them back; a default-replace leaves every `useStore(s => s.increment)` call site reading undefined and the app broken until reload. Buoy's own Time Machine restore avoids this by re-grafting the live store's functions onto the snapshot before replacing (packages/zustand/src/zustand/utils/snapshotProvider.ts:8-12) — this raw action does NOT do that. For a QA-facing tweak always pass replace:false to merge just the fields you're changing. Returns {ok:true}, or {ok:false, error:\"Missing storeName.\"} / {ok:false, error:'No store named \"X\".'}. The write also lands in the change log as a normal recorded change. A write that adds the first item to an EMPTY list comes back ok with `unchecked`: the list had no items, so nothing knew its item shape and yours was not verified. Treat that as a warning to check the screen, not as a pass. 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.",
|
|
468
499
|
"requires": ["the store must be registered — get the exact name from listStores", "the store's registered setState handle: watchStores registers the raw setState, buoyDevTools registers its instrumented set (both apply the write)"]
|
|
469
500
|
}, {
|
|
470
501
|
"action": "rehydrate",
|
|
@@ -761,10 +792,10 @@ const CATALOG = exports.CATALOG = [{
|
|
|
761
792
|
}, {
|
|
762
793
|
"toolId": "query",
|
|
763
794
|
"title": "React Query",
|
|
764
|
-
"summary": "
|
|
795
|
+
"summary": "THE way to change what a server-backed screen shows. Most screens that render API data render this cache, so \"edit what I'm looking at\" on such a screen means setQueryData on the query that is mounted (observers > 0), not a store and not the API. Also reads and mutates the rest of the app's live TanStack Query (React Query) cache on the device: list every query with status/staleness/observers/error, pull one query's real cached data, and then refetch / invalidate / reset / remove / overwrite it, simulate a query error or a perpetual loading state, clear the whole query or mutation cache, and flip TanStack's onlineManager to fake offline. Reach for it when data on screen is stale, wrong, or missing and you need to know whether the CACHE or the API is at fault (network.getSnapshot answers the API half), and for the QA moves — force the error view (triggerError), the loading view (triggerLoading), a specific payload (setQueryData). A cache edit lasts until the next successful refetch; when the change must survive a refetch or a reload, put a network override on the request instead and invalidate. Everything here is the current cache — for the history of query updates over time use the events tool with sources:['react-query'].",
|
|
765
796
|
"actions": [{
|
|
766
797
|
"action": "listQueries",
|
|
767
|
-
"summary": "List every query in the cache — hash, key, status, staleness, observer count, last-updated, error message. The token-cheap cache reader; start here.",
|
|
798
|
+
"summary": "List every query in the cache — hash, key, status, staleness, observer count, last-updated, error message. The token-cheap cache reader; start here. Send `staleOnly:true` to see only stale queries. THIS IS A CACHE, NOT AN INVENTORY: it holds only what this app has already fetched in this session, so a short list means the user has not visited those screens yet, never that the data does not exist.",
|
|
768
799
|
"params": {
|
|
769
800
|
"type": "object",
|
|
770
801
|
"properties": {
|
|
@@ -785,17 +816,21 @@ const CATALOG = exports.CATALOG = [{
|
|
|
785
816
|
},
|
|
786
817
|
"effect": "read",
|
|
787
818
|
"release": "works",
|
|
788
|
-
"description": "Projects each live query to light fields: queryHash, queryKey, status (one of fresh/stale/fetching/error/inactive/paused/disabled, from getQueryStatusLabel), fetchStatus, isStale, observers, updatedAt (state.dataUpdatedAt), and error.message when present. Returns {queries, total, returned, includedData}. Sorted most-recently-updated first. Safe to call repeatedly — unlike the full dehydrated snapshot, the heavy cached data stays on the device by default. TWO TRAPS: (1) the adapter has NO default limit — omit it and you get every query in the cache; (2) includeData:true returns q.state.data RAW and UNCAPPED (no 16KB wire marker, no 8MB cap like getQueryData), so it can blow the wire/token budget on a big cache — prefer getQueryData for one query's payload. The queryHash of each row is the handle every other action takes.",
|
|
819
|
+
"description": "Projects each live query to light fields: queryHash, queryKey, status (one of fresh/stale/fetching/error/inactive/paused/disabled, from getQueryStatusLabel), fetchStatus, isStale, observers, updatedAt (state.dataUpdatedAt), and error.message when present. Returns {queries, total, returned, includedData}. Sorted most-recently-updated first. Safe to call repeatedly — unlike the full dehydrated snapshot, the heavy cached data stays on the device by default. TWO TRAPS: (1) the adapter has NO default limit — omit it and you get every query in the cache; (2) includeData:true returns q.state.data RAW and UNCAPPED (no 16KB wire marker, no 8MB cap like getQueryData), so it can blow the wire/token budget on a big cache — prefer getQueryData for one query's payload. The queryHash of each row is the handle every other action takes. What is NOT here has not been fetched yet — the cache fills as the user visits screens — so treat a missing key as a screen to go to (route-events.navigate, then highlight-updates.waitFor, then read again), not as an absence to report. Every read here also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. It is tiny and does not grow with the data, so it survives the 24,000-character cut when the data itself does not — read it before writing, and match it exactly. A list whose `item` is missing is EMPTY, which means nothing in the running app knows what belongs in it; anything you add there cannot be checked and will be accepted as-is, so say so rather than inventing fields. Send `staleOnly:true` to see only stale queries.",
|
|
789
820
|
"requires": ["QueryClientProvider above <FloatingDevTools/>", "@tanstack/react-query v5"]
|
|
790
821
|
}, {
|
|
791
822
|
"action": "getQueryData",
|
|
792
|
-
"summary": "Get ONE query's real cached data by queryHash — the size-guarded channel for a payload the snapshot replaced with a marker.",
|
|
823
|
+
"summary": "Get ONE query's real cached data by queryHash — the size-guarded channel for a payload the snapshot replaced with a marker. Send `path` to get back ONE value instead of the whole payload (path:\"item.name\", path:\"results[name=pikachu].url\") — do that whenever the payload is big, because results are cut off at 24,000 characters.",
|
|
793
824
|
"params": {
|
|
794
825
|
"type": "object",
|
|
795
826
|
"properties": {
|
|
796
827
|
"queryHash": {
|
|
797
828
|
"type": "string",
|
|
798
829
|
"description": "The target query's hash from listQueries — TanStack's default hash is the JSON-stringified key, e.g. '[\"todos\",{\"page\":1}]'. Tolerated if omitted (returns found:false) but then the call does nothing useful."
|
|
830
|
+
},
|
|
831
|
+
"path": {
|
|
832
|
+
"type": "string",
|
|
833
|
+
"description": "Return only the value at this path instead of the whole payload. Use it when you only need one field, and ALWAYS when the payload is big: results are cut off at 24,000 characters, and a field you never saw is a field you will guess the shape of. The path must match the CURRENT data — read `shape` first rather than assuming a wrapper. On a bad path it answers with the field names that do exist, and with the path that would have worked if that field lives somewhere else."
|
|
799
834
|
}
|
|
800
835
|
},
|
|
801
836
|
"required": ["queryHash"],
|
|
@@ -803,7 +838,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
803
838
|
},
|
|
804
839
|
"effect": "read",
|
|
805
840
|
"release": "works",
|
|
806
|
-
"description": "Returns {found:true, queryHash, data} where data is query.state.data capped at 8MB (over that you get {__buoyTruncated:true}; non-JSON-serializable values such as circular refs or bigint come back as {__buoyUnserializable:true}). Use this when a snapshot or detail pane shows the {__buoyDataOnDevice:true} marker — streamed snapshots strip anything over 16KB. Never throws: an unknown or missing hash returns {found:false, reason:'unknown queryHash'|'missing queryHash'}.",
|
|
841
|
+
"description": "Returns {found:true, queryHash, data} where data is query.state.data capped at 8MB (over that you get {__buoyTruncated:true}; non-JSON-serializable values such as circular refs or bigint come back as {__buoyUnserializable:true}). Use this when a snapshot or detail pane shows the {__buoyDataOnDevice:true} marker — streamed snapshots strip anything over 16KB. Never throws: an unknown or missing hash returns {found:false, reason:'unknown queryHash'|'missing queryHash'}. Every read here also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. It is tiny and does not grow with the data, so it survives the 24,000-character cut when the data itself does not — read it before writing, and match it exactly. A list whose `item` is missing is EMPTY, which means nothing in the running app knows what belongs in it; anything you add there cannot be checked and will be accepted as-is, so say so rather than inventing fields. Send `path` to get back ONE value instead of the whole payload (path:\"item.name\", path:\"results[name=pikachu].url\") — do that whenever the payload is big, because results are cut off at 24,000 characters.",
|
|
807
842
|
"requires": ["QueryClientProvider above <FloatingDevTools/>"]
|
|
808
843
|
}, {
|
|
809
844
|
"action": "refetch",
|
|
@@ -825,7 +860,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
825
860
|
"requires": ["QueryClientProvider above <FloatingDevTools/>", "the query must have a queryFn to actually fetch"]
|
|
826
861
|
}, {
|
|
827
862
|
"action": "invalidate",
|
|
828
|
-
"summary": "Mark a query stale and refetch it if it has active observers — the normal 'this data is out of date' fix.",
|
|
863
|
+
"summary": "Mark a query stale and refetch it if it has active observers — the normal 'this data is out of date' fix. Takes the query's `queryHash`.",
|
|
829
864
|
"params": {
|
|
830
865
|
"type": "object",
|
|
831
866
|
"properties": {
|
|
@@ -839,11 +874,11 @@ const CATALOG = exports.CATALOG = [{
|
|
|
839
874
|
},
|
|
840
875
|
"effect": "write",
|
|
841
876
|
"release": "works",
|
|
842
|
-
"description": "Looks the query up by hash, then passes the Query itself to queryClient.invalidateQueries() as the filter. Because the filter carries only queryKey with no exact:true, matching is a NON-EXACT prefix match: invalidating '[\\\"todos\\\"]' also invalidates '[\\\"todos\\\",{\\\"page\\\":1}]' and any other key that extends it. Mounted (observed) queries refetch immediately, so this can fire real API calls; inactive ones just go stale. Prefer this over refetch when you want the app's own screens to re-render with fresh data. THROWS for an unknown hash.",
|
|
877
|
+
"description": "Looks the query up by hash, then passes the Query itself to queryClient.invalidateQueries() as the filter. Because the filter carries only queryKey with no exact:true, matching is a NON-EXACT prefix match: invalidating '[\\\"todos\\\"]' also invalidates '[\\\"todos\\\",{\\\"page\\\":1}]' and any other key that extends it. Mounted (observed) queries refetch immediately, so this can fire real API calls; inactive ones just go stale. Prefer this over refetch when you want the app's own screens to re-render with fresh data. THROWS for an unknown hash. Takes the query's `queryHash`.",
|
|
843
878
|
"requires": ["QueryClientProvider above <FloatingDevTools/>"]
|
|
844
879
|
}, {
|
|
845
880
|
"action": "reset",
|
|
846
|
-
"summary": "Reset a query to its initial state — DISCARDS its cached data, then refetches if it is active.",
|
|
881
|
+
"summary": "Reset a query to its initial state — DISCARDS its cached data, then refetches if it is active. Takes the query's `queryHash`.",
|
|
847
882
|
"params": {
|
|
848
883
|
"type": "object",
|
|
849
884
|
"properties": {
|
|
@@ -857,11 +892,11 @@ const CATALOG = exports.CATALOG = [{
|
|
|
857
892
|
},
|
|
858
893
|
"effect": "destructive",
|
|
859
894
|
"release": "works",
|
|
860
|
-
"description": "queryClient.resetQueries() with the Query as filter, so the same NON-EXACT prefix match as invalidate applies (resetting '[\\\"todos\\\"]' also resets deeper todos keys). Unlike invalidate this throws the cached value away and reverts to initialData/pending — screens bound to it will flash their loading state. There is no undo: the data only comes back if the query has a queryFn and an active observer to refetch it. THROWS for an unknown hash.",
|
|
895
|
+
"description": "queryClient.resetQueries() with the Query as filter, so the same NON-EXACT prefix match as invalidate applies (resetting '[\\\"todos\\\"]' also resets deeper todos keys). Unlike invalidate this throws the cached value away and reverts to initialData/pending — screens bound to it will flash their loading state. There is no undo: the data only comes back if the query has a queryFn and an active observer to refetch it. THROWS for an unknown hash. Takes the query's `queryHash`.",
|
|
861
896
|
"requires": ["QueryClientProvider above <FloatingDevTools/>"]
|
|
862
897
|
}, {
|
|
863
898
|
"action": "remove",
|
|
864
|
-
"summary": "Delete a query from the cache entirely — the entry, its data, and its state are gone.",
|
|
899
|
+
"summary": "Delete a query from the cache entirely — the entry, its data, and its state are gone. Takes the query's `queryHash`.",
|
|
865
900
|
"params": {
|
|
866
901
|
"type": "object",
|
|
867
902
|
"properties": {
|
|
@@ -875,11 +910,11 @@ const CATALOG = exports.CATALOG = [{
|
|
|
875
910
|
},
|
|
876
911
|
"effect": "destructive",
|
|
877
912
|
"release": "works",
|
|
878
|
-
"description": "queryClient.removeQueries() with the Query as filter — again a NON-EXACT prefix match, so removing '[\\\"todos\\\"]' removes every key that extends it. Harsher than reset: the cache entry itself disappears rather than reverting to pending. Irreversible; a mounted component will create a brand-new entry and fetch from scratch on its next render. THROWS for an unknown hash. Returns nothing.",
|
|
913
|
+
"description": "queryClient.removeQueries() with the Query as filter — again a NON-EXACT prefix match, so removing '[\\\"todos\\\"]' removes every key that extends it. Harsher than reset: the cache entry itself disappears rather than reverting to pending. Irreversible; a mounted component will create a brand-new entry and fetch from scratch on its next render. THROWS for an unknown hash. Returns nothing. Takes the query's `queryHash`.",
|
|
879
914
|
"requires": ["QueryClientProvider above <FloatingDevTools/>"]
|
|
880
915
|
}, {
|
|
881
916
|
"action": "setQueryData",
|
|
882
|
-
"summary": "
|
|
917
|
+
"summary": "Change a query's cached data — takes the queryKey ARRAY, not the hash. The instant, on-screen edit for anything a mounted query renders. To change some fields send merge:true and ONLY those fields (deep-merged into the cached value); never paste a whole payload back — large getQueryData results are truncated, and a replace that drops fields the screen renders is refused. Reverts on the next successful refetch — say so; use a network override when it must not. To change a single field, `path` + `value` is the safest form (path:\"item.name\", value:\"test123\") — always a merge, and no nesting to get wrong. Read the data first either way: the path has to match the shape that is actually there.",
|
|
883
918
|
"params": {
|
|
884
919
|
"type": "object",
|
|
885
920
|
"properties": {
|
|
@@ -888,23 +923,38 @@ const CATALOG = exports.CATALOG = [{
|
|
|
888
923
|
"description": "The query's key ARRAY exactly as listQueries returns it, e.g. [\"todos\",{\"page\":1}] — NOT the queryHash string. An unknown key creates a new cache entry."
|
|
889
924
|
},
|
|
890
925
|
"data": {
|
|
891
|
-
"description": "
|
|
926
|
+
"description": "With merge:true: only the fields to change, nested to match the current shape. Without: the complete new value. If you are changing a single field, use `path`+`value` instead — it cannot be nested wrongly."
|
|
892
927
|
},
|
|
893
928
|
"queryHash": {
|
|
894
929
|
"type": "string",
|
|
895
930
|
"description": "Declared by the adapter's param type but never read by the handler — passing it has no effect."
|
|
931
|
+
},
|
|
932
|
+
"merge": {
|
|
933
|
+
"type": "boolean",
|
|
934
|
+
"description": "Deep-merge `data` into the cached value as a TYPED LEAF EDIT — like the React Query devtools editor. Change the value of a field that already exists, to the SAME type: no new object fields, no type changes, no null-ing a rendered list/object. A LIST may gain or lose items, but every item you send must have the same fields as the items already in it — to change one item, resend the WHOLE list with just that item changed. Refused with the exact field on a violation. Default false."
|
|
935
|
+
},
|
|
936
|
+
"force": {
|
|
937
|
+
"type": "boolean",
|
|
938
|
+
"description": "Bypass the typed-edit safety and write raw — can add/remove fields, change types, resize lists. This is what crashes screens; use ONLY when you deliberately mean to replace the whole shape. Default false."
|
|
939
|
+
},
|
|
940
|
+
"path": {
|
|
941
|
+
"type": "string",
|
|
942
|
+
"description": "Set ONE value, named by its full path in the CURRENT data: path:\"<the real path>\", value:<new value>. READ THE DATA FIRST AND COPY THE PATH FROM IT — `shape` on getQueryData/listQueries prints it. A path is only safer than a hand-nested `data` if it matches the payload you are actually looking at; if the field is at the top level the path is just \"name\", and inventing a wrapper that is not there is refused. A list step is written [field=value] or [0] and resolves to that item's own id, so it still means the same row if the list changed. Always a merge, and always through the same typed-edit guard as `data`. Requires `value`; send `data` OR `path`+`value`, not both."
|
|
943
|
+
},
|
|
944
|
+
"value": {
|
|
945
|
+
"description": "The value `path` is set to. Required whenever `path` is sent. null is a real value (only allowed if the field is already nullable), not a delete."
|
|
896
946
|
}
|
|
897
947
|
},
|
|
898
|
-
"required": ["queryKey"
|
|
948
|
+
"required": ["queryKey"],
|
|
899
949
|
"additionalProperties": false
|
|
900
950
|
},
|
|
901
951
|
"effect": "write",
|
|
902
952
|
"release": "works",
|
|
903
|
-
"description": "queryClient.setQueryData(queryKey,
|
|
953
|
+
"description": "queryClient.setQueryData(queryKey, value, {updatedAt: Date.now()}). With merge:true the value written is the CURRENT cached data deep-merged with `data` (plain objects merge key by key, arrays and primitives are replaced), so `{name:'test123'}` changes one field of a 30KB payload. TO CHANGE ONE ITEM IN A LIST, address it by its own id rather than resending the array: if rows carry an id, `{results:{pikachu:{name:'test123'}}}` edits that row and leaves every other row untouched. This is the ONLY correct form when the read was capped and you did not see every row — a plain array REPLACES the list, so a one-item array deletes the rest. Without merge it REPLACES — and if the cached value is an object whose top-level keys `data` lacks, the call is refused with {ok:false, error, missingKeys} unless force:true, because a screen that renders the dropped fields crashes. Returns {ok:true, mode:'merge'|'replace'}. Gotchas: (1) it keys off queryKey, the actual array from a listQueries row (e.g. ['todos',{page:1}]) — the declared param type also mentions queryHash but the handler IGNORES it; (2) if that key is not in the cache, setQueryData CREATES a new entry with no queryFn, which then errors if anything refetches it. The injected value is overwritten by the next successful refetch. When a merge is refused for writing at the wrong depth, the refusal carries `suggestedData`: the SAME edit rebuilt at the right depth. Send that back as `data` rather than re-deriving it. To change a single field, `path` + `value` is the safest form (path:\"item.name\", value:\"test123\") — always a merge, and no nesting to get wrong. Read the data first either way: the path has to match the shape that is actually there.",
|
|
904
954
|
"requires": ["QueryClientProvider above <FloatingDevTools/>"]
|
|
905
955
|
}, {
|
|
906
956
|
"action": "triggerError",
|
|
907
|
-
"summary": "Force a query into error status with a fake Error, to exercise the app's error UI.",
|
|
957
|
+
"summary": "Force a query into error status with a fake Error, to exercise the app's error UI. The QA move for 'show me this screen's error state'; undo with restoreError.",
|
|
908
958
|
"params": {
|
|
909
959
|
"type": "object",
|
|
910
960
|
"properties": {
|
|
@@ -940,7 +990,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
940
990
|
"requires": ["QueryClientProvider above <FloatingDevTools/>"]
|
|
941
991
|
}, {
|
|
942
992
|
"action": "triggerLoading",
|
|
943
|
-
"summary": "Pin a query in a permanent loading/suspense state to exercise skeletons and spinners. MUST be undone.",
|
|
993
|
+
"summary": "Pin a query in a permanent loading/suspense state to exercise skeletons and spinners. MUST be undone. The QA move for 'show me this screen's loading state'; undo with restoreLoading.",
|
|
944
994
|
"params": {
|
|
945
995
|
"type": "object",
|
|
946
996
|
"properties": {
|
|
@@ -1373,7 +1423,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
1373
1423
|
"description": "Probes the engine against one URL (method is always GET) without recording a hit, so it never burns a rule's `times` budget or nudges an alternating rule's phase. Returns {interceptorInstalled, listenerCount, engine:{hooksInstalled, devFlag, ruleCount, matches}, store:{enabled, ruleCount, rulesVisibleToEngine}}. READ engine.devFlag FIRST: if it is `false`, this is a release build and NO rule will ever be applied to real traffic no matter what the add/enable actions reported. `rulesVisibleToEngine` < `ruleCount` means the master switch is off. `matches:false` almost always means the glob missed the query string — a pattern must match the WHOLE url. Caution: unlike getCaptureStatus this probes the listener unguarded, so it throws in a runtime with no XMLHttpRequest."
|
|
1374
1424
|
}, {
|
|
1375
1425
|
"action": "upsertOverrideRule",
|
|
1376
|
-
"summary": "Create (or replace by id) a rule that forces matching requests to return a chosen status/body, fail outright, or arrive late.",
|
|
1426
|
+
"summary": "Create (or replace by id) a rule that forces matching requests to return a chosen status/body, fail outright, or arrive late. Pick this over query.setQueryData when the change must survive a refetch or reload, or when the ask is about what the API returns; follow it with query.invalidate so the mounted screen refetches through the rule. To change ONE value of the real response send fromRequestId plus `bodyPath` and `bodyValue` (bodyPath:\"sprites.front_default\", or \"stats[stat.name=hp].base_stat\" for one row of a list) — there is no nesting to get wrong, which matters most here because you read that body through a wire that truncates at 24,000 characters. For several fields use `bodyPatch` with ONLY those fields; never paste a whole captured body back. For \"just the NEXT call\" / \"once, then let it work again\", send rule.times:1 — the rule auto-disables after one match. Without times it overrides EVERY matching request until deleted. Never fake one-shot by triggering the request yourself and deleting the rule: that spends the failure the user wanted to see.",
|
|
1377
1427
|
"params": {
|
|
1378
1428
|
"type": "object",
|
|
1379
1429
|
"properties": {
|
|
@@ -1411,7 +1461,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
1411
1461
|
"kind": {
|
|
1412
1462
|
"type": "string",
|
|
1413
1463
|
"enum": ["respond", "fail", "delay"],
|
|
1414
|
-
"description": "respond = return your status/body, never hitting the network (default). fail = transport failure, as if offline. delay = run the real request, just late. Anything else falls back to respond."
|
|
1464
|
+
"description": "respond = return your status/body, never hitting the network (default). fail = transport failure, as if offline (`fail:true` is accepted for this). delay = run the real request, just late. Anything else falls back to respond."
|
|
1415
1465
|
},
|
|
1416
1466
|
"status": {
|
|
1417
1467
|
"type": "number",
|
|
@@ -1426,7 +1476,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
1426
1476
|
"description": "kind 'respond': response headers, string values."
|
|
1427
1477
|
},
|
|
1428
1478
|
"body": {
|
|
1429
|
-
"description": "kind 'respond': the response body. A string is used as-is; an object or array is JSON.stringify'd for you — do not pre-stringify."
|
|
1479
|
+
"description": "kind 'respond': the COMPLETE response body. A string is used as-is; an object or array is JSON.stringify'd for you — do not pre-stringify. To change a few fields of the real response use bodyPatch instead — bodies you read over the wire are truncated past 24K chars, and pasting a torn body back gives the app a response missing half its fields."
|
|
1430
1480
|
},
|
|
1431
1481
|
"failKind": {
|
|
1432
1482
|
"type": "string",
|
|
@@ -1439,7 +1489,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
1439
1489
|
},
|
|
1440
1490
|
"times": {
|
|
1441
1491
|
"type": "number",
|
|
1442
|
-
"description": "Auto-disable after N matches.
|
|
1492
|
+
"description": "Auto-disable after N matches. **Set times:1 whenever the ask is about the NEXT call** — \"make the next request fail\", \"just once\", \"then let it work again\". Omitted = the rule overrides EVERY matching request until something deletes it, and the result will tell you so. `once:true`, `failOnce:true` and `maxHits:N` are accepted as ways of saying this."
|
|
1443
1493
|
},
|
|
1444
1494
|
"alternate": {
|
|
1445
1495
|
"type": "boolean",
|
|
@@ -1448,6 +1498,21 @@ const CATALOG = exports.CATALOG = [{
|
|
|
1448
1498
|
"bodyOmitted": {
|
|
1449
1499
|
"type": "boolean",
|
|
1450
1500
|
"description": "Wire flag: send true WITH an `id` when re-saving a rule whose body the snapshot withheld, so the stored body is preserved instead of erased."
|
|
1501
|
+
},
|
|
1502
|
+
"bodyPatch": {
|
|
1503
|
+
"type": "object",
|
|
1504
|
+
"description": "Fields to change in the real captured response (needs fromRequestId, or the id of an existing rule) as a TYPED LEAF EDIT — like the devtools editor: same-type changes to existing fields, no new fields, no type changes, no null-ing a rendered list/object. A list may gain/lose items but each item must match the existing item shape (to change one item resend the whole list). Refused with the exact field on a violation. Ignored when `body` is sent. To change ONE ROW of a list in the response, address it by its own id instead of resending the array: {results:{pikachu:{name:'test123'}}} edits that row and leaves every other row exactly as the server sent it. That is the only correct form when you read the response through a view that truncated it."
|
|
1505
|
+
},
|
|
1506
|
+
"force": {
|
|
1507
|
+
"type": "boolean",
|
|
1508
|
+
"description": "Bypass the typed-edit safety on bodyPatch and write the patch raw. Default false — a violating patch is refused."
|
|
1509
|
+
},
|
|
1510
|
+
"bodyPath": {
|
|
1511
|
+
"type": "string",
|
|
1512
|
+
"description": "Set ONE value inside the real response, named by its path (\"sprites.front_default\", \"stats[stat.name=hp].base_stat\"). Needs fromRequestId or an existing rule id to address. Always goes through the same typed-edit guard as bodyPatch."
|
|
1513
|
+
},
|
|
1514
|
+
"bodyValue": {
|
|
1515
|
+
"description": "The value `bodyPath` is set to. Required whenever bodyPath is sent."
|
|
1451
1516
|
}
|
|
1452
1517
|
},
|
|
1453
1518
|
"additionalProperties": false
|
|
@@ -1457,7 +1522,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
1457
1522
|
},
|
|
1458
1523
|
"effect": "write",
|
|
1459
1524
|
"release": "noop",
|
|
1460
|
-
"description": "
|
|
1525
|
+
"description": "Create (or replace by id) a rule that forces matching requests to return a chosen status/body, fail outright, or arrive late. Pick this over query.setQueryData when the change must survive a refetch or reload, or when the ask is about what the API returns; follow it with query.invalidate so the mounted screen refetches through the rule. To change ONE value of the real response send fromRequestId plus `bodyPath` and `bodyValue` (bodyPath:\"sprites.front_default\", or \"stats[stat.name=hp].base_stat\" for one row of a list) — there is no nesting to get wrong, which matters most here because you read that body through a wire that truncates at 24,000 characters. For several fields use `bodyPatch` with ONLY those fields; never paste a whole captured body back. For \"just the NEXT call\" / \"once, then let it work again\", send rule.times:1 — the rule auto-disables after one match. Without times it overrides EVERY matching request until deleted. Never fake one-shot by triggering the request yourself and deleting the rule: that spends the failure the user wanted to see.",
|
|
1461
1526
|
"releaseNote": "packages/network/src/network/overrides/engine.ts:74 — overrideForRequest returns null when __DEV__ === false. The rule is still created, persisted and listed, and this action still returns ok:true, but it is NEVER applied to real traffic in a release build. Confirm with debugOverrides → engine.devFlag before telling anyone the override took effect.",
|
|
1462
1527
|
"requires": ["a __DEV__ build for the rule to actually apply", "something holding the interceptor open (an enabled rule pins it itself)", "Buoy Pro only to keep more than 1 rule — and only in the on-device UI; this action is not license-gated"]
|
|
1463
1528
|
}, {
|
|
@@ -2382,20 +2447,25 @@ const CATALOG = exports.CATALOG = [{
|
|
|
2382
2447
|
"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.",
|
|
2383
2448
|
"actions": [{
|
|
2384
2449
|
"action": "describeScreen",
|
|
2385
|
-
"summary": "List every meaningful/interactive element currently on screen, with normalized tap points — the read half of driving the app.",
|
|
2450
|
+
"summary": "List every meaningful/interactive element currently on screen, with normalized tap points — the read half of driving the app. The ONLY way to read what is on screen but in no store: text, values and names held in component state, which no other Buoy tool can see.",
|
|
2386
2451
|
"params": {
|
|
2387
2452
|
"type": "object",
|
|
2388
|
-
"properties": {
|
|
2453
|
+
"properties": {
|
|
2454
|
+
"includeBuoy": {
|
|
2455
|
+
"type": "boolean",
|
|
2456
|
+
"description": "Also list Buoy's own overlay (the dial, tool sheets, Ask Buoy's own chat). Default false — those are the tool, not the app."
|
|
2457
|
+
}
|
|
2458
|
+
},
|
|
2389
2459
|
"additionalProperties": false
|
|
2390
2460
|
},
|
|
2391
2461
|
"effect": "read",
|
|
2392
2462
|
"release": "empty",
|
|
2393
|
-
"description": "Walks the live React fiber tree across ALL renderers and measures each candidate. Returns {screen:{width,height}, count, elements[]} where each element has: nativeTag, name (owning component, not the host View), role, testID, label, text, interactive, control ('toggle'|'slider'|'text', omitted for a plain press), value (current value for toggle/slider/text), longPressable, tap:{x,y} and frame:{x,y,width,height} both normalized to 0-1. Sorted top-to-bottom then left-to-right. Prunes offscreen/inactive react-native-screens and hidden Offscreen subtrees, so it reflects the CURRENT screen only — re-run after every navigation, positions and tags move. No screenshot and no tracking needed; it does not require the highlight overlay to be enabled. Call this before tapElement to get an exact testID/nativeTag instead of guessing a fuzzy query.",
|
|
2463
|
+
"description": "Walks the live React fiber tree across ALL renderers and measures each candidate. Returns {screen:{width,height}, count, elements[]} where each element has: nativeTag, name (owning component, not the host View), role, testID, label, text, interactive, control ('toggle'|'slider'|'text', omitted for a plain press), value (current value for toggle/slider/text), longPressable, tap:{x,y} and frame:{x,y,width,height} both normalized to 0-1. Sorted top-to-bottom then left-to-right. Prunes offscreen/inactive react-native-screens and hidden Offscreen subtrees, so it reflects the CURRENT screen only — re-run after every navigation, positions and tags move. No screenshot and no tracking needed; it does not require the highlight overlay to be enabled. Call this before tapElement to get an exact testID/nativeTag instead of guessing a fuzzy query. Buoy's OWN overlay is left out — the dial, tool sheets and your own chat sheet are the tool, not the app under test — and `hiddenBuoy` counts what was omitted; pass includeBuoy:true only when the task is about Buoy itself.",
|
|
2394
2464
|
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/screenElements.ts:57 — getReactDevToolsHook() reads __REACT_DEVTOOLS_GLOBAL_HOOK__, which RN installs only under __DEV__; getAllFiberRoots() then returns [] and collectCandidates() short-circuits at line 539, so the result is a valid-looking {count:0, elements:[]} rather than an error.",
|
|
2395
2465
|
"requires": ["@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>", "dev build (__DEV__ === true)"]
|
|
2396
2466
|
}, {
|
|
2397
2467
|
"action": "tapElement",
|
|
2398
|
-
"summary": "Interact with one on-screen element by invoking its real handler in JS: press, long-press, toggle/slider, or type into a text input.",
|
|
2468
|
+
"summary": "Interact with one on-screen element by invoking its real handler in JS: press, long-press, toggle/slider, or type into a text input. This is how you make the app FETCH MORE — press its own \"next\", \"load more\" or a list row, waitFor, then read: the app fetches through its own code, so the data arrives in the shape its own screens render.",
|
|
2399
2469
|
"params": {
|
|
2400
2470
|
"type": "object",
|
|
2401
2471
|
"properties": {
|
|
@@ -2430,19 +2500,23 @@ const CATALOG = exports.CATALOG = [{
|
|
|
2430
2500
|
"scrollIntoView": {
|
|
2431
2501
|
"type": "boolean",
|
|
2432
2502
|
"description": "Scroll an ancestor ScrollView so the target is visible before acting. Default true — set false to avoid moving the user's screen."
|
|
2503
|
+
},
|
|
2504
|
+
"includeBuoy": {
|
|
2505
|
+
"type": "boolean",
|
|
2506
|
+
"description": "Let a fuzzy `query` match Buoy's own overlay. Default false. An exact nativeTag/testID always may."
|
|
2433
2507
|
}
|
|
2434
2508
|
},
|
|
2435
2509
|
"additionalProperties": false
|
|
2436
2510
|
},
|
|
2437
2511
|
"effect": "write",
|
|
2438
2512
|
"release": "empty",
|
|
2439
|
-
"description": "WARNING: this fires the app's ACTUAL handler, so it can trigger irreversible flows (delete, purchase, logout, submit). Matching by fuzzy `query` can select the wrong element — prefer an exact nativeTag or testID from describeScreen, and confirm the target before firing anything consequential. Resolution order: the element's OWN onValueChange (toggle/slider) or onChangeText (text input) wins; otherwise the nearest onPress walking up the fiber chain (up to 25 levels). Returns {tapped, reason?, scrolled?, matched:{nativeTag,name,testID,label,text,via,value}, candidates?} — `via` is onPress|onLongPress|onValueChange|onChangeText. Trap: passing `text` to something that is not a text input silently runs its onPress instead, so check `matched.via` in the result. Elements driven only by react-native-gesture-handler GestureDetector have no JS handler and return tapped:false with a clear reason. If the target is off-screen it is scrolled into view first (this moves the user's screen). Provide exactly one of nativeTag / testID / query.",
|
|
2513
|
+
"description": "WARNING: this fires the app's ACTUAL handler, so it can trigger irreversible flows (delete, purchase, logout, submit). Matching by fuzzy `query` can select the wrong element — prefer an exact nativeTag or testID from describeScreen, and confirm the target before firing anything consequential. Resolution order: the element's OWN onValueChange (toggle/slider) or onChangeText (text input) wins; otherwise the nearest onPress walking up the fiber chain (up to 25 levels). Returns {tapped, reason?, scrolled?, matched:{nativeTag,name,testID,label,text,via,value}, candidates?} — `via` is onPress|onLongPress|onValueChange|onChangeText. Trap: passing `text` to something that is not a text input silently runs its onPress instead, so check `matched.via` in the result. Elements driven only by react-native-gesture-handler GestureDetector have no JS handler and return tapped:false with a clear reason. If the target is off-screen it is scrolled into view first (this moves the user's screen). Provide exactly one of nativeTag / testID / query. A fuzzy query never matches Buoy's own overlay unless includeBuoy:true (your chat sheet echoes the words you search for; that echo used to be the best match). READ THE RESULT BEFORE TRUSTING THE TAP: `effect.commits` is how many React commits followed inside the settle window, and 0 means the handler ran but NOTHING re-rendered — the match was probably a screen still mounted underneath the current one (a stack keeps them), or an inert control — so treat it as not done and pick another target from describeScreen. `alsoMatched` lists elements that matched equally well; non-empty means the choice was a coin toss, so re-tap by nativeTag.",
|
|
2440
2514
|
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/screenElements.ts:793 — collectCandidates() returns [] with no DevTools hook, so it returns {tapped:false, reason:\"No React fiber roots / elements found on screen.\"}; it reports the failure honestly rather than claiming a tap.",
|
|
2441
2515
|
"requires": ["@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>", "dev build (__DEV__ === true)"]
|
|
2442
2516
|
}, {
|
|
2443
2517
|
"action": "waitFor",
|
|
2444
2518
|
"summary": "Block until an element is on screen (or gone), then report how long it took. Use it between navigating and tapping.",
|
|
2445
|
-
"description": "Returns {ok, waitedMs, polls, matched?, reason?}. USE THIS AFTER ANY ACTION THAT STARTS A LOAD — `route-events.navigate` returns the moment the route is pushed, not when the screen has data, so tapping straight after it hits a loading skeleton and fails for a reason that looks like a bad selector. Matching is `tapElement`'s exactly (same fields, same ranking), so a wait can never resolve on an element the tap then cannot find. Presence means ON SCREEN: a candidate matching by name is measured before it counts, so a previous screen still mounted behind this one does not satisfy the wait. `gone:true` waits for absence instead — and returns immediately when nothing matched in the first place, which is a real answer, not a failure. Defaults: timeoutMs 5000 (capped at 30000), pollMs 150 (floored at 50, because this polls on the JS thread the app renders on). Provide exactly one of nativeTag / testID / query.",
|
|
2519
|
+
"description": "Returns {ok, waitedMs, polls, matched?, reason?}. USE THIS AFTER ANY ACTION THAT STARTS A LOAD — `route-events.navigate` returns the moment the route is pushed, not when the screen has data, so tapping straight after it hits a loading skeleton and fails for a reason that looks like a bad selector. Matching is `tapElement`'s exactly (same fields, same ranking), so a wait can never resolve on an element the tap then cannot find. Presence means ON SCREEN: a candidate matching by name is measured before it counts, so a previous screen still mounted behind this one does not satisfy the wait. `gone:true` waits for absence instead — and returns immediately when nothing matched in the first place, which is a real answer, not a failure. Like tapElement, a fuzzy query ignores Buoy's own overlay unless includeBuoy:true; an exact nativeTag/testID always counts. Defaults: timeoutMs 5000 (capped at 30000), pollMs 150 (floored at 50, because this polls on the JS thread the app renders on). Provide exactly one of nativeTag / testID / query.",
|
|
2446
2520
|
"params": {
|
|
2447
2521
|
"type": "object",
|
|
2448
2522
|
"properties": {
|
|
@@ -2469,6 +2543,10 @@ const CATALOG = exports.CATALOG = [{
|
|
|
2469
2543
|
"pollMs": {
|
|
2470
2544
|
"type": "number",
|
|
2471
2545
|
"description": "Gap between scans. Default 150, floored at 50."
|
|
2546
|
+
},
|
|
2547
|
+
"includeBuoy": {
|
|
2548
|
+
"type": "boolean",
|
|
2549
|
+
"description": "Let a fuzzy `query` match Buoy's own overlay. Default false. An exact nativeTag/testID always may."
|
|
2472
2550
|
}
|
|
2473
2551
|
},
|
|
2474
2552
|
"additionalProperties": false
|
|
@@ -4011,7 +4089,7 @@ const CATALOG = exports.CATALOG = [{
|
|
|
4011
4089
|
}, {
|
|
4012
4090
|
"toolId": "ask-buoy",
|
|
4013
4091
|
"title": "Ask Buoy",
|
|
4014
|
-
"summary": "Ask Buoy's OWN session — the changes it has made in this conversation, and the undo for them. Call listChanges when the user asks \"what did you change?\"; call undoAll when they say \"undo that\" / \"put it back\" / \"clean up\". Undo restores exactly what the ledger captured at write time: override rules Ask Buoy created are deleted, impersonation is stopped, storage keys are restored to their pre-write values. Changes listed with reversible:false (state writes, wipes, one-shot actions) cannot be automatically reversed — say so honestly. NEVER improvise a reverse-write with a guessed shape instead of calling undoAll; a hand-rolled \"undo\" that writes invented data is worse than telling the user a change is permanent.",
|
|
4092
|
+
"summary": "Ask Buoy's OWN session — the changes it has made in this conversation, and the undo for them. Call listChanges when the user asks \"what did you change?\"; call undoAll when they say \"undo that\" / \"put it back\" / \"clean up\". Undo restores exactly what the ledger captured at write time: override rules Ask Buoy created are deleted, impersonation is stopped, storage keys are restored to their pre-write values, and a query cache edit (setQueryData) is put back to the data read just before the write — unless the app has refetched it since, in which case the server's data is already back and undo says so. Changes listed with reversible:false (state writes, wipes, one-shot actions) cannot be automatically reversed — say so honestly. NEVER improvise a reverse-write with a guessed shape instead of calling undoAll; a hand-rolled \"undo\" that writes invented data is worse than telling the user a change is permanent.",
|
|
4015
4093
|
"actions": [{
|
|
4016
4094
|
"action": "listChanges",
|
|
4017
4095
|
"summary": "List what Ask Buoy has changed in this conversation and whether each change can be undone. Use it to answer \"what did you change?\" before offering undo.",
|