@avasapp/agent-bridge 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +19 -5
  2. package/dist/adapters/ably.cjs +79 -3
  3. package/dist/adapters/ably.d.cts +2 -1
  4. package/dist/adapters/async-storage.cjs +158 -0
  5. package/dist/adapters/async-storage.d.cts +14 -0
  6. package/dist/adapters/device.cjs +1110 -0
  7. package/dist/adapters/device.d.cts +49 -0
  8. package/dist/adapters/expo-router.cjs +112 -6
  9. package/dist/adapters/expo-router.d.cts +1 -1
  10. package/dist/adapters/jotai.cjs +266 -0
  11. package/dist/adapters/jotai.d.cts +19 -0
  12. package/dist/adapters/react-native-mmkv.cjs +80 -0
  13. package/dist/adapters/react-native-mmkv.d.cts +3 -1
  14. package/dist/adapters/react-native-webview.d.cts +1 -1
  15. package/dist/adapters/react-navigation.cjs +293 -0
  16. package/dist/adapters/react-navigation.d.cts +55 -0
  17. package/dist/adapters/redux.cjs +322 -0
  18. package/dist/adapters/redux.d.cts +28 -0
  19. package/dist/adapters/secure-store.cjs +169 -0
  20. package/dist/adapters/secure-store.d.cts +28 -0
  21. package/dist/adapters/socket-io.cjs +77 -3
  22. package/dist/adapters/socket-io.d.cts +2 -1
  23. package/dist/adapters/tanstack-query.d.cts +1 -1
  24. package/dist/adapters/zustand.cjs +193 -22
  25. package/dist/adapters/zustand.d.cts +11 -15
  26. package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
  27. package/dist/cli.js +1336 -255
  28. package/dist/client/index.cjs +233 -27
  29. package/dist/client/index.d.ts +67 -4
  30. package/dist/client/index.js +5 -3
  31. package/dist/expo/index.d.cts +1 -1
  32. package/dist/network/index.cjs +320 -73
  33. package/dist/network/index.d.cts +18 -3
  34. package/dist/noop/async-storage.cjs +26 -0
  35. package/dist/noop/device.cjs +26 -0
  36. package/dist/noop/jotai.cjs +26 -0
  37. package/dist/noop/react-navigation.cjs +26 -0
  38. package/dist/noop/realtime.cjs +6 -1
  39. package/dist/noop/redux.cjs +28 -0
  40. package/dist/noop/secure-store.cjs +26 -0
  41. package/dist/realtime/index.cjs +71 -4
  42. package/dist/realtime/index.d.cts +21 -2
  43. package/dist/runtime/index.cjs +785 -128
  44. package/dist/runtime/index.d.cts +2 -2
  45. package/dist/store-common-j0cHHvBr.d.cts +23 -0
  46. package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
  47. package/entries/async-storage.cjs +9 -0
  48. package/entries/device.cjs +9 -0
  49. package/entries/jotai.cjs +9 -0
  50. package/entries/react-navigation.cjs +9 -0
  51. package/entries/redux.cjs +9 -0
  52. package/entries/secure-store.cjs +9 -0
  53. package/package.json +32 -1
  54. package/skills/agent-bridge/SKILL.md +39 -8
  55. package/skills/agent-bridge/references/device.md +21 -0
  56. package/skills/agent-bridge/references/flows.md +13 -3
  57. package/skills/agent-bridge/references/network.md +19 -1
  58. package/skills/agent-bridge/references/realtime.md +1 -1
  59. package/skills/agent-bridge/references/restore.md +21 -3
  60. package/skills/agent-bridge/references/screen.md +8 -2
  61. package/skills/agent-bridge/references/state.md +21 -1
@@ -0,0 +1,28 @@
1
+ import { T as Tools } from '../types-qlwdcGjt.cjs';
2
+ import { N as NamedRedact } from '../store-common-j0cHHvBr.cjs';
3
+
4
+ /** Anything with a Redux store's shape, including Redux Toolkit's configureStore. */
5
+ type ReduxStoreLike = {
6
+ getState: () => unknown;
7
+ dispatch(action: {
8
+ type: string;
9
+ payload?: unknown;
10
+ }): unknown;
11
+ };
12
+
13
+ type ReduxToolsOptions = {
14
+ /** By slice name, as `{ auth: ['token'] }` or `{ session: true }`, or a hook `(slice, path, value)`. */
15
+ redact?: NamedRedact;
16
+ };
17
+ /**
18
+ * Wrap the root reducer so the bridge can replace the state (redux.set and
19
+ * restore): `bridgeReducer(rootReducer)`. With redux-persist, wrap the reducer
20
+ * inside persistReducer: `persistReducer(config, bridgeReducer(rootReducer))`,
21
+ * so the saved copy follows. Release builds get the noop stub, which returns
22
+ * the reducer unchanged.
23
+ */
24
+ declare function bridgeReducer<R extends (state: any, action: any) => any>(reducer: R): R;
25
+ /** Read, set, dispatch to and restore a Redux store. */
26
+ declare function reduxTools(store: ReduxStoreLike, options?: ReduxToolsOptions): Tools;
27
+
28
+ export { NamedRedact as ReduxRedact, type ReduxStoreLike, type ReduxToolsOptions, bridgeReducer, reduxTools };
@@ -0,0 +1,169 @@
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/secure-store.ts
21
+ var secure_store_exports = {};
22
+ __export(secure_store_exports, {
23
+ secureStoreTools: () => secureStoreTools
24
+ });
25
+ module.exports = __toCommonJS(secure_store_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/secure-store.ts
94
+ var originals = /* @__PURE__ */ new WeakMap();
95
+ var hide = (value) => value === null ? null : `[redacted, ${value.length} chars]`;
96
+ function secureStoreTools(store, keys = [], options) {
97
+ let undo = originals.get(store);
98
+ if (!undo) originals.set(store, undo = /* @__PURE__ */ new Map());
99
+ const pending = undo;
100
+ const optionsFor = (key) => typeof options === "function" ? options(key) : options;
101
+ const guarded = (key) => optionsFor(key)?.requireAuthentication === true;
102
+ const idOf = (key) => {
103
+ const o = optionsFor(key);
104
+ return JSON.stringify([o?.keychainService ?? "", o?.accessGroup ?? "", key]);
105
+ };
106
+ const apply = async (key, value) => {
107
+ const o = optionsFor(key);
108
+ const io = {
109
+ read: () => store.getItemAsync(key, o),
110
+ write: (v) => store.setItemAsync(key, v, o),
111
+ remove: () => store.deleteItemAsync(key, o)
112
+ };
113
+ try {
114
+ await change(pending, idOf(key), io, value);
115
+ } catch (error) {
116
+ if (guarded(key) && !pending.has(idOf(key)))
117
+ throw new Error(
118
+ `Could not read the protected key "${key}" to save its old value (${error instanceof Error ? error.message : String(error)}); nothing was changed.`
119
+ );
120
+ throw error;
121
+ }
122
+ };
123
+ return {
124
+ "secureStore.list": {
125
+ description: "The keys the app passed to secureStoreTools, each with whether it is set and its length. Keys the app protects with requireAuthentication are not read (they would prompt): they show {authRequired:true}. SecureStore cannot list keys itself: for any other key, call secureStore.get with its name. Values are not shown.",
126
+ run: async () => Object.fromEntries(
127
+ await Promise.all(
128
+ keys.map(async (k) => {
129
+ if (guarded(k)) return [k, { authRequired: true }];
130
+ const v = await store.getItemAsync(k, optionsFor(k));
131
+ return [k, v === null ? { set: false } : { set: true, length: v.length }];
132
+ })
133
+ )
134
+ )
135
+ },
136
+ "secureStore.get": {
137
+ description: "A key's value, redacted to its length unless {full:true}; null when missing. Pass the key name: SecureStore has no key listing.",
138
+ run: async (key, getOptions) => {
139
+ const value = await store.getItemAsync(key, optionsFor(key));
140
+ return getOptions?.full ? value : hide(value);
141
+ }
142
+ },
143
+ "secureStore.set": {
144
+ description: "Set a value (a string; other values are stored as JSON). The old value is kept in memory for secureStore.restore.",
145
+ run: async (key, value) => {
146
+ await apply(key, toStored("secureStore.set", key, value));
147
+ return true;
148
+ }
149
+ },
150
+ "secureStore.delete": {
151
+ description: "Delete a key.",
152
+ run: async (key) => {
153
+ if (typeof key !== "string" || !key)
154
+ throw new Error("secureStore.delete needs the key name as a non-empty string");
155
+ await apply(key, null);
156
+ return true;
157
+ }
158
+ },
159
+ "secureStore.restore": {
160
+ // Always a plain true: bridge.pending must not show key names or values.
161
+ pending: () => pending.size > 0,
162
+ description: "Undo the agent: put back keys changed with secureStore.set or secureStore.delete (with the same options), 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.",
163
+ run: async () => describeRestore(
164
+ await restore(pending, (id) => JSON.parse(id)[2]),
165
+ "Those keys"
166
+ )
167
+ }
168
+ };
169
+ }
@@ -0,0 +1,28 @@
1
+ import { T as Tools } from '../types-qlwdcGjt.cjs';
2
+
3
+ /** The options expo-secure-store takes on every call. */
4
+ type SecureStoreOptions = {
5
+ keychainService?: string;
6
+ keychainAccessible?: number;
7
+ requireAuthentication?: boolean;
8
+ authenticationPrompt?: string;
9
+ accessGroup?: string;
10
+ };
11
+ /** The expo-secure-store surface this adapter uses (`import * as SecureStore`). */
12
+ type SecureStoreLike = {
13
+ getItemAsync: (key: string, options?: SecureStoreOptions) => Promise<string | null>;
14
+ setItemAsync: (key: string, value: string, options?: SecureStoreOptions) => Promise<void>;
15
+ deleteItemAsync: (key: string, options?: SecureStoreOptions) => Promise<void>;
16
+ };
17
+ /** One options object for every key, or a function that picks them per key. */
18
+ type SecureStoreOptionsFor = SecureStoreOptions | ((key: string) => SecureStoreOptions | undefined);
19
+ /**
20
+ * Read and write expo-secure-store. Its values are secrets, so reads are
21
+ * redacted unless {full:true}. It cannot list keys: pass the ones the app
22
+ * knows as `keys` to let `secureStore.list` show them. `options` must match
23
+ * what the app passes (a `keychainService` picks a different item), as one
24
+ * object or a function of the key.
25
+ */
26
+ declare function secureStoreTools(store: SecureStoreLike, keys?: readonly string[], options?: SecureStoreOptionsFor): Tools;
27
+
28
+ export { type SecureStoreLike, type SecureStoreOptions, type SecureStoreOptionsFor, secureStoreTools };
@@ -72,6 +72,21 @@ function tapListeners(tap, target, options) {
72
72
  };
73
73
  }
74
74
  }
75
+ function tapOutgoing(tap, target, options) {
76
+ const methods = target;
77
+ for (const name of list(options.methods)) {
78
+ const send = methods[name];
79
+ if (typeof send !== "function") continue;
80
+ methods[name] = function(...args) {
81
+ try {
82
+ const channel = options.skip?.(this) ? void 0 : options.channel(args);
83
+ if (channel !== void 0) tap.sent(channel, options.describe(args));
84
+ } catch {
85
+ }
86
+ return send.apply(this, args);
87
+ };
88
+ }
89
+ }
75
90
  function fakeableConnection(target, options) {
76
91
  const methods = target;
77
92
  const events = methods?.[options.events];
@@ -116,6 +131,46 @@ function realtimeAdapter(install) {
116
131
  };
117
132
  }
118
133
 
134
+ // src/network/headers.ts
135
+ var SECRET = /auth|cookie|token|secret|session|password|passwd|signature|api-?key|credential|jwt/i;
136
+
137
+ // src/realtime/snapshot.ts
138
+ var MAX_STRING = 2e3;
139
+ var MAX_ITEMS = 50;
140
+ var MAX_DEPTH = 5;
141
+ var isPlain = (v) => {
142
+ const proto = Object.getPrototypeOf(v);
143
+ return proto === Object.prototype || proto === null;
144
+ };
145
+ function snapshot(value, depth = 0) {
146
+ if (typeof value === "string")
147
+ return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}\u2026 (+${value.length - MAX_STRING} chars)` : value;
148
+ if (typeof value === "function") return "[function]";
149
+ if (!value || typeof value !== "object") return value;
150
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer)
151
+ return `[${value.constructor.name} ${value.byteLength} bytes]`;
152
+ if (depth >= MAX_DEPTH) return "[\u2026]";
153
+ if (Array.isArray(value)) {
154
+ const items = value.slice(0, MAX_ITEMS).map((v) => snapshot(v, depth + 1));
155
+ if (value.length > MAX_ITEMS) items.push(`\u2026 (+${value.length - MAX_ITEMS} items)`);
156
+ return items;
157
+ }
158
+ if (value instanceof Date) return value.toISOString();
159
+ if (!isPlain(value)) return `[${value.constructor?.name ?? "object"}]`;
160
+ const out = {};
161
+ for (const key of Object.keys(value).slice(0, MAX_ITEMS))
162
+ out[key] = snapshot(value[key], depth + 1);
163
+ return out;
164
+ }
165
+ function redact(value, depth = 0) {
166
+ if (!value || typeof value !== "object" || depth >= MAX_DEPTH + 1) return value;
167
+ if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1));
168
+ const out = {};
169
+ for (const [key, v] of Object.entries(value))
170
+ out[key] = SECRET.test(key) ? "[redacted]" : redact(v, depth + 1);
171
+ return out;
172
+ }
173
+
119
174
  // src/realtime/tap.ts
120
175
  var LOG_SIZE = 50;
121
176
  var ALL = "*";
@@ -159,6 +214,17 @@ function createRealtimeTap(options = {}) {
159
214
  }
160
215
  if (!dropped) entry.deliver(message, ...rest);
161
216
  }
217
+ const sent = (channel, summary) => {
218
+ log.push({
219
+ at: (/* @__PURE__ */ new Date()).toISOString(),
220
+ channel,
221
+ ...summary,
222
+ ..."data" in summary && { data: snapshot(summary.data) },
223
+ injected: false,
224
+ sent: true
225
+ });
226
+ if (log.length > LOG_SIZE) log.splice(0, log.length - LOG_SIZE);
227
+ };
162
228
  const wrap = (channel, listener, wrapOptions = {}) => {
163
229
  const entry = {
164
230
  deliver: listener,
@@ -208,11 +274,11 @@ function createRealtimeTap(options = {}) {
208
274
  }))
209
275
  },
210
276
  [`${ns}.log`]: {
211
- description: `Recent messages, real and injected, newest first (last ${LOG_SIZE}). Filter: { channel?, event?, limit? }; clear: true empties the log after reading.`,
277
+ description: `Recent messages, real, injected, and ones the app sent (sent: true) (socket.emit, Ably publish), newest first (last ${LOG_SIZE}). Filter: { channel?, event?, limit?, full? }; clear: true empties the log after reading. Sent messages are copied and cut, and keys like token, password or authorization show [redacted] unless full: true.`,
212
278
  run: (filter = {}) => {
213
279
  const entries = log.filter(
214
280
  (e) => (!filter.channel || e.channel === filter.channel) && (!filter.event || e.event === filter.event)
215
- ).reverse().slice(0, filter.limit ?? 20);
281
+ ).reverse().slice(0, filter.limit ?? 20).map((e) => e.sent && !filter.full && "data" in e ? { ...e, data: redact(e.data) } : e);
216
282
  if (filter.clear) log.length = 0;
217
283
  return entries;
218
284
  }
@@ -273,7 +339,7 @@ function createRealtimeTap(options = {}) {
273
339
  return { state: faked ?? connection.state(), real: connection.state(), faked: faked !== null };
274
340
  }
275
341
  };
276
- return { wrap, tools };
342
+ return { wrap, sent, tools };
277
343
  }
278
344
 
279
345
  // src/adapters/socket-io.ts
@@ -314,6 +380,14 @@ var socketIoTools = realtimeAdapter(
314
380
  channel: ([event]) => typeof event === "string" && !RESERVED.has(event) ? event : void 0,
315
381
  message: "args"
316
382
  });
383
+ tapOutgoing(tap, socket, {
384
+ methods: "emit",
385
+ channel: ([event]) => typeof event === "string" && !RESERVED.has(event) ? event : void 0,
386
+ // socket.io's retry queue calls emit again for the same message.
387
+ skip: (target) => Boolean(target.flags?.fromQueue),
388
+ // Always the argument list, so emit('x', [1, 2]) and emit('x', 1, 2) differ.
389
+ describe: ([, ...args]) => ({ data: args.filter((a) => typeof a !== "function") })
390
+ });
317
391
  return tap;
318
392
  }
319
393
  );
@@ -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
  type Listener = (...args: any[]) => void;
4
4
  /** A socket.io client socket (`io(url)`), typed structurally. */
@@ -6,6 +6,7 @@ type SocketLike = {
6
6
  on(event: string, listener: Listener): unknown;
7
7
  off(event?: string, listener?: Listener): unknown;
8
8
  connected?: boolean;
9
+ emit?(event: string, ...args: never[]): unknown;
9
10
  };
10
11
  type SocketIoToolsOptions = {
11
12
  /** Tool name prefix. Defaults to "realtime". */
@@ -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
  /** A TanStack Query `QueryClient`, e.g. from `useQueryClient()`. */
4
4
  type QueryClientLike = {
@@ -43,8 +43,7 @@ function toJson(value) {
43
43
  return text === void 0 ? null : JSON.parse(text);
44
44
  }
45
45
 
46
- // src/adapters/zustand.ts
47
- var snapshots = /* @__PURE__ */ new WeakMap();
46
+ // src/adapters/store-common.ts
48
47
  var isObject = (value) => typeof value === "object" && value !== null;
49
48
  function toPath(part) {
50
49
  if (typeof part === "string") return part === "" ? [] : part.split(".");
@@ -52,7 +51,7 @@ function toPath(part) {
52
51
  if (Array.isArray(part) && part.every((p) => typeof p === "string" || typeof p === "number"))
53
52
  return part;
54
53
  throw new Error(
55
- `A store path is a dotted string or an array of segments, got ${JSON.stringify(part)}`
54
+ `A path is a dotted string or an array of segments, got ${JSON.stringify(part)}`
56
55
  );
57
56
  }
58
57
  function at(value, path) {
@@ -62,7 +61,7 @@ function at(value, path) {
62
61
  );
63
62
  }
64
63
  var kindOf = (v) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
65
- function readState(state, given) {
64
+ function readState(state, given, tool = "store.get") {
66
65
  const args = given.filter((a) => a != null);
67
66
  const last = args.at(-1);
68
67
  const isOptions = isObject(last) && !Array.isArray(last);
@@ -70,12 +69,12 @@ function readState(state, given) {
70
69
  const unknown = Object.keys(options).filter((k) => k !== "pick" && k !== "keys");
71
70
  if (unknown.length)
72
71
  throw new Error(
73
- `Unknown store.get option ${unknown.map((k) => `"${k}"`).join(", ")}. Options: pick, keys`
72
+ `Unknown ${tool} option ${unknown.map((k) => `"${k}"`).join(", ")}. Options: pick, keys`
74
73
  );
75
74
  const value = at(state, (isOptions ? args.slice(0, -1) : args).flatMap(toPath));
76
75
  if (options.pick !== void 0) {
77
76
  if (!Array.isArray(options.pick))
78
- throw new Error("store.get pick is an array of paths");
77
+ throw new Error(`${tool} pick is an array of paths`);
79
78
  return Object.fromEntries(
80
79
  options.pick.map((p) => [
81
80
  Array.isArray(p) ? p.join(".") : String(p),
@@ -87,17 +86,6 @@ function readState(state, given) {
87
86
  return isObject(value) ? Object.fromEntries(Object.entries(value).map(([k, v]) => [k, kindOf(v)])) : kindOf(value);
88
87
  return value;
89
88
  }
90
- function restore(store, snapshot) {
91
- const current = store.getState();
92
- if (!isObject(snapshot) || !isObject(current)) {
93
- store.setState(snapshot, true);
94
- return;
95
- }
96
- const actions = Object.fromEntries(
97
- Object.entries(current).filter(([, v]) => typeof v === "function")
98
- );
99
- store.setState({ ...snapshot, ...actions }, true);
100
- }
101
89
  var PREVIEW = 200;
102
90
  function preview(value) {
103
91
  try {
@@ -109,10 +97,11 @@ function preview(value) {
109
97
  return "[unserializable]";
110
98
  }
111
99
  }
112
- function differences(snapshot, current, show) {
100
+ function differences(snapshot, current, show, only) {
113
101
  if (!isObject(snapshot) || !isObject(current)) return {};
114
102
  const out = {};
115
103
  for (const k of /* @__PURE__ */ new Set([...Object.keys(snapshot), ...Object.keys(current)])) {
104
+ if (only && !only.has(k)) continue;
116
105
  const a = snapshot[k];
117
106
  const b = current[k];
118
107
  if (typeof a === "function" || typeof b === "function") continue;
@@ -137,6 +126,79 @@ function redactValue(redact, name, value, path, seen = []) {
137
126
  ]);
138
127
  return Array.isArray(value) ? entries.map(([, v]) => v) : Object.fromEntries(entries);
139
128
  }
129
+
130
+ // src/adapters/zustand.ts
131
+ var HYDRATION_WAIT_MS = 2e3;
132
+ var rev = 0;
133
+ function survivesJson(v, depth = 0) {
134
+ if (v === null || typeof v === "string" || typeof v === "boolean") return true;
135
+ if (typeof v === "number") return Number.isFinite(v);
136
+ if (depth > 50 || !isObject(v)) return false;
137
+ if (Array.isArray(v)) return v.every((x) => survivesJson(x, depth + 1));
138
+ const proto = Object.getPrototypeOf(v);
139
+ if (proto !== Object.prototype && proto !== null) return false;
140
+ return Object.values(v).every((x) => x === void 0 || survivesJson(x, depth + 1));
141
+ }
142
+ var ok = (v) => v === void 0 || survivesJson(v);
143
+ var has = (x, k) => x[k] !== void 0;
144
+ function persisted(store, state) {
145
+ try {
146
+ const partialize = store.persist?.getOptions?.().partialize;
147
+ const part = partialize ? partialize(state) : state;
148
+ if (!isObject(part) || Array.isArray(part)) return void 0;
149
+ const raw = Object.fromEntries(Object.entries(part).filter(([, v]) => typeof v !== "function"));
150
+ const refused = /* @__PURE__ */ new Set();
151
+ if (partialize && isObject(state)) {
152
+ for (const [k, v] of Object.entries(raw)) if (!(k in state) || !Object.is(state[k], v)) refused.add(k);
153
+ }
154
+ return { raw, refused };
155
+ } catch {
156
+ return void 0;
157
+ }
158
+ }
159
+ var differs = (a, b) => ok(a) && ok(b) ? !same(a, b) : !Object.is(a, b);
160
+ var json = (v) => JSON.parse(JSON.stringify(v));
161
+ var same = (a, b) => {
162
+ if (Object.is(a, b)) return true;
163
+ if (!isObject(a) || !isObject(b) || Array.isArray(a) !== Array.isArray(b)) return false;
164
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
165
+ return [...keys].every((k) => same(a[k], b[k]));
166
+ };
167
+ async function hydrated(store) {
168
+ const p = store.persist;
169
+ if (!p?.hasHydrated || p.hasHydrated()) return true;
170
+ return new Promise((resolve) => {
171
+ const off = p.onFinishHydration?.(() => finish(true));
172
+ const timer = setTimeout(() => finish(false), HYDRATION_WAIT_MS);
173
+ function finish(ok2) {
174
+ clearTimeout(timer);
175
+ off?.();
176
+ resolve(ok2);
177
+ }
178
+ });
179
+ }
180
+ var snapshots = /* @__PURE__ */ new WeakMap();
181
+ var adoptedKeys = /* @__PURE__ */ new WeakMap();
182
+ function restore(store, snapshot, only) {
183
+ const current = store.getState();
184
+ if (only && isObject(snapshot) && isObject(current)) {
185
+ const next = { ...current };
186
+ for (const k of only) {
187
+ if (k in snapshot) next[k] = snapshot[k];
188
+ else delete next[k];
189
+ }
190
+ store.setState(next, true);
191
+ return;
192
+ }
193
+ if (!isObject(snapshot) || !isObject(current)) {
194
+ store.setState(snapshot, true);
195
+ return;
196
+ }
197
+ const actions = Object.fromEntries(
198
+ Object.entries(current).filter(([, v]) => typeof v === "function")
199
+ );
200
+ store.setState({ ...snapshot, ...actions }, true);
201
+ }
140
202
  function storeTools(stores, options = {}) {
141
203
  const out = (name, value, path = "") => redactValue(options.redact, name, value, path);
142
204
  const get = (name) => {
@@ -147,8 +209,26 @@ function storeTools(stores, options = {}) {
147
209
  );
148
210
  return store;
149
211
  };
212
+ const hidden = (name, k, ...values) => {
213
+ const { redact } = options;
214
+ if (!redact) return false;
215
+ if (typeof redact !== "function")
216
+ return !!redact[name]?.some((p) => p === k || p.startsWith(`${k}.`) || k.startsWith(`${p}.`));
217
+ return values.some((v) => !same(redactValue(redact, name, v, k), v));
218
+ };
150
219
  const beforeChange = (store) => {
151
- if (!snapshots.has(store)) snapshots.set(store, store.getState());
220
+ rev += 1;
221
+ const only = adoptedKeys.get(store);
222
+ if (only) {
223
+ const next = { ...store.getState() };
224
+ const saved = snapshots.get(store);
225
+ for (const k of only) {
226
+ if (k in saved) next[k] = saved[k];
227
+ else delete next[k];
228
+ }
229
+ snapshots.set(store, next);
230
+ adoptedKeys.delete(store);
231
+ } else if (!snapshots.has(store)) snapshots.set(store, store.getState());
152
232
  };
153
233
  return {
154
234
  "store.list": {
@@ -183,7 +263,12 @@ function storeTools(stores, options = {}) {
183
263
  throw new Error(`Store "${name}" has no action "${action}"`);
184
264
  beforeChange(store);
185
265
  const before = store.getState();
186
- const result = await fn(...args);
266
+ let result;
267
+ try {
268
+ result = await fn(...args);
269
+ } finally {
270
+ rev += 1;
271
+ }
187
272
  const after = store.getState();
188
273
  if (result !== void 0 && result !== before && result !== after) {
189
274
  const key = isObject(after) ? Object.keys(after).find((k) => after[k] === result) : void 0;
@@ -203,6 +288,8 @@ function storeTools(stores, options = {}) {
203
288
  description: "Keep a store as it is now: drops its pending snapshot, so bridge.restore leaves it alone and the next store.set or store.call snapshots again from this state. Use it after repairing a value that was already wrong when the snapshot was taken. Returns { store, committed } (committed: false when nothing was pending).",
204
289
  run: (name) => {
205
290
  const store = get(name);
291
+ rev += 1;
292
+ adoptedKeys.delete(store);
206
293
  return { store: name, committed: snapshots.delete(store) };
207
294
  }
208
295
  },
@@ -210,6 +297,87 @@ function storeTools(stores, options = {}) {
210
297
  maxArgs: 0,
211
298
  // Per store, its snapshot against now (secrets redacted), so an agent can
212
299
  // tell what a restore will put back before it does.
300
+ journalRev: () => rev,
301
+ // Persisted stores only: others are rebuilt from scratch on reload.
302
+ journal: () => {
303
+ const out2 = {};
304
+ for (const [name, store] of Object.entries(stores)) {
305
+ if (!store.persist || !snapshots.has(store)) continue;
306
+ const before = persisted(store, snapshots.get(store));
307
+ const now = persisted(store, store.getState());
308
+ if (!before || !now) continue;
309
+ const entry = { original: {}, after: {} };
310
+ const skipped = [];
311
+ const only = adoptedKeys.get(store);
312
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(before.raw), ...Object.keys(now.raw)])) {
313
+ if (only && !only.has(k)) continue;
314
+ const a = before.raw[k];
315
+ const b = now.raw[k];
316
+ if (!differs(a, b)) continue;
317
+ if (before.refused.has(k) || now.refused.has(k) || !ok(a) || !ok(b) || hidden(name, k, a, b)) {
318
+ skipped.push(k);
319
+ continue;
320
+ }
321
+ if (has(before.raw, k)) entry.original[k] = json(a);
322
+ if (has(now.raw, k)) entry.after[k] = json(b);
323
+ }
324
+ if (skipped.length) entry.skipped = skipped;
325
+ if (skipped.length || Object.keys(entry.original).length || Object.keys(entry.after).length) out2[name] = entry;
326
+ }
327
+ return Object.keys(out2).length ? out2 : void 0;
328
+ },
329
+ adopt: async (raw) => {
330
+ const data = isObject(raw) && !Array.isArray(raw) ? raw : {};
331
+ const result = { adopted: [], already: [], changed: [] };
332
+ const retry = {};
333
+ const ready = await Promise.all(
334
+ Object.keys(data).map((n) => stores[n] ? hydrated(stores[n]) : false)
335
+ );
336
+ const names = Object.keys(data);
337
+ for (const [name, entry] of Object.entries(data)) {
338
+ const store = stores[name];
339
+ const skipped = entry?.skipped;
340
+ if (!isObject(entry) || !isObject(entry.original) || !isObject(entry.after) || skipped !== void 0 && !(Array.isArray(skipped) && skipped.every((k) => typeof k === "string"))) {
341
+ (result.invalid ?? (result.invalid = [])).push(name);
342
+ continue;
343
+ }
344
+ if (!store || !ready[names.indexOf(name)]) {
345
+ retry[name] = entry;
346
+ continue;
347
+ }
348
+ for (const k of skipped ?? []) (result.notKept ?? (result.notKept = [])).push(`${name}:${k}`);
349
+ const now = persisted(store, store.getState());
350
+ const saved = snapshots.has(store) && !adoptedKeys.has(store) ? persisted(store, snapshots.get(store)) : void 0;
351
+ const eq = (x, y, k) => has(x, k) === has(y, k) && (!has(x, k) || same(x[k], y[k]));
352
+ const back = [];
353
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(entry.original), ...Object.keys(entry.after)])) {
354
+ const label = `${name}:${k}`;
355
+ if (!now || now.refused.has(k) || !ok(now.raw[k])) {
356
+ (result.notKept ?? (result.notKept = [])).push(label);
357
+ continue;
358
+ }
359
+ const savedFake = !!saved && ok(saved.raw[k]) && eq(saved.raw, entry.after, k);
360
+ if (!savedFake && eq(now.raw, entry.original, k)) result.already.push(label);
361
+ else if (savedFake || eq(now.raw, entry.after, k)) {
362
+ back.push(k);
363
+ result.adopted.push(label);
364
+ } else result.changed.push(label);
365
+ }
366
+ if (back.length) {
367
+ if (!snapshots.has(store)) adoptedKeys.set(store, /* @__PURE__ */ new Set());
368
+ const next = { ...snapshots.get(store) ?? store.getState() };
369
+ for (const k of back) {
370
+ if (k in entry.original) next[k] = entry.original[k];
371
+ else delete next[k];
372
+ adoptedKeys.get(store)?.add(k);
373
+ }
374
+ snapshots.set(store, next);
375
+ rev += 1;
376
+ }
377
+ }
378
+ if (Object.keys(retry).length) result.retry = retry;
379
+ return result;
380
+ },
213
381
  pending: ({ detail } = {}) => {
214
382
  const changed = Object.entries(stores).filter(([, s]) => snapshots.has(s));
215
383
  if (!changed.length) return false;
@@ -220,7 +388,8 @@ function storeTools(stores, options = {}) {
220
388
  differences(
221
389
  snapshots.get(store),
222
390
  store.getState(),
223
- (key, value) => out(name, value, key)
391
+ (key, value) => out(name, value, key),
392
+ adoptedKeys.get(store)
224
393
  )
225
394
  ])
226
395
  );
@@ -228,8 +397,10 @@ function storeTools(stores, options = {}) {
228
397
  description: "Undo the agent: put back each store changed with store.set or store.call, to its state at the first of those (store.commit drops a snapshot). Returns their names.",
229
398
  run: () => Object.entries(stores).flatMap(([name, store]) => {
230
399
  if (!snapshots.has(store)) return [];
231
- restore(store, snapshots.get(store));
400
+ rev += 1;
401
+ restore(store, snapshots.get(store), adoptedKeys.get(store));
232
402
  snapshots.delete(store);
403
+ adoptedKeys.delete(store);
233
404
  return [name];
234
405
  })
235
406
  }