agent-yes 1.95.0 → 1.97.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/ts/share.ts ADDED
@@ -0,0 +1,190 @@
1
+ // `ay serve --share` host peer: connect to the signaling server as a room host
2
+ // and bridge each browser peer's WebRTC DataChannel to this machine's local
3
+ // `ay serve` HTTP API. The browser (agent-yes.com) thus reaches local agents
4
+ // peer-to-peer — no public port, no tunnel. See lab/ui/cf/worker.ts for the
5
+ // signaling protocol and lab/ui/index.html for the browser side.
6
+ import { randomBytes } from "crypto";
7
+
8
+ const SUB = "ay-signal-1";
9
+ const ICE = [{ urls: "stun:stun.l.google.com:19302" }];
10
+ const MAX_CHUNK = 15_000; // keep DataChannel messages under the SCTP limit
11
+ const DEFAULT_SIGHOST = "s.agent-yes.com";
12
+
13
+ export interface ShareOpts {
14
+ /** webrtc://room:token@host, or undefined to mint a fresh room+token */
15
+ url?: string;
16
+ /** signaling host when minting (default s.agent-yes.com) */
17
+ sighost?: string;
18
+ /** local ay-serve base URL the channel bridges to */
19
+ apiUrl: string;
20
+ /** bearer token for the local ay-serve API */
21
+ apiToken: string;
22
+ }
23
+
24
+ function parseShareUrl(s: string): { room: string; token: string; host: string } {
25
+ const m = /^webrtc:\/\/([^:@/]+):([^@/]+)@(.+)$/.exec(s);
26
+ if (!m) throw new Error(`bad --share url: ${s} (want webrtc://room:token@host)`);
27
+ return { room: m[1]!, token: m[2]!, host: m[3]! };
28
+ }
29
+
30
+ // node-datachannel ships a native addon. Under Bun the module sometimes resolves
31
+ // from the global cache where the prebuilt .node isn't linked; this best-effort
32
+ // shim symlinks it in before we import. In a normal npm/bunx install the binary
33
+ // resolves from node_modules and the first import just works.
34
+ async function importRTC(): Promise<any> {
35
+ try {
36
+ return (await import("node-datachannel/polyfill")).RTCPeerConnection;
37
+ } catch {
38
+ try {
39
+ const { existsSync, symlinkSync, mkdirSync, readdirSync } = await import("fs");
40
+ const path = (await import("path")).default;
41
+ const { createRequire } = await import("module");
42
+ const require = createRequire(import.meta.url);
43
+ const pkg = path.dirname(require.resolve("node-datachannel/package.json"));
44
+ const bin = path.join(pkg, "build", "Release", "node_datachannel.node");
45
+ const cacheRoot = path.join((await import("os")).homedir(), ".bun", "install", "cache");
46
+ if (existsSync(bin) && existsSync(cacheRoot)) {
47
+ for (const d of readdirSync(cacheRoot)) {
48
+ if (!d.startsWith("node-datachannel@")) continue;
49
+ const dst = path.join(cacheRoot, d, "build", "Release");
50
+ mkdirSync(dst, { recursive: true });
51
+ const link = path.join(dst, "node_datachannel.node");
52
+ if (!existsSync(link)) symlinkSync(bin, link);
53
+ }
54
+ }
55
+ } catch {
56
+ /* fall through — rethrow the original import error below */
57
+ }
58
+ return (await import("node-datachannel/polyfill")).RTCPeerConnection;
59
+ }
60
+ }
61
+
62
+ /** Start the share bridge. Resolves once signaling is connected; runs until the
63
+ * process exits, reconnecting signaling on drop. Returns the shareable link. */
64
+ export async function startShare(opts: ShareOpts): Promise<{ room: string; link: string }> {
65
+ const minted = !opts.url;
66
+ const sighost = opts.sighost ?? DEFAULT_SIGHOST;
67
+ const { room, token, host } = opts.url
68
+ ? parseShareUrl(opts.url)
69
+ : {
70
+ room: "r" + randomBytes(3).toString("hex"),
71
+ token: randomBytes(32).toString("hex"),
72
+ host: sighost,
73
+ };
74
+
75
+ const RTCPeerConnection = await importRTC();
76
+ const wsScheme = host.startsWith("localhost") || host.startsWith("127.") ? "ws" : "wss";
77
+ const ui = host === "s.agent-yes.com" ? "https://agent-yes.com" : "http://localhost:7778";
78
+ const suffix = host === "s.agent-yes.com" ? "" : "@" + host;
79
+ const link = `${ui}/#${room}:${token}${suffix}`;
80
+
81
+ type Peer = { pc: any; aborts: Map<number, AbortController> };
82
+ const peers = new Map<string, Peer>();
83
+
84
+ const connectSignaling = (onReady: () => void) => {
85
+ const ws = new WebSocket(`${wsScheme}://${host}/${room}`, [SUB]);
86
+ let ready = false;
87
+ ws.onopen = () => {
88
+ ws.send(JSON.stringify({ type: "hello", role: "host", token }));
89
+ ready = true;
90
+ onReady();
91
+ };
92
+ ws.onmessage = async (ev) => {
93
+ const m = JSON.parse(ev.data as string);
94
+ if (m.type === "peer-join") startPeer(ws, m.peer);
95
+ else if (m.type === "answer")
96
+ await peers.get(m.from)?.pc.setRemoteDescription({ type: "answer", sdp: m.sdp });
97
+ else if (m.type === "candidate")
98
+ await peers
99
+ .get(m.from)
100
+ ?.pc.addIceCandidate(m.candidate)
101
+ .catch(() => {});
102
+ else if (m.type === "peer-leave") closePeer(m.peer);
103
+ };
104
+ ws.onclose = () => {
105
+ // Keep established WebRTC peers; just re-establish the rendezvous so new
106
+ // browsers can still join. Backoff a little to avoid hot-looping.
107
+ setTimeout(() => connectSignaling(() => {}), ready ? 1500 : 4000);
108
+ };
109
+ ws.onerror = () => {};
110
+ return ws;
111
+ };
112
+
113
+ function startPeer(ws: WebSocket, peerId: string) {
114
+ const pc = new RTCPeerConnection({ iceServers: ICE });
115
+ const aborts = new Map<number, AbortController>();
116
+ peers.set(peerId, { pc, aborts });
117
+ pc.onicecandidate = (e: any) => {
118
+ if (e.candidate)
119
+ ws.send(JSON.stringify({ type: "candidate", to: peerId, candidate: e.candidate }));
120
+ };
121
+ pc.onconnectionstatechange = () => {
122
+ if (["failed", "closed", "disconnected"].includes(pc.connectionState)) closePeer(peerId);
123
+ };
124
+ const dc = pc.createDataChannel("api");
125
+ dc.onmessage = (e: any) => onReq(dc, aborts, JSON.parse(e.data));
126
+ pc.createOffer()
127
+ .then((o: any) => pc.setLocalDescription(o))
128
+ .then(() =>
129
+ ws.send(JSON.stringify({ type: "offer", to: peerId, sdp: pc.localDescription.sdp })),
130
+ );
131
+ }
132
+
133
+ function closePeer(peerId: string) {
134
+ const p = peers.get(peerId);
135
+ if (!p) return;
136
+ for (const a of p.aborts.values()) a.abort();
137
+ try {
138
+ p.pc.close();
139
+ } catch {
140
+ /* already closed */
141
+ }
142
+ peers.delete(peerId);
143
+ }
144
+
145
+ function send(dc: any, obj: object) {
146
+ if (dc.readyState === "open") dc.send(JSON.stringify(obj));
147
+ }
148
+
149
+ async function onReq(dc: any, aborts: Map<number, AbortController>, req: any) {
150
+ if (req.t === "abort") {
151
+ aborts.get(req.id)?.abort();
152
+ aborts.delete(req.id);
153
+ return;
154
+ }
155
+ if (req.t !== "req") return;
156
+ const { id, method, path: p, body } = req;
157
+ const ac = new AbortController();
158
+ aborts.set(id, ac);
159
+ try {
160
+ const res = await fetch(opts.apiUrl + p, {
161
+ method,
162
+ headers: {
163
+ Authorization: `Bearer ${opts.apiToken}`,
164
+ ...(body ? { "Content-Type": "application/json" } : {}),
165
+ },
166
+ body: body ?? undefined,
167
+ signal: ac.signal,
168
+ });
169
+ send(dc, { t: "res", id, status: res.status, ct: res.headers.get("content-type") ?? "" });
170
+ const reader = res.body!.getReader();
171
+ const dec = new TextDecoder();
172
+ for (;;) {
173
+ const { done, value } = await reader.read();
174
+ if (done) break;
175
+ const text = dec.decode(value, { stream: true });
176
+ for (let i = 0; i < text.length; i += MAX_CHUNK)
177
+ send(dc, { t: "data", id, chunk: text.slice(i, i + MAX_CHUNK) });
178
+ }
179
+ send(dc, { t: "end", id });
180
+ } catch (e) {
181
+ if ((e as Error).name !== "AbortError") send(dc, { t: "end", id, error: String(e) });
182
+ } finally {
183
+ aborts.delete(id);
184
+ }
185
+ }
186
+
187
+ await new Promise<void>((resolve) => connectSignaling(resolve));
188
+ void minted; // (informational) caller decides how to surface the link
189
+ return { room, link };
190
+ }
@@ -1,12 +0,0 @@
1
- import { t as CLIS_CONFIG } from "./ts-BvWaEGsr.js";
2
- import "./logger-B9h0djqx.js";
3
- import "./versionChecker-DfIPG9ui.js";
4
- import "./pidStore-DTzl6zeh.js";
5
- import "./globalPidIndex-yVd3mbsV.js";
6
-
7
- //#region ts/SUPPORTED_CLIS.ts
8
- const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
9
-
10
- //#endregion
11
- export { SUPPORTED_CLIS };
12
- //# sourceMappingURL=SUPPORTED_CLIS-CrlcmUcE.js.map
@@ -1,5 +0,0 @@
1
- import "./logger-B9h0djqx.js";
2
- import { t as PidStore } from "./pidStore-DTzl6zeh.js";
3
- import "./globalPidIndex-yVd3mbsV.js";
4
-
5
- export { PidStore };