@avasapp/agent-bridge 0.1.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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/adapters/expo-router.cjs +144 -0
  4. package/dist/adapters/expo-router.d.cts +38 -0
  5. package/dist/adapters/react-native-mmkv.cjs +102 -0
  6. package/dist/adapters/react-native-mmkv.d.cts +15 -0
  7. package/dist/adapters/tanstack-query.cjs +161 -0
  8. package/dist/adapters/tanstack-query.d.cts +10 -0
  9. package/dist/adapters/zustand.cjs +97 -0
  10. package/dist/adapters/zustand.d.cts +15 -0
  11. package/dist/chunk-UEWFQWCY.js +575 -0
  12. package/dist/cli.js +657 -0
  13. package/dist/client/index.cjs +568 -0
  14. package/dist/client/index.d.ts +112 -0
  15. package/dist/client/index.js +14 -0
  16. package/dist/expo/index.cjs +62 -0
  17. package/dist/expo/index.d.cts +9 -0
  18. package/dist/network/index.cjs +568 -0
  19. package/dist/network/index.d.cts +85 -0
  20. package/dist/noop/expo-router.cjs +26 -0
  21. package/dist/noop/expo.cjs +27 -0
  22. package/dist/noop/index.cjs +36 -0
  23. package/dist/noop/network.cjs +34 -0
  24. package/dist/noop/react-native-mmkv.cjs +26 -0
  25. package/dist/noop/tanstack-query.cjs +26 -0
  26. package/dist/noop/zustand.cjs +26 -0
  27. package/dist/runtime/index.cjs +933 -0
  28. package/dist/runtime/index.d.cts +69 -0
  29. package/dist/types-C6DUUHnB.d.cts +76 -0
  30. package/entries/expo-router.cjs +9 -0
  31. package/entries/expo.cjs +9 -0
  32. package/entries/index.cjs +9 -0
  33. package/entries/network.cjs +9 -0
  34. package/entries/react-native-mmkv.cjs +9 -0
  35. package/entries/tanstack-query.cjs +9 -0
  36. package/entries/zustand.cjs +9 -0
  37. package/package.json +120 -0
  38. package/skills/agent-bridge/SKILL.md +89 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Avas Enterprises
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # @avasapp/agent-bridge
2
+
3
+ Let coding agents drive a running React Native app directly. Seed data, flip flags, navigate and check the screen in **milliseconds**, without tapping through the app.
4
+
5
+ ## Demo
6
+
7
+ ![An agent hides a tab, turns on dark mode, fills in a form, seeds data, checks the screen and undoes it all](example/media/demo.gif)
8
+
9
+ <!-- TODO: upload example/media/demo-x.mp4 in GitHub's web editor and put the URL it gives here. GitHub only plays an mp4 uploaded that way. -->
10
+
11
+ [`example/flows/demo.mjs`](example/flows/demo.mjs) driving [the example app](example) in Expo Go, all on one Mac:
12
+
13
+ | Call | Round trip |
14
+ | --- | --- |
15
+ | `bridge.ping` | 1–4 ms |
16
+ | `screen.findText`, `screen.waitFor` (already there) | 3–9 ms |
17
+ | `screen.fill`, `screen.press` | 20–65 ms, render included |
18
+ | `query.pin`, `store.call`, `router.navigate` | 11–44 ms, render included |
19
+ | 14 steps, no pauses | 0.8 s wall, 0.4 s of it waiting on a save |
20
+
21
+ ```
22
+ 14 steps, all local on one Mac: 0.8 s wall with no pauses
23
+ same flow from another machine: ~57 ms per call (network)
24
+ same screen check via a11y tree: 450–970 ms
25
+ ```
26
+
27
+ Works on Expo (dev-tools socket) and on bare React Native (CDP over Metro). Dev builds only: release builds get empty stubs.
28
+
29
+ ## Install
30
+
31
+ ```sh
32
+ bun add -d @avasapp/agent-bridge # or npm / yarn / pnpm
33
+ ```
34
+
35
+ ## In the app
36
+
37
+ Mount the hook in a file that only runs in development. You choose every tool; adapters for common libraries are one import away.
38
+
39
+ ```tsx
40
+ import { cdpTransport, useAgentBridge } from '@avasapp/agent-bridge'
41
+ import { expoTransport } from '@avasapp/agent-bridge/expo'
42
+ import { queryTools } from '@avasapp/agent-bridge/tanstack-query'
43
+ import { storeTools } from '@avasapp/agent-bridge/zustand'
44
+ import { mmkvTools } from '@avasapp/agent-bridge/react-native-mmkv'
45
+ import { routerTools } from '@avasapp/agent-bridge/expo-router'
46
+ import { networkTools } from '@avasapp/agent-bridge/network'
47
+ import { router, useNavigationContainerRef } from 'expo-router'
48
+
49
+ export function AgentBridge() {
50
+ const queryClient = useQueryClient()
51
+ useAgentBridge({
52
+ name: 'my-app',
53
+ transports: [expoTransport(), cdpTransport()],
54
+ tools: {
55
+ ...queryTools(queryClient),
56
+ ...storeTools({ settings: useSettingsStore, auth: useAuthStore }),
57
+ ...mmkvTools({ storage }),
58
+ ...routerTools(router, { navigation: useNavigationContainerRef() }),
59
+ ...networkTools(),
60
+ // Your own tools: any function, JSON in and out.
61
+ 'auth.signIn': (session) => signInWith(session),
62
+ },
63
+ })
64
+ return null
65
+ }
66
+ ```
67
+
68
+ Every app also gets:
69
+
70
+ - `screen.snapshot`: buttons, inputs, text and `testID` views on screen, with positions.
71
+ - `screen.fill`, `screen.press`: call an input's or button's own handlers, found by `testID`, label, placeholder or text, and return once React has rendered the result.
72
+ - `screen.waitFor`, `screen.findText`: wait for, or check, text or a target on screen.
73
+ - `bridge.restore`: undo what the agent changed, by running every `*.restore` tool.
74
+ - `bridge.logs`, `bridge.ping`, `bridge.tools`.
75
+
76
+ Custom tools that change the screen can `await settle()` (from `@avasapp/agent-bridge`) so the next check sees the render.
77
+
78
+ Errors come back on their own: each reply carries what the app logged with `console.error`, threw or left unhandled since the previous reply, tagged with the call it happened during or after.
79
+
80
+ ## From the agent
81
+
82
+ ```sh
83
+ npx agent-bridge session start # hold one connection; call/tools/run reuse it
84
+ npx agent-bridge tools
85
+ npx agent-bridge call query.pin '[["features"], {"beta": false}]'
86
+ npx agent-bridge call screen.press '"add-plant"'
87
+ npx agent-bridge call screen.fill '["plant-name", "Fiddle leaf fig"]'
88
+ npx agent-bridge call screen.waitFor '"Name is required"'
89
+ npx agent-bridge run flows/add-plant.mjs --strict # fail if the app logged an error
90
+ npx agent-bridge session stop # runs bridge.restore, then disconnects
91
+ ```
92
+
93
+ A session stops itself, restore included, after 15 minutes without calls (`--idle`), and reconnects if the app reloads.
94
+
95
+ ```ts
96
+ import { connect } from '@avasapp/agent-bridge/client'
97
+
98
+ const app = await connect({ metro: 'localhost:8081' })
99
+ await app.call('router.navigate', '/add')
100
+ await app.call('screen.fill', 'plant-name', 'Fiddle leaf fig')
101
+ await app.call('screen.press', 'save-plant')
102
+ ```
103
+
104
+ A flow is a module the CLI runs without a model in the loop:
105
+
106
+ ```js
107
+ export default async ({ step }) => {
108
+ await step('flag: beta off', 'query.pin', ['features'], { beta: false })
109
+ await step('save empty form', 'screen.press', 'save-plant')
110
+ await step('error shown', 'screen.waitFor', 'Name is required')
111
+ await step('undo', 'bridge.restore')
112
+ }
113
+ ```
114
+
115
+ 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.
116
+
117
+ ## Adapters
118
+
119
+ | Import | Tools | Needs |
120
+ | --- | --- | --- |
121
+ | `@avasapp/agent-bridge/tanstack-query` | `query.list` `get` `set` `pin` `unpin` `unpinAll` `refetch` `invalidate` `restore` | your `QueryClient` |
122
+ | `@avasapp/agent-bridge/zustand` | `store.list` `get` `set` `call` `restore` | your stores |
123
+ | `@avasapp/agent-bridge/react-native-mmkv` | `mmkv.list` `keys` `get` `set` `delete` `restore` | your MMKV instances |
124
+ | `@avasapp/agent-bridge/expo-router` | `router.navigate` `push` `replace` `back` `current` | `router` and `useNavigationContainerRef()` from expo-router |
125
+ | `@avasapp/agent-bridge/network` | `net.log` `mock` `mocks` `unmock` `clear` `restore` | nothing: patches `fetch` and `XMLHttpRequest` in dev |
126
+
127
+ A pin keeps seeded data in place through refetches until you unpin it. `net.mock('/inbox', { status: 500 })` or `{ offline: true }` fails a route; apps can fake a whole backend with `mockRequests` from the same import.
128
+
129
+ ## Transports
130
+
131
+ | | Expo dev-tools socket | CDP |
132
+ | --- | --- | --- |
133
+ | Round trip, local | 0.5 ms | 1.3 ms |
134
+ | Emoji in payloads | yes | yes (escaped for you) |
135
+ | Works with | Expo CLI | any RN app on Metro |
136
+
137
+ `connect()` tries Expo first and falls back to CDP.
138
+
139
+ ## Release builds
140
+
141
+ Every entry point is gated on `process.env.NODE_ENV`, like `react/index.js`: Metro inlines it and a release bundle gets empty stubs. Check a bundle in CI:
142
+
143
+ ```sh
144
+ npx agent-bridge assert-absent path/to/main.jsbundle
145
+ ```
146
+
147
+ ## Traps we hit
148
+
149
+ - **Run the agent on the machine with the simulator.** Every call pays the network otherwise.
150
+ - **Hidden tabs stay mounted.** `screen.findText` skips anything under an inactive `RNSScreen`.
151
+ - **Expo checks the debugger's Origin** against the host Metro advertises and drops mismatches silently. The client reads it from the manifest.
152
+ - **Expo's socket broadcasts to every app.** Calls are addressed to one device; pick it with `--device` when several are connected.
153
+ - **Screen checks through the accessibility tree are slow** (hundreds of ms each). Check in-app, and keep one real UI check per flow.
154
+ - **`screen.fill` skips the keyboard.** It runs the input's handlers, so validation and state are real, but autocorrect, native `maxLength` and uncontrolled inputs' native text are not.
155
+ - **Keep your `QueryClient` in state** (`useState(() => new QueryClient())`). Created at module level, a Fast Refresh can leave the bridge holding a different client than the screen.
156
+
157
+ ## License
158
+
159
+ MIT © Avas Enterprises
@@ -0,0 +1,144 @@
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/expo-router.ts
21
+ var expo_router_exports = {};
22
+ __export(expo_router_exports, {
23
+ routerTools: () => routerTools
24
+ });
25
+ module.exports = __toCommonJS(expo_router_exports);
26
+ var ROOT = "__root";
27
+ var NOT_FOUND = "+not-found";
28
+ var SETTLE_MS = 1e3;
29
+ var decode = (value) => {
30
+ try {
31
+ return decodeURIComponent(value);
32
+ } catch {
33
+ return value;
34
+ }
35
+ };
36
+ var isGroup = (segment) => segment.startsWith("(") && segment.endsWith(")");
37
+ var focused = (state) => state.routes[state.index ?? 0];
38
+ function routeFromState(state) {
39
+ let route = state && focused(state);
40
+ if (!route) return { pathname: "/", href: "/", params: {}, segments: [], name: "" };
41
+ if (route.name !== ROOT) {
42
+ const path = route.path ?? (route.name === NOT_FOUND ? "/" : `/${route.name}`);
43
+ return { pathname: path, href: path, params: {}, segments: [route.name], name: route.name };
44
+ }
45
+ const segments = [];
46
+ const raw = {};
47
+ let name = "";
48
+ let next = route.state;
49
+ while (next && (route = focused(next))) {
50
+ Object.assign(raw, route.params);
51
+ name = route.name;
52
+ segments.push(...route.name.replace(/^\//, "").split("/"));
53
+ next = route.state;
54
+ }
55
+ let pending = route?.params;
56
+ while (pending && typeof pending.screen === "string") {
57
+ segments.push(...pending.screen.replace(/^\//, "").split("/"));
58
+ name = pending.screen;
59
+ pending = pending.params && typeof pending.params === "object" ? pending.params : void 0;
60
+ }
61
+ if (segments.at(-1) === "index") segments.pop();
62
+ delete raw.screen;
63
+ delete raw.params;
64
+ const params = {};
65
+ for (const [key, value] of Object.entries(raw)) {
66
+ if (typeof value === "string") params[key] = decode(value);
67
+ else if (Array.isArray(value)) params[key] = value.map((v) => decode(String(v)));
68
+ else if (value != null) params[key] = String(value);
69
+ }
70
+ const inPath = /* @__PURE__ */ new Set();
71
+ const fill = (key) => {
72
+ inPath.add(key);
73
+ const value = params[key];
74
+ return value === void 0 ? [] : Array.isArray(value) ? value : [value];
75
+ };
76
+ const pathname = "/" + segments.filter((s) => !isGroup(s)).flatMap((s) => {
77
+ if (s === NOT_FOUND) return fill("not-found");
78
+ if (s.startsWith("[...") && s.endsWith("]")) return fill(s.slice(4, -1).replace(/\?$/, ""));
79
+ if (s.startsWith("[") && s.endsWith("]")) return fill(s.slice(1, -1));
80
+ return [s];
81
+ }).join("/");
82
+ const search = new URLSearchParams();
83
+ for (const [key, value] of Object.entries(params))
84
+ if (!inPath.has(key)) for (const v of [value].flat()) search.append(key, v);
85
+ const query = search.toString();
86
+ return { pathname, href: query ? `${pathname}?${query}` : pathname, params, segments, name };
87
+ }
88
+ function settle(navigation, go) {
89
+ return new Promise((resolve, reject) => {
90
+ let off;
91
+ const done = () => {
92
+ off?.();
93
+ clearTimeout(timer);
94
+ resolve();
95
+ };
96
+ const timer = setTimeout(done, navigation.addListener ? SETTLE_MS : 0);
97
+ off = navigation.addListener?.("__unsafe_action__", done);
98
+ try {
99
+ go();
100
+ } catch (error) {
101
+ off?.();
102
+ clearTimeout(timer);
103
+ reject(error);
104
+ }
105
+ });
106
+ }
107
+ function routerTools(router, options = {}) {
108
+ const { navigation } = options;
109
+ const go = (fn, value) => navigation ? settle(navigation, fn).then(() => value) : (fn(), value);
110
+ return {
111
+ "router.navigate": {
112
+ description: "Go to a route, reusing it if it is already in the stack.",
113
+ run: (href) => go(() => router.navigate(href), href)
114
+ },
115
+ "router.push": {
116
+ description: "Push a route.",
117
+ run: (href) => go(() => router.push(href), href)
118
+ },
119
+ "router.replace": {
120
+ description: "Replace the current route.",
121
+ run: (href) => go(() => router.replace(href), href)
122
+ },
123
+ "router.back": {
124
+ description: "Go back if possible. Returns whether it could.",
125
+ run: () => {
126
+ const could = router.canGoBack();
127
+ return could ? go(() => router.back(), true) : false;
128
+ }
129
+ },
130
+ "router.current": {
131
+ description: "The current route: pathname, href, params, segments, name, canGoBack.",
132
+ run: () => {
133
+ if (!navigation)
134
+ throw new Error(
135
+ "router.current needs the navigation container. Pass it: routerTools(router, { navigation: useNavigationContainerRef() }) with useNavigationContainerRef from expo-router."
136
+ );
137
+ if (navigation.isReady && !navigation.isReady())
138
+ throw new Error("The navigation container has not mounted yet.");
139
+ const state = navigation.getRootState();
140
+ return { ...routeFromState(state), canGoBack: router.canGoBack() };
141
+ }
142
+ }
143
+ };
144
+ }
@@ -0,0 +1,38 @@
1
+ import { T as Tools } from '../types-C6DUUHnB.cjs';
2
+
3
+ /** The imperative `router` from expo-router. Passed in, so this adds no dependency. */
4
+ type RouterLike = {
5
+ navigate(href: never): void;
6
+ push(href: never): void;
7
+ replace(href: never): void;
8
+ back(): void;
9
+ canGoBack(): boolean;
10
+ };
11
+ /** The root navigation container, from `useNavigationContainerRef()` in expo-router. */
12
+ type NavigationLike = {
13
+ getRootState(): unknown;
14
+ isReady?(): boolean;
15
+ addListener?(type: '__unsafe_action__', callback: () => void): () => void;
16
+ };
17
+ type RouterToolsOptions = {
18
+ /** Enables `router.current`, and makes navigation tools return once the route has changed. */
19
+ navigation?: NavigationLike;
20
+ };
21
+ /** Where the app is, as expo-router's `usePathname`, `useSegments` and `useGlobalSearchParams` would say. */
22
+ type CurrentRoute = {
23
+ pathname: string;
24
+ /** `pathname` plus the params that are not part of the path, e.g. `/inbox?filter=unread`. */
25
+ href: string;
26
+ params: Record<string, string | string[]>;
27
+ segments: string[];
28
+ /** The focused route's name in its navigator, e.g. `index` or `[id]`. */
29
+ name: string;
30
+ canGoBack: boolean;
31
+ };
32
+ /**
33
+ * Navigate without taps. Pass `router` from `expo-router`, and
34
+ * `{ navigation: useNavigationContainerRef() }` to read the current route.
35
+ */
36
+ declare function routerTools(router: RouterLike, options?: RouterToolsOptions): Tools;
37
+
38
+ export { type CurrentRoute, type NavigationLike, type RouterLike, type RouterToolsOptions, routerTools };
@@ -0,0 +1,102 @@
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-native-mmkv.ts
21
+ var react_native_mmkv_exports = {};
22
+ __export(react_native_mmkv_exports, {
23
+ mmkvTools: () => mmkvTools
24
+ });
25
+ module.exports = __toCommonJS(react_native_mmkv_exports);
26
+ var originals = /* @__PURE__ */ new WeakMap();
27
+ function remove(mmkv, name, key) {
28
+ if (mmkv.remove) mmkv.remove(key);
29
+ else if (mmkv.delete) mmkv.delete(key);
30
+ else throw new Error(`MMKV instance "${name}" has neither remove() nor delete()`);
31
+ }
32
+ function mmkvTools(instances) {
33
+ const get = (name) => {
34
+ const mmkv = instances[name];
35
+ if (!mmkv)
36
+ throw new Error(
37
+ `Unknown MMKV instance "${name}". Known: ${Object.keys(instances).join(", ")}`
38
+ );
39
+ return mmkv;
40
+ };
41
+ const beforeChange = (mmkv, key) => {
42
+ let saved = originals.get(mmkv);
43
+ if (!saved) originals.set(mmkv, saved = /* @__PURE__ */ new Map());
44
+ if (saved.has(key)) return;
45
+ const exists = mmkv.contains?.(key) ?? mmkv.getAllKeys().includes(key);
46
+ saved.set(key, exists ? mmkv.getString(key) ?? null : void 0);
47
+ };
48
+ return {
49
+ "mmkv.list": {
50
+ description: "Instance names.",
51
+ run: () => Object.keys(instances)
52
+ },
53
+ "mmkv.keys": {
54
+ description: "Keys in an instance.",
55
+ run: (name) => get(name).getAllKeys()
56
+ },
57
+ "mmkv.get": {
58
+ description: "A string value; parsed as JSON when {json:true}.",
59
+ run: (name, key, options) => {
60
+ const raw = get(name).getString(key);
61
+ return options?.json && raw !== void 0 ? JSON.parse(raw) : raw;
62
+ }
63
+ },
64
+ "mmkv.set": {
65
+ description: "Set a value. Objects and arrays are stored as JSON strings.",
66
+ run: (name, key, value) => {
67
+ const stored = typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value);
68
+ const mmkv = get(name);
69
+ beforeChange(mmkv, key);
70
+ mmkv.set(key, stored);
71
+ return true;
72
+ }
73
+ },
74
+ "mmkv.delete": {
75
+ description: "Delete a key.",
76
+ run: (name, key) => {
77
+ const mmkv = get(name);
78
+ beforeChange(mmkv, key);
79
+ remove(mmkv, name, key);
80
+ return true;
81
+ }
82
+ },
83
+ "mmkv.restore": {
84
+ description: "Undo the agent: put back keys changed with mmkv.set or mmkv.delete, deleting ones that did not exist. Returns how many.",
85
+ run: () => {
86
+ let restored = 0;
87
+ for (const [name, mmkv] of Object.entries(instances)) {
88
+ const saved = originals.get(mmkv);
89
+ if (!saved) continue;
90
+ for (const [key, value] of saved) {
91
+ if (value === null) continue;
92
+ if (value === void 0) remove(mmkv, name, key);
93
+ else mmkv.set(key, value);
94
+ restored += 1;
95
+ }
96
+ originals.delete(mmkv);
97
+ }
98
+ return restored;
99
+ }
100
+ }
101
+ };
102
+ }
@@ -0,0 +1,15 @@
1
+ import { T as Tools } from '../types-C6DUUHnB.cjs';
2
+
3
+ /** The MMKV surface this adapter uses. v3 has `delete`, v4 has `remove`. */
4
+ type MMKVLike = {
5
+ getAllKeys: () => string[];
6
+ getString: (key: string) => string | undefined;
7
+ set: (key: string, value: string | number | boolean) => void;
8
+ contains?: (key: string) => boolean;
9
+ delete?: (key: string) => void;
10
+ remove?: (key: string) => unknown;
11
+ };
12
+ /** Read and write named MMKV instances. */
13
+ declare function mmkvTools(instances: Record<string, MMKVLike>): Tools;
14
+
15
+ export { type MMKVLike, mmkvTools };
@@ -0,0 +1,161 @@
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/tanstack-query.ts
21
+ var tanstack_query_exports = {};
22
+ __export(tanstack_query_exports, {
23
+ queryTools: () => queryTools
24
+ });
25
+ module.exports = __toCommonJS(tanstack_query_exports);
26
+ var pinStates = /* @__PURE__ */ new WeakMap();
27
+ function pinStateFor(queryClient) {
28
+ let state = pinStates.get(queryClient);
29
+ if (!state) {
30
+ state = {
31
+ pins: /* @__PURE__ */ new Map(),
32
+ changed: /* @__PURE__ */ new Map(),
33
+ seeded: /* @__PURE__ */ new WeakSet(),
34
+ applying: false,
35
+ unsubscribe: null
36
+ };
37
+ pinStates.set(queryClient, state);
38
+ }
39
+ return state;
40
+ }
41
+ var rendered = () => new Promise((resolve) => setTimeout(resolve, 0));
42
+ function queryTools(queryClient) {
43
+ const cache = queryClient.getQueryCache();
44
+ const state = pinStateFor(queryClient);
45
+ const { pins, seeded } = state;
46
+ const hashOf = (key) => cache.build(queryClient, { queryKey: key }).queryHash;
47
+ const apply = (key, data) => {
48
+ state.applying = true;
49
+ try {
50
+ queryClient.setQueryData(key, data);
51
+ } finally {
52
+ state.applying = false;
53
+ }
54
+ const stored = queryClient.getQueryData(key);
55
+ if (stored && typeof stored === "object") seeded.add(stored);
56
+ };
57
+ const watch = () => {
58
+ state.unsubscribe ?? (state.unsubscribe = cache.subscribe((event) => {
59
+ if (state.applying || event.type !== "updated") return;
60
+ const pin = pins.get(event.query.queryHash);
61
+ const data = event.query.state.data;
62
+ if (!pin || data && typeof data === "object" && seeded.has(data)) return;
63
+ apply(pin.key, pin.data);
64
+ }));
65
+ };
66
+ const unpin = (key) => {
67
+ const removed = pins.delete(hashOf(key));
68
+ if (!pins.size) {
69
+ state.unsubscribe?.();
70
+ state.unsubscribe = null;
71
+ }
72
+ if (removed)
73
+ void queryClient.invalidateQueries({ queryKey: key, exact: true });
74
+ return removed;
75
+ };
76
+ const refetchReal = (key) => {
77
+ const query = cache.find({ queryKey: key, exact: true });
78
+ if (!query) return false;
79
+ const queryFn = query.options.queryFn ?? queryClient.getDefaultOptions().queries?.queryFn;
80
+ if (!queryFn) {
81
+ cache.remove(query);
82
+ return false;
83
+ }
84
+ void queryClient.invalidateQueries({
85
+ queryKey: key,
86
+ exact: true,
87
+ refetchType: "all"
88
+ });
89
+ return true;
90
+ };
91
+ return {
92
+ "query.list": {
93
+ description: "Cached queries: key, status, observer count, pinned.",
94
+ run: () => cache.getAll().map((q) => ({
95
+ key: q.queryKey,
96
+ status: q.state.status,
97
+ observers: q.getObserversCount(),
98
+ pinned: pins.has(q.queryHash)
99
+ }))
100
+ },
101
+ "query.get": {
102
+ description: "Cached data for a query key.",
103
+ run: (key) => queryClient.getQueryData(key)
104
+ },
105
+ "query.set": {
106
+ description: "Replace cached data once. A refetch will overwrite it; use query.pin to keep it.",
107
+ run: async (key, data) => {
108
+ const hash = hashOf(key);
109
+ if (!state.changed.has(hash)) state.changed.set(hash, key);
110
+ queryClient.setQueryData(key, data);
111
+ await rendered();
112
+ return queryClient.getQueryData(key);
113
+ }
114
+ },
115
+ "query.pin": {
116
+ description: "Seed data for a query key and keep it through refetches until unpinned.",
117
+ run: async (key, data) => {
118
+ pins.set(hashOf(key), { key, data });
119
+ watch();
120
+ apply(key, data);
121
+ await rendered();
122
+ return queryClient.getQueryData(key);
123
+ }
124
+ },
125
+ "query.unpin": {
126
+ description: "Stop pinning a key and refetch its real data.",
127
+ run: unpin
128
+ },
129
+ "query.unpinAll": {
130
+ description: "Stop every pin and refetch real data.",
131
+ run: () => [...pins.values()].map((p) => p.key).filter((key) => unpin(key)).length
132
+ },
133
+ "query.refetch": {
134
+ description: "Refetch a query and return its data.",
135
+ run: async (key) => {
136
+ await queryClient.refetchQueries({ queryKey: key });
137
+ await rendered();
138
+ return queryClient.getQueryData(key);
139
+ }
140
+ },
141
+ "query.restore": {
142
+ description: "Undo the agent: unpin everything and refetch real data for keys changed with query.set.",
143
+ run: async () => {
144
+ const unpinned = [...pins.values()].map((p) => p.key);
145
+ const changed = [...state.changed].filter(([hash]) => !pins.has(hash)).map(([, key]) => key);
146
+ pins.clear();
147
+ state.changed.clear();
148
+ state.unsubscribe?.();
149
+ state.unsubscribe = null;
150
+ for (const key of unpinned) refetchReal(key);
151
+ const refetched = changed.filter((key) => refetchReal(key)).length;
152
+ await rendered();
153
+ return { unpinned: unpinned.length, refetched };
154
+ }
155
+ },
156
+ "query.invalidate": {
157
+ description: "Invalidate queries matching a key prefix.",
158
+ run: (key) => queryClient.invalidateQueries({ queryKey: key })
159
+ }
160
+ };
161
+ }
@@ -0,0 +1,10 @@
1
+ import { QueryClient } from '@tanstack/query-core';
2
+ import { T as Tools } from '../types-C6DUUHnB.cjs';
3
+
4
+ /**
5
+ * Read and seed the TanStack Query cache. `query.pin` keeps a seed in place:
6
+ * when a refetch lands with real data, the seed is put back until unpinned.
7
+ */
8
+ declare function queryTools(queryClient: QueryClient): Tools;
9
+
10
+ export { queryTools };