@avasapp/agent-bridge 0.5.0 → 0.6.1
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 +20 -6
- package/dist/adapters/ably.cjs +79 -3
- package/dist/adapters/ably.d.cts +2 -1
- package/dist/adapters/async-storage.cjs +158 -0
- package/dist/adapters/async-storage.d.cts +14 -0
- package/dist/adapters/device.cjs +1110 -0
- package/dist/adapters/device.d.cts +49 -0
- package/dist/adapters/expo-router.cjs +112 -6
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/jotai.cjs +266 -0
- package/dist/adapters/jotai.d.cts +19 -0
- package/dist/adapters/react-native-mmkv.cjs +80 -0
- package/dist/adapters/react-native-mmkv.d.cts +3 -1
- package/dist/adapters/react-native-webview.d.cts +1 -1
- package/dist/adapters/react-navigation.cjs +293 -0
- package/dist/adapters/react-navigation.d.cts +55 -0
- package/dist/adapters/redux.cjs +322 -0
- package/dist/adapters/redux.d.cts +28 -0
- package/dist/adapters/secure-store.cjs +169 -0
- package/dist/adapters/secure-store.d.cts +28 -0
- package/dist/adapters/socket-io.cjs +77 -3
- package/dist/adapters/socket-io.d.cts +2 -1
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
- package/dist/cli.js +1383 -255
- package/dist/client/index.cjs +233 -27
- package/dist/client/index.d.ts +67 -4
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +320 -73
- package/dist/network/index.d.cts +18 -3
- package/dist/noop/async-storage.cjs +26 -0
- package/dist/noop/device.cjs +26 -0
- package/dist/noop/jotai.cjs +26 -0
- package/dist/noop/react-navigation.cjs +26 -0
- package/dist/noop/realtime.cjs +6 -1
- package/dist/noop/redux.cjs +28 -0
- package/dist/noop/secure-store.cjs +26 -0
- package/dist/realtime/index.cjs +71 -4
- package/dist/realtime/index.d.cts +21 -2
- package/dist/runtime/index.cjs +822 -140
- package/dist/runtime/index.d.cts +2 -2
- package/dist/store-common-j0cHHvBr.d.cts +23 -0
- package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
- package/entries/async-storage.cjs +9 -0
- package/entries/device.cjs +9 -0
- package/entries/jotai.cjs +9 -0
- package/entries/react-navigation.cjs +9 -0
- package/entries/redux.cjs +9 -0
- package/entries/secure-store.cjs +9 -0
- package/package.json +32 -1
- package/skills/agent-bridge/SKILL.md +39 -8
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +13 -3
- package/skills/agent-bridge/references/network.md +19 -1
- package/skills/agent-bridge/references/realtime.md +1 -1
- package/skills/agent-bridge/references/restore.md +21 -3
- package/skills/agent-bridge/references/screen.md +8 -2
- package/skills/agent-bridge/references/state.md +21 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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)
|
|
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
|

|
|
6
6
|
|
|
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)).
|
|
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. With [Argent](https://github.com/software-mansion/argent) instead of agent-device it is [17.1 s against 1.4 s](example/media/demo-argent.gif) while screen recording (Argent alone takes 11.6 s without the recorder). Most calls take a few milliseconds ([benchmarks](docs/benchmarks.md)).
|
|
8
8
|
|
|
9
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
|
|
|
@@ -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
|
|
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
|
|
package/dist/adapters/ably.cjs
CHANGED
|
@@ -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
|
|
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;
|
package/dist/adapters/ably.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { T as Tools } from '../types-
|
|
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 };
|