@avasapp/agent-bridge 0.1.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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/adapters/expo-router.cjs +144 -0
- package/dist/adapters/expo-router.d.cts +38 -0
- package/dist/adapters/react-native-mmkv.cjs +102 -0
- package/dist/adapters/react-native-mmkv.d.cts +15 -0
- package/dist/adapters/tanstack-query.cjs +161 -0
- package/dist/adapters/tanstack-query.d.cts +10 -0
- package/dist/adapters/zustand.cjs +97 -0
- package/dist/adapters/zustand.d.cts +15 -0
- package/dist/chunk-UEWFQWCY.js +575 -0
- package/dist/cli.js +657 -0
- package/dist/client/index.cjs +568 -0
- package/dist/client/index.d.ts +112 -0
- package/dist/client/index.js +14 -0
- package/dist/expo/index.cjs +62 -0
- package/dist/expo/index.d.cts +9 -0
- package/dist/network/index.cjs +568 -0
- package/dist/network/index.d.cts +85 -0
- package/dist/noop/expo-router.cjs +26 -0
- package/dist/noop/expo.cjs +27 -0
- package/dist/noop/index.cjs +36 -0
- package/dist/noop/network.cjs +34 -0
- package/dist/noop/react-native-mmkv.cjs +26 -0
- package/dist/noop/tanstack-query.cjs +26 -0
- package/dist/noop/zustand.cjs +26 -0
- package/dist/runtime/index.cjs +933 -0
- package/dist/runtime/index.d.cts +69 -0
- package/dist/types-C6DUUHnB.d.cts +76 -0
- package/entries/expo-router.cjs +9 -0
- package/entries/expo.cjs +9 -0
- package/entries/index.cjs +9 -0
- package/entries/network.cjs +9 -0
- package/entries/react-native-mmkv.cjs +9 -0
- package/entries/tanstack-query.cjs +9 -0
- package/entries/zustand.cjs +9 -0
- package/package.json +120 -0
- package/skills/agent-bridge/SKILL.md +89 -0
|
@@ -0,0 +1,97 @@
|
|
|
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/zustand.ts
|
|
21
|
+
var zustand_exports = {};
|
|
22
|
+
__export(zustand_exports, {
|
|
23
|
+
storeTools: () => storeTools
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(zustand_exports);
|
|
26
|
+
var snapshots = /* @__PURE__ */ new WeakMap();
|
|
27
|
+
var isObject = (value) => typeof value === "object" && value !== null;
|
|
28
|
+
function pick(value, path) {
|
|
29
|
+
if (!path) return value;
|
|
30
|
+
return path.split(".").reduce(
|
|
31
|
+
(v, k) => v?.[k],
|
|
32
|
+
value
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
function restore(store, snapshot) {
|
|
36
|
+
const current = store.getState();
|
|
37
|
+
if (!isObject(snapshot) || !isObject(current)) {
|
|
38
|
+
store.setState(snapshot, true);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const actions = Object.fromEntries(
|
|
42
|
+
Object.entries(current).filter(([, v]) => typeof v === "function")
|
|
43
|
+
);
|
|
44
|
+
store.setState({ ...snapshot, ...actions }, true);
|
|
45
|
+
}
|
|
46
|
+
function storeTools(stores) {
|
|
47
|
+
const get = (name) => {
|
|
48
|
+
const store = stores[name];
|
|
49
|
+
if (!store)
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Unknown store "${name}". Known: ${Object.keys(stores).join(", ")}`
|
|
52
|
+
);
|
|
53
|
+
return store;
|
|
54
|
+
};
|
|
55
|
+
const beforeChange = (store) => {
|
|
56
|
+
if (!snapshots.has(store)) snapshots.set(store, store.getState());
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
"store.list": {
|
|
60
|
+
description: "Store names.",
|
|
61
|
+
run: () => Object.keys(stores)
|
|
62
|
+
},
|
|
63
|
+
"store.get": {
|
|
64
|
+
description: 'State of a store, or one dotted path inside it (e.g. "auth.isLoggedIn").',
|
|
65
|
+
run: (name, path) => pick(get(name).getState(), path)
|
|
66
|
+
},
|
|
67
|
+
"store.set": {
|
|
68
|
+
description: "Shallow-merge a partial into a store.",
|
|
69
|
+
run: (name, partial) => {
|
|
70
|
+
const store = get(name);
|
|
71
|
+
beforeChange(store);
|
|
72
|
+
store.setState(partial);
|
|
73
|
+
return store.getState();
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"store.call": {
|
|
77
|
+
description: 'Call an action on a store, e.g. ("settings", "setColorScheme", "dark").',
|
|
78
|
+
run: (name, action, ...args) => {
|
|
79
|
+
const store = get(name);
|
|
80
|
+
const fn = store.getState()[action];
|
|
81
|
+
if (typeof fn !== "function")
|
|
82
|
+
throw new Error(`Store "${name}" has no action "${action}"`);
|
|
83
|
+
beforeChange(store);
|
|
84
|
+
return fn(...args);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"store.restore": {
|
|
88
|
+
description: "Undo the agent: put back each store changed with store.set or store.call. Returns their names.",
|
|
89
|
+
run: () => Object.entries(stores).flatMap(([name, store]) => {
|
|
90
|
+
if (!snapshots.has(store)) return [];
|
|
91
|
+
restore(store, snapshots.get(store));
|
|
92
|
+
snapshots.delete(store);
|
|
93
|
+
return [name];
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { T as Tools } from '../types-C6DUUHnB.cjs';
|
|
2
|
+
|
|
3
|
+
/** Anything with zustand's store shape. */
|
|
4
|
+
type StoreLike = {
|
|
5
|
+
getState: () => unknown;
|
|
6
|
+
/**
|
|
7
|
+
* `replace: true` swaps the whole state, as zustand's setState does. Method
|
|
8
|
+
* syntax, so zustand's overloads (replace: false | true) still fit.
|
|
9
|
+
*/
|
|
10
|
+
setState(partial: Record<string, unknown>, replace?: boolean): void;
|
|
11
|
+
};
|
|
12
|
+
/** Read, merge into, and call actions on named stores. */
|
|
13
|
+
declare function storeTools(stores: Record<string, StoreLike>): Tools;
|
|
14
|
+
|
|
15
|
+
export { type StoreLike, storeTools };
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
// src/client/discover.ts
|
|
2
|
+
var DEFAULT_METRO = "localhost:8081";
|
|
3
|
+
function metroHost(metro) {
|
|
4
|
+
return (metro ?? process.env.AGENT_BRIDGE_METRO ?? DEFAULT_METRO).replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
5
|
+
}
|
|
6
|
+
async function expoHostUri(metro) {
|
|
7
|
+
try {
|
|
8
|
+
const res = await fetch(`http://${metro}/`, {
|
|
9
|
+
headers: {
|
|
10
|
+
"expo-platform": "ios",
|
|
11
|
+
accept: "application/expo+json,application/json"
|
|
12
|
+
},
|
|
13
|
+
signal: AbortSignal.timeout(3e3)
|
|
14
|
+
});
|
|
15
|
+
if (!res.ok) return null;
|
|
16
|
+
const manifest = await res.json();
|
|
17
|
+
return manifest.extra?.expoClient?.hostUri ?? manifest.extra?.expoGo?.debuggerHost ?? manifest.hostUri ?? null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function listCdpTargets(metro) {
|
|
23
|
+
const res = await fetch(`http://${metro}/json/list`, {
|
|
24
|
+
signal: AbortSignal.timeout(3e3)
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok)
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Metro at ${metro} answered /json/list with HTTP ${res.status}`
|
|
29
|
+
);
|
|
30
|
+
return await res.json();
|
|
31
|
+
}
|
|
32
|
+
function pickOne(items, filter, label) {
|
|
33
|
+
const matching = filter ? items.filter(
|
|
34
|
+
(item) => label(item).toLowerCase().includes(filter.toLowerCase())
|
|
35
|
+
) : items;
|
|
36
|
+
const connected = items.map(label).join("; ") || "none";
|
|
37
|
+
if (matching.length === 0) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
filter ? `No app matches "${filter}". Connected: ${connected}` : "No app is connected to Metro."
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (matching.length > 1 && !filter) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`${matching.length} apps are connected; pick one with --device. Connected: ${connected}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return matching[0];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/shared/protocol.ts
|
|
51
|
+
var RUNTIME_MARKER = "@avasapp/agent-bridge/runtime";
|
|
52
|
+
var PLUGIN_NAME = "agent-bridge";
|
|
53
|
+
var CDP_GLOBAL = "__AGENT_BRIDGE__";
|
|
54
|
+
var CDP_REPLY_BINDING = "__agentBridgeReply";
|
|
55
|
+
function toAsciiJson(value) {
|
|
56
|
+
return JSON.stringify(value).replace(
|
|
57
|
+
/[\u007f-]/g,
|
|
58
|
+
(c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/client/connection.ts
|
|
63
|
+
import WebSocket from "ws";
|
|
64
|
+
function openSocket(url, headers) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const ws = new WebSocket(url, headers ? { headers } : void 0);
|
|
67
|
+
let opened = false;
|
|
68
|
+
ws.on("error", (event) => {
|
|
69
|
+
if (opened) return;
|
|
70
|
+
const message = event?.message ?? String(event);
|
|
71
|
+
reject(new Error(`Could not open ${url}: ${message}`));
|
|
72
|
+
});
|
|
73
|
+
ws.once("close", () => {
|
|
74
|
+
if (!opened) reject(new Error(`${url} closed before opening`));
|
|
75
|
+
});
|
|
76
|
+
ws.once("open", () => {
|
|
77
|
+
opened = true;
|
|
78
|
+
resolve(ws);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
var newCallId = () => `c${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
83
|
+
function createPending() {
|
|
84
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
85
|
+
return {
|
|
86
|
+
wait(id, tool, timeoutMs) {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
waiting.delete(id);
|
|
90
|
+
reject(new Error(`No reply to "${tool}" within ${timeoutMs} ms`));
|
|
91
|
+
}, timeoutMs);
|
|
92
|
+
waiting.set(id, (result) => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
waiting.delete(id);
|
|
95
|
+
resolve(result);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
settle(result) {
|
|
100
|
+
waiting.get(result.id)?.(result);
|
|
101
|
+
},
|
|
102
|
+
failAll(reason) {
|
|
103
|
+
for (const [id, done] of waiting) {
|
|
104
|
+
done({ id, from: "", ok: false, error: reason, ms: 0 });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/client/expo.ts
|
|
111
|
+
var deviceLabel = (d) => `${d.name} (${d.platform}, ${d.deviceId})`;
|
|
112
|
+
async function openBroadcast(metro, discoveryMs) {
|
|
113
|
+
const ws = await openSocket(`ws://${metro}/expo-dev-plugins/broadcast`);
|
|
114
|
+
const devices = /* @__PURE__ */ new Map();
|
|
115
|
+
const pending = createPending();
|
|
116
|
+
ws.on("message", (data, isBinary) => {
|
|
117
|
+
if (isBinary) return;
|
|
118
|
+
let frame;
|
|
119
|
+
try {
|
|
120
|
+
frame = JSON.parse(String(data));
|
|
121
|
+
} catch {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (frame.messageKey?.pluginName !== PLUGIN_NAME) return;
|
|
125
|
+
if (frame.messageKey.method === "hello:reply") {
|
|
126
|
+
const info = frame.payload;
|
|
127
|
+
devices.set(info.deviceId, info);
|
|
128
|
+
} else if (frame.messageKey.method === "result") {
|
|
129
|
+
pending.settle(frame.payload);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
ws.on("close", () => pending.failAll("Expo's dev-tools socket closed"));
|
|
133
|
+
const send = (method, payload) => ws.send(
|
|
134
|
+
JSON.stringify({
|
|
135
|
+
messageKey: { pluginName: PLUGIN_NAME, method },
|
|
136
|
+
payload
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
send("hello", {});
|
|
140
|
+
await new Promise((r) => setTimeout(r, discoveryMs));
|
|
141
|
+
return { ws, devices, pending, send };
|
|
142
|
+
}
|
|
143
|
+
async function listExpoDevices(metro, discoveryMs = 600) {
|
|
144
|
+
const { ws, devices } = await openBroadcast(metro, discoveryMs);
|
|
145
|
+
ws.close();
|
|
146
|
+
return [...devices.values()];
|
|
147
|
+
}
|
|
148
|
+
async function connectExpo(metro, device, discoveryMs = 600) {
|
|
149
|
+
const { ws, devices, pending, send } = await openBroadcast(metro, discoveryMs);
|
|
150
|
+
let info;
|
|
151
|
+
try {
|
|
152
|
+
info = pickOne([...devices.values()], device, deviceLabel);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
ws.close();
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
transport: "expo",
|
|
159
|
+
device: info,
|
|
160
|
+
call(tool, args, timeoutMs) {
|
|
161
|
+
const call = {
|
|
162
|
+
id: newCallId(),
|
|
163
|
+
tool,
|
|
164
|
+
args,
|
|
165
|
+
to: info.deviceId
|
|
166
|
+
};
|
|
167
|
+
const reply = pending.wait(call.id, tool, timeoutMs);
|
|
168
|
+
send("call", call);
|
|
169
|
+
return reply;
|
|
170
|
+
},
|
|
171
|
+
close: () => ws.close()
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/client/cdp.ts
|
|
176
|
+
import WebSocket2 from "ws";
|
|
177
|
+
var targetLabel = (t) => `${t.title}${t.deviceName ? ` [${t.deviceName}]` : ""}`;
|
|
178
|
+
async function connectCdp(metro, device) {
|
|
179
|
+
const target = pickOne(await listCdpTargets(metro), device, targetLabel);
|
|
180
|
+
const hostUri = await expoHostUri(metro);
|
|
181
|
+
const port = metro.split(":")[1] ?? "8081";
|
|
182
|
+
const origin = hostUri ? `http://${hostUri}` : `http://localhost:${port}`;
|
|
183
|
+
const closedEarly = `Metro closed the debugger socket. Origin sent: ${origin}. Is that the host Metro advertises?`;
|
|
184
|
+
const ws = await openSocket(target.webSocketDebuggerUrl, {
|
|
185
|
+
Origin: origin
|
|
186
|
+
}).catch(() => {
|
|
187
|
+
throw new Error(closedEarly);
|
|
188
|
+
});
|
|
189
|
+
let seq = 0;
|
|
190
|
+
let answered = false;
|
|
191
|
+
const commands = /* @__PURE__ */ new Map();
|
|
192
|
+
const pending = createPending();
|
|
193
|
+
ws.on("message", (data) => {
|
|
194
|
+
const message = JSON.parse(String(data));
|
|
195
|
+
if (message.id && commands.has(message.id)) {
|
|
196
|
+
answered = true;
|
|
197
|
+
commands.get(message.id)?.resolve(message);
|
|
198
|
+
commands.delete(message.id);
|
|
199
|
+
} else if (message.method === "Runtime.bindingCalled" && message.params?.name === CDP_REPLY_BINDING) {
|
|
200
|
+
pending.settle(JSON.parse(message.params.payload));
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
ws.on("close", () => {
|
|
204
|
+
const reason = answered ? "The debugger socket closed" : closedEarly;
|
|
205
|
+
for (const command of commands.values()) command.reject(new Error(reason));
|
|
206
|
+
commands.clear();
|
|
207
|
+
pending.failAll(reason);
|
|
208
|
+
});
|
|
209
|
+
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
|
210
|
+
if (ws.readyState !== WebSocket2.OPEN) {
|
|
211
|
+
reject(new Error(answered ? "The debugger socket closed" : closedEarly));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const id = ++seq;
|
|
215
|
+
commands.set(id, { resolve, reject });
|
|
216
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
217
|
+
});
|
|
218
|
+
const evaluate = async (expression) => {
|
|
219
|
+
const message = await send("Runtime.evaluate", {
|
|
220
|
+
expression,
|
|
221
|
+
returnByValue: true
|
|
222
|
+
});
|
|
223
|
+
const details = message.result?.exceptionDetails;
|
|
224
|
+
if (details) {
|
|
225
|
+
const text = details.exception?.description ?? details.text ?? "evaluation failed";
|
|
226
|
+
throw new Error(text.split("\n")[0]);
|
|
227
|
+
}
|
|
228
|
+
return message.result?.result?.value;
|
|
229
|
+
};
|
|
230
|
+
await send("Runtime.enable");
|
|
231
|
+
await send("Runtime.addBinding", { name: CDP_REPLY_BINDING });
|
|
232
|
+
let info;
|
|
233
|
+
try {
|
|
234
|
+
info = JSON.parse(
|
|
235
|
+
String(await evaluate(`${CDP_GLOBAL}.info()`))
|
|
236
|
+
);
|
|
237
|
+
} catch {
|
|
238
|
+
ws.close();
|
|
239
|
+
throw new Error(
|
|
240
|
+
`agent-bridge isn't running in ${targetLabel(target)}. Is useAgentBridge mounted in a dev build?`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
transport: "cdp",
|
|
245
|
+
device: { ...info, name: `${info.name} (${targetLabel(target)})` },
|
|
246
|
+
async call(tool, args, timeoutMs) {
|
|
247
|
+
const call = { id: newCallId(), tool, args };
|
|
248
|
+
const reply = pending.wait(call.id, tool, timeoutMs);
|
|
249
|
+
try {
|
|
250
|
+
await evaluate(
|
|
251
|
+
`${CDP_GLOBAL}.dispatch(${JSON.stringify(toAsciiJson(call))})`
|
|
252
|
+
);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
pending.settle({
|
|
255
|
+
id: call.id,
|
|
256
|
+
from: info.deviceId,
|
|
257
|
+
ok: false,
|
|
258
|
+
error: String(error),
|
|
259
|
+
ms: 0
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return reply;
|
|
263
|
+
},
|
|
264
|
+
close: () => ws.close()
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/client/open.ts
|
|
269
|
+
async function openConnection(options = {}) {
|
|
270
|
+
const metro = metroHost(options.metro);
|
|
271
|
+
const want = options.transport ?? "auto";
|
|
272
|
+
let expoError;
|
|
273
|
+
if (want !== "cdp") {
|
|
274
|
+
try {
|
|
275
|
+
return await connectExpo(metro, options.device);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (want === "expo") throw error;
|
|
278
|
+
expoError = error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
return await connectCdp(metro, options.device);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const expoNote = expoError ? ` (Expo socket: ${String(expoError)})` : "";
|
|
285
|
+
throw new Error(
|
|
286
|
+
`${error instanceof Error ? error.message : String(error)}${expoNote}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/client/session/client.ts
|
|
292
|
+
import { createConnection } from "net";
|
|
293
|
+
import { createInterface } from "readline";
|
|
294
|
+
|
|
295
|
+
// src/client/session/state.ts
|
|
296
|
+
import { createHash } from "crypto";
|
|
297
|
+
import {
|
|
298
|
+
chmodSync,
|
|
299
|
+
lstatSync,
|
|
300
|
+
mkdirSync,
|
|
301
|
+
readFileSync,
|
|
302
|
+
readdirSync,
|
|
303
|
+
rmSync,
|
|
304
|
+
writeFileSync
|
|
305
|
+
} from "fs";
|
|
306
|
+
import { tmpdir, userInfo } from "os";
|
|
307
|
+
import { join } from "path";
|
|
308
|
+
var uid = () => process.getuid?.() ?? userInfo().username;
|
|
309
|
+
function privateDir(dir) {
|
|
310
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
311
|
+
const stat = lstatSync(dir);
|
|
312
|
+
if (!stat.isDirectory())
|
|
313
|
+
throw new Error(`${dir} is not a directory; remove it and retry`);
|
|
314
|
+
if (process.getuid && stat.uid !== process.getuid())
|
|
315
|
+
throw new Error(`${dir} belongs to another user; refusing to use it`);
|
|
316
|
+
if (process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
317
|
+
chmodSync(dir, 448);
|
|
318
|
+
return dir;
|
|
319
|
+
}
|
|
320
|
+
function stateDir() {
|
|
321
|
+
return privateDir(
|
|
322
|
+
process.env.AGENT_BRIDGE_STATE_DIR ?? (process.env.XDG_RUNTIME_DIR ? join(process.env.XDG_RUNTIME_DIR, "agent-bridge") : join(tmpdir(), `agent-bridge-${uid()}`))
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
var shortHash = (text) => createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
326
|
+
function socketPath(name, dir) {
|
|
327
|
+
if (process.platform === "win32")
|
|
328
|
+
return `\\\\.\\pipe\\agent-bridge-${shortHash(dir)}-${name}`;
|
|
329
|
+
const path = join(dir, `${name}.sock`);
|
|
330
|
+
if (Buffer.byteLength(path) <= 100) return path;
|
|
331
|
+
const short = privateDir(`/tmp/agent-bridge-${uid()}`);
|
|
332
|
+
return join(short, `${shortHash(path)}.sock`);
|
|
333
|
+
}
|
|
334
|
+
var sessionFiles = (name, dir = stateDir()) => ({
|
|
335
|
+
state: join(dir, `${name}.json`),
|
|
336
|
+
error: join(dir, `${name}.error`),
|
|
337
|
+
log: join(dir, `${name}.log`),
|
|
338
|
+
socket: socketPath(name, dir)
|
|
339
|
+
});
|
|
340
|
+
function checkName(name) {
|
|
341
|
+
if (!/^[\w.-]{1,40}$/.test(name))
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Session name "${name}" must be 1-40 letters, digits, ".", "_" or "-"`
|
|
344
|
+
);
|
|
345
|
+
return name;
|
|
346
|
+
}
|
|
347
|
+
function parseDuration(text) {
|
|
348
|
+
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h)?$/.exec(text.trim());
|
|
349
|
+
if (!match) throw new Error(`Can't read duration "${text}"; try 15m or 90s`);
|
|
350
|
+
const units = { s: 1e3, m: 6e4, h: 36e5 };
|
|
351
|
+
return Math.round(Number(match[1]) * (units[match[2] ?? "ms"] ?? 1));
|
|
352
|
+
}
|
|
353
|
+
function formatDuration(ms) {
|
|
354
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
355
|
+
if (s < 60) return `${s}s`;
|
|
356
|
+
if (s < 3600)
|
|
357
|
+
return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
358
|
+
return `${Math.floor(s / 3600)}h${String(Math.floor(s / 60) % 60).padStart(2, "0")}m`;
|
|
359
|
+
}
|
|
360
|
+
function isAlive(pid) {
|
|
361
|
+
try {
|
|
362
|
+
process.kill(pid, 0);
|
|
363
|
+
return true;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
return error.code === "EPERM";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function writePrivate(file, text) {
|
|
369
|
+
writeFileSync(file, text, { mode: 384 });
|
|
370
|
+
}
|
|
371
|
+
function removeSessionFiles(name, dir = stateDir()) {
|
|
372
|
+
const files = sessionFiles(name, dir);
|
|
373
|
+
rmSync(files.state, { force: true });
|
|
374
|
+
if (process.platform !== "win32") rmSync(files.socket, { force: true });
|
|
375
|
+
}
|
|
376
|
+
function readState(file) {
|
|
377
|
+
try {
|
|
378
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
379
|
+
} catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function listSessions(dir = stateDir()) {
|
|
384
|
+
const live = [];
|
|
385
|
+
for (const entry of readdirSync(dir)) {
|
|
386
|
+
if (!entry.endsWith(".json")) continue;
|
|
387
|
+
const state = readState(join(dir, entry));
|
|
388
|
+
if (!state) continue;
|
|
389
|
+
if (isAlive(state.pid)) live.push(state);
|
|
390
|
+
else removeSessionFiles(state.name, dir);
|
|
391
|
+
}
|
|
392
|
+
return live.sort((a, b) => a.startedAt - b.startedAt);
|
|
393
|
+
}
|
|
394
|
+
function readSession(name, dir = stateDir()) {
|
|
395
|
+
return listSessions(dir).find((s) => s.name === name) ?? null;
|
|
396
|
+
}
|
|
397
|
+
function pickSession(filter) {
|
|
398
|
+
if (filter.name) {
|
|
399
|
+
const named = readSession(checkName(filter.name));
|
|
400
|
+
if (!named) throw new Error(`No session named "${filter.name}" is running`);
|
|
401
|
+
return named;
|
|
402
|
+
}
|
|
403
|
+
const device = filter.device?.toLowerCase();
|
|
404
|
+
const matching = listSessions().filter(
|
|
405
|
+
(s) => s.metro === filter.metro && (!device || s.device.name.toLowerCase().includes(device) || s.device.deviceId.toLowerCase().includes(device)) && (!filter.transport || filter.transport === "auto" || filter.transport === s.transport)
|
|
406
|
+
);
|
|
407
|
+
return matching.length === 1 ? matching[0] : null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/client/session/client.ts
|
|
411
|
+
function openSessionLink(state) {
|
|
412
|
+
return new Promise((resolve, reject) => {
|
|
413
|
+
const socket = createConnection(state.socket);
|
|
414
|
+
const waiting = /* @__PURE__ */ new Map();
|
|
415
|
+
let seq = 0;
|
|
416
|
+
let open = false;
|
|
417
|
+
const gone = `Session "${state.name}" closed the connection`;
|
|
418
|
+
socket.once("connect", () => {
|
|
419
|
+
open = true;
|
|
420
|
+
resolve({
|
|
421
|
+
request: (req) => new Promise((done, fail) => {
|
|
422
|
+
if (socket.destroyed || socket.writableEnded) {
|
|
423
|
+
fail(new Error(gone));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const id = ++seq;
|
|
427
|
+
waiting.set(id, done);
|
|
428
|
+
socket.write(`${JSON.stringify({ ...req, id })}
|
|
429
|
+
`);
|
|
430
|
+
}),
|
|
431
|
+
close: () => socket.end()
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
socket.on("error", (error) => {
|
|
435
|
+
if (!open)
|
|
436
|
+
reject(
|
|
437
|
+
new Error(
|
|
438
|
+
`Session "${state.name}" isn't answering on ${state.socket}: ${error.message}`
|
|
439
|
+
)
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
socket.on("close", () => {
|
|
443
|
+
for (const [id, done] of waiting) done({ id, error: gone });
|
|
444
|
+
waiting.clear();
|
|
445
|
+
});
|
|
446
|
+
createInterface({ input: socket }).on("line", (line) => {
|
|
447
|
+
const res = JSON.parse(line);
|
|
448
|
+
waiting.get(res.id)?.(res);
|
|
449
|
+
waiting.delete(res.id);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async function sessionRequest(state, req) {
|
|
454
|
+
const link = await openSessionLink(state);
|
|
455
|
+
try {
|
|
456
|
+
return await link.request(req);
|
|
457
|
+
} finally {
|
|
458
|
+
link.close();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function connectSession(options = {}) {
|
|
462
|
+
const state = pickSession({
|
|
463
|
+
name: options.name ?? process.env.AGENT_BRIDGE_SESSION,
|
|
464
|
+
metro: metroHost(options.metro),
|
|
465
|
+
device: options.device,
|
|
466
|
+
transport: options.transport
|
|
467
|
+
});
|
|
468
|
+
if (!state)
|
|
469
|
+
throw new Error(
|
|
470
|
+
"No single session matches. Start one with `agent-bridge session start`, or name it."
|
|
471
|
+
);
|
|
472
|
+
const link = await openSessionLink(state);
|
|
473
|
+
const info = await link.request({ op: "info" });
|
|
474
|
+
if (info.error || !info.device || !info.transport) {
|
|
475
|
+
link.close();
|
|
476
|
+
throw new Error(info.error ?? `Session "${state.name}" sent no device`);
|
|
477
|
+
}
|
|
478
|
+
const device = info.device;
|
|
479
|
+
const timed = async (tool, ...args) => {
|
|
480
|
+
const res = await link.request({
|
|
481
|
+
op: "call",
|
|
482
|
+
tool,
|
|
483
|
+
args,
|
|
484
|
+
timeoutMs: options.timeoutMs
|
|
485
|
+
});
|
|
486
|
+
if (res.error || !res.result) throw new Error(res.error ?? "No result");
|
|
487
|
+
const result = res.result;
|
|
488
|
+
const logs = result.logs ?? [];
|
|
489
|
+
if (!result.ok) throw new AgentBridgeCallError(tool, result.error, logs);
|
|
490
|
+
const { id: _id, from: _from, ok: _ok, ms: appMs, value, ...extra } = result;
|
|
491
|
+
return { ...extra, value, ms: res.ms ?? 0, appMs, logs };
|
|
492
|
+
};
|
|
493
|
+
return {
|
|
494
|
+
session: info.state ?? state,
|
|
495
|
+
transport: info.transport,
|
|
496
|
+
device,
|
|
497
|
+
timed,
|
|
498
|
+
call: async (tool, ...args) => (await timed(tool, ...args)).value,
|
|
499
|
+
tools: () => device.tools,
|
|
500
|
+
close: link.close
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/client/index.ts
|
|
505
|
+
var AgentBridgeCallError = class extends Error {
|
|
506
|
+
constructor(tool, message, logs = []) {
|
|
507
|
+
super(`${tool}: ${message}`);
|
|
508
|
+
this.tool = tool;
|
|
509
|
+
this.logs = logs;
|
|
510
|
+
this.name = "AgentBridgeCallError";
|
|
511
|
+
}
|
|
512
|
+
tool;
|
|
513
|
+
logs;
|
|
514
|
+
};
|
|
515
|
+
async function connect(options = {}) {
|
|
516
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
517
|
+
const conn = await openConnection(options);
|
|
518
|
+
const timed = async (tool, ...args) => {
|
|
519
|
+
const t0 = performance.now();
|
|
520
|
+
const result = await conn.call(tool, args, timeoutMs);
|
|
521
|
+
const ms = performance.now() - t0;
|
|
522
|
+
const logs = result.logs ?? [];
|
|
523
|
+
if (!result.ok) throw new AgentBridgeCallError(tool, result.error, logs);
|
|
524
|
+
return { value: result.value, ms, appMs: result.ms, logs };
|
|
525
|
+
};
|
|
526
|
+
return {
|
|
527
|
+
transport: conn.transport,
|
|
528
|
+
device: conn.device,
|
|
529
|
+
timed,
|
|
530
|
+
call: async (tool, ...args) => (await timed(tool, ...args)).value,
|
|
531
|
+
tools: () => conn.device.tools,
|
|
532
|
+
close: conn.close
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async function listDevices(options = {}) {
|
|
536
|
+
const metro = metroHost(options.metro);
|
|
537
|
+
const [expo, cdp] = await Promise.all([
|
|
538
|
+
listExpoDevices(metro).catch(() => []),
|
|
539
|
+
listCdpTargets(metro).catch(() => [])
|
|
540
|
+
]);
|
|
541
|
+
return [
|
|
542
|
+
...expo.map((d) => ({
|
|
543
|
+
transport: "expo",
|
|
544
|
+
name: d.name,
|
|
545
|
+
deviceId: d.deviceId,
|
|
546
|
+
tools: d.tools.length
|
|
547
|
+
})),
|
|
548
|
+
...cdp.map((t) => ({
|
|
549
|
+
transport: "cdp",
|
|
550
|
+
name: `${t.title}${t.deviceName ? ` [${t.deviceName}]` : ""}`
|
|
551
|
+
}))
|
|
552
|
+
];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export {
|
|
556
|
+
metroHost,
|
|
557
|
+
RUNTIME_MARKER,
|
|
558
|
+
openConnection,
|
|
559
|
+
stateDir,
|
|
560
|
+
sessionFiles,
|
|
561
|
+
checkName,
|
|
562
|
+
parseDuration,
|
|
563
|
+
formatDuration,
|
|
564
|
+
isAlive,
|
|
565
|
+
writePrivate,
|
|
566
|
+
removeSessionFiles,
|
|
567
|
+
listSessions,
|
|
568
|
+
readSession,
|
|
569
|
+
pickSession,
|
|
570
|
+
sessionRequest,
|
|
571
|
+
connectSession,
|
|
572
|
+
AgentBridgeCallError,
|
|
573
|
+
connect,
|
|
574
|
+
listDevices
|
|
575
|
+
};
|