@avasapp/agent-bridge 0.1.0 → 0.2.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 +96 -8
- package/dist/adapters/ably.cjs +344 -0
- package/dist/adapters/ably.d.cts +37 -0
- package/dist/adapters/socket-io.cjs +317 -0
- package/dist/adapters/socket-io.d.cts +21 -0
- package/dist/{chunk-UEWFQWCY.js → chunk-LBYZ5JHF.js} +5 -2
- package/dist/cli.js +1 -1
- package/dist/client/index.cjs +5 -2
- package/dist/client/index.js +1 -1
- package/dist/noop/ably.cjs +26 -0
- 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/entries/ably.cjs +9 -0
- package/entries/realtime.cjs +9 -0
- package/entries/socket.io.cjs +9 -0
- package/package.json +20 -4
- package/skills/agent-bridge/SKILL.md +41 -5
|
@@ -0,0 +1,279 @@
|
|
|
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/realtime/index.ts
|
|
21
|
+
var realtime_exports = {};
|
|
22
|
+
__export(realtime_exports, {
|
|
23
|
+
createRealtimeTap: () => createRealtimeTap,
|
|
24
|
+
fakeableConnection: () => fakeableConnection,
|
|
25
|
+
realtimeAdapter: () => realtimeAdapter,
|
|
26
|
+
tapListeners: () => tapListeners
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(realtime_exports);
|
|
29
|
+
|
|
30
|
+
// src/realtime/patch.ts
|
|
31
|
+
var list = (names) => Array.isArray(names) ? names : [names];
|
|
32
|
+
var sameKey = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
33
|
+
function split(args) {
|
|
34
|
+
const last = args[args.length - 1];
|
|
35
|
+
return typeof last === "function" ? { key: args.slice(0, -1), listener: last } : { key: args };
|
|
36
|
+
}
|
|
37
|
+
function tapListeners(tap, target, options) {
|
|
38
|
+
const methods = target;
|
|
39
|
+
const tracked = [];
|
|
40
|
+
const spread = options.message === "args";
|
|
41
|
+
for (const name of list(options.add)) {
|
|
42
|
+
const add = methods[name];
|
|
43
|
+
if (typeof add !== "function") continue;
|
|
44
|
+
methods[name] = function(...args) {
|
|
45
|
+
const { key, listener: original } = split(args);
|
|
46
|
+
const channel = original && options.channel(key);
|
|
47
|
+
if (!original || channel === void 0) return add.apply(this, args);
|
|
48
|
+
const accepts = options.accepts;
|
|
49
|
+
const { listener, unsubscribe } = tap.wrap(
|
|
50
|
+
channel,
|
|
51
|
+
(message, ...rest) => spread ? original.apply(target, message) : original.call(target, message, ...rest),
|
|
52
|
+
accepts ? { accepts: (m) => accepts(key, m) } : {}
|
|
53
|
+
);
|
|
54
|
+
const wrapped = spread ? (...received) => listener(received) : (message, ...rest) => listener(message, ...rest);
|
|
55
|
+
tracked.push({ key, original, wrapped, unsubscribe });
|
|
56
|
+
return add.apply(this, [...key, wrapped]);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
for (const name of list(options.remove)) {
|
|
60
|
+
const remove = methods[name];
|
|
61
|
+
if (typeof remove !== "function") continue;
|
|
62
|
+
methods[name] = function(...args) {
|
|
63
|
+
const { key, listener } = split(args);
|
|
64
|
+
const hits = tracked.filter(
|
|
65
|
+
(t) => (!listener || t.original === listener || t.original.fn === listener) && (!key.length || sameKey(t.key, key))
|
|
66
|
+
);
|
|
67
|
+
for (const hit of hits) {
|
|
68
|
+
tracked.splice(tracked.indexOf(hit), 1);
|
|
69
|
+
hit.unsubscribe();
|
|
70
|
+
}
|
|
71
|
+
if (!listener || !hits.length) return remove.apply(this, args);
|
|
72
|
+
let result;
|
|
73
|
+
for (const hit of hits) result = remove.apply(this, [...hit.key, hit.wrapped]);
|
|
74
|
+
return result;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function fakeableConnection(target, options) {
|
|
79
|
+
const methods = target;
|
|
80
|
+
const events = methods?.[options.events];
|
|
81
|
+
if (!methods || typeof events !== "function") return void 0;
|
|
82
|
+
const read = options.read ?? String;
|
|
83
|
+
const write = options.write ?? ((state) => state);
|
|
84
|
+
const values = methods;
|
|
85
|
+
let fake = null;
|
|
86
|
+
let real = values[options.property];
|
|
87
|
+
const readReal = () => read(options.fakeProperty === false ? values[options.property] : real);
|
|
88
|
+
if (options.fakeProperty !== false)
|
|
89
|
+
Object.defineProperty(target, options.property, {
|
|
90
|
+
configurable: true,
|
|
91
|
+
enumerable: true,
|
|
92
|
+
get: () => fake === null ? real : write(fake),
|
|
93
|
+
set: (value) => {
|
|
94
|
+
real = value;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
methods[options.events] = function(...args) {
|
|
98
|
+
return fake === null ? events.apply(this, args) : void 0;
|
|
99
|
+
};
|
|
100
|
+
const emit = (...args) => void events.apply(target, args);
|
|
101
|
+
return {
|
|
102
|
+
state: readReal,
|
|
103
|
+
states: options.states,
|
|
104
|
+
live: options.live,
|
|
105
|
+
fake: (state, previous) => {
|
|
106
|
+
fake = state;
|
|
107
|
+
const current = state ?? readReal();
|
|
108
|
+
if (current !== previous)
|
|
109
|
+
options.announce(emit, { current, previous, faked: state !== null });
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function realtimeAdapter(install) {
|
|
114
|
+
const taps = /* @__PURE__ */ new WeakMap();
|
|
115
|
+
return (client, options = {}) => {
|
|
116
|
+
let tools = taps.get(client);
|
|
117
|
+
if (!tools) taps.set(client, tools = install(client, options).tools);
|
|
118
|
+
return tools;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/realtime/tap.ts
|
|
123
|
+
var LOG_SIZE = 50;
|
|
124
|
+
var ALL = "*";
|
|
125
|
+
var defaultDescribe = (message) => {
|
|
126
|
+
const id = message?.id;
|
|
127
|
+
return typeof id === "string" ? { data: message, id } : { data: message };
|
|
128
|
+
};
|
|
129
|
+
var sameMessage = (a, b) => a === b || Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => v === b[i]);
|
|
130
|
+
function createRealtimeTap(options = {}) {
|
|
131
|
+
const ns = options.namespace ?? "realtime";
|
|
132
|
+
const describe = options.describe ?? defaultDescribe;
|
|
133
|
+
const toMessage = options.toMessage ?? ((values) => values[0]);
|
|
134
|
+
const connection = options.connection;
|
|
135
|
+
const live = connection?.live ?? "connected";
|
|
136
|
+
const channels = /* @__PURE__ */ new Map();
|
|
137
|
+
const mutes = /* @__PURE__ */ new Map();
|
|
138
|
+
const log = [];
|
|
139
|
+
const last = /* @__PURE__ */ new Map();
|
|
140
|
+
let injectedCount = 0;
|
|
141
|
+
let faked = null;
|
|
142
|
+
const muteFor = (channel) => mutes.get(channel) ?? mutes.get(ALL);
|
|
143
|
+
const liveChannels = () => [...channels.keys()].join(", ") || "none";
|
|
144
|
+
function record(channel, message, injected, dropped) {
|
|
145
|
+
log.push({
|
|
146
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
147
|
+
channel,
|
|
148
|
+
...describe(message),
|
|
149
|
+
injected,
|
|
150
|
+
...dropped && { dropped }
|
|
151
|
+
});
|
|
152
|
+
if (log.length > LOG_SIZE) log.splice(0, log.length - LOG_SIZE);
|
|
153
|
+
}
|
|
154
|
+
function receive(channel, entry, message, rest) {
|
|
155
|
+
const previous = last.get(channel);
|
|
156
|
+
const dropped = muteFor(channel) ? "muted" : faked !== null && faked !== live ? "connection" : void 0;
|
|
157
|
+
if (previous && !previous.seen.has(entry) && sameMessage(previous.message, message)) {
|
|
158
|
+
previous.seen.add(entry);
|
|
159
|
+
} else {
|
|
160
|
+
last.set(channel, { message, seen: /* @__PURE__ */ new Set([entry]) });
|
|
161
|
+
record(channel, message, false, dropped);
|
|
162
|
+
}
|
|
163
|
+
if (!dropped) entry.deliver(message, ...rest);
|
|
164
|
+
}
|
|
165
|
+
const wrap = (channel, listener, wrapOptions = {}) => {
|
|
166
|
+
const entry = {
|
|
167
|
+
deliver: listener,
|
|
168
|
+
accepts: wrapOptions.accepts
|
|
169
|
+
};
|
|
170
|
+
let entries = channels.get(channel);
|
|
171
|
+
if (!entries) channels.set(channel, entries = /* @__PURE__ */ new Set());
|
|
172
|
+
entries.add(entry);
|
|
173
|
+
return {
|
|
174
|
+
listener: (message, ...rest) => receive(channel, entry, message, rest),
|
|
175
|
+
unsubscribe: () => {
|
|
176
|
+
const current = channels.get(channel);
|
|
177
|
+
if (!current?.delete(entry) || current.size) return;
|
|
178
|
+
channels.delete(channel);
|
|
179
|
+
last.delete(channel);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
function setMute(channel, mute) {
|
|
184
|
+
if (typeof channel !== "string" || !channel)
|
|
185
|
+
throw new Error(`Pass a channel name, or "${ALL}" for every channel`);
|
|
186
|
+
if (mute) mutes.set(channel, mute);
|
|
187
|
+
else if (channel === ALL) mutes.clear();
|
|
188
|
+
else mutes.delete(channel);
|
|
189
|
+
return { muted: [...mutes.keys()] };
|
|
190
|
+
}
|
|
191
|
+
function setConnection(conn, state) {
|
|
192
|
+
const shown = faked ?? conn.state();
|
|
193
|
+
if (state !== null && typeof state !== "string")
|
|
194
|
+
throw new Error('Pass a state such as "disconnected", or null for the real one');
|
|
195
|
+
if (state !== null && conn.states && !conn.states.includes(state))
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Unknown state "${state}". Known: ${conn.states.join(", ")}`
|
|
198
|
+
);
|
|
199
|
+
if (state === faked) return;
|
|
200
|
+
faked = state;
|
|
201
|
+
conn.fake(state, shown);
|
|
202
|
+
}
|
|
203
|
+
const tools = {
|
|
204
|
+
[`${ns}.channels`]: {
|
|
205
|
+
description: "Channels the app listens on: listener count, muted, and what the library says about each.",
|
|
206
|
+
run: () => [...channels].map(([name, entries]) => ({
|
|
207
|
+
name,
|
|
208
|
+
listeners: entries.size,
|
|
209
|
+
muted: Boolean(muteFor(name)),
|
|
210
|
+
...options.channelInfo?.(name)
|
|
211
|
+
}))
|
|
212
|
+
},
|
|
213
|
+
[`${ns}.log`]: {
|
|
214
|
+
description: `Recent messages, real and injected, newest first (last ${LOG_SIZE}). Filter: { channel?, event?, limit? }; clear: true empties the log after reading.`,
|
|
215
|
+
run: (filter = {}) => {
|
|
216
|
+
const entries = log.filter(
|
|
217
|
+
(e) => (!filter.channel || e.channel === filter.channel) && (!filter.event || e.event === filter.event)
|
|
218
|
+
).reverse().slice(0, filter.limit ?? 20);
|
|
219
|
+
if (filter.clear) log.length = 0;
|
|
220
|
+
return entries;
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
[`${ns}.emit`]: {
|
|
224
|
+
description: "Deliver a fake message to every listener on a channel: [channel, message]. Runs the app's own handlers. Returns { id, delivered }.",
|
|
225
|
+
run: (channel, ...values) => {
|
|
226
|
+
const entries = channels.get(channel);
|
|
227
|
+
if (!entries?.size)
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Nothing listens on "${channel}". Live channels: ${liveChannels()}`
|
|
230
|
+
);
|
|
231
|
+
injectedCount += 1;
|
|
232
|
+
const message = toMessage(values, {
|
|
233
|
+
channel,
|
|
234
|
+
id: `agent-bridge-${injectedCount}`
|
|
235
|
+
});
|
|
236
|
+
const id = describe(message).id;
|
|
237
|
+
if (muteFor(channel)?.dropInjected) {
|
|
238
|
+
record(channel, message, true, "muted");
|
|
239
|
+
return { id, delivered: 0 };
|
|
240
|
+
}
|
|
241
|
+
record(channel, message, true);
|
|
242
|
+
let delivered = 0;
|
|
243
|
+
for (const entry of [...entries]) {
|
|
244
|
+
if (entry.accepts && !entry.accepts(message)) continue;
|
|
245
|
+
entry.deliver(message);
|
|
246
|
+
delivered += 1;
|
|
247
|
+
}
|
|
248
|
+
return { id, delivered };
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
[`${ns}.mute`]: {
|
|
252
|
+
description: `Drop real messages on a channel ("${ALL}" for all) until unmuted. Injected messages still get through unless { dropInjected: true }.`,
|
|
253
|
+
run: (channel, muteOptions = {}) => setMute(channel, { dropInjected: Boolean(muteOptions.dropInjected) })
|
|
254
|
+
},
|
|
255
|
+
[`${ns}.unmute`]: {
|
|
256
|
+
description: `Stop dropping messages on a channel ("${ALL}" for all).`,
|
|
257
|
+
run: (channel) => setMute(channel, null)
|
|
258
|
+
},
|
|
259
|
+
[`${ns}.restore`]: {
|
|
260
|
+
description: "Undo the agent: unmute every channel and go back to the real connection state.",
|
|
261
|
+
run: () => {
|
|
262
|
+
const unmuted = [...mutes.keys()];
|
|
263
|
+
mutes.clear();
|
|
264
|
+
const connectionFaked = faked !== null;
|
|
265
|
+
if (connection && connectionFaked) setConnection(connection, null);
|
|
266
|
+
return { unmuted, connectionFaked };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
if (connection)
|
|
271
|
+
tools[`${ns}.connection`] = {
|
|
272
|
+
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.`,
|
|
273
|
+
run: (...args) => {
|
|
274
|
+
if (args.length) setConnection(connection, args[0] ?? null);
|
|
275
|
+
return { state: faked ?? connection.state(), real: connection.state(), faked: faked !== null };
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
return { wrap, tools };
|
|
279
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { T as Tools } from '../types-C6DUUHnB.cjs';
|
|
2
|
+
|
|
3
|
+
/** How a message shows up in `realtime.log`. */
|
|
4
|
+
type MessageSummary = {
|
|
5
|
+
/** The event or message name, when the library has one. */
|
|
6
|
+
event?: string;
|
|
7
|
+
data?: unknown;
|
|
8
|
+
id?: string;
|
|
9
|
+
};
|
|
10
|
+
/** Lets the agent fake the client's connection state. */
|
|
11
|
+
type RealtimeConnection = {
|
|
12
|
+
/** The client's real state, e.g. "connected". */
|
|
13
|
+
state: () => string;
|
|
14
|
+
/**
|
|
15
|
+
* Show `state` to the app instead of the real one, as if the client had
|
|
16
|
+
* moved there from `previous`. `null` goes back to the real state.
|
|
17
|
+
*/
|
|
18
|
+
fake: (state: string | null, previous: string) => void;
|
|
19
|
+
/** States the agent may pick, for error messages. */
|
|
20
|
+
states?: readonly string[];
|
|
21
|
+
/** The state in which real messages flow. Defaults to "connected". */
|
|
22
|
+
live?: string;
|
|
23
|
+
};
|
|
24
|
+
type RealtimeTapOptions<M> = {
|
|
25
|
+
/** Tool name prefix. Defaults to "realtime"; set it when an app has two taps. */
|
|
26
|
+
namespace?: string;
|
|
27
|
+
/** What to log for a message. Defaults to the message as `data`, with its `id` if it has one. */
|
|
28
|
+
describe?: (message: M) => MessageSummary;
|
|
29
|
+
/**
|
|
30
|
+
* Builds the message `realtime.emit` delivers, from the JSON values the
|
|
31
|
+
* agent passed after the channel. Defaults to the first value.
|
|
32
|
+
*/
|
|
33
|
+
toMessage?: (values: unknown[], context: {
|
|
34
|
+
channel: string;
|
|
35
|
+
id: string;
|
|
36
|
+
}) => M;
|
|
37
|
+
/** Extra fields for `realtime.channels`, e.g. the library's channel state. */
|
|
38
|
+
channelInfo?: (channel: string) => Record<string, unknown>;
|
|
39
|
+
/** Adds `realtime.connection`, to fake connection states. */
|
|
40
|
+
connection?: RealtimeConnection;
|
|
41
|
+
};
|
|
42
|
+
type WrapOptions<M> = {
|
|
43
|
+
/**
|
|
44
|
+
* Whether this listener would get the message from the library, e.g. an
|
|
45
|
+
* event-name filter. Injected messages skip listeners that say no.
|
|
46
|
+
*/
|
|
47
|
+
accepts?: (message: M) => boolean;
|
|
48
|
+
};
|
|
49
|
+
type RealtimeTap<M> = {
|
|
50
|
+
/**
|
|
51
|
+
* Wraps the app's listener for `channel`. Hand `listener` to the library
|
|
52
|
+
* instead of the original, and call `unsubscribe` when the app unsubscribes.
|
|
53
|
+
* Arguments after the message reach the app's listener untouched.
|
|
54
|
+
*/
|
|
55
|
+
wrap: (channel: string, listener: (message: M, ...rest: never[]) => void, options?: WrapOptions<M>) => {
|
|
56
|
+
listener: (message: M, ...rest: unknown[]) => void;
|
|
57
|
+
unsubscribe: () => void;
|
|
58
|
+
};
|
|
59
|
+
/** `realtime.*`, or `<namespace>.*`. */
|
|
60
|
+
tools: Tools;
|
|
61
|
+
};
|
|
62
|
+
type RealtimeLogEntry = MessageSummary & {
|
|
63
|
+
at: string;
|
|
64
|
+
channel: string;
|
|
65
|
+
injected: boolean;
|
|
66
|
+
/** Why the app didn't get it. */
|
|
67
|
+
dropped?: 'muted' | 'connection';
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Lets the agent see, fake and drop realtime messages, whatever library
|
|
71
|
+
* delivers them. Wrap each listener with `wrap`, and add `tools` to the bridge.
|
|
72
|
+
*/
|
|
73
|
+
declare function createRealtimeTap<M = unknown>(options?: RealtimeTapOptions<M>): RealtimeTap<M>;
|
|
74
|
+
|
|
75
|
+
type TapListenersOptions<M> = {
|
|
76
|
+
/** Methods that add a listener, e.g. "on" or ["on", "addEventListener"]. */
|
|
77
|
+
add: string | string[];
|
|
78
|
+
/** Methods that remove one, e.g. "off". */
|
|
79
|
+
remove: string | string[];
|
|
80
|
+
/**
|
|
81
|
+
* The channel a call is for, from the arguments before the listener (its
|
|
82
|
+
* "key", e.g. the event name). Return undefined to leave the call alone.
|
|
83
|
+
*/
|
|
84
|
+
channel: (key: unknown[]) => string | undefined;
|
|
85
|
+
/** Whether a listener added with `key` would get `message`, e.g. an event-name filter. */
|
|
86
|
+
accepts?: (key: unknown[], message: M) => boolean;
|
|
87
|
+
/**
|
|
88
|
+
* "first" (default): the message is the listener's first argument, and the
|
|
89
|
+
* rest reach the listener untouched. "args": every argument, as an array.
|
|
90
|
+
*/
|
|
91
|
+
message?: 'first' | 'args';
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Routes a client's add/remove-listener methods through `tap`, e.g.
|
|
95
|
+
* `socket.on` / `socket.off` or `channel.subscribe` / `channel.unsubscribe`.
|
|
96
|
+
* Removing works in every usual form: all, by key, by listener, or both.
|
|
97
|
+
*/
|
|
98
|
+
declare function tapListeners<M>(tap: RealtimeTap<M>, target: object, options: TapListenersOptions<M>): void;
|
|
99
|
+
type FakeableConnectionOptions = {
|
|
100
|
+
/** The property holding the state, e.g. "state" or "connected". */
|
|
101
|
+
property: string;
|
|
102
|
+
/** The method the client tells listeners about changes with. Silent while a state is faked. */
|
|
103
|
+
events: string;
|
|
104
|
+
/** States the agent may pick. */
|
|
105
|
+
states: readonly string[];
|
|
106
|
+
/** The state in which real messages flow. Defaults to "connected". */
|
|
107
|
+
live?: string;
|
|
108
|
+
/** Property value to state name. Defaults to the value itself. */
|
|
109
|
+
read?: (value: unknown) => string;
|
|
110
|
+
/** State name to property value. Defaults to the name itself. */
|
|
111
|
+
write?: (state: string) => unknown;
|
|
112
|
+
/**
|
|
113
|
+
* false leaves the property showing the real state. Use it when the client
|
|
114
|
+
* reads the property itself to decide whether to deliver or send (socket.io
|
|
115
|
+
* does), or faking it would hold real traffic back. Defaults to true.
|
|
116
|
+
*/
|
|
117
|
+
fakeProperty?: boolean;
|
|
118
|
+
/** Tells the app about a change, through the client's own (unsilenced) events method. */
|
|
119
|
+
announce: (emit: (...args: unknown[]) => void, change: {
|
|
120
|
+
current: string;
|
|
121
|
+
previous: string;
|
|
122
|
+
faked: boolean;
|
|
123
|
+
}) => void;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Lets `realtime.connection` fake a client's connection state: the property
|
|
127
|
+
* reads the fake (unless `fakeProperty: false`), real changes still land
|
|
128
|
+
* underneath but aren't announced, and going back announces the real state. Undefined when `target` lacks
|
|
129
|
+
* the events method, so the tool is left out rather than broken.
|
|
130
|
+
*/
|
|
131
|
+
declare function fakeableConnection(target: object | undefined, options: FakeableConnectionOptions): RealtimeConnection | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* Turns an install function into an adapter's `xxxTools(client, options)`.
|
|
134
|
+
* The client is patched on the first call; later calls return the same tools.
|
|
135
|
+
*/
|
|
136
|
+
declare function realtimeAdapter<C extends object, O extends object>(install: (client: C, options: O) => Pick<RealtimeTap<unknown>, 'tools'>): (client: C, options?: O) => Tools;
|
|
137
|
+
|
|
138
|
+
export { type FakeableConnectionOptions, type MessageSummary, type RealtimeConnection, type RealtimeLogEntry, type RealtimeTap, type RealtimeTapOptions, type TapListenersOptions, type WrapOptions, createRealtimeTap, fakeableConnection, realtimeAdapter, tapListeners };
|
package/entries/ably.cjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/ably.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/ably.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/realtime.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/realtime/index.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/socket-io.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/socket-io.cjs')
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avasapp/agent-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Let coding agents drive a running React Native app directly: seed state, flip flags, navigate and check the screen in about a millisecond per call, with no taps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/avas-app/agent-bridge/issues"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
|
-
"access": "public"
|
|
17
|
-
"provenance": true
|
|
16
|
+
"access": "public"
|
|
18
17
|
},
|
|
19
18
|
"engines": {
|
|
20
19
|
"node": ">=20"
|
|
@@ -51,6 +50,18 @@
|
|
|
51
50
|
"types": "./dist/network/index.d.cts",
|
|
52
51
|
"default": "./entries/network.cjs"
|
|
53
52
|
},
|
|
53
|
+
"./realtime": {
|
|
54
|
+
"types": "./dist/realtime/index.d.cts",
|
|
55
|
+
"default": "./entries/realtime.cjs"
|
|
56
|
+
},
|
|
57
|
+
"./ably": {
|
|
58
|
+
"types": "./dist/adapters/ably.d.cts",
|
|
59
|
+
"default": "./entries/ably.cjs"
|
|
60
|
+
},
|
|
61
|
+
"./socket.io": {
|
|
62
|
+
"types": "./dist/adapters/socket-io.d.cts",
|
|
63
|
+
"default": "./entries/socket.io.cjs"
|
|
64
|
+
},
|
|
54
65
|
"./client": {
|
|
55
66
|
"types": "./dist/client/index.d.ts",
|
|
56
67
|
"import": "./dist/client/index.js",
|
|
@@ -77,7 +88,10 @@
|
|
|
77
88
|
"cdp",
|
|
78
89
|
"tanstack-query",
|
|
79
90
|
"zustand",
|
|
80
|
-
"mmkv"
|
|
91
|
+
"mmkv",
|
|
92
|
+
"realtime",
|
|
93
|
+
"ably",
|
|
94
|
+
"socket.io"
|
|
81
95
|
],
|
|
82
96
|
"scripts": {
|
|
83
97
|
"build": "rm -rf dist && tsup",
|
|
@@ -113,6 +127,8 @@
|
|
|
113
127
|
"@types/bun": "^1.4.2",
|
|
114
128
|
"@types/react": "^19.3.0",
|
|
115
129
|
"@types/ws": "^8.18.1",
|
|
130
|
+
"ably": "^2.29.0",
|
|
131
|
+
"socket.io-client": "^4.8.4",
|
|
116
132
|
"tsup": "^8.5.1",
|
|
117
133
|
"typescript": "~6.0.3",
|
|
118
134
|
"zustand": "^5.0.15"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agent-bridge
|
|
3
|
-
description: "Drive a running React Native or Expo dev build directly through @avasapp/agent-bridge: see what's on screen, fill in forms, press buttons, seed query data, flip feature flags, set store state, navigate, mock the network and undo it all, in milliseconds per call instead of tapping through the app. Use whenever you need the app in a particular state to test a change, or need to verify what a screen shows."
|
|
3
|
+
description: "Drive a running React Native or Expo dev build directly through @avasapp/agent-bridge: see what's on screen, fill in forms, press buttons, seed query data, flip feature flags, set store state, navigate, mock the network, fake realtime messages and undo it all, in milliseconds per call instead of tapping through the app. Pairs with agent-device, which opens the app and covers native UI, real taps and screenshots. Use whenever you need the app in a particular state to test a change, or need to verify what a screen shows."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Driving an app with agent-bridge
|
|
@@ -8,6 +8,22 @@ description: "Drive a running React Native or Expo dev build directly through @a
|
|
|
8
8
|
The app mounts `useAgentBridge({ tools })` in a dev build. You call those tools
|
|
9
9
|
by name. Nothing is tapped, so there is no setup to click through.
|
|
10
10
|
|
|
11
|
+
## Pair it with agent-device
|
|
12
|
+
|
|
13
|
+
agent-bridge works inside the app's JavaScript; agent-device works the device.
|
|
14
|
+
Use both, each for what it's good at:
|
|
15
|
+
|
|
16
|
+
| agent-bridge | agent-device |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Put the app in a state: flags, query data, store values, network mocks, realtime messages | Install and open the app, reload, relaunch |
|
|
19
|
+
| Navigate, fill forms, press buttons, check text on screen | System alerts, permission prompts, native sheets, the keyboard |
|
|
20
|
+
| Undo it all with `bridge.restore` | Screenshots, recordings, one real tap to prove the UI responds |
|
|
21
|
+
|
|
22
|
+
A typical run: `agent-device open` the app, set up and check the screen with
|
|
23
|
+
agent-bridge, then finish with one real tap or a screenshot from agent-device.
|
|
24
|
+
Don't tap through setup screens or edit backend data to reach a state that one
|
|
25
|
+
bridge call can set.
|
|
26
|
+
|
|
11
27
|
## Start a session
|
|
12
28
|
|
|
13
29
|
```sh
|
|
@@ -32,7 +48,7 @@ npx agent-bridge call screen.waitFor '"Name is required"' # {"gone": true}
|
|
|
32
48
|
|
|
33
49
|
`press` and `fill` return once the app has rendered the result, so the next
|
|
34
50
|
check sees it. `fill` skips the keyboard: autocorrect and native-only input
|
|
35
|
-
behaviour need one real typing step from
|
|
51
|
+
behaviour need one real typing step from agent-device.
|
|
36
52
|
|
|
37
53
|
## Get the app into a state
|
|
38
54
|
|
|
@@ -48,6 +64,25 @@ npx agent-bridge call net.log
|
|
|
48
64
|
Arguments are a JSON array (or a single JSON value). Read the current value
|
|
49
65
|
first (`query.get`, `store.get`) and change only what you need.
|
|
50
66
|
|
|
67
|
+
## Realtime messages
|
|
68
|
+
|
|
69
|
+
If the app has `realtime.*` tools, you can see its realtime messages and fake
|
|
70
|
+
them instead of waiting for a backend to send one:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
npx agent-bridge call realtime.channels # what the app listens on
|
|
74
|
+
npx agent-bridge call realtime.log '{"channel": "chat"}' # recent messages, real and faked
|
|
75
|
+
npx agent-bridge call realtime.mute '"chat"' # stop real ones overwriting your state
|
|
76
|
+
npx agent-bridge call realtime.emit '["chat", {"name": "message", "data": {"text": "hi"}}]'
|
|
77
|
+
npx agent-bridge call realtime.connection '"disconnected"' # null goes back to the real state
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Copy the shape of a real message from `realtime.log` before you emit one. With
|
|
81
|
+
Ably, pass `{ name, data }`; with socket.io, pass the arguments after the
|
|
82
|
+
channel (the event name). Muting still lets your emitted messages through.
|
|
83
|
+
`emit` fails and lists the live channels when nothing listens on yours: open
|
|
84
|
+
the screen that subscribes first.
|
|
85
|
+
|
|
51
86
|
## Watch for errors
|
|
52
87
|
|
|
53
88
|
Every reply carries errors the app logged or threw since the previous reply,
|
|
@@ -78,12 +113,13 @@ npx agent-bridge run flow.mjs --strict # --strict fails if the app logged an
|
|
|
78
113
|
npx agent-bridge session stop # runs bridge.restore; --keep leaves the app as is
|
|
79
114
|
```
|
|
80
115
|
|
|
81
|
-
`bridge.restore` unpins queries, puts back store and MMKV values
|
|
82
|
-
network mocks,
|
|
116
|
+
`bridge.restore` unpins queries, puts back store and MMKV values, removes
|
|
117
|
+
network mocks, unmutes realtime channels and ends a faked connection state, so
|
|
118
|
+
the next run starts from the real state.
|
|
83
119
|
|
|
84
120
|
## Traps
|
|
85
121
|
|
|
86
122
|
- Run next to the simulator. From another machine every call pays the network.
|
|
87
123
|
- It can't see native UI (system alerts, permission prompts, native sheets) or
|
|
88
|
-
overlap. Finish a flow with one real UI check or screenshot from
|
|
124
|
+
overlap. Finish a flow with one real UI check or screenshot from agent-device.
|
|
89
125
|
- With several apps on one Metro, pass `--device`.
|