@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
package/dist/cli.js CHANGED
@@ -2,7 +2,9 @@
2
2
  import {
3
3
  AgentBridgeCallError,
4
4
  RUNTIME_MARKER,
5
+ SessionLinkError,
5
6
  assertListening,
7
+ callTimeout,
6
8
  checkName,
7
9
  connect,
8
10
  connectSession,
@@ -24,27 +26,201 @@ import {
24
26
  projectRoot,
25
27
  readSession,
26
28
  removeSessionFiles,
27
- runFlow,
29
+ runFlows,
28
30
  sessionFiles,
29
31
  sessionRequest,
30
32
  stateDir,
31
33
  writePrivate
32
- } from "./chunk-3QIG7A5W.js";
34
+ } from "./chunk-Y6QFU5ON.js";
33
35
 
34
36
  // src/cli.ts
35
- import { readFile } from "fs/promises";
36
- import { resolve as resolve2 } from "path";
37
- import { pathToFileURL } from "url";
37
+ import { createRequire } from "module";
38
+ import { mkdir as mkdir3, readFile } from "fs/promises";
39
+ import { dirname as dirname7 } from "path";
38
40
  import { parseArgs } from "util";
39
41
 
42
+ // src/client/flow-cli.ts
43
+ import { mkdir, realpath as realpath2, rename, writeFile } from "fs/promises";
44
+ import { dirname, join as join2, relative as relative2 } from "path";
45
+ import { pathToFileURL } from "url";
46
+
47
+ // src/client/flow-files.ts
48
+ import { readdir, realpath, stat } from "fs/promises";
49
+ import { basename, join, relative, resolve } from "path";
50
+ var FLOW_SUFFIX = /\.flow\.mjs$/;
51
+ function globRegExp(glob) {
52
+ let re = "";
53
+ for (let i = 0; i < glob.length; i++) {
54
+ const c = glob[i];
55
+ if (c === "*" && glob[i + 1] === "*") {
56
+ i++;
57
+ if (glob[i + 1] === "/") {
58
+ i++;
59
+ re += "(?:.*/)?";
60
+ } else re += ".*";
61
+ } else if (c === "*") re += "[^/]*";
62
+ else if (c === "?") re += "[^/]";
63
+ else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
64
+ }
65
+ return new RegExp(`^${re}$`);
66
+ }
67
+ var collator = new Intl.Collator("en", { numeric: true });
68
+ var posix = (p) => p.split("\\").join("/");
69
+ var byPath = (a, b) => collator.compare(posix(a), posix(b));
70
+ async function walk(dir, seen = /* @__PURE__ */ new Set()) {
71
+ const real = await realpath(dir);
72
+ if (seen.has(real)) return [];
73
+ seen.add(real);
74
+ const out = [];
75
+ for (const e of await readdir(dir, { withFileTypes: true })) {
76
+ if (e.name === "node_modules" || e.name.startsWith(".")) continue;
77
+ const full = join(dir, e.name);
78
+ const isDir = e.isDirectory() || e.isSymbolicLink() && (await stat(full).catch(() => null))?.isDirectory();
79
+ if (isDir) out.push(...await walk(full, seen));
80
+ else out.push(full);
81
+ }
82
+ return out;
83
+ }
84
+ async function resolveFlowFiles(args, cwd = process.cwd()) {
85
+ const files = [];
86
+ const seen = /* @__PURE__ */ new Set();
87
+ for (const given of args) {
88
+ const arg = process.platform === "win32" ? given.split("\\").join("/") : given;
89
+ const path = resolve(cwd, arg);
90
+ const info = await stat(path).catch(() => null);
91
+ let found;
92
+ if (info?.isDirectory())
93
+ found = (await walk(path)).filter((f) => FLOW_SUFFIX.test(f)).sort(byPath);
94
+ else if (info) found = [path];
95
+ else if (/[*?]/.test(arg)) {
96
+ const fixed2 = arg.split("/").findIndex((seg) => /[*?]/.test(seg));
97
+ const base = resolve(cwd, arg.split("/").slice(0, fixed2).join("/") || ".");
98
+ const re = globRegExp(posix(relative(cwd, resolve(cwd, arg))));
99
+ found = (await walk(base).catch(() => [])).filter((f) => FLOW_SUFFIX.test(f) && re.test(posix(relative(cwd, f)))).sort(byPath);
100
+ } else throw new Error(`No such flow file or directory: ${given}`);
101
+ if (!found.length) throw new Error(`No flow files match ${given} (a directory or glob only finds *.flow.mjs)`);
102
+ for (const f of found) {
103
+ const real = await realpath(f);
104
+ if (!seen.has(real)) {
105
+ seen.add(real);
106
+ files.push(real);
107
+ }
108
+ }
109
+ }
110
+ return files;
111
+ }
112
+ var flowName = (file, exported) => exported ?? basename(file).replace(FLOW_SUFFIX, "").replace(/\.[mc]?[jt]s$/, "");
113
+
114
+ // src/client/flow-cli.ts
115
+ var REPORTERS = ["text", "json", "junit"];
116
+ function checkRunFlags(flags) {
117
+ const reporter = flags.reporter ?? "text";
118
+ if (!REPORTERS.includes(reporter))
119
+ throw new Error(`--reporter is text, json or junit, not ${reporter}`);
120
+ if (flags.out && reporter === "text")
121
+ throw new Error("--out needs --reporter json or junit (run: --out is the report file)");
122
+ return reporter !== "text";
123
+ }
124
+ var failedRun = (file, name, error) => ({
125
+ file,
126
+ name,
127
+ status: "failed",
128
+ ms: 0,
129
+ steps: 0,
130
+ errors: 0,
131
+ error,
132
+ restoreErrors: [],
133
+ notices: []
134
+ });
135
+ var reason = (error) => error instanceof Error ? error.message : String(error);
136
+ async function loadFlows(files, cwd = process.cwd()) {
137
+ const root = await realpath2(cwd).catch(() => cwd);
138
+ const entries = [];
139
+ const failures = [];
140
+ for (const abs of files) {
141
+ const file = relative2(root, abs).split("\\").join("/");
142
+ try {
143
+ const module = await import(pathToFileURL(abs).href);
144
+ if (typeof module.default !== "function")
145
+ throw new Error(`${file} has no \`export default async ({ step }) => {}\``);
146
+ if (module.name !== void 0 && typeof module.name !== "string")
147
+ throw new Error(`${file}: \`export const name\` must be a string, not ${typeof module.name}`);
148
+ flowScenarios(module);
149
+ entries.push({ file, name: flowName(abs, module.name), module });
150
+ } catch (error) {
151
+ failures.push(failedRun(file, flowName(abs), reason(error)));
152
+ }
153
+ }
154
+ return { entries, failures };
155
+ }
156
+ function selectFlows(entries, grep) {
157
+ if (!grep) return entries;
158
+ const needle = grep.toLowerCase();
159
+ return entries.filter((e) => `${e.name}
160
+ ${e.file}`.toLowerCase().includes(needle));
161
+ }
162
+ async function writeReport(out, doc) {
163
+ await mkdir(dirname(out), { recursive: true });
164
+ const tmp = join2(dirname(out), `.${process.pid}-${Date.now()}.tmp`);
165
+ await writeFile(tmp, doc.endsWith("\n") ? doc : `${doc}
166
+ `);
167
+ await rename(tmp, out);
168
+ }
169
+ async function logToStderr(fn) {
170
+ const log = console.log;
171
+ console.log = (...args) => console.error(...args);
172
+ try {
173
+ return await fn();
174
+ } finally {
175
+ console.log = log;
176
+ }
177
+ }
178
+
179
+ // src/client/flow-report.ts
180
+ var ANSI = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)|\u001b[@-Z\\-_]/g;
181
+ var escape = (s) => s.replace(ANSI, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ufffe\uffff]/g, "").replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
182
+ var classname = (file) => {
183
+ const at = file.lastIndexOf("/");
184
+ return at > 0 ? file.slice(0, at) : "agent-bridge";
185
+ };
186
+ function renderJson(runs) {
187
+ const count = (s) => runs.filter((r) => r.status === s).length;
188
+ return JSON.stringify(
189
+ { passed: count("passed"), failed: count("failed"), skipped: count("skipped"), flows: runs },
190
+ null,
191
+ 2
192
+ );
193
+ }
194
+ function renderJunit(runs) {
195
+ const failed = runs.filter((r) => r.status === "failed").length;
196
+ const skipped = runs.filter((r) => r.status === "skipped").length;
197
+ const time = runs.reduce((t, r) => t + r.ms, 0) / 1e3;
198
+ const cases = runs.map((r) => {
199
+ const open = `<testcase classname="${escape(classname(r.file))}" name="${escape(r.name)}" file="${escape(r.file)}" time="${(r.ms / 1e3).toFixed(3)}"`;
200
+ if (r.status === "skipped") return `${open}><skipped/></testcase>`;
201
+ if (r.status === "passed") return `${open}/>`;
202
+ const screen = r.state?.screen?.length ? `
203
+ on screen:
204
+ ${r.state.screen.join("\n")}` : "";
205
+ const route = r.state?.route !== void 0 ? `
206
+ route: ${JSON.stringify(r.state.route)}` : "";
207
+ return `${open}><failure message="${escape(r.error ?? "failed")}">${escape((r.error ?? "") + route + screen)}</failure></testcase>`;
208
+ });
209
+ return `<?xml version="1.0" encoding="UTF-8"?>
210
+ <testsuite name="agent-bridge" tests="${runs.length}" failures="${failed}" skipped="${skipped}" time="${time.toFixed(3)}">
211
+ ${cases.join("\n")}
212
+ </testsuite>
213
+ `;
214
+ }
215
+
40
216
  // src/client/batch.ts
41
217
  import { readFileSync } from "fs";
42
- import { mkdir, readdir, stat } from "fs/promises";
43
- import { join } from "path";
218
+ import { mkdir as mkdir2, readdir as readdir2, stat as stat2 } from "fs/promises";
219
+ import { join as join3 } from "path";
44
220
  import { createInterface } from "readline";
45
221
 
46
222
  // src/client/output.ts
47
- import { writeFile } from "fs/promises";
223
+ import { writeFile as writeFile2 } from "fs/promises";
48
224
  var OUTPUT_LIMIT_BYTES = 32 * 1024;
49
225
  var kb = (bytes) => bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
50
226
  function shapeOf(value) {
@@ -76,7 +252,7 @@ async function shapedResult(value, options = {}) {
76
252
  const json = JSON.stringify(value, null, 2) ?? "null";
77
253
  const bytes = Buffer.byteLength(json);
78
254
  if (options.out) {
79
- await writeFile(options.out, `${json}
255
+ await writeFile2(options.out, `${json}
80
256
  `);
81
257
  return fileSummary(options.out, bytes, value);
82
258
  }
@@ -88,7 +264,7 @@ async function renderResult(value, options = {}) {
88
264
  const json = JSON.stringify(value, null, 2) ?? "null";
89
265
  const bytes = Buffer.byteLength(json);
90
266
  if (options.out) {
91
- await writeFile(options.out, `${json}
267
+ await writeFile2(options.out, `${json}
92
268
  `);
93
269
  return JSON.stringify(fileSummary(options.out, bytes, value), null, 2);
94
270
  }
@@ -96,6 +272,16 @@ async function renderResult(value, options = {}) {
96
272
  return json;
97
273
  return JSON.stringify(tooLarge(bytes, value), null, 2);
98
274
  }
275
+ async function renderText(text2, options = {}) {
276
+ const bytes = Buffer.byteLength(text2);
277
+ if (options.out) {
278
+ await writeFile2(options.out, `${text2}
279
+ `);
280
+ return JSON.stringify(fileSummary(options.out, bytes, text2), null, 2);
281
+ }
282
+ if (options.full || bytes <= (options.limit ?? OUTPUT_LIMIT_BYTES)) return text2;
283
+ return JSON.stringify(tooLarge(bytes, text2), null, 2);
284
+ }
99
285
 
100
286
  // src/client/batch.ts
101
287
  function parseCallArgs(raw) {
@@ -123,9 +309,9 @@ function fileJson(ref, read) {
123
309
  throw new Error(
124
310
  `An argument that is just "@" needs a path (@file, or @- for stdin). To pass the text itself, quote it as JSON: '"@"'`
125
311
  );
126
- const text = read(path);
312
+ const text2 = read(path);
127
313
  try {
128
- return JSON.parse(text);
314
+ return JSON.parse(text2);
129
315
  } catch (error) {
130
316
  throw new Error(
131
317
  `${path === "-" ? "stdin" : path} is not valid JSON (${error instanceof Error ? error.message : error})`
@@ -149,19 +335,19 @@ function parseCallArgv(argv, read = readArgFile) {
149
335
  });
150
336
  }
151
337
  var BatchParseError = class extends Error {
152
- constructor(tool, message2) {
153
- super(message2);
338
+ constructor(tool, message3) {
339
+ super(message3);
154
340
  this.tool = tool;
155
341
  }
156
342
  tool;
157
343
  };
158
344
  function parseBatchLine(line) {
159
- const text = line.trim();
160
- if (!text || text.startsWith("#")) return null;
161
- const at = text.search(/\s/);
162
- if (at < 0) return { tool: text, args: [] };
163
- const tool = text.slice(0, at);
164
- const raw = text.slice(at).trim();
345
+ const text2 = line.trim();
346
+ if (!text2 || text2.startsWith("#")) return null;
347
+ const at = text2.search(/\s/);
348
+ if (at < 0) return { tool: text2, args: [] };
349
+ const tool = text2.slice(0, at);
350
+ const raw = text2.slice(at).trim();
165
351
  if (/^[[{"]/.test(raw)) {
166
352
  try {
167
353
  JSON.parse(raw);
@@ -292,11 +478,11 @@ async function dotLine(bridge, line) {
292
478
  }
293
479
  }
294
480
  async function checkOutDir(dir) {
295
- const info = await stat(dir).catch(() => null);
296
- if (!info) return mkdir(dir, { recursive: true });
481
+ const info = await stat2(dir).catch(() => null);
482
+ if (!info) return mkdir2(dir, { recursive: true });
297
483
  if (!info.isDirectory())
298
484
  throw new Error(`--out ${dir} is a file; with --batch it must be a directory`);
299
- if ((await readdir(dir)).length)
485
+ if ((await readdir2(dir)).length)
300
486
  throw new Error(
301
487
  `--out ${dir} is not empty; use a new or empty directory so old files aren't mistaken for this run's`
302
488
  );
@@ -309,9 +495,9 @@ async function runBatch(bridge, input, options = {}) {
309
495
  let stopped = false;
310
496
  try {
311
497
  for await (const line of rl) {
312
- const text = line.trim();
313
- const outcome = text.startsWith(".") ? await dotLine(bridge, text) : await runLine(bridge, line);
314
- if (text.startsWith(".") && outcome === null) break;
498
+ const text2 = line.trim();
499
+ const outcome = text2.startsWith(".") ? await dotLine(bridge, text2) : await runLine(bridge, line);
500
+ if (text2.startsWith(".") && outcome === null) break;
315
501
  if (!outcome) continue;
316
502
  result.calls++;
317
503
  const raw = outcome;
@@ -326,7 +512,7 @@ async function runBatch(bridge, input, options = {}) {
326
512
  record.value = await shapedResult(raw.value, {
327
513
  full: options.full,
328
514
  outKind: "dir",
329
- out: options.outDir ? join(
515
+ out: options.outDir ? join3(
330
516
  options.outDir,
331
517
  `${result.calls}-${String(raw.tool).replace(/[^\w.-]/g, "_")}.json`
332
518
  ) : void 0
@@ -355,13 +541,27 @@ async function runBatch(bridge, input, options = {}) {
355
541
  return result;
356
542
  }
357
543
 
544
+ // src/client/snapshot-format.ts
545
+ function withCompactSnapshot(tool, args, json = false) {
546
+ if (tool !== "screen.snapshot" || args.length > 1) return args;
547
+ const options = args[0];
548
+ if (options !== void 0 && (options === null || typeof options !== "object" || Array.isArray(options)))
549
+ return args;
550
+ const given = options ?? {};
551
+ if (given.format === "text" && json)
552
+ throw new Error('--json and {"format":"text"} ask for different formats; drop one');
553
+ if (given.format !== void 0) return args;
554
+ return [{ ...given, format: json ? "json" : "text" }];
555
+ }
556
+ var isSnapshotText = (tool, value) => tool === "screen.snapshot" && typeof value === "string";
557
+
358
558
  // src/client/repl.ts
359
559
  import { appendFileSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
360
560
  import { homedir } from "os";
361
- import { dirname, join as join2 } from "path";
561
+ import { dirname as dirname2, join as join4 } from "path";
362
562
  import { createInterface as createInterface2 } from "readline";
363
563
  var HISTORY_LIMIT = 500;
364
- var DEFAULT_HISTORY_FILE = join2(
564
+ var DEFAULT_HISTORY_FILE = join4(
365
565
  homedir(),
366
566
  ".agent-bridge",
367
567
  "repl_history"
@@ -413,7 +613,7 @@ function loadHistory(file) {
413
613
  }
414
614
  function saveHistory(file, lines) {
415
615
  try {
416
- mkdirSync(dirname(file), { recursive: true });
616
+ mkdirSync(dirname2(file), { recursive: true });
417
617
  writeFileSync(file, `${lines.slice(-HISTORY_LIMIT).join("\n")}
418
618
  `, {
419
619
  mode: 384
@@ -423,7 +623,7 @@ function saveHistory(file, lines) {
423
623
  }
424
624
  function appendHistory(file, line) {
425
625
  try {
426
- mkdirSync(dirname(file), { recursive: true });
626
+ mkdirSync(dirname2(file), { recursive: true });
427
627
  appendFileSync(file, `${line}
428
628
  `, { mode: 384 });
429
629
  } catch {
@@ -435,9 +635,9 @@ async function runRepl(bridge, options) {
435
635
  const { output } = options;
436
636
  const tty = !!options.tty;
437
637
  const { historyFile } = options;
438
- const paint = (code, text) => tty && !process.env.NO_COLOR ? `\x1B[${code}m${text}\x1B[0m` : text;
439
- const print = (text = "") => {
440
- output.write(`${text}
638
+ const paint = (code, text2) => tty && !process.env.NO_COLOR ? `\x1B[${code}m${text2}\x1B[0m` : text2;
639
+ const print = (text2 = "") => {
640
+ output.write(`${text2}
441
641
  `);
442
642
  };
443
643
  const toolNames = () => bridge.tools().map((t) => t.name);
@@ -590,21 +790,201 @@ import { spawn } from "child_process";
590
790
  import { existsSync, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
591
791
 
592
792
  // src/client/session/daemon.ts
793
+ import { randomBytes } from "crypto";
593
794
  import { appendFileSync as appendFileSync2, chmodSync, rmSync } from "fs";
594
795
  import { createServer } from "net";
595
796
  import { createInterface as createInterface3 } from "readline";
797
+
798
+ // src/client/session/journal.ts
799
+ var MAX_TRIES = 3;
800
+ var lostConnection = (r) => !r.ok && (r.from === "" || /socket closed|closed the debugger|not open/i.test(r.error));
801
+ var plain = (v) => !!v && typeof v === "object" && !Array.isArray(v);
802
+ var isStore = (v) => plain(v.original) && plain(v.after);
803
+ function mergeStore(old, next) {
804
+ const known = /* @__PURE__ */ new Set([...Object.keys(old.original), ...Object.keys(old.after), ...old.skipped ?? []]);
805
+ const original = { ...old.original };
806
+ const after = { ...old.after };
807
+ const touched = /* @__PURE__ */ new Set([...Object.keys(next.original), ...Object.keys(next.after), ...next.skipped ?? []]);
808
+ for (const k of touched) {
809
+ if (!known.has(k) && k in next.original) original[k] = next.original[k];
810
+ if (k in next.after) after[k] = next.after[k];
811
+ else delete after[k];
812
+ }
813
+ const skipped = [.../* @__PURE__ */ new Set([...old.skipped ?? [], ...next.skipped ?? []])];
814
+ return { original, after, ...skipped.length && { skipped } };
815
+ }
816
+ function merge(carry, data) {
817
+ for (const [area, part] of Object.entries(data)) {
818
+ const old = carry[area];
819
+ if (!plain(old) || !plain(part)) {
820
+ carry[area] = part;
821
+ continue;
822
+ }
823
+ const next = { ...old };
824
+ for (const [name, value] of Object.entries(part)) {
825
+ const was = old[name];
826
+ next[name] = plain(was) && plain(value) ? isStore(was) && isStore(value) ? mergeStore(was, value) : { ...value, ...was } : value;
827
+ }
828
+ carry[area] = next;
829
+ }
830
+ }
831
+ var names = (part) => plain(part) ? Object.keys(part) : [];
832
+ function createJournal(call, firstLoad, alive = async () => false) {
833
+ let data = {};
834
+ let tag = "";
835
+ let load = firstLoad;
836
+ const carry = {};
837
+ let tries = 0;
838
+ let lastWaiting = "";
839
+ const why = (area, part, reason2) => {
840
+ const keys = names(part);
841
+ return keys.length ? keys.map((n) => `${area}:${n} (${reason2(n)})`) : [`${area} (${reason2(area)})`];
842
+ };
843
+ const gaveUp = (reasons) => {
844
+ const left = Object.entries(carry).flatMap(
845
+ ([area, part]) => why(area, part, () => reasons[area] ?? "not ready")
846
+ );
847
+ for (const k of Object.keys(carry)) delete carry[k];
848
+ return left.length ? `could not re-arm after the reload: ${left.join(", ")}` : "";
849
+ };
850
+ return {
851
+ /** The tag to send with every call. */
852
+ ack: () => tag,
853
+ /** Takes the journal a reply carries; one from another load is dropped. */
854
+ accept(r) {
855
+ if (r.journalData === void 0 || !r.loadId || r.loadId !== load) return;
856
+ data = r.journalData;
857
+ tag = `${r.loadId}:${r.journal ?? 0}`;
858
+ },
859
+ /** The app reloaded into `newLoad`: what the old load held moves to `carry`. Returns the pending areas that had nothing persistent. */
860
+ reloaded(lastPending, newLoad) {
861
+ merge(carry, data);
862
+ data = {};
863
+ tag = "";
864
+ load = newLoad;
865
+ tries = 0;
866
+ lastWaiting = "";
867
+ return lastPending.filter((area) => !(area in carry));
868
+ },
869
+ hasCarry: () => Object.keys(carry).length > 0,
870
+ /** The load the journal is for now. */
871
+ currentLoad: () => load,
872
+ /** Hands `carry` to the new load. Resolves to a note ('' when there was nothing to say). */
873
+ async adopt() {
874
+ if (!Object.keys(carry).length) return "";
875
+ const sent = structuredClone(carry);
876
+ const sentLoad = load;
877
+ let r;
878
+ try {
879
+ r = await call("bridge.adopt", [sent]);
880
+ } catch {
881
+ if (!await alive().catch(() => false)) return "";
882
+ r = { id: "", from: "app", ok: false, error: "timed out", ms: 0 };
883
+ }
884
+ if (lostConnection({ ...r })) return "";
885
+ if (load !== sentLoad || r.loadId && sentLoad && r.loadId !== sentLoad) return "";
886
+ this.accept(r);
887
+ tries += 1;
888
+ const bits = [];
889
+ const reasons = {};
890
+ if (!r.ok) {
891
+ for (const area of Object.keys(sent)) reasons[area] = r.error;
892
+ } else {
893
+ const reply = r.value;
894
+ const adopted = [];
895
+ const already = [];
896
+ const changed = [];
897
+ const invalid = [];
898
+ const skipped = [];
899
+ const notKept = [];
900
+ for (const area of Object.keys(sent)) {
901
+ const part = reply[area];
902
+ if (!part) continue;
903
+ adopted.push(...part.adopted ?? []);
904
+ already.push(...part.already ?? []);
905
+ changed.push(...part.changed ?? []);
906
+ invalid.push(...part.invalid ?? []);
907
+ skipped.push(...part.skipped ?? []);
908
+ notKept.push(...part.notKept ?? []);
909
+ if (part.retry !== void 0) {
910
+ carry[area] = part.retry;
911
+ reasons[area] = part.error ?? "not ready";
912
+ } else delete carry[area];
913
+ }
914
+ if (adopted.length)
915
+ bits.push(`kept ${adopted.length} undo${adopted.length === 1 ? "" : "s"} across the reload (${adopted.join(", ")}); bridge.restore still puts them back`);
916
+ if (already.length) bits.push(`already back to the original: ${already.join(", ")}`);
917
+ if (changed.length) bits.push(`left alone, the app changed them since: ${changed.join(", ")}`);
918
+ if (skipped.length) bits.push(`left alone, a different storage instance than the one you wrote: ${skipped.join(", ")}`);
919
+ if (notKept.length) bits.push(`not kept across reload (not plain JSON, renamed by partialize, or hidden by redact): ${notKept.join(", ")}`);
920
+ if (invalid.length) bits.push(`ignored, not a value this session saved: ${invalid.join(", ")}`);
921
+ }
922
+ if (Object.keys(carry).length) {
923
+ if (tries >= MAX_TRIES) {
924
+ bits.push(gaveUp(reasons));
925
+ lastWaiting = "";
926
+ } else {
927
+ const waiting = Object.entries(carry).flatMap(([area, part]) => why(area, part, () => reasons[area] ?? "not ready")).join(", ");
928
+ if (waiting !== lastWaiting) bits.push(`not ready yet, will retry: ${waiting}`);
929
+ lastWaiting = waiting;
930
+ }
931
+ }
932
+ return bits.join("; ");
933
+ },
934
+ /** At session end: drops what is still waiting and says so. */
935
+ giveUp: () => gaveUp({})
936
+ };
937
+ }
938
+
939
+ // src/client/session/daemon.ts
596
940
  var RELOAD = "app.reload";
597
- var pause = (ms, signal) => new Promise((resolve3) => {
941
+ var REPEATABLE = /* @__PURE__ */ new Set([
942
+ "bridge.ping",
943
+ "bridge.pending",
944
+ "bridge.tools",
945
+ "screen.snapshot",
946
+ "screen.findText",
947
+ "screen.waitFor",
948
+ "screen.capture",
949
+ "store.get",
950
+ "store.list",
951
+ "query.get",
952
+ "query.list",
953
+ "mmkv.get",
954
+ "mmkv.keys",
955
+ "mmkv.list",
956
+ "net.entry",
957
+ "net.mocks",
958
+ "router.current",
959
+ "scenario.list",
960
+ "webview.list",
961
+ "webview.url",
962
+ "webview.messages",
963
+ "webview.message",
964
+ "webview.snapshot",
965
+ "webview.waitFor",
966
+ "realtime.channels"
967
+ ]);
968
+ var LOG_TOOLS = /* @__PURE__ */ new Set(["net.log", "realtime.log", "bridge.logs"]);
969
+ var WAIT_TOOLS = /* @__PURE__ */ new Set(["screen.waitFor", "webview.waitFor"]);
970
+ var WAIT_DEFAULT_MS = 5e3;
971
+ var repeatable = (tool, args) => REPEATABLE.has(tool) || LOG_TOOLS.has(tool) && !args.some((a) => a?.clear);
972
+ var withTimeLeft = (args, leftMs) => {
973
+ const next = [...args];
974
+ const options = next[1] ?? {};
975
+ next[1] = { ...options, timeoutMs: Math.max(1, Math.round(leftMs)) };
976
+ return next;
977
+ };
978
+ var pause = (ms, signal) => new Promise((resolve4) => {
598
979
  const done = () => {
599
980
  clearTimeout(timer);
600
981
  signal.removeEventListener("abort", done);
601
- resolve3();
982
+ resolve4();
602
983
  };
603
984
  const timer = setTimeout(done, ms);
604
985
  signal.addEventListener("abort", done, { once: true });
605
986
  });
606
987
  var message = (error) => error instanceof Error ? error.message : String(error);
607
- var lostConnection = (r) => !r.ok && (r.from === "" || /socket closed|closed the debugger|not open/i.test(r.error));
608
988
  async function runSessionDaemon(options) {
609
989
  const name = checkName(options.name);
610
990
  const dir = stateDir();
@@ -612,7 +992,7 @@ async function runSessionDaemon(options) {
612
992
  const metro = metroHost(options.metro);
613
993
  const timeoutMs = options.timeoutMs ?? 1e4;
614
994
  const reloadWaitMs = options.reloadWaitMs ?? 2e4;
615
- const log = (text) => appendFileSync2(files.log, `${(/* @__PURE__ */ new Date()).toISOString()} ${text}
995
+ const log = (text2) => appendFileSync2(files.log, `${(/* @__PURE__ */ new Date()).toISOString()} ${text2}
616
996
  `, {
617
997
  mode: 384
618
998
  });
@@ -669,20 +1049,68 @@ async function runSessionDaemon(options) {
669
1049
  let lastPending = [];
670
1050
  const reloads = [];
671
1051
  let unseen = [];
672
- const noteReload = () => {
673
- const lost = lastPending;
674
- lastPending = [];
675
- const note = `app reloaded; ${lost.length} pending restore${lost.length === 1 ? "" : "s"} lost${lost.length ? `: ${lost.join(", ")}` : ""}`;
1052
+ const journalKey = randomBytes(16).toString("hex");
1053
+ const send = (tool, args, ms) => conn.call(tool, args, ms, { journalAck: journal.ack(), journalKey });
1054
+ const adoptTimeoutMs = options.adoptTimeoutMs ?? Math.max(timeoutMs, 3500);
1055
+ const journal = createJournal(
1056
+ (tool, args) => send(tool, args, adoptTimeoutMs),
1057
+ conn.device.loadId,
1058
+ () => ping()
1059
+ );
1060
+ const addNote = (note) => {
676
1061
  reloads.push(note);
677
1062
  unseen.push(note);
678
1063
  log(`warning: ${note}`);
679
1064
  };
1065
+ const adoptRetryMs = options.adoptRetryMs ?? 1e3;
1066
+ let adoptRun = null;
1067
+ let adoptLoad;
1068
+ let nextAdopt = 0;
1069
+ const adoptJournal = async (force = false) => {
1070
+ if (adoptRun) {
1071
+ const startedFor = adoptLoad;
1072
+ await adoptRun;
1073
+ if (!force || startedFor === journal.currentLoad()) return;
1074
+ if (adoptRun) return adoptRun;
1075
+ }
1076
+ if (!journal.hasCarry() || !force && Date.now() < nextAdopt) return;
1077
+ inflight++;
1078
+ adoptLoad = journal.currentLoad();
1079
+ adoptRun = journal.adopt().then(
1080
+ (note) => {
1081
+ if (note) addNote(`app reloaded; ${note}`);
1082
+ },
1083
+ () => {
1084
+ }
1085
+ ).finally(() => {
1086
+ inflight--;
1087
+ nextAdopt = Date.now() + adoptRetryMs;
1088
+ adoptRun = null;
1089
+ });
1090
+ return adoptRun;
1091
+ };
1092
+ const noteReload = (newLoad) => {
1093
+ const lost = journal.reloaded(lastPending, newLoad);
1094
+ lastPending = [];
1095
+ if (lost.length || !journal.hasCarry())
1096
+ addNote(
1097
+ `app reloaded; ${lost.length} pending restore${lost.length === 1 ? "" : "s"} lost${lost.length ? `: ${lost.join(", ")}` : ""}`
1098
+ );
1099
+ nextAdopt = 0;
1100
+ void adoptJournal(true);
1101
+ };
680
1102
  const observe = (r) => {
681
1103
  if (!r.loadId) return;
682
- if (knownLoad && r.loadId !== knownLoad) noteReload();
1104
+ if (knownLoad && r.loadId !== knownLoad) noteReload(r.loadId);
683
1105
  knownLoad = r.loadId;
684
1106
  lastPending = r.pending ?? [];
685
1107
  };
1108
+ const seen = (r) => {
1109
+ observe(r);
1110
+ journal.accept(r);
1111
+ const { journalData: _, ...rest } = r;
1112
+ return rest;
1113
+ };
686
1114
  const takeNotice = () => {
687
1115
  const notice = unseen.join("; ");
688
1116
  unseen = [];
@@ -691,9 +1119,9 @@ async function runSessionDaemon(options) {
691
1119
  let reloadingUntil = 0;
692
1120
  let stale = false;
693
1121
  let reconnecting = null;
694
- const reconnect = (reason) => {
1122
+ const reconnect = (reason2) => {
695
1123
  reconnecting ??= (async () => {
696
- log(`reconnecting: ${reason}`);
1124
+ log(`reconnecting: ${reason2}`);
697
1125
  conn.close();
698
1126
  const patient = Date.now() < reloadingUntil;
699
1127
  const deadline = Date.now() + (patient ? reloadWaitMs : 0);
@@ -712,7 +1140,7 @@ async function runSessionDaemon(options) {
712
1140
  stale = true;
713
1141
  log(`reconnect failed: ${message(error)}`);
714
1142
  throw new Error(
715
- patient ? `The app is reloading; its bridge isn't ready yet (${message(error)})` : `The app is gone (${reason}). Reconnecting failed: ${message(error)}`
1143
+ patient ? `The app is reloading; its bridge isn't ready yet (${message(error)})` : `The app is gone (${reason2}). Reconnecting failed: ${message(error)}`
716
1144
  );
717
1145
  }
718
1146
  await pause(300, stopping.signal);
@@ -721,7 +1149,7 @@ async function runSessionDaemon(options) {
721
1149
  stale = false;
722
1150
  reloadingUntil = 0;
723
1151
  if (conn.device.loadId) {
724
- if (knownLoad && conn.device.loadId !== knownLoad) noteReload();
1152
+ if (knownLoad && conn.device.loadId !== knownLoad) noteReload(conn.device.loadId);
725
1153
  knownLoad = conn.device.loadId;
726
1154
  }
727
1155
  state.device = deviceOf(conn);
@@ -733,8 +1161,8 @@ async function runSessionDaemon(options) {
733
1161
  });
734
1162
  return reconnecting;
735
1163
  };
736
- const connected = async (reason = "app stopped answering") => {
737
- const attempt2 = reconnecting ?? reconnect(reason);
1164
+ const connected = async (reason2 = "app stopped answering") => {
1165
+ const attempt2 = reconnecting ?? reconnect(reason2);
738
1166
  const wait = Date.now() < reloadingUntil ? reloadWaitMs + 2 * attemptMs + 500 : 2 * attemptMs + 500;
739
1167
  let timer2;
740
1168
  try {
@@ -755,16 +1183,16 @@ async function runSessionDaemon(options) {
755
1183
  clearTimeout(timer2);
756
1184
  }
757
1185
  };
758
- const ping = (ms = 1500) => conn.call("bridge.ping", [], Math.min(ms, 1500)).then(
1186
+ const ping = (ms = 1500) => send("bridge.ping", [], Math.min(ms, 1500)).then(
759
1187
  (r) => {
760
- if (r.ok) observe(r);
1188
+ if (r.ok) seen(r);
761
1189
  return r.ok;
762
1190
  },
763
1191
  () => false
764
1192
  );
765
1193
  const attempt = async (tool, args, ms) => {
766
1194
  try {
767
- const result = await conn.call(tool, args, ms);
1195
+ const result = await send(tool, args, ms);
768
1196
  return lostConnection(result) ? { lost: result.error } : { result };
769
1197
  } catch (error) {
770
1198
  if (await ping(ms)) throw error;
@@ -773,12 +1201,15 @@ async function runSessionDaemon(options) {
773
1201
  };
774
1202
  const callApp = async (tool, args, ms) => {
775
1203
  if (stale || reconnecting) await connected();
1204
+ await adoptJournal(tool === "bridge.restore");
1205
+ if (tool === RELOAD) await ping();
776
1206
  let t0 = performance.now();
1207
+ const start2 = t0;
777
1208
  const first = await attempt(tool, args, ms);
778
1209
  if (first.result) {
779
- observe(first.result);
780
- if (tool === RELOAD && first.result.ok) waitForReload();
781
- return { result: first.result, ms: performance.now() - t0 };
1210
+ const result = seen(first.result);
1211
+ if (tool === RELOAD && result.ok) waitForReload();
1212
+ return { result, ms: performance.now() - t0 };
782
1213
  }
783
1214
  if (tool === RELOAD) {
784
1215
  waitForReload();
@@ -791,20 +1222,48 @@ async function runSessionDaemon(options) {
791
1222
  };
792
1223
  return { result, ms: performance.now() - t0 };
793
1224
  }
794
- await connected(first.lost);
1225
+ const before = knownLoad;
1226
+ const goneWait = WAIT_TOOLS.has(tool) && args[1]?.gone;
1227
+ let reconnectFailure = "";
1228
+ try {
1229
+ await connected(first.lost);
1230
+ } catch (error) {
1231
+ reconnectFailure = message(error);
1232
+ }
1233
+ const reloaded = knownLoad !== before;
1234
+ if (!reconnectFailure) await sync();
1235
+ const again = repeatable(tool, args) && !(goneWait && reloaded);
1236
+ if (again && reconnectFailure) throw new Error(reconnectFailure);
1237
+ if (!again) {
1238
+ if ((reloaded || reconnectFailure) && /^(mmkv|store)\./.test(tool))
1239
+ addNote(
1240
+ `${tool} was cut off; if it wrote to MMKV or a persisted store, that undo may be lost`
1241
+ );
1242
+ const how = reloaded ? "the app reloaded" : before ? "the app disconnected" : "the connection dropped";
1243
+ throw new Error(
1244
+ `${how} during ${tool}; it may or may not have run (${first.lost})${reconnectFailure ? `; reconnecting failed: ${reconnectFailure}` : ""}`
1245
+ );
1246
+ }
795
1247
  t0 = performance.now();
796
- const second = await attempt(tool, args, ms);
1248
+ const retryArgs = WAIT_TOOLS.has(tool) ? withTimeLeft(
1249
+ args,
1250
+ (args[1]?.timeoutMs ?? WAIT_DEFAULT_MS) - (t0 - start2)
1251
+ ) : args;
1252
+ const second = await attempt(tool, retryArgs, ms);
797
1253
  if (!second.result)
798
1254
  throw new Error(
799
1255
  `The app stopped answering after a reconnect: ${second.lost}`
800
1256
  );
801
- observe(second.result);
802
1257
  return {
803
- result: second.result,
1258
+ result: seen(second.result),
804
1259
  ms: performance.now() - t0,
805
1260
  reconnected: true
806
1261
  };
807
1262
  };
1263
+ const sync = async () => {
1264
+ if (journal.hasCarry()) await adoptJournal(true);
1265
+ else await ping();
1266
+ };
808
1267
  const waitForReload = () => {
809
1268
  reloadingUntil = Date.now() + reloadWaitMs;
810
1269
  stale = true;
@@ -827,19 +1286,47 @@ async function runSessionDaemon(options) {
827
1286
  };
828
1287
  const health = options.healthMs === 0 ? void 0 : setInterval(async () => {
829
1288
  if (inflight || reconnecting || finishing) return;
830
- if (stale || !await ping())
1289
+ const pinged = conn;
1290
+ const answered = await ping();
1291
+ if (inflight || reconnecting || finishing || conn !== pinged) return;
1292
+ if (stale || !answered)
831
1293
  await reconnect("no answer to bridge.ping").catch(() => {
832
1294
  });
833
1295
  }, options.healthMs ?? 5e3);
834
1296
  const timer = health;
835
1297
  timer?.unref?.();
1298
+ const restoreOnce = async () => {
1299
+ const fail = (error) => ({ id: "restore", from: "", ok: false, error, ms: 0 });
1300
+ const run = (tool) => attempt(tool, [], timeoutMs);
1301
+ try {
1302
+ if (stale || reconnecting || !await ping()) await connected("no answer before restore");
1303
+ for (let i = 0; i < 3 && journal.hasCarry(); i++) {
1304
+ if (i) await new Promise((r) => setTimeout(r, Math.min(adoptRetryMs, 500)));
1305
+ await adoptJournal(true);
1306
+ }
1307
+ const first = await run("bridge.restore");
1308
+ if (first.result) return seen(first.result);
1309
+ await connected(first.lost);
1310
+ await adoptJournal(true);
1311
+ const check = await run("bridge.pending");
1312
+ if (!check.result) return fail(`restore may or may not have run (${first.lost})`);
1313
+ const left = seen(check.result);
1314
+ if (!left.ok) return fail(`restore may or may not have run (${first.lost}); ${left.error}`);
1315
+ if (!Object.keys(left.value).length)
1316
+ return { ...left, id: "restore", value: { note: "the reply was lost; nothing is pending now" } };
1317
+ const second = await run("bridge.restore");
1318
+ return second.result ? seen(second.result) : fail(`The app stopped answering after a reconnect: ${second.lost}`);
1319
+ } catch (error) {
1320
+ return fail(message(error));
1321
+ }
1322
+ };
836
1323
  let finishing = null;
837
1324
  let resolveDone = () => {
838
1325
  };
839
1326
  const done = new Promise((r) => {
840
1327
  resolveDone = r;
841
1328
  });
842
- const teardown = (reason, keep) => {
1329
+ const teardown = (reason2, keep) => {
843
1330
  finishing ??= (async () => {
844
1331
  clearTimeout(idleTimer);
845
1332
  clearInterval(health);
@@ -847,16 +1334,9 @@ async function runSessionDaemon(options) {
847
1334
  const cut = setTimeout(() => cutOff.abort(), Math.min(attemptMs, 5e3));
848
1335
  let restore = null;
849
1336
  if (!keep) {
850
- restore = await callApp("bridge.restore", [], timeoutMs).then(
851
- (r) => r.result,
852
- (error) => ({
853
- id: "restore",
854
- from: "",
855
- ok: false,
856
- error: message(error),
857
- ms: 0
858
- })
859
- );
1337
+ restore = await restoreOnce();
1338
+ const left = journal.giveUp();
1339
+ if (left) addNote(`app reloaded; ${left}`);
860
1340
  }
861
1341
  clearTimeout(cut);
862
1342
  cutOff.abort();
@@ -864,10 +1344,10 @@ async function runSessionDaemon(options) {
864
1344
  server.close();
865
1345
  removeSessionFiles(name, dir);
866
1346
  const outcome = !restore ? "kept app state" : restore.ok ? `restore: ${JSON.stringify(restore.value)}` : `restore: ${restore.error}`;
867
- log(`stopped (${reason}); ${outcome}`);
1347
+ log(`stopped (${reason2}); ${outcome}`);
868
1348
  const deadline = Date.now() + 2e3;
869
1349
  const settle = () => {
870
- if (!clients.size || Date.now() > deadline) resolveDone(reason);
1350
+ if (!clients.size || Date.now() > deadline) resolveDone(reason2);
871
1351
  else setTimeout(settle, 10);
872
1352
  };
873
1353
  settle();
@@ -889,16 +1369,21 @@ async function runSessionDaemon(options) {
889
1369
  switch (req.op) {
890
1370
  case "call": {
891
1371
  if (!req.tool) return { id: req.id, error: "call needs a tool" };
1372
+ if (req.tool === "bridge.adopt")
1373
+ return { id: req.id, error: "bridge.adopt is for a session's own use" };
892
1374
  inflight++;
893
1375
  touch();
894
1376
  try {
1377
+ const args = req.args ?? [];
895
1378
  const answer = await callApp(
896
1379
  req.tool,
897
- req.args ?? [],
898
- req.timeoutMs ?? timeoutMs
1380
+ args,
1381
+ callTimeout(req.timeoutMs ?? timeoutMs, req.tool, args)
899
1382
  );
1383
+ await adoptRun;
900
1384
  return { id: req.id, ...answer, ...takeNotice() };
901
1385
  } catch (error) {
1386
+ await adoptRun;
902
1387
  return { id: req.id, error: message(error), ...takeNotice() };
903
1388
  } finally {
904
1389
  inflight--;
@@ -941,7 +1426,10 @@ async function runSessionDaemon(options) {
941
1426
  socket.on("close", () => clients.delete(socket));
942
1427
  socket.on("error", () => {
943
1428
  });
944
- createInterface3({ input: socket }).on("line", async (line) => {
1429
+ const lines = createInterface3({ input: socket });
1430
+ lines.on("error", () => {
1431
+ });
1432
+ lines.on("line", async (line) => {
945
1433
  let req;
946
1434
  try {
947
1435
  req = JSON.parse(line);
@@ -958,11 +1446,11 @@ async function runSessionDaemon(options) {
958
1446
  });
959
1447
  if (process.platform !== "win32") rmSync(files.socket, { force: true });
960
1448
  try {
961
- await new Promise((resolve3, reject) => {
1449
+ await new Promise((resolve4, reject) => {
962
1450
  server.once("error", reject);
963
1451
  server.listen(files.socket, () => {
964
1452
  server.off("error", reject);
965
- resolve3();
1453
+ resolve4();
966
1454
  });
967
1455
  });
968
1456
  } catch (error) {
@@ -989,13 +1477,14 @@ async function runSessionDaemon(options) {
989
1477
 
990
1478
  // src/client/session/cli.ts
991
1479
  var DAEMON_COMMAND = "__session-daemon";
992
- function sessionFor(flags) {
1480
+ function sessionFor(flags, skip) {
993
1481
  if (flags["no-session"]) return null;
994
1482
  return pickSession({
995
1483
  name: flags.session ?? process.env.AGENT_BRIDGE_SESSION,
996
1484
  metro: explicitMetro(flags.metro),
997
1485
  device: flags.device,
998
- transport: flags.transport
1486
+ transport: flags.transport,
1487
+ skip
999
1488
  });
1000
1489
  }
1001
1490
  var describeRestore = (restore) => {
@@ -1049,7 +1538,7 @@ async function start(flags) {
1049
1538
  [...process.execArgv, process.argv[1], ...args],
1050
1539
  { detached: true, stdio: "ignore", windowsHide: true }
1051
1540
  );
1052
- const state = await new Promise((resolve3, reject) => {
1541
+ const state = await new Promise((resolve4, reject) => {
1053
1542
  const deadline = Date.now() + 3e4;
1054
1543
  let exited = false;
1055
1544
  child.once("exit", () => {
@@ -1060,13 +1549,13 @@ async function start(flags) {
1060
1549
  const failed = existsSync(files.error);
1061
1550
  if (ready && ready.pid === child.pid) {
1062
1551
  clearInterval(tick);
1063
- resolve3(ready);
1552
+ resolve4(ready);
1064
1553
  } else if (failed || exited || Date.now() > deadline) {
1065
1554
  clearInterval(tick);
1066
1555
  if (!exited) child.kill();
1067
- const reason = failed ? readFileSync3(files.error, "utf8") : exited ? `The session exited early; see ${files.log}` : `The session didn't connect within 30 s; see ${files.log}`;
1556
+ const reason2 = failed ? readFileSync3(files.error, "utf8") : exited ? `The session exited early; see ${files.log}` : `The session didn't connect within 30 s; see ${files.log}`;
1068
1557
  rmSync2(files.error, { force: true });
1069
- reject(new Error(reason));
1558
+ reject(new Error(reason2));
1070
1559
  }
1071
1560
  }, 50);
1072
1561
  });
@@ -1192,17 +1681,17 @@ import {
1192
1681
  readFileSync as readFileSync4,
1193
1682
  writeFileSync as writeFileSync2
1194
1683
  } from "fs";
1195
- import { dirname as dirname5, relative as relative4, resolve, sep as sep2 } from "path";
1684
+ import { dirname as dirname6, relative as relative6, resolve as resolve2, sep as sep2 } from "path";
1196
1685
  import { createInterface as createInterface4 } from "readline/promises";
1197
1686
 
1198
1687
  // package.json
1199
- var version = "0.5.0";
1688
+ var version = "0.6.0";
1200
1689
 
1201
1690
  // src/init/detect.ts
1202
- import { dirname as dirname3, join as join4, relative } from "path";
1691
+ import { dirname as dirname4, join as join6, relative as relative3 } from "path";
1203
1692
 
1204
1693
  // src/init/package-manager.ts
1205
- import { dirname as dirname2, join as join3 } from "path";
1694
+ import { dirname as dirname3, join as join5 } from "path";
1206
1695
  var LOCKFILES = [
1207
1696
  ["bun.lock", "bun"],
1208
1697
  ["bun.lockb", "bun"],
@@ -1216,12 +1705,12 @@ var fromName = (value) => {
1216
1705
  };
1217
1706
  function detectPackageManager(dir, root, fs, userAgent = "") {
1218
1707
  for (const d of [dir, root]) {
1219
- const pm = fromName(fs.readJson(join3(d, "package.json"))?.packageManager);
1708
+ const pm = fromName(fs.readJson(join5(d, "package.json"))?.packageManager);
1220
1709
  if (pm) return pm;
1221
1710
  }
1222
- for (let d = dir; ; d = dirname2(d)) {
1223
- for (const [file, pm] of LOCKFILES) if (fs.exists(join3(d, file))) return pm;
1224
- if (d === root || dirname2(d) === d) break;
1711
+ for (let d = dir; ; d = dirname3(d)) {
1712
+ for (const [file, pm] of LOCKFILES) if (fs.exists(join5(d, file))) return pm;
1713
+ if (d === root || dirname3(d) === d) break;
1225
1714
  }
1226
1715
  return fromName(userAgent.split("/")[0]) ?? "npm";
1227
1716
  }
@@ -1258,7 +1747,12 @@ var LIBRARIES = {
1258
1747
  reactQuery: "@tanstack/react-query",
1259
1748
  webview: "react-native-webview",
1260
1749
  zustand: "zustand",
1750
+ redux: "@reduxjs/toolkit",
1751
+ reduxCore: "redux",
1752
+ jotai: "jotai",
1261
1753
  mmkv: "react-native-mmkv",
1754
+ asyncStorage: "@react-native-async-storage/async-storage",
1755
+ secureStore: "expo-secure-store",
1262
1756
  ably: "ably",
1263
1757
  socketIo: "socket.io-client",
1264
1758
  reactNavigation: "@react-navigation/native",
@@ -1270,18 +1764,18 @@ var depsOf = (pkg) => ({
1270
1764
  ...pkg?.devDependencies,
1271
1765
  ...pkg?.dependencies
1272
1766
  });
1273
- var isApp = (dir, fs) => !!fs.readJson(join4(dir, "package.json"))?.dependencies?.["react-native"] || ["app.json", "app.config.js", "app.config.ts", "ios", "android"].some((f) => fs.exists(join4(dir, f)));
1274
- var isWorkspaceRoot = (dir, fs) => !!fs.readJson(join4(dir, "package.json"))?.workspaces || fs.exists(join4(dir, "pnpm-workspace.yaml"));
1767
+ var isApp = (dir, fs) => !!fs.readJson(join6(dir, "package.json"))?.dependencies?.["react-native"] || ["app.json", "app.config.js", "app.config.ts", "ios", "android"].some((f) => fs.exists(join6(dir, f)));
1768
+ var isWorkspaceRoot = (dir, fs) => !!fs.readJson(join6(dir, "package.json"))?.workspaces || fs.exists(join6(dir, "pnpm-workspace.yaml"));
1275
1769
  function findUp(dir, fs, test) {
1276
- for (let d = dir; ; d = dirname3(d)) {
1770
+ for (let d = dir; ; d = dirname4(d)) {
1277
1771
  if (test(d)) return d;
1278
- if (fs.exists(join4(d, ".git")) || dirname3(d) === d) return void 0;
1772
+ if (fs.exists(join6(d, ".git")) || dirname4(d) === d) return void 0;
1279
1773
  }
1280
1774
  }
1281
1775
  function workspacePatterns(root, fs) {
1282
- const ws = fs.readJson(join4(root, "package.json"))?.workspaces;
1776
+ const ws = fs.readJson(join6(root, "package.json"))?.workspaces;
1283
1777
  const fromPkg = Array.isArray(ws) ? ws : ws?.packages ?? [];
1284
- const yaml = fs.read(join4(root, "pnpm-workspace.yaml")) ?? "";
1778
+ const yaml = fs.read(join6(root, "pnpm-workspace.yaml")) ?? "";
1285
1779
  const block = yaml.match(/^packages:\s*\n((?:[ \t]*(?:-.*|#.*)?\n?)*)/m)?.[1] ?? "";
1286
1780
  const fromYaml = [...block.matchAll(/^[ \t]*-[ \t]*['"]?([^'"#\n]+?)['"]?[ \t]*$/gm)].map(
1287
1781
  (m) => m[1]
@@ -1292,40 +1786,40 @@ function workspacePatterns(root, fs) {
1292
1786
  }
1293
1787
  function expandWorkspaces(root, patterns, fs) {
1294
1788
  const found = /* @__PURE__ */ new Set();
1295
- const walk = (dir, segments, depth) => {
1789
+ const walk2 = (dir, segments, depth) => {
1296
1790
  if (!segments.length) {
1297
- if (fs.exists(join4(dir, "package.json"))) found.add(dir);
1791
+ if (fs.exists(join6(dir, "package.json"))) found.add(dir);
1298
1792
  return;
1299
1793
  }
1300
1794
  const [head, ...rest] = segments;
1301
1795
  const children = () => fs.list(dir).filter((e) => e.dir && e.name !== "node_modules" && !e.name.startsWith("."));
1302
1796
  if (head === "**") {
1303
- walk(dir, rest, depth);
1797
+ walk2(dir, rest, depth);
1304
1798
  if (depth < 4)
1305
- for (const c of children()) walk(join4(dir, c.name), segments, depth + 1);
1799
+ for (const c of children()) walk2(join6(dir, c.name), segments, depth + 1);
1306
1800
  } else if (head.includes("*")) {
1307
1801
  const re = new RegExp(
1308
1802
  `^${head.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`
1309
1803
  );
1310
1804
  for (const c of children())
1311
- if (re.test(c.name)) walk(join4(dir, c.name), rest, depth);
1805
+ if (re.test(c.name)) walk2(join6(dir, c.name), rest, depth);
1312
1806
  } else {
1313
- walk(join4(dir, head), rest, depth);
1807
+ walk2(join6(dir, head), rest, depth);
1314
1808
  }
1315
1809
  };
1316
1810
  for (const p of patterns)
1317
- walk(root, p.replace(/\/+$/, "").split("/").filter((s) => s && s !== "."), 0);
1811
+ walk2(root, p.replace(/\/+$/, "").split("/").filter((s) => s && s !== "."), 0);
1318
1812
  return [...found].sort();
1319
1813
  }
1320
- var firstExisting = (dir, fs, candidates) => candidates.find((c) => fs.exists(join4(dir, c)));
1814
+ var firstExisting = (dir, fs, candidates) => candidates.find((c) => fs.exists(join6(dir, c)));
1321
1815
  var withExts = (base) => SOURCE_EXTS.map((e) => `${base}.${e}`);
1322
1816
  function detect(dir, fs, env = {}) {
1323
- const pkg = fs.readJson(join4(dir, "package.json"));
1817
+ const pkg = fs.readJson(join6(dir, "package.json"));
1324
1818
  if (!pkg) return { error: `No package.json in ${dir}.` };
1325
1819
  const deps = depsOf(pkg);
1326
1820
  if (!deps["react-native"]) {
1327
1821
  if (isWorkspaceRoot(dir, fs)) {
1328
- const apps = expandWorkspaces(dir, workspacePatterns(dir, fs), fs).filter((d) => isApp(d, fs)).map((d) => relative(dir, d));
1822
+ const apps = expandWorkspaces(dir, workspacePatterns(dir, fs), fs).filter((d) => isApp(d, fs)).map((d) => relative3(dir, d));
1329
1823
  return {
1330
1824
  error: apps.length ? `${dir} is a monorepo root. Rerun with --dir pointing at the app: ${apps.map((a) => `--dir ${a}`).join(", ")}` : `${dir} is a monorepo root and no workspace package depends on react-native.`,
1331
1825
  workspaces: apps
@@ -1336,11 +1830,11 @@ function detect(dir, fs, env = {}) {
1336
1830
  };
1337
1831
  }
1338
1832
  const root = findUp(dir, fs, (d) => isWorkspaceRoot(d, fs)) ?? dir;
1339
- const repoRoot = findUp(dir, fs, (d) => fs.exists(join4(d, ".git"))) ?? root;
1833
+ const repoRoot = findUp(dir, fs, (d) => fs.exists(join6(d, ".git"))) ?? root;
1340
1834
  const has = Object.fromEntries(
1341
1835
  Object.entries(LIBRARIES).map(([k, dep]) => [k, !!deps[dep]])
1342
1836
  );
1343
- const appJson = fs.readJson(join4(dir, "app.json"));
1837
+ const appJson = fs.readJson(join6(dir, "app.json"));
1344
1838
  const appName = [appJson?.expo?.name, appJson?.name, pkg.name].find(
1345
1839
  (n) => typeof n === "string" && !!n
1346
1840
  ) ?? "app";
@@ -1350,15 +1844,15 @@ function detect(dir, fs, env = {}) {
1350
1844
  fs,
1351
1845
  has.expoRouter ? [...withExts("app/_layout"), ...withExts("src/app/_layout")] : [...withExts("App"), ...withExts("src/App")]
1352
1846
  );
1353
- const agentsFile = [.../* @__PURE__ */ new Set([dir, repoRoot])].flatMap((d) => [join4(d, "AGENTS.md"), join4(d, "CLAUDE.md")]).find((f) => fs.exists(f));
1847
+ const agentsFile = [.../* @__PURE__ */ new Set([dir, repoRoot])].flatMap((d) => [join6(d, "AGENTS.md"), join6(d, "CLAUDE.md")]).find((f) => fs.exists(f));
1354
1848
  let bin;
1355
- for (let d = dir; ; d = dirname3(d)) {
1356
- const candidate = join4(d, "node_modules", ".bin", "agent-bridge");
1849
+ for (let d = dir; ; d = dirname4(d)) {
1850
+ const candidate = join6(d, "node_modules", ".bin", "agent-bridge");
1357
1851
  if (fs.exists(candidate)) {
1358
1852
  bin = candidate;
1359
1853
  break;
1360
1854
  }
1361
- if (d === repoRoot || dirname3(d) === d) break;
1855
+ if (d === repoRoot || dirname4(d) === d) break;
1362
1856
  }
1363
1857
  return {
1364
1858
  dir,
@@ -1369,7 +1863,7 @@ function detect(dir, fs, env = {}) {
1369
1863
  has,
1370
1864
  installed: !!deps[PACKAGE],
1371
1865
  typescript: !!tsconfig || has.typescript,
1372
- srcDir: fs.exists(join4(dir, "src")),
1866
+ srcDir: fs.exists(join6(dir, "src")),
1373
1867
  rootFile,
1374
1868
  tsconfig,
1375
1869
  oxlint: firstExisting(dir, fs, [".oxlintrc.json"]),
@@ -1385,28 +1879,28 @@ function detect(dir, fs, env = {}) {
1385
1879
  }
1386
1880
 
1387
1881
  // src/init/plan.ts
1388
- import { dirname as dirname4, isAbsolute, join as join5, relative as relative2, sep } from "path";
1882
+ import { dirname as dirname5, isAbsolute, join as join7, relative as relative4, sep } from "path";
1389
1883
 
1390
1884
  // src/init/jsonc.ts
1391
- function strip(text) {
1885
+ function strip(text2) {
1392
1886
  let out = "";
1393
1887
  let i = 0;
1394
- while (i < text.length) {
1395
- const c = text[i];
1888
+ while (i < text2.length) {
1889
+ const c = text2[i];
1396
1890
  if (c === '"') {
1397
1891
  let j = i + 1;
1398
- while (j < text.length && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
1399
- out += text.slice(i, j + 1);
1892
+ while (j < text2.length && text2[j] !== '"') j += text2[j] === "\\" ? 2 : 1;
1893
+ out += text2.slice(i, j + 1);
1400
1894
  i = j + 1;
1401
- } else if (c === "/" && text[i + 1] === "/") {
1402
- const end = text.indexOf("\n", i);
1403
- const j = end === -1 ? text.length : end;
1895
+ } else if (c === "/" && text2[i + 1] === "/") {
1896
+ const end = text2.indexOf("\n", i);
1897
+ const j = end === -1 ? text2.length : end;
1404
1898
  out += " ".repeat(j - i);
1405
1899
  i = j;
1406
- } else if (c === "/" && text[i + 1] === "*") {
1407
- const end = text.indexOf("*/", i + 2);
1408
- const j = end === -1 ? text.length : end + 2;
1409
- out += text.slice(i, j).replace(/[^\n]/g, " ");
1900
+ } else if (c === "/" && text2[i + 1] === "*") {
1901
+ const end = text2.indexOf("*/", i + 2);
1902
+ const j = end === -1 ? text2.length : end + 2;
1903
+ out += text2.slice(i, j).replace(/[^\n]/g, " ");
1410
1904
  i = j;
1411
1905
  } else {
1412
1906
  out += c;
@@ -1415,37 +1909,37 @@ function strip(text) {
1415
1909
  }
1416
1910
  return out.replace(/,(\s*[}\]])/g, " $1");
1417
1911
  }
1418
- function parseJsonc(text) {
1912
+ function parseJsonc(text2) {
1419
1913
  try {
1420
- return JSON.parse(strip(text));
1914
+ return JSON.parse(strip(text2));
1421
1915
  } catch {
1422
1916
  return void 0;
1423
1917
  }
1424
1918
  }
1425
- function addToArray(text, key, value, initial = []) {
1426
- const json = parseJsonc(text);
1919
+ function addToArray(text2, key, value, initial = []) {
1920
+ const json = parseJsonc(text2);
1427
1921
  if (!json || typeof json !== "object" || Array.isArray(json)) return void 0;
1428
1922
  const current = json[key];
1429
- const clean = strip(text);
1923
+ const clean = strip(text2);
1430
1924
  const item = JSON.stringify(value);
1431
1925
  if (Array.isArray(current)) {
1432
1926
  const matches = [...clean.matchAll(new RegExp(`"${key}"\\s*:\\s*\\[`, "g"))];
1433
1927
  if (matches.length !== 1) return void 0;
1434
1928
  const at = matches[0].index + matches[0][0].length;
1435
- const after = text.slice(at);
1929
+ const after = text2.slice(at);
1436
1930
  const multiline = after.match(/^[ \t]*\n([ \t]*)/);
1437
- if (!current.length) return `${text.slice(0, at)}${item}${after}`;
1438
- if (multiline) return `${text.slice(0, at)}
1931
+ if (!current.length) return `${text2.slice(0, at)}${item}${after}`;
1932
+ if (multiline) return `${text2.slice(0, at)}
1439
1933
  ${multiline[1]}${item},${after}`;
1440
- return `${text.slice(0, at)}${item}, ${after}`;
1934
+ return `${text2.slice(0, at)}${item}, ${after}`;
1441
1935
  }
1442
1936
  if (current !== void 0) return void 0;
1443
1937
  const end = clean.slice(0, clean.lastIndexOf("}")).trimEnd().length;
1444
- const indent = text.match(/^([ \t]+)"/m)?.[1] ?? " ";
1938
+ const indent = text2.match(/^([ \t]+)"/m)?.[1] ?? " ";
1445
1939
  const comma = clean.slice(0, end).endsWith("{") ? "" : ",";
1446
1940
  const list2 = [...initial, value].map((v) => JSON.stringify(v)).join(", ");
1447
- return `${text.slice(0, end)}${comma}
1448
- ${indent}"${key}": [${list2}]${text.slice(end)}`;
1941
+ return `${text2.slice(0, end)}${comma}
1942
+ ${indent}"${key}": [${list2}]${text2.slice(end)}`;
1449
1943
  }
1450
1944
 
1451
1945
  // src/init/render.ts
@@ -1458,6 +1952,10 @@ var REALTIME = {
1458
1952
  };
1459
1953
  var STORES_WHY = "stores often hold tokens: add `redact` for those paths before wiring them";
1460
1954
  var MMKV_WHY = "MMKV often holds tokens and mmkv.get returns values as they are: wire only instances without secrets";
1955
+ var ASYNC_STORAGE_WHY = "AsyncStorage often holds tokens and asyncStorage.get returns values as they are: wire it only if it holds no secrets";
1956
+ var SECURE_STORE_WHY = "SecureStore holds secrets: secureStore.get hides values unless the agent asks for {full:true}, but secureStore.set can still overwrite them";
1957
+ var REDUX_WHY = "the Redux state often holds tokens: add `redact` for those slices and paths, and wrap the root reducer in bridgeReducer() (inside persistReducer) before wiring it";
1958
+ var JOTAI_WHY = "atoms often hold tokens: list only the atoms you want the agent to read and set, and add `redact` for secret ones";
1461
1959
  var REALTIME_WHY = "the adapter must wrap the client where it is created, before the app subscribes";
1462
1960
  var NETWORK_PROMPT = "Wire network tools? In dev builds they patch fetch and XMLHttpRequest so the agent can log and mock requests.";
1463
1961
  var NETWORK_WHY = "networkTools patches fetch and XMLHttpRequest in dev builds so the agent can log and mock requests (net.*); it also enables the offline scenario";
@@ -1495,6 +1993,13 @@ function renderBridgeFile(o) {
1495
1993
  }
1496
1994
  if (o.expoRouter) L.push(`import { router, useNavigationContainerRef } from 'expo-router'`);
1497
1995
  if (o.reactQuery) L.push(`import { useContext } from 'react'`);
1996
+ if (o.reactNavigation)
1997
+ L.push(
1998
+ "",
1999
+ "// Pass the ref you give to <NavigationContainer ref={\u2026}>, then uncomment navTools below.",
2000
+ `// import { navTools } from '@avasapp/agent-bridge/react-navigation'`,
2001
+ `// import { navigationRef } from './path-to-your-navigation-ref'`
2002
+ );
1498
2003
  if (o.realtimeTools?.length)
1499
2004
  L.push("", ...o.realtimeTools.map((t) => `import { ${t.name} } from '${t.from}'`));
1500
2005
  if (o.stores) {
@@ -1519,6 +2024,48 @@ function renderBridgeFile(o) {
1519
2024
  ...mmkv.map((m) => `// import { ${m.name} } from '${m.from}'`)
1520
2025
  );
1521
2026
  }
2027
+ if (o.asyncStorage)
2028
+ L.push(
2029
+ "",
2030
+ "// AsyncStorage is left out: it often holds tokens, and asyncStorage.get returns",
2031
+ "// values as they are. Uncomment these and asyncStorageTools below if it holds none,",
2032
+ "// passing the storage the app uses (a createAsyncStorage(name) instance, if it has one).",
2033
+ `// See ${DOCS}/adapters.md`,
2034
+ `// import { asyncStorageTools } from '@avasapp/agent-bridge/async-storage'`,
2035
+ `// import AsyncStorage from '@react-native-async-storage/async-storage'`
2036
+ );
2037
+ if (o.secureStore)
2038
+ L.push(
2039
+ "",
2040
+ "// SecureStore is left out: it holds secrets. secureStore.get hides values unless",
2041
+ "// the agent asks for {full:true}. Uncomment these and secureStoreTools below,",
2042
+ "// passing the keys the app uses so secureStore.list can show them, and the same",
2043
+ "// options the app passes to SecureStore (keychainService, requireAuthentication).",
2044
+ `// See ${DOCS}/adapters.md`,
2045
+ `// import { secureStoreTools } from '@avasapp/agent-bridge/secure-store'`,
2046
+ `// import * as SecureStore from 'expo-secure-store'`
2047
+ );
2048
+ if (o.redux)
2049
+ L.push(
2050
+ "",
2051
+ "// Redux is left out: the state often holds tokens. Add `redact` for those slices,",
2052
+ "// wrap the root reducer in bridgeReducer() (inside persistReducer) where the store",
2053
+ "// is created, then uncomment these and reduxTools below.",
2054
+ `// See ${DOCS}/adapters.md`,
2055
+ `// import { reduxTools } from '@avasapp/agent-bridge/redux'`,
2056
+ `// import { store } from '../store'`
2057
+ );
2058
+ if (o.jotai)
2059
+ L.push(
2060
+ "",
2061
+ "// Jotai is left out: atoms often hold tokens. List the atoms the agent may read and",
2062
+ "// set, add `redact` for secret ones, then uncomment these and jotaiTools below.",
2063
+ "// Use the store your app uses: a <Provider> without a store prop has its own, out of reach here.",
2064
+ `// See ${DOCS}/adapters.md`,
2065
+ `// import { jotaiTools } from '@avasapp/agent-bridge/jotai'`,
2066
+ `// import { getDefaultStore } from 'jotai'`,
2067
+ `// import { userAtom } from '../atoms'`
2068
+ );
1522
2069
  for (const todo of o.realtime) {
1523
2070
  const r = REALTIME[todo.kind];
1524
2071
  L.push(
@@ -1591,16 +2138,17 @@ function renderBridgeFile(o) {
1591
2138
  const mmkv = o.mmkv.length ? o.mmkv : [{ name: "storage", from: "" }];
1592
2139
  L.push(` // ...mmkvTools({ ${mmkv.map((m) => m.name).join(", ")} }),`);
1593
2140
  }
2141
+ if (o.asyncStorage) L.push(" // ...asyncStorageTools(AsyncStorage),");
2142
+ if (o.secureStore) L.push(` // ...secureStoreTools(SecureStore, ['authToken']),`);
2143
+ if (o.redux) L.push(` // ...reduxTools(store, { redact: { auth: ['token'] } }),`);
2144
+ if (o.jotai) L.push(" // ...jotaiTools(getDefaultStore(), { user: userAtom }),");
1594
2145
  if (o.realtime.length) L.push(" // ...realtimeDevTools,");
1595
- if (o.reactNavigation) {
1596
- const params = o.ts ? "name: string, params?: object" : "name, params";
2146
+ if (o.reactNavigation)
1597
2147
  L.push(
1598
- " // React Navigation has no adapter. With your container ref",
1599
- " // (createNavigationContainerRef from @react-navigation/native), tools like these work:",
1600
- ` // 'nav.navigate': (${params}) => navigationRef.navigate(name, params),`,
1601
- ` // 'nav.current': () => navigationRef.getCurrentRoute(),`
2148
+ " // React Navigation: uncomment this and the imports above, with the ref you pass to",
2149
+ " // <NavigationContainer ref={\u2026}> (createNavigationContainerRef from @react-navigation/native).",
2150
+ " // ...navTools(navigationRef),"
1602
2151
  );
1603
- }
1604
2152
  L.push(
1605
2153
  ` // Your own tools: any function, JSON in and out, e.g. 'cart.clear': () => clearCart(),`,
1606
2154
  " },",
@@ -1621,12 +2169,11 @@ function renderSmokeFlow(o) {
1621
2169
  `// Generated by agent-bridge init ${o.version}. Run it with:`,
1622
2170
  `// ${o.bin} run flows/smoke.flow.mjs`,
1623
2171
  `/** @type {import('@avasapp/agent-bridge/client').FlowModule['default']} */`,
1624
- "const smoke = async ({ step }) => {",
2172
+ "const smoke = async ({ step, expect }) => {",
1625
2173
  ` await step('snapshot', 'screen.snapshot')`,
1626
2174
  ...o.expoRouter ? [` await step('open home', 'router.navigate', '/')`] : [],
1627
- " // TODO: wait for text your first screen always shows, for example:",
1628
- ` // await step('home shown', 'screen.waitFor', 'Welcome')`,
1629
- ` await step('undo', 'bridge.restore')`,
2175
+ " // TODO: check text your first screen always shows, for example:",
2176
+ ` // await expect.text('Welcome')`,
1630
2177
  "}",
1631
2178
  "",
1632
2179
  "export default smoke",
@@ -1648,32 +2195,32 @@ function stamp(content) {
1648
2195
  const nl = content.indexOf("\n");
1649
2196
  return content.slice(0, nl).replace(HEADER, `$1 (#${hash(content.slice(nl))}).`) + content.slice(nl);
1650
2197
  }
1651
- function isPristine(text) {
1652
- const nl = text.indexOf("\n");
1653
- const recorded = text.slice(0, nl).match(HEADER)?.[3];
1654
- return !!recorded && recorded === hash(text.slice(nl));
2198
+ function isPristine(text2) {
2199
+ const nl = text2.indexOf("\n");
2200
+ const recorded = text2.slice(0, nl).match(HEADER)?.[3];
2201
+ return !!recorded && recorded === hash(text2.slice(nl));
1655
2202
  }
1656
2203
  var NETWORK_LINES = [
1657
2204
  `import { networkTools } from '@avasapp/agent-bridge/network'`,
1658
2205
  "...networkTools(), // in useAgentBridge({ tools })"
1659
2206
  ];
1660
- var indentOf = (text) => text.match(/^([ \t]+)"/m)?.[1] ?? " ";
1661
- function patchJson(text, edit, manual) {
2207
+ var indentOf = (text2) => text2.match(/^([ \t]+)"/m)?.[1] ?? " ";
2208
+ function patchJson(text2, edit, manual) {
1662
2209
  let json;
1663
2210
  try {
1664
- json = JSON.parse(text);
2211
+ json = JSON.parse(text2);
1665
2212
  } catch {
1666
2213
  return { manual };
1667
2214
  }
1668
2215
  if (!json || typeof json !== "object" || Array.isArray(json)) return { manual };
1669
2216
  if (!edit(json)) return { unchanged: true };
1670
2217
  return {
1671
- text: JSON.stringify(json, null, indentOf(text)) + (text.endsWith("\n") ? "\n" : "")
2218
+ text: JSON.stringify(json, null, indentOf(text2)) + (text2.endsWith("\n") ? "\n" : "")
1672
2219
  };
1673
2220
  }
1674
- function patchPackageScripts(text) {
2221
+ function patchPackageScripts(text2) {
1675
2222
  return patchJson(
1676
- text,
2223
+ text2,
1677
2224
  (pkg) => {
1678
2225
  if (pkg.scripts?.flow !== void 0) return false;
1679
2226
  pkg.scripts = { ...pkg.scripts, flow: "agent-bridge run" };
@@ -1690,9 +2237,9 @@ function includesFlows(pattern) {
1690
2237
  const glob = last.includes(".") ? pattern : `${pattern.replace(/\/+$/, "")}${last === "*" ? "" : "/**/*"}`;
1691
2238
  return globRe(glob).test("flows/smoke.flow.mjs");
1692
2239
  }
1693
- function patchTsconfig(text, baseAllowJs) {
2240
+ function patchTsconfig(text2, baseAllowJs) {
1694
2241
  const manual = `Add "${FLOW_GLOB}" to "exclude" in tsconfig.json`;
1695
- const json = parseJsonc(text);
2242
+ const json = parseJsonc(text2);
1696
2243
  if (!json || typeof json !== "object") return { manual };
1697
2244
  if (Array.isArray(json.exclude) && json.exclude.includes(FLOW_GLOB)) return { unchanged: true };
1698
2245
  const allowJs = json.compilerOptions?.allowJs ?? baseAllowJs ?? true;
@@ -1703,25 +2250,25 @@ function patchTsconfig(text, baseAllowJs) {
1703
2250
  return {
1704
2251
  manual: `${manual}, along with what it inherits from ${JSON.stringify(json.extends)}: an "exclude" here replaces the inherited one`
1705
2252
  };
1706
- const patched = addToArray(text, "exclude", FLOW_GLOB, ["node_modules"]);
2253
+ const patched = addToArray(text2, "exclude", FLOW_GLOB, ["node_modules"]);
1707
2254
  return patched ? { text: patched } : { manual };
1708
2255
  }
1709
- function patchOxlint(text) {
1710
- const ignore = parseJsonc(text)?.ignorePatterns;
2256
+ function patchOxlint(text2) {
2257
+ const ignore = parseJsonc(text2)?.ignorePatterns;
1711
2258
  if (Array.isArray(ignore) && ignore.includes(FLOW_GLOB)) return { unchanged: true };
1712
- const patched = addToArray(text, "ignorePatterns", FLOW_GLOB);
2259
+ const patched = addToArray(text2, "ignorePatterns", FLOW_GLOB);
1713
2260
  return patched ? { text: patched } : { manual: `Add "${FLOW_GLOB}" to "ignorePatterns" in .oxlintrc.json` };
1714
2261
  }
1715
2262
  var ESLINT_SNIPPET = `{ ignores: ['${FLOW_GLOB}'] },`;
1716
- function patchEslint(text) {
1717
- if (text.includes(FLOW_GLOB)) return { unchanged: true };
2263
+ function patchEslint(text2) {
2264
+ if (text2.includes(FLOW_GLOB)) return { unchanged: true };
1718
2265
  const manual = `Add to the start of your ESLint flat config array: ${ESLINT_SNIPPET}`;
1719
2266
  const re = /^((?:module\.exports\s*=|export\s+default)\s*(?:defineConfig\(\s*)?\[)/gm;
1720
- const matches = [...text.matchAll(re)];
2267
+ const matches = [...text2.matchAll(re)];
1721
2268
  if (matches.length !== 1) return { manual };
1722
2269
  const at = matches[0].index + matches[0][1].length;
1723
- return { text: `${text.slice(0, at)}
1724
- ${ESLINT_SNIPPET}${text.slice(at)}` };
2270
+ return { text: `${text2.slice(0, at)}
2271
+ ${ESLINT_SNIPPET}${text2.slice(at)}` };
1725
2272
  }
1726
2273
 
1727
2274
  // src/init/plan.ts
@@ -1737,27 +2284,32 @@ var MARKERS = [
1737
2284
  ["reactQuery", /queryTools/, "@tanstack/react-query"],
1738
2285
  ["webview", /webviewTools/, "react-native-webview"],
1739
2286
  ["zustand", /storeTools/, "zustand"],
2287
+ ["redux", /reduxTools/, "@reduxjs/toolkit"],
2288
+ ["reduxCore", /reduxTools/, "redux"],
2289
+ ["jotai", /jotaiTools/, "jotai"],
1740
2290
  ["mmkv", /mmkvTools/, "react-native-mmkv"],
2291
+ ["asyncStorage", /asyncStorageTools/, "@react-native-async-storage/async-storage"],
2292
+ ["secureStore", /secureStoreTools/, "expo-secure-store"],
1741
2293
  ["socketIo", /realtimeDevTools|socketIoTools|realtime/i, "socket.io-client"],
1742
2294
  ["ably", /realtimeDevTools|ablyTools|realtime/i, "ably"]
1743
2295
  ];
1744
- var posix = (p) => p.split(sep).join("/");
2296
+ var posix2 = (p) => p.split(sep).join("/");
1745
2297
  function importPath(fromFile, toFile) {
1746
- const rel = posix(relative2(dirname4(fromFile), toFile)).replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2298
+ const rel = posix2(relative4(dirname5(fromFile), toFile)).replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
1747
2299
  return rel.startsWith(".") ? rel : `./${rel}`;
1748
2300
  }
1749
2301
  function binFor(d) {
1750
2302
  if (d.bin) {
1751
- const rel = posix(relative2(d.dir, d.bin));
2303
+ const rel = posix2(relative4(d.dir, d.bin));
1752
2304
  return rel.startsWith(".") ? rel : `./${rel}`;
1753
2305
  }
1754
2306
  if (d.pm === "npm" && d.root !== d.dir)
1755
- return `${posix(relative2(d.dir, d.root))}/node_modules/.bin/agent-bridge`;
2307
+ return `${posix2(relative4(d.dir, d.root))}/node_modules/.bin/agent-bridge`;
1756
2308
  return binFallback(d.pm);
1757
2309
  }
1758
2310
  function bridgePath(d, out) {
1759
- if (out) return isAbsolute(out) ? out : join5(d.dir, out);
1760
- return join5(d.dir, d.srcDir ? "src/dev" : "dev", `agent-bridge.${d.typescript ? "tsx" : "js"}`);
2311
+ if (out) return isAbsolute(out) ? out : join7(d.dir, out);
2312
+ return join7(d.dir, d.srcDir ? "src/dev" : "dev", `agent-bridge.${d.typescript ? "tsx" : "js"}`);
1761
2313
  }
1762
2314
  function nextSteps(bin, o) {
1763
2315
  const { offline, runFrom } = o;
@@ -1784,13 +2336,13 @@ function nextSteps(bin, o) {
1784
2336
  var stripExt = (p) => p.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "").replace(/\/index$/, "");
1785
2337
  function readTsconfig(d, fs) {
1786
2338
  if (!d.tsconfig) return { aliases: [] };
1787
- const file = join5(d.dir, d.tsconfig);
2339
+ const file = join7(d.dir, d.tsconfig);
1788
2340
  const json = parseJsonc(fs.read(file) ?? "");
1789
2341
  if (!json) return { aliases: [] };
1790
2342
  const findExtended = (from2, ext2) => {
1791
- const names = ext2.endsWith(".json") ? [ext2] : [ext2, `${ext2}.json`];
1792
- const roots = ext2.startsWith(".") ? [dirname4(from2)] : ancestors(dirname4(from2), d.repoRoot).map((a) => join5(a, "node_modules"));
1793
- for (const r of roots) for (const n of names) if (fs.exists(join5(r, n)) && !fs.list(join5(r, n)).length) return join5(r, n);
2343
+ const names2 = ext2.endsWith(".json") ? [ext2] : [ext2, `${ext2}.json`];
2344
+ const roots = ext2.startsWith(".") ? [dirname5(from2)] : ancestors(dirname5(from2), d.repoRoot).map((a) => join7(a, "node_modules"));
2345
+ for (const r of roots) for (const n of names2) if (fs.exists(join7(r, n)) && !fs.list(join7(r, n)).length) return join7(r, n);
1794
2346
  return void 0;
1795
2347
  };
1796
2348
  let baseAllowJs;
@@ -1807,30 +2359,30 @@ function readTsconfig(d, fs) {
1807
2359
  ext = base.extends;
1808
2360
  from = path;
1809
2361
  }
1810
- const baseDir = join5(d.dir, json.compilerOptions?.baseUrl ?? ".");
2362
+ const baseDir = join7(d.dir, json.compilerOptions?.baseUrl ?? ".");
1811
2363
  const paths = json.compilerOptions?.paths ?? {};
1812
2364
  const aliases = Object.entries(paths).flatMap(
1813
- ([pattern, targets]) => Array.isArray(targets) && typeof targets[0] === "string" ? [[pattern.replace(/\*$/, ""), join5(baseDir, targets[0].replace(/\*$/, ""))]] : []
2365
+ ([pattern, targets]) => Array.isArray(targets) && typeof targets[0] === "string" ? [[pattern.replace(/\*$/, ""), join7(baseDir, targets[0].replace(/\*$/, ""))]] : []
1814
2366
  );
1815
2367
  return { json, baseAllowJs, aliases };
1816
2368
  }
1817
2369
  function ancestors(dir, stop2) {
1818
2370
  const out = [];
1819
- for (let d = dir; ; d = dirname4(d)) {
2371
+ for (let d = dir; ; d = dirname5(d)) {
1820
2372
  out.push(d);
1821
- if (d === stop2 || dirname4(d) === d) return out;
2373
+ if (d === stop2 || dirname5(d) === d) return out;
1822
2374
  }
1823
2375
  }
1824
- function importedFiles(file, text, aliases) {
1825
- const specs = [...text.matchAll(/(?:from\s+|import\s+|require\(\s*)['"]([^'"]+)['"]/g)].map((m) => m[1]);
2376
+ function importedFiles(file, text2, aliases) {
2377
+ const specs = [...text2.matchAll(/(?:from\s+|import\s+|require\(\s*)['"]([^'"]+)['"]/g)].map((m) => m[1]);
1826
2378
  return specs.flatMap((spec) => {
1827
- if (spec.startsWith(".")) return [stripExt(join5(dirname4(file), spec))];
2379
+ if (spec.startsWith(".")) return [stripExt(join7(dirname5(file), spec))];
1828
2380
  const alias = aliases.find(([prefix]) => prefix && spec.startsWith(prefix));
1829
- return alias ? [stripExt(join5(alias[1], spec.slice(alias[0].length)))] : [];
2381
+ return alias ? [stripExt(join7(alias[1], spec.slice(alias[0].length)))] : [];
1830
2382
  });
1831
2383
  }
1832
2384
  function plan(d, found, fs, o, cwd) {
1833
- const rel = (p) => posix(relative2(d.dir, p));
2385
+ const rel = (p) => posix2(relative4(d.dir, p));
1834
2386
  const out = bridgePath(d, o.out);
1835
2387
  if (rel(out).startsWith("..") || isAbsolute(rel(out)))
1836
2388
  return { error: `--out ${o.out} is outside the app (${d.dir}).` };
@@ -1840,7 +2392,7 @@ function plan(d, found, fs, o, cwd) {
1840
2392
  };
1841
2393
  const ts = /\.tsx?$/.test(out);
1842
2394
  const bin = binFor(d);
1843
- const runFrom = posix(relative2(cwd, d.dir)) || ".";
2395
+ const runFrom = posix2(relative4(cwd, d.dir)) || ".";
1844
2396
  const tsconfig = readTsconfig(d, fs);
1845
2397
  const actions = [];
1846
2398
  const files = [];
@@ -1877,14 +2429,14 @@ function plan(d, found, fs, o, cwd) {
1877
2429
  files.push({ path: `${rel(path)}.new`, status: "written" });
1878
2430
  return "edited";
1879
2431
  };
1880
- const imported = (list2) => list2.map((f) => ({ name: f.name, from: importPath(out, join5(d.dir, f.file)) }));
2432
+ const imported = (list2) => list2.map((f) => ({ name: f.name, from: importPath(out, join7(d.dir, f.file)) }));
1881
2433
  const realtimeTools = imported(found.realtimeTools);
1882
2434
  const realtime = realtimeTools.length ? [] : [
1883
2435
  ...d.has.socketIo ? found.socketIo.length ? found.socketIo.map((f) => ({ kind: "socket.io", ...f })) : [{ kind: "socket.io" }] : [],
1884
2436
  ...d.has.ably ? found.ably.length ? found.ably.map((f) => ({ kind: "ably", ...f })) : [{ kind: "ably" }] : []
1885
2437
  ];
1886
2438
  const existingText = fs.read(out);
1887
- const existing = found.bridgeUsers.filter((f) => stripExt(join5(d.dir, f)) !== stripExt(out));
2439
+ const existing = found.bridgeUsers.filter((f) => stripExt(join7(d.dir, f)) !== stripExt(out));
1888
2440
  const elsewhere = existingText === void 0 && existing.length > 0 && !o.out;
1889
2441
  let state = "elsewhere";
1890
2442
  if (elsewhere) {
@@ -1908,13 +2460,17 @@ function plan(d, found, fs, o, cwd) {
1908
2460
  reactNavigation: d.has.reactNavigation && !d.has.expoRouter,
1909
2461
  stores: d.has.zustand ? imported(found.stores) : void 0,
1910
2462
  mmkv: d.has.mmkv ? imported(found.mmkv) : void 0,
2463
+ asyncStorage: d.has.asyncStorage,
2464
+ secureStore: d.has.secureStore,
2465
+ redux: d.has.redux || d.has.reduxCore,
2466
+ jotai: d.has.jotai,
1911
2467
  realtime,
1912
2468
  realtimeTools
1913
2469
  })
1914
2470
  );
1915
2471
  }
1916
2472
  const wrote = state === "written";
1917
- const liveText = wrote ? void 0 : elsewhere ? existing.map((f) => fs.read(join5(d.dir, f)) ?? "").join("\n") : existingText;
2473
+ const liveText = wrote ? void 0 : elsewhere ? existing.map((f) => fs.read(join7(d.dir, f)) ?? "").join("\n") : existingText;
1918
2474
  const network = wrote ? o.network.enabled : /networkTools\s*\(/.test(liveText ?? "");
1919
2475
  const offline = wrote ? o.network.enabled : /\boffline\s*:/.test(liveText ?? "");
1920
2476
  if (wrote) {
@@ -1930,21 +2486,45 @@ function plan(d, found, fs, o, cwd) {
1930
2486
  items: found.mmkv.map((s) => `${s.name} (${s.file})`),
1931
2487
  why: `${MMKV_WHY} (${DOCS}/adapters.md)`
1932
2488
  });
2489
+ if (d.has.asyncStorage)
2490
+ commented.push({
2491
+ adapter: "async-storage",
2492
+ items: ["AsyncStorage"],
2493
+ why: `${ASYNC_STORAGE_WHY} (${DOCS}/adapters.md)`
2494
+ });
2495
+ if (d.has.secureStore)
2496
+ commented.push({
2497
+ adapter: "secure-store",
2498
+ items: ["SecureStore"],
2499
+ why: `${SECURE_STORE_WHY} (${DOCS}/adapters.md)`
2500
+ });
2501
+ if (d.has.redux || d.has.reduxCore)
2502
+ commented.push({
2503
+ adapter: "redux",
2504
+ items: ["reduxTools(store)"],
2505
+ why: `${REDUX_WHY} (${DOCS}/adapters.md)`
2506
+ });
2507
+ if (d.has.jotai)
2508
+ commented.push({
2509
+ adapter: "jotai",
2510
+ items: ["jotaiTools(store, atoms)"],
2511
+ why: `${JOTAI_WHY} (${DOCS}/adapters.md)`
2512
+ });
1933
2513
  if (d.has.reactNavigation && !d.has.expoRouter)
1934
2514
  commented.push({
1935
2515
  adapter: "@react-navigation/native",
1936
- items: ["nav.navigate", "nav.current"],
1937
- why: "there is no React Navigation adapter; the sample tools need your container ref"
2516
+ items: ["navTools(navigationRef)"],
2517
+ why: `navTools needs the container ref you pass to <NavigationContainer> (${DOCS}/adapters.md)`
1938
2518
  });
1939
2519
  for (const t of realtime) todos.push(`${realtimeTodoText(t)}: ${REALTIME_WHY} (${DOCS}/realtime.md).`);
1940
2520
  }
1941
2521
  const missing = !wrote && liveText !== void 0 ? MARKERS.filter(([lib, marker]) => d.has[lib] && !marker.test(liveText)).map(([, , dep]) => dep) : [];
1942
- const flows = join5(d.dir, "flows");
2522
+ const flows = join7(d.dir, "flows");
1943
2523
  generate(
1944
- join5(flows, "smoke.flow.mjs"),
2524
+ join7(flows, "smoke.flow.mjs"),
1945
2525
  renderSmokeFlow({ version: o.version, expoRouter: d.has.expoRouter, bin })
1946
2526
  );
1947
- const readme = join5(flows, "README.md");
2527
+ const readme = join7(flows, "README.md");
1948
2528
  if (fs.exists(readme)) files.push({ path: rel(readme), status: "skipped", note: "exists" });
1949
2529
  else {
1950
2530
  actions.push({ kind: "write", path: readme, content: renderFlowsReadme(bin) });
@@ -1952,7 +2532,7 @@ function plan(d, found, fs, o, cwd) {
1952
2532
  }
1953
2533
  const patch = (path, fn) => {
1954
2534
  if (!path) return;
1955
- const abs = join5(d.dir, path);
2535
+ const abs = join7(d.dir, path);
1956
2536
  const result = fn(fs.read(abs) ?? "");
1957
2537
  if ("text" in result) {
1958
2538
  actions.push({ kind: "patch", path: abs, patch: fn });
@@ -1964,15 +2544,15 @@ function plan(d, found, fs, o, cwd) {
1964
2544
  }
1965
2545
  };
1966
2546
  patch("package.json", patchPackageScripts);
1967
- patch(d.tsconfig, (text) => patchTsconfig(text, tsconfig.baseAllowJs));
2547
+ patch(d.tsconfig, (text2) => patchTsconfig(text2, tsconfig.baseAllowJs));
1968
2548
  patch(d.oxlint, patchOxlint);
1969
2549
  patch(d.eslint, patchEslint);
1970
2550
  if (d.agentsFile) {
1971
- const text = fs.read(d.agentsFile) ?? "";
1972
- const path = posix(relative2(d.dir, d.agentsFile));
1973
- if (text.includes(".flow.mjs")) files.push({ path, status: "unchanged" });
2551
+ const text2 = fs.read(d.agentsFile) ?? "";
2552
+ const path = posix2(relative4(d.dir, d.agentsFile));
2553
+ if (text2.includes(".flow.mjs")) files.push({ path, status: "unchanged" });
1974
2554
  else {
1975
- const appDir = posix(relative2(dirname4(d.agentsFile), d.dir));
2555
+ const appDir = posix2(relative4(dirname5(d.agentsFile), d.dir));
1976
2556
  actions.push({ kind: "append", path: d.agentsFile, line: agentsLine(appDir, bin) });
1977
2557
  files.push({ path, status: "appended" });
1978
2558
  }
@@ -1982,11 +2562,11 @@ function plan(d, found, fs, o, cwd) {
1982
2562
  if (!d.installed && o.install) actions.unshift({ kind: "install", command: install, cwd: d.dir });
1983
2563
  const skill = o.skillYes ? [...SKILL_RUN, "-y"] : SKILL_RUN;
1984
2564
  if (o.skill) actions.push({ kind: "skill", command: skill, cwd: d.repoRoot });
1985
- const rootFile = d.rootFile ? join5(d.dir, d.rootFile) : void 0;
2565
+ const rootFile = d.rootFile ? join7(d.dir, d.rootFile) : void 0;
1986
2566
  const rootImports = rootFile ? importedFiles(rootFile, fs.read(rootFile) ?? "", tsconfig.aliases) : [];
1987
2567
  const reachedFromRoot = (file) => stripExt(file) === stripExt(rootFile ?? "") || rootImports.includes(stripExt(file));
1988
- const mounted = elsewhere ? existing.some((f) => reachedFromRoot(join5(d.dir, f))) : rootImports.includes(stripExt(out));
1989
- const snippet = renderMountSnippet(importPath(rootFile ?? join5(d.dir, "App.tsx"), out));
2568
+ const mounted = elsewhere ? existing.some((f) => reachedFromRoot(join7(d.dir, f))) : rootImports.includes(stripExt(out));
2569
+ const snippet = renderMountSnippet(importPath(rootFile ?? join7(d.dir, "App.tsx"), out));
1990
2570
  const where = elsewhere ? `The app already uses the bridge in ${existing.join(", ")}; make sure the root component renders it` : d.rootFile ? `Paste the import into ${d.rootFile} and the element into the root component's JSX` : `No root component found (looked for ${d.has.expoRouter ? "app/_layout or src/app/_layout" : "App or src/App"}): paste it into yours, fixing the import path`;
1991
2571
  const note = `${where}${d.has.reactQuery && !elsewhere ? ", inside QueryClientProvider so query.* tools work" : ""}.`;
1992
2572
  const regenerable = state === "written" || state === "kept";
@@ -2028,12 +2608,12 @@ function plan(d, found, fs, o, cwd) {
2028
2608
  }
2029
2609
 
2030
2610
  // src/init/scan.ts
2031
- import { join as join6, relative as relative3 } from "path";
2611
+ import { join as join8, relative as relative5 } from "path";
2032
2612
  var CODE = /\.(tsx?|jsx?|mjs|cjs)$/;
2033
2613
  var TESTS = /\.(test|spec)\.|(^|\/)__tests__\//;
2034
- var imports = (text, module) => new RegExp(
2614
+ var imports = (text2, module) => new RegExp(
2035
2615
  `(from\\s+|require\\(\\s*)['"]${module.replace(/[.]/g, "\\.")}(/[\\w./-]+)?['"]`
2036
- ).test(text);
2616
+ ).test(text2);
2037
2617
  var TYPED = "(?:\\s*:[^=]+)?";
2038
2618
  var PATTERNS = [
2039
2619
  ["stores", "zustand", new RegExp(`export\\s+const\\s+(use\\w+)${TYPED}\\s*=\\s*create\\w*\\s*[<(]`, "g")],
@@ -2044,31 +2624,31 @@ var PATTERNS = [
2044
2624
  ];
2045
2625
  function scan(dir, fs, { maxFiles = 2e3, maxBytes = 512 * 1024 } = {}) {
2046
2626
  const files = [];
2047
- const walk = (d, deep) => {
2627
+ const walk2 = (d, deep) => {
2048
2628
  const entries = fs.list(d).sort((a, b) => a.name < b.name ? -1 : 1);
2049
2629
  for (const e of entries) {
2050
2630
  if (files.length >= maxFiles) return;
2051
2631
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
2052
- const path = join6(d, e.name);
2632
+ const path = join8(d, e.name);
2053
2633
  if (e.dir) {
2054
- if (deep) walk(path, true);
2055
- } else if (CODE.test(e.name) && !TESTS.test(relative3(dir, path))) {
2634
+ if (deep) walk2(path, true);
2635
+ } else if (CODE.test(e.name) && !TESTS.test(relative5(dir, path))) {
2056
2636
  files.push(path);
2057
2637
  }
2058
2638
  }
2059
2639
  };
2060
- walk(dir, false);
2061
- for (const sub of ["src", "app", "lib"]) walk(join6(dir, sub), true);
2640
+ walk2(dir, false);
2641
+ for (const sub of ["src", "app", "lib"]) walk2(join8(dir, sub), true);
2062
2642
  const result = { stores: [], mmkv: [], socketIo: [], ably: [], realtimeTools: [], bridgeUsers: [] };
2063
2643
  for (const path of files) {
2064
- const text = fs.read(path);
2065
- if (!text || text.length > maxBytes) continue;
2066
- if (/\buseAgentBridge\s*\(|(from\s+|require\(\s*)['"]@avasapp\/agent-bridge['"]/.test(text))
2067
- result.bridgeUsers.push(relative3(dir, path));
2644
+ const text2 = fs.read(path);
2645
+ if (!text2 || text2.length > maxBytes) continue;
2646
+ if (/\buseAgentBridge\s*\(|(from\s+|require\(\s*)['"]@avasapp\/agent-bridge['"]/.test(text2))
2647
+ result.bridgeUsers.push(relative5(dir, path));
2068
2648
  for (const [key, module, re] of PATTERNS) {
2069
- if (!imports(text, module)) continue;
2070
- for (const m of text.matchAll(re))
2071
- result[key].push({ name: m[1], file: relative3(dir, path) });
2649
+ if (!imports(text2, module)) continue;
2650
+ for (const m of text2.matchAll(re))
2651
+ result[key].push({ name: m[1], file: relative5(dir, path) });
2072
2652
  }
2073
2653
  }
2074
2654
  return result;
@@ -2077,7 +2657,7 @@ function scan(dir, fs, { maxFiles = 2e3, maxBytes = 512 * 1024 } = {}) {
2077
2657
  // src/init/index.ts
2078
2658
  var INSTALL_TIMEOUT_MS = 5 * 6e4;
2079
2659
  var SKILL_TIMEOUT_MS = 2 * 6e4;
2080
- var posix2 = (p) => p.split(sep2).join("/");
2660
+ var posix3 = (p) => p.split(sep2).join("/");
2081
2661
  var nodeFs = {
2082
2662
  exists: existsSync2,
2083
2663
  read: (path) => {
@@ -2109,10 +2689,10 @@ function nodeIo() {
2109
2689
  return {
2110
2690
  fs: nodeFs,
2111
2691
  write: (path, content) => {
2112
- mkdirSync2(dirname5(path), { recursive: true });
2692
+ mkdirSync2(dirname6(path), { recursive: true });
2113
2693
  writeFileSync2(path, content);
2114
2694
  },
2115
- append: (path, text) => appendFileSync3(path, text),
2695
+ append: (path, text2) => appendFileSync3(path, text2),
2116
2696
  exec: (command, cwd, interactive, timeoutMs) => new Promise((done) => {
2117
2697
  const [cmd, ...args] = command;
2118
2698
  const child = spawn2(cmd, args, {
@@ -2141,9 +2721,9 @@ function nodeIo() {
2141
2721
  rl.close();
2142
2722
  }
2143
2723
  },
2144
- print: (text) => process.stdout.write(`${text}
2724
+ print: (text2) => process.stdout.write(`${text2}
2145
2725
  `),
2146
- warn: (text) => process.stderr.write(`${text}
2726
+ warn: (text2) => process.stderr.write(`${text2}
2147
2727
  `),
2148
2728
  interactive: !!process.stdin.isTTY && !!process.stdout.isTTY && !/^(1|true)$/i.test(process.env.CI ?? ""),
2149
2729
  cwd: process.cwd(),
@@ -2182,8 +2762,8 @@ async function apply(actions, report, io, interactive) {
2182
2762
  break;
2183
2763
  }
2184
2764
  case "append": {
2185
- const text = io.fs.read(a.path) ?? "";
2186
- io.append(a.path, `${text.endsWith("\n") || !text ? "" : "\n"}${a.line}
2765
+ const text2 = io.fs.read(a.path) ?? "";
2766
+ io.append(a.path, `${text2.endsWith("\n") || !text2 ? "" : "\n"}${a.line}
2187
2767
  `);
2188
2768
  break;
2189
2769
  }
@@ -2191,7 +2771,7 @@ async function apply(actions, report, io, interactive) {
2191
2771
  }
2192
2772
  }
2193
2773
  var describe = (a, dir) => {
2194
- const rel = (p) => relative4(dir, p) || ".";
2774
+ const rel = (p) => relative6(dir, p) || ".";
2195
2775
  switch (a.kind) {
2196
2776
  case "install":
2197
2777
  return `install ${a.command.join(" ")}`;
@@ -2217,7 +2797,7 @@ function formatReport(r, actions) {
2217
2797
  L.push("Would run:", ...actions.map((a) => ` ${describe(a, r.app.dir)}`), "");
2218
2798
  for (const a of actions)
2219
2799
  if (a.kind === "write")
2220
- L.push(`--- ${relative4(r.app.dir, a.path)}`, a.content.trimEnd(), "");
2800
+ L.push(`--- ${relative6(r.app.dir, a.path)}`, a.content.trimEnd(), "");
2221
2801
  }
2222
2802
  L.push("Files");
2223
2803
  for (const f of r.files)
@@ -2263,7 +2843,7 @@ function formatReport(r, actions) {
2263
2843
  return L.join("\n");
2264
2844
  }
2265
2845
  async function runInit(args, io) {
2266
- const dir = resolve(io.cwd, args.dir ?? ".");
2846
+ const dir = resolve2(io.cwd, args.dir ?? ".");
2267
2847
  const fail = (error) => {
2268
2848
  if (args.json) io.print(JSON.stringify({ ok: false, ...error }, null, 2));
2269
2849
  else io.warn(error.error);
@@ -2274,7 +2854,7 @@ async function runInit(args, io) {
2274
2854
  const interactive = io.interactive && !args.json && !args.yes;
2275
2855
  const options = (network2) => ({
2276
2856
  version,
2277
- out: args.out === void 0 ? void 0 : resolve(io.cwd, args.out),
2857
+ out: args.out === void 0 ? void 0 : resolve2(io.cwd, args.out),
2278
2858
  force: args.force,
2279
2859
  install: args.install !== false,
2280
2860
  skill: args.skill !== false,
@@ -2290,13 +2870,13 @@ async function runInit(args, io) {
2290
2870
  report2.dryRun = true;
2291
2871
  if (args.json) {
2292
2872
  const contents = Object.fromEntries(
2293
- actions2.flatMap((a) => a.kind === "write" ? [[relative4(dir, a.path), a.content]] : [])
2873
+ actions2.flatMap((a) => a.kind === "write" ? [[relative6(dir, a.path), a.content]] : [])
2294
2874
  );
2295
2875
  io.print(JSON.stringify({ ...report2, contents }, null, 2));
2296
2876
  } else io.print(formatReport(report2, actions2));
2297
2877
  return 0;
2298
2878
  }
2299
- const bridge = posix2(relative4(dir, bridgePath(d, options(network).out)));
2879
+ const bridge = posix3(relative6(dir, bridgePath(d, options(network).out)));
2300
2880
  const writesBridge = first.report.files.some((f) => f.path === bridge && f.status === "written");
2301
2881
  if (!network.chosen && interactive && writesBridge) {
2302
2882
  const answer = await io.prompt(`${NETWORK_PROMPT} [y/N] `);
@@ -2321,6 +2901,439 @@ async function runInit(args, io) {
2321
2901
  return report.ok ? 0 : 1;
2322
2902
  }
2323
2903
 
2904
+ // src/mcp/server.ts
2905
+ import { createInterface as createInterface5 } from "readline";
2906
+ var SUPPORTED = ["2025-06-18", "2024-11-05"];
2907
+ var STATUS_TOOL = "agent_bridge_status";
2908
+ var MAX_NAME = 64;
2909
+ function mcpNames(tools, reserved = []) {
2910
+ const out = /* @__PURE__ */ new Map();
2911
+ const taken = new Set(reserved);
2912
+ const sorted = [...tools].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
2913
+ for (const { name } of sorted) {
2914
+ const base = name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, MAX_NAME - 4) || "_";
2915
+ let mcp = base;
2916
+ for (let n = 2; taken.has(mcp); n++) mcp = `${base}_${n}`;
2917
+ taken.add(mcp);
2918
+ out.set(mcp, name);
2919
+ }
2920
+ return out;
2921
+ }
2922
+ var INPUT_SCHEMA = {
2923
+ type: "object",
2924
+ properties: {
2925
+ args: {
2926
+ type: "array",
2927
+ items: {},
2928
+ description: "The tool's arguments, in order (each any JSON value). Leave out for none."
2929
+ },
2930
+ full: {
2931
+ type: "boolean",
2932
+ description: `Return a result over ${OUTPUT_LIMIT_BYTES / 1024} KB whole instead of a summary of its size and shape.`
2933
+ }
2934
+ },
2935
+ additionalProperties: false
2936
+ };
2937
+ var text = (t) => ({ type: "text", text: t });
2938
+ var logBlock = (logs) => logs.length ? [text(`App errors:
2939
+ ${logs.map(logLine).join("\n")}`)] : [];
2940
+ var IMAGE_MAGIC = [
2941
+ ["iVBORw0KGgo", "image/png"],
2942
+ ["/9j/", "image/jpeg"],
2943
+ ["UklGR", "image/webp"],
2944
+ ["R0lGOD", "image/gif"]
2945
+ ];
2946
+ function withImage(tool, value) {
2947
+ if (tool !== "screen.capture" || !value || typeof value !== "object") return null;
2948
+ const { base64, ...rest } = value;
2949
+ if (typeof base64 !== "string" || !base64) return null;
2950
+ const url = /^data:([^;,]*)[^,]*,/.exec(base64);
2951
+ const data = url ? base64.slice(url[0].length) : base64;
2952
+ const mimeType = IMAGE_MAGIC.find(([magic]) => data.startsWith(magic))?.[1];
2953
+ if (!mimeType) return null;
2954
+ const image = { type: "image", data, mimeType };
2955
+ return Object.keys(rest).length ? [image, text(JSON.stringify(rest, null, 2))] : [image];
2956
+ }
2957
+ async function resultContent(tool, value, full) {
2958
+ const image = withImage(tool, value);
2959
+ if (image) return image;
2960
+ if (isSnapshotText(tool, value) && (full || Buffer.byteLength(value) <= OUTPUT_LIMIT_BYTES)) return [text(value)];
2961
+ const shaped = await shapedResult(value, { full });
2962
+ const json = JSON.stringify(shaped, null, 2) ?? "null";
2963
+ if (!full && shaped && typeof shaped === "object" && shaped.resultTooLarge)
2964
+ return [
2965
+ text(
2966
+ JSON.stringify(
2967
+ {
2968
+ ...shaped,
2969
+ hint: "Not returned: the result is large. Call again with full: true to get it whole, or narrow the call (a path, a filter, a smaller page)."
2970
+ },
2971
+ null,
2972
+ 2
2973
+ )
2974
+ )
2975
+ ];
2976
+ return [text(json)];
2977
+ }
2978
+ var message2 = (error) => error instanceof Error ? error.message : String(error);
2979
+ function startMcpServer(options) {
2980
+ const pollMs = options.pollMs ?? 5e3;
2981
+ let bridge = null;
2982
+ let opening = null;
2983
+ let tools = [];
2984
+ let names2 = /* @__PURE__ */ new Map();
2985
+ let signature = "";
2986
+ let listed = null;
2987
+ let announced = "";
2988
+ let initialized = false;
2989
+ let closed = false;
2990
+ let inflight = 0;
2991
+ let lastError;
2992
+ const send = (message3) => {
2993
+ if (!closed) options.output.write(encodeLine(message3));
2994
+ };
2995
+ const reply = (id, result) => send({ jsonrpc: "2.0", id, result });
2996
+ const fail = (id, code, error) => send({ jsonrpc: "2.0", id, error: { code, message: error } });
2997
+ const noteError = (error) => {
2998
+ const now = message2(error);
2999
+ if (now === lastError) return;
3000
+ lastError = now;
3001
+ options.log?.(`agent-bridge mcp: not connected: ${now}`);
3002
+ };
3003
+ const announce = () => {
3004
+ if (!initialized || listed === null || signature === listed || signature === announced)
3005
+ return;
3006
+ announced = signature;
3007
+ send({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
3008
+ };
3009
+ const adopt = (next) => {
3010
+ const list2 = next.tools();
3011
+ const sig = JSON.stringify([next.device.deviceId, list2]);
3012
+ bridge = next;
3013
+ lastError = void 0;
3014
+ if (sig !== signature) {
3015
+ signature = sig;
3016
+ names2 = mcpNames(list2, [STATUS_TOOL]);
3017
+ tools = [...names2].map(([mcp, name]) => ({
3018
+ name,
3019
+ mcp,
3020
+ description: list2.find((t) => t.name === name)?.description
3021
+ }));
3022
+ announce();
3023
+ }
3024
+ };
3025
+ const ensure = () => {
3026
+ if (bridge) return Promise.resolve(bridge);
3027
+ opening ??= options.connect().then((next) => {
3028
+ adopt(next);
3029
+ return next;
3030
+ }).catch((error) => {
3031
+ noteError(error);
3032
+ throw error;
3033
+ }).finally(() => {
3034
+ opening = null;
3035
+ });
3036
+ return opening;
3037
+ };
3038
+ const drop = () => {
3039
+ const old = bridge;
3040
+ bridge = null;
3041
+ try {
3042
+ old?.close();
3043
+ } catch {
3044
+ }
3045
+ };
3046
+ let polling = false;
3047
+ const poll = async () => {
3048
+ if (polling || inflight || closed) return;
3049
+ polling = true;
3050
+ try {
3051
+ if (!bridge) {
3052
+ await ensure();
3053
+ return;
3054
+ }
3055
+ const next = await options.connect();
3056
+ if (inflight || closed) return next.close();
3057
+ const old = bridge;
3058
+ adopt(next);
3059
+ if (old !== next) old.close();
3060
+ } catch (error) {
3061
+ noteError(error);
3062
+ } finally {
3063
+ polling = false;
3064
+ }
3065
+ };
3066
+ const timer = pollMs > 0 ? setInterval(() => void poll(), pollMs) : void 0;
3067
+ timer?.unref?.();
3068
+ const statusTool = {
3069
+ name: STATUS_TOOL,
3070
+ title: "agent-bridge status",
3071
+ description: "Whether this server is connected to the app, which app and session, how many tools it has, and why the last connect failed. Works with no app connected.",
3072
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
3073
+ };
3074
+ const listTools = () => {
3075
+ listed = signature;
3076
+ return {
3077
+ tools: [
3078
+ ...tools.map((t) => ({
3079
+ name: t.mcp,
3080
+ title: t.name,
3081
+ description: t.description ?? t.name,
3082
+ inputSchema: INPUT_SCHEMA
3083
+ })),
3084
+ statusTool
3085
+ ]
3086
+ };
3087
+ };
3088
+ const statusResult = async () => {
3089
+ await ensure().catch(() => {
3090
+ });
3091
+ const json = {
3092
+ connected: !!bridge,
3093
+ ...bridge && {
3094
+ app: { name: bridge.device.name, deviceId: bridge.device.deviceId },
3095
+ tools: tools.length
3096
+ },
3097
+ ...options.sessionName?.() && { session: options.sessionName() },
3098
+ ...lastError && { lastError }
3099
+ };
3100
+ return { content: [text(JSON.stringify(json, null, 2))] };
3101
+ };
3102
+ const callTool = async (params) => {
3103
+ const failure = (...content) => ({ isError: true, content });
3104
+ if (params.name === STATUS_TOOL) return statusResult();
3105
+ const notice = options.takeNotice?.();
3106
+ if (notice) {
3107
+ drop();
3108
+ return failure(text(notice));
3109
+ }
3110
+ inflight++;
3111
+ try {
3112
+ let b;
3113
+ try {
3114
+ b = await ensure();
3115
+ } catch (error) {
3116
+ return failure(
3117
+ text(
3118
+ `Not connected to the app: ${message2(error)}
3119
+ Start the app and call again; the server reconnects on the next call. The ${STATUS_TOOL} tool says what state it is in.`
3120
+ )
3121
+ );
3122
+ }
3123
+ const tool = typeof params.name === "string" ? names2.get(params.name) : void 0;
3124
+ if (!tool) return null;
3125
+ const input = params.arguments ?? {};
3126
+ let args = input.args ?? [];
3127
+ if (typeof args === "string") {
3128
+ try {
3129
+ args = JSON.parse(args);
3130
+ } catch {
3131
+ }
3132
+ }
3133
+ if (!Array.isArray(args))
3134
+ return failure(text("`args` must be an array of the tool's arguments."));
3135
+ try {
3136
+ const timed = await b.timed(tool, ...withCompactSnapshot(tool, args));
3137
+ const content = await resultContent(tool, timed.value, input.full === true);
3138
+ content.push(...logBlock(timed.logs));
3139
+ if (timed.notice) content.push(text(`Warning: ${timed.notice}`));
3140
+ return { content };
3141
+ } catch (error) {
3142
+ if (error instanceof AgentBridgeCallError)
3143
+ return failure(text(error.message), ...logBlock(error.logs));
3144
+ if (error instanceof SessionLinkError) {
3145
+ drop();
3146
+ return failure(
3147
+ text(
3148
+ `${error.message}
3149
+ The call may or may not have run; check before repeating one that changes state. The server reconnects on the next call.`
3150
+ )
3151
+ );
3152
+ }
3153
+ if (/^No reply to "/.test(message2(error))) drop();
3154
+ return failure(text(message2(error)));
3155
+ }
3156
+ } finally {
3157
+ inflight--;
3158
+ }
3159
+ };
3160
+ const active = /* @__PURE__ */ new Map();
3161
+ const dispatch = async (method, params, id, isRequest) => {
3162
+ switch (method) {
3163
+ case "initialize": {
3164
+ if (!isRequest) return null;
3165
+ const asked = params?.protocolVersion;
3166
+ void ensure().catch(() => {
3167
+ });
3168
+ return {
3169
+ result: {
3170
+ protocolVersion: SUPPORTED.includes(asked) ? asked : SUPPORTED[0],
3171
+ capabilities: { tools: { listChanged: true } },
3172
+ serverInfo: { name: "agent-bridge", version: options.version ?? "0.0.0" },
3173
+ instructions: `Tools of the connected React Native app. Each takes \`args\`, an array of the tool's arguments. Results over 32 KB come back as a summary unless \`full\` is true. If the list is empty or calls fail, call ${STATUS_TOOL}.`
3174
+ }
3175
+ };
3176
+ }
3177
+ case "notifications/initialized":
3178
+ initialized = true;
3179
+ return null;
3180
+ case "ping":
3181
+ return isRequest ? { result: {} } : null;
3182
+ case "tools/list":
3183
+ if (!isRequest) return null;
3184
+ await Promise.race([
3185
+ ensure().catch(() => {
3186
+ }),
3187
+ new Promise((r) => {
3188
+ const t = setTimeout(r, 5e3);
3189
+ if (typeof t === "object") t.unref();
3190
+ })
3191
+ ]);
3192
+ return { result: listTools() };
3193
+ case "tools/call": {
3194
+ if (!isRequest) return null;
3195
+ const result = await callTool(params ?? {});
3196
+ return result ? { result } : { code: -32602, error: `Unknown tool: ${String(params?.name)}` };
3197
+ }
3198
+ default:
3199
+ return isRequest ? { code: -32601, error: `Method not found: ${method}` } : null;
3200
+ }
3201
+ };
3202
+ const handle = async (raw) => {
3203
+ let req;
3204
+ try {
3205
+ req = JSON.parse(raw);
3206
+ } catch {
3207
+ return void fail(null, -32700, "Parse error");
3208
+ }
3209
+ if (!req || typeof req !== "object" || Array.isArray(req))
3210
+ return void fail(null, -32600, "Invalid request");
3211
+ if (req.method === void 0 && req.id !== void 0) return;
3212
+ if (typeof req.method !== "string")
3213
+ return void fail(req.id ?? null, -32600, "Invalid request");
3214
+ const { id, method, params } = req;
3215
+ const isRequest = id !== void 0 && id !== null;
3216
+ if (method === "notifications/cancelled") {
3217
+ active.get(JSON.stringify(params?.requestId))?.();
3218
+ return;
3219
+ }
3220
+ const key = JSON.stringify(id);
3221
+ let cancel = () => {
3222
+ };
3223
+ const cancelled = new Promise((r) => {
3224
+ cancel = () => r("cancelled");
3225
+ });
3226
+ if (isRequest) active.set(key, cancel);
3227
+ try {
3228
+ const outcome = await Promise.race([dispatch(method, params, id, isRequest), cancelled]);
3229
+ if (outcome === "cancelled" || !outcome) return;
3230
+ if ("result" in outcome) reply(id, outcome.result);
3231
+ else fail(id, outcome.code, outcome.error);
3232
+ } catch (error) {
3233
+ if (isRequest) fail(id, -32603, message2(error));
3234
+ } finally {
3235
+ if (isRequest) active.delete(key);
3236
+ }
3237
+ };
3238
+ const lines = createInterface5({ input: options.input });
3239
+ const pending = /* @__PURE__ */ new Set();
3240
+ lines.on("line", (line) => {
3241
+ if (!line.trim()) return;
3242
+ const p = handle(line).finally(() => pending.delete(p));
3243
+ pending.add(p);
3244
+ });
3245
+ const done = new Promise((resolve4) => {
3246
+ lines.once("close", () => {
3247
+ void Promise.allSettled([...pending]).then(() => {
3248
+ close();
3249
+ resolve4();
3250
+ });
3251
+ });
3252
+ });
3253
+ function close() {
3254
+ if (closed) return;
3255
+ closed = true;
3256
+ if (timer) clearInterval(timer);
3257
+ lines.close();
3258
+ drop();
3259
+ }
3260
+ return { done, close };
3261
+ }
3262
+
3263
+ // src/mcp/run.ts
3264
+ function createConnector(flags, options) {
3265
+ let daemon;
3266
+ let pinned;
3267
+ let ending = false;
3268
+ let notice;
3269
+ const ownName = `mcp-${process.pid}`;
3270
+ const stopped = (name) => {
3271
+ notice = `The session "${name}" this server was using stopped (session stop, or its idle time ran out). Stopping runs bridge.restore, so state changed through it was most likely undone. This call did not run; call again and the server uses a new session.`;
3272
+ };
3273
+ const connectBridge = async () => {
3274
+ if (flags["no-session"]) return connect(options);
3275
+ const timeoutMs = options.timeoutMs;
3276
+ if (daemon) return connectSession({ name: daemon.state.name, timeoutMs });
3277
+ if (pinned) {
3278
+ if (readSession(pinned)) return connectSession({ name: pinned, timeoutMs });
3279
+ stopped(pinned);
3280
+ pinned = void 0;
3281
+ }
3282
+ const existing = sessionFor(flags, (s) => s.name.startsWith("mcp-") && s.name !== ownName);
3283
+ if (existing) {
3284
+ pinned = existing.name;
3285
+ return connectSession({ name: pinned, timeoutMs });
3286
+ }
3287
+ const started = await runSessionDaemon({
3288
+ name: ownName,
3289
+ metro: options.metro,
3290
+ device: options.device,
3291
+ transport: options.transport,
3292
+ idleMs: 0,
3293
+ timeoutMs
3294
+ });
3295
+ daemon = started;
3296
+ void started.done.then(() => {
3297
+ if (daemon !== started) return;
3298
+ daemon = void 0;
3299
+ if (!ending) stopped(started.state.name);
3300
+ });
3301
+ return connectSession({ name: started.state.name, timeoutMs });
3302
+ };
3303
+ return {
3304
+ connect: connectBridge,
3305
+ takeNotice: () => {
3306
+ const n = notice;
3307
+ notice = void 0;
3308
+ return n;
3309
+ },
3310
+ stop: async () => {
3311
+ ending = true;
3312
+ return daemon?.stop(flags.keep).catch(() => null);
3313
+ },
3314
+ sessionName: () => daemon?.state.name ?? pinned
3315
+ };
3316
+ }
3317
+ async function mcpMain(flags, options, version2) {
3318
+ const connector = createConnector(flags, options);
3319
+ const server = startMcpServer({
3320
+ input: process.stdin,
3321
+ output: process.stdout,
3322
+ connect: connector.connect,
3323
+ takeNotice: connector.takeNotice,
3324
+ sessionName: connector.sessionName,
3325
+ log: (line) => console.error(line),
3326
+ pollMs: flags["no-session"] ? 0 : void 0,
3327
+ version: version2
3328
+ });
3329
+ const quit = () => server.close();
3330
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"])
3331
+ process.on(signal, quit);
3332
+ await server.done;
3333
+ const restore = await connector.stop();
3334
+ if (restore && !restore.ok) console.error(`bridge.restore failed: ${restore.error}`);
3335
+ }
3336
+
2324
3337
  // src/cli.ts
2325
3338
  var HELP2 = `agent-bridge: drive a running React Native app from an agent
2326
3339
 
@@ -2337,10 +3350,15 @@ Usage
2337
3350
  agent-bridge call --batch Read \`tool args\` lines from stdin (args may be @file); one JSON line per call, one connection
2338
3351
  agent-bridge repl Interactive prompt (history, tab completion, .help). With
2339
3352
  stdin not a terminal it behaves like call --batch
2340
- agent-bridge run <flow.mjs> Run a flow: export default async ({ step, call }) => {}
2341
- export const scenario = 'signedIn' applies it first
2342
- and runs bridge.restore after, even on failure
3353
+ agent-bridge run <flow|dir|glob>... Run flows in order on one connection: export default
3354
+ async ({ step, call, expect }) => {}. A directory means
3355
+ its *.flow.mjs. Each flow ends with bridge.restore, even
3356
+ on failure (--keep skips it); a scenario export is
3357
+ applied first. Exits 1 if any flow failed
2343
3358
  agent-bridge scenarios Scenarios the app defines (scenario.apply applies one)
3359
+ agent-bridge mcp Serve the app's tools as an MCP server on stdio (add to your agent's MCP
3360
+ config). Uses this project's session, or starts one and
3361
+ runs bridge.restore on exit (--keep: don't)
2344
3362
  agent-bridge assert-absent <files...> Fail if a release bundle contains the bridge
2345
3363
 
2346
3364
  Sessions: one connection for all of an agent's calls
@@ -2356,11 +3374,18 @@ Options
2356
3374
  --device <text> Pick an app when several are connected
2357
3375
  --transport <name> auto (default), expo or cdp
2358
3376
  --timeout <ms> Per-call timeout (default 10000)
2359
- --out <file> call: write the result to a file; print its size and shape
3377
+ --out <file> call: write the result to a file and print its size and shape
2360
3378
  (--batch: a directory, one <n>-<tool>.json per call)
3379
+ run: the report file (with --reporter json|junit)
2361
3380
  --stop-on-error call --batch: stop after the first failed call
2362
3381
  --full call: print results over 32 KB instead of a summary
2363
- --strict run: exit non-zero if the app logged an error
3382
+ --json call screen.snapshot: print JSON with rects, not one line per element (call --batch and repl stay JSON)
3383
+ (--out also writes JSON)
3384
+ --strict run: fail a flow if the app logged an error
3385
+ --bail run: stop at the first failed flow
3386
+ --grep <text> run: only flows whose name or path (relative to here) contains text
3387
+ --keep run: no bridge.restore after each flow (session stop: keep the session)
3388
+ --reporter <name> run: text (default), json or junit; step lines and flow output go to stderr
2364
3389
  --scenario <name> run: also apply this scenario (repeatable). Options as
2365
3390
  JSON after "=": --scenario 'signedIn={"user":{"name":"Ada"}}'
2366
3391
  --session <name> Use this session (env AGENT_BRIDGE_SESSION)
@@ -2402,6 +3427,9 @@ async function main() {
2402
3427
  batch: { type: "boolean" },
2403
3428
  "stop-on-error": { type: "boolean" },
2404
3429
  scenario: { type: "string", multiple: true },
3430
+ bail: { type: "boolean" },
3431
+ grep: { type: "string" },
3432
+ reporter: { type: "string" },
2405
3433
  name: { type: "string" },
2406
3434
  idle: { type: "string" },
2407
3435
  keep: { type: "boolean" },
@@ -2507,12 +3535,12 @@ async function main() {
2507
3535
  (path) => path === "-" ? stdin : readArgFile(path)
2508
3536
  );
2509
3537
  return withBridge(async (bridge) => {
2510
- const { value, ms, appMs, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
3538
+ const { value, ms, appMs, logs, notice } = await bridge.timed(tool, ...withCompactSnapshot(tool, args, values.json || !!values.out)).catch((error) => {
2511
3539
  for (const e of failedLogs(error)) console.error(logLine(e));
2512
3540
  throw error;
2513
3541
  });
2514
3542
  console.log(
2515
- await renderResult(value, { out: values.out, full: values.full })
3543
+ isSnapshotText(tool, value) ? await renderText(value, { out: values.out, full: values.full }) : await renderResult(value, { out: values.out, full: values.full })
2516
3544
  );
2517
3545
  for (const e of logs) console.error(logLine(e));
2518
3546
  if (notice) console.error(`Warning: ${notice}`);
@@ -2537,18 +3565,65 @@ async function main() {
2537
3565
  });
2538
3566
  });
2539
3567
  case "run": {
2540
- const [file] = rest;
2541
- if (!file) throw new Error("Usage: agent-bridge run <flow file>");
2542
- const flow = await import(pathToFileURL(resolve2(file)).href);
2543
- flowScenarios(flow);
3568
+ if (!rest.length)
3569
+ throw new Error("Usage: agent-bridge run <flow file | directory | glob>...");
3570
+ const machine = checkRunFlags({ reporter: values.reporter, out: values.out });
3571
+ const reporter = values.reporter ?? "text";
3572
+ const files = await resolveFlowFiles(rest);
3573
+ const { entries: loaded, failures } = await loadFlows(files);
3574
+ const entries = selectFlows(loaded, values.grep);
3575
+ if (!entries.length && !failures.length) {
3576
+ if (!machine) throw new Error(`--grep ${values.grep}: no flow matches`);
3577
+ failures.push(failedRun("", `--grep ${values.grep}`, "no flow matches"));
3578
+ }
3579
+ if (machine && values.out) await mkdir3(dirname7(values.out), { recursive: true });
2544
3580
  const extra = (values.scenario ?? []).map(parseScenarioFlag);
2545
- return withBridge(async (bridge) => {
2546
- const { errors, restoreErrors } = await runFlow(bridge, flow, {
2547
- scenarios: extra
2548
- });
2549
- if (values.strict && errors || restoreErrors.length)
2550
- process.exitCode = 1;
3581
+ const finish = async (runs2) => {
3582
+ const failed = runs2.filter((r) => r.status === "failed");
3583
+ if (machine) {
3584
+ const doc = reporter === "json" ? renderJson(runs2) : renderJunit(runs2);
3585
+ if (values.out) await writeReport(values.out, doc);
3586
+ else console.log(doc);
3587
+ }
3588
+ const log = machine ? console.error : console.log;
3589
+ if (runs2.length > 1 || machine) {
3590
+ log("");
3591
+ for (const r of runs2)
3592
+ log(
3593
+ `${r.status === "passed" ? "PASS" : r.status === "failed" ? "FAIL" : "SKIP"} ${r.name}${r.status === "skipped" ? "" : ` (${(r.ms / 1e3).toFixed(1)} s)`}${r.error ? `: ${r.error}` : ""}`
3594
+ );
3595
+ const count = (st) => runs2.filter((r) => r.status === st).length;
3596
+ log(
3597
+ `${count("passed")} passed, ${failed.length} failed${count("skipped") ? `, ${count("skipped")} skipped` : ""}`
3598
+ );
3599
+ }
3600
+ if (failed.length) process.exitCode = 1;
3601
+ };
3602
+ const execute = (bridge) => runFlows(bridge, entries, {
3603
+ scenarios: extra,
3604
+ strict: values.strict,
3605
+ bail: values.bail,
3606
+ keep: values.keep,
3607
+ single: entries.length === 1 && !machine,
3608
+ print: machine ? (line) => console.error(line) : void 0
2551
3609
  });
3610
+ let runs;
3611
+ if (!entries.length) runs = [];
3612
+ else {
3613
+ try {
3614
+ const run = (bridge) => machine ? logToStderr(() => execute(bridge)) : execute(bridge);
3615
+ let result = [];
3616
+ await withBridge(async (bridge) => {
3617
+ result = await run(bridge);
3618
+ });
3619
+ runs = result;
3620
+ } catch (error) {
3621
+ if (!machine) throw error;
3622
+ const why = error instanceof Error ? error.message : String(error);
3623
+ runs = entries.map((e) => failedRun(e.file, e.name, why));
3624
+ }
3625
+ }
3626
+ return finish([...failures, ...runs]);
2552
3627
  }
2553
3628
  case "scenarios":
2554
3629
  return withBridge(async (bridge) => {
@@ -2568,6 +3643,12 @@ async function main() {
2568
3643
  console.log(` ${"".padEnd(20)} options: ${JSON.stringify(s.options)}`);
2569
3644
  }
2570
3645
  });
3646
+ case "mcp":
3647
+ return mcpMain(
3648
+ values,
3649
+ options,
3650
+ createRequire(import.meta.url)("../package.json").version
3651
+ );
2571
3652
  case "session":
2572
3653
  return sessionCommand(rest[0], values);
2573
3654
  case DAEMON_COMMAND: