@avasapp/agent-bridge 0.5.0 → 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 (61) hide show
  1. package/README.md +19 -5
  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.d.cts +1 -1
  24. package/dist/adapters/zustand.cjs +193 -22
  25. package/dist/adapters/zustand.d.cts +11 -15
  26. package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
  27. package/dist/cli.js +1336 -255
  28. package/dist/client/index.cjs +233 -27
  29. package/dist/client/index.d.ts +67 -4
  30. package/dist/client/index.js +5 -3
  31. package/dist/expo/index.d.cts +1 -1
  32. package/dist/network/index.cjs +320 -73
  33. package/dist/network/index.d.cts +18 -3
  34. package/dist/noop/async-storage.cjs +26 -0
  35. package/dist/noop/device.cjs +26 -0
  36. package/dist/noop/jotai.cjs +26 -0
  37. package/dist/noop/react-navigation.cjs +26 -0
  38. package/dist/noop/realtime.cjs +6 -1
  39. package/dist/noop/redux.cjs +28 -0
  40. package/dist/noop/secure-store.cjs +26 -0
  41. package/dist/realtime/index.cjs +71 -4
  42. package/dist/realtime/index.d.cts +21 -2
  43. package/dist/runtime/index.cjs +785 -128
  44. package/dist/runtime/index.d.cts +2 -2
  45. package/dist/store-common-j0cHHvBr.d.cts +23 -0
  46. package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
  47. package/entries/async-storage.cjs +9 -0
  48. package/entries/device.cjs +9 -0
  49. package/entries/jotai.cjs +9 -0
  50. package/entries/react-navigation.cjs +9 -0
  51. package/entries/redux.cjs +9 -0
  52. package/entries/secure-store.cjs +9 -0
  53. package/package.json +32 -1
  54. package/skills/agent-bridge/SKILL.md +39 -8
  55. package/skills/agent-bridge/references/device.md +21 -0
  56. package/skills/agent-bridge/references/flows.md +13 -3
  57. package/skills/agent-bridge/references/network.md +19 -1
  58. package/skills/agent-bridge/references/realtime.md +1 -1
  59. package/skills/agent-bridge/references/restore.md +21 -3
  60. package/skills/agent-bridge/references/screen.md +8 -2
  61. package/skills/agent-bridge/references/state.md +21 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @avasapp/agent-bridge
2
2
 
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 [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
 
@@ -45,6 +45,10 @@ import { expoTransport } from '@avasapp/agent-bridge/expo'
45
45
  import { queryTools } from '@avasapp/agent-bridge/tanstack-query'
46
46
  import { storeTools } from '@avasapp/agent-bridge/zustand'
47
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'
48
52
  import { routerTools } from '@avasapp/agent-bridge/expo-router'
49
53
  import { networkTools } from '@avasapp/agent-bridge/network'
50
54
  import { router, useNavigationContainerRef } from 'expo-router'
@@ -65,6 +69,12 @@ export function AgentBridge() {
65
69
  { redact: { auth: ['accessToken', 'refreshToken', 'user.phone'] } },
66
70
  ),
67
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' }),
68
78
  ...routerTools(router, { navigation: useNavigationContainerRef() }),
69
79
  ...networkTools(),
70
80
  // Your own tools: any function, JSON in and out.
@@ -75,7 +85,7 @@ export function AgentBridge() {
75
85
  }
76
86
  ```
77
87
 
78
- 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.
79
89
 
80
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).
81
91
 
@@ -89,21 +99,25 @@ npx @avasapp/agent-bridge call query.pin '[["features"], {"beta": false}]'
89
99
  npx @avasapp/agent-bridge call screen.press '"add-plant"'
90
100
  npx @avasapp/agent-bridge call screen.waitFor '"Name is required"'
91
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
92
103
  npx @avasapp/agent-bridge session stop # runs bridge.restore, then disconnects
93
104
  ```
94
105
 
95
- 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.
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).
96
107
 
97
- 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.
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.
109
+
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).
98
111
 
99
112
  ## Docs
100
113
 
101
114
  - [Built-in tools and restore](docs/built-in-tools.md)
102
115
  - [The CLI, flows and the client](docs/cli.md)
103
116
  - [Scenarios](docs/scenarios.md): signed-in state, strict network, fixtures, gates
104
- - [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
105
118
  - [WebViews](docs/webviews.md)
106
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
107
121
  - [Security and release builds](docs/security.md)
108
122
  - [Benchmarks](docs/benchmarks.md)
109
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 };