@avasapp/agent-bridge 0.4.2 → 0.6.0

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 (65) hide show
  1. package/README.md +36 -14
  2. package/dist/adapters/ably.cjs +79 -3
  3. package/dist/adapters/ably.d.cts +2 -1
  4. package/dist/adapters/async-storage.cjs +158 -0
  5. package/dist/adapters/async-storage.d.cts +14 -0
  6. package/dist/adapters/device.cjs +1110 -0
  7. package/dist/adapters/device.d.cts +49 -0
  8. package/dist/adapters/expo-router.cjs +112 -6
  9. package/dist/adapters/expo-router.d.cts +1 -1
  10. package/dist/adapters/jotai.cjs +266 -0
  11. package/dist/adapters/jotai.d.cts +19 -0
  12. package/dist/adapters/react-native-mmkv.cjs +80 -0
  13. package/dist/adapters/react-native-mmkv.d.cts +3 -1
  14. package/dist/adapters/react-native-webview.d.cts +1 -1
  15. package/dist/adapters/react-navigation.cjs +293 -0
  16. package/dist/adapters/react-navigation.d.cts +55 -0
  17. package/dist/adapters/redux.cjs +322 -0
  18. package/dist/adapters/redux.d.cts +28 -0
  19. package/dist/adapters/secure-store.cjs +169 -0
  20. package/dist/adapters/secure-store.d.cts +28 -0
  21. package/dist/adapters/socket-io.cjs +77 -3
  22. package/dist/adapters/socket-io.d.cts +2 -1
  23. package/dist/adapters/tanstack-query.cjs +3 -0
  24. package/dist/adapters/tanstack-query.d.cts +13 -4
  25. package/dist/adapters/zustand.cjs +193 -22
  26. package/dist/adapters/zustand.d.cts +11 -15
  27. package/dist/{chunk-TA2OLPPT.js → chunk-Y6QFU5ON.js} +477 -165
  28. package/dist/cli.js +2370 -103
  29. package/dist/client/index.cjs +441 -136
  30. package/dist/client/index.d.ts +67 -4
  31. package/dist/client/index.js +5 -3
  32. package/dist/expo/index.d.cts +1 -1
  33. package/dist/network/index.cjs +320 -73
  34. package/dist/network/index.d.cts +18 -3
  35. package/dist/noop/async-storage.cjs +26 -0
  36. package/dist/noop/device.cjs +26 -0
  37. package/dist/noop/jotai.cjs +26 -0
  38. package/dist/noop/react-navigation.cjs +26 -0
  39. package/dist/noop/realtime.cjs +6 -1
  40. package/dist/noop/redux.cjs +28 -0
  41. package/dist/noop/secure-store.cjs +26 -0
  42. package/dist/realtime/index.cjs +71 -4
  43. package/dist/realtime/index.d.cts +21 -2
  44. package/dist/runtime/index.cjs +838 -128
  45. package/dist/runtime/index.d.cts +4 -2
  46. package/dist/store-common-j0cHHvBr.d.cts +23 -0
  47. package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
  48. package/entries/async-storage.cjs +9 -0
  49. package/entries/device.cjs +9 -0
  50. package/entries/jotai.cjs +9 -0
  51. package/entries/react-navigation.cjs +9 -0
  52. package/entries/redux.cjs +9 -0
  53. package/entries/secure-store.cjs +9 -0
  54. package/package.json +34 -2
  55. package/skills/agent-bridge/SKILL.md +70 -22
  56. package/skills/agent-bridge/references/device.md +21 -0
  57. package/skills/agent-bridge/references/flows.md +19 -8
  58. package/skills/agent-bridge/references/large-data.md +6 -6
  59. package/skills/agent-bridge/references/network.md +25 -7
  60. package/skills/agent-bridge/references/realtime.md +5 -5
  61. package/skills/agent-bridge/references/restore.md +25 -7
  62. package/skills/agent-bridge/references/screen.md +25 -11
  63. package/skills/agent-bridge/references/state.md +31 -11
  64. package/skills/agent-bridge/references/webview.md +9 -9
  65. package/skills/agent-bridge/references/writing-scenarios.md +3 -3
package/README.md CHANGED
@@ -1,15 +1,23 @@
1
1
  # @avasapp/agent-bridge
2
2
 
3
- Let coding agents drive a running React Native app directly. Seed data, flip flags, fake realtime messages, navigate and check the screen in **milliseconds**, without tapping through the app. It works alongside [agent-device](https://github.com/callstack/agent-device): agent-device opens the app and handles native UI, and agent-bridge handles everything inside the app.
3
+ Let coding agents drive any running Expo or React Native app directly. Seed data, flip flags, fake realtime messages, navigate and check the screen in **milliseconds**, without tapping through the app. It works alongside device tools such as [agent-device](https://github.com/callstack/agent-device) and [Argent](https://github.com/software-mansion/argent): they open the app and handle native UI, and agent-bridge handles everything inside the app. Use it from a shell, from [flows](docs/cli.md) or as an [MCP server](docs/cli.md#mcp-server).
4
4
 
5
5
  ![The same checks with agent-device alone (22.4 s) and with agent-bridge (1.4 s): hide a tab, turn on dark mode, fill in a form, seed data, fake a live message and undo it all](example/media/demo.gif)
6
6
 
7
7
  The same 19 checks on [the example app](example), at real speed: 22.4 s with agent-device alone, 1.4 s with agent-bridge. Most calls take a few milliseconds ([benchmarks](docs/benchmarks.md)).
8
8
 
9
- Works on Expo (dev-tools socket) and on bare React Native (CDP over Metro). Dev builds only: release builds get empty stubs.
9
+ Works on Expo (dev-tools socket) and on bare React Native (CDP over Metro). Nothing ships to users: every entry point compiles to an empty stub in release builds, and `agent-bridge assert-absent` checks a bundle in CI ([security](docs/security.md)).
10
10
 
11
11
  ## Install
12
12
 
13
+ ```sh
14
+ npx @avasapp/agent-bridge init
15
+ ```
16
+
17
+ Run it in the app. It installs the package, writes a dev-only `src/dev/agent-bridge.tsx` wired for the libraries it finds, adds a smoke flow and the agent skill, and prints the two lines to paste into your root component. Agents run it as `init --json`; see [the CLI](docs/cli.md#init).
18
+
19
+ ### Manual setup
20
+
13
21
  ```sh
14
22
  bun add -d @avasapp/agent-bridge # or npm / yarn / pnpm
15
23
  ```
@@ -37,6 +45,10 @@ import { expoTransport } from '@avasapp/agent-bridge/expo'
37
45
  import { queryTools } from '@avasapp/agent-bridge/tanstack-query'
38
46
  import { storeTools } from '@avasapp/agent-bridge/zustand'
39
47
  import { mmkvTools } from '@avasapp/agent-bridge/react-native-mmkv'
48
+ // import { asyncStorageTools } from '@avasapp/agent-bridge/async-storage'
49
+ // import { secureStoreTools } from '@avasapp/agent-bridge/secure-store'
50
+ // import { reduxTools } from '@avasapp/agent-bridge/redux'
51
+ // import { jotaiTools } from '@avasapp/agent-bridge/jotai'
40
52
  import { routerTools } from '@avasapp/agent-bridge/expo-router'
41
53
  import { networkTools } from '@avasapp/agent-bridge/network'
42
54
  import { router, useNavigationContainerRef } from 'expo-router'
@@ -57,6 +69,12 @@ export function AgentBridge() {
57
69
  { redact: { auth: ['accessToken', 'refreshToken', 'user.phone'] } },
58
70
  ),
59
71
  ...mmkvTools({ storage }),
72
+ // Redux and Jotai state often holds tokens: add `redact` before wiring them.
73
+ // ...reduxTools(store, { redact: { auth: ['token'] } }),
74
+ // ...jotaiTools(getDefaultStore(), { user: userAtom }),
75
+ // AsyncStorage and SecureStore often hold tokens: wire them only if you accept that.
76
+ // ...asyncStorageTools(AsyncStorage),
77
+ // ...secureStoreTools(SecureStore, ['authToken'], { keychainService: 'auth' }),
60
78
  ...routerTools(router, { navigation: useNavigationContainerRef() }),
61
79
  ...networkTools(),
62
80
  // Your own tools: any function, JSON in and out.
@@ -67,35 +85,39 @@ export function AgentBridge() {
67
85
  }
68
86
  ```
69
87
 
70
- Every app also gets `screen.snapshot`, `screen.press`, `screen.fill`, `screen.scroll`, `screen.waitFor`, `screen.findText`, `app.reload`, and `bridge.restore` to undo everything the agent changed. See [built-in tools and restore](docs/built-in-tools.md), including how to give agents your own hooks and register their undo.
88
+ Every app also gets `screen.snapshot` (one short line per element), `screen.press` (taps, long presses, Switch toggles), `screen.fill`, `screen.setValue` (Switch, Slider, Picker, SegmentedControl), `screen.scroll`, `screen.waitFor`, `screen.findText`, `app.reload`, and `bridge.restore` to undo everything the agent changed, even across a reload in a session. See [built-in tools and restore](docs/built-in-tools.md), including how to give agents your own hooks and register their undo.
71
89
 
72
90
  Most flows need the app in a known state first, such as a user signed in locally with every request mocked. Define that once as a [scenario](docs/scenarios.md).
73
91
 
74
92
  ## From the agent
75
93
 
76
94
  ```sh
77
- npx agent-bridge session start # hold one connection; call/tools/run reuse it
78
- npx agent-bridge tools
79
- npx agent-bridge call scenario.apply '["signedIn", {"user": {"name": "Ada"}}]'
80
- npx agent-bridge call query.pin '[["features"], {"beta": false}]'
81
- npx agent-bridge call screen.press '"add-plant"'
82
- npx agent-bridge call screen.waitFor '"Name is required"'
83
- npx agent-bridge run flows/add-plant.mjs --strict # fail if the app logged an error
84
- npx agent-bridge session stop # runs bridge.restore, then disconnects
95
+ npx @avasapp/agent-bridge session start # hold one connection; call/tools/run reuse it
96
+ npx @avasapp/agent-bridge tools
97
+ npx @avasapp/agent-bridge call scenario.apply '["signedIn", {"user": {"name": "Ada"}}]'
98
+ npx @avasapp/agent-bridge call query.pin '[["features"], {"beta": false}]'
99
+ npx @avasapp/agent-bridge call screen.press '"add-plant"'
100
+ npx @avasapp/agent-bridge call screen.waitFor '"Name is required"'
101
+ npx @avasapp/agent-bridge run flows/add-plant.flow.mjs --strict # fail if the app logged an error
102
+ npx @avasapp/agent-bridge run flows/ --bail # every *.flow.mjs in order, restore after each
103
+ npx @avasapp/agent-bridge session stop # runs bridge.restore, then disconnects
85
104
  ```
86
105
 
87
- Agents should run `./node_modules/.bin/agent-bridge` (or `bunx agent-bridge`) rather than `npx`, which is slower and can print npm warnings into their output. See [the CLI](docs/cli.md) for flows, batch mode, the REPL, sessions and the `connect()` client.
106
+ Agents should run `./node_modules/.bin/agent-bridge` rather than `npx`, which is slower and can print npm warnings into their output. See [the CLI](docs/cli.md) for flows, batch mode, the REPL, sessions and the `connect()` client. Agents that prefer MCP tools to a shell can run `agent-bridge mcp`; see [the MCP server](docs/cli.md#mcp-server).
107
+
108
+ No device tool is needed. Pair one (such as agent-device or Argent) with the bridge for what it can't reach: system alerts, permission prompts, the keyboard, screenshots, and one real tap per flow. Argent's tap, swipe, screenshot and UI tree tools work next to the bridge as they are. Its debugger tools do too on Expo apps (the bridge uses the dev-tools socket) and on React Native 0.85 and later, which let several debuggers attach. Before 0.85, Metro's debugger socket takes one client at a time, so a bare React Native app on `--transport cdp` and a tool that holds the debugger open (Argent's debugger tools, React Native DevTools) can take it from each other.
88
109
 
89
- No device tool is needed. Pair one (such as agent-device) with the bridge for what it can't reach: system alerts, permission prompts, the keyboard, screenshots, and one real tap per flow.
110
+ The screen tools measure with Fabric, so they need React Native's New Architecture; on the old one the tools that need positions fail with an error that says so (`snapshot {all: true}` and `findText` still return text, without rects).
90
111
 
91
112
  ## Docs
92
113
 
93
114
  - [Built-in tools and restore](docs/built-in-tools.md)
94
115
  - [The CLI, flows and the client](docs/cli.md)
95
116
  - [Scenarios](docs/scenarios.md): signed-in state, strict network, fixtures, gates
96
- - [Adapters](docs/adapters.md): TanStack Query, Zustand, MMKV, Expo Router, network
117
+ - [Adapters](docs/adapters.md): TanStack Query, Zustand, Redux, Jotai, MMKV, AsyncStorage, SecureStore, Expo Router, React Navigation, network
97
118
  - [WebViews](docs/webviews.md)
98
119
  - [Realtime](docs/realtime.md): Ably, socket.io, any other library
120
+ - [Device events](docs/device-events.md): deep links, app state, offline, push notifications, the clock
99
121
  - [Security and release builds](docs/security.md)
100
122
  - [Benchmarks](docs/benchmarks.md)
101
123
 
@@ -72,6 +72,21 @@ function tapListeners(tap, target, options) {
72
72
  };
73
73
  }
74
74
  }
75
+ function tapOutgoing(tap, target, options) {
76
+ const methods = target;
77
+ for (const name of list(options.methods)) {
78
+ const send = methods[name];
79
+ if (typeof send !== "function") continue;
80
+ methods[name] = function(...args) {
81
+ try {
82
+ const channel = options.skip?.(this) ? void 0 : options.channel(args);
83
+ if (channel !== void 0) tap.sent(channel, options.describe(args));
84
+ } catch {
85
+ }
86
+ return send.apply(this, args);
87
+ };
88
+ }
89
+ }
75
90
  function fakeableConnection(target, options) {
76
91
  const methods = target;
77
92
  const events = methods?.[options.events];
@@ -116,6 +131,46 @@ function realtimeAdapter(install) {
116
131
  };
117
132
  }
118
133
 
134
+ // src/network/headers.ts
135
+ var SECRET = /auth|cookie|token|secret|session|password|passwd|signature|api-?key|credential|jwt/i;
136
+
137
+ // src/realtime/snapshot.ts
138
+ var MAX_STRING = 2e3;
139
+ var MAX_ITEMS = 50;
140
+ var MAX_DEPTH = 5;
141
+ var isPlain = (v) => {
142
+ const proto = Object.getPrototypeOf(v);
143
+ return proto === Object.prototype || proto === null;
144
+ };
145
+ function snapshot(value, depth = 0) {
146
+ if (typeof value === "string")
147
+ return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}\u2026 (+${value.length - MAX_STRING} chars)` : value;
148
+ if (typeof value === "function") return "[function]";
149
+ if (!value || typeof value !== "object") return value;
150
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer)
151
+ return `[${value.constructor.name} ${value.byteLength} bytes]`;
152
+ if (depth >= MAX_DEPTH) return "[\u2026]";
153
+ if (Array.isArray(value)) {
154
+ const items = value.slice(0, MAX_ITEMS).map((v) => snapshot(v, depth + 1));
155
+ if (value.length > MAX_ITEMS) items.push(`\u2026 (+${value.length - MAX_ITEMS} items)`);
156
+ return items;
157
+ }
158
+ if (value instanceof Date) return value.toISOString();
159
+ if (!isPlain(value)) return `[${value.constructor?.name ?? "object"}]`;
160
+ const out = {};
161
+ for (const key of Object.keys(value).slice(0, MAX_ITEMS))
162
+ out[key] = snapshot(value[key], depth + 1);
163
+ return out;
164
+ }
165
+ function redact(value, depth = 0) {
166
+ if (!value || typeof value !== "object" || depth >= MAX_DEPTH + 1) return value;
167
+ if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1));
168
+ const out = {};
169
+ for (const [key, v] of Object.entries(value))
170
+ out[key] = SECRET.test(key) ? "[redacted]" : redact(v, depth + 1);
171
+ return out;
172
+ }
173
+
119
174
  // src/realtime/tap.ts
120
175
  var LOG_SIZE = 50;
121
176
  var ALL = "*";
@@ -159,6 +214,17 @@ function createRealtimeTap(options = {}) {
159
214
  }
160
215
  if (!dropped) entry.deliver(message, ...rest);
161
216
  }
217
+ const sent = (channel, summary) => {
218
+ log.push({
219
+ at: (/* @__PURE__ */ new Date()).toISOString(),
220
+ channel,
221
+ ...summary,
222
+ ..."data" in summary && { data: snapshot(summary.data) },
223
+ injected: false,
224
+ sent: true
225
+ });
226
+ if (log.length > LOG_SIZE) log.splice(0, log.length - LOG_SIZE);
227
+ };
162
228
  const wrap = (channel, listener, wrapOptions = {}) => {
163
229
  const entry = {
164
230
  deliver: listener,
@@ -208,11 +274,11 @@ function createRealtimeTap(options = {}) {
208
274
  }))
209
275
  },
210
276
  [`${ns}.log`]: {
211
- description: `Recent messages, real and injected, newest first (last ${LOG_SIZE}). Filter: { channel?, event?, limit? }; clear: true empties the log after reading.`,
277
+ description: `Recent messages, real, injected, and ones the app sent (sent: true) (socket.emit, Ably publish), newest first (last ${LOG_SIZE}). Filter: { channel?, event?, limit?, full? }; clear: true empties the log after reading. Sent messages are copied and cut, and keys like token, password or authorization show [redacted] unless full: true.`,
212
278
  run: (filter = {}) => {
213
279
  const entries = log.filter(
214
280
  (e) => (!filter.channel || e.channel === filter.channel) && (!filter.event || e.event === filter.event)
215
- ).reverse().slice(0, filter.limit ?? 20);
281
+ ).reverse().slice(0, filter.limit ?? 20).map((e) => e.sent && !filter.full && "data" in e ? { ...e, data: redact(e.data) } : e);
216
282
  if (filter.clear) log.length = 0;
217
283
  return entries;
218
284
  }
@@ -273,7 +339,7 @@ function createRealtimeTap(options = {}) {
273
339
  return { state: faked ?? connection.state(), real: connection.state(), faked: faked !== null };
274
340
  }
275
341
  };
276
- return { wrap, tools };
342
+ return { wrap, sent, tools };
277
343
  }
278
344
 
279
345
  // src/adapters/ably.ts
@@ -349,6 +415,16 @@ var ablyTools = realtimeAdapter(
349
415
  channel: () => channel.name,
350
416
  accepts: ([filter], message) => accepts(filter, message)
351
417
  });
418
+ tapOutgoing(tap, channel, {
419
+ methods: "publish",
420
+ channel: () => channel.name,
421
+ describe: ([first, second]) => {
422
+ if (typeof first === "string") return { event: first, data: second };
423
+ const m = first;
424
+ if (Array.isArray(m)) return { data: m.map((x) => ({ event: x.name, data: x.data })) };
425
+ return { event: m?.name, data: m?.data, id: m?.id };
426
+ }
427
+ });
352
428
  return channel;
353
429
  };
354
430
  const registry = client.channels;
@@ -1,4 +1,4 @@
1
- import { T as Tools } from '../types-BmpykP1P.cjs';
1
+ import { T as Tools } from '../types-qlwdcGjt.cjs';
2
2
 
3
3
  /** The fields of an Ably message this adapter reads. */
4
4
  type AblyMessageLike = {
@@ -18,6 +18,7 @@ type AblyMessageLike = {
18
18
  type AblyChannelLike = {
19
19
  name: string;
20
20
  state?: string;
21
+ publish?(...args: never[]): unknown;
21
22
  subscribe(...args: never[]): unknown;
22
23
  unsubscribe(...args: never[]): unknown;
23
24
  };
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/adapters/async-storage.ts
21
+ var async_storage_exports = {};
22
+ __export(async_storage_exports, {
23
+ asyncStorageTools: () => asyncStorageTools
24
+ });
25
+ module.exports = __toCommonJS(async_storage_exports);
26
+
27
+ // src/adapters/storage-undo.ts
28
+ function toStored(tool, key, value) {
29
+ if (typeof key !== "string" || !key)
30
+ throw new Error(`${tool} needs the key name as a non-empty string`);
31
+ if (value === void 0 || value === null)
32
+ throw new Error(`${tool} needs a value; use the delete tool to remove "${key}"`);
33
+ const stored = typeof value === "string" ? value : JSON.stringify(value);
34
+ if (typeof stored !== "string")
35
+ throw new Error(`${tool} cannot store that value as JSON`);
36
+ return stored;
37
+ }
38
+ async function change(undo, id, io, value) {
39
+ let entry = undo.get(id);
40
+ if (!entry) {
41
+ const original = io.read();
42
+ entry = { io, original, last: null };
43
+ undo.set(id, entry);
44
+ try {
45
+ entry.last = await original;
46
+ } catch (error) {
47
+ if (undo.get(id) === entry) undo.delete(id);
48
+ throw error;
49
+ }
50
+ } else {
51
+ await entry.original;
52
+ }
53
+ if (value === null) await io.remove();
54
+ else await io.write(value);
55
+ entry.last = value;
56
+ }
57
+ async function restore(undo, label) {
58
+ let restored = 0;
59
+ const skipped = [];
60
+ const failed = [];
61
+ for (const [id, entry] of [...undo]) {
62
+ try {
63
+ let original;
64
+ try {
65
+ original = await entry.original;
66
+ } catch {
67
+ undo.delete(id);
68
+ continue;
69
+ }
70
+ if (await entry.io.read() !== entry.last) {
71
+ undo.delete(id);
72
+ skipped.push(label(id));
73
+ continue;
74
+ }
75
+ if (original === null) await entry.io.remove();
76
+ else await entry.io.write(original);
77
+ undo.delete(id);
78
+ restored += 1;
79
+ } catch {
80
+ failed.push(label(id));
81
+ }
82
+ }
83
+ return { restored, skipped, failed };
84
+ }
85
+ function describeRestore(r, what) {
86
+ if (r.failed.length)
87
+ throw new Error(
88
+ `Restored ${r.restored}, could not restore: ${r.failed.join(", ")}. ${what} stay pending; run it again.`
89
+ );
90
+ return r.skipped.length ? { restored: r.restored, skipped: r.skipped } : r.restored;
91
+ }
92
+
93
+ // src/adapters/async-storage.ts
94
+ var LIST_LIMIT = 50;
95
+ var VALUE_CHARS = 200;
96
+ var originals = /* @__PURE__ */ new WeakMap();
97
+ var clip = (value) => value !== null && value.length > VALUE_CHARS ? `${value.slice(0, VALUE_CHARS)}\u2026 (${value.length} chars)` : value;
98
+ function asyncStorageTools(storage) {
99
+ let undo = originals.get(storage);
100
+ if (!undo) originals.set(storage, undo = /* @__PURE__ */ new Map());
101
+ const pending = undo;
102
+ const apply = (key, value) => change(
103
+ pending,
104
+ key,
105
+ {
106
+ read: () => storage.getItem(key),
107
+ write: (v) => storage.setItem(key, v),
108
+ remove: () => storage.removeItem(key)
109
+ },
110
+ value
111
+ );
112
+ return {
113
+ "asyncStorage.list": {
114
+ description: "Keys with their values, cut to 200 characters. {limit} (default 50) caps the rows, {prefix} filters keys. Returns {total, keys}.",
115
+ run: async (options) => {
116
+ const all = (await storage.getAllKeys()).filter(
117
+ (k) => !options?.prefix || k.startsWith(options.prefix)
118
+ );
119
+ const shown = all.slice(0, options?.limit ?? LIST_LIMIT);
120
+ const rows = storage.multiGet ? await storage.multiGet(shown) : await Promise.all(
121
+ shown.map(async (k) => [k, await storage.getItem(k)])
122
+ );
123
+ return {
124
+ total: all.length,
125
+ keys: Object.fromEntries(rows.map(([k, v]) => [k, clip(v)]))
126
+ };
127
+ }
128
+ },
129
+ "asyncStorage.get": {
130
+ description: "A key's string value (null when missing); parsed as JSON when {json:true}.",
131
+ run: async (key, options) => {
132
+ const raw = await storage.getItem(key);
133
+ return options?.json && raw !== null ? JSON.parse(raw) : raw;
134
+ }
135
+ },
136
+ "asyncStorage.set": {
137
+ description: "Set a value. Objects, arrays, numbers and booleans are stored as JSON strings.",
138
+ run: async (key, value) => {
139
+ await apply(key, toStored("asyncStorage.set", key, value));
140
+ return true;
141
+ }
142
+ },
143
+ "asyncStorage.delete": {
144
+ description: "Delete a key.",
145
+ run: async (key) => {
146
+ if (typeof key !== "string" || !key)
147
+ throw new Error("asyncStorage.delete needs the key name as a non-empty string");
148
+ await apply(key, null);
149
+ return true;
150
+ }
151
+ },
152
+ "asyncStorage.restore": {
153
+ pending: () => pending.size > 0,
154
+ description: "Undo the agent: put back keys changed with asyncStorage.set or asyncStorage.delete, deleting ones that did not exist. Returns how many; a key the app has written since is left alone and returned in {skipped}. Lost when the JS reloads: restore before app.reload.",
155
+ run: async () => describeRestore(await restore(pending, (id) => id), "Those keys")
156
+ }
157
+ };
158
+ }
@@ -0,0 +1,14 @@
1
+ import { T as Tools } from '../types-qlwdcGjt.cjs';
2
+
3
+ /** The AsyncStorage surface this adapter uses. */
4
+ type AsyncStorageLike = {
5
+ getAllKeys: () => Promise<readonly string[]>;
6
+ getItem: (key: string) => Promise<string | null>;
7
+ setItem: (key: string, value: string) => Promise<void>;
8
+ removeItem: (key: string) => Promise<void>;
9
+ multiGet?: (keys: readonly string[]) => Promise<readonly (readonly [string, string | null])[]>;
10
+ };
11
+ /** Read and write AsyncStorage. Values are returned as they are: wire a storage without secrets. */
12
+ declare function asyncStorageTools(storage: AsyncStorageLike): Tools;
13
+
14
+ export { type AsyncStorageLike, asyncStorageTools };