@avasapp/agent-bridge 0.4.2 → 0.5.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.
package/dist/cli.js CHANGED
@@ -2,10 +2,12 @@
2
2
  import {
3
3
  AgentBridgeCallError,
4
4
  RUNTIME_MARKER,
5
+ assertListening,
5
6
  checkName,
6
7
  connect,
7
8
  connectSession,
8
9
  encodeLine,
10
+ explainConnectError,
9
11
  explicitMetro,
10
12
  flowScenarios,
11
13
  formatDuration,
@@ -14,6 +16,7 @@ import {
14
16
  listSessions,
15
17
  logLine,
16
18
  metroHost,
19
+ noAppHint,
17
20
  openConnection,
18
21
  parseDuration,
19
22
  parseScenarioFlag,
@@ -26,11 +29,11 @@ import {
26
29
  sessionRequest,
27
30
  stateDir,
28
31
  writePrivate
29
- } from "./chunk-TA2OLPPT.js";
32
+ } from "./chunk-3QIG7A5W.js";
30
33
 
31
34
  // src/cli.ts
32
35
  import { readFile } from "fs/promises";
33
- import { resolve } from "path";
36
+ import { resolve as resolve2 } from "path";
34
37
  import { pathToFileURL } from "url";
35
38
  import { parseArgs } from "util";
36
39
 
@@ -591,11 +594,11 @@ import { appendFileSync as appendFileSync2, chmodSync, rmSync } from "fs";
591
594
  import { createServer } from "net";
592
595
  import { createInterface as createInterface3 } from "readline";
593
596
  var RELOAD = "app.reload";
594
- var pause = (ms, signal) => new Promise((resolve2) => {
597
+ var pause = (ms, signal) => new Promise((resolve3) => {
595
598
  const done = () => {
596
599
  clearTimeout(timer);
597
600
  signal.removeEventListener("abort", done);
598
- resolve2();
601
+ resolve3();
599
602
  };
600
603
  const timer = setTimeout(done, ms);
601
604
  signal.addEventListener("abort", done, { once: true });
@@ -636,7 +639,9 @@ async function runSessionDaemon(options) {
636
639
  `${c.device.name} already belongs to session "${owner.name}". Use --session ${owner.name}, or stop it first.`
637
640
  );
638
641
  };
639
- let conn = await reach();
642
+ let conn = await reach().catch(async (error) => {
643
+ throw await explainConnectError(error, metro, options.device);
644
+ });
640
645
  claim(conn);
641
646
  const pin = conn.pin;
642
647
  const deviceOf = (c) => ({
@@ -953,11 +958,11 @@ async function runSessionDaemon(options) {
953
958
  });
954
959
  if (process.platform !== "win32") rmSync(files.socket, { force: true });
955
960
  try {
956
- await new Promise((resolve2, reject) => {
961
+ await new Promise((resolve3, reject) => {
957
962
  server.once("error", reject);
958
963
  server.listen(files.socket, () => {
959
964
  server.off("error", reject);
960
- resolve2();
965
+ resolve3();
961
966
  });
962
967
  });
963
968
  } catch (error) {
@@ -1044,7 +1049,7 @@ async function start(flags) {
1044
1049
  [...process.execArgv, process.argv[1], ...args],
1045
1050
  { detached: true, stdio: "ignore", windowsHide: true }
1046
1051
  );
1047
- const state = await new Promise((resolve2, reject) => {
1052
+ const state = await new Promise((resolve3, reject) => {
1048
1053
  const deadline = Date.now() + 3e4;
1049
1054
  let exited = false;
1050
1055
  child.once("exit", () => {
@@ -1055,7 +1060,7 @@ async function start(flags) {
1055
1060
  const failed = existsSync(files.error);
1056
1061
  if (ready && ready.pid === child.pid) {
1057
1062
  clearInterval(tick);
1058
- resolve2(ready);
1063
+ resolve3(ready);
1059
1064
  } else if (failed || exited || Date.now() > deadline) {
1060
1065
  clearInterval(tick);
1061
1066
  if (!exited) child.kill();
@@ -1177,10 +1182,1151 @@ async function daemonMain(flags) {
1177
1182
  process.exit(0);
1178
1183
  }
1179
1184
 
1185
+ // src/init/index.ts
1186
+ import { spawn as spawn2 } from "child_process";
1187
+ import {
1188
+ appendFileSync as appendFileSync3,
1189
+ existsSync as existsSync2,
1190
+ mkdirSync as mkdirSync2,
1191
+ readdirSync,
1192
+ readFileSync as readFileSync4,
1193
+ writeFileSync as writeFileSync2
1194
+ } from "fs";
1195
+ import { dirname as dirname5, relative as relative4, resolve, sep as sep2 } from "path";
1196
+ import { createInterface as createInterface4 } from "readline/promises";
1197
+
1198
+ // package.json
1199
+ var version = "0.5.0";
1200
+
1201
+ // src/init/detect.ts
1202
+ import { dirname as dirname3, join as join4, relative } from "path";
1203
+
1204
+ // src/init/package-manager.ts
1205
+ import { dirname as dirname2, join as join3 } from "path";
1206
+ var LOCKFILES = [
1207
+ ["bun.lock", "bun"],
1208
+ ["bun.lockb", "bun"],
1209
+ ["pnpm-lock.yaml", "pnpm"],
1210
+ ["yarn.lock", "yarn"],
1211
+ ["package-lock.json", "npm"]
1212
+ ];
1213
+ var fromName = (value) => {
1214
+ const name = typeof value === "string" ? value.split("@")[0] : void 0;
1215
+ return name === "bun" || name === "pnpm" || name === "yarn" || name === "npm" ? name : void 0;
1216
+ };
1217
+ function detectPackageManager(dir, root, fs, userAgent = "") {
1218
+ for (const d of [dir, root]) {
1219
+ const pm = fromName(fs.readJson(join3(d, "package.json"))?.packageManager);
1220
+ if (pm) return pm;
1221
+ }
1222
+ for (let d = dir; ; d = dirname2(d)) {
1223
+ for (const [file, pm] of LOCKFILES) if (fs.exists(join3(d, file))) return pm;
1224
+ if (d === root || dirname2(d) === d) break;
1225
+ }
1226
+ return fromName(userAgent.split("/")[0]) ?? "npm";
1227
+ }
1228
+ function installCommand(pm, spec) {
1229
+ switch (pm) {
1230
+ case "bun":
1231
+ return ["bun", "add", "-d", spec];
1232
+ case "pnpm":
1233
+ return ["pnpm", "add", "-D", spec];
1234
+ case "yarn":
1235
+ return ["yarn", "add", "-D", spec];
1236
+ case "npm":
1237
+ return ["npm", "install", "-D", spec];
1238
+ }
1239
+ }
1240
+ function binFallback(pm) {
1241
+ switch (pm) {
1242
+ case "bun":
1243
+ return "bunx @avasapp/agent-bridge";
1244
+ case "pnpm":
1245
+ return "pnpm exec agent-bridge";
1246
+ case "yarn":
1247
+ return "yarn agent-bridge";
1248
+ case "npm":
1249
+ return "./node_modules/.bin/agent-bridge";
1250
+ }
1251
+ }
1252
+
1253
+ // src/init/detect.ts
1254
+ var PACKAGE = "@avasapp/agent-bridge";
1255
+ var LIBRARIES = {
1256
+ expo: "expo",
1257
+ expoRouter: "expo-router",
1258
+ reactQuery: "@tanstack/react-query",
1259
+ webview: "react-native-webview",
1260
+ zustand: "zustand",
1261
+ mmkv: "react-native-mmkv",
1262
+ ably: "ably",
1263
+ socketIo: "socket.io-client",
1264
+ reactNavigation: "@react-navigation/native",
1265
+ typescript: "typescript"
1266
+ };
1267
+ var SOURCE_EXTS = ["tsx", "ts", "jsx", "js"];
1268
+ var depsOf = (pkg) => ({
1269
+ ...pkg?.peerDependencies,
1270
+ ...pkg?.devDependencies,
1271
+ ...pkg?.dependencies
1272
+ });
1273
+ var isApp = (dir, fs) => !!fs.readJson(join4(dir, "package.json"))?.dependencies?.["react-native"] || ["app.json", "app.config.js", "app.config.ts", "ios", "android"].some((f) => fs.exists(join4(dir, f)));
1274
+ var isWorkspaceRoot = (dir, fs) => !!fs.readJson(join4(dir, "package.json"))?.workspaces || fs.exists(join4(dir, "pnpm-workspace.yaml"));
1275
+ function findUp(dir, fs, test) {
1276
+ for (let d = dir; ; d = dirname3(d)) {
1277
+ if (test(d)) return d;
1278
+ if (fs.exists(join4(d, ".git")) || dirname3(d) === d) return void 0;
1279
+ }
1280
+ }
1281
+ function workspacePatterns(root, fs) {
1282
+ const ws = fs.readJson(join4(root, "package.json"))?.workspaces;
1283
+ const fromPkg = Array.isArray(ws) ? ws : ws?.packages ?? [];
1284
+ const yaml = fs.read(join4(root, "pnpm-workspace.yaml")) ?? "";
1285
+ const block = yaml.match(/^packages:\s*\n((?:[ \t]*(?:-.*|#.*)?\n?)*)/m)?.[1] ?? "";
1286
+ const fromYaml = [...block.matchAll(/^[ \t]*-[ \t]*['"]?([^'"#\n]+?)['"]?[ \t]*$/gm)].map(
1287
+ (m) => m[1]
1288
+ );
1289
+ return [...fromPkg, ...fromYaml].filter(
1290
+ (p) => typeof p === "string" && !p.startsWith("!")
1291
+ );
1292
+ }
1293
+ function expandWorkspaces(root, patterns, fs) {
1294
+ const found = /* @__PURE__ */ new Set();
1295
+ const walk = (dir, segments, depth) => {
1296
+ if (!segments.length) {
1297
+ if (fs.exists(join4(dir, "package.json"))) found.add(dir);
1298
+ return;
1299
+ }
1300
+ const [head, ...rest] = segments;
1301
+ const children = () => fs.list(dir).filter((e) => e.dir && e.name !== "node_modules" && !e.name.startsWith("."));
1302
+ if (head === "**") {
1303
+ walk(dir, rest, depth);
1304
+ if (depth < 4)
1305
+ for (const c of children()) walk(join4(dir, c.name), segments, depth + 1);
1306
+ } else if (head.includes("*")) {
1307
+ const re = new RegExp(
1308
+ `^${head.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`
1309
+ );
1310
+ for (const c of children())
1311
+ if (re.test(c.name)) walk(join4(dir, c.name), rest, depth);
1312
+ } else {
1313
+ walk(join4(dir, head), rest, depth);
1314
+ }
1315
+ };
1316
+ for (const p of patterns)
1317
+ walk(root, p.replace(/\/+$/, "").split("/").filter((s) => s && s !== "."), 0);
1318
+ return [...found].sort();
1319
+ }
1320
+ var firstExisting = (dir, fs, candidates) => candidates.find((c) => fs.exists(join4(dir, c)));
1321
+ var withExts = (base) => SOURCE_EXTS.map((e) => `${base}.${e}`);
1322
+ function detect(dir, fs, env = {}) {
1323
+ const pkg = fs.readJson(join4(dir, "package.json"));
1324
+ if (!pkg) return { error: `No package.json in ${dir}.` };
1325
+ const deps = depsOf(pkg);
1326
+ if (!deps["react-native"]) {
1327
+ if (isWorkspaceRoot(dir, fs)) {
1328
+ const apps = expandWorkspaces(dir, workspacePatterns(dir, fs), fs).filter((d) => isApp(d, fs)).map((d) => relative(dir, d));
1329
+ return {
1330
+ error: apps.length ? `${dir} is a monorepo root. Rerun with --dir pointing at the app: ${apps.map((a) => `--dir ${a}`).join(", ")}` : `${dir} is a monorepo root and no workspace package depends on react-native.`,
1331
+ workspaces: apps
1332
+ };
1333
+ }
1334
+ return {
1335
+ error: `${dir} isn't a React Native app: its package.json has no react-native dependency. Pass --dir <app>.`
1336
+ };
1337
+ }
1338
+ const root = findUp(dir, fs, (d) => isWorkspaceRoot(d, fs)) ?? dir;
1339
+ const repoRoot = findUp(dir, fs, (d) => fs.exists(join4(d, ".git"))) ?? root;
1340
+ const has = Object.fromEntries(
1341
+ Object.entries(LIBRARIES).map(([k, dep]) => [k, !!deps[dep]])
1342
+ );
1343
+ const appJson = fs.readJson(join4(dir, "app.json"));
1344
+ const appName = [appJson?.expo?.name, appJson?.name, pkg.name].find(
1345
+ (n) => typeof n === "string" && !!n
1346
+ ) ?? "app";
1347
+ const tsconfig = firstExisting(dir, fs, ["tsconfig.json"]);
1348
+ const rootFile = firstExisting(
1349
+ dir,
1350
+ fs,
1351
+ has.expoRouter ? [...withExts("app/_layout"), ...withExts("src/app/_layout")] : [...withExts("App"), ...withExts("src/App")]
1352
+ );
1353
+ const agentsFile = [.../* @__PURE__ */ new Set([dir, repoRoot])].flatMap((d) => [join4(d, "AGENTS.md"), join4(d, "CLAUDE.md")]).find((f) => fs.exists(f));
1354
+ let bin;
1355
+ for (let d = dir; ; d = dirname3(d)) {
1356
+ const candidate = join4(d, "node_modules", ".bin", "agent-bridge");
1357
+ if (fs.exists(candidate)) {
1358
+ bin = candidate;
1359
+ break;
1360
+ }
1361
+ if (d === repoRoot || dirname3(d) === d) break;
1362
+ }
1363
+ return {
1364
+ dir,
1365
+ root,
1366
+ repoRoot,
1367
+ appName,
1368
+ pm: detectPackageManager(dir, root, fs, env.userAgent),
1369
+ has,
1370
+ installed: !!deps[PACKAGE],
1371
+ typescript: !!tsconfig || has.typescript,
1372
+ srcDir: fs.exists(join4(dir, "src")),
1373
+ rootFile,
1374
+ tsconfig,
1375
+ oxlint: firstExisting(dir, fs, [".oxlintrc.json"]),
1376
+ eslint: firstExisting(dir, fs, [
1377
+ "eslint.config.js",
1378
+ "eslint.config.mjs",
1379
+ "eslint.config.cjs",
1380
+ "eslint.config.ts"
1381
+ ]),
1382
+ agentsFile,
1383
+ bin
1384
+ };
1385
+ }
1386
+
1387
+ // src/init/plan.ts
1388
+ import { dirname as dirname4, isAbsolute, join as join5, relative as relative2, sep } from "path";
1389
+
1390
+ // src/init/jsonc.ts
1391
+ function strip(text) {
1392
+ let out = "";
1393
+ let i = 0;
1394
+ while (i < text.length) {
1395
+ const c = text[i];
1396
+ if (c === '"') {
1397
+ let j = i + 1;
1398
+ while (j < text.length && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
1399
+ out += text.slice(i, j + 1);
1400
+ i = j + 1;
1401
+ } else if (c === "/" && text[i + 1] === "/") {
1402
+ const end = text.indexOf("\n", i);
1403
+ const j = end === -1 ? text.length : end;
1404
+ out += " ".repeat(j - i);
1405
+ i = j;
1406
+ } else if (c === "/" && text[i + 1] === "*") {
1407
+ const end = text.indexOf("*/", i + 2);
1408
+ const j = end === -1 ? text.length : end + 2;
1409
+ out += text.slice(i, j).replace(/[^\n]/g, " ");
1410
+ i = j;
1411
+ } else {
1412
+ out += c;
1413
+ i++;
1414
+ }
1415
+ }
1416
+ return out.replace(/,(\s*[}\]])/g, " $1");
1417
+ }
1418
+ function parseJsonc(text) {
1419
+ try {
1420
+ return JSON.parse(strip(text));
1421
+ } catch {
1422
+ return void 0;
1423
+ }
1424
+ }
1425
+ function addToArray(text, key, value, initial = []) {
1426
+ const json = parseJsonc(text);
1427
+ if (!json || typeof json !== "object" || Array.isArray(json)) return void 0;
1428
+ const current = json[key];
1429
+ const clean = strip(text);
1430
+ const item = JSON.stringify(value);
1431
+ if (Array.isArray(current)) {
1432
+ const matches = [...clean.matchAll(new RegExp(`"${key}"\\s*:\\s*\\[`, "g"))];
1433
+ if (matches.length !== 1) return void 0;
1434
+ const at = matches[0].index + matches[0][0].length;
1435
+ const after = text.slice(at);
1436
+ const multiline = after.match(/^[ \t]*\n([ \t]*)/);
1437
+ if (!current.length) return `${text.slice(0, at)}${item}${after}`;
1438
+ if (multiline) return `${text.slice(0, at)}
1439
+ ${multiline[1]}${item},${after}`;
1440
+ return `${text.slice(0, at)}${item}, ${after}`;
1441
+ }
1442
+ if (current !== void 0) return void 0;
1443
+ const end = clean.slice(0, clean.lastIndexOf("}")).trimEnd().length;
1444
+ const indent = text.match(/^([ \t]+)"/m)?.[1] ?? " ";
1445
+ const comma = clean.slice(0, end).endsWith("{") ? "" : ",";
1446
+ const list2 = [...initial, value].map((v) => JSON.stringify(v)).join(", ");
1447
+ return `${text.slice(0, end)}${comma}
1448
+ ${indent}"${key}": [${list2}]${text.slice(end)}`;
1449
+ }
1450
+
1451
+ // src/init/render.ts
1452
+ import { createHash } from "crypto";
1453
+ var DOCS = "https://github.com/avas-app/agent-bridge/blob/main/docs";
1454
+ var FLOW_GLOB = "**/*.flow.*";
1455
+ var REALTIME = {
1456
+ "socket.io": { fn: "socketIoTools", from: "@avasapp/agent-bridge/socket.io", client: "socket" },
1457
+ ably: { fn: "ablyTools", from: "@avasapp/agent-bridge/ably", client: "ably" }
1458
+ };
1459
+ var STORES_WHY = "stores often hold tokens: add `redact` for those paths before wiring them";
1460
+ var MMKV_WHY = "MMKV often holds tokens and mmkv.get returns values as they are: wire only instances without secrets";
1461
+ var REALTIME_WHY = "the adapter must wrap the client where it is created, before the app subscribes";
1462
+ var NETWORK_PROMPT = "Wire network tools? In dev builds they patch fetch and XMLHttpRequest so the agent can log and mock requests.";
1463
+ 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";
1464
+ var storeKey = (name) => {
1465
+ const bare = name.replace(/^use/, "").replace(/Store$/, "") || name;
1466
+ return bare[0].toLowerCase() + bare.slice(1);
1467
+ };
1468
+ function realtimeLine(todo) {
1469
+ const r = REALTIME[todo.kind];
1470
+ return `export const realtimeDevTools = ${r.fn}(${todo.name ?? r.client})`;
1471
+ }
1472
+ function realtimeTodoText(todo) {
1473
+ const r = REALTIME[todo.kind];
1474
+ const where = todo.file ? `In ${todo.file}, right after \`${todo.name}\` is created` : `Where your ${todo.kind} client is created`;
1475
+ return `${where}, add \`${realtimeLine(todo)}\` (import { ${r.fn} } from '${r.from}'), then spread realtimeDevTools into tools`;
1476
+ }
1477
+ function renderBridgeFile(o) {
1478
+ const L = [];
1479
+ const core = ["cdpTransport", ...o.ts ? ["type Scenarios"] : [], "useAgentBridge"];
1480
+ L.push(
1481
+ `// Generated by agent-bridge init ${o.version}. Yours to edit: once changed, init never overwrites it.`,
1482
+ `import { ${core.join(", ")} } from '@avasapp/agent-bridge'`
1483
+ );
1484
+ if (o.expo) L.push(`import { expoTransport } from '@avasapp/agent-bridge/expo'`);
1485
+ if (o.expoRouter) L.push(`import { routerTools } from '@avasapp/agent-bridge/expo-router'`);
1486
+ if (o.network)
1487
+ L.push(`import { networkTools, strictNetwork } from '@avasapp/agent-bridge/network'`);
1488
+ if (o.webview)
1489
+ L.push(`import { webviewTools } from '@avasapp/agent-bridge/react-native-webview'`);
1490
+ if (o.reactQuery) {
1491
+ L.push(
1492
+ `import { queryTools } from '@avasapp/agent-bridge/tanstack-query'`,
1493
+ `import { QueryClientContext } from '@tanstack/react-query'`
1494
+ );
1495
+ }
1496
+ if (o.expoRouter) L.push(`import { router, useNavigationContainerRef } from 'expo-router'`);
1497
+ if (o.reactQuery) L.push(`import { useContext } from 'react'`);
1498
+ if (o.realtimeTools?.length)
1499
+ L.push("", ...o.realtimeTools.map((t) => `import { ${t.name} } from '${t.from}'`));
1500
+ if (o.stores) {
1501
+ const stores = o.stores.length ? o.stores : [{ name: "useAuthStore", from: "../stores/auth" }];
1502
+ L.push(
1503
+ "",
1504
+ "// Zustand stores are left out: they often hold tokens. Add `redact` for those",
1505
+ "// paths, then uncomment these and storeTools below. See",
1506
+ `// ${DOCS}/adapters.md`,
1507
+ `// import { storeTools } from '@avasapp/agent-bridge/zustand'`,
1508
+ ...stores.map((s) => `// import { ${s.name} } from '${s.from}'`)
1509
+ );
1510
+ }
1511
+ if (o.mmkv) {
1512
+ const mmkv = o.mmkv.length ? o.mmkv : [{ name: "storage", from: "../storage" }];
1513
+ L.push(
1514
+ "",
1515
+ "// MMKV is left out: it often holds tokens, and mmkv.get returns values as they",
1516
+ "// are. Wire only instances without secrets: uncomment these and mmkvTools below.",
1517
+ `// See ${DOCS}/adapters.md`,
1518
+ `// import { mmkvTools } from '@avasapp/agent-bridge/react-native-mmkv'`,
1519
+ ...mmkv.map((m) => `// import { ${m.name} } from '${m.from}'`)
1520
+ );
1521
+ }
1522
+ for (const todo of o.realtime) {
1523
+ const r = REALTIME[todo.kind];
1524
+ L.push(
1525
+ "",
1526
+ `// TODO ${todo.kind}: the adapter must wrap the client where it is created,`,
1527
+ `// before the app subscribes (${DOCS}/realtime.md).`,
1528
+ todo.file ? `// In ${todo.file}, right after \`${todo.name}\` is created, add:` : `// Where your ${todo.kind} client is created, add:`,
1529
+ `// import { ${r.fn} } from '${r.from}'`,
1530
+ `// ${realtimeLine(todo)}`,
1531
+ `// then import realtimeDevTools here and uncomment it in tools.`
1532
+ );
1533
+ }
1534
+ L.push("");
1535
+ if (o.ts) L.push("const scenarios: Scenarios = {");
1536
+ else L.push(`/** @type {import('@avasapp/agent-bridge').Scenarios} */`, "const scenarios = {");
1537
+ if (o.network) {
1538
+ L.push(
1539
+ " offline: {",
1540
+ ` description: 'Every request fails like a network failure; Metro still gets through.',`,
1541
+ " apply: ({ onUndo }) => {",
1542
+ " onUndo(strictNetwork({ offline: true }))",
1543
+ " },",
1544
+ " },"
1545
+ );
1546
+ }
1547
+ L.push(
1548
+ " // A user signed in locally with every request mocked: the state most flows",
1549
+ ` // start from. See ${DOCS}/scenarios.md`,
1550
+ " // signedIn: {",
1551
+ ` // description: 'Signed in locally; no request reaches a server.',`,
1552
+ " // apply: async ({ call, onUndo }) => {",
1553
+ ` // onUndo(strictNetwork()) // and mockApi, both from '@avasapp/agent-bridge/network'`,
1554
+ ` // onUndo(mockApi(API_URL, { 'GET /me': { json: user } }))`,
1555
+ ` // await call('store.set', 'auth', { token: 'local', user })`,
1556
+ " // return { user }",
1557
+ " // },",
1558
+ " // },",
1559
+ "}",
1560
+ "",
1561
+ "export function AgentBridge() {"
1562
+ );
1563
+ if (o.reactQuery)
1564
+ L.push(
1565
+ " // Undefined outside a QueryClientProvider: mount this inside one for query.* tools.",
1566
+ " const queryClient = useContext(QueryClientContext)"
1567
+ );
1568
+ L.push(
1569
+ " useAgentBridge({",
1570
+ ` name: '${o.appName.replace(/[\\']/g, "\\$&")}',`,
1571
+ ` transports: [${o.expo ? "expoTransport(), " : ""}cdpTransport()],`,
1572
+ " tools: {"
1573
+ );
1574
+ if (o.reactQuery) L.push(" ...(queryClient ? queryTools(queryClient) : {}),");
1575
+ if (o.expoRouter)
1576
+ L.push(" ...routerTools(router, { navigation: useNavigationContainerRef() }),");
1577
+ if (o.webview)
1578
+ L.push(
1579
+ ` // Each WebView also needs useWebViewTools (${DOCS}/webviews.md).`,
1580
+ " ...webviewTools(),"
1581
+ );
1582
+ if (o.network) L.push(" ...networkTools(),");
1583
+ for (const t of o.realtimeTools ?? []) L.push(` ...${t.name},`);
1584
+ if (o.stores) {
1585
+ const stores = o.stores.length ? o.stores : [{ name: "useAuthStore", from: "" }];
1586
+ const map = stores.map((s) => `${storeKey(s.name)}: ${s.name}`).join(", ");
1587
+ const first = storeKey(stores[0].name);
1588
+ L.push(` // ...storeTools({ ${map} }, { redact: { ${first}: ['token'] } }),`);
1589
+ }
1590
+ if (o.mmkv) {
1591
+ const mmkv = o.mmkv.length ? o.mmkv : [{ name: "storage", from: "" }];
1592
+ L.push(` // ...mmkvTools({ ${mmkv.map((m) => m.name).join(", ")} }),`);
1593
+ }
1594
+ if (o.realtime.length) L.push(" // ...realtimeDevTools,");
1595
+ if (o.reactNavigation) {
1596
+ const params = o.ts ? "name: string, params?: object" : "name, params";
1597
+ L.push(
1598
+ " // React Navigation has no adapter. With your container ref",
1599
+ " // (createNavigationContainerRef from @react-navigation/native), tools like these work:",
1600
+ ` // 'nav.navigate': (${params}) => navigationRef.navigate(name, params),`,
1601
+ ` // 'nav.current': () => navigationRef.getCurrentRoute(),`
1602
+ );
1603
+ }
1604
+ L.push(
1605
+ ` // Your own tools: any function, JSON in and out, e.g. 'cart.clear': () => clearCart(),`,
1606
+ " },",
1607
+ " scenarios,",
1608
+ " })",
1609
+ " return null",
1610
+ "}",
1611
+ ""
1612
+ );
1613
+ return stamp(L.join("\n"));
1614
+ }
1615
+ function renderMountSnippet(importPath2) {
1616
+ return `import { AgentBridge } from '${importPath2}'
1617
+ {__DEV__ ? <AgentBridge /> : null}`;
1618
+ }
1619
+ function renderSmokeFlow(o) {
1620
+ return stamp([
1621
+ `// Generated by agent-bridge init ${o.version}. Run it with:`,
1622
+ `// ${o.bin} run flows/smoke.flow.mjs`,
1623
+ `/** @type {import('@avasapp/agent-bridge/client').FlowModule['default']} */`,
1624
+ "const smoke = async ({ step }) => {",
1625
+ ` await step('snapshot', 'screen.snapshot')`,
1626
+ ...o.expoRouter ? [` await step('open home', 'router.navigate', '/')`] : [],
1627
+ " // TODO: wait for text your first screen always shows, for example:",
1628
+ ` // await step('home shown', 'screen.waitFor', 'Welcome')`,
1629
+ ` await step('undo', 'bridge.restore')`,
1630
+ "}",
1631
+ "",
1632
+ "export default smoke",
1633
+ ""
1634
+ ].join("\n"));
1635
+ }
1636
+ function renderFlowsReadme(bin) {
1637
+ 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.
1638
+ `;
1639
+ }
1640
+ function agentsLine(appDir, bin) {
1641
+ const where = appDir ? `${appDir}/flows` : "flows";
1642
+ const run = `\`${bin} run flows/smoke.flow.mjs\``;
1643
+ 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}`}.`;
1644
+ }
1645
+ var HEADER = /^(\/\/ Generated by agent-bridge init \d+\.\d+\.\d+(?:-[\w.]+)?)( \(#([0-9a-f]{12})\))?\./;
1646
+ var hash = (body) => createHash("sha256").update(body).digest("hex").slice(0, 12);
1647
+ function stamp(content) {
1648
+ const nl = content.indexOf("\n");
1649
+ return content.slice(0, nl).replace(HEADER, `$1 (#${hash(content.slice(nl))}).`) + content.slice(nl);
1650
+ }
1651
+ function isPristine(text) {
1652
+ const nl = text.indexOf("\n");
1653
+ const recorded = text.slice(0, nl).match(HEADER)?.[3];
1654
+ return !!recorded && recorded === hash(text.slice(nl));
1655
+ }
1656
+ var NETWORK_LINES = [
1657
+ `import { networkTools } from '@avasapp/agent-bridge/network'`,
1658
+ "...networkTools(), // in useAgentBridge({ tools })"
1659
+ ];
1660
+ var indentOf = (text) => text.match(/^([ \t]+)"/m)?.[1] ?? " ";
1661
+ function patchJson(text, edit, manual) {
1662
+ let json;
1663
+ try {
1664
+ json = JSON.parse(text);
1665
+ } catch {
1666
+ return { manual };
1667
+ }
1668
+ if (!json || typeof json !== "object" || Array.isArray(json)) return { manual };
1669
+ if (!edit(json)) return { unchanged: true };
1670
+ return {
1671
+ text: JSON.stringify(json, null, indentOf(text)) + (text.endsWith("\n") ? "\n" : "")
1672
+ };
1673
+ }
1674
+ function patchPackageScripts(text) {
1675
+ return patchJson(
1676
+ text,
1677
+ (pkg) => {
1678
+ if (pkg.scripts?.flow !== void 0) return false;
1679
+ pkg.scripts = { ...pkg.scripts, flow: "agent-bridge run" };
1680
+ return true;
1681
+ },
1682
+ `Add to package.json scripts: "flow": "agent-bridge run"`
1683
+ );
1684
+ }
1685
+ var globRe = (glob) => new RegExp(
1686
+ `^${glob.replace(/^\.\//, "").replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?")}$`
1687
+ );
1688
+ function includesFlows(pattern) {
1689
+ const last = pattern.split("/").pop() ?? "";
1690
+ const glob = last.includes(".") ? pattern : `${pattern.replace(/\/+$/, "")}${last === "*" ? "" : "/**/*"}`;
1691
+ return globRe(glob).test("flows/smoke.flow.mjs");
1692
+ }
1693
+ function patchTsconfig(text, baseAllowJs) {
1694
+ const manual = `Add "${FLOW_GLOB}" to "exclude" in tsconfig.json`;
1695
+ const json = parseJsonc(text);
1696
+ if (!json || typeof json !== "object") return { manual };
1697
+ if (Array.isArray(json.exclude) && json.exclude.includes(FLOW_GLOB)) return { unchanged: true };
1698
+ const allowJs = json.compilerOptions?.allowJs ?? baseAllowJs ?? true;
1699
+ const include = Array.isArray(json.include) ? json.include : json.files ? [] : ["**/*"];
1700
+ if (!allowJs || !include.some((p) => typeof p === "string" && includesFlows(p)))
1701
+ return { unchanged: true, note: "doesn't include .mjs files" };
1702
+ if (json.extends && !Array.isArray(json.exclude))
1703
+ return {
1704
+ manual: `${manual}, along with what it inherits from ${JSON.stringify(json.extends)}: an "exclude" here replaces the inherited one`
1705
+ };
1706
+ const patched = addToArray(text, "exclude", FLOW_GLOB, ["node_modules"]);
1707
+ return patched ? { text: patched } : { manual };
1708
+ }
1709
+ function patchOxlint(text) {
1710
+ const ignore = parseJsonc(text)?.ignorePatterns;
1711
+ if (Array.isArray(ignore) && ignore.includes(FLOW_GLOB)) return { unchanged: true };
1712
+ const patched = addToArray(text, "ignorePatterns", FLOW_GLOB);
1713
+ return patched ? { text: patched } : { manual: `Add "${FLOW_GLOB}" to "ignorePatterns" in .oxlintrc.json` };
1714
+ }
1715
+ var ESLINT_SNIPPET = `{ ignores: ['${FLOW_GLOB}'] },`;
1716
+ function patchEslint(text) {
1717
+ if (text.includes(FLOW_GLOB)) return { unchanged: true };
1718
+ const manual = `Add to the start of your ESLint flat config array: ${ESLINT_SNIPPET}`;
1719
+ const re = /^((?:module\.exports\s*=|export\s+default)\s*(?:defineConfig\(\s*)?\[)/gm;
1720
+ const matches = [...text.matchAll(re)];
1721
+ if (matches.length !== 1) return { manual };
1722
+ const at = matches[0].index + matches[0][1].length;
1723
+ return { text: `${text.slice(0, at)}
1724
+ ${ESLINT_SNIPPET}${text.slice(at)}` };
1725
+ }
1726
+
1727
+ // src/init/plan.ts
1728
+ var SKILL_COMMAND = ["npx", "skills", "add", "avas-app/agent-bridge"];
1729
+ var SKILL_RUN = ["npx", "--yes", "skills", "add", "avas-app/agent-bridge"];
1730
+ var PLUGIN_LINES = [
1731
+ "/plugin marketplace add avas-app/agent-bridge",
1732
+ "/plugin install agent-bridge@agent-bridge"
1733
+ ];
1734
+ var MARKERS = [
1735
+ ["expo", /expoTransport/, "expo"],
1736
+ ["expoRouter", /routerTools/, "expo-router"],
1737
+ ["reactQuery", /queryTools/, "@tanstack/react-query"],
1738
+ ["webview", /webviewTools/, "react-native-webview"],
1739
+ ["zustand", /storeTools/, "zustand"],
1740
+ ["mmkv", /mmkvTools/, "react-native-mmkv"],
1741
+ ["socketIo", /realtimeDevTools|socketIoTools|realtime/i, "socket.io-client"],
1742
+ ["ably", /realtimeDevTools|ablyTools|realtime/i, "ably"]
1743
+ ];
1744
+ var posix = (p) => p.split(sep).join("/");
1745
+ function importPath(fromFile, toFile) {
1746
+ const rel = posix(relative2(dirname4(fromFile), toFile)).replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
1747
+ return rel.startsWith(".") ? rel : `./${rel}`;
1748
+ }
1749
+ function binFor(d) {
1750
+ if (d.bin) {
1751
+ const rel = posix(relative2(d.dir, d.bin));
1752
+ return rel.startsWith(".") ? rel : `./${rel}`;
1753
+ }
1754
+ if (d.pm === "npm" && d.root !== d.dir)
1755
+ return `${posix(relative2(d.dir, d.root))}/node_modules/.bin/agent-bridge`;
1756
+ return binFallback(d.pm);
1757
+ }
1758
+ function bridgePath(d, out) {
1759
+ if (out) return isAbsolute(out) ? out : join5(d.dir, out);
1760
+ return join5(d.dir, d.srcDir ? "src/dev" : "dev", `agent-bridge.${d.typescript ? "tsx" : "js"}`);
1761
+ }
1762
+ function nextSteps(bin, o) {
1763
+ const { offline, runFrom } = o;
1764
+ const next = [];
1765
+ if (!o.mounted)
1766
+ next.push({
1767
+ step: o.elsewhere ? "Make sure the root component renders the component that calls useAgentBridge" : "Mount it: paste the snippet above"
1768
+ });
1769
+ next.push(
1770
+ { step: "Reload the app (press r in Metro)" },
1771
+ { step: "Start a session", command: `${bin} session start` },
1772
+ { step: "List the tools", command: `${bin} tools` },
1773
+ { step: "See what's on screen", command: `${bin} call screen.snapshot` }
1774
+ );
1775
+ if (offline)
1776
+ next.push({ step: "Try a scenario", command: `${bin} call scenario.apply '"offline"'` });
1777
+ next.push({ step: "Undo everything and disconnect", command: `${bin} session stop` });
1778
+ const from = runFrom === "." ? "" : `From \`${runFrom}\`: `;
1779
+ return {
1780
+ next,
1781
+ 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.`
1782
+ };
1783
+ }
1784
+ var stripExt = (p) => p.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "").replace(/\/index$/, "");
1785
+ function readTsconfig(d, fs) {
1786
+ if (!d.tsconfig) return { aliases: [] };
1787
+ const file = join5(d.dir, d.tsconfig);
1788
+ const json = parseJsonc(fs.read(file) ?? "");
1789
+ if (!json) return { aliases: [] };
1790
+ const findExtended = (from2, ext2) => {
1791
+ const names = ext2.endsWith(".json") ? [ext2] : [ext2, `${ext2}.json`];
1792
+ const roots = ext2.startsWith(".") ? [dirname4(from2)] : ancestors(dirname4(from2), d.repoRoot).map((a) => join5(a, "node_modules"));
1793
+ for (const r of roots) for (const n of names) if (fs.exists(join5(r, n)) && !fs.list(join5(r, n)).length) return join5(r, n);
1794
+ return void 0;
1795
+ };
1796
+ let baseAllowJs;
1797
+ let ext = json.extends;
1798
+ let from = file;
1799
+ for (let depth = 0; typeof ext === "string" && depth < 4; depth++) {
1800
+ const path = findExtended(from, ext);
1801
+ const base = path ? parseJsonc(fs.read(path) ?? "") : void 0;
1802
+ if (!base) break;
1803
+ if (typeof base.compilerOptions?.allowJs === "boolean") {
1804
+ baseAllowJs = base.compilerOptions.allowJs;
1805
+ break;
1806
+ }
1807
+ ext = base.extends;
1808
+ from = path;
1809
+ }
1810
+ const baseDir = join5(d.dir, json.compilerOptions?.baseUrl ?? ".");
1811
+ const paths = json.compilerOptions?.paths ?? {};
1812
+ const aliases = Object.entries(paths).flatMap(
1813
+ ([pattern, targets]) => Array.isArray(targets) && typeof targets[0] === "string" ? [[pattern.replace(/\*$/, ""), join5(baseDir, targets[0].replace(/\*$/, ""))]] : []
1814
+ );
1815
+ return { json, baseAllowJs, aliases };
1816
+ }
1817
+ function ancestors(dir, stop2) {
1818
+ const out = [];
1819
+ for (let d = dir; ; d = dirname4(d)) {
1820
+ out.push(d);
1821
+ if (d === stop2 || dirname4(d) === d) return out;
1822
+ }
1823
+ }
1824
+ function importedFiles(file, text, aliases) {
1825
+ const specs = [...text.matchAll(/(?:from\s+|import\s+|require\(\s*)['"]([^'"]+)['"]/g)].map((m) => m[1]);
1826
+ return specs.flatMap((spec) => {
1827
+ if (spec.startsWith(".")) return [stripExt(join5(dirname4(file), spec))];
1828
+ const alias = aliases.find(([prefix]) => prefix && spec.startsWith(prefix));
1829
+ return alias ? [stripExt(join5(alias[1], spec.slice(alias[0].length)))] : [];
1830
+ });
1831
+ }
1832
+ function plan(d, found, fs, o, cwd) {
1833
+ const rel = (p) => posix(relative2(d.dir, p));
1834
+ const out = bridgePath(d, o.out);
1835
+ if (rel(out).startsWith("..") || isAbsolute(rel(out)))
1836
+ return { error: `--out ${o.out} is outside the app (${d.dir}).` };
1837
+ if (/^(src\/)?app\//.test(rel(out)))
1838
+ return {
1839
+ error: `${rel(out)} is under app/, where expo-router treats every file as a route. Pick another --out.`
1840
+ };
1841
+ const ts = /\.tsx?$/.test(out);
1842
+ const bin = binFor(d);
1843
+ const runFrom = posix(relative2(cwd, d.dir)) || ".";
1844
+ const tsconfig = readTsconfig(d, fs);
1845
+ const actions = [];
1846
+ const files = [];
1847
+ const commented = [];
1848
+ const todos = [];
1849
+ const generate = (path, content) => {
1850
+ const current = fs.read(path);
1851
+ if (current === void 0) {
1852
+ actions.push({ kind: "write", path, content });
1853
+ files.push({ path: rel(path), status: "written" });
1854
+ return "written";
1855
+ }
1856
+ const pristine = isPristine(current);
1857
+ if (!o.force) {
1858
+ files.push({
1859
+ path: rel(path),
1860
+ status: "skipped",
1861
+ note: pristine ? "exists; --force regenerates it" : "exists, edited since init wrote it"
1862
+ });
1863
+ return pristine ? "kept" : "edited";
1864
+ }
1865
+ if (pristine) {
1866
+ const same = current === content;
1867
+ if (!same) actions.push({ kind: "write", path, content });
1868
+ files.push({ path: rel(path), status: same ? "unchanged" : "written" });
1869
+ return "written";
1870
+ }
1871
+ actions.push({ kind: "write", path: `${path}.new`, content });
1872
+ files.push({
1873
+ path: rel(path),
1874
+ status: "skipped",
1875
+ note: `edited since init wrote it, so --force left it; wrote ${rel(path)}.new to compare`
1876
+ });
1877
+ files.push({ path: `${rel(path)}.new`, status: "written" });
1878
+ return "edited";
1879
+ };
1880
+ const imported = (list2) => list2.map((f) => ({ name: f.name, from: importPath(out, join5(d.dir, f.file)) }));
1881
+ const realtimeTools = imported(found.realtimeTools);
1882
+ const realtime = realtimeTools.length ? [] : [
1883
+ ...d.has.socketIo ? found.socketIo.length ? found.socketIo.map((f) => ({ kind: "socket.io", ...f })) : [{ kind: "socket.io" }] : [],
1884
+ ...d.has.ably ? found.ably.length ? found.ably.map((f) => ({ kind: "ably", ...f })) : [{ kind: "ably" }] : []
1885
+ ];
1886
+ const existingText = fs.read(out);
1887
+ const existing = found.bridgeUsers.filter((f) => stripExt(join5(d.dir, f)) !== stripExt(out));
1888
+ const elsewhere = existingText === void 0 && existing.length > 0 && !o.out;
1889
+ let state = "elsewhere";
1890
+ if (elsewhere) {
1891
+ files.push({
1892
+ path: rel(out),
1893
+ status: "skipped",
1894
+ note: `the app already uses the bridge in ${existing.join(", ")}; pass --out to write one anyway`
1895
+ });
1896
+ } else {
1897
+ state = generate(
1898
+ out,
1899
+ renderBridgeFile({
1900
+ version: o.version,
1901
+ appName: d.appName,
1902
+ ts,
1903
+ expo: d.has.expo,
1904
+ expoRouter: d.has.expoRouter,
1905
+ reactQuery: d.has.reactQuery,
1906
+ webview: d.has.webview,
1907
+ network: o.network.enabled,
1908
+ reactNavigation: d.has.reactNavigation && !d.has.expoRouter,
1909
+ stores: d.has.zustand ? imported(found.stores) : void 0,
1910
+ mmkv: d.has.mmkv ? imported(found.mmkv) : void 0,
1911
+ realtime,
1912
+ realtimeTools
1913
+ })
1914
+ );
1915
+ }
1916
+ const wrote = state === "written";
1917
+ const liveText = wrote ? void 0 : elsewhere ? existing.map((f) => fs.read(join5(d.dir, f)) ?? "").join("\n") : existingText;
1918
+ const network = wrote ? o.network.enabled : /networkTools\s*\(/.test(liveText ?? "");
1919
+ const offline = wrote ? o.network.enabled : /\boffline\s*:/.test(liveText ?? "");
1920
+ if (wrote) {
1921
+ if (d.has.zustand)
1922
+ commented.push({
1923
+ adapter: "zustand",
1924
+ items: found.stores.map((s) => `${s.name} (${s.file})`),
1925
+ why: `${STORES_WHY} (${DOCS}/adapters.md)`
1926
+ });
1927
+ if (d.has.mmkv)
1928
+ commented.push({
1929
+ adapter: "react-native-mmkv",
1930
+ items: found.mmkv.map((s) => `${s.name} (${s.file})`),
1931
+ why: `${MMKV_WHY} (${DOCS}/adapters.md)`
1932
+ });
1933
+ if (d.has.reactNavigation && !d.has.expoRouter)
1934
+ commented.push({
1935
+ adapter: "@react-navigation/native",
1936
+ items: ["nav.navigate", "nav.current"],
1937
+ why: "there is no React Navigation adapter; the sample tools need your container ref"
1938
+ });
1939
+ for (const t of realtime) todos.push(`${realtimeTodoText(t)}: ${REALTIME_WHY} (${DOCS}/realtime.md).`);
1940
+ }
1941
+ const missing = !wrote && liveText !== void 0 ? MARKERS.filter(([lib, marker]) => d.has[lib] && !marker.test(liveText)).map(([, , dep]) => dep) : [];
1942
+ const flows = join5(d.dir, "flows");
1943
+ generate(
1944
+ join5(flows, "smoke.flow.mjs"),
1945
+ renderSmokeFlow({ version: o.version, expoRouter: d.has.expoRouter, bin })
1946
+ );
1947
+ const readme = join5(flows, "README.md");
1948
+ if (fs.exists(readme)) files.push({ path: rel(readme), status: "skipped", note: "exists" });
1949
+ else {
1950
+ actions.push({ kind: "write", path: readme, content: renderFlowsReadme(bin) });
1951
+ files.push({ path: rel(readme), status: "written" });
1952
+ }
1953
+ const patch = (path, fn) => {
1954
+ if (!path) return;
1955
+ const abs = join5(d.dir, path);
1956
+ const result = fn(fs.read(abs) ?? "");
1957
+ if ("text" in result) {
1958
+ actions.push({ kind: "patch", path: abs, patch: fn });
1959
+ files.push({ path, status: "patched" });
1960
+ } else if ("manual" in result) {
1961
+ files.push({ path, status: "manual", note: result.manual });
1962
+ } else {
1963
+ files.push({ path, status: "unchanged", ...result.note && { note: result.note } });
1964
+ }
1965
+ };
1966
+ patch("package.json", patchPackageScripts);
1967
+ patch(d.tsconfig, (text) => patchTsconfig(text, tsconfig.baseAllowJs));
1968
+ patch(d.oxlint, patchOxlint);
1969
+ patch(d.eslint, patchEslint);
1970
+ if (d.agentsFile) {
1971
+ const text = fs.read(d.agentsFile) ?? "";
1972
+ const path = posix(relative2(d.dir, d.agentsFile));
1973
+ if (text.includes(".flow.mjs")) files.push({ path, status: "unchanged" });
1974
+ else {
1975
+ const appDir = posix(relative2(dirname4(d.agentsFile), d.dir));
1976
+ actions.push({ kind: "append", path: d.agentsFile, line: agentsLine(appDir, bin) });
1977
+ files.push({ path, status: "appended" });
1978
+ }
1979
+ }
1980
+ const spec = `${PACKAGE}@^${o.version}`;
1981
+ const install = installCommand(d.pm, spec);
1982
+ if (!d.installed && o.install) actions.unshift({ kind: "install", command: install, cwd: d.dir });
1983
+ const skill = o.skillYes ? [...SKILL_RUN, "-y"] : SKILL_RUN;
1984
+ if (o.skill) actions.push({ kind: "skill", command: skill, cwd: d.repoRoot });
1985
+ const rootFile = d.rootFile ? join5(d.dir, d.rootFile) : void 0;
1986
+ const rootImports = rootFile ? importedFiles(rootFile, fs.read(rootFile) ?? "", tsconfig.aliases) : [];
1987
+ const reachedFromRoot = (file) => stripExt(file) === stripExt(rootFile ?? "") || rootImports.includes(stripExt(file));
1988
+ const mounted = elsewhere ? existing.some((f) => reachedFromRoot(join5(d.dir, f))) : rootImports.includes(stripExt(out));
1989
+ const snippet = renderMountSnippet(importPath(rootFile ?? join5(d.dir, "App.tsx"), out));
1990
+ const where = elsewhere ? `The app already uses the bridge in ${existing.join(", ")}; make sure the root component renders it` : d.rootFile ? `Paste the import into ${d.rootFile} and the element into the root component's JSX` : `No root component found (looked for ${d.has.expoRouter ? "app/_layout or src/app/_layout" : "App or src/App"}): paste it into yours, fixing the import path`;
1991
+ const note = `${where}${d.has.reactQuery && !elsewhere ? ", inside QueryClientProvider so query.* tools work" : ""}.`;
1992
+ const regenerable = state === "written" || state === "kept";
1993
+ 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;
1994
+ const choice = {
1995
+ enabled: network,
1996
+ open: !network && !(o.network.chosen && wrote),
1997
+ why: NETWORK_WHY,
1998
+ ...regenerable && { flag: network ? "--no-network --force" : "--network --force" },
1999
+ ...!network && { lines: NETWORK_LINES },
2000
+ ...ignored && { note: ignored }
2001
+ };
2002
+ const detected = MARKERS.filter(([lib]) => d.has[lib]).map(([, , dep]) => dep);
2003
+ if (d.has.reactNavigation && !d.has.expoRouter) detected.push("@react-navigation/native");
2004
+ return {
2005
+ actions,
2006
+ report: {
2007
+ ok: true,
2008
+ version: o.version,
2009
+ dryRun: false,
2010
+ app: { dir: d.dir, name: d.appName, packageManager: d.pm, detected },
2011
+ files,
2012
+ install: {
2013
+ status: d.installed ? "present" : "not-run",
2014
+ command: install.join(" ")
2015
+ },
2016
+ skill: { status: "not-run", command: SKILL_COMMAND.join(" "), plugin: PLUGIN_LINES },
2017
+ mount: { file: d.rootFile ?? null, mounted, snippet, note },
2018
+ choices: { network: choice },
2019
+ commented,
2020
+ todos,
2021
+ missing,
2022
+ existing,
2023
+ runFrom,
2024
+ scenarios: offline ? ["offline"] : [],
2025
+ ...nextSteps(bin, { mounted, offline, runFrom, elsewhere })
2026
+ }
2027
+ };
2028
+ }
2029
+
2030
+ // src/init/scan.ts
2031
+ import { join as join6, relative as relative3 } from "path";
2032
+ var CODE = /\.(tsx?|jsx?|mjs|cjs)$/;
2033
+ var TESTS = /\.(test|spec)\.|(^|\/)__tests__\//;
2034
+ var imports = (text, module) => new RegExp(
2035
+ `(from\\s+|require\\(\\s*)['"]${module.replace(/[.]/g, "\\.")}(/[\\w./-]+)?['"]`
2036
+ ).test(text);
2037
+ var TYPED = "(?:\\s*:[^=]+)?";
2038
+ var PATTERNS = [
2039
+ ["stores", "zustand", new RegExp(`export\\s+const\\s+(use\\w+)${TYPED}\\s*=\\s*create\\w*\\s*[<(]`, "g")],
2040
+ ["mmkv", "react-native-mmkv", new RegExp(`export\\s+const\\s+(\\w+)${TYPED}\\s*=\\s*(?:new\\s+MMKV|createMMKV)\\s*[<(]`, "g")],
2041
+ ["socketIo", "socket.io-client", new RegExp(`(?:const|let)\\s+(\\w+)${TYPED}\\s*=\\s*io\\s*\\(`, "g")],
2042
+ ["ably", "ably", new RegExp(`(?:const|let)\\s+(\\w+)${TYPED}\\s*=\\s*new\\s+(?:Ably\\.)?Realtime(?:\\.Promise)?\\s*\\(`, "g")],
2043
+ ["realtimeTools", "@avasapp/agent-bridge", /export\s+const\s+(\w+)\s*=\s*(?:socketIoTools|ablyTools)\s*\(/g]
2044
+ ];
2045
+ function scan(dir, fs, { maxFiles = 2e3, maxBytes = 512 * 1024 } = {}) {
2046
+ const files = [];
2047
+ const walk = (d, deep) => {
2048
+ const entries = fs.list(d).sort((a, b) => a.name < b.name ? -1 : 1);
2049
+ for (const e of entries) {
2050
+ if (files.length >= maxFiles) return;
2051
+ if (e.name.startsWith(".") || e.name === "node_modules") continue;
2052
+ const path = join6(d, e.name);
2053
+ if (e.dir) {
2054
+ if (deep) walk(path, true);
2055
+ } else if (CODE.test(e.name) && !TESTS.test(relative3(dir, path))) {
2056
+ files.push(path);
2057
+ }
2058
+ }
2059
+ };
2060
+ walk(dir, false);
2061
+ for (const sub of ["src", "app", "lib"]) walk(join6(dir, sub), true);
2062
+ const result = { stores: [], mmkv: [], socketIo: [], ably: [], realtimeTools: [], bridgeUsers: [] };
2063
+ for (const path of files) {
2064
+ const text = fs.read(path);
2065
+ if (!text || text.length > maxBytes) continue;
2066
+ if (/\buseAgentBridge\s*\(|(from\s+|require\(\s*)['"]@avasapp\/agent-bridge['"]/.test(text))
2067
+ result.bridgeUsers.push(relative3(dir, path));
2068
+ for (const [key, module, re] of PATTERNS) {
2069
+ if (!imports(text, module)) continue;
2070
+ for (const m of text.matchAll(re))
2071
+ result[key].push({ name: m[1], file: relative3(dir, path) });
2072
+ }
2073
+ }
2074
+ return result;
2075
+ }
2076
+
2077
+ // src/init/index.ts
2078
+ var INSTALL_TIMEOUT_MS = 5 * 6e4;
2079
+ var SKILL_TIMEOUT_MS = 2 * 6e4;
2080
+ var posix2 = (p) => p.split(sep2).join("/");
2081
+ var nodeFs = {
2082
+ exists: existsSync2,
2083
+ read: (path) => {
2084
+ try {
2085
+ return readFileSync4(path, "utf8");
2086
+ } catch {
2087
+ return void 0;
2088
+ }
2089
+ },
2090
+ readJson: (path) => {
2091
+ try {
2092
+ return JSON.parse(readFileSync4(path, "utf8"));
2093
+ } catch {
2094
+ return void 0;
2095
+ }
2096
+ },
2097
+ list: (dir) => {
2098
+ try {
2099
+ return readdirSync(dir, { withFileTypes: true }).map((e) => ({
2100
+ name: e.name,
2101
+ dir: e.isDirectory()
2102
+ }));
2103
+ } catch {
2104
+ return [];
2105
+ }
2106
+ }
2107
+ };
2108
+ function nodeIo() {
2109
+ return {
2110
+ fs: nodeFs,
2111
+ write: (path, content) => {
2112
+ mkdirSync2(dirname5(path), { recursive: true });
2113
+ writeFileSync2(path, content);
2114
+ },
2115
+ append: (path, text) => appendFileSync3(path, text),
2116
+ exec: (command, cwd, interactive, timeoutMs) => new Promise((done) => {
2117
+ const [cmd, ...args] = command;
2118
+ const child = spawn2(cmd, args, {
2119
+ cwd,
2120
+ stdio: [interactive ? "inherit" : "ignore", 2, 2],
2121
+ shell: process.platform === "win32"
2122
+ });
2123
+ const timer = setTimeout(() => {
2124
+ child.kill("SIGTERM");
2125
+ done("timeout");
2126
+ }, timeoutMs);
2127
+ child.on("error", () => {
2128
+ clearTimeout(timer);
2129
+ done(127);
2130
+ });
2131
+ child.on("close", (code) => {
2132
+ clearTimeout(timer);
2133
+ done(code ?? 1);
2134
+ });
2135
+ }),
2136
+ prompt: async (question) => {
2137
+ const rl = createInterface4({ input: process.stdin, output: process.stderr });
2138
+ try {
2139
+ return await rl.question(question);
2140
+ } finally {
2141
+ rl.close();
2142
+ }
2143
+ },
2144
+ print: (text) => process.stdout.write(`${text}
2145
+ `),
2146
+ warn: (text) => process.stderr.write(`${text}
2147
+ `),
2148
+ interactive: !!process.stdin.isTTY && !!process.stdout.isTTY && !/^(1|true)$/i.test(process.env.CI ?? ""),
2149
+ cwd: process.cwd(),
2150
+ userAgent: process.env.npm_config_user_agent
2151
+ };
2152
+ }
2153
+ async function apply(actions, report, io, interactive) {
2154
+ const why = (code, ms) => code === "timeout" ? `timed out after ${ms / 6e4} min` : `exit code ${code}`;
2155
+ for (const a of actions) {
2156
+ switch (a.kind) {
2157
+ case "install": {
2158
+ const code = await io.exec(a.command, a.cwd, false, INSTALL_TIMEOUT_MS);
2159
+ report.install.status = code === 0 ? "installed" : "failed";
2160
+ if (code !== 0) {
2161
+ report.ok = false;
2162
+ report.install.error = why(code, INSTALL_TIMEOUT_MS);
2163
+ io.warn(`Install failed (${a.command.join(" ")}): ${report.install.error}`);
2164
+ }
2165
+ break;
2166
+ }
2167
+ case "skill": {
2168
+ const code = await io.exec(a.command, a.cwd, interactive, SKILL_TIMEOUT_MS);
2169
+ report.skill.status = code === 0 ? "added" : "failed";
2170
+ if (code !== 0) {
2171
+ report.skill.error = why(code, SKILL_TIMEOUT_MS);
2172
+ io.warn(`Adding the skill failed (${report.skill.error}); add it later with ${report.skill.command}`);
2173
+ }
2174
+ break;
2175
+ }
2176
+ case "write":
2177
+ io.write(a.path, a.content);
2178
+ break;
2179
+ case "patch": {
2180
+ const result = a.patch(io.fs.read(a.path) ?? "");
2181
+ if ("text" in result) io.write(a.path, result.text);
2182
+ break;
2183
+ }
2184
+ case "append": {
2185
+ const text = io.fs.read(a.path) ?? "";
2186
+ io.append(a.path, `${text.endsWith("\n") || !text ? "" : "\n"}${a.line}
2187
+ `);
2188
+ break;
2189
+ }
2190
+ }
2191
+ }
2192
+ }
2193
+ var describe = (a, dir) => {
2194
+ const rel = (p) => relative4(dir, p) || ".";
2195
+ switch (a.kind) {
2196
+ case "install":
2197
+ return `install ${a.command.join(" ")}`;
2198
+ case "skill":
2199
+ return `skill ${a.command.join(" ")} (in ${a.cwd})`;
2200
+ case "write":
2201
+ return `write ${rel(a.path)}`;
2202
+ case "patch":
2203
+ return `patch ${rel(a.path)}`;
2204
+ case "append":
2205
+ return `append ${rel(a.path)}: ${a.line}`;
2206
+ }
2207
+ };
2208
+ function formatReport(r, actions) {
2209
+ const L = [];
2210
+ const detected = r.app.detected.length ? ` (${r.app.detected.join(", ")})` : "";
2211
+ L.push(
2212
+ `agent-bridge init ${r.version}${r.dryRun ? " --dry-run: nothing changed" : ""}`,
2213
+ `App "${r.app.name}"${detected}, ${r.app.packageManager}`,
2214
+ ""
2215
+ );
2216
+ if (r.dryRun) {
2217
+ L.push("Would run:", ...actions.map((a) => ` ${describe(a, r.app.dir)}`), "");
2218
+ for (const a of actions)
2219
+ if (a.kind === "write")
2220
+ L.push(`--- ${relative4(r.app.dir, a.path)}`, a.content.trimEnd(), "");
2221
+ }
2222
+ L.push("Files");
2223
+ for (const f of r.files)
2224
+ L.push(` ${f.status.padEnd(9)} ${f.path}${f.note ? `: ${f.note}` : ""}`);
2225
+ L.push(
2226
+ "",
2227
+ `Install: ${r.install.status} (${r.install.command})`,
2228
+ `Skill: ${r.skill.status} (${r.skill.command})`,
2229
+ " In Claude Code, install it as a plugin instead:",
2230
+ ...r.skill.plugin.map((l) => ` ${l}`)
2231
+ );
2232
+ if (r.existing.length)
2233
+ L.push("", `Already uses the bridge: ${r.existing.join(", ")}`);
2234
+ if (r.missing.length)
2235
+ L.push("", `Detected but not wired in the bridge file: ${r.missing.join(", ")} (see ${DOCS}/adapters.md)`);
2236
+ if (r.commented.length) {
2237
+ L.push("", "Left as comments in the bridge file:");
2238
+ for (const c of r.commented)
2239
+ L.push(` ${c.adapter}${c.items.length ? `: ${c.items.join(", ")}` : ""}. Why: ${c.why}`);
2240
+ }
2241
+ if (r.todos.length) L.push("", "TODO:", ...r.todos.map((t) => ` ${t}`));
2242
+ const n = r.choices.network;
2243
+ L.push("");
2244
+ if (n.enabled) L.push("Network tools: on (net.*).");
2245
+ else {
2246
+ const how = [n.flag && `rerun with ${n.flag}`, n.lines && "add these lines"].filter(Boolean).join(", or ");
2247
+ L.push(`Network tools: off. ${n.open ? "Open choice: " : ""}${n.why}. To wire them, ${how}:`);
2248
+ L.push(...(n.lines ?? []).map((l) => ` ${l}`));
2249
+ }
2250
+ if (n.note) L.push(n.note);
2251
+ L.push(
2252
+ "",
2253
+ r.mount.mounted ? `Mounted: yes (from ${r.mount.file}).` : `Mounted: no. ${r.mount.note}`
2254
+ );
2255
+ if (!r.mount.mounted && !r.existing.length)
2256
+ L.push(...r.mount.snippet.split("\n").map((l) => ` ${l}`));
2257
+ L.push("", `First run${r.runFrom === "." ? "" : ` (from ${r.runFrom})`}:`);
2258
+ r.next.forEach(
2259
+ (s, i) => L.push(` ${i + 1}. ${s.step}${s.command ? `
2260
+ ${s.command}` : ""}`)
2261
+ );
2262
+ L.push("", "Prompt for your agent:", ` ${r.prompt}`);
2263
+ return L.join("\n");
2264
+ }
2265
+ async function runInit(args, io) {
2266
+ const dir = resolve(io.cwd, args.dir ?? ".");
2267
+ const fail = (error) => {
2268
+ if (args.json) io.print(JSON.stringify({ ok: false, ...error }, null, 2));
2269
+ else io.warn(error.error);
2270
+ return 1;
2271
+ };
2272
+ let d = detect(dir, io.fs, { userAgent: io.userAgent });
2273
+ if ("error" in d) return fail(d);
2274
+ const interactive = io.interactive && !args.json && !args.yes;
2275
+ const options = (network2) => ({
2276
+ version,
2277
+ out: args.out === void 0 ? void 0 : resolve(io.cwd, args.out),
2278
+ force: args.force,
2279
+ install: args.install !== false,
2280
+ skill: args.skill !== false,
2281
+ network: network2,
2282
+ skillYes: !interactive
2283
+ });
2284
+ const planFor = (det, network2, install2 = true) => plan(det, scan(dir, io.fs), io.fs, { ...options(network2), install: install2 && args.install !== false }, io.cwd);
2285
+ let network = { enabled: args.network ?? false, chosen: args.network !== void 0 };
2286
+ const first = planFor(d, network);
2287
+ if ("error" in first) return fail(first);
2288
+ if (args.dryRun) {
2289
+ const { actions: actions2, report: report2 } = first;
2290
+ report2.dryRun = true;
2291
+ if (args.json) {
2292
+ const contents = Object.fromEntries(
2293
+ actions2.flatMap((a) => a.kind === "write" ? [[relative4(dir, a.path), a.content]] : [])
2294
+ );
2295
+ io.print(JSON.stringify({ ...report2, contents }, null, 2));
2296
+ } else io.print(formatReport(report2, actions2));
2297
+ return 0;
2298
+ }
2299
+ const bridge = posix2(relative4(dir, bridgePath(d, options(network).out)));
2300
+ const writesBridge = first.report.files.some((f) => f.path === bridge && f.status === "written");
2301
+ if (!network.chosen && interactive && writesBridge) {
2302
+ const answer = await io.prompt(`${NETWORK_PROMPT} [y/N] `);
2303
+ network = { enabled: /^y(es)?$/i.test(answer.trim()), chosen: true };
2304
+ }
2305
+ const install = first.actions.find((a) => a.kind === "install");
2306
+ const installReport = first.report.install;
2307
+ if (install) {
2308
+ await apply([install], first.report, io, interactive);
2309
+ const after = detect(dir, io.fs, { userAgent: io.userAgent });
2310
+ if (!("error" in after)) d = after;
2311
+ }
2312
+ const p = planFor(d, network, false);
2313
+ if ("error" in p) return fail(p);
2314
+ const { actions, report } = p;
2315
+ if (install) {
2316
+ report.install = installReport;
2317
+ report.ok = first.report.ok;
2318
+ }
2319
+ await apply(actions, report, io, interactive);
2320
+ io.print(args.json ? JSON.stringify(report, null, 2) : formatReport(report, actions));
2321
+ return report.ok ? 0 : 1;
2322
+ }
2323
+
1180
2324
  // src/cli.ts
1181
2325
  var HELP2 = `agent-bridge: drive a running React Native app from an agent
1182
2326
 
1183
2327
  Usage
2328
+ agent-bridge init Set up this app: install, write the dev-only bridge
2329
+ file and a smoke flow, print the line to mount it
1184
2330
  agent-bridge devices Apps connected to Metro
1185
2331
  agent-bridge tools Tools the app exposes
1186
2332
  agent-bridge call <tool> [args] Call a tool. args: a JSON array, or one JSON value
@@ -1191,7 +2337,7 @@ Usage
1191
2337
  agent-bridge call --batch Read \`tool args\` lines from stdin (args may be @file); one JSON line per call, one connection
1192
2338
  agent-bridge repl Interactive prompt (history, tab completion, .help). With
1193
2339
  stdin not a terminal it behaves like call --batch
1194
- agent-bridge run <flow.mjs|.ts> Run a flow: export default async ({ step, call }) => {}
2340
+ agent-bridge run <flow.mjs> Run a flow: export default async ({ step, call }) => {}
1195
2341
  export const scenario = 'signedIn' applies it first
1196
2342
  and runs bridge.restore after, even on failure
1197
2343
  agent-bridge scenarios Scenarios the app defines (scenario.apply applies one)
@@ -1219,6 +2365,17 @@ Options
1219
2365
  JSON after "=": --scenario 'signedIn={"user":{"name":"Ada"}}'
1220
2366
  --session <name> Use this session (env AGENT_BRIDGE_SESSION)
1221
2367
  --no-session Connect directly even if a session is running
2368
+
2369
+ init options
2370
+ --dir <path> The app (default: the current directory)
2371
+ --out <file> Bridge file path, inside the app (default src/dev/agent-bridge.tsx, or dev/)
2372
+ --dry-run Print what it would do and the files; change nothing
2373
+ --force Regenerate files init wrote that are still unedited (edited ones get a .new copy)
2374
+ --no-install Print the install command instead of running it
2375
+ --no-skill Don't run \`npx skills add avas-app/agent-bridge\`
2376
+ --network, --no-network Wire networkTools into a new bridge file (asked in a terminal; off otherwise)
2377
+ --json Machine-readable report on stdout; never prompts
2378
+ --yes, -y Never prompt
1222
2379
  `;
1223
2380
  var failedLogs = (error) => error instanceof AgentBridgeCallError ? error.logs : [];
1224
2381
  async function readStdin() {
@@ -1251,6 +2408,14 @@ async function main() {
1251
2408
  "dry-run": { type: "boolean" },
1252
2409
  session: { type: "string" },
1253
2410
  "no-session": { type: "boolean" },
2411
+ dir: { type: "string" },
2412
+ force: { type: "boolean" },
2413
+ "no-install": { type: "boolean" },
2414
+ "no-skill": { type: "boolean" },
2415
+ network: { type: "boolean" },
2416
+ "no-network": { type: "boolean" },
2417
+ json: { type: "boolean" },
2418
+ yes: { type: "boolean", short: "y" },
1254
2419
  help: { type: "boolean", short: "h" }
1255
2420
  }
1256
2421
  });
@@ -1290,9 +2455,30 @@ async function main() {
1290
2455
  if (failed) process.exitCode = 1;
1291
2456
  };
1292
2457
  switch (command) {
2458
+ case "init":
2459
+ process.exitCode = await runInit(
2460
+ {
2461
+ dir: values.dir,
2462
+ out: values.out,
2463
+ dryRun: values["dry-run"],
2464
+ force: values.force,
2465
+ install: !values["no-install"],
2466
+ skill: !values["no-skill"],
2467
+ network: values.network ? true : values["no-network"] ? false : void 0,
2468
+ json: values.json,
2469
+ yes: values.yes
2470
+ },
2471
+ nodeIo()
2472
+ );
2473
+ return;
1293
2474
  case "devices": {
1294
2475
  const devices = await listDevices(options);
1295
- if (!devices.length) console.log("No apps connected.");
2476
+ if (!devices.length) {
2477
+ const metro = metroHost(options.metro);
2478
+ await assertListening(metro);
2479
+ console.log("No apps connected.");
2480
+ console.error(await noAppHint(metro));
2481
+ }
1296
2482
  for (const d of devices) {
1297
2483
  const extra = d.tools !== void 0 ? ` ${d.tools} tools ${d.deviceId}` : "";
1298
2484
  console.log(`${d.transport.padEnd(5)} ${d.name}${extra}`);
@@ -1353,7 +2539,7 @@ async function main() {
1353
2539
  case "run": {
1354
2540
  const [file] = rest;
1355
2541
  if (!file) throw new Error("Usage: agent-bridge run <flow file>");
1356
- const flow = await import(pathToFileURL(resolve(file)).href);
2542
+ const flow = await import(pathToFileURL(resolve2(file)).href);
1357
2543
  flowScenarios(flow);
1358
2544
  const extra = (values.scenario ?? []).map(parseScenarioFlag);
1359
2545
  return withBridge(async (bridge) => {