agent-yes 1.144.0 → 1.146.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/default.config.yaml +27 -0
- package/dist/{SUPPORTED_CLIS-D0dDlvLS.js → SUPPORTED_CLIS-Bh6K521N.js} +2 -2
- package/dist/SUPPORTED_CLIS-DCpkNN0b.js +8 -0
- package/dist/cli.js +5 -5
- package/dist/e2e-ClOI_aqV.js +175 -0
- package/dist/index.js +2 -2
- package/dist/{remotes-BdankQeI.js → remotes-5lHLAAqn.js} +3 -1
- package/dist/{remotes-PKKjfTI1.js → remotes-qK6uozO4.js} +28 -2
- package/dist/{schedule-BKyOLSSe.js → schedule-B34TqMjC.js} +5 -5
- package/dist/{serve-QXNICu0K.js → serve-CbXS8sWv.js} +10 -8
- package/dist/{setup-dJlAGJEz.js → setup-C8vJmagV.js} +3 -3
- package/dist/{share-CoyAOa6e.js → share-ShLKJTUE.js} +2 -174
- package/dist/{subcommands-o3WRyHb9.js → subcommands-DWf6MHH6.js} +21 -9
- package/dist/{subcommands-_raQNJO3.js → subcommands-ZKr7Fks8.js} +4 -2
- package/dist/{tray-DsTv-C04.js → tray-5Hezw9uj.js} +1 -1
- package/dist/{ts-Qh0Z7nsZ.js → ts-CLOLKDtz.js} +2 -2
- package/dist/{versionChecker-BjKAfiI-.js → versionChecker-B6EYG8nW.js} +2 -2
- package/dist/webrtcLink-BWhuA74k.js +45 -0
- package/dist/webrtcRemote-jGM3ZHK3.js +259 -0
- package/dist/{workspaceConfig-BCOqRBEW.js → workspaceConfig-B3ylOZAO.js} +1 -1
- package/package.json +1 -1
- package/ts/remotes.ts +28 -1
- package/ts/subcommands.ts +16 -3
- package/ts/webrtcLink.spec.ts +45 -0
- package/ts/webrtcLink.ts +45 -0
- package/ts/webrtcRemote.ts +280 -0
- package/dist/SUPPORTED_CLIS-CwWcK6Fl.js +0 -8
|
@@ -2,7 +2,8 @@ import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
|
|
|
2
2
|
import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
|
|
3
3
|
import { a as updateGlobalPidStatus, i as readGlobalPids } from "./globalPidIndex-DlmmJlO8.js";
|
|
4
4
|
import { t as loadSharedCliDefaults } from "./configShared-C1C04bbq.js";
|
|
5
|
-
import {
|
|
5
|
+
import { n as isWebrtcSpec } from "./webrtcLink-BWhuA74k.js";
|
|
6
|
+
import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-qK6uozO4.js";
|
|
6
7
|
import ms from "ms";
|
|
7
8
|
import yargs from "yargs";
|
|
8
9
|
import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
|
|
@@ -553,19 +554,19 @@ async function runSubcommand(argv) {
|
|
|
553
554
|
case "restart": return await cmdRestart(rest);
|
|
554
555
|
case "note": return await cmdNote(rest);
|
|
555
556
|
case "serve": {
|
|
556
|
-
const { cmdServe } = await import("./serve-
|
|
557
|
+
const { cmdServe } = await import("./serve-CbXS8sWv.js");
|
|
557
558
|
return cmdServe(rest);
|
|
558
559
|
}
|
|
559
560
|
case "setup": {
|
|
560
|
-
const { cmdSetup } = await import("./setup-
|
|
561
|
+
const { cmdSetup } = await import("./setup-C8vJmagV.js");
|
|
561
562
|
return cmdSetup(rest);
|
|
562
563
|
}
|
|
563
564
|
case "schedule": {
|
|
564
|
-
const { cmdSchedule } = await import("./schedule-
|
|
565
|
+
const { cmdSchedule } = await import("./schedule-B34TqMjC.js");
|
|
565
566
|
return cmdSchedule(rest);
|
|
566
567
|
}
|
|
567
568
|
case "remote": {
|
|
568
|
-
const { cmdRemote } = await import("./remotes-
|
|
569
|
+
const { cmdRemote } = await import("./remotes-5lHLAAqn.js");
|
|
569
570
|
return cmdRemote(rest);
|
|
570
571
|
}
|
|
571
572
|
case "reap":
|
|
@@ -893,15 +894,26 @@ async function fetchRemoteRecordsRaw(url, token, opts) {
|
|
|
893
894
|
if (opts.all) params.set("all", "1");
|
|
894
895
|
if (opts.active) params.set("active", "1");
|
|
895
896
|
if (opts.keyword) params.set("keyword", opts.keyword);
|
|
897
|
+
let bridge = null;
|
|
896
898
|
try {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
899
|
+
let base = url;
|
|
900
|
+
let bearer = token;
|
|
901
|
+
if (isWebrtcSpec(url)) {
|
|
902
|
+
const { startWebrtcBridge } = await import("./webrtcRemote-jGM3ZHK3.js");
|
|
903
|
+
bridge = await startWebrtcBridge(url);
|
|
904
|
+
base = bridge.baseUrl;
|
|
905
|
+
bearer = bridge.token;
|
|
906
|
+
}
|
|
907
|
+
const res = await fetch(`${base}/api/ls?${params}`, {
|
|
908
|
+
headers: { Authorization: `Bearer ${bearer}` },
|
|
909
|
+
signal: AbortSignal.timeout(8e3)
|
|
900
910
|
});
|
|
901
911
|
if (!res.ok) return [];
|
|
902
912
|
return await res.json();
|
|
903
913
|
} catch {
|
|
904
914
|
return [];
|
|
915
|
+
} finally {
|
|
916
|
+
bridge?.close();
|
|
905
917
|
}
|
|
906
918
|
}
|
|
907
919
|
async function runAllRemotesLs(opts) {
|
|
@@ -2581,4 +2593,4 @@ async function cmdResultSet(rest) {
|
|
|
2581
2593
|
|
|
2582
2594
|
//#endregion
|
|
2583
2595
|
export { stopTipForCli as C, snapshotStatus as S, renderRawLog as _, cursorAbs as a, resolveReadWindow as b, extractTaskCounts as c, isExitRequest as d, isPidAlive as f, readNotes as g, matchKeyword as h, controlCodeFromName as i, finalizedLines as l, listRecords as m, READ_PAGE_DEFAULT as n, deriveLiveStatus as o, isSubcommand as p, cmdHelp as r, extractNeedsInput as s, GRACEFUL_EXIT_COMMANDS as t, isAgentStuck as u, renderRawLogLines as v, writeToIpc as w, runSubcommand as x, resolveOne as y };
|
|
2584
|
-
//# sourceMappingURL=subcommands-
|
|
2596
|
+
//# sourceMappingURL=subcommands-DWf6MHH6.js.map
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import "./logger-CDIsZ-Pp.js";
|
|
2
2
|
import "./globalPidIndex-DlmmJlO8.js";
|
|
3
3
|
import "./configShared-C1C04bbq.js";
|
|
4
|
-
import "./
|
|
5
|
-
import
|
|
4
|
+
import "./e2e-ClOI_aqV.js";
|
|
5
|
+
import "./webrtcLink-BWhuA74k.js";
|
|
6
|
+
import "./remotes-qK6uozO4.js";
|
|
7
|
+
import { C as stopTipForCli, S as snapshotStatus, _ as renderRawLog, a as cursorAbs, b as resolveReadWindow, c as extractTaskCounts, d as isExitRequest, f as isPidAlive, g as readNotes, h as matchKeyword, i as controlCodeFromName, l as finalizedLines, m as listRecords, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isSubcommand, r as cmdHelp, s as extractNeedsInput, t as GRACEFUL_EXIT_COMMANDS, u as isAgentStuck, v as renderRawLogLines, w as writeToIpc, x as runSubcommand, y as resolveOne } from "./subcommands-DWf6MHH6.js";
|
|
6
8
|
|
|
7
9
|
export { cmdHelp, isSubcommand, runSubcommand };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-B6EYG8nW.js";
|
|
3
3
|
import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
|
|
4
4
|
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-V4qvXgAw.js";
|
|
5
5
|
import { t as PidStore } from "./pidStore-fqXqTKkh.js";
|
|
@@ -1800,4 +1800,4 @@ function sleep(ms) {
|
|
|
1800
1800
|
|
|
1801
1801
|
//#endregion
|
|
1802
1802
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1803
|
-
//# sourceMappingURL=ts-
|
|
1803
|
+
//# sourceMappingURL=ts-CLOLKDtz.js.map
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
//#region package.json
|
|
9
9
|
var name = "agent-yes";
|
|
10
|
-
var version = "1.
|
|
10
|
+
var version = "1.146.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -215,4 +215,4 @@ async function displayVersion() {
|
|
|
215
215
|
|
|
216
216
|
//#endregion
|
|
217
217
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
218
|
-
//# sourceMappingURL=versionChecker-
|
|
218
|
+
//# sourceMappingURL=versionChecker-B6EYG8nW.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { u as parseSecret } from "./e2e-ClOI_aqV.js";
|
|
2
|
+
|
|
3
|
+
//#region ts/webrtcLink.ts
|
|
4
|
+
const SIGNAL_SUBPROTOCOL = "ay-signal-1";
|
|
5
|
+
const DEFAULT_SIGHOST = "s.agent-yes.com";
|
|
6
|
+
/**
|
|
7
|
+
* Parse a share link into { room, secret, signaling-host }. Accepts:
|
|
8
|
+
* webrtc://<room>:<token>@<host>
|
|
9
|
+
* https://<anyhost>/w/#<room>:<token> (signaling host defaults to s.agent-yes.com)
|
|
10
|
+
* https://<anyhost>/w/#<room>:<token>@<sighost> (explicit signaling host)
|
|
11
|
+
* Returns null if the string isn't a recognizable share link.
|
|
12
|
+
*/
|
|
13
|
+
function parseWebrtcLink(link) {
|
|
14
|
+
const wr = /^webrtc:\/\/([^:]+):([^@]+)@(.+)$/.exec(link);
|
|
15
|
+
if (wr) {
|
|
16
|
+
const { s } = parseSecret(wr[2]);
|
|
17
|
+
return {
|
|
18
|
+
room: wr[1],
|
|
19
|
+
s,
|
|
20
|
+
host: wr[3]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (/^https?:\/\//.test(link) && link.includes("#")) {
|
|
24
|
+
const at = (link.split("#")[1] ?? "").split("@");
|
|
25
|
+
const host = at[1] || DEFAULT_SIGHOST;
|
|
26
|
+
const seg = at[0];
|
|
27
|
+
const i = seg.indexOf(":");
|
|
28
|
+
if (i < 0) return null;
|
|
29
|
+
const { s } = parseSecret(seg.slice(i + 1));
|
|
30
|
+
return {
|
|
31
|
+
room: seg.slice(0, i),
|
|
32
|
+
s,
|
|
33
|
+
host
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** True if `spec` looks like a WebRTC share link (vs. an http remote or alias). */
|
|
39
|
+
function isWebrtcSpec(spec) {
|
|
40
|
+
return spec.startsWith("webrtc://") || /^https?:\/\//.test(spec) && spec.includes("#");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { isWebrtcSpec as n, parseWebrtcLink as r, SIGNAL_SUBPROTOCOL as t };
|
|
45
|
+
//# sourceMappingURL=webrtcLink-BWhuA74k.js.map
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { a as computeTranscriptHash, c as open, d as randomHex, f as seal, l as packEnvelope, n as FLAG_CONFIRM, o as deriveAuthToken, p as unpackEnvelope, s as deriveDirKeys } from "./e2e-ClOI_aqV.js";
|
|
2
|
+
import { n as isWebrtcSpec, r as parseWebrtcLink, t as SIGNAL_SUBPROTOCOL } from "./webrtcLink-BWhuA74k.js";
|
|
3
|
+
import { RTCPeerConnection } from "node-datachannel/polyfill";
|
|
4
|
+
|
|
5
|
+
//#region ts/webrtcRemote.ts
|
|
6
|
+
const CONNECT_TIMEOUT_MS = 25e3;
|
|
7
|
+
/** A live, key-confirmed WebRTC connection to a single share room. */
|
|
8
|
+
var WebRtcConn = class {
|
|
9
|
+
ws;
|
|
10
|
+
pc = null;
|
|
11
|
+
dc = null;
|
|
12
|
+
send = { sendCtr: 0n };
|
|
13
|
+
recv = { lastSeen: -1n };
|
|
14
|
+
keyC2H = null;
|
|
15
|
+
keyH2C = null;
|
|
16
|
+
th = null;
|
|
17
|
+
hostPeer = null;
|
|
18
|
+
myNonce = randomHex(16);
|
|
19
|
+
confirmedIn = false;
|
|
20
|
+
confirmedOut = false;
|
|
21
|
+
confirmed = false;
|
|
22
|
+
sendChain = Promise.resolve();
|
|
23
|
+
recvChain = Promise.resolve();
|
|
24
|
+
idCounter = 0;
|
|
25
|
+
pending = /* @__PURE__ */ new Map();
|
|
26
|
+
ready;
|
|
27
|
+
resolveReady;
|
|
28
|
+
rejectReady;
|
|
29
|
+
constructor(link) {
|
|
30
|
+
this.link = link;
|
|
31
|
+
this.ready = new Promise((res, rej) => {
|
|
32
|
+
this.resolveReady = res;
|
|
33
|
+
this.rejectReady = rej;
|
|
34
|
+
});
|
|
35
|
+
const timer = setTimeout(() => this.rejectReady(/* @__PURE__ */ new Error(`WebRTC connect timeout after ${CONNECT_TIMEOUT_MS}ms`)), CONNECT_TIMEOUT_MS);
|
|
36
|
+
this.ready.then(() => clearTimeout(timer), () => clearTimeout(timer));
|
|
37
|
+
this.ws = this.dial();
|
|
38
|
+
}
|
|
39
|
+
dial() {
|
|
40
|
+
const { room, host } = this.link;
|
|
41
|
+
const ws = new WebSocket(`wss://${host}/${room}`, [SIGNAL_SUBPROTOCOL]);
|
|
42
|
+
ws.onopen = async () => {
|
|
43
|
+
const authToken = await deriveAuthToken(this.link.s, room, host);
|
|
44
|
+
ws.send(JSON.stringify({
|
|
45
|
+
type: "hello",
|
|
46
|
+
role: "client",
|
|
47
|
+
v: 2,
|
|
48
|
+
token: authToken
|
|
49
|
+
}));
|
|
50
|
+
};
|
|
51
|
+
ws.onerror = (e) => this.rejectReady(/* @__PURE__ */ new Error(`signaling error: ${String(e?.message ?? e)}`));
|
|
52
|
+
ws.onclose = () => {
|
|
53
|
+
if (!this.confirmed) this.rejectReady(/* @__PURE__ */ new Error("signaling closed before connect"));
|
|
54
|
+
};
|
|
55
|
+
ws.onmessage = (ev) => void this.onSignal(ev);
|
|
56
|
+
return ws;
|
|
57
|
+
}
|
|
58
|
+
async onSignal(ev) {
|
|
59
|
+
const m = JSON.parse(typeof ev.data === "string" ? ev.data : await ev.data.text());
|
|
60
|
+
if (m.type === "offer") {
|
|
61
|
+
this.hostPeer = m.from;
|
|
62
|
+
const pc = new RTCPeerConnection({ iceServers: m.iceServers || [] });
|
|
63
|
+
this.pc = pc;
|
|
64
|
+
pc.onicecandidate = (e) => {
|
|
65
|
+
if (e.candidate) this.ws.send(JSON.stringify({
|
|
66
|
+
type: "candidate",
|
|
67
|
+
to: this.hostPeer,
|
|
68
|
+
candidate: e.candidate
|
|
69
|
+
}));
|
|
70
|
+
};
|
|
71
|
+
pc.ondatachannel = (e) => {
|
|
72
|
+
const dc = e.channel;
|
|
73
|
+
this.dc = dc;
|
|
74
|
+
dc.binaryType = "arraybuffer";
|
|
75
|
+
dc.onopen = () => this.enqueueSeal(FLAG_CONFIRM, {
|
|
76
|
+
t: "confirm",
|
|
77
|
+
nonce: this.myNonce
|
|
78
|
+
});
|
|
79
|
+
dc.onmessage = (ev2) => {
|
|
80
|
+
this.recvChain = this.recvChain.then(() => this.onFrame(ev2.data)).catch(() => {});
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
await pc.setRemoteDescription({
|
|
84
|
+
type: "offer",
|
|
85
|
+
sdp: m.sdp
|
|
86
|
+
});
|
|
87
|
+
const answer = await pc.createAnswer();
|
|
88
|
+
await pc.setLocalDescription(answer);
|
|
89
|
+
this.th = await computeTranscriptHash(m.sdp, pc.localDescription.sdp);
|
|
90
|
+
const keys = await deriveDirKeys(this.link.s, this.th);
|
|
91
|
+
this.keyC2H = keys.keyC2H;
|
|
92
|
+
this.keyH2C = keys.keyH2C;
|
|
93
|
+
this.ws.send(JSON.stringify({
|
|
94
|
+
type: "answer",
|
|
95
|
+
to: this.hostPeer,
|
|
96
|
+
sdp: pc.localDescription.sdp
|
|
97
|
+
}));
|
|
98
|
+
} else if (m.type === "candidate" && this.pc) await this.pc.addIceCandidate(m.candidate).catch(() => {});
|
|
99
|
+
}
|
|
100
|
+
enqueueSeal(flags, obj) {
|
|
101
|
+
this.sendChain = this.sendChain.then(async () => {
|
|
102
|
+
if (!this.dc || this.dc.readyState !== "open" || !this.keyC2H || !this.th) return;
|
|
103
|
+
const frame = await seal(this.keyC2H, this.send, flags, this.th, packEnvelope(obj));
|
|
104
|
+
try {
|
|
105
|
+
this.dc.send(frame);
|
|
106
|
+
} catch {}
|
|
107
|
+
});
|
|
108
|
+
return this.sendChain;
|
|
109
|
+
}
|
|
110
|
+
async onFrame(data) {
|
|
111
|
+
if (typeof data === "string" || !this.keyH2C || !this.th) return;
|
|
112
|
+
let env;
|
|
113
|
+
try {
|
|
114
|
+
const { plaintext } = await open(this.keyH2C, data, this.th, this.recv);
|
|
115
|
+
env = unpackEnvelope(plaintext);
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!this.confirmed) {
|
|
120
|
+
if (!env || env.t !== "confirm") return;
|
|
121
|
+
if (typeof env.nonce === "string" && !this.confirmedOut) {
|
|
122
|
+
await this.enqueueSeal(FLAG_CONFIRM, {
|
|
123
|
+
t: "confirm",
|
|
124
|
+
nonce: this.myNonce,
|
|
125
|
+
echo: env.nonce
|
|
126
|
+
});
|
|
127
|
+
this.confirmedOut = true;
|
|
128
|
+
}
|
|
129
|
+
if (env.echo && env.echo === this.myNonce) this.confirmedIn = true;
|
|
130
|
+
if (this.confirmedIn && this.confirmedOut) {
|
|
131
|
+
this.confirmed = true;
|
|
132
|
+
this.resolveReady();
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!env || env.t === "confirm") return;
|
|
137
|
+
const p = this.pending.get(env.id);
|
|
138
|
+
if (!p) return;
|
|
139
|
+
if (env.t === "res") p.onRes(env.status, env.ct);
|
|
140
|
+
else if (env.t === "data") p.onData(env.chunk);
|
|
141
|
+
else if (env.t === "end") p.onEnd(env.error);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Issue one request over the channel. Resolves once the response head (status,
|
|
145
|
+
* content-type) arrives; the body is a ReadableStream fed by `data` frames as
|
|
146
|
+
* they land, so streaming endpoints (SSE `tail`) flow without buffering.
|
|
147
|
+
*/
|
|
148
|
+
request(method, path, body) {
|
|
149
|
+
const id = String(++this.idCounter);
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
let controller;
|
|
152
|
+
const enc = new TextEncoder();
|
|
153
|
+
let head = false;
|
|
154
|
+
const stream = new ReadableStream({
|
|
155
|
+
start: (c) => {
|
|
156
|
+
controller = c;
|
|
157
|
+
},
|
|
158
|
+
cancel: () => {
|
|
159
|
+
this.pending.delete(id);
|
|
160
|
+
this.enqueueSeal(0, {
|
|
161
|
+
t: "abort",
|
|
162
|
+
id
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
this.pending.set(id, {
|
|
167
|
+
onRes: (status, ct) => {
|
|
168
|
+
if (!head) {
|
|
169
|
+
head = true;
|
|
170
|
+
resolve({
|
|
171
|
+
status,
|
|
172
|
+
ct,
|
|
173
|
+
stream
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
onData: (chunk) => {
|
|
178
|
+
try {
|
|
179
|
+
controller.enqueue(enc.encode(chunk));
|
|
180
|
+
} catch {}
|
|
181
|
+
},
|
|
182
|
+
onEnd: (error) => {
|
|
183
|
+
this.pending.delete(id);
|
|
184
|
+
if (error) {
|
|
185
|
+
if (!head) {
|
|
186
|
+
head = true;
|
|
187
|
+
reject(new Error(error));
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
controller.error(new Error(error));
|
|
191
|
+
} catch {}
|
|
192
|
+
} else try {
|
|
193
|
+
controller.close();
|
|
194
|
+
} catch {}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
this.enqueueSeal(0, {
|
|
198
|
+
t: "req",
|
|
199
|
+
id,
|
|
200
|
+
method,
|
|
201
|
+
path,
|
|
202
|
+
body
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
close() {
|
|
207
|
+
try {
|
|
208
|
+
this.ws.close();
|
|
209
|
+
} catch {}
|
|
210
|
+
try {
|
|
211
|
+
this.pc?.close();
|
|
212
|
+
} catch {}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Connect to a share room and start a local HTTP server that forwards every
|
|
217
|
+
* request over the encrypted DataChannel. Returns the loopback base URL the
|
|
218
|
+
* existing remote commands can `fetch()` against. The process owns teardown:
|
|
219
|
+
* `ay` subcommands `process.exit()` when done, which closes the socket/peer.
|
|
220
|
+
*/
|
|
221
|
+
async function startWebrtcBridge(link) {
|
|
222
|
+
const parsed = parseWebrtcLink(link);
|
|
223
|
+
if (!parsed) throw new Error(`not a WebRTC share link: ${link}`);
|
|
224
|
+
const conn = new WebRtcConn(parsed);
|
|
225
|
+
await conn.ready;
|
|
226
|
+
const server = Bun.serve({
|
|
227
|
+
port: 0,
|
|
228
|
+
hostname: "127.0.0.1",
|
|
229
|
+
idleTimeout: 0,
|
|
230
|
+
async fetch(req) {
|
|
231
|
+
const u = new URL(req.url);
|
|
232
|
+
const pathWithQuery = u.pathname + u.search;
|
|
233
|
+
const body = req.method !== "GET" && req.method !== "HEAD" ? await req.text() : void 0;
|
|
234
|
+
try {
|
|
235
|
+
const { status, ct, stream } = await conn.request(req.method, pathWithQuery, body);
|
|
236
|
+
return new Response(stream, {
|
|
237
|
+
status,
|
|
238
|
+
headers: ct ? { "content-type": ct } : {}
|
|
239
|
+
});
|
|
240
|
+
} catch (e) {
|
|
241
|
+
return new Response(String(e?.message ?? e), { status: 502 });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
baseUrl: `http://127.0.0.1:${server.port}`,
|
|
247
|
+
token: "webrtc",
|
|
248
|
+
close: () => {
|
|
249
|
+
try {
|
|
250
|
+
server.stop(true);
|
|
251
|
+
} catch {}
|
|
252
|
+
conn.close();
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
//#endregion
|
|
258
|
+
export { startWebrtcBridge };
|
|
259
|
+
//# sourceMappingURL=webrtcRemote-jGM3ZHK3.js.map
|
package/package.json
CHANGED
package/ts/remotes.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "fs/promises";
|
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import yaml from "yaml";
|
|
5
|
+
import { isWebrtcSpec } from "./webrtcLink.ts";
|
|
5
6
|
|
|
6
7
|
function remotesPath(): string {
|
|
7
8
|
const dir = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
@@ -79,6 +80,10 @@ export function parseDirectRemoteSpec(
|
|
|
79
80
|
* Returns null if the spec doesn't match any remote.
|
|
80
81
|
*/
|
|
81
82
|
export async function resolveRemoteSpec(spec: string): Promise<ResolvedRemote | null> {
|
|
83
|
+
// Inline WebRTC share link: `ay ls webrtc://…` or `ay ls https://…/w/#room:token`.
|
|
84
|
+
// These carry their own secret and have no keyword (use an alias to add one).
|
|
85
|
+
if (isWebrtcSpec(spec)) return resolveWebrtc(spec, undefined);
|
|
86
|
+
|
|
82
87
|
const direct = parseDirectRemoteSpec(spec);
|
|
83
88
|
if (direct) {
|
|
84
89
|
return { url: direct.baseUrl, token: direct.token, keyword: direct.keyword };
|
|
@@ -92,9 +97,22 @@ export async function resolveRemoteSpec(spec: string): Promise<ResolvedRemote |
|
|
|
92
97
|
const remotes = await readRemotes();
|
|
93
98
|
const cfg = remotes.get(alias);
|
|
94
99
|
if (!cfg) return null;
|
|
100
|
+
// A saved alias may point at a WebRTC link; bridge it just like an inline one.
|
|
101
|
+
if (isWebrtcSpec(cfg.url)) return resolveWebrtc(cfg.url, keyword);
|
|
95
102
|
return { url: cfg.url, token: cfg.token, keyword };
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Start a local HTTP↔WebRTC bridge for a share link and present it as an
|
|
107
|
+
* ordinary http remote, so every fetch-based remote command works unchanged.
|
|
108
|
+
* The bridge lives for the rest of the process (torn down on `process.exit`).
|
|
109
|
+
*/
|
|
110
|
+
async function resolveWebrtc(link: string, keyword?: string): Promise<ResolvedRemote> {
|
|
111
|
+
const { startWebrtcBridge } = await import("./webrtcRemote.ts");
|
|
112
|
+
const bridge = await startWebrtcBridge(link);
|
|
113
|
+
return { url: bridge.baseUrl, token: bridge.token, keyword };
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
// ---------------------------------------------------------------------------
|
|
99
117
|
// ay remote subcommand
|
|
100
118
|
// ---------------------------------------------------------------------------
|
|
@@ -108,7 +126,9 @@ export async function cmdRemote(rest: string[]): Promise<number> {
|
|
|
108
126
|
`Manage saved remote server aliases.\n\n` +
|
|
109
127
|
`Subcommands:\n` +
|
|
110
128
|
` ay remote ls list configured remotes\n` +
|
|
111
|
-
` ay remote add <alias> http://<token>@<host>:<port> add
|
|
129
|
+
` ay remote add <alias> http://<token>@<host>:<port> add an http remote\n` +
|
|
130
|
+
` ay remote add <alias> webrtc://<room>:<token>@<host> add a WebRTC share remote\n` +
|
|
131
|
+
` ay remote add <alias> https://agent-yes.com/w/#<room>:<token> (share link form)\n` +
|
|
112
132
|
` ay remote rm <alias> remove a remote\n\n` +
|
|
113
133
|
`Once added, use the alias anywhere a keyword is accepted:\n` +
|
|
114
134
|
` ay ls <alias>\n` +
|
|
@@ -145,6 +165,13 @@ export async function cmdRemote(rest: string[]): Promise<number> {
|
|
|
145
165
|
);
|
|
146
166
|
return 1;
|
|
147
167
|
}
|
|
168
|
+
// WebRTC share links carry their own secret — store verbatim (token in the link).
|
|
169
|
+
if (isWebrtcSpec(rawUrl)) {
|
|
170
|
+
await writeRemoteAlias(alias, { url: rawUrl, token: "" });
|
|
171
|
+
process.stdout.write(`remote '${alias}' added → ${rawUrl} (webrtc)\n`);
|
|
172
|
+
process.stderr.write(`\n ay ls ${alias} # list agents on ${alias}\n`);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
148
175
|
let url: string, token: string;
|
|
149
176
|
try {
|
|
150
177
|
const parsed = new URL(rawUrl);
|
package/ts/subcommands.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { invokedCliName } from "./invokedCli.ts";
|
|
|
32
32
|
import type { AgentCliConfig } from "./index.ts";
|
|
33
33
|
import yargs from "yargs";
|
|
34
34
|
import { type ResolvedRemote, readRemotes, resolveRemoteSpec } from "./remotes.ts";
|
|
35
|
+
import { isWebrtcSpec } from "./webrtcLink.ts";
|
|
35
36
|
|
|
36
37
|
// ---------------------------------------------------------------------------
|
|
37
38
|
// notes store (~/.agent-yes/notes.jsonl)
|
|
@@ -865,15 +866,27 @@ async function fetchRemoteRecordsRaw(
|
|
|
865
866
|
if (opts.all) params.set("all", "1");
|
|
866
867
|
if (opts.active) params.set("active", "1");
|
|
867
868
|
if (opts.keyword) params.set("keyword", opts.keyword);
|
|
869
|
+
// WebRTC remotes have no http port — bridge them, then fetch the loopback URL.
|
|
870
|
+
let bridge: { baseUrl: string; token: string; close: () => void } | null = null;
|
|
868
871
|
try {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
+
let base = url;
|
|
873
|
+
let bearer = token;
|
|
874
|
+
if (isWebrtcSpec(url)) {
|
|
875
|
+
const { startWebrtcBridge } = await import("./webrtcRemote.ts");
|
|
876
|
+
bridge = await startWebrtcBridge(url);
|
|
877
|
+
base = bridge.baseUrl;
|
|
878
|
+
bearer = bridge.token;
|
|
879
|
+
}
|
|
880
|
+
const res = await fetch(`${base}/api/ls?${params}`, {
|
|
881
|
+
headers: { Authorization: `Bearer ${bearer}` },
|
|
882
|
+
signal: AbortSignal.timeout(8000),
|
|
872
883
|
});
|
|
873
884
|
if (!res.ok) return [];
|
|
874
885
|
return (await res.json()) as any[];
|
|
875
886
|
} catch {
|
|
876
887
|
return [];
|
|
888
|
+
} finally {
|
|
889
|
+
bridge?.close();
|
|
877
890
|
}
|
|
878
891
|
}
|
|
879
892
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { DEFAULT_SIGHOST, isWebrtcSpec, parseWebrtcLink } from "./webrtcLink.ts";
|
|
3
|
+
|
|
4
|
+
// A representative v2 share secret (e1.<64 hex>); parseSecret keeps the hex `s`.
|
|
5
|
+
const SECRET = "e1.982610a3034f065bfe9700037b306a6afeb7dc48567064058e6c4bbc09e502c2";
|
|
6
|
+
|
|
7
|
+
describe("isWebrtcSpec", () => {
|
|
8
|
+
it("accepts webrtc:// links", () => {
|
|
9
|
+
expect(isWebrtcSpec(`webrtc://r1:${SECRET}@s.agent-yes.com`)).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
it("accepts https share links (have a # fragment)", () => {
|
|
12
|
+
expect(isWebrtcSpec(`https://agent-yes.com/w/#r1:${SECRET}`)).toBe(true);
|
|
13
|
+
expect(isWebrtcSpec(`http://localhost:8080/w/#r1:${SECRET}`)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it("rejects http remotes and bare aliases", () => {
|
|
16
|
+
expect(isWebrtcSpec("token@192.168.1.5:7432")).toBe(false);
|
|
17
|
+
expect(isWebrtcSpec("work-mac")).toBe(false);
|
|
18
|
+
expect(isWebrtcSpec("work-mac:claude")).toBe(false);
|
|
19
|
+
expect(isWebrtcSpec("http://192.168.1.5:7432")).toBe(false); // no fragment
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("parseWebrtcLink", () => {
|
|
24
|
+
it("parses webrtc://room:token@host", () => {
|
|
25
|
+
const r = parseWebrtcLink(`webrtc://r223104:${SECRET}@example.com`);
|
|
26
|
+
expect(r).toEqual({ room: "r223104", s: expect.any(String), host: "example.com" });
|
|
27
|
+
expect(r!.s.length).toBeGreaterThan(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("parses an https share link and defaults the signaling host", () => {
|
|
31
|
+
const r = parseWebrtcLink(`https://agent-yes.com/w/#r223104:${SECRET}`);
|
|
32
|
+
expect(r).toMatchObject({ room: "r223104", host: DEFAULT_SIGHOST });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("honors an explicit @sighost in the fragment", () => {
|
|
36
|
+
const r = parseWebrtcLink(`https://agent-yes.com/w/#r1:${SECRET}@sig.example.com`);
|
|
37
|
+
expect(r).toMatchObject({ room: "r1", host: "sig.example.com" });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns null for non-share strings and malformed fragments", () => {
|
|
41
|
+
expect(parseWebrtcLink("token@host:7432")).toBeNull();
|
|
42
|
+
expect(parseWebrtcLink("just-an-alias")).toBeNull();
|
|
43
|
+
expect(parseWebrtcLink("https://agent-yes.com/w/#noColonHere")).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
package/ts/webrtcLink.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Pure parsing/detection for WebRTC share links. Kept free of node-datachannel
|
|
2
|
+
// (the native WebRTC dep) so callers — and resolveRemoteSpec on every remote
|
|
3
|
+
// command — can detect/parse a link without loading the native module, and so
|
|
4
|
+
// the helpers stay unit-testable. The actual connection lives in webrtcRemote.ts.
|
|
5
|
+
import { parseSecret } from "../lab/ui/e2e.js";
|
|
6
|
+
|
|
7
|
+
export const SIGNAL_SUBPROTOCOL = "ay-signal-1";
|
|
8
|
+
export const DEFAULT_SIGHOST = "s.agent-yes.com";
|
|
9
|
+
|
|
10
|
+
export interface WebrtcLink {
|
|
11
|
+
room: string;
|
|
12
|
+
s: string;
|
|
13
|
+
host: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse a share link into { room, secret, signaling-host }. Accepts:
|
|
18
|
+
* webrtc://<room>:<token>@<host>
|
|
19
|
+
* https://<anyhost>/w/#<room>:<token> (signaling host defaults to s.agent-yes.com)
|
|
20
|
+
* https://<anyhost>/w/#<room>:<token>@<sighost> (explicit signaling host)
|
|
21
|
+
* Returns null if the string isn't a recognizable share link.
|
|
22
|
+
*/
|
|
23
|
+
export function parseWebrtcLink(link: string): WebrtcLink | null {
|
|
24
|
+
const wr = /^webrtc:\/\/([^:]+):([^@]+)@(.+)$/.exec(link);
|
|
25
|
+
if (wr) {
|
|
26
|
+
const { s } = parseSecret(wr[2]!);
|
|
27
|
+
return { room: wr[1]!, s, host: wr[3]! };
|
|
28
|
+
}
|
|
29
|
+
if (/^https?:\/\//.test(link) && link.includes("#")) {
|
|
30
|
+
const frag = link.split("#")[1] ?? "";
|
|
31
|
+
const at = frag.split("@");
|
|
32
|
+
const host = at[1] || DEFAULT_SIGHOST;
|
|
33
|
+
const seg = at[0]!;
|
|
34
|
+
const i = seg.indexOf(":");
|
|
35
|
+
if (i < 0) return null;
|
|
36
|
+
const { s } = parseSecret(seg.slice(i + 1));
|
|
37
|
+
return { room: seg.slice(0, i), s, host };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True if `spec` looks like a WebRTC share link (vs. an http remote or alias). */
|
|
43
|
+
export function isWebrtcSpec(spec: string): boolean {
|
|
44
|
+
return spec.startsWith("webrtc://") || (/^https?:\/\//.test(spec) && spec.includes("#"));
|
|
45
|
+
}
|