@avasapp/agent-bridge 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,15 +23,70 @@ __export(zustand_exports, {
23
23
  storeTools: () => storeTools
24
24
  });
25
25
  module.exports = __toCommonJS(zustand_exports);
26
+
27
+ // src/runtime/to-json.ts
28
+ function toJson(value) {
29
+ if (value === void 0) return null;
30
+ const seen = /* @__PURE__ */ new WeakSet();
31
+ const text = JSON.stringify(value, (_key, v) => {
32
+ if (typeof v === "bigint") return v.toString();
33
+ if (typeof v === "function" || typeof v === "symbol") return void 0;
34
+ if (v instanceof Error) return { name: v.name, message: v.message };
35
+ if (v instanceof Map) return Object.fromEntries(v);
36
+ if (v instanceof Set) return [...v];
37
+ if (v && typeof v === "object") {
38
+ if (seen.has(v)) return "[Circular]";
39
+ seen.add(v);
40
+ }
41
+ return v;
42
+ });
43
+ return text === void 0 ? null : JSON.parse(text);
44
+ }
45
+
46
+ // src/adapters/zustand.ts
26
47
  var snapshots = /* @__PURE__ */ new WeakMap();
27
48
  var isObject = (value) => typeof value === "object" && value !== null;
28
- function pick(value, path) {
29
- if (!path) return value;
30
- return path.split(".").reduce(
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 store 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(
31
60
  (v, k) => v?.[k],
32
61
  value
33
62
  );
34
63
  }
64
+ var kindOf = (v) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
65
+ function readState(state, given) {
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 store.get 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("store.get 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
+ }
35
90
  function restore(store, snapshot) {
36
91
  const current = store.getState();
37
92
  if (!isObject(snapshot) || !isObject(current)) {
@@ -43,7 +98,47 @@ function restore(store, snapshot) {
43
98
  );
44
99
  store.setState({ ...snapshot, ...actions }, true);
45
100
  }
46
- function storeTools(stores) {
101
+ var PREVIEW = 200;
102
+ function preview(value) {
103
+ try {
104
+ const safe = toJson(value);
105
+ const text = JSON.stringify(safe);
106
+ if (text.length <= PREVIEW) return safe;
107
+ return `${text.slice(0, PREVIEW)}\u2026 (${text.length} chars)`;
108
+ } catch {
109
+ return "[unserializable]";
110
+ }
111
+ }
112
+ function differences(snapshot, current, show) {
113
+ if (!isObject(snapshot) || !isObject(current)) return {};
114
+ const out = {};
115
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(snapshot), ...Object.keys(current)])) {
116
+ const a = snapshot[k];
117
+ const b = current[k];
118
+ if (typeof a === "function" || typeof b === "function") continue;
119
+ if (!Object.is(a, b))
120
+ out[k] = { snapshot: preview(show(k, a)), current: preview(show(k, b)) };
121
+ }
122
+ return out;
123
+ }
124
+ var REDACTED = "[redacted]";
125
+ function redactValue(redact, name, value, path, seen = []) {
126
+ if (!redact || typeof value === "function") return value;
127
+ if (typeof redact === "function") {
128
+ if (path) value = redact(name, path, value);
129
+ } else if (path && redact[name]?.some((p) => path === p || path.startsWith(`${p}.`))) {
130
+ return REDACTED;
131
+ }
132
+ if (!isObject(value) || seen.includes(value)) return value;
133
+ const next = [...seen, value];
134
+ const entries = Object.entries(value).map(([k, v]) => [
135
+ k,
136
+ redactValue(redact, name, v, path ? `${path}.${k}` : k, next)
137
+ ]);
138
+ return Array.isArray(value) ? entries.map(([, v]) => v) : Object.fromEntries(entries);
139
+ }
140
+ function storeTools(stores, options = {}) {
141
+ const out = (name, value, path = "") => redactValue(options.redact, name, value, path);
47
142
  const get = (name) => {
48
143
  const store = stores[name];
49
144
  if (!store)
@@ -58,34 +153,79 @@ function storeTools(stores) {
58
153
  return {
59
154
  "store.list": {
60
155
  description: "Store names.",
156
+ maxArgs: 0,
61
157
  run: () => Object.keys(stores)
62
158
  },
63
159
  "store.get": {
64
- description: 'State of a store, or one dotted path inside it (e.g. "auth.isLoggedIn").',
65
- run: (name, path) => pick(get(name).getState(), path)
160
+ description: 'State of a store, or a path inside it: ("auth", "auth.isLoggedIn"), ("auth", "auth", "isLoggedIn") or ("auth", ["auth", "isLoggedIn"]). A last { pick: ["a.b", "c"] } returns just those fields; { keys: true } lists keys and their types without values. Paths configured with redact print as "[redacted]".',
161
+ // Redact the whole state first, so paths, pick and keys all see it.
162
+ run: (name, ...rest) => readState(out(name, get(name).getState()), rest)
66
163
  },
67
164
  "store.set": {
68
- description: "Shallow-merge a partial into a store.",
165
+ description: "Shallow-merge a partial into a store. Returns only the keys you set, with their new values, not the whole store. A store is snapshotted on its first store.set or store.call since the last restore (or store.commit); bridge.restore puts that snapshot back. Changes made before that, e.g. by tapping the UI, are not in it: run store.set <store> {} first to snapshot early, and see bridge.pending.",
69
166
  run: (name, partial) => {
70
167
  const store = get(name);
71
168
  beforeChange(store);
72
169
  store.setState(partial);
73
- return store.getState();
170
+ const state = store.getState();
171
+ return out(
172
+ name,
173
+ Object.fromEntries(Object.keys(partial).map((k) => [k, state[k]]))
174
+ );
74
175
  }
75
176
  },
76
177
  "store.call": {
77
- description: 'Call an action on a store, e.g. ("settings", "setColorScheme", "dark").',
78
- run: (name, action, ...args) => {
178
+ description: `Call an action on a store, e.g. ("settings", "setColorScheme", "dark"). Returns the action's return value, or { changed: [keys] } when it returns nothing or the store state. Snapshots the store first, like store.set: the first store.set or store.call since the last restore is the state bridge.restore returns to.`,
179
+ run: async (name, action, ...args) => {
79
180
  const store = get(name);
80
181
  const fn = store.getState()[action];
81
182
  if (typeof fn !== "function")
82
183
  throw new Error(`Store "${name}" has no action "${action}"`);
83
184
  beforeChange(store);
84
- return fn(...args);
185
+ const before = store.getState();
186
+ const result = await fn(...args);
187
+ const after = store.getState();
188
+ if (result !== void 0 && result !== before && result !== after) {
189
+ const key = isObject(after) ? Object.keys(after).find((k) => after[k] === result) : void 0;
190
+ return out(name, result, key);
191
+ }
192
+ const a = before;
193
+ const b = after;
194
+ return {
195
+ changed: Object.keys(b).filter(
196
+ (k) => typeof b[k] !== "function" && !Object.is(a?.[k], b[k])
197
+ )
198
+ };
199
+ }
200
+ },
201
+ "store.commit": {
202
+ maxArgs: 1,
203
+ 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
+ run: (name) => {
205
+ const store = get(name);
206
+ return { store: name, committed: snapshots.delete(store) };
85
207
  }
86
208
  },
87
209
  "store.restore": {
88
- description: "Undo the agent: put back each store changed with store.set or store.call. Returns their names.",
210
+ maxArgs: 0,
211
+ // Per store, its snapshot against now (secrets redacted), so an agent can
212
+ // tell what a restore will put back before it does.
213
+ pending: ({ detail } = {}) => {
214
+ const changed = Object.entries(stores).filter(([, s]) => snapshots.has(s));
215
+ if (!changed.length) return false;
216
+ if (!detail) return true;
217
+ return Object.fromEntries(
218
+ changed.map(([name, store]) => [
219
+ name,
220
+ differences(
221
+ snapshots.get(store),
222
+ store.getState(),
223
+ (key, value) => out(name, value, key)
224
+ )
225
+ ])
226
+ );
227
+ },
228
+ 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.",
89
229
  run: () => Object.entries(stores).flatMap(([name, store]) => {
90
230
  if (!snapshots.has(store)) return [];
91
231
  restore(store, snapshots.get(store));
@@ -1,4 +1,4 @@
1
- import { T as Tools } from '../types-6kKQsLC1.cjs';
1
+ import { T as Tools } from '../types-BmpykP1P.cjs';
2
2
 
3
3
  /** Anything with zustand's store shape. */
4
4
  type StoreLike = {
@@ -9,7 +9,23 @@ type StoreLike = {
9
9
  */
10
10
  setState(partial: Record<string, unknown>, replace?: boolean): void;
11
11
  };
12
+ /**
13
+ * Keeps secrets out of what store.* prints. Either dotted paths per store
14
+ * (`{ auth: ['accessToken', 'user.phone'] }`), or a hook called for the store
15
+ * value and every nested value with its dotted path; what it returns is
16
+ * printed instead.
17
+ *
18
+ * Paths are store paths. A `store.call` result that is a piece of the store's
19
+ * state (same object, e.g. `() => get().user`) is matched by where it lives; a
20
+ * copy or derived value (`() => ({ ...get().user })`) is not, so its paths are
21
+ * matched relative to the result and store paths like `user.phone` won't apply.
22
+ * Use the hook, or don't return secrets from actions.
23
+ */
24
+ type Redact = Record<string, string[]> | ((store: string, path: string, value: unknown) => unknown);
25
+ type StoreToolsOptions = {
26
+ redact?: Redact;
27
+ };
12
28
  /** Read, merge into, and call actions on named stores. */
13
- declare function storeTools(stores: Record<string, StoreLike>): Tools;
29
+ declare function storeTools(stores: Record<string, StoreLike>, options?: StoreToolsOptions): Tools;
14
30
 
15
- export { type StoreLike, storeTools };
31
+ export { type Redact, type StoreLike, type StoreToolsOptions, storeTools };