@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.
Files changed (61) hide show
  1. package/README.md +20 -6
  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 +1383 -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 +822 -140
  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
@@ -0,0 +1,49 @@
1
+ import { T as Tools } from '../types-qlwdcGjt.cjs';
2
+ import { NavigationLike } from './expo-router.cjs';
3
+
4
+ type Fn = (...args: any[]) => any;
5
+ /** The parts of `react-native` the device tools use. Defaults to `require('react-native')`. */
6
+ type ReactNativeLike = {
7
+ Linking: {
8
+ getInitialURL: Fn;
9
+ };
10
+ AppState: {
11
+ currentState: string;
12
+ };
13
+ DeviceEventEmitter: {
14
+ emit: Fn;
15
+ listenerCount?: Fn;
16
+ };
17
+ Platform?: {
18
+ OS: string;
19
+ };
20
+ };
21
+ /** `@react-native-community/netinfo`'s default export. */
22
+ type NetInfoLike = {
23
+ refresh?: Fn;
24
+ };
25
+ /** `expo-notifications`. Only its default action id is read. */
26
+ type NotificationsLike = {
27
+ DEFAULT_ACTION_IDENTIFIER?: string;
28
+ };
29
+ type DeviceToolsOptions = {
30
+ /** NetInfo, so `device.offline` also flips it. Leave out if the app has none. */
31
+ NetInfo?: NetInfoLike;
32
+ /** `import * as Notifications from 'expo-notifications'`. Adds `device.notification`. */
33
+ Notifications?: NotificationsLike;
34
+ /** `useNavigationContainerRef()` from expo-router. Makes `device.openUrl` return the route it ended on. */
35
+ navigation?: NavigationLike;
36
+ /** Adds `device.setTime`, which fakes `Date.now()` and `new Date()` app-wide. Off by default. */
37
+ time?: boolean;
38
+ /** Override where Linking, AppState and the emitter come from. For tests. */
39
+ reactNative?: ReactNativeLike;
40
+ };
41
+
42
+ /**
43
+ * Fake what comes from outside the JS: an incoming deep link, the app going
44
+ * to the background, going offline, a push notification and (opt in) the clock.
45
+ * `device.restore` undoes all of it.
46
+ */
47
+ declare function deviceTools(options?: DeviceToolsOptions): Tools;
48
+
49
+ export { type DeviceToolsOptions, type NetInfoLike, type NotificationsLike, type ReactNativeLike, deviceTools };
@@ -23,6 +23,8 @@ __export(expo_router_exports, {
23
23
  routerTools: () => routerTools
24
24
  });
25
25
  module.exports = __toCommonJS(expo_router_exports);
26
+
27
+ // src/adapters/expo-router-route.ts
26
28
  var ROOT = "__root";
27
29
  var NOT_FOUND = "+not-found";
28
30
  var SETTLE_MS = 1e3;
@@ -104,6 +106,86 @@ function settle(navigation, go) {
104
106
  }
105
107
  });
106
108
  }
109
+
110
+ // src/runtime/screen/settle.ts
111
+ var STATE = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/commits");
112
+ var UPDATE_LANES = (1 << 22) - 1;
113
+ var hook = () => globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
114
+ var now = () => performance.now();
115
+ function install() {
116
+ const h = hook();
117
+ if (!h) return null;
118
+ const current = h.onCommitFiberRoot;
119
+ const existing = current?.[STATE];
120
+ if (existing) return existing;
121
+ const state = { commits: 0, listeners: /* @__PURE__ */ new Set() };
122
+ const wrapped = function(...args) {
123
+ state.commits += 1;
124
+ for (const listener of state.listeners) {
125
+ try {
126
+ listener();
127
+ } catch {
128
+ }
129
+ }
130
+ return current?.apply(this, args);
131
+ };
132
+ Object.defineProperty(wrapped, STATE, { value: state });
133
+ h.onCommitFiberRoot = wrapped;
134
+ return state;
135
+ }
136
+ function busy() {
137
+ const h = hook();
138
+ if (!h?.renderers || !h.getFiberRoots) return false;
139
+ for (const id of h.renderers.keys()) {
140
+ for (const root of h.getFiberRoots(id)) {
141
+ const pending = root.pendingLanes;
142
+ if (typeof pending !== "number") continue;
143
+ if (pending & ~(root.suspendedLanes ?? 0) & UPDATE_LANES) return true;
144
+ }
145
+ }
146
+ return false;
147
+ }
148
+ var frame = () => new Promise((resolve) => {
149
+ const raf = globalThis.requestAnimationFrame;
150
+ if (raf) raf(() => resolve());
151
+ else setTimeout(resolve, 16);
152
+ });
153
+ async function settle2(options = {}) {
154
+ const maxMs = options.maxMs ?? 500;
155
+ const t0 = now();
156
+ const state = install();
157
+ const start = state?.commits ?? 0;
158
+ const result = () => ({
159
+ commits: (state?.commits ?? 0) - start,
160
+ ms: Math.round((now() - t0) * 100) / 100
161
+ });
162
+ await new Promise((resolve) => setTimeout(resolve, 0));
163
+ if (!state) return result();
164
+ let seen = state.commits;
165
+ if (seen === start && !busy()) return result();
166
+ while (now() - t0 < maxMs) {
167
+ await frame();
168
+ if (state.commits === seen && !busy()) break;
169
+ seen = state.commits;
170
+ }
171
+ return result();
172
+ }
173
+
174
+ // src/adapters/expo-router.ts
175
+ function targetPath(href) {
176
+ if (!href.startsWith("/")) return null;
177
+ const path = href.split(/[?#]/)[0];
178
+ const segments = path.split("/").filter((s) => s && !isGroup(s));
179
+ if (segments.at(-1) === "index") segments.pop();
180
+ return `/${segments.map(decode).join("/")}`;
181
+ }
182
+ function carriesQuery(href, params) {
183
+ const query = href.split("#")[0]?.split("?")[1];
184
+ if (!query) return true;
185
+ for (const [key, value] of new URLSearchParams(query))
186
+ if (![params[key]].flat().some((v) => String(v) === value)) return false;
187
+ return true;
188
+ }
107
189
  function needDismiss(fn) {
108
190
  if (!fn)
109
191
  throw new Error(
@@ -114,18 +196,43 @@ function needDismiss(fn) {
114
196
  function routerTools(router, options = {}) {
115
197
  const { navigation } = options;
116
198
  const go = (fn, value) => navigation ? settle(navigation, fn).then(() => value) : (fn(), value);
199
+ const read = (nav) => ({
200
+ ...routeFromState(nav.getRootState()),
201
+ canGoBack: router.canGoBack()
202
+ });
203
+ const goTo = (fn, href) => navigation ? settle(navigation, fn).then(() => settle2()).then(() => landed(navigation, href)) : (fn(), href);
204
+ const landed = (navigation2, href) => {
205
+ if (navigation2.isReady && !navigation2.isReady())
206
+ return {
207
+ href,
208
+ warning: "The navigation container has not mounted yet, so the route the app ended on could not be read."
209
+ };
210
+ const route = read(navigation2);
211
+ const onNotFound = route.segments.includes(NOT_FOUND);
212
+ if (onNotFound && !href.includes(NOT_FOUND))
213
+ throw new Error(
214
+ `${href} matched no route: the app is on +not-found (${route.pathname}). Check the href against the app's routes.`
215
+ );
216
+ const want = targetPath(href);
217
+ if (want !== null && (want !== route.pathname || !carriesQuery(href, route.params)))
218
+ return {
219
+ ...route,
220
+ warning: `asked for ${href} but the app is on ${route.href}; it may have redirected, or the navigation did not run`
221
+ };
222
+ return route;
223
+ };
117
224
  return {
118
225
  "router.navigate": {
119
- description: "Go to a route, reusing it if it is already in the stack.",
120
- run: (href) => go(() => router.navigate(href), href)
226
+ description: "Go to a route, reusing it if it is already in the stack. With the navigation container it returns the route it ended on as an object (with a `warning` if that is not the href, path or query) and throws on +not-found.",
227
+ run: (href) => goTo(() => router.navigate(href), href)
121
228
  },
122
229
  "router.push": {
123
230
  description: "Push a route.",
124
- run: (href) => go(() => router.push(href), href)
231
+ run: (href) => goTo(() => router.push(href), href)
125
232
  },
126
233
  "router.replace": {
127
234
  description: "Replace the current route.",
128
- run: (href) => go(() => router.replace(href), href)
235
+ run: (href) => goTo(() => router.replace(href), href)
129
236
  },
130
237
  "router.back": {
131
238
  description: "Pop the navigator once if it can. Returns whether it could. Ignores modals and sheets the app renders itself, so it can pop the screen under one: close those with the app's own tool, e.g. modal.close.",
@@ -157,8 +264,7 @@ function routerTools(router, options = {}) {
157
264
  );
158
265
  if (navigation.isReady && !navigation.isReady())
159
266
  throw new Error("The navigation container has not mounted yet.");
160
- const state = navigation.getRootState();
161
- return { ...routeFromState(state), canGoBack: router.canGoBack() };
267
+ return read(navigation);
162
268
  }
163
269
  }
164
270
  };
@@ -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 imperative `router` from expo-router. Passed in, so this adds no dependency. */
4
4
  type RouterLike = {
@@ -0,0 +1,266 @@
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/jotai.ts
21
+ var jotai_exports = {};
22
+ __export(jotai_exports, {
23
+ jotaiTools: () => jotaiTools
24
+ });
25
+ module.exports = __toCommonJS(jotai_exports);
26
+
27
+ // src/runtime/to-json.ts
28
+ function toJson(value) {
29
+ if (value === void 0) return null;
30
+ const seen = /* @__PURE__ */ new WeakSet();
31
+ const text = JSON.stringify(value, (_key, v) => {
32
+ if (typeof v === "bigint") return v.toString();
33
+ if (typeof v === "function" || typeof v === "symbol") return void 0;
34
+ if (v instanceof Error) return { name: v.name, message: v.message };
35
+ if (v instanceof Map) return Object.fromEntries(v);
36
+ if (v instanceof Set) return [...v];
37
+ if (v && typeof v === "object") {
38
+ if (seen.has(v)) return "[Circular]";
39
+ seen.add(v);
40
+ }
41
+ return v;
42
+ });
43
+ return text === void 0 ? null : JSON.parse(text);
44
+ }
45
+
46
+ // src/adapters/store-common.ts
47
+ var isObject = (value) => typeof value === "object" && value !== null;
48
+ function toPath(part) {
49
+ if (typeof part === "string") return part === "" ? [] : part.split(".");
50
+ if (typeof part === "number") return [part];
51
+ if (Array.isArray(part) && part.every((p) => typeof p === "string" || typeof p === "number"))
52
+ return part;
53
+ throw new Error(
54
+ `A path is a dotted string or an array of segments, got ${JSON.stringify(part)}`
55
+ );
56
+ }
57
+ function at(value, path) {
58
+ return path.reduce(
59
+ (v, k) => v?.[k],
60
+ value
61
+ );
62
+ }
63
+ var kindOf = (v) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
64
+ function readState(state, given, tool = "store.get") {
65
+ const args = given.filter((a) => a != null);
66
+ const last = args.at(-1);
67
+ const isOptions = isObject(last) && !Array.isArray(last);
68
+ const options = isOptions ? last : {};
69
+ const unknown = Object.keys(options).filter((k) => k !== "pick" && k !== "keys");
70
+ if (unknown.length)
71
+ throw new Error(
72
+ `Unknown ${tool} option ${unknown.map((k) => `"${k}"`).join(", ")}. Options: pick, keys`
73
+ );
74
+ const value = at(state, (isOptions ? args.slice(0, -1) : args).flatMap(toPath));
75
+ if (options.pick !== void 0) {
76
+ if (!Array.isArray(options.pick))
77
+ throw new Error(`${tool} pick is an array of paths`);
78
+ return Object.fromEntries(
79
+ options.pick.map((p) => [
80
+ Array.isArray(p) ? p.join(".") : String(p),
81
+ at(value, toPath(p))
82
+ ])
83
+ );
84
+ }
85
+ if (options.keys)
86
+ return isObject(value) ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, kindOf(v)])) : kindOf(value);
87
+ return value;
88
+ }
89
+ var PREVIEW = 200;
90
+ function preview(value) {
91
+ try {
92
+ const safe = toJson(value);
93
+ const text = JSON.stringify(safe);
94
+ if (text.length <= PREVIEW) return safe;
95
+ return `${text.slice(0, PREVIEW)}\u2026 (${text.length} chars)`;
96
+ } catch {
97
+ return "[unserializable]";
98
+ }
99
+ }
100
+ var REDACTED = "[redacted]";
101
+ function redactValue(redact, name, value, path, seen = []) {
102
+ if (!redact || typeof value === "function") return value;
103
+ if (typeof redact === "function") {
104
+ if (path) value = redact(name, path, value);
105
+ } else if (path && redact[name]?.some((p) => path === p || path.startsWith(`${p}.`))) {
106
+ return REDACTED;
107
+ }
108
+ if (!isObject(value) || seen.includes(value)) return value;
109
+ const next = [...seen, value];
110
+ const entries = Object.entries(value).map(([k, v]) => [
111
+ k,
112
+ redactValue(redact, name, v, path ? `${path}.${k}` : k, next)
113
+ ]);
114
+ return Array.isArray(value) ? entries.map(([, v]) => v) : Object.fromEntries(entries);
115
+ }
116
+ function redactNamed(redact, name, value) {
117
+ if (!redact) return value;
118
+ if (typeof redact === "function") {
119
+ return redactValue(redact, name, redact(name, "", value), "");
120
+ }
121
+ const rule = redact[name];
122
+ if (rule === true) return REDACTED;
123
+ return rule ? redactValue({ [name]: rule }, name, value, "") : value;
124
+ }
125
+ var warned = /* @__PURE__ */ new Set();
126
+ function warnUnknownRedact(redact, known, adapter) {
127
+ if (!redact || typeof redact === "function") return;
128
+ for (const key of Object.keys(redact)) {
129
+ if (known.includes(key) || warned.has(`${adapter}:${key}`)) continue;
130
+ warned.add(`${adapter}:${key}`);
131
+ console.warn(`[agent-bridge] ${adapter} redact key "${key}" matches no ${adapter === "redux" ? "slice" : "atom"}, so it hides nothing. Known: ${known.join(", ")}`);
132
+ }
133
+ }
134
+
135
+ // src/adapters/jotai.ts
136
+ var snapshots = /* @__PURE__ */ new WeakMap();
137
+ var isWritable = (atom) => typeof atom.write === "function";
138
+ var same = (a, b) => {
139
+ if (Object.is(a, b)) return true;
140
+ try {
141
+ return JSON.stringify(a) === JSON.stringify(b);
142
+ } catch {
143
+ return false;
144
+ }
145
+ };
146
+ function jotaiTools(store, atoms, options = {}) {
147
+ let saved = snapshots.get(store);
148
+ if (!saved) snapshots.set(store, saved = /* @__PURE__ */ new Map());
149
+ const entries = saved;
150
+ warnUnknownRedact(options.redact, Object.keys(atoms), "jotai");
151
+ const find = (name) => {
152
+ const atom = atoms[name];
153
+ if (!atom) throw new Error(`Unknown atom "${name}". Known: ${Object.keys(atoms).join(", ")}`);
154
+ return atom;
155
+ };
156
+ const show = (name, value) => redactNamed(options.redact, name, value);
157
+ const read = async (atom) => {
158
+ const store2 = store;
159
+ const unsub = store2.sub?.(atom, () => {
160
+ });
161
+ try {
162
+ return await store.get(atom);
163
+ } finally {
164
+ unsub?.();
165
+ }
166
+ };
167
+ return {
168
+ "jotai.list": {
169
+ description: 'Atom names, each "read-write" or "read-only" (derived atoms with no write cannot be set). Action atoms (write-only, like atom(null, write)) show as read-write but jotai.set refuses them.',
170
+ maxArgs: 0,
171
+ run: () => Object.fromEntries(
172
+ Object.entries(atoms).map(([name, a]) => [name, isWritable(a) ? "read-write" : "read-only"])
173
+ )
174
+ },
175
+ "jotai.get": {
176
+ description: 'Value of an atom, or a path inside it: ("user"), ("user", "address.city"), ("user", "address", "city") or ("user", ["address", "city"]). Async atoms are awaited. A last { pick: ["a.b", "c"] } returns just those fields; { keys: true } lists keys and their types without values. Atoms and paths configured with redact print as "[redacted]". Only atoms in the store you passed are visible: a <Provider> without a store prop has its own store.',
177
+ run: async (name, ...rest) => readState(show(name, await store.get(find(name))), rest, "jotai.get")
178
+ },
179
+ "jotai.set": {
180
+ maxArgs: 2,
181
+ description: `Set a writable atom to a value: ("count", 5). Returns the atom's new value. An atom is snapshotted on its first jotai.set since the last restore (or jotai.commit); bridge.restore sets it back to that value, and leaves alone an atom the app changed after you (returned in {skipped}). Changes made before that, e.g. by tapping the UI, are not in it: set an atom to its current value first to snapshot early, and see bridge.pending. Read-only atoms are refused, and so is any atom whose value is not the one you sent afterwards (an action atom or atomWithReducer: its write already ran and cannot be undone). A writable derived atom is set through its own write, and restored the same way.`,
182
+ run: async (name, value) => {
183
+ const atom = find(name);
184
+ if (!isWritable(atom))
185
+ throw new Error(`Atom "${name}" is read-only: it has no write, so it cannot be set`);
186
+ const first = !entries.has(name);
187
+ if (first) {
188
+ const original = await read(atom);
189
+ entries.set(name, { original, after: original });
190
+ }
191
+ try {
192
+ await store.set(atom, value);
193
+ } catch (e) {
194
+ if (first) entries.delete(name);
195
+ throw e;
196
+ }
197
+ const now = await store.get(atom);
198
+ if (!same(now, value)) {
199
+ if (first) entries.delete(name);
200
+ throw new Error(
201
+ `Atom "${name}" did not take the value: it reads ${JSON.stringify(preview(show(name, now)))} after the set. Its write is probably an action or a reducer, which already ran and cannot be undone, so it is not snapshotted.`
202
+ );
203
+ }
204
+ entries.get(name).after = now;
205
+ return show(name, now);
206
+ }
207
+ },
208
+ "jotai.commit": {
209
+ maxArgs: 1,
210
+ description: "Keep an atom as it is now: drops its pending snapshot, so bridge.restore leaves it alone and the next jotai.set snapshots again from this value. Returns { atom, committed } (committed: false when nothing was pending).",
211
+ run: (name) => ({ atom: name, committed: entries.delete(name) })
212
+ },
213
+ "jotai.restore": {
214
+ maxArgs: 0,
215
+ pending: ({ detail } = {}) => {
216
+ if (!entries.size) return false;
217
+ if (!detail) return true;
218
+ const out = {};
219
+ for (const [name, e] of entries) {
220
+ const atom = atoms[name];
221
+ const raw = atom ? store.get(atom) : void 0;
222
+ if (atom && same(e.original, raw)) continue;
223
+ out[name] = {
224
+ snapshot: preview(show(name, e.original)),
225
+ current: !atom ? "(no atom has this name now)" : raw instanceof Promise ? "(async)" : preview(show(name, raw))
226
+ };
227
+ }
228
+ return out;
229
+ },
230
+ description: "Undo the agent: set each atom changed with jotai.set back to its value at the first set (jotai.commit drops a snapshot), newest first. Returns their names; an atom the app changed after the agent is left alone and returned as { restored, skipped }. If an atom cannot be set back (or the write does not take), the rest are still restored and the error names it; its snapshot stays.",
231
+ run: async () => {
232
+ const done = [];
233
+ const skipped = [];
234
+ const failed = [];
235
+ for (const [name, e] of [...entries].reverse()) {
236
+ const atom = atoms[name];
237
+ if (!atom) {
238
+ failed.push(`${name} (no atom has this name now)`);
239
+ continue;
240
+ }
241
+ try {
242
+ if (!same(await store.get(atom), e.after)) {
243
+ entries.delete(name);
244
+ skipped.push(name);
245
+ continue;
246
+ }
247
+ await store.set(atom, e.original);
248
+ if (!same(await store.get(atom), e.original)) {
249
+ failed.push(`${name} (the write did not change it back)`);
250
+ continue;
251
+ }
252
+ entries.delete(name);
253
+ done.push(name);
254
+ } catch (err) {
255
+ failed.push(`${name} (${err instanceof Error ? err.message : String(err)})`);
256
+ }
257
+ }
258
+ if (failed.length)
259
+ throw new Error(
260
+ `Could not restore ${failed.join(", ")}. Restored: ${done.join(", ") || "none"}. The snapshots of the failed atoms are kept.`
261
+ );
262
+ return skipped.length ? { restored: done, skipped } : done;
263
+ }
264
+ }
265
+ };
266
+ }
@@ -0,0 +1,19 @@
1
+ import { T as Tools } from '../types-qlwdcGjt.cjs';
2
+ import { N as NamedRedact } from '../store-common-j0cHHvBr.cjs';
3
+
4
+ /** Anything with a Jotai store's shape: getDefaultStore(), createStore(), or the Provider's store. */
5
+ type JotaiStoreLike = {
6
+ get(atom: any): unknown;
7
+ set(atom: any, ...args: any[]): unknown;
8
+ };
9
+ /** A Jotai atom; the adapter only checks whether it has a `write`. */
10
+ type JotaiAtomLike = object;
11
+
12
+ type JotaiToolsOptions = {
13
+ /** By atom name, as `{ token: true }` or `{ user: ['phone'] }`, or a hook `(atom, path, value)`. */
14
+ redact?: NamedRedact;
15
+ };
16
+ /** Read, set and restore named Jotai atoms. */
17
+ declare function jotaiTools(store: JotaiStoreLike, atoms: Record<string, JotaiAtomLike>, options?: JotaiToolsOptions): Tools;
18
+
19
+ export { type JotaiAtomLike, NamedRedact as JotaiRedact, type JotaiStoreLike, type JotaiToolsOptions, jotaiTools };
@@ -24,6 +24,10 @@ __export(react_native_mmkv_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(react_native_mmkv_exports);
26
26
  var originals = /* @__PURE__ */ new WeakMap();
27
+ var written = /* @__PURE__ */ new WeakMap();
28
+ var rev = 0;
29
+ var idOf = (mmkv) => typeof mmkv.id === "string" ? mmkv.id : void 0;
30
+ var isPlain = (v) => !!v && typeof v === "object" && !Array.isArray(v);
27
31
  function remove(mmkv, name, key) {
28
32
  if (mmkv.remove) mmkv.remove(key);
29
33
  else if (mmkv.delete) mmkv.delete(key);
@@ -45,6 +49,12 @@ function mmkvTools(instances) {
45
49
  const exists = mmkv.contains?.(key) ?? mmkv.getAllKeys().includes(key);
46
50
  saved.set(key, exists ? mmkv.getString(key) ?? null : void 0);
47
51
  };
52
+ const note = (mmkv, key, value) => {
53
+ let map = written.get(mmkv);
54
+ if (!map) written.set(mmkv, map = /* @__PURE__ */ new Map());
55
+ map.set(key, value);
56
+ rev += 1;
57
+ };
48
58
  return {
49
59
  "mmkv.list": {
50
60
  description: "Instance names.",
@@ -68,6 +78,7 @@ function mmkvTools(instances) {
68
78
  const mmkv = get(name);
69
79
  beforeChange(mmkv, key);
70
80
  mmkv.set(key, stored);
81
+ note(mmkv, key, typeof stored === "string" ? stored : "native");
71
82
  return true;
72
83
  }
73
84
  },
@@ -77,11 +88,78 @@ function mmkvTools(instances) {
77
88
  const mmkv = get(name);
78
89
  beforeChange(mmkv, key);
79
90
  remove(mmkv, name, key);
91
+ note(mmkv, key, null);
80
92
  return true;
81
93
  }
82
94
  },
83
95
  "mmkv.restore": {
84
96
  pending: () => Object.values(instances).some((m) => originals.has(m)),
97
+ journalRev: () => rev,
98
+ // Only string values: an original or written number can't be read back as one.
99
+ journal: () => {
100
+ const out = {};
101
+ for (const [name, mmkv] of Object.entries(instances)) {
102
+ const saved = originals.get(mmkv);
103
+ for (const [key, original] of saved ?? []) {
104
+ const now = written.get(mmkv)?.get(key);
105
+ if (original === null || now === void 0 || now === "native") continue;
106
+ const id = idOf(mmkv);
107
+ (out[name] ?? (out[name] = {}))[key] = { original: original ?? null, written: now, ...id !== void 0 && { id } };
108
+ }
109
+ }
110
+ return Object.keys(out).length ? out : void 0;
111
+ },
112
+ adopt: (raw) => {
113
+ const data = isPlain(raw) ? raw : {};
114
+ const result = { adopted: [], already: [], changed: [] };
115
+ const retry = {};
116
+ for (const [name, keys] of Object.entries(data)) {
117
+ if (!isPlain(keys)) {
118
+ (result.invalid ?? (result.invalid = [])).push(name);
119
+ continue;
120
+ }
121
+ const mmkv = instances[name];
122
+ if (!mmkv) {
123
+ retry[name] = keys;
124
+ continue;
125
+ }
126
+ for (const [key, entry] of Object.entries(keys)) {
127
+ const label = `${name}:${key}`;
128
+ const e = entry;
129
+ const text = (v) => v === null || typeof v === "string";
130
+ if (!isPlain(e) || !text(e.original) || !text(e.written) || e.id !== void 0 && typeof e.id !== "string") {
131
+ (result.invalid ?? (result.invalid = [])).push(label);
132
+ continue;
133
+ }
134
+ const now = idOf(mmkv);
135
+ if (e.id !== void 0 && now !== void 0 && e.id !== now) {
136
+ (result.skipped ?? (result.skipped = [])).push(label);
137
+ continue;
138
+ }
139
+ const exists = mmkv.contains?.(key) ?? mmkv.getAllKeys().includes(key);
140
+ if (!exists && e.written === null && !(e.id !== void 0 && e.id === now)) {
141
+ result.already.push(label);
142
+ continue;
143
+ }
144
+ const current = exists ? mmkv.getString(key) ?? void 0 : null;
145
+ const saved = originals.get(mmkv);
146
+ const savedNow = saved?.get(key);
147
+ const savedFake = saved?.has(key) && savedNow !== null && (savedNow ?? null) === entry.written && (entry.written !== null || e.id !== void 0 && e.id === now);
148
+ if (current === entry.original && !savedFake) {
149
+ result.already.push(label);
150
+ } else if (savedFake || current === entry.written) {
151
+ let map = originals.get(mmkv);
152
+ if (!map) originals.set(mmkv, map = /* @__PURE__ */ new Map());
153
+ map.set(key, entry.original ?? void 0);
154
+ if (!savedFake) note(mmkv, key, entry.written);
155
+ else rev += 1;
156
+ result.adopted.push(label);
157
+ } else result.changed.push(label);
158
+ }
159
+ }
160
+ if (Object.keys(retry).length) result.retry = retry;
161
+ return result;
162
+ },
85
163
  description: "Undo the agent: put back keys changed with mmkv.set or mmkv.delete, deleting ones that did not exist. Returns how many.",
86
164
  run: () => {
87
165
  let restored = 0;
@@ -95,7 +173,9 @@ function mmkvTools(instances) {
95
173
  restored += 1;
96
174
  }
97
175
  originals.delete(mmkv);
176
+ written.delete(mmkv);
98
177
  }
178
+ rev += 1;
99
179
  return restored;
100
180
  }
101
181
  }
@@ -1,7 +1,9 @@
1
- import { T as Tools } from '../types-BmpykP1P.cjs';
1
+ import { T as Tools } from '../types-qlwdcGjt.cjs';
2
2
 
3
3
  /** The MMKV surface this adapter uses. v3 has `delete`, v4 has `remove`. */
4
4
  type MMKVLike = {
5
+ /** MMKV v2 and v3 instances carry their `id`; v4 does not. Used only to tell instances apart. */
6
+ id?: string;
5
7
  getAllKeys: () => string[];
6
8
  getString: (key: string) => string | undefined;
7
9
  set: (key: string, value: string | number | boolean) => void;
@@ -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
  /** What the adapter needs from a react-native-webview instance. */
4
4
  type WebViewLike = {