@avasapp/agent-bridge 0.1.0 → 0.3.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.
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/adapters/socket-io.ts
21
+ var socket_io_exports = {};
22
+ __export(socket_io_exports, {
23
+ socketIoTools: () => socketIoTools
24
+ });
25
+ module.exports = __toCommonJS(socket_io_exports);
26
+
27
+ // src/realtime/patch.ts
28
+ var list = (names) => Array.isArray(names) ? names : [names];
29
+ var sameKey = (a, b) => JSON.stringify(a) === JSON.stringify(b);
30
+ function split(args) {
31
+ const last = args[args.length - 1];
32
+ return typeof last === "function" ? { key: args.slice(0, -1), listener: last } : { key: args };
33
+ }
34
+ function tapListeners(tap, target, options) {
35
+ const methods = target;
36
+ const tracked = [];
37
+ const spread = options.message === "args";
38
+ for (const name of list(options.add)) {
39
+ const add = methods[name];
40
+ if (typeof add !== "function") continue;
41
+ methods[name] = function(...args) {
42
+ const { key, listener: original } = split(args);
43
+ const channel = original && options.channel(key);
44
+ if (!original || channel === void 0) return add.apply(this, args);
45
+ const accepts = options.accepts;
46
+ const { listener, unsubscribe } = tap.wrap(
47
+ channel,
48
+ (message, ...rest) => spread ? original.apply(target, message) : original.call(target, message, ...rest),
49
+ accepts ? { accepts: (m) => accepts(key, m) } : {}
50
+ );
51
+ const wrapped = spread ? (...received) => listener(received) : (message, ...rest) => listener(message, ...rest);
52
+ tracked.push({ key, original, wrapped, unsubscribe });
53
+ return add.apply(this, [...key, wrapped]);
54
+ };
55
+ }
56
+ for (const name of list(options.remove)) {
57
+ const remove = methods[name];
58
+ if (typeof remove !== "function") continue;
59
+ methods[name] = function(...args) {
60
+ const { key, listener } = split(args);
61
+ const hits = tracked.filter(
62
+ (t) => (!listener || t.original === listener || t.original.fn === listener) && (!key.length || sameKey(t.key, key))
63
+ );
64
+ for (const hit of hits) {
65
+ tracked.splice(tracked.indexOf(hit), 1);
66
+ hit.unsubscribe();
67
+ }
68
+ if (!listener || !hits.length) return remove.apply(this, args);
69
+ let result;
70
+ for (const hit of hits) result = remove.apply(this, [...hit.key, hit.wrapped]);
71
+ return result;
72
+ };
73
+ }
74
+ }
75
+ function fakeableConnection(target, options) {
76
+ const methods = target;
77
+ const events = methods?.[options.events];
78
+ if (!methods || typeof events !== "function") return void 0;
79
+ const read = options.read ?? String;
80
+ const write = options.write ?? ((state) => state);
81
+ const values = methods;
82
+ let fake = null;
83
+ let real = values[options.property];
84
+ const readReal = () => read(options.fakeProperty === false ? values[options.property] : real);
85
+ if (options.fakeProperty !== false)
86
+ Object.defineProperty(target, options.property, {
87
+ configurable: true,
88
+ enumerable: true,
89
+ get: () => fake === null ? real : write(fake),
90
+ set: (value) => {
91
+ real = value;
92
+ }
93
+ });
94
+ methods[options.events] = function(...args) {
95
+ return fake === null ? events.apply(this, args) : void 0;
96
+ };
97
+ const emit = (...args) => void events.apply(target, args);
98
+ return {
99
+ state: readReal,
100
+ states: options.states,
101
+ live: options.live,
102
+ fake: (state, previous) => {
103
+ fake = state;
104
+ const current = state ?? readReal();
105
+ if (current !== previous)
106
+ options.announce(emit, { current, previous, faked: state !== null });
107
+ }
108
+ };
109
+ }
110
+ function realtimeAdapter(install) {
111
+ const taps = /* @__PURE__ */ new WeakMap();
112
+ return (client, options = {}) => {
113
+ let tools = taps.get(client);
114
+ if (!tools) taps.set(client, tools = install(client, options).tools);
115
+ return tools;
116
+ };
117
+ }
118
+
119
+ // src/realtime/tap.ts
120
+ var LOG_SIZE = 50;
121
+ var ALL = "*";
122
+ var defaultDescribe = (message) => {
123
+ const id = message?.id;
124
+ return typeof id === "string" ? { data: message, id } : { data: message };
125
+ };
126
+ var sameMessage = (a, b) => a === b || Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => v === b[i]);
127
+ function createRealtimeTap(options = {}) {
128
+ const ns = options.namespace ?? "realtime";
129
+ const describe = options.describe ?? defaultDescribe;
130
+ const toMessage = options.toMessage ?? ((values) => values[0]);
131
+ const connection = options.connection;
132
+ const live = connection?.live ?? "connected";
133
+ const channels = /* @__PURE__ */ new Map();
134
+ const mutes = /* @__PURE__ */ new Map();
135
+ const log = [];
136
+ const last = /* @__PURE__ */ new Map();
137
+ let injectedCount = 0;
138
+ let faked = null;
139
+ const muteFor = (channel) => mutes.get(channel) ?? mutes.get(ALL);
140
+ const liveChannels = () => [...channels.keys()].join(", ") || "none";
141
+ function record(channel, message, injected, dropped) {
142
+ log.push({
143
+ at: (/* @__PURE__ */ new Date()).toISOString(),
144
+ channel,
145
+ ...describe(message),
146
+ injected,
147
+ ...dropped && { dropped }
148
+ });
149
+ if (log.length > LOG_SIZE) log.splice(0, log.length - LOG_SIZE);
150
+ }
151
+ function receive(channel, entry, message, rest) {
152
+ const previous = last.get(channel);
153
+ const dropped = muteFor(channel) ? "muted" : faked !== null && faked !== live ? "connection" : void 0;
154
+ if (previous && !previous.seen.has(entry) && sameMessage(previous.message, message)) {
155
+ previous.seen.add(entry);
156
+ } else {
157
+ last.set(channel, { message, seen: /* @__PURE__ */ new Set([entry]) });
158
+ record(channel, message, false, dropped);
159
+ }
160
+ if (!dropped) entry.deliver(message, ...rest);
161
+ }
162
+ const wrap = (channel, listener, wrapOptions = {}) => {
163
+ const entry = {
164
+ deliver: listener,
165
+ accepts: wrapOptions.accepts
166
+ };
167
+ let entries = channels.get(channel);
168
+ if (!entries) channels.set(channel, entries = /* @__PURE__ */ new Set());
169
+ entries.add(entry);
170
+ return {
171
+ listener: (message, ...rest) => receive(channel, entry, message, rest),
172
+ unsubscribe: () => {
173
+ const current = channels.get(channel);
174
+ if (!current?.delete(entry) || current.size) return;
175
+ channels.delete(channel);
176
+ last.delete(channel);
177
+ }
178
+ };
179
+ };
180
+ function setMute(channel, mute) {
181
+ if (typeof channel !== "string" || !channel)
182
+ throw new Error(`Pass a channel name, or "${ALL}" for every channel`);
183
+ if (mute) mutes.set(channel, mute);
184
+ else if (channel === ALL) mutes.clear();
185
+ else mutes.delete(channel);
186
+ return { muted: [...mutes.keys()] };
187
+ }
188
+ function setConnection(conn, state) {
189
+ const shown = faked ?? conn.state();
190
+ if (state !== null && typeof state !== "string")
191
+ throw new Error('Pass a state such as "disconnected", or null for the real one');
192
+ if (state !== null && conn.states && !conn.states.includes(state))
193
+ throw new Error(
194
+ `Unknown state "${state}". Known: ${conn.states.join(", ")}`
195
+ );
196
+ if (state === faked) return;
197
+ faked = state;
198
+ conn.fake(state, shown);
199
+ }
200
+ const tools = {
201
+ [`${ns}.channels`]: {
202
+ description: "Channels the app listens on: listener count, muted, and what the library says about each.",
203
+ run: () => [...channels].map(([name, entries]) => ({
204
+ name,
205
+ listeners: entries.size,
206
+ muted: Boolean(muteFor(name)),
207
+ ...options.channelInfo?.(name)
208
+ }))
209
+ },
210
+ [`${ns}.log`]: {
211
+ description: `Recent messages, real and injected, newest first (last ${LOG_SIZE}). Filter: { channel?, event?, limit? }; clear: true empties the log after reading.`,
212
+ run: (filter = {}) => {
213
+ const entries = log.filter(
214
+ (e) => (!filter.channel || e.channel === filter.channel) && (!filter.event || e.event === filter.event)
215
+ ).reverse().slice(0, filter.limit ?? 20);
216
+ if (filter.clear) log.length = 0;
217
+ return entries;
218
+ }
219
+ },
220
+ [`${ns}.emit`]: {
221
+ description: "Deliver a fake message to every listener on a channel: [channel, message]. Runs the app's own handlers. Returns { id, delivered }.",
222
+ run: (channel, ...values) => {
223
+ const entries = channels.get(channel);
224
+ if (!entries?.size)
225
+ throw new Error(
226
+ `Nothing listens on "${channel}". Live channels: ${liveChannels()}`
227
+ );
228
+ injectedCount += 1;
229
+ const message = toMessage(values, {
230
+ channel,
231
+ id: `agent-bridge-${injectedCount}`
232
+ });
233
+ const id = describe(message).id;
234
+ if (muteFor(channel)?.dropInjected) {
235
+ record(channel, message, true, "muted");
236
+ return { id, delivered: 0 };
237
+ }
238
+ record(channel, message, true);
239
+ let delivered = 0;
240
+ for (const entry of [...entries]) {
241
+ if (entry.accepts && !entry.accepts(message)) continue;
242
+ entry.deliver(message);
243
+ delivered += 1;
244
+ }
245
+ return { id, delivered };
246
+ }
247
+ },
248
+ [`${ns}.mute`]: {
249
+ description: `Drop real messages on a channel ("${ALL}" for all) until unmuted. Injected messages still get through unless { dropInjected: true }.`,
250
+ run: (channel, muteOptions = {}) => setMute(channel, { dropInjected: Boolean(muteOptions.dropInjected) })
251
+ },
252
+ [`${ns}.unmute`]: {
253
+ description: `Stop dropping messages on a channel ("${ALL}" for all).`,
254
+ run: (channel) => setMute(channel, null)
255
+ },
256
+ [`${ns}.restore`]: {
257
+ description: "Undo the agent: unmute every channel and go back to the real connection state.",
258
+ run: () => {
259
+ const unmuted = [...mutes.keys()];
260
+ mutes.clear();
261
+ const connectionFaked = faked !== null;
262
+ if (connection && connectionFaked) setConnection(connection, null);
263
+ return { unmuted, connectionFaked };
264
+ }
265
+ }
266
+ };
267
+ if (connection)
268
+ tools[`${ns}.connection`] = {
269
+ description: `The connection state, or fake one: [state] shows the app that state${connection.states ? ` (${connection.states.join(", ")})` : ""} and drops real messages unless it is "${live}"; [null] goes back to the real one.`,
270
+ run: (...args) => {
271
+ if (args.length) setConnection(connection, args[0] ?? null);
272
+ return { state: faked ?? connection.state(), real: connection.state(), faked: faked !== null };
273
+ }
274
+ };
275
+ return { wrap, tools };
276
+ }
277
+
278
+ // src/adapters/socket-io.ts
279
+ var RESERVED = /* @__PURE__ */ new Set([
280
+ "connect",
281
+ "connect_error",
282
+ "disconnect",
283
+ "disconnecting",
284
+ "newListener",
285
+ "removeListener"
286
+ ]);
287
+ var socketIoTools = realtimeAdapter(
288
+ (socket, options) => {
289
+ const tap = createRealtimeTap({
290
+ namespace: options.namespace,
291
+ // The last argument is an ack callback when the server asked for one.
292
+ describe: (args) => {
293
+ const values = args.filter((a) => typeof a !== "function");
294
+ return { data: values.length === 1 ? values[0] : values };
295
+ },
296
+ toMessage: (values) => values,
297
+ // socket.io-client announces lifecycle events with emitReserved.
298
+ connection: fakeableConnection(socket, {
299
+ property: "connected",
300
+ events: "emitReserved",
301
+ states: ["connected", "disconnected"],
302
+ read: (connected) => connected ? "connected" : "disconnected",
303
+ // socket.io reads `connected` to decide whether to deliver incoming
304
+ // events and send outgoing ones; faking it would hold them back.
305
+ fakeProperty: false,
306
+ announce: (emit, { current }) => current === "connected" ? emit("connect") : emit("disconnect", "transport close")
307
+ })
308
+ });
309
+ tapListeners(tap, socket, {
310
+ add: ["on", "addEventListener"],
311
+ remove: ["off", "removeListener", "removeAllListeners", "removeEventListener"],
312
+ channel: ([event]) => typeof event === "string" && !RESERVED.has(event) ? event : void 0,
313
+ message: "args"
314
+ });
315
+ return tap;
316
+ }
317
+ );
@@ -0,0 +1,21 @@
1
+ import { T as Tools } from '../types-6kKQsLC1.cjs';
2
+
3
+ type Listener = (...args: any[]) => void;
4
+ /** A socket.io client socket (`io(url)`), typed structurally. */
5
+ type SocketLike = {
6
+ on(event: string, listener: Listener): unknown;
7
+ off(event?: string, listener?: Listener): unknown;
8
+ connected?: boolean;
9
+ };
10
+ type SocketIoToolsOptions = {
11
+ /** Tool name prefix. Defaults to "realtime". */
12
+ namespace?: string;
13
+ };
14
+ /**
15
+ * See, fake and drop socket.io events: `realtime.*`, where a channel is an
16
+ * event name. Patches the socket, so call it where you create the socket,
17
+ * before the app adds listeners. Calling it again with the same socket is free.
18
+ */
19
+ declare const socketIoTools: (client: SocketLike, options?: SocketIoToolsOptions | undefined) => Tools;
20
+
21
+ export { type SocketIoToolsOptions, type SocketLike, socketIoTools };
@@ -1,5 +1,5 @@
1
1
  import { QueryClient } from '@tanstack/query-core';
2
- import { T as Tools } from '../types-C6DUUHnB.cjs';
2
+ import { T as Tools } from '../types-6kKQsLC1.cjs';
3
3
 
4
4
  /**
5
5
  * Read and seed the TanStack Query cache. `query.pin` keeps a seed in place:
@@ -1,4 +1,4 @@
1
- import { T as Tools } from '../types-C6DUUHnB.cjs';
1
+ import { T as Tools } from '../types-6kKQsLC1.cjs';
2
2
 
3
3
  /** Anything with zustand's store shape. */
4
4
  type StoreLike = {
@@ -174,6 +174,8 @@ async function connectExpo(metro, device, discoveryMs = 600) {
174
174
 
175
175
  // src/client/cdp.ts
176
176
  import WebSocket2 from "ws";
177
+ var CannotEvaluate = class extends Error {
178
+ };
177
179
  var targetLabel = (t) => `${t.title}${t.deviceName ? ` [${t.deviceName}]` : ""}`;
178
180
  async function connectCdp(metro, device) {
179
181
  const target = pickOne(await listCdpTargets(metro), device, targetLabel);
@@ -220,6 +222,7 @@ async function connectCdp(metro, device) {
220
222
  expression,
221
223
  returnByValue: true
222
224
  });
225
+ if (message.error?.code === -32601) throw new CannotEvaluate();
223
226
  const details = message.result?.exceptionDetails;
224
227
  if (details) {
225
228
  const text = details.exception?.description ?? details.text ?? "evaluation failed";
@@ -234,10 +237,10 @@ async function connectCdp(metro, device) {
234
237
  info = JSON.parse(
235
238
  String(await evaluate(`${CDP_GLOBAL}.info()`))
236
239
  );
237
- } catch {
240
+ } catch (error) {
238
241
  ws.close();
239
242
  throw new Error(
240
- `agent-bridge isn't running in ${targetLabel(target)}. Is useAgentBridge mounted in a dev build?`
243
+ error instanceof CannotEvaluate ? `The debugger for ${targetLabel(target)} can't run code (no Runtime.evaluate, as in Expo Go on Android). Use --transport expo, or a dev build.` : `agent-bridge isn't running in ${targetLabel(target)}. Is useAgentBridge mounted in a dev build?`
241
244
  );
242
245
  }
243
246
  return {
@@ -288,6 +291,119 @@ async function openConnection(options = {}) {
288
291
  }
289
292
  }
290
293
 
294
+ // src/client/log-lines.ts
295
+ function logLine(entry) {
296
+ const where = entry.during ? ` during ${entry.during}` : entry.after ? ` after ${entry.after}` : "";
297
+ const first = entry.message.split("\n")[0] ?? "";
298
+ const text = first.length > 300 ? `${first.slice(0, 300)}...` : first;
299
+ return `! ${entry.level}${where}: ${text}`;
300
+ }
301
+
302
+ // src/client/flow.ts
303
+ function flowScenarios(flow) {
304
+ const declared = [
305
+ ...flow.scenario === void 0 ? [] : [flow.scenario],
306
+ ...flow.scenarios ?? []
307
+ ];
308
+ return declared.map((s) => {
309
+ if (typeof s === "string") return { name: s };
310
+ if (s && typeof s === "object" && typeof s.name === "string") return s;
311
+ throw new Error(
312
+ `A flow's scenario is a name or { name, options }, not ${JSON.stringify(s)}`
313
+ );
314
+ });
315
+ }
316
+ function parseScenarioFlag(value) {
317
+ const at = value.indexOf("=");
318
+ if (at === -1) return { name: value };
319
+ const name = value.slice(0, at);
320
+ const raw = value.slice(at + 1);
321
+ try {
322
+ return { name, options: JSON.parse(raw) };
323
+ } catch {
324
+ throw new Error(
325
+ `--scenario ${name}: the options after "=" must be JSON, got ${raw}`
326
+ );
327
+ }
328
+ }
329
+ function mergeScenarios(declared, extra) {
330
+ const names = new Set(extra.map((s) => s.name));
331
+ return [...declared.filter((s) => !names.has(s.name)), ...extra];
332
+ }
333
+ var failedLogs = (error) => error instanceof AgentBridgeCallError ? error.logs : [];
334
+ async function runFlow(bridge, flow, options = {}) {
335
+ const print = options.print ?? console.log;
336
+ const needs = mergeScenarios(flowScenarios(flow), options.scenarios ?? []);
337
+ let n = 0;
338
+ let total = 0;
339
+ let errors = 0;
340
+ const t0 = performance.now();
341
+ const report = (logs) => {
342
+ errors += logs.length;
343
+ for (const e of logs) print(` ${logLine(e)}`);
344
+ };
345
+ const timed = async (tool, ...args) => {
346
+ try {
347
+ const result = await bridge.timed(tool, ...args);
348
+ report(result.logs);
349
+ return result;
350
+ } catch (error) {
351
+ report(failedLogs(error));
352
+ throw error;
353
+ }
354
+ };
355
+ const call = async (tool, ...args) => (await timed(tool, ...args)).value;
356
+ const step = async (label, tool, ...args) => {
357
+ const { value, ms, logs } = await bridge.timed(tool, ...args).catch((error) => {
358
+ report(failedLogs(error));
359
+ throw error;
360
+ });
361
+ total += ms;
362
+ print(
363
+ `${String(++n).padStart(2, "0")} ${label.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
364
+ );
365
+ report(logs);
366
+ return value;
367
+ };
368
+ const restoreErrors = [];
369
+ const applied = {};
370
+ if (needs.length && !bridge.tools().some((t) => t.name === "scenario.apply"))
371
+ throw new Error(
372
+ `The flow needs scenario "${needs[0].name}", but the app defines none: pass \`scenarios\` to useAgentBridge.`
373
+ );
374
+ try {
375
+ for (const { name, options: options2 } of needs) {
376
+ const args = options2 === void 0 ? [name] : [name, options2];
377
+ applied[name] = await step(`scenario ${name}`, "scenario.apply", ...args);
378
+ }
379
+ await flow.default({
380
+ bridge: { ...bridge, timed, call },
381
+ call,
382
+ step,
383
+ scenarios: applied
384
+ });
385
+ } finally {
386
+ if (needs.length) {
387
+ try {
388
+ const result = await step("restore", "bridge.restore");
389
+ for (const [name, value] of Object.entries(result ?? {})) {
390
+ const error = value?.error;
391
+ if (error !== void 0) restoreErrors.push(`${name}: ${String(error)}`);
392
+ }
393
+ } catch (error) {
394
+ restoreErrors.push(
395
+ `bridge.restore: ${error instanceof Error ? error.message : String(error)}`
396
+ );
397
+ }
398
+ for (const e of restoreErrors) print(` ! restore failed: ${e}`);
399
+ }
400
+ }
401
+ print(
402
+ `${n} steps, ${total.toFixed(1)} ms in calls, ${(performance.now() - t0).toFixed(0)} ms wall (${bridge.transport})${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}`
403
+ );
404
+ return { steps: n, errors, restoreErrors };
405
+ }
406
+
291
407
  // src/client/session/client.ts
292
408
  import { createConnection } from "net";
293
409
  import { createInterface } from "readline";
@@ -556,6 +672,10 @@ export {
556
672
  metroHost,
557
673
  RUNTIME_MARKER,
558
674
  openConnection,
675
+ logLine,
676
+ flowScenarios,
677
+ parseScenarioFlag,
678
+ runFlow,
559
679
  stateDir,
560
680
  sessionFiles,
561
681
  checkName,
package/dist/cli.js CHANGED
@@ -5,21 +5,25 @@ import {
5
5
  checkName,
6
6
  connect,
7
7
  connectSession,
8
+ flowScenarios,
8
9
  formatDuration,
9
10
  isAlive,
10
11
  listDevices,
11
12
  listSessions,
13
+ logLine,
12
14
  metroHost,
13
15
  openConnection,
14
16
  parseDuration,
17
+ parseScenarioFlag,
15
18
  pickSession,
16
19
  readSession,
17
20
  removeSessionFiles,
21
+ runFlow,
18
22
  sessionFiles,
19
23
  sessionRequest,
20
24
  stateDir,
21
25
  writePrivate
22
- } from "./chunk-UEWFQWCY.js";
26
+ } from "./chunk-S2FYN2R5.js";
23
27
 
24
28
  // src/cli.ts
25
29
  import { readFile } from "fs/promises";
@@ -27,14 +31,6 @@ import { resolve } from "path";
27
31
  import { pathToFileURL } from "url";
28
32
  import { parseArgs } from "util";
29
33
 
30
- // src/client/log-lines.ts
31
- function logLine(entry) {
32
- const where = entry.during ? ` during ${entry.during}` : entry.after ? ` after ${entry.after}` : "";
33
- const first = entry.message.split("\n")[0] ?? "";
34
- const text = first.length > 300 ? `${first.slice(0, 300)}...` : first;
35
- return `! ${entry.level}${where}: ${text}`;
36
- }
37
-
38
34
  // src/client/session/cli.ts
39
35
  import { spawn } from "child_process";
40
36
  import { existsSync, readFileSync, rmSync as rmSync2 } from "fs";
@@ -480,6 +476,9 @@ Usage
480
476
  agent-bridge tools Tools the app exposes
481
477
  agent-bridge call <tool> [args] Call a tool. args: a JSON array, or one JSON value
482
478
  agent-bridge run <flow.mjs|.ts> Run a flow: export default async ({ step, call }) => {}
479
+ export const scenario = 'signedIn' applies it first
480
+ and runs bridge.restore after, even on failure
481
+ agent-bridge scenarios Scenarios the app defines (scenario.apply applies one)
483
482
  agent-bridge assert-absent <files...> Fail if a release bundle contains the bridge
484
483
 
485
484
  Sessions: one connection for all of an agent's calls
@@ -494,6 +493,8 @@ Options
494
493
  --transport <name> auto (default), expo or cdp
495
494
  --timeout <ms> Per-call timeout (default 10000)
496
495
  --strict run: exit non-zero if the app logged an error
496
+ --scenario <name> run: also apply this scenario (repeatable). Options as
497
+ JSON after "=": --scenario 'signedIn={"user":{"name":"Ada"}}'
497
498
  --session <name> Use this session (env AGENT_BRIDGE_SESSION)
498
499
  --no-session Connect directly even if a session is running
499
500
  `;
@@ -517,6 +518,7 @@ async function main() {
517
518
  transport: { type: "string" },
518
519
  timeout: { type: "string" },
519
520
  strict: { type: "boolean" },
521
+ scenario: { type: "string", multiple: true },
520
522
  name: { type: "string" },
521
523
  idle: { type: "string" },
522
524
  keep: { type: "boolean" },
@@ -583,47 +585,34 @@ async function main() {
583
585
  const [file] = rest;
584
586
  if (!file) throw new Error("Usage: agent-bridge run <flow file>");
585
587
  const flow = await import(pathToFileURL(resolve(file)).href);
588
+ flowScenarios(flow);
589
+ const extra = (values.scenario ?? []).map(parseScenarioFlag);
586
590
  return withBridge(async (bridge) => {
587
- let n = 0;
588
- let total = 0;
589
- let errors = 0;
590
- const t0 = performance.now();
591
- const report = (logs) => {
592
- errors += logs.length;
593
- for (const e of logs) console.log(` ${logLine(e)}`);
594
- };
595
- const timed = async (tool, ...args) => {
596
- try {
597
- const result = await bridge.timed(tool, ...args);
598
- report(result.logs);
599
- return result;
600
- } catch (error) {
601
- report(failedLogs(error));
602
- throw error;
603
- }
604
- };
605
- const call = async (tool, ...args) => (await timed(tool, ...args)).value;
606
- const step = async (label, tool, ...args) => {
607
- const { value, ms, logs } = await bridge.timed(tool, ...args).catch(
608
- (error) => {
609
- report(failedLogs(error));
610
- throw error;
611
- }
591
+ const { errors, restoreErrors } = await runFlow(bridge, flow, {
592
+ scenarios: extra
593
+ });
594
+ if (values.strict && errors || restoreErrors.length)
595
+ process.exitCode = 1;
596
+ });
597
+ }
598
+ case "scenarios":
599
+ return withBridge(async (bridge) => {
600
+ if (!bridge.tools().some((t) => t.name === "scenario.list")) {
601
+ console.log(
602
+ `${bridge.device.name} defines no scenarios (pass \`scenarios\` to useAgentBridge).`
612
603
  );
613
- total += ms;
604
+ return;
605
+ }
606
+ const list2 = await bridge.call("scenario.list");
607
+ console.log(`${bridge.device.name} via ${bridge.transport}`);
608
+ for (const s of list2) {
614
609
  console.log(
615
- `${String(++n).padStart(2, "0")} ${label.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
610
+ ` ${s.name.padEnd(20)} ${s.active ? "(active) " : ""}${s.description ?? ""}`
616
611
  );
617
- report(logs);
618
- return value;
619
- };
620
- await flow.default({ bridge: { ...bridge, timed, call }, call, step });
621
- console.log(
622
- `${n} steps, ${total.toFixed(1)} ms in calls, ${(performance.now() - t0).toFixed(0)} ms wall (${bridge.transport})${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""}`
623
- );
624
- if (values.strict && errors) process.exitCode = 1;
612
+ if (s.options)
613
+ console.log(` ${"".padEnd(20)} options: ${JSON.stringify(s.options)}`);
614
+ }
625
615
  });
626
- }
627
616
  case "session":
628
617
  return sessionCommand(rest[0], values);
629
618
  case DAEMON_COMMAND: