@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
@@ -0,0 +1,293 @@
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/react-navigation.ts
21
+ var react_navigation_exports = {};
22
+ __export(react_navigation_exports, {
23
+ navTools: () => navTools
24
+ });
25
+ module.exports = __toCommonJS(react_navigation_exports);
26
+
27
+ // src/runtime/screen/settle.ts
28
+ var STATE = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/commits");
29
+ var UPDATE_LANES = (1 << 22) - 1;
30
+ var hook = () => globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
31
+ var now = () => performance.now();
32
+ function install() {
33
+ const h = hook();
34
+ if (!h) return null;
35
+ const current = h.onCommitFiberRoot;
36
+ const existing = current?.[STATE];
37
+ if (existing) return existing;
38
+ const state = { commits: 0, listeners: /* @__PURE__ */ new Set() };
39
+ const wrapped = function(...args) {
40
+ state.commits += 1;
41
+ for (const listener of state.listeners) {
42
+ try {
43
+ listener();
44
+ } catch {
45
+ }
46
+ }
47
+ return current?.apply(this, args);
48
+ };
49
+ Object.defineProperty(wrapped, STATE, { value: state });
50
+ h.onCommitFiberRoot = wrapped;
51
+ return state;
52
+ }
53
+ function busy() {
54
+ const h = hook();
55
+ if (!h?.renderers || !h.getFiberRoots) return false;
56
+ for (const id of h.renderers.keys()) {
57
+ for (const root of h.getFiberRoots(id)) {
58
+ const pending = root.pendingLanes;
59
+ if (typeof pending !== "number") continue;
60
+ if (pending & ~(root.suspendedLanes ?? 0) & UPDATE_LANES) return true;
61
+ }
62
+ }
63
+ return false;
64
+ }
65
+ var frame = () => new Promise((resolve) => {
66
+ const raf = globalThis.requestAnimationFrame;
67
+ if (raf) raf(() => resolve());
68
+ else setTimeout(resolve, 16);
69
+ });
70
+ async function settle(options = {}) {
71
+ const maxMs = options.maxMs ?? 500;
72
+ const t0 = now();
73
+ const state = install();
74
+ const start = state?.commits ?? 0;
75
+ const result = () => ({
76
+ commits: (state?.commits ?? 0) - start,
77
+ ms: Math.round((now() - t0) * 100) / 100
78
+ });
79
+ await new Promise((resolve) => setTimeout(resolve, 0));
80
+ if (!state) return result();
81
+ let seen = state.commits;
82
+ if (seen === start && !busy()) return result();
83
+ while (now() - t0 < maxMs) {
84
+ await frame();
85
+ if (state.commits === seen && !busy()) break;
86
+ seen = state.commits;
87
+ }
88
+ return result();
89
+ }
90
+
91
+ // src/adapters/react-navigation.ts
92
+ var SETTLE_MS = 500;
93
+ var focused = (state) => state.routes[state.index ?? state.routes.length - 1];
94
+ function read(nav) {
95
+ if (!nav.isReady()) throw new Error("The navigation container has not mounted yet.");
96
+ const root = nav.getRootState();
97
+ const path = [];
98
+ let holder = root;
99
+ let stackHolder = root?.type === "stack" ? root : void 0;
100
+ let leaf = root && focused(root);
101
+ while (leaf) {
102
+ path.push(leaf.name);
103
+ if (!leaf.state?.routes.length) break;
104
+ holder = leaf.state;
105
+ if (holder.type === "stack") stackHolder = holder;
106
+ leaf = focused(holder);
107
+ }
108
+ const routes = stackHolder ? stackHolder.routes.slice(0, (stackHolder.index ?? stackHolder.routes.length - 1) + 1) : leaf ? [leaf] : [];
109
+ const out = {
110
+ name: leaf?.name ?? "",
111
+ path,
112
+ stack: routes.map((r) => r.params ? { name: r.name, params: r.params } : { name: r.name }),
113
+ canGoBack: nav.canGoBack()
114
+ };
115
+ if (leaf?.params) out.params = leaf.params;
116
+ return out;
117
+ }
118
+ function focusedNavigators(root) {
119
+ let holder = root;
120
+ let stack = root?.type === "stack" ? root : void 0;
121
+ let leaf = root && focused(root);
122
+ while (leaf?.state?.routes.length) {
123
+ holder = leaf.state;
124
+ if (holder.type === "stack") stack = holder;
125
+ leaf = focused(holder);
126
+ }
127
+ return { holder, stack };
128
+ }
129
+ function wantedChain(name, params) {
130
+ const chain = [name];
131
+ let p = params;
132
+ while (p && typeof p.screen === "string") {
133
+ chain.push(p.screen);
134
+ p = p.params;
135
+ }
136
+ return chain;
137
+ }
138
+ var endsWithChain = (path, chain) => path.some((_, i) => chain.every((n, j) => path[i + j] === n));
139
+ function allRouteNames(state, into = /* @__PURE__ */ new Set()) {
140
+ if (!state) return into;
141
+ for (const n of state.routeNames ?? []) into.add(n);
142
+ for (const r of state.routes) {
143
+ into.add(r.name);
144
+ allRouteNames(r.state, into);
145
+ }
146
+ return into;
147
+ }
148
+ function nestedNavigate(state) {
149
+ const route = focused(state);
150
+ const next = route.state && route.state.routes.length ? nestedNavigate(route.state) : void 0;
151
+ if (!next) return [route.name, route.params];
152
+ return [route.name, { ...route.params, screen: next[0], ...next[1] ? { params: next[1] } : {} }];
153
+ }
154
+ function navTools(nav, options = {}) {
155
+ const parse = options.linking?.getStateFromPath ?? options.getStateFromPath;
156
+ const wait = (go) => new Promise((resolve, reject) => {
157
+ let off;
158
+ const done = () => {
159
+ off?.();
160
+ clearTimeout(timer);
161
+ resolve();
162
+ };
163
+ const timer = setTimeout(done, nav.addListener ? SETTLE_MS : 0);
164
+ off = nav.addListener?.("state", done);
165
+ try {
166
+ go();
167
+ } catch (error) {
168
+ off?.();
169
+ clearTimeout(timer);
170
+ reject(error);
171
+ }
172
+ });
173
+ const ready = () => {
174
+ if (!nav.isReady()) throw new Error("The navigation container has not mounted yet.");
175
+ };
176
+ const goTo = async (go, want) => {
177
+ ready();
178
+ const before = nav.getRootState();
179
+ await wait(go);
180
+ await settle();
181
+ const route = read(nav);
182
+ const missed = want && !endsWithChain(route.path, want.chain);
183
+ if (want && (missed || want.mustChange && nav.getRootState() === before))
184
+ return {
185
+ ...route,
186
+ warning: `asked for ${want.label} but the app is on ${route.path.join(" > ")}; the screen may be in a navigator that is not mounted, or the navigation did not run`
187
+ };
188
+ return route;
189
+ };
190
+ const checkName = (name) => {
191
+ ready();
192
+ if (typeof name !== "string" || !name) throw new Error("give a route name, e.g. nav.navigate Profile");
193
+ const known = allRouteNames(nav.getRootState());
194
+ if (!known.has(name))
195
+ throw new Error(
196
+ `no route named "${name}". Known routes: ${[...known].join(", ")}. If it is inside a navigator that has not been opened yet, navigate to the parent with {screen: name}.`
197
+ );
198
+ return name;
199
+ };
200
+ const asPath = (target) => {
201
+ if (target.startsWith("/")) return target;
202
+ const prefix = options.linking?.prefixes?.find((p) => target.startsWith(p));
203
+ return prefix === void 0 ? void 0 : `/${target.slice(prefix.length).replace(/^\/+/, "")}`;
204
+ };
205
+ const fromPath = (path) => {
206
+ if (!parse)
207
+ throw new Error(
208
+ "paths need getStateFromPath: navTools(ref, { linking, getStateFromPath }) with getStateFromPath from @react-navigation/native. Or pass a route name."
209
+ );
210
+ if (options.linking?.config === void 0)
211
+ throw new Error(
212
+ 'paths need linking.config (the screens map). With a static config or linking {enabled: "auto"} there is none to pass: use a route name.'
213
+ );
214
+ const state = parse(path, options.linking?.config);
215
+ if (!state || !state.routes.length)
216
+ throw new Error(`${path} matched no route. Check it against your linking config.`);
217
+ const [name, nested] = nestedNavigate(state);
218
+ return [checkName(name), nested];
219
+ };
220
+ const needStack = (verb) => {
221
+ ready();
222
+ const { holder } = focusedNavigators(nav.getRootState());
223
+ if (holder?.type && holder.type !== "stack")
224
+ throw new Error(
225
+ `the focused navigator is a ${holder.type} navigator, so ${verb} would act on a parent. Use nav.navigate.`
226
+ );
227
+ };
228
+ const action = (type, name, params) => ({
229
+ type,
230
+ payload: { name, params }
231
+ });
232
+ return {
233
+ "nav.navigate": {
234
+ description: "Go to a route by name and params ({screen, params} reaches a nested screen), or by a path like /post/7 (or a URL with one of linking.prefixes) when getStateFromPath and linking.config are set. In a stack it goes back to the screen if that is focused and otherwise pushes (React Navigation 7; v6 reuses a screen already in the stack): use nav.popTo to return to an earlier one. Returns the route it ended on (name, params, path, stack, canGoBack), with a `warning` if the screens asked for are not at the end of the path. Throws on an unknown name.",
235
+ run: (target, params) => {
236
+ const path = typeof target === "string" ? asPath(target) : void 0;
237
+ if (path !== void 0) {
238
+ const [name2, nested] = fromPath(path);
239
+ return goTo(() => nav.navigate(name2, nested), {
240
+ chain: wantedChain(name2, nested),
241
+ label: target
242
+ });
243
+ }
244
+ const name = checkName(target);
245
+ const chain = wantedChain(name, params);
246
+ return goTo(() => nav.navigate(name, params), { chain, label: chain.join(" > ") });
247
+ }
248
+ },
249
+ "nav.push": {
250
+ description: "Push a route onto the focused stack, even if it is already there. Throws when the focused navigator is tabs or a drawer (use nav.navigate). Same reply as nav.navigate.",
251
+ run: (target, params) => {
252
+ const name = checkName(target);
253
+ needStack("push");
254
+ return goTo(() => nav.dispatch(action("PUSH", name, params)), { chain: [name], label: name, mustChange: true });
255
+ }
256
+ },
257
+ "nav.replace": {
258
+ description: "Replace the current screen in its stack. Throws when the focused navigator is tabs or a drawer (use nav.navigate). Same reply as nav.navigate.",
259
+ run: (target, params) => {
260
+ const name = checkName(target);
261
+ needStack("replace");
262
+ return goTo(() => nav.dispatch(action("REPLACE", name, params)), { chain: [name], label: name, mustChange: true });
263
+ }
264
+ },
265
+ "nav.popTo": {
266
+ description: "Go back to the nearest earlier screen with this name in the focused stack, or replace the current screen with it if there is none (React Navigation 7 only). Same reply as nav.navigate.",
267
+ run: (target, params) => {
268
+ const name = checkName(target);
269
+ return goTo(() => nav.dispatch({ type: "POP_TO", payload: { name, params } }), { chain: [name], label: name });
270
+ }
271
+ },
272
+ "nav.popToTop": {
273
+ description: "Pop the nearest stack back to its first screen. If that stack is already at its first screen it does nothing, so a parent stack is never popped. Returns the route it ended on.",
274
+ run: async () => {
275
+ ready();
276
+ const { stack } = focusedNavigators(nav.getRootState());
277
+ if (stack && (stack.index ?? stack.routes.length - 1) === 0) return read(nav);
278
+ return goTo(() => nav.dispatch({ type: "POP_TO_TOP" }));
279
+ }
280
+ },
281
+ "nav.back": {
282
+ description: "Go back once if it can. Returns the route it ended on, or false if it could not. Ignores modals and sheets the app renders itself: close those with the app's own tool.",
283
+ run: () => {
284
+ ready();
285
+ return nav.canGoBack() ? goTo(() => nav.goBack()) : false;
286
+ }
287
+ },
288
+ "nav.current": {
289
+ description: "The current route: name, params, path (root navigator down to the screen), stack and canGoBack.",
290
+ run: () => read(nav)
291
+ }
292
+ };
293
+ }
@@ -0,0 +1,55 @@
1
+ import { T as Tools } from '../types-qlwdcGjt.cjs';
2
+
3
+ /** The container ref from `createNavigationContainerRef()` or `useNavigationContainerRef()`. Passed in, so this adds no dependency. */
4
+ type NavigationRefLike = {
5
+ getRootState(): unknown;
6
+ isReady(): boolean;
7
+ canGoBack(): boolean;
8
+ navigate(...args: never[]): void;
9
+ goBack(): void;
10
+ dispatch(action: never): void;
11
+ addListener?(type: 'state', callback: () => void): () => void;
12
+ };
13
+ type StateLike = {
14
+ type?: string;
15
+ index?: number;
16
+ routeNames?: string[];
17
+ routes: {
18
+ name: string;
19
+ params?: Record<string, unknown>;
20
+ state?: StateLike;
21
+ }[];
22
+ };
23
+ type GetStateFromPath = (path: string, config?: never) => StateLike | undefined;
24
+ type NavToolsOptions = {
25
+ /** Your `linking` object; its `config` is how a path like `/post/7` is matched to a screen. */
26
+ linking?: {
27
+ config?: unknown;
28
+ prefixes?: string[];
29
+ getStateFromPath?: GetStateFromPath;
30
+ };
31
+ /** `getStateFromPath` from `@react-navigation/native`, to let the tools take paths. */
32
+ getStateFromPath?: GetStateFromPath;
33
+ };
34
+ /** Where the app is. */
35
+ type CurrentRoute = {
36
+ /** The focused screen's name. */
37
+ name: string;
38
+ params?: Record<string, unknown>;
39
+ /** Names from the root navigator down to the focused screen, e.g. `['Main', 'Post']`. */
40
+ path: string[];
41
+ /** The screens in the focused navigator, bottom first. */
42
+ stack: {
43
+ name: string;
44
+ params?: Record<string, unknown>;
45
+ }[];
46
+ canGoBack: boolean;
47
+ };
48
+ /**
49
+ * Navigate without taps. Pass the container ref from `createNavigationContainerRef()`
50
+ * (or `useNavigationContainerRef()`). The tools are named `nav.*`, so they sit
51
+ * beside `routerTools` from the expo-router adapter without clashing.
52
+ */
53
+ declare function navTools(nav: NavigationRefLike, options?: NavToolsOptions): Tools;
54
+
55
+ export { type CurrentRoute, type NavToolsOptions, type NavigationRefLike, navTools };
@@ -0,0 +1,322 @@
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/redux.ts
21
+ var redux_exports = {};
22
+ __export(redux_exports, {
23
+ bridgeReducer: () => bridgeReducer,
24
+ reduxTools: () => reduxTools
25
+ });
26
+ module.exports = __toCommonJS(redux_exports);
27
+
28
+ // src/runtime/to-json.ts
29
+ function toJson(value) {
30
+ if (value === void 0) return null;
31
+ const seen = /* @__PURE__ */ new WeakSet();
32
+ const text = JSON.stringify(value, (_key, v) => {
33
+ if (typeof v === "bigint") return v.toString();
34
+ if (typeof v === "function" || typeof v === "symbol") return void 0;
35
+ if (v instanceof Error) return { name: v.name, message: v.message };
36
+ if (v instanceof Map) return Object.fromEntries(v);
37
+ if (v instanceof Set) return [...v];
38
+ if (v && typeof v === "object") {
39
+ if (seen.has(v)) return "[Circular]";
40
+ seen.add(v);
41
+ }
42
+ return v;
43
+ });
44
+ return text === void 0 ? null : JSON.parse(text);
45
+ }
46
+
47
+ // src/adapters/store-common.ts
48
+ var isObject = (value) => typeof value === "object" && value !== null;
49
+ function toPath(part) {
50
+ if (typeof part === "string") return part === "" ? [] : part.split(".");
51
+ if (typeof part === "number") return [part];
52
+ if (Array.isArray(part) && part.every((p) => typeof p === "string" || typeof p === "number"))
53
+ return part;
54
+ throw new Error(
55
+ `A path is a dotted string or an array of segments, got ${JSON.stringify(part)}`
56
+ );
57
+ }
58
+ function at(value, path) {
59
+ return path.reduce(
60
+ (v, k) => v?.[k],
61
+ value
62
+ );
63
+ }
64
+ var kindOf = (v) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
65
+ function readState(state, given, tool = "store.get") {
66
+ const args = given.filter((a) => a != null);
67
+ const last = args.at(-1);
68
+ const isOptions = isObject(last) && !Array.isArray(last);
69
+ const options = isOptions ? last : {};
70
+ const unknown = Object.keys(options).filter((k) => k !== "pick" && k !== "keys");
71
+ if (unknown.length)
72
+ throw new Error(
73
+ `Unknown ${tool} option ${unknown.map((k) => `"${k}"`).join(", ")}. Options: pick, keys`
74
+ );
75
+ const value = at(state, (isOptions ? args.slice(0, -1) : args).flatMap(toPath));
76
+ if (options.pick !== void 0) {
77
+ if (!Array.isArray(options.pick))
78
+ throw new Error(`${tool} pick is an array of paths`);
79
+ return Object.fromEntries(
80
+ options.pick.map((p) => [
81
+ Array.isArray(p) ? p.join(".") : String(p),
82
+ at(value, toPath(p))
83
+ ])
84
+ );
85
+ }
86
+ if (options.keys)
87
+ return isObject(value) ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, kindOf(v)])) : kindOf(value);
88
+ return value;
89
+ }
90
+ var PREVIEW = 200;
91
+ function preview(value) {
92
+ try {
93
+ const safe = toJson(value);
94
+ const text = JSON.stringify(safe);
95
+ if (text.length <= PREVIEW) return safe;
96
+ return `${text.slice(0, PREVIEW)}\u2026 (${text.length} chars)`;
97
+ } catch {
98
+ return "[unserializable]";
99
+ }
100
+ }
101
+ function differences(snapshot, current, show, only) {
102
+ if (!isObject(snapshot) || !isObject(current)) return {};
103
+ const out = {};
104
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(snapshot), ...Object.keys(current)])) {
105
+ if (only && !only.has(k)) continue;
106
+ const a = snapshot[k];
107
+ const b = current[k];
108
+ if (typeof a === "function" || typeof b === "function") continue;
109
+ if (!Object.is(a, b))
110
+ out[k] = { snapshot: preview(show(k, a)), current: preview(show(k, b)) };
111
+ }
112
+ return out;
113
+ }
114
+ var REDACTED = "[redacted]";
115
+ function redactValue(redact, name, value, path, seen = []) {
116
+ if (!redact || typeof value === "function") return value;
117
+ if (typeof redact === "function") {
118
+ if (path) value = redact(name, path, value);
119
+ } else if (path && redact[name]?.some((p) => path === p || path.startsWith(`${p}.`))) {
120
+ return REDACTED;
121
+ }
122
+ if (!isObject(value) || seen.includes(value)) return value;
123
+ const next = [...seen, value];
124
+ const entries = Object.entries(value).map(([k, v]) => [
125
+ k,
126
+ redactValue(redact, name, v, path ? `${path}.${k}` : k, next)
127
+ ]);
128
+ return Array.isArray(value) ? entries.map(([, v]) => v) : Object.fromEntries(entries);
129
+ }
130
+ function redactNamed(redact, name, value) {
131
+ if (!redact) return value;
132
+ if (typeof redact === "function") {
133
+ return redactValue(redact, name, redact(name, "", value), "");
134
+ }
135
+ const rule = redact[name];
136
+ if (rule === true) return REDACTED;
137
+ return rule ? redactValue({ [name]: rule }, name, value, "") : value;
138
+ }
139
+ var warned = /* @__PURE__ */ new Set();
140
+ function warnUnknownRedact(redact, known, adapter) {
141
+ if (!redact || typeof redact === "function") return;
142
+ for (const key of Object.keys(redact)) {
143
+ if (known.includes(key) || warned.has(`${adapter}:${key}`)) continue;
144
+ warned.add(`${adapter}:${key}`);
145
+ console.warn(`[agent-bridge] ${adapter} redact key "${key}" matches no ${adapter === "redux" ? "slice" : "atom"}, so it hides nothing. Known: ${known.join(", ")}`);
146
+ }
147
+ }
148
+
149
+ // src/adapters/redux.ts
150
+ var REPLACE = "@@agent-bridge/replace";
151
+ var stash;
152
+ var handled = 0;
153
+ function bridgeReducer(reducer) {
154
+ return ((state, action) => {
155
+ if (action.type !== REPLACE || !stash) return reducer(state, action);
156
+ handled += 1;
157
+ return stash.state;
158
+ });
159
+ }
160
+ var snapshots = /* @__PURE__ */ new WeakMap();
161
+ var NOT_WRAPPED = "The bridge could not replace the state: wrap the root reducer with bridgeReducer() from @avasapp/agent-bridge/redux (inside redux-persist's persistReducer, and again after store.replaceReducer), so the bridge can set slices and restore.";
162
+ var isRecord = (v) => isObject(v) && !Array.isArray(v);
163
+ var PERSIST = "_persist";
164
+ var withoutPersist = (v) => {
165
+ if (!isRecord(v) || !(PERSIST in v)) return v;
166
+ const { [PERSIST]: _, ...rest } = v;
167
+ return rest;
168
+ };
169
+ function reduxTools(store, options = {}) {
170
+ const state = () => store.getState();
171
+ const slices = () => isRecord(state()) ? Object.keys(state()).filter((k) => k !== PERSIST) : [];
172
+ warnUnknownRedact(options.redact, [...slices(), "state"], "redux");
173
+ const redacted = (value) => isRecord(value) ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactNamed(options.redact, k, v)])) : redactNamed(options.redact, "state", value);
174
+ const replaceWith = (next) => {
175
+ const before = handled;
176
+ stash = { state: next };
177
+ try {
178
+ store.dispatch({ type: REPLACE });
179
+ } finally {
180
+ stash = void 0;
181
+ }
182
+ if (handled === before) throw new Error(NOT_WRAPPED);
183
+ };
184
+ const snapshot = () => {
185
+ let snap = snapshots.get(store);
186
+ if (!snap) snapshots.set(store, snap = { slices: /* @__PURE__ */ new Map() });
187
+ return snap;
188
+ };
189
+ const empty = (snap) => {
190
+ if (!snap.slices.size && !snap.whole) snapshots.delete(store);
191
+ };
192
+ const slice = (name) => {
193
+ const s = state();
194
+ if (!isRecord(s) || !(name in s) || name === PERSIST)
195
+ throw new Error(`Unknown slice "${name}". Known: ${slices().join(", ")}`);
196
+ return s;
197
+ };
198
+ const changedSlices = (before, after) => {
199
+ if (!isRecord(before) || !isRecord(after)) return Object.is(before, after) ? [] : ["state"];
200
+ return [.../* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])].filter(
201
+ (k) => k !== PERSIST && !Object.is(before[k], after[k])
202
+ );
203
+ };
204
+ const diffOf = (k, before, now) => isRecord(before) && isRecord(now) ? differences(
205
+ before,
206
+ now,
207
+ (key, v) => redactNamed(options.redact, k, { [key]: v })[key]
208
+ ) : { "(value)": { snapshot: preview(redactNamed(options.redact, k, before)), current: preview(redactNamed(options.redact, k, now)) } };
209
+ return {
210
+ "redux.list": { description: "Slice names (the keys of the root state).", maxArgs: 0, run: () => slices() },
211
+ "redux.get": {
212
+ description: 'The whole state, or a path inside it, starting at a slice: (), ("auth"), ("auth.user.name"), ("auth", "user", "name") or ("auth", ["user", "name"]). A last { pick: ["a.b", "c"] } returns just those fields; { keys: true } lists keys and their types without values. Slices and paths configured with redact print as "[redacted]".',
213
+ run: (...rest) => readState(redacted(state()), rest, "redux.get")
214
+ },
215
+ "redux.dispatch": {
216
+ maxArgs: 1,
217
+ description: `Dispatch a plain action, e.g. { type: "cart/add", payload: "apple" }. Returns { changed: [slices] }. The first redux.dispatch since the last restore snapshots the WHOLE state, so bridge.restore puts all of it back, including what the app changed in other slices after that (redux.set is per slice: prefer it for a state you want to set). redux-persist's _persist is never put back. Thunks and functions cannot be sent.`,
218
+ run: (action) => {
219
+ if (!isObject(action) || typeof action.type !== "string")
220
+ throw new Error("redux.dispatch takes an action as { type, payload }");
221
+ if (action.type.startsWith("@@agent-bridge/"))
222
+ throw new Error(`Action types starting with "@@agent-bridge/" are reserved`);
223
+ const snap = snapshot();
224
+ const before = state();
225
+ const first = !snap.whole;
226
+ if (first) snap.whole = { state: withoutPersist(before) };
227
+ try {
228
+ store.dispatch(action);
229
+ } catch (e) {
230
+ if (first && Object.is(state(), before)) delete snap.whole;
231
+ empty(snap);
232
+ throw e;
233
+ }
234
+ const changed = changedSlices(before, state());
235
+ for (const k of changed) {
236
+ const entry = snap.slices.get(k);
237
+ if (entry) entry.after = state()[k];
238
+ }
239
+ empty(snap);
240
+ return { changed };
241
+ }
242
+ },
243
+ "redux.set": {
244
+ maxArgs: 2,
245
+ description: `Shallow-merge a partial into an object slice: ("auth", { token: "abc" }). Skips the app's reducers, so no other state moves. Array slices are refused. Returns only the keys you set, with their new values. Each slice is snapshotted on its first redux.set since the last restore (redux.commit drops them); bridge.restore puts that slice back, and leaves alone any slice the app changed after you (returned in {skipped}). Changes made before the first set, e.g. by tapping the UI, are not in the snapshot: run redux.set <slice> {} first to snapshot early, and see bridge.pending. Needs bridgeReducer() around the root reducer.`,
246
+ run: (name, partial) => {
247
+ const s = slice(name);
248
+ const current = s[name];
249
+ if (Array.isArray(current))
250
+ throw new Error(`Slice "${name}" is an array, so redux.set cannot merge into it. Use redux.dispatch with one of the app's actions`);
251
+ if (!isRecord(current)) throw new Error(`Slice "${name}" is not an object, so there is nothing to merge into`);
252
+ if (!isRecord(partial)) throw new Error("redux.set takes a slice name and an object (not an array)");
253
+ const snap = snapshot();
254
+ const track = !snap.whole && !snap.slices.has(name);
255
+ if (track) snap.slices.set(name, { original: current, after: current });
256
+ try {
257
+ replaceWith({ ...s, [name]: { ...current, ...partial } });
258
+ } catch (e) {
259
+ if (track) snap.slices.delete(name);
260
+ empty(snap);
261
+ throw e;
262
+ }
263
+ const now = state()[name];
264
+ const entry = snap.slices.get(name);
265
+ if (entry) entry.after = now;
266
+ return redactNamed(
267
+ options.redact,
268
+ name,
269
+ Object.fromEntries(Object.keys(partial).map((k) => [k, now[k]]))
270
+ );
271
+ }
272
+ },
273
+ "redux.commit": {
274
+ maxArgs: 0,
275
+ description: "Keep the state as it is now: drops the pending snapshots, so bridge.restore leaves it alone and the next redux.set or redux.dispatch snapshots again from here. Returns { committed } (false when nothing was pending).",
276
+ run: () => ({ committed: snapshots.delete(store) })
277
+ },
278
+ "redux.restore": {
279
+ maxArgs: 0,
280
+ pending: ({ detail } = {}) => {
281
+ const snap = snapshots.get(store);
282
+ if (!snap) return false;
283
+ if (!detail) return true;
284
+ const now = state();
285
+ const out = {};
286
+ if (snap.whole) {
287
+ const before = snap.whole.state;
288
+ for (const k of changedSlices(before, now))
289
+ out[k] = diffOf(k, isRecord(before) ? before[k] : before, isRecord(now) ? now[k] : now);
290
+ }
291
+ for (const [k, e] of snap.slices)
292
+ if (isRecord(now) && !Object.is(e.original, now[k])) out[k] = diffOf(k, e.original, now[k]);
293
+ return out;
294
+ },
295
+ description: 'Undo the agent: put back each slice changed with redux.set, and, if redux.dispatch was used, the whole state from before its first call (redux.commit drops the snapshots). Returns ["redux"], or [] when nothing was pending. A slice the app changed after the agent set it is left alone and returned as { restored, skipped }. With redux-persist (bridgeReducer inside persistReducer) the saved copy follows. Lost when the JS reloads: restore before app.reload.',
296
+ run: () => {
297
+ const snap = snapshots.get(store);
298
+ if (!snap) return [];
299
+ const cur = state();
300
+ let next = cur;
301
+ if (snap.whole) {
302
+ next = snap.whole.state;
303
+ if (isRecord(next) && isRecord(cur) && PERSIST in cur) next = { ...next, [PERSIST]: cur[PERSIST] };
304
+ }
305
+ const skipped = [];
306
+ let any = Boolean(snap.whole);
307
+ for (const [k, e] of snap.slices) {
308
+ if (!isRecord(cur) || !Object.is(cur[k], e.after)) {
309
+ skipped.push(k);
310
+ if (isRecord(next) && isRecord(cur) && k in cur) next = { ...next, [k]: cur[k] };
311
+ continue;
312
+ }
313
+ next = { ...next, [k]: e.original };
314
+ any = true;
315
+ }
316
+ if (any) replaceWith(next);
317
+ snapshots.delete(store);
318
+ return skipped.length ? { restored: any ? ["redux"] : [], skipped } : ["redux"];
319
+ }
320
+ }
321
+ };
322
+ }