@avasapp/agent-bridge 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +36 -14
  2. package/dist/adapters/ably.cjs +79 -3
  3. package/dist/adapters/ably.d.cts +2 -1
  4. package/dist/adapters/async-storage.cjs +158 -0
  5. package/dist/adapters/async-storage.d.cts +14 -0
  6. package/dist/adapters/device.cjs +1110 -0
  7. package/dist/adapters/device.d.cts +49 -0
  8. package/dist/adapters/expo-router.cjs +112 -6
  9. package/dist/adapters/expo-router.d.cts +1 -1
  10. package/dist/adapters/jotai.cjs +266 -0
  11. package/dist/adapters/jotai.d.cts +19 -0
  12. package/dist/adapters/react-native-mmkv.cjs +80 -0
  13. package/dist/adapters/react-native-mmkv.d.cts +3 -1
  14. package/dist/adapters/react-native-webview.d.cts +1 -1
  15. package/dist/adapters/react-navigation.cjs +293 -0
  16. package/dist/adapters/react-navigation.d.cts +55 -0
  17. package/dist/adapters/redux.cjs +322 -0
  18. package/dist/adapters/redux.d.cts +28 -0
  19. package/dist/adapters/secure-store.cjs +169 -0
  20. package/dist/adapters/secure-store.d.cts +28 -0
  21. package/dist/adapters/socket-io.cjs +77 -3
  22. package/dist/adapters/socket-io.d.cts +2 -1
  23. package/dist/adapters/tanstack-query.cjs +3 -0
  24. package/dist/adapters/tanstack-query.d.cts +13 -4
  25. package/dist/adapters/zustand.cjs +193 -22
  26. package/dist/adapters/zustand.d.cts +11 -15
  27. package/dist/{chunk-TA2OLPPT.js → chunk-Y6QFU5ON.js} +477 -165
  28. package/dist/cli.js +2370 -103
  29. package/dist/client/index.cjs +441 -136
  30. package/dist/client/index.d.ts +67 -4
  31. package/dist/client/index.js +5 -3
  32. package/dist/expo/index.d.cts +1 -1
  33. package/dist/network/index.cjs +320 -73
  34. package/dist/network/index.d.cts +18 -3
  35. package/dist/noop/async-storage.cjs +26 -0
  36. package/dist/noop/device.cjs +26 -0
  37. package/dist/noop/jotai.cjs +26 -0
  38. package/dist/noop/react-navigation.cjs +26 -0
  39. package/dist/noop/realtime.cjs +6 -1
  40. package/dist/noop/redux.cjs +28 -0
  41. package/dist/noop/secure-store.cjs +26 -0
  42. package/dist/realtime/index.cjs +71 -4
  43. package/dist/realtime/index.d.cts +21 -2
  44. package/dist/runtime/index.cjs +838 -128
  45. package/dist/runtime/index.d.cts +4 -2
  46. package/dist/store-common-j0cHHvBr.d.cts +23 -0
  47. package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
  48. package/entries/async-storage.cjs +9 -0
  49. package/entries/device.cjs +9 -0
  50. package/entries/jotai.cjs +9 -0
  51. package/entries/react-navigation.cjs +9 -0
  52. package/entries/redux.cjs +9 -0
  53. package/entries/secure-store.cjs +9 -0
  54. package/package.json +34 -2
  55. package/skills/agent-bridge/SKILL.md +70 -22
  56. package/skills/agent-bridge/references/device.md +21 -0
  57. package/skills/agent-bridge/references/flows.md +19 -8
  58. package/skills/agent-bridge/references/large-data.md +6 -6
  59. package/skills/agent-bridge/references/network.md +25 -7
  60. package/skills/agent-bridge/references/realtime.md +5 -5
  61. package/skills/agent-bridge/references/restore.md +25 -7
  62. package/skills/agent-bridge/references/screen.md +25 -11
  63. package/skills/agent-bridge/references/state.md +31 -11
  64. package/skills/agent-bridge/references/webview.md +9 -9
  65. package/skills/agent-bridge/references/writing-scenarios.md +3 -3
package/dist/cli.js CHANGED
@@ -2,10 +2,14 @@
2
2
  import {
3
3
  AgentBridgeCallError,
4
4
  RUNTIME_MARKER,
5
+ SessionLinkError,
6
+ assertListening,
7
+ callTimeout,
5
8
  checkName,
6
9
  connect,
7
10
  connectSession,
8
11
  encodeLine,
12
+ explainConnectError,
9
13
  explicitMetro,
10
14
  flowScenarios,
11
15
  formatDuration,
@@ -14,6 +18,7 @@ import {
14
18
  listSessions,
15
19
  logLine,
16
20
  metroHost,
21
+ noAppHint,
17
22
  openConnection,
18
23
  parseDuration,
19
24
  parseScenarioFlag,
@@ -21,27 +26,201 @@ import {
21
26
  projectRoot,
22
27
  readSession,
23
28
  removeSessionFiles,
24
- runFlow,
29
+ runFlows,
25
30
  sessionFiles,
26
31
  sessionRequest,
27
32
  stateDir,
28
33
  writePrivate
29
- } from "./chunk-TA2OLPPT.js";
34
+ } from "./chunk-Y6QFU5ON.js";
30
35
 
31
36
  // src/cli.ts
32
- import { readFile } from "fs/promises";
33
- import { resolve } from "path";
34
- 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";
35
40
  import { parseArgs } from "util";
36
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
+
37
216
  // src/client/batch.ts
38
217
  import { readFileSync } from "fs";
39
- import { mkdir, readdir, stat } from "fs/promises";
40
- 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";
41
220
  import { createInterface } from "readline";
42
221
 
43
222
  // src/client/output.ts
44
- import { writeFile } from "fs/promises";
223
+ import { writeFile as writeFile2 } from "fs/promises";
45
224
  var OUTPUT_LIMIT_BYTES = 32 * 1024;
46
225
  var kb = (bytes) => bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
47
226
  function shapeOf(value) {
@@ -73,7 +252,7 @@ async function shapedResult(value, options = {}) {
73
252
  const json = JSON.stringify(value, null, 2) ?? "null";
74
253
  const bytes = Buffer.byteLength(json);
75
254
  if (options.out) {
76
- await writeFile(options.out, `${json}
255
+ await writeFile2(options.out, `${json}
77
256
  `);
78
257
  return fileSummary(options.out, bytes, value);
79
258
  }
@@ -85,7 +264,7 @@ async function renderResult(value, options = {}) {
85
264
  const json = JSON.stringify(value, null, 2) ?? "null";
86
265
  const bytes = Buffer.byteLength(json);
87
266
  if (options.out) {
88
- await writeFile(options.out, `${json}
267
+ await writeFile2(options.out, `${json}
89
268
  `);
90
269
  return JSON.stringify(fileSummary(options.out, bytes, value), null, 2);
91
270
  }
@@ -93,6 +272,16 @@ async function renderResult(value, options = {}) {
93
272
  return json;
94
273
  return JSON.stringify(tooLarge(bytes, value), null, 2);
95
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
+ }
96
285
 
97
286
  // src/client/batch.ts
98
287
  function parseCallArgs(raw) {
@@ -120,9 +309,9 @@ function fileJson(ref, read) {
120
309
  throw new Error(
121
310
  `An argument that is just "@" needs a path (@file, or @- for stdin). To pass the text itself, quote it as JSON: '"@"'`
122
311
  );
123
- const text = read(path);
312
+ const text2 = read(path);
124
313
  try {
125
- return JSON.parse(text);
314
+ return JSON.parse(text2);
126
315
  } catch (error) {
127
316
  throw new Error(
128
317
  `${path === "-" ? "stdin" : path} is not valid JSON (${error instanceof Error ? error.message : error})`
@@ -146,19 +335,19 @@ function parseCallArgv(argv, read = readArgFile) {
146
335
  });
147
336
  }
148
337
  var BatchParseError = class extends Error {
149
- constructor(tool, message2) {
150
- super(message2);
338
+ constructor(tool, message3) {
339
+ super(message3);
151
340
  this.tool = tool;
152
341
  }
153
342
  tool;
154
343
  };
155
344
  function parseBatchLine(line) {
156
- const text = line.trim();
157
- if (!text || text.startsWith("#")) return null;
158
- const at = text.search(/\s/);
159
- if (at < 0) return { tool: text, args: [] };
160
- const tool = text.slice(0, at);
161
- 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();
162
351
  if (/^[[{"]/.test(raw)) {
163
352
  try {
164
353
  JSON.parse(raw);
@@ -289,11 +478,11 @@ async function dotLine(bridge, line) {
289
478
  }
290
479
  }
291
480
  async function checkOutDir(dir) {
292
- const info = await stat(dir).catch(() => null);
293
- if (!info) return mkdir(dir, { recursive: true });
481
+ const info = await stat2(dir).catch(() => null);
482
+ if (!info) return mkdir2(dir, { recursive: true });
294
483
  if (!info.isDirectory())
295
484
  throw new Error(`--out ${dir} is a file; with --batch it must be a directory`);
296
- if ((await readdir(dir)).length)
485
+ if ((await readdir2(dir)).length)
297
486
  throw new Error(
298
487
  `--out ${dir} is not empty; use a new or empty directory so old files aren't mistaken for this run's`
299
488
  );
@@ -306,9 +495,9 @@ async function runBatch(bridge, input, options = {}) {
306
495
  let stopped = false;
307
496
  try {
308
497
  for await (const line of rl) {
309
- const text = line.trim();
310
- const outcome = text.startsWith(".") ? await dotLine(bridge, text) : await runLine(bridge, line);
311
- 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;
312
501
  if (!outcome) continue;
313
502
  result.calls++;
314
503
  const raw = outcome;
@@ -323,7 +512,7 @@ async function runBatch(bridge, input, options = {}) {
323
512
  record.value = await shapedResult(raw.value, {
324
513
  full: options.full,
325
514
  outKind: "dir",
326
- out: options.outDir ? join(
515
+ out: options.outDir ? join3(
327
516
  options.outDir,
328
517
  `${result.calls}-${String(raw.tool).replace(/[^\w.-]/g, "_")}.json`
329
518
  ) : void 0
@@ -352,13 +541,27 @@ async function runBatch(bridge, input, options = {}) {
352
541
  return result;
353
542
  }
354
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
+
355
558
  // src/client/repl.ts
356
559
  import { appendFileSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
357
560
  import { homedir } from "os";
358
- import { dirname, join as join2 } from "path";
561
+ import { dirname as dirname2, join as join4 } from "path";
359
562
  import { createInterface as createInterface2 } from "readline";
360
563
  var HISTORY_LIMIT = 500;
361
- var DEFAULT_HISTORY_FILE = join2(
564
+ var DEFAULT_HISTORY_FILE = join4(
362
565
  homedir(),
363
566
  ".agent-bridge",
364
567
  "repl_history"
@@ -410,7 +613,7 @@ function loadHistory(file) {
410
613
  }
411
614
  function saveHistory(file, lines) {
412
615
  try {
413
- mkdirSync(dirname(file), { recursive: true });
616
+ mkdirSync(dirname2(file), { recursive: true });
414
617
  writeFileSync(file, `${lines.slice(-HISTORY_LIMIT).join("\n")}
415
618
  `, {
416
619
  mode: 384
@@ -420,7 +623,7 @@ function saveHistory(file, lines) {
420
623
  }
421
624
  function appendHistory(file, line) {
422
625
  try {
423
- mkdirSync(dirname(file), { recursive: true });
626
+ mkdirSync(dirname2(file), { recursive: true });
424
627
  appendFileSync(file, `${line}
425
628
  `, { mode: 384 });
426
629
  } catch {
@@ -432,9 +635,9 @@ async function runRepl(bridge, options) {
432
635
  const { output } = options;
433
636
  const tty = !!options.tty;
434
637
  const { historyFile } = options;
435
- const paint = (code, text) => tty && !process.env.NO_COLOR ? `\x1B[${code}m${text}\x1B[0m` : text;
436
- const print = (text = "") => {
437
- 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}
438
641
  `);
439
642
  };
440
643
  const toolNames = () => bridge.tools().map((t) => t.name);
@@ -587,21 +790,201 @@ import { spawn } from "child_process";
587
790
  import { existsSync, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
588
791
 
589
792
  // src/client/session/daemon.ts
793
+ import { randomBytes } from "crypto";
590
794
  import { appendFileSync as appendFileSync2, chmodSync, rmSync } from "fs";
591
795
  import { createServer } from "net";
592
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
593
940
  var RELOAD = "app.reload";
594
- var pause = (ms, signal) => new Promise((resolve2) => {
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) => {
595
979
  const done = () => {
596
980
  clearTimeout(timer);
597
981
  signal.removeEventListener("abort", done);
598
- resolve2();
982
+ resolve4();
599
983
  };
600
984
  const timer = setTimeout(done, ms);
601
985
  signal.addEventListener("abort", done, { once: true });
602
986
  });
603
987
  var message = (error) => error instanceof Error ? error.message : String(error);
604
- var lostConnection = (r) => !r.ok && (r.from === "" || /socket closed|closed the debugger|not open/i.test(r.error));
605
988
  async function runSessionDaemon(options) {
606
989
  const name = checkName(options.name);
607
990
  const dir = stateDir();
@@ -609,7 +992,7 @@ async function runSessionDaemon(options) {
609
992
  const metro = metroHost(options.metro);
610
993
  const timeoutMs = options.timeoutMs ?? 1e4;
611
994
  const reloadWaitMs = options.reloadWaitMs ?? 2e4;
612
- const log = (text) => appendFileSync2(files.log, `${(/* @__PURE__ */ new Date()).toISOString()} ${text}
995
+ const log = (text2) => appendFileSync2(files.log, `${(/* @__PURE__ */ new Date()).toISOString()} ${text2}
613
996
  `, {
614
997
  mode: 384
615
998
  });
@@ -636,7 +1019,9 @@ async function runSessionDaemon(options) {
636
1019
  `${c.device.name} already belongs to session "${owner.name}". Use --session ${owner.name}, or stop it first.`
637
1020
  );
638
1021
  };
639
- let conn = await reach();
1022
+ let conn = await reach().catch(async (error) => {
1023
+ throw await explainConnectError(error, metro, options.device);
1024
+ });
640
1025
  claim(conn);
641
1026
  const pin = conn.pin;
642
1027
  const deviceOf = (c) => ({
@@ -664,20 +1049,68 @@ async function runSessionDaemon(options) {
664
1049
  let lastPending = [];
665
1050
  const reloads = [];
666
1051
  let unseen = [];
667
- const noteReload = () => {
668
- const lost = lastPending;
669
- lastPending = [];
670
- 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) => {
671
1061
  reloads.push(note);
672
1062
  unseen.push(note);
673
1063
  log(`warning: ${note}`);
674
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
+ };
675
1102
  const observe = (r) => {
676
1103
  if (!r.loadId) return;
677
- if (knownLoad && r.loadId !== knownLoad) noteReload();
1104
+ if (knownLoad && r.loadId !== knownLoad) noteReload(r.loadId);
678
1105
  knownLoad = r.loadId;
679
1106
  lastPending = r.pending ?? [];
680
1107
  };
1108
+ const seen = (r) => {
1109
+ observe(r);
1110
+ journal.accept(r);
1111
+ const { journalData: _, ...rest } = r;
1112
+ return rest;
1113
+ };
681
1114
  const takeNotice = () => {
682
1115
  const notice = unseen.join("; ");
683
1116
  unseen = [];
@@ -686,9 +1119,9 @@ async function runSessionDaemon(options) {
686
1119
  let reloadingUntil = 0;
687
1120
  let stale = false;
688
1121
  let reconnecting = null;
689
- const reconnect = (reason) => {
1122
+ const reconnect = (reason2) => {
690
1123
  reconnecting ??= (async () => {
691
- log(`reconnecting: ${reason}`);
1124
+ log(`reconnecting: ${reason2}`);
692
1125
  conn.close();
693
1126
  const patient = Date.now() < reloadingUntil;
694
1127
  const deadline = Date.now() + (patient ? reloadWaitMs : 0);
@@ -707,7 +1140,7 @@ async function runSessionDaemon(options) {
707
1140
  stale = true;
708
1141
  log(`reconnect failed: ${message(error)}`);
709
1142
  throw new Error(
710
- 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)}`
711
1144
  );
712
1145
  }
713
1146
  await pause(300, stopping.signal);
@@ -716,7 +1149,7 @@ async function runSessionDaemon(options) {
716
1149
  stale = false;
717
1150
  reloadingUntil = 0;
718
1151
  if (conn.device.loadId) {
719
- if (knownLoad && conn.device.loadId !== knownLoad) noteReload();
1152
+ if (knownLoad && conn.device.loadId !== knownLoad) noteReload(conn.device.loadId);
720
1153
  knownLoad = conn.device.loadId;
721
1154
  }
722
1155
  state.device = deviceOf(conn);
@@ -728,8 +1161,8 @@ async function runSessionDaemon(options) {
728
1161
  });
729
1162
  return reconnecting;
730
1163
  };
731
- const connected = async (reason = "app stopped answering") => {
732
- const attempt2 = reconnecting ?? reconnect(reason);
1164
+ const connected = async (reason2 = "app stopped answering") => {
1165
+ const attempt2 = reconnecting ?? reconnect(reason2);
733
1166
  const wait = Date.now() < reloadingUntil ? reloadWaitMs + 2 * attemptMs + 500 : 2 * attemptMs + 500;
734
1167
  let timer2;
735
1168
  try {
@@ -750,16 +1183,16 @@ async function runSessionDaemon(options) {
750
1183
  clearTimeout(timer2);
751
1184
  }
752
1185
  };
753
- 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(
754
1187
  (r) => {
755
- if (r.ok) observe(r);
1188
+ if (r.ok) seen(r);
756
1189
  return r.ok;
757
1190
  },
758
1191
  () => false
759
1192
  );
760
1193
  const attempt = async (tool, args, ms) => {
761
1194
  try {
762
- const result = await conn.call(tool, args, ms);
1195
+ const result = await send(tool, args, ms);
763
1196
  return lostConnection(result) ? { lost: result.error } : { result };
764
1197
  } catch (error) {
765
1198
  if (await ping(ms)) throw error;
@@ -768,12 +1201,15 @@ async function runSessionDaemon(options) {
768
1201
  };
769
1202
  const callApp = async (tool, args, ms) => {
770
1203
  if (stale || reconnecting) await connected();
1204
+ await adoptJournal(tool === "bridge.restore");
1205
+ if (tool === RELOAD) await ping();
771
1206
  let t0 = performance.now();
1207
+ const start2 = t0;
772
1208
  const first = await attempt(tool, args, ms);
773
1209
  if (first.result) {
774
- observe(first.result);
775
- if (tool === RELOAD && first.result.ok) waitForReload();
776
- 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 };
777
1213
  }
778
1214
  if (tool === RELOAD) {
779
1215
  waitForReload();
@@ -786,20 +1222,48 @@ async function runSessionDaemon(options) {
786
1222
  };
787
1223
  return { result, ms: performance.now() - t0 };
788
1224
  }
789
- 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
+ }
790
1247
  t0 = performance.now();
791
- 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);
792
1253
  if (!second.result)
793
1254
  throw new Error(
794
1255
  `The app stopped answering after a reconnect: ${second.lost}`
795
1256
  );
796
- observe(second.result);
797
1257
  return {
798
- result: second.result,
1258
+ result: seen(second.result),
799
1259
  ms: performance.now() - t0,
800
1260
  reconnected: true
801
1261
  };
802
1262
  };
1263
+ const sync = async () => {
1264
+ if (journal.hasCarry()) await adoptJournal(true);
1265
+ else await ping();
1266
+ };
803
1267
  const waitForReload = () => {
804
1268
  reloadingUntil = Date.now() + reloadWaitMs;
805
1269
  stale = true;
@@ -822,19 +1286,47 @@ async function runSessionDaemon(options) {
822
1286
  };
823
1287
  const health = options.healthMs === 0 ? void 0 : setInterval(async () => {
824
1288
  if (inflight || reconnecting || finishing) return;
825
- 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)
826
1293
  await reconnect("no answer to bridge.ping").catch(() => {
827
1294
  });
828
1295
  }, options.healthMs ?? 5e3);
829
1296
  const timer = health;
830
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
+ };
831
1323
  let finishing = null;
832
1324
  let resolveDone = () => {
833
1325
  };
834
1326
  const done = new Promise((r) => {
835
1327
  resolveDone = r;
836
1328
  });
837
- const teardown = (reason, keep) => {
1329
+ const teardown = (reason2, keep) => {
838
1330
  finishing ??= (async () => {
839
1331
  clearTimeout(idleTimer);
840
1332
  clearInterval(health);
@@ -842,16 +1334,9 @@ async function runSessionDaemon(options) {
842
1334
  const cut = setTimeout(() => cutOff.abort(), Math.min(attemptMs, 5e3));
843
1335
  let restore = null;
844
1336
  if (!keep) {
845
- restore = await callApp("bridge.restore", [], timeoutMs).then(
846
- (r) => r.result,
847
- (error) => ({
848
- id: "restore",
849
- from: "",
850
- ok: false,
851
- error: message(error),
852
- ms: 0
853
- })
854
- );
1337
+ restore = await restoreOnce();
1338
+ const left = journal.giveUp();
1339
+ if (left) addNote(`app reloaded; ${left}`);
855
1340
  }
856
1341
  clearTimeout(cut);
857
1342
  cutOff.abort();
@@ -859,10 +1344,10 @@ async function runSessionDaemon(options) {
859
1344
  server.close();
860
1345
  removeSessionFiles(name, dir);
861
1346
  const outcome = !restore ? "kept app state" : restore.ok ? `restore: ${JSON.stringify(restore.value)}` : `restore: ${restore.error}`;
862
- log(`stopped (${reason}); ${outcome}`);
1347
+ log(`stopped (${reason2}); ${outcome}`);
863
1348
  const deadline = Date.now() + 2e3;
864
1349
  const settle = () => {
865
- if (!clients.size || Date.now() > deadline) resolveDone(reason);
1350
+ if (!clients.size || Date.now() > deadline) resolveDone(reason2);
866
1351
  else setTimeout(settle, 10);
867
1352
  };
868
1353
  settle();
@@ -884,16 +1369,21 @@ async function runSessionDaemon(options) {
884
1369
  switch (req.op) {
885
1370
  case "call": {
886
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" };
887
1374
  inflight++;
888
1375
  touch();
889
1376
  try {
1377
+ const args = req.args ?? [];
890
1378
  const answer = await callApp(
891
1379
  req.tool,
892
- req.args ?? [],
893
- req.timeoutMs ?? timeoutMs
1380
+ args,
1381
+ callTimeout(req.timeoutMs ?? timeoutMs, req.tool, args)
894
1382
  );
1383
+ await adoptRun;
895
1384
  return { id: req.id, ...answer, ...takeNotice() };
896
1385
  } catch (error) {
1386
+ await adoptRun;
897
1387
  return { id: req.id, error: message(error), ...takeNotice() };
898
1388
  } finally {
899
1389
  inflight--;
@@ -936,7 +1426,10 @@ async function runSessionDaemon(options) {
936
1426
  socket.on("close", () => clients.delete(socket));
937
1427
  socket.on("error", () => {
938
1428
  });
939
- createInterface3({ input: socket }).on("line", async (line) => {
1429
+ const lines = createInterface3({ input: socket });
1430
+ lines.on("error", () => {
1431
+ });
1432
+ lines.on("line", async (line) => {
940
1433
  let req;
941
1434
  try {
942
1435
  req = JSON.parse(line);
@@ -953,11 +1446,11 @@ async function runSessionDaemon(options) {
953
1446
  });
954
1447
  if (process.platform !== "win32") rmSync(files.socket, { force: true });
955
1448
  try {
956
- await new Promise((resolve2, reject) => {
1449
+ await new Promise((resolve4, reject) => {
957
1450
  server.once("error", reject);
958
1451
  server.listen(files.socket, () => {
959
1452
  server.off("error", reject);
960
- resolve2();
1453
+ resolve4();
961
1454
  });
962
1455
  });
963
1456
  } catch (error) {
@@ -984,13 +1477,14 @@ async function runSessionDaemon(options) {
984
1477
 
985
1478
  // src/client/session/cli.ts
986
1479
  var DAEMON_COMMAND = "__session-daemon";
987
- function sessionFor(flags) {
1480
+ function sessionFor(flags, skip) {
988
1481
  if (flags["no-session"]) return null;
989
1482
  return pickSession({
990
1483
  name: flags.session ?? process.env.AGENT_BRIDGE_SESSION,
991
1484
  metro: explicitMetro(flags.metro),
992
1485
  device: flags.device,
993
- transport: flags.transport
1486
+ transport: flags.transport,
1487
+ skip
994
1488
  });
995
1489
  }
996
1490
  var describeRestore = (restore) => {
@@ -1044,7 +1538,7 @@ async function start(flags) {
1044
1538
  [...process.execArgv, process.argv[1], ...args],
1045
1539
  { detached: true, stdio: "ignore", windowsHide: true }
1046
1540
  );
1047
- const state = await new Promise((resolve2, reject) => {
1541
+ const state = await new Promise((resolve4, reject) => {
1048
1542
  const deadline = Date.now() + 3e4;
1049
1543
  let exited = false;
1050
1544
  child.once("exit", () => {
@@ -1055,13 +1549,13 @@ async function start(flags) {
1055
1549
  const failed = existsSync(files.error);
1056
1550
  if (ready && ready.pid === child.pid) {
1057
1551
  clearInterval(tick);
1058
- resolve2(ready);
1552
+ resolve4(ready);
1059
1553
  } else if (failed || exited || Date.now() > deadline) {
1060
1554
  clearInterval(tick);
1061
1555
  if (!exited) child.kill();
1062
- 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}`;
1063
1557
  rmSync2(files.error, { force: true });
1064
- reject(new Error(reason));
1558
+ reject(new Error(reason2));
1065
1559
  }
1066
1560
  }, 50);
1067
1561
  });
@@ -1177,10 +1671,1675 @@ async function daemonMain(flags) {
1177
1671
  process.exit(0);
1178
1672
  }
1179
1673
 
1674
+ // src/init/index.ts
1675
+ import { spawn as spawn2 } from "child_process";
1676
+ import {
1677
+ appendFileSync as appendFileSync3,
1678
+ existsSync as existsSync2,
1679
+ mkdirSync as mkdirSync2,
1680
+ readdirSync,
1681
+ readFileSync as readFileSync4,
1682
+ writeFileSync as writeFileSync2
1683
+ } from "fs";
1684
+ import { dirname as dirname6, relative as relative6, resolve as resolve2, sep as sep2 } from "path";
1685
+ import { createInterface as createInterface4 } from "readline/promises";
1686
+
1687
+ // package.json
1688
+ var version = "0.6.0";
1689
+
1690
+ // src/init/detect.ts
1691
+ import { dirname as dirname4, join as join6, relative as relative3 } from "path";
1692
+
1693
+ // src/init/package-manager.ts
1694
+ import { dirname as dirname3, join as join5 } from "path";
1695
+ var LOCKFILES = [
1696
+ ["bun.lock", "bun"],
1697
+ ["bun.lockb", "bun"],
1698
+ ["pnpm-lock.yaml", "pnpm"],
1699
+ ["yarn.lock", "yarn"],
1700
+ ["package-lock.json", "npm"]
1701
+ ];
1702
+ var fromName = (value) => {
1703
+ const name = typeof value === "string" ? value.split("@")[0] : void 0;
1704
+ return name === "bun" || name === "pnpm" || name === "yarn" || name === "npm" ? name : void 0;
1705
+ };
1706
+ function detectPackageManager(dir, root, fs, userAgent = "") {
1707
+ for (const d of [dir, root]) {
1708
+ const pm = fromName(fs.readJson(join5(d, "package.json"))?.packageManager);
1709
+ if (pm) return pm;
1710
+ }
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;
1714
+ }
1715
+ return fromName(userAgent.split("/")[0]) ?? "npm";
1716
+ }
1717
+ function installCommand(pm, spec) {
1718
+ switch (pm) {
1719
+ case "bun":
1720
+ return ["bun", "add", "-d", spec];
1721
+ case "pnpm":
1722
+ return ["pnpm", "add", "-D", spec];
1723
+ case "yarn":
1724
+ return ["yarn", "add", "-D", spec];
1725
+ case "npm":
1726
+ return ["npm", "install", "-D", spec];
1727
+ }
1728
+ }
1729
+ function binFallback(pm) {
1730
+ switch (pm) {
1731
+ case "bun":
1732
+ return "bunx @avasapp/agent-bridge";
1733
+ case "pnpm":
1734
+ return "pnpm exec agent-bridge";
1735
+ case "yarn":
1736
+ return "yarn agent-bridge";
1737
+ case "npm":
1738
+ return "./node_modules/.bin/agent-bridge";
1739
+ }
1740
+ }
1741
+
1742
+ // src/init/detect.ts
1743
+ var PACKAGE = "@avasapp/agent-bridge";
1744
+ var LIBRARIES = {
1745
+ expo: "expo",
1746
+ expoRouter: "expo-router",
1747
+ reactQuery: "@tanstack/react-query",
1748
+ webview: "react-native-webview",
1749
+ zustand: "zustand",
1750
+ redux: "@reduxjs/toolkit",
1751
+ reduxCore: "redux",
1752
+ jotai: "jotai",
1753
+ mmkv: "react-native-mmkv",
1754
+ asyncStorage: "@react-native-async-storage/async-storage",
1755
+ secureStore: "expo-secure-store",
1756
+ ably: "ably",
1757
+ socketIo: "socket.io-client",
1758
+ reactNavigation: "@react-navigation/native",
1759
+ typescript: "typescript"
1760
+ };
1761
+ var SOURCE_EXTS = ["tsx", "ts", "jsx", "js"];
1762
+ var depsOf = (pkg) => ({
1763
+ ...pkg?.peerDependencies,
1764
+ ...pkg?.devDependencies,
1765
+ ...pkg?.dependencies
1766
+ });
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"));
1769
+ function findUp(dir, fs, test) {
1770
+ for (let d = dir; ; d = dirname4(d)) {
1771
+ if (test(d)) return d;
1772
+ if (fs.exists(join6(d, ".git")) || dirname4(d) === d) return void 0;
1773
+ }
1774
+ }
1775
+ function workspacePatterns(root, fs) {
1776
+ const ws = fs.readJson(join6(root, "package.json"))?.workspaces;
1777
+ const fromPkg = Array.isArray(ws) ? ws : ws?.packages ?? [];
1778
+ const yaml = fs.read(join6(root, "pnpm-workspace.yaml")) ?? "";
1779
+ const block = yaml.match(/^packages:\s*\n((?:[ \t]*(?:-.*|#.*)?\n?)*)/m)?.[1] ?? "";
1780
+ const fromYaml = [...block.matchAll(/^[ \t]*-[ \t]*['"]?([^'"#\n]+?)['"]?[ \t]*$/gm)].map(
1781
+ (m) => m[1]
1782
+ );
1783
+ return [...fromPkg, ...fromYaml].filter(
1784
+ (p) => typeof p === "string" && !p.startsWith("!")
1785
+ );
1786
+ }
1787
+ function expandWorkspaces(root, patterns, fs) {
1788
+ const found = /* @__PURE__ */ new Set();
1789
+ const walk2 = (dir, segments, depth) => {
1790
+ if (!segments.length) {
1791
+ if (fs.exists(join6(dir, "package.json"))) found.add(dir);
1792
+ return;
1793
+ }
1794
+ const [head, ...rest] = segments;
1795
+ const children = () => fs.list(dir).filter((e) => e.dir && e.name !== "node_modules" && !e.name.startsWith("."));
1796
+ if (head === "**") {
1797
+ walk2(dir, rest, depth);
1798
+ if (depth < 4)
1799
+ for (const c of children()) walk2(join6(dir, c.name), segments, depth + 1);
1800
+ } else if (head.includes("*")) {
1801
+ const re = new RegExp(
1802
+ `^${head.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`
1803
+ );
1804
+ for (const c of children())
1805
+ if (re.test(c.name)) walk2(join6(dir, c.name), rest, depth);
1806
+ } else {
1807
+ walk2(join6(dir, head), rest, depth);
1808
+ }
1809
+ };
1810
+ for (const p of patterns)
1811
+ walk2(root, p.replace(/\/+$/, "").split("/").filter((s) => s && s !== "."), 0);
1812
+ return [...found].sort();
1813
+ }
1814
+ var firstExisting = (dir, fs, candidates) => candidates.find((c) => fs.exists(join6(dir, c)));
1815
+ var withExts = (base) => SOURCE_EXTS.map((e) => `${base}.${e}`);
1816
+ function detect(dir, fs, env = {}) {
1817
+ const pkg = fs.readJson(join6(dir, "package.json"));
1818
+ if (!pkg) return { error: `No package.json in ${dir}.` };
1819
+ const deps = depsOf(pkg);
1820
+ if (!deps["react-native"]) {
1821
+ if (isWorkspaceRoot(dir, fs)) {
1822
+ const apps = expandWorkspaces(dir, workspacePatterns(dir, fs), fs).filter((d) => isApp(d, fs)).map((d) => relative3(dir, d));
1823
+ return {
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.`,
1825
+ workspaces: apps
1826
+ };
1827
+ }
1828
+ return {
1829
+ error: `${dir} isn't a React Native app: its package.json has no react-native dependency. Pass --dir <app>.`
1830
+ };
1831
+ }
1832
+ const root = findUp(dir, fs, (d) => isWorkspaceRoot(d, fs)) ?? dir;
1833
+ const repoRoot = findUp(dir, fs, (d) => fs.exists(join6(d, ".git"))) ?? root;
1834
+ const has = Object.fromEntries(
1835
+ Object.entries(LIBRARIES).map(([k, dep]) => [k, !!deps[dep]])
1836
+ );
1837
+ const appJson = fs.readJson(join6(dir, "app.json"));
1838
+ const appName = [appJson?.expo?.name, appJson?.name, pkg.name].find(
1839
+ (n) => typeof n === "string" && !!n
1840
+ ) ?? "app";
1841
+ const tsconfig = firstExisting(dir, fs, ["tsconfig.json"]);
1842
+ const rootFile = firstExisting(
1843
+ dir,
1844
+ fs,
1845
+ has.expoRouter ? [...withExts("app/_layout"), ...withExts("src/app/_layout")] : [...withExts("App"), ...withExts("src/App")]
1846
+ );
1847
+ const agentsFile = [.../* @__PURE__ */ new Set([dir, repoRoot])].flatMap((d) => [join6(d, "AGENTS.md"), join6(d, "CLAUDE.md")]).find((f) => fs.exists(f));
1848
+ let bin;
1849
+ for (let d = dir; ; d = dirname4(d)) {
1850
+ const candidate = join6(d, "node_modules", ".bin", "agent-bridge");
1851
+ if (fs.exists(candidate)) {
1852
+ bin = candidate;
1853
+ break;
1854
+ }
1855
+ if (d === repoRoot || dirname4(d) === d) break;
1856
+ }
1857
+ return {
1858
+ dir,
1859
+ root,
1860
+ repoRoot,
1861
+ appName,
1862
+ pm: detectPackageManager(dir, root, fs, env.userAgent),
1863
+ has,
1864
+ installed: !!deps[PACKAGE],
1865
+ typescript: !!tsconfig || has.typescript,
1866
+ srcDir: fs.exists(join6(dir, "src")),
1867
+ rootFile,
1868
+ tsconfig,
1869
+ oxlint: firstExisting(dir, fs, [".oxlintrc.json"]),
1870
+ eslint: firstExisting(dir, fs, [
1871
+ "eslint.config.js",
1872
+ "eslint.config.mjs",
1873
+ "eslint.config.cjs",
1874
+ "eslint.config.ts"
1875
+ ]),
1876
+ agentsFile,
1877
+ bin
1878
+ };
1879
+ }
1880
+
1881
+ // src/init/plan.ts
1882
+ import { dirname as dirname5, isAbsolute, join as join7, relative as relative4, sep } from "path";
1883
+
1884
+ // src/init/jsonc.ts
1885
+ function strip(text2) {
1886
+ let out = "";
1887
+ let i = 0;
1888
+ while (i < text2.length) {
1889
+ const c = text2[i];
1890
+ if (c === '"') {
1891
+ let j = i + 1;
1892
+ while (j < text2.length && text2[j] !== '"') j += text2[j] === "\\" ? 2 : 1;
1893
+ out += text2.slice(i, j + 1);
1894
+ i = j + 1;
1895
+ } else if (c === "/" && text2[i + 1] === "/") {
1896
+ const end = text2.indexOf("\n", i);
1897
+ const j = end === -1 ? text2.length : end;
1898
+ out += " ".repeat(j - i);
1899
+ i = j;
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, " ");
1904
+ i = j;
1905
+ } else {
1906
+ out += c;
1907
+ i++;
1908
+ }
1909
+ }
1910
+ return out.replace(/,(\s*[}\]])/g, " $1");
1911
+ }
1912
+ function parseJsonc(text2) {
1913
+ try {
1914
+ return JSON.parse(strip(text2));
1915
+ } catch {
1916
+ return void 0;
1917
+ }
1918
+ }
1919
+ function addToArray(text2, key, value, initial = []) {
1920
+ const json = parseJsonc(text2);
1921
+ if (!json || typeof json !== "object" || Array.isArray(json)) return void 0;
1922
+ const current = json[key];
1923
+ const clean = strip(text2);
1924
+ const item = JSON.stringify(value);
1925
+ if (Array.isArray(current)) {
1926
+ const matches = [...clean.matchAll(new RegExp(`"${key}"\\s*:\\s*\\[`, "g"))];
1927
+ if (matches.length !== 1) return void 0;
1928
+ const at = matches[0].index + matches[0][0].length;
1929
+ const after = text2.slice(at);
1930
+ const multiline = after.match(/^[ \t]*\n([ \t]*)/);
1931
+ if (!current.length) return `${text2.slice(0, at)}${item}${after}`;
1932
+ if (multiline) return `${text2.slice(0, at)}
1933
+ ${multiline[1]}${item},${after}`;
1934
+ return `${text2.slice(0, at)}${item}, ${after}`;
1935
+ }
1936
+ if (current !== void 0) return void 0;
1937
+ const end = clean.slice(0, clean.lastIndexOf("}")).trimEnd().length;
1938
+ const indent = text2.match(/^([ \t]+)"/m)?.[1] ?? " ";
1939
+ const comma = clean.slice(0, end).endsWith("{") ? "" : ",";
1940
+ const list2 = [...initial, value].map((v) => JSON.stringify(v)).join(", ");
1941
+ return `${text2.slice(0, end)}${comma}
1942
+ ${indent}"${key}": [${list2}]${text2.slice(end)}`;
1943
+ }
1944
+
1945
+ // src/init/render.ts
1946
+ import { createHash } from "crypto";
1947
+ var DOCS = "https://github.com/avas-app/agent-bridge/blob/main/docs";
1948
+ var FLOW_GLOB = "**/*.flow.*";
1949
+ var REALTIME = {
1950
+ "socket.io": { fn: "socketIoTools", from: "@avasapp/agent-bridge/socket.io", client: "socket" },
1951
+ ably: { fn: "ablyTools", from: "@avasapp/agent-bridge/ably", client: "ably" }
1952
+ };
1953
+ var STORES_WHY = "stores often hold tokens: add `redact` for those paths before wiring them";
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";
1959
+ var REALTIME_WHY = "the adapter must wrap the client where it is created, before the app subscribes";
1960
+ var NETWORK_PROMPT = "Wire network tools? In dev builds they patch fetch and XMLHttpRequest so the agent can log and mock requests.";
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";
1962
+ var storeKey = (name) => {
1963
+ const bare = name.replace(/^use/, "").replace(/Store$/, "") || name;
1964
+ return bare[0].toLowerCase() + bare.slice(1);
1965
+ };
1966
+ function realtimeLine(todo) {
1967
+ const r = REALTIME[todo.kind];
1968
+ return `export const realtimeDevTools = ${r.fn}(${todo.name ?? r.client})`;
1969
+ }
1970
+ function realtimeTodoText(todo) {
1971
+ const r = REALTIME[todo.kind];
1972
+ const where = todo.file ? `In ${todo.file}, right after \`${todo.name}\` is created` : `Where your ${todo.kind} client is created`;
1973
+ return `${where}, add \`${realtimeLine(todo)}\` (import { ${r.fn} } from '${r.from}'), then spread realtimeDevTools into tools`;
1974
+ }
1975
+ function renderBridgeFile(o) {
1976
+ const L = [];
1977
+ const core = ["cdpTransport", ...o.ts ? ["type Scenarios"] : [], "useAgentBridge"];
1978
+ L.push(
1979
+ `// Generated by agent-bridge init ${o.version}. Yours to edit: once changed, init never overwrites it.`,
1980
+ `import { ${core.join(", ")} } from '@avasapp/agent-bridge'`
1981
+ );
1982
+ if (o.expo) L.push(`import { expoTransport } from '@avasapp/agent-bridge/expo'`);
1983
+ if (o.expoRouter) L.push(`import { routerTools } from '@avasapp/agent-bridge/expo-router'`);
1984
+ if (o.network)
1985
+ L.push(`import { networkTools, strictNetwork } from '@avasapp/agent-bridge/network'`);
1986
+ if (o.webview)
1987
+ L.push(`import { webviewTools } from '@avasapp/agent-bridge/react-native-webview'`);
1988
+ if (o.reactQuery) {
1989
+ L.push(
1990
+ `import { queryTools } from '@avasapp/agent-bridge/tanstack-query'`,
1991
+ `import { QueryClientContext } from '@tanstack/react-query'`
1992
+ );
1993
+ }
1994
+ if (o.expoRouter) L.push(`import { router, useNavigationContainerRef } from 'expo-router'`);
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
+ );
2003
+ if (o.realtimeTools?.length)
2004
+ L.push("", ...o.realtimeTools.map((t) => `import { ${t.name} } from '${t.from}'`));
2005
+ if (o.stores) {
2006
+ const stores = o.stores.length ? o.stores : [{ name: "useAuthStore", from: "../stores/auth" }];
2007
+ L.push(
2008
+ "",
2009
+ "// Zustand stores are left out: they often hold tokens. Add `redact` for those",
2010
+ "// paths, then uncomment these and storeTools below. See",
2011
+ `// ${DOCS}/adapters.md`,
2012
+ `// import { storeTools } from '@avasapp/agent-bridge/zustand'`,
2013
+ ...stores.map((s) => `// import { ${s.name} } from '${s.from}'`)
2014
+ );
2015
+ }
2016
+ if (o.mmkv) {
2017
+ const mmkv = o.mmkv.length ? o.mmkv : [{ name: "storage", from: "../storage" }];
2018
+ L.push(
2019
+ "",
2020
+ "// MMKV is left out: it often holds tokens, and mmkv.get returns values as they",
2021
+ "// are. Wire only instances without secrets: uncomment these and mmkvTools below.",
2022
+ `// See ${DOCS}/adapters.md`,
2023
+ `// import { mmkvTools } from '@avasapp/agent-bridge/react-native-mmkv'`,
2024
+ ...mmkv.map((m) => `// import { ${m.name} } from '${m.from}'`)
2025
+ );
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
+ );
2069
+ for (const todo of o.realtime) {
2070
+ const r = REALTIME[todo.kind];
2071
+ L.push(
2072
+ "",
2073
+ `// TODO ${todo.kind}: the adapter must wrap the client where it is created,`,
2074
+ `// before the app subscribes (${DOCS}/realtime.md).`,
2075
+ todo.file ? `// In ${todo.file}, right after \`${todo.name}\` is created, add:` : `// Where your ${todo.kind} client is created, add:`,
2076
+ `// import { ${r.fn} } from '${r.from}'`,
2077
+ `// ${realtimeLine(todo)}`,
2078
+ `// then import realtimeDevTools here and uncomment it in tools.`
2079
+ );
2080
+ }
2081
+ L.push("");
2082
+ if (o.ts) L.push("const scenarios: Scenarios = {");
2083
+ else L.push(`/** @type {import('@avasapp/agent-bridge').Scenarios} */`, "const scenarios = {");
2084
+ if (o.network) {
2085
+ L.push(
2086
+ " offline: {",
2087
+ ` description: 'Every request fails like a network failure; Metro still gets through.',`,
2088
+ " apply: ({ onUndo }) => {",
2089
+ " onUndo(strictNetwork({ offline: true }))",
2090
+ " },",
2091
+ " },"
2092
+ );
2093
+ }
2094
+ L.push(
2095
+ " // A user signed in locally with every request mocked: the state most flows",
2096
+ ` // start from. See ${DOCS}/scenarios.md`,
2097
+ " // signedIn: {",
2098
+ ` // description: 'Signed in locally; no request reaches a server.',`,
2099
+ " // apply: async ({ call, onUndo }) => {",
2100
+ ` // onUndo(strictNetwork()) // and mockApi, both from '@avasapp/agent-bridge/network'`,
2101
+ ` // onUndo(mockApi(API_URL, { 'GET /me': { json: user } }))`,
2102
+ ` // await call('store.set', 'auth', { token: 'local', user })`,
2103
+ " // return { user }",
2104
+ " // },",
2105
+ " // },",
2106
+ "}",
2107
+ "",
2108
+ "export function AgentBridge() {"
2109
+ );
2110
+ if (o.reactQuery)
2111
+ L.push(
2112
+ " // Undefined outside a QueryClientProvider: mount this inside one for query.* tools.",
2113
+ " const queryClient = useContext(QueryClientContext)"
2114
+ );
2115
+ L.push(
2116
+ " useAgentBridge({",
2117
+ ` name: '${o.appName.replace(/[\\']/g, "\\$&")}',`,
2118
+ ` transports: [${o.expo ? "expoTransport(), " : ""}cdpTransport()],`,
2119
+ " tools: {"
2120
+ );
2121
+ if (o.reactQuery) L.push(" ...(queryClient ? queryTools(queryClient) : {}),");
2122
+ if (o.expoRouter)
2123
+ L.push(" ...routerTools(router, { navigation: useNavigationContainerRef() }),");
2124
+ if (o.webview)
2125
+ L.push(
2126
+ ` // Each WebView also needs useWebViewTools (${DOCS}/webviews.md).`,
2127
+ " ...webviewTools(),"
2128
+ );
2129
+ if (o.network) L.push(" ...networkTools(),");
2130
+ for (const t of o.realtimeTools ?? []) L.push(` ...${t.name},`);
2131
+ if (o.stores) {
2132
+ const stores = o.stores.length ? o.stores : [{ name: "useAuthStore", from: "" }];
2133
+ const map = stores.map((s) => `${storeKey(s.name)}: ${s.name}`).join(", ");
2134
+ const first = storeKey(stores[0].name);
2135
+ L.push(` // ...storeTools({ ${map} }, { redact: { ${first}: ['token'] } }),`);
2136
+ }
2137
+ if (o.mmkv) {
2138
+ const mmkv = o.mmkv.length ? o.mmkv : [{ name: "storage", from: "" }];
2139
+ L.push(` // ...mmkvTools({ ${mmkv.map((m) => m.name).join(", ")} }),`);
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 }),");
2145
+ if (o.realtime.length) L.push(" // ...realtimeDevTools,");
2146
+ if (o.reactNavigation)
2147
+ L.push(
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),"
2151
+ );
2152
+ L.push(
2153
+ ` // Your own tools: any function, JSON in and out, e.g. 'cart.clear': () => clearCart(),`,
2154
+ " },",
2155
+ " scenarios,",
2156
+ " })",
2157
+ " return null",
2158
+ "}",
2159
+ ""
2160
+ );
2161
+ return stamp(L.join("\n"));
2162
+ }
2163
+ function renderMountSnippet(importPath2) {
2164
+ return `import { AgentBridge } from '${importPath2}'
2165
+ {__DEV__ ? <AgentBridge /> : null}`;
2166
+ }
2167
+ function renderSmokeFlow(o) {
2168
+ return stamp([
2169
+ `// Generated by agent-bridge init ${o.version}. Run it with:`,
2170
+ `// ${o.bin} run flows/smoke.flow.mjs`,
2171
+ `/** @type {import('@avasapp/agent-bridge/client').FlowModule['default']} */`,
2172
+ "const smoke = async ({ step, expect }) => {",
2173
+ ` await step('snapshot', 'screen.snapshot')`,
2174
+ ...o.expoRouter ? [` await step('open home', 'router.navigate', '/')`] : [],
2175
+ " // TODO: check text your first screen always shows, for example:",
2176
+ ` // await expect.text('Welcome')`,
2177
+ "}",
2178
+ "",
2179
+ "export default smoke",
2180
+ ""
2181
+ ].join("\n"));
2182
+ }
2183
+ function renderFlowsReadme(bin) {
2184
+ return `Flows for \`${bin} run flows/<name>.flow.mjs\`: plain .mjs files named \`*.flow.mjs\`, kept out of \`app/\` and test globs. See ${DOCS}/cli.md#flows-where-they-go.
2185
+ `;
2186
+ }
2187
+ function agentsLine(appDir, bin) {
2188
+ const where = appDir ? `${appDir}/flows` : "flows";
2189
+ const run = `\`${bin} run flows/smoke.flow.mjs\``;
2190
+ return `- Flows for agent-bridge live in \`${where}/<name>.flow.mjs\` (never under \`app/\`, never \`*.test.*\`); run one ${appDir ? `from \`${appDir}\` with ${run}` : `with ${run}`}.`;
2191
+ }
2192
+ var HEADER = /^(\/\/ Generated by agent-bridge init \d+\.\d+\.\d+(?:-[\w.]+)?)( \(#([0-9a-f]{12})\))?\./;
2193
+ var hash = (body) => createHash("sha256").update(body).digest("hex").slice(0, 12);
2194
+ function stamp(content) {
2195
+ const nl = content.indexOf("\n");
2196
+ return content.slice(0, nl).replace(HEADER, `$1 (#${hash(content.slice(nl))}).`) + content.slice(nl);
2197
+ }
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));
2202
+ }
2203
+ var NETWORK_LINES = [
2204
+ `import { networkTools } from '@avasapp/agent-bridge/network'`,
2205
+ "...networkTools(), // in useAgentBridge({ tools })"
2206
+ ];
2207
+ var indentOf = (text2) => text2.match(/^([ \t]+)"/m)?.[1] ?? " ";
2208
+ function patchJson(text2, edit, manual) {
2209
+ let json;
2210
+ try {
2211
+ json = JSON.parse(text2);
2212
+ } catch {
2213
+ return { manual };
2214
+ }
2215
+ if (!json || typeof json !== "object" || Array.isArray(json)) return { manual };
2216
+ if (!edit(json)) return { unchanged: true };
2217
+ return {
2218
+ text: JSON.stringify(json, null, indentOf(text2)) + (text2.endsWith("\n") ? "\n" : "")
2219
+ };
2220
+ }
2221
+ function patchPackageScripts(text2) {
2222
+ return patchJson(
2223
+ text2,
2224
+ (pkg) => {
2225
+ if (pkg.scripts?.flow !== void 0) return false;
2226
+ pkg.scripts = { ...pkg.scripts, flow: "agent-bridge run" };
2227
+ return true;
2228
+ },
2229
+ `Add to package.json scripts: "flow": "agent-bridge run"`
2230
+ );
2231
+ }
2232
+ var globRe = (glob) => new RegExp(
2233
+ `^${glob.replace(/^\.\//, "").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?")}$`
2234
+ );
2235
+ function includesFlows(pattern) {
2236
+ const last = pattern.split("/").pop() ?? "";
2237
+ const glob = last.includes(".") ? pattern : `${pattern.replace(/\/+$/, "")}${last === "*" ? "" : "/**/*"}`;
2238
+ return globRe(glob).test("flows/smoke.flow.mjs");
2239
+ }
2240
+ function patchTsconfig(text2, baseAllowJs) {
2241
+ const manual = `Add "${FLOW_GLOB}" to "exclude" in tsconfig.json`;
2242
+ const json = parseJsonc(text2);
2243
+ if (!json || typeof json !== "object") return { manual };
2244
+ if (Array.isArray(json.exclude) && json.exclude.includes(FLOW_GLOB)) return { unchanged: true };
2245
+ const allowJs = json.compilerOptions?.allowJs ?? baseAllowJs ?? true;
2246
+ const include = Array.isArray(json.include) ? json.include : json.files ? [] : ["**/*"];
2247
+ if (!allowJs || !include.some((p) => typeof p === "string" && includesFlows(p)))
2248
+ return { unchanged: true, note: "doesn't include .mjs files" };
2249
+ if (json.extends && !Array.isArray(json.exclude))
2250
+ return {
2251
+ manual: `${manual}, along with what it inherits from ${JSON.stringify(json.extends)}: an "exclude" here replaces the inherited one`
2252
+ };
2253
+ const patched = addToArray(text2, "exclude", FLOW_GLOB, ["node_modules"]);
2254
+ return patched ? { text: patched } : { manual };
2255
+ }
2256
+ function patchOxlint(text2) {
2257
+ const ignore = parseJsonc(text2)?.ignorePatterns;
2258
+ if (Array.isArray(ignore) && ignore.includes(FLOW_GLOB)) return { unchanged: true };
2259
+ const patched = addToArray(text2, "ignorePatterns", FLOW_GLOB);
2260
+ return patched ? { text: patched } : { manual: `Add "${FLOW_GLOB}" to "ignorePatterns" in .oxlintrc.json` };
2261
+ }
2262
+ var ESLINT_SNIPPET = `{ ignores: ['${FLOW_GLOB}'] },`;
2263
+ function patchEslint(text2) {
2264
+ if (text2.includes(FLOW_GLOB)) return { unchanged: true };
2265
+ const manual = `Add to the start of your ESLint flat config array: ${ESLINT_SNIPPET}`;
2266
+ const re = /^((?:module\.exports\s*=|export\s+default)\s*(?:defineConfig\(\s*)?\[)/gm;
2267
+ const matches = [...text2.matchAll(re)];
2268
+ if (matches.length !== 1) return { manual };
2269
+ const at = matches[0].index + matches[0][1].length;
2270
+ return { text: `${text2.slice(0, at)}
2271
+ ${ESLINT_SNIPPET}${text2.slice(at)}` };
2272
+ }
2273
+
2274
+ // src/init/plan.ts
2275
+ var SKILL_COMMAND = ["npx", "skills", "add", "avas-app/agent-bridge"];
2276
+ var SKILL_RUN = ["npx", "--yes", "skills", "add", "avas-app/agent-bridge"];
2277
+ var PLUGIN_LINES = [
2278
+ "/plugin marketplace add avas-app/agent-bridge",
2279
+ "/plugin install agent-bridge@agent-bridge"
2280
+ ];
2281
+ var MARKERS = [
2282
+ ["expo", /expoTransport/, "expo"],
2283
+ ["expoRouter", /routerTools/, "expo-router"],
2284
+ ["reactQuery", /queryTools/, "@tanstack/react-query"],
2285
+ ["webview", /webviewTools/, "react-native-webview"],
2286
+ ["zustand", /storeTools/, "zustand"],
2287
+ ["redux", /reduxTools/, "@reduxjs/toolkit"],
2288
+ ["reduxCore", /reduxTools/, "redux"],
2289
+ ["jotai", /jotaiTools/, "jotai"],
2290
+ ["mmkv", /mmkvTools/, "react-native-mmkv"],
2291
+ ["asyncStorage", /asyncStorageTools/, "@react-native-async-storage/async-storage"],
2292
+ ["secureStore", /secureStoreTools/, "expo-secure-store"],
2293
+ ["socketIo", /realtimeDevTools|socketIoTools|realtime/i, "socket.io-client"],
2294
+ ["ably", /realtimeDevTools|ablyTools|realtime/i, "ably"]
2295
+ ];
2296
+ var posix2 = (p) => p.split(sep).join("/");
2297
+ function importPath(fromFile, toFile) {
2298
+ const rel = posix2(relative4(dirname5(fromFile), toFile)).replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
2299
+ return rel.startsWith(".") ? rel : `./${rel}`;
2300
+ }
2301
+ function binFor(d) {
2302
+ if (d.bin) {
2303
+ const rel = posix2(relative4(d.dir, d.bin));
2304
+ return rel.startsWith(".") ? rel : `./${rel}`;
2305
+ }
2306
+ if (d.pm === "npm" && d.root !== d.dir)
2307
+ return `${posix2(relative4(d.dir, d.root))}/node_modules/.bin/agent-bridge`;
2308
+ return binFallback(d.pm);
2309
+ }
2310
+ function bridgePath(d, out) {
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"}`);
2313
+ }
2314
+ function nextSteps(bin, o) {
2315
+ const { offline, runFrom } = o;
2316
+ const next = [];
2317
+ if (!o.mounted)
2318
+ next.push({
2319
+ step: o.elsewhere ? "Make sure the root component renders the component that calls useAgentBridge" : "Mount it: paste the snippet above"
2320
+ });
2321
+ next.push(
2322
+ { step: "Reload the app (press r in Metro)" },
2323
+ { step: "Start a session", command: `${bin} session start` },
2324
+ { step: "List the tools", command: `${bin} tools` },
2325
+ { step: "See what's on screen", command: `${bin} call screen.snapshot` }
2326
+ );
2327
+ if (offline)
2328
+ next.push({ step: "Try a scenario", command: `${bin} call scenario.apply '"offline"'` });
2329
+ next.push({ step: "Undo everything and disconnect", command: `${bin} session stop` });
2330
+ const from = runFrom === "." ? "" : `From \`${runFrom}\`: `;
2331
+ return {
2332
+ next,
2333
+ prompt: `This app has agent-bridge. ${from ? `${from}s` : "S"}tart a session with \`${bin} session start\`, see the screen with \`${bin} call screen.snapshot\`, and check changes through the bridge instead of tapping; stop the session when done.`
2334
+ };
2335
+ }
2336
+ var stripExt = (p) => p.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "").replace(/\/index$/, "");
2337
+ function readTsconfig(d, fs) {
2338
+ if (!d.tsconfig) return { aliases: [] };
2339
+ const file = join7(d.dir, d.tsconfig);
2340
+ const json = parseJsonc(fs.read(file) ?? "");
2341
+ if (!json) return { aliases: [] };
2342
+ const findExtended = (from2, ext2) => {
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);
2346
+ return void 0;
2347
+ };
2348
+ let baseAllowJs;
2349
+ let ext = json.extends;
2350
+ let from = file;
2351
+ for (let depth = 0; typeof ext === "string" && depth < 4; depth++) {
2352
+ const path = findExtended(from, ext);
2353
+ const base = path ? parseJsonc(fs.read(path) ?? "") : void 0;
2354
+ if (!base) break;
2355
+ if (typeof base.compilerOptions?.allowJs === "boolean") {
2356
+ baseAllowJs = base.compilerOptions.allowJs;
2357
+ break;
2358
+ }
2359
+ ext = base.extends;
2360
+ from = path;
2361
+ }
2362
+ const baseDir = join7(d.dir, json.compilerOptions?.baseUrl ?? ".");
2363
+ const paths = json.compilerOptions?.paths ?? {};
2364
+ const aliases = Object.entries(paths).flatMap(
2365
+ ([pattern, targets]) => Array.isArray(targets) && typeof targets[0] === "string" ? [[pattern.replace(/\*$/, ""), join7(baseDir, targets[0].replace(/\*$/, ""))]] : []
2366
+ );
2367
+ return { json, baseAllowJs, aliases };
2368
+ }
2369
+ function ancestors(dir, stop2) {
2370
+ const out = [];
2371
+ for (let d = dir; ; d = dirname5(d)) {
2372
+ out.push(d);
2373
+ if (d === stop2 || dirname5(d) === d) return out;
2374
+ }
2375
+ }
2376
+ function importedFiles(file, text2, aliases) {
2377
+ const specs = [...text2.matchAll(/(?:from\s+|import\s+|require\(\s*)['"]([^'"]+)['"]/g)].map((m) => m[1]);
2378
+ return specs.flatMap((spec) => {
2379
+ if (spec.startsWith(".")) return [stripExt(join7(dirname5(file), spec))];
2380
+ const alias = aliases.find(([prefix]) => prefix && spec.startsWith(prefix));
2381
+ return alias ? [stripExt(join7(alias[1], spec.slice(alias[0].length)))] : [];
2382
+ });
2383
+ }
2384
+ function plan(d, found, fs, o, cwd) {
2385
+ const rel = (p) => posix2(relative4(d.dir, p));
2386
+ const out = bridgePath(d, o.out);
2387
+ if (rel(out).startsWith("..") || isAbsolute(rel(out)))
2388
+ return { error: `--out ${o.out} is outside the app (${d.dir}).` };
2389
+ if (/^(src\/)?app\//.test(rel(out)))
2390
+ return {
2391
+ error: `${rel(out)} is under app/, where expo-router treats every file as a route. Pick another --out.`
2392
+ };
2393
+ const ts = /\.tsx?$/.test(out);
2394
+ const bin = binFor(d);
2395
+ const runFrom = posix2(relative4(cwd, d.dir)) || ".";
2396
+ const tsconfig = readTsconfig(d, fs);
2397
+ const actions = [];
2398
+ const files = [];
2399
+ const commented = [];
2400
+ const todos = [];
2401
+ const generate = (path, content) => {
2402
+ const current = fs.read(path);
2403
+ if (current === void 0) {
2404
+ actions.push({ kind: "write", path, content });
2405
+ files.push({ path: rel(path), status: "written" });
2406
+ return "written";
2407
+ }
2408
+ const pristine = isPristine(current);
2409
+ if (!o.force) {
2410
+ files.push({
2411
+ path: rel(path),
2412
+ status: "skipped",
2413
+ note: pristine ? "exists; --force regenerates it" : "exists, edited since init wrote it"
2414
+ });
2415
+ return pristine ? "kept" : "edited";
2416
+ }
2417
+ if (pristine) {
2418
+ const same = current === content;
2419
+ if (!same) actions.push({ kind: "write", path, content });
2420
+ files.push({ path: rel(path), status: same ? "unchanged" : "written" });
2421
+ return "written";
2422
+ }
2423
+ actions.push({ kind: "write", path: `${path}.new`, content });
2424
+ files.push({
2425
+ path: rel(path),
2426
+ status: "skipped",
2427
+ note: `edited since init wrote it, so --force left it; wrote ${rel(path)}.new to compare`
2428
+ });
2429
+ files.push({ path: `${rel(path)}.new`, status: "written" });
2430
+ return "edited";
2431
+ };
2432
+ const imported = (list2) => list2.map((f) => ({ name: f.name, from: importPath(out, join7(d.dir, f.file)) }));
2433
+ const realtimeTools = imported(found.realtimeTools);
2434
+ const realtime = realtimeTools.length ? [] : [
2435
+ ...d.has.socketIo ? found.socketIo.length ? found.socketIo.map((f) => ({ kind: "socket.io", ...f })) : [{ kind: "socket.io" }] : [],
2436
+ ...d.has.ably ? found.ably.length ? found.ably.map((f) => ({ kind: "ably", ...f })) : [{ kind: "ably" }] : []
2437
+ ];
2438
+ const existingText = fs.read(out);
2439
+ const existing = found.bridgeUsers.filter((f) => stripExt(join7(d.dir, f)) !== stripExt(out));
2440
+ const elsewhere = existingText === void 0 && existing.length > 0 && !o.out;
2441
+ let state = "elsewhere";
2442
+ if (elsewhere) {
2443
+ files.push({
2444
+ path: rel(out),
2445
+ status: "skipped",
2446
+ note: `the app already uses the bridge in ${existing.join(", ")}; pass --out to write one anyway`
2447
+ });
2448
+ } else {
2449
+ state = generate(
2450
+ out,
2451
+ renderBridgeFile({
2452
+ version: o.version,
2453
+ appName: d.appName,
2454
+ ts,
2455
+ expo: d.has.expo,
2456
+ expoRouter: d.has.expoRouter,
2457
+ reactQuery: d.has.reactQuery,
2458
+ webview: d.has.webview,
2459
+ network: o.network.enabled,
2460
+ reactNavigation: d.has.reactNavigation && !d.has.expoRouter,
2461
+ stores: d.has.zustand ? imported(found.stores) : void 0,
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,
2467
+ realtime,
2468
+ realtimeTools
2469
+ })
2470
+ );
2471
+ }
2472
+ const wrote = state === "written";
2473
+ const liveText = wrote ? void 0 : elsewhere ? existing.map((f) => fs.read(join7(d.dir, f)) ?? "").join("\n") : existingText;
2474
+ const network = wrote ? o.network.enabled : /networkTools\s*\(/.test(liveText ?? "");
2475
+ const offline = wrote ? o.network.enabled : /\boffline\s*:/.test(liveText ?? "");
2476
+ if (wrote) {
2477
+ if (d.has.zustand)
2478
+ commented.push({
2479
+ adapter: "zustand",
2480
+ items: found.stores.map((s) => `${s.name} (${s.file})`),
2481
+ why: `${STORES_WHY} (${DOCS}/adapters.md)`
2482
+ });
2483
+ if (d.has.mmkv)
2484
+ commented.push({
2485
+ adapter: "react-native-mmkv",
2486
+ items: found.mmkv.map((s) => `${s.name} (${s.file})`),
2487
+ why: `${MMKV_WHY} (${DOCS}/adapters.md)`
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
+ });
2513
+ if (d.has.reactNavigation && !d.has.expoRouter)
2514
+ commented.push({
2515
+ adapter: "@react-navigation/native",
2516
+ items: ["navTools(navigationRef)"],
2517
+ why: `navTools needs the container ref you pass to <NavigationContainer> (${DOCS}/adapters.md)`
2518
+ });
2519
+ for (const t of realtime) todos.push(`${realtimeTodoText(t)}: ${REALTIME_WHY} (${DOCS}/realtime.md).`);
2520
+ }
2521
+ const missing = !wrote && liveText !== void 0 ? MARKERS.filter(([lib, marker]) => d.has[lib] && !marker.test(liveText)).map(([, , dep]) => dep) : [];
2522
+ const flows = join7(d.dir, "flows");
2523
+ generate(
2524
+ join7(flows, "smoke.flow.mjs"),
2525
+ renderSmokeFlow({ version: o.version, expoRouter: d.has.expoRouter, bin })
2526
+ );
2527
+ const readme = join7(flows, "README.md");
2528
+ if (fs.exists(readme)) files.push({ path: rel(readme), status: "skipped", note: "exists" });
2529
+ else {
2530
+ actions.push({ kind: "write", path: readme, content: renderFlowsReadme(bin) });
2531
+ files.push({ path: rel(readme), status: "written" });
2532
+ }
2533
+ const patch = (path, fn) => {
2534
+ if (!path) return;
2535
+ const abs = join7(d.dir, path);
2536
+ const result = fn(fs.read(abs) ?? "");
2537
+ if ("text" in result) {
2538
+ actions.push({ kind: "patch", path: abs, patch: fn });
2539
+ files.push({ path, status: "patched" });
2540
+ } else if ("manual" in result) {
2541
+ files.push({ path, status: "manual", note: result.manual });
2542
+ } else {
2543
+ files.push({ path, status: "unchanged", ...result.note && { note: result.note } });
2544
+ }
2545
+ };
2546
+ patch("package.json", patchPackageScripts);
2547
+ patch(d.tsconfig, (text2) => patchTsconfig(text2, tsconfig.baseAllowJs));
2548
+ patch(d.oxlint, patchOxlint);
2549
+ patch(d.eslint, patchEslint);
2550
+ if (d.agentsFile) {
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" });
2554
+ else {
2555
+ const appDir = posix2(relative4(dirname5(d.agentsFile), d.dir));
2556
+ actions.push({ kind: "append", path: d.agentsFile, line: agentsLine(appDir, bin) });
2557
+ files.push({ path, status: "appended" });
2558
+ }
2559
+ }
2560
+ const spec = `${PACKAGE}@^${o.version}`;
2561
+ const install = installCommand(d.pm, spec);
2562
+ if (!d.installed && o.install) actions.unshift({ kind: "install", command: install, cwd: d.dir });
2563
+ const skill = o.skillYes ? [...SKILL_RUN, "-y"] : SKILL_RUN;
2564
+ if (o.skill) actions.push({ kind: "skill", command: skill, cwd: d.repoRoot });
2565
+ const rootFile = d.rootFile ? join7(d.dir, d.rootFile) : void 0;
2566
+ const rootImports = rootFile ? importedFiles(rootFile, fs.read(rootFile) ?? "", tsconfig.aliases) : [];
2567
+ const reachedFromRoot = (file) => stripExt(file) === stripExt(rootFile ?? "") || rootImports.includes(stripExt(file));
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));
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`;
2571
+ const note = `${where}${d.has.reactQuery && !elsewhere ? ", inside QueryClientProvider so query.* tools work" : ""}.`;
2572
+ const regenerable = state === "written" || state === "kept";
2573
+ const ignored = o.network.chosen && !wrote && o.network.enabled !== network ? `${o.network.enabled ? "--network" : "--no-network"} had no effect: ${elsewhere ? existing.join(", ") : rel(out)} already exists${regenerable ? "; add --force to regenerate it" : "; edit it by hand"}.` : void 0;
2574
+ const choice = {
2575
+ enabled: network,
2576
+ open: !network && !(o.network.chosen && wrote),
2577
+ why: NETWORK_WHY,
2578
+ ...regenerable && { flag: network ? "--no-network --force" : "--network --force" },
2579
+ ...!network && { lines: NETWORK_LINES },
2580
+ ...ignored && { note: ignored }
2581
+ };
2582
+ const detected = MARKERS.filter(([lib]) => d.has[lib]).map(([, , dep]) => dep);
2583
+ if (d.has.reactNavigation && !d.has.expoRouter) detected.push("@react-navigation/native");
2584
+ return {
2585
+ actions,
2586
+ report: {
2587
+ ok: true,
2588
+ version: o.version,
2589
+ dryRun: false,
2590
+ app: { dir: d.dir, name: d.appName, packageManager: d.pm, detected },
2591
+ files,
2592
+ install: {
2593
+ status: d.installed ? "present" : "not-run",
2594
+ command: install.join(" ")
2595
+ },
2596
+ skill: { status: "not-run", command: SKILL_COMMAND.join(" "), plugin: PLUGIN_LINES },
2597
+ mount: { file: d.rootFile ?? null, mounted, snippet, note },
2598
+ choices: { network: choice },
2599
+ commented,
2600
+ todos,
2601
+ missing,
2602
+ existing,
2603
+ runFrom,
2604
+ scenarios: offline ? ["offline"] : [],
2605
+ ...nextSteps(bin, { mounted, offline, runFrom, elsewhere })
2606
+ }
2607
+ };
2608
+ }
2609
+
2610
+ // src/init/scan.ts
2611
+ import { join as join8, relative as relative5 } from "path";
2612
+ var CODE = /\.(tsx?|jsx?|mjs|cjs)$/;
2613
+ var TESTS = /\.(test|spec)\.|(^|\/)__tests__\//;
2614
+ var imports = (text2, module) => new RegExp(
2615
+ `(from\\s+|require\\(\\s*)['"]${module.replace(/[.]/g, "\\.")}(/[\\w./-]+)?['"]`
2616
+ ).test(text2);
2617
+ var TYPED = "(?:\\s*:[^=]+)?";
2618
+ var PATTERNS = [
2619
+ ["stores", "zustand", new RegExp(`export\\s+const\\s+(use\\w+)${TYPED}\\s*=\\s*create\\w*\\s*[<(]`, "g")],
2620
+ ["mmkv", "react-native-mmkv", new RegExp(`export\\s+const\\s+(\\w+)${TYPED}\\s*=\\s*(?:new\\s+MMKV|createMMKV)\\s*[<(]`, "g")],
2621
+ ["socketIo", "socket.io-client", new RegExp(`(?:const|let)\\s+(\\w+)${TYPED}\\s*=\\s*io\\s*\\(`, "g")],
2622
+ ["ably", "ably", new RegExp(`(?:const|let)\\s+(\\w+)${TYPED}\\s*=\\s*new\\s+(?:Ably\\.)?Realtime(?:\\.Promise)?\\s*\\(`, "g")],
2623
+ ["realtimeTools", "@avasapp/agent-bridge", /export\s+const\s+(\w+)\s*=\s*(?:socketIoTools|ablyTools)\s*\(/g]
2624
+ ];
2625
+ function scan(dir, fs, { maxFiles = 2e3, maxBytes = 512 * 1024 } = {}) {
2626
+ const files = [];
2627
+ const walk2 = (d, deep) => {
2628
+ const entries = fs.list(d).sort((a, b) => a.name < b.name ? -1 : 1);
2629
+ for (const e of entries) {
2630
+ if (files.length >= maxFiles) return;
2631
+ if (e.name.startsWith(".") || e.name === "node_modules") continue;
2632
+ const path = join8(d, e.name);
2633
+ if (e.dir) {
2634
+ if (deep) walk2(path, true);
2635
+ } else if (CODE.test(e.name) && !TESTS.test(relative5(dir, path))) {
2636
+ files.push(path);
2637
+ }
2638
+ }
2639
+ };
2640
+ walk2(dir, false);
2641
+ for (const sub of ["src", "app", "lib"]) walk2(join8(dir, sub), true);
2642
+ const result = { stores: [], mmkv: [], socketIo: [], ably: [], realtimeTools: [], bridgeUsers: [] };
2643
+ for (const path of files) {
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));
2648
+ for (const [key, module, re] of PATTERNS) {
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) });
2652
+ }
2653
+ }
2654
+ return result;
2655
+ }
2656
+
2657
+ // src/init/index.ts
2658
+ var INSTALL_TIMEOUT_MS = 5 * 6e4;
2659
+ var SKILL_TIMEOUT_MS = 2 * 6e4;
2660
+ var posix3 = (p) => p.split(sep2).join("/");
2661
+ var nodeFs = {
2662
+ exists: existsSync2,
2663
+ read: (path) => {
2664
+ try {
2665
+ return readFileSync4(path, "utf8");
2666
+ } catch {
2667
+ return void 0;
2668
+ }
2669
+ },
2670
+ readJson: (path) => {
2671
+ try {
2672
+ return JSON.parse(readFileSync4(path, "utf8"));
2673
+ } catch {
2674
+ return void 0;
2675
+ }
2676
+ },
2677
+ list: (dir) => {
2678
+ try {
2679
+ return readdirSync(dir, { withFileTypes: true }).map((e) => ({
2680
+ name: e.name,
2681
+ dir: e.isDirectory()
2682
+ }));
2683
+ } catch {
2684
+ return [];
2685
+ }
2686
+ }
2687
+ };
2688
+ function nodeIo() {
2689
+ return {
2690
+ fs: nodeFs,
2691
+ write: (path, content) => {
2692
+ mkdirSync2(dirname6(path), { recursive: true });
2693
+ writeFileSync2(path, content);
2694
+ },
2695
+ append: (path, text2) => appendFileSync3(path, text2),
2696
+ exec: (command, cwd, interactive, timeoutMs) => new Promise((done) => {
2697
+ const [cmd, ...args] = command;
2698
+ const child = spawn2(cmd, args, {
2699
+ cwd,
2700
+ stdio: [interactive ? "inherit" : "ignore", 2, 2],
2701
+ shell: process.platform === "win32"
2702
+ });
2703
+ const timer = setTimeout(() => {
2704
+ child.kill("SIGTERM");
2705
+ done("timeout");
2706
+ }, timeoutMs);
2707
+ child.on("error", () => {
2708
+ clearTimeout(timer);
2709
+ done(127);
2710
+ });
2711
+ child.on("close", (code) => {
2712
+ clearTimeout(timer);
2713
+ done(code ?? 1);
2714
+ });
2715
+ }),
2716
+ prompt: async (question) => {
2717
+ const rl = createInterface4({ input: process.stdin, output: process.stderr });
2718
+ try {
2719
+ return await rl.question(question);
2720
+ } finally {
2721
+ rl.close();
2722
+ }
2723
+ },
2724
+ print: (text2) => process.stdout.write(`${text2}
2725
+ `),
2726
+ warn: (text2) => process.stderr.write(`${text2}
2727
+ `),
2728
+ interactive: !!process.stdin.isTTY && !!process.stdout.isTTY && !/^(1|true)$/i.test(process.env.CI ?? ""),
2729
+ cwd: process.cwd(),
2730
+ userAgent: process.env.npm_config_user_agent
2731
+ };
2732
+ }
2733
+ async function apply(actions, report, io, interactive) {
2734
+ const why = (code, ms) => code === "timeout" ? `timed out after ${ms / 6e4} min` : `exit code ${code}`;
2735
+ for (const a of actions) {
2736
+ switch (a.kind) {
2737
+ case "install": {
2738
+ const code = await io.exec(a.command, a.cwd, false, INSTALL_TIMEOUT_MS);
2739
+ report.install.status = code === 0 ? "installed" : "failed";
2740
+ if (code !== 0) {
2741
+ report.ok = false;
2742
+ report.install.error = why(code, INSTALL_TIMEOUT_MS);
2743
+ io.warn(`Install failed (${a.command.join(" ")}): ${report.install.error}`);
2744
+ }
2745
+ break;
2746
+ }
2747
+ case "skill": {
2748
+ const code = await io.exec(a.command, a.cwd, interactive, SKILL_TIMEOUT_MS);
2749
+ report.skill.status = code === 0 ? "added" : "failed";
2750
+ if (code !== 0) {
2751
+ report.skill.error = why(code, SKILL_TIMEOUT_MS);
2752
+ io.warn(`Adding the skill failed (${report.skill.error}); add it later with ${report.skill.command}`);
2753
+ }
2754
+ break;
2755
+ }
2756
+ case "write":
2757
+ io.write(a.path, a.content);
2758
+ break;
2759
+ case "patch": {
2760
+ const result = a.patch(io.fs.read(a.path) ?? "");
2761
+ if ("text" in result) io.write(a.path, result.text);
2762
+ break;
2763
+ }
2764
+ case "append": {
2765
+ const text2 = io.fs.read(a.path) ?? "";
2766
+ io.append(a.path, `${text2.endsWith("\n") || !text2 ? "" : "\n"}${a.line}
2767
+ `);
2768
+ break;
2769
+ }
2770
+ }
2771
+ }
2772
+ }
2773
+ var describe = (a, dir) => {
2774
+ const rel = (p) => relative6(dir, p) || ".";
2775
+ switch (a.kind) {
2776
+ case "install":
2777
+ return `install ${a.command.join(" ")}`;
2778
+ case "skill":
2779
+ return `skill ${a.command.join(" ")} (in ${a.cwd})`;
2780
+ case "write":
2781
+ return `write ${rel(a.path)}`;
2782
+ case "patch":
2783
+ return `patch ${rel(a.path)}`;
2784
+ case "append":
2785
+ return `append ${rel(a.path)}: ${a.line}`;
2786
+ }
2787
+ };
2788
+ function formatReport(r, actions) {
2789
+ const L = [];
2790
+ const detected = r.app.detected.length ? ` (${r.app.detected.join(", ")})` : "";
2791
+ L.push(
2792
+ `agent-bridge init ${r.version}${r.dryRun ? " --dry-run: nothing changed" : ""}`,
2793
+ `App "${r.app.name}"${detected}, ${r.app.packageManager}`,
2794
+ ""
2795
+ );
2796
+ if (r.dryRun) {
2797
+ L.push("Would run:", ...actions.map((a) => ` ${describe(a, r.app.dir)}`), "");
2798
+ for (const a of actions)
2799
+ if (a.kind === "write")
2800
+ L.push(`--- ${relative6(r.app.dir, a.path)}`, a.content.trimEnd(), "");
2801
+ }
2802
+ L.push("Files");
2803
+ for (const f of r.files)
2804
+ L.push(` ${f.status.padEnd(9)} ${f.path}${f.note ? `: ${f.note}` : ""}`);
2805
+ L.push(
2806
+ "",
2807
+ `Install: ${r.install.status} (${r.install.command})`,
2808
+ `Skill: ${r.skill.status} (${r.skill.command})`,
2809
+ " In Claude Code, install it as a plugin instead:",
2810
+ ...r.skill.plugin.map((l) => ` ${l}`)
2811
+ );
2812
+ if (r.existing.length)
2813
+ L.push("", `Already uses the bridge: ${r.existing.join(", ")}`);
2814
+ if (r.missing.length)
2815
+ L.push("", `Detected but not wired in the bridge file: ${r.missing.join(", ")} (see ${DOCS}/adapters.md)`);
2816
+ if (r.commented.length) {
2817
+ L.push("", "Left as comments in the bridge file:");
2818
+ for (const c of r.commented)
2819
+ L.push(` ${c.adapter}${c.items.length ? `: ${c.items.join(", ")}` : ""}. Why: ${c.why}`);
2820
+ }
2821
+ if (r.todos.length) L.push("", "TODO:", ...r.todos.map((t) => ` ${t}`));
2822
+ const n = r.choices.network;
2823
+ L.push("");
2824
+ if (n.enabled) L.push("Network tools: on (net.*).");
2825
+ else {
2826
+ const how = [n.flag && `rerun with ${n.flag}`, n.lines && "add these lines"].filter(Boolean).join(", or ");
2827
+ L.push(`Network tools: off. ${n.open ? "Open choice: " : ""}${n.why}. To wire them, ${how}:`);
2828
+ L.push(...(n.lines ?? []).map((l) => ` ${l}`));
2829
+ }
2830
+ if (n.note) L.push(n.note);
2831
+ L.push(
2832
+ "",
2833
+ r.mount.mounted ? `Mounted: yes (from ${r.mount.file}).` : `Mounted: no. ${r.mount.note}`
2834
+ );
2835
+ if (!r.mount.mounted && !r.existing.length)
2836
+ L.push(...r.mount.snippet.split("\n").map((l) => ` ${l}`));
2837
+ L.push("", `First run${r.runFrom === "." ? "" : ` (from ${r.runFrom})`}:`);
2838
+ r.next.forEach(
2839
+ (s, i) => L.push(` ${i + 1}. ${s.step}${s.command ? `
2840
+ ${s.command}` : ""}`)
2841
+ );
2842
+ L.push("", "Prompt for your agent:", ` ${r.prompt}`);
2843
+ return L.join("\n");
2844
+ }
2845
+ async function runInit(args, io) {
2846
+ const dir = resolve2(io.cwd, args.dir ?? ".");
2847
+ const fail = (error) => {
2848
+ if (args.json) io.print(JSON.stringify({ ok: false, ...error }, null, 2));
2849
+ else io.warn(error.error);
2850
+ return 1;
2851
+ };
2852
+ let d = detect(dir, io.fs, { userAgent: io.userAgent });
2853
+ if ("error" in d) return fail(d);
2854
+ const interactive = io.interactive && !args.json && !args.yes;
2855
+ const options = (network2) => ({
2856
+ version,
2857
+ out: args.out === void 0 ? void 0 : resolve2(io.cwd, args.out),
2858
+ force: args.force,
2859
+ install: args.install !== false,
2860
+ skill: args.skill !== false,
2861
+ network: network2,
2862
+ skillYes: !interactive
2863
+ });
2864
+ const planFor = (det, network2, install2 = true) => plan(det, scan(dir, io.fs), io.fs, { ...options(network2), install: install2 && args.install !== false }, io.cwd);
2865
+ let network = { enabled: args.network ?? false, chosen: args.network !== void 0 };
2866
+ const first = planFor(d, network);
2867
+ if ("error" in first) return fail(first);
2868
+ if (args.dryRun) {
2869
+ const { actions: actions2, report: report2 } = first;
2870
+ report2.dryRun = true;
2871
+ if (args.json) {
2872
+ const contents = Object.fromEntries(
2873
+ actions2.flatMap((a) => a.kind === "write" ? [[relative6(dir, a.path), a.content]] : [])
2874
+ );
2875
+ io.print(JSON.stringify({ ...report2, contents }, null, 2));
2876
+ } else io.print(formatReport(report2, actions2));
2877
+ return 0;
2878
+ }
2879
+ const bridge = posix3(relative6(dir, bridgePath(d, options(network).out)));
2880
+ const writesBridge = first.report.files.some((f) => f.path === bridge && f.status === "written");
2881
+ if (!network.chosen && interactive && writesBridge) {
2882
+ const answer = await io.prompt(`${NETWORK_PROMPT} [y/N] `);
2883
+ network = { enabled: /^y(es)?$/i.test(answer.trim()), chosen: true };
2884
+ }
2885
+ const install = first.actions.find((a) => a.kind === "install");
2886
+ const installReport = first.report.install;
2887
+ if (install) {
2888
+ await apply([install], first.report, io, interactive);
2889
+ const after = detect(dir, io.fs, { userAgent: io.userAgent });
2890
+ if (!("error" in after)) d = after;
2891
+ }
2892
+ const p = planFor(d, network, false);
2893
+ if ("error" in p) return fail(p);
2894
+ const { actions, report } = p;
2895
+ if (install) {
2896
+ report.install = installReport;
2897
+ report.ok = first.report.ok;
2898
+ }
2899
+ await apply(actions, report, io, interactive);
2900
+ io.print(args.json ? JSON.stringify(report, null, 2) : formatReport(report, actions));
2901
+ return report.ok ? 0 : 1;
2902
+ }
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
+
1180
3337
  // src/cli.ts
1181
3338
  var HELP2 = `agent-bridge: drive a running React Native app from an agent
1182
3339
 
1183
3340
  Usage
3341
+ agent-bridge init Set up this app: install, write the dev-only bridge
3342
+ file and a smoke flow, print the line to mount it
1184
3343
  agent-bridge devices Apps connected to Metro
1185
3344
  agent-bridge tools Tools the app exposes
1186
3345
  agent-bridge call <tool> [args] Call a tool. args: a JSON array, or one JSON value
@@ -1191,10 +3350,15 @@ Usage
1191
3350
  agent-bridge call --batch Read \`tool args\` lines from stdin (args may be @file); one JSON line per call, one connection
1192
3351
  agent-bridge repl Interactive prompt (history, tab completion, .help). With
1193
3352
  stdin not a terminal it behaves like call --batch
1194
- agent-bridge run <flow.mjs|.ts> Run a flow: export default async ({ step, call }) => {}
1195
- export const scenario = 'signedIn' applies it first
1196
- 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
1197
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)
1198
3362
  agent-bridge assert-absent <files...> Fail if a release bundle contains the bridge
1199
3363
 
1200
3364
  Sessions: one connection for all of an agent's calls
@@ -1210,15 +3374,33 @@ Options
1210
3374
  --device <text> Pick an app when several are connected
1211
3375
  --transport <name> auto (default), expo or cdp
1212
3376
  --timeout <ms> Per-call timeout (default 10000)
1213
- --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
1214
3378
  (--batch: a directory, one <n>-<tool>.json per call)
3379
+ run: the report file (with --reporter json|junit)
1215
3380
  --stop-on-error call --batch: stop after the first failed call
1216
3381
  --full call: print results over 32 KB instead of a summary
1217
- --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
1218
3389
  --scenario <name> run: also apply this scenario (repeatable). Options as
1219
3390
  JSON after "=": --scenario 'signedIn={"user":{"name":"Ada"}}'
1220
3391
  --session <name> Use this session (env AGENT_BRIDGE_SESSION)
1221
3392
  --no-session Connect directly even if a session is running
3393
+
3394
+ init options
3395
+ --dir <path> The app (default: the current directory)
3396
+ --out <file> Bridge file path, inside the app (default src/dev/agent-bridge.tsx, or dev/)
3397
+ --dry-run Print what it would do and the files; change nothing
3398
+ --force Regenerate files init wrote that are still unedited (edited ones get a .new copy)
3399
+ --no-install Print the install command instead of running it
3400
+ --no-skill Don't run \`npx skills add avas-app/agent-bridge\`
3401
+ --network, --no-network Wire networkTools into a new bridge file (asked in a terminal; off otherwise)
3402
+ --json Machine-readable report on stdout; never prompts
3403
+ --yes, -y Never prompt
1222
3404
  `;
1223
3405
  var failedLogs = (error) => error instanceof AgentBridgeCallError ? error.logs : [];
1224
3406
  async function readStdin() {
@@ -1245,12 +3427,23 @@ async function main() {
1245
3427
  batch: { type: "boolean" },
1246
3428
  "stop-on-error": { type: "boolean" },
1247
3429
  scenario: { type: "string", multiple: true },
3430
+ bail: { type: "boolean" },
3431
+ grep: { type: "string" },
3432
+ reporter: { type: "string" },
1248
3433
  name: { type: "string" },
1249
3434
  idle: { type: "string" },
1250
3435
  keep: { type: "boolean" },
1251
3436
  "dry-run": { type: "boolean" },
1252
3437
  session: { type: "string" },
1253
3438
  "no-session": { type: "boolean" },
3439
+ dir: { type: "string" },
3440
+ force: { type: "boolean" },
3441
+ "no-install": { type: "boolean" },
3442
+ "no-skill": { type: "boolean" },
3443
+ network: { type: "boolean" },
3444
+ "no-network": { type: "boolean" },
3445
+ json: { type: "boolean" },
3446
+ yes: { type: "boolean", short: "y" },
1254
3447
  help: { type: "boolean", short: "h" }
1255
3448
  }
1256
3449
  });
@@ -1290,9 +3483,30 @@ async function main() {
1290
3483
  if (failed) process.exitCode = 1;
1291
3484
  };
1292
3485
  switch (command) {
3486
+ case "init":
3487
+ process.exitCode = await runInit(
3488
+ {
3489
+ dir: values.dir,
3490
+ out: values.out,
3491
+ dryRun: values["dry-run"],
3492
+ force: values.force,
3493
+ install: !values["no-install"],
3494
+ skill: !values["no-skill"],
3495
+ network: values.network ? true : values["no-network"] ? false : void 0,
3496
+ json: values.json,
3497
+ yes: values.yes
3498
+ },
3499
+ nodeIo()
3500
+ );
3501
+ return;
1293
3502
  case "devices": {
1294
3503
  const devices = await listDevices(options);
1295
- if (!devices.length) console.log("No apps connected.");
3504
+ if (!devices.length) {
3505
+ const metro = metroHost(options.metro);
3506
+ await assertListening(metro);
3507
+ console.log("No apps connected.");
3508
+ console.error(await noAppHint(metro));
3509
+ }
1296
3510
  for (const d of devices) {
1297
3511
  const extra = d.tools !== void 0 ? ` ${d.tools} tools ${d.deviceId}` : "";
1298
3512
  console.log(`${d.transport.padEnd(5)} ${d.name}${extra}`);
@@ -1321,12 +3535,12 @@ async function main() {
1321
3535
  (path) => path === "-" ? stdin : readArgFile(path)
1322
3536
  );
1323
3537
  return withBridge(async (bridge) => {
1324
- 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) => {
1325
3539
  for (const e of failedLogs(error)) console.error(logLine(e));
1326
3540
  throw error;
1327
3541
  });
1328
3542
  console.log(
1329
- 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 })
1330
3544
  );
1331
3545
  for (const e of logs) console.error(logLine(e));
1332
3546
  if (notice) console.error(`Warning: ${notice}`);
@@ -1351,18 +3565,65 @@ async function main() {
1351
3565
  });
1352
3566
  });
1353
3567
  case "run": {
1354
- const [file] = rest;
1355
- if (!file) throw new Error("Usage: agent-bridge run <flow file>");
1356
- const flow = await import(pathToFileURL(resolve(file)).href);
1357
- 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 });
1358
3580
  const extra = (values.scenario ?? []).map(parseScenarioFlag);
1359
- return withBridge(async (bridge) => {
1360
- const { errors, restoreErrors } = await runFlow(bridge, flow, {
1361
- scenarios: extra
1362
- });
1363
- if (values.strict && errors || restoreErrors.length)
1364
- 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
1365
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]);
1366
3627
  }
1367
3628
  case "scenarios":
1368
3629
  return withBridge(async (bridge) => {
@@ -1382,6 +3643,12 @@ async function main() {
1382
3643
  console.log(` ${"".padEnd(20)} options: ${JSON.stringify(s.options)}`);
1383
3644
  }
1384
3645
  });
3646
+ case "mcp":
3647
+ return mcpMain(
3648
+ values,
3649
+ options,
3650
+ createRequire(import.meta.url)("../package.json").version
3651
+ );
1385
3652
  case "session":
1386
3653
  return sessionCommand(rest[0], values);
1387
3654
  case DAEMON_COMMAND: