@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.
- package/README.md +168 -18
- package/dist/adapters/ably.cjs +344 -0
- package/dist/adapters/ably.d.cts +37 -0
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/react-native-mmkv.d.cts +1 -1
- package/dist/adapters/socket-io.cjs +317 -0
- package/dist/adapters/socket-io.d.cts +21 -0
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.d.cts +1 -1
- package/dist/{chunk-UEWFQWCY.js → chunk-S2FYN2R5.js} +122 -2
- package/dist/cli.js +34 -45
- package/dist/client/index.cjs +109 -4
- package/dist/client/index.d.ts +44 -1
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +186 -21
- package/dist/network/index.d.cts +45 -2
- package/dist/noop/ably.cjs +26 -0
- package/dist/noop/index.cjs +9 -0
- package/dist/noop/network.cjs +7 -1
- package/dist/noop/realtime.cjs +40 -0
- package/dist/noop/socket-io.cjs +26 -0
- package/dist/realtime/index.cjs +279 -0
- package/dist/realtime/index.d.cts +138 -0
- package/dist/runtime/index.cjs +209 -13
- package/dist/runtime/index.d.cts +23 -3
- package/dist/{types-C6DUUHnB.d.cts → types-6kKQsLC1.d.cts} +40 -1
- package/entries/ably.cjs +9 -0
- package/entries/realtime.cjs +9 -0
- package/entries/socket.io.cjs +9 -0
- package/package.json +21 -4
- package/skills/agent-bridge/SKILL.md +68 -5
|
@@ -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 };
|
|
@@ -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-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
610
|
+
` ${s.name.padEnd(20)} ${s.active ? "(active) " : ""}${s.description ?? ""}`
|
|
616
611
|
);
|
|
617
|
-
|
|
618
|
-
|
|
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:
|