agent-yes 1.97.0 → 1.99.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/dist/SUPPORTED_CLIS-BGUPuqya.js +8 -0
- package/dist/{SUPPORTED_CLIS-eD-UlqO_.js → SUPPORTED_CLIS-eIjVu8HF.js} +2 -2
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/serve-SQYFRbm3.js +554 -0
- package/dist/{share-DUhUA1Pi.js → share-BsCeIfQM.js} +21 -4
- package/dist/{subcommands-B4gXEu5I.js → subcommands-D3Z9cD9u.js} +1 -1
- package/dist/{subcommands-K242usI5.js → subcommands-z8Y8gcD_.js} +8 -3
- package/dist/{ts-BAc4Jcrw.js → ts-BECoCPV1.js} +2 -2
- package/dist/{versionChecker-MNvA73o9.js → versionChecker-pct2j3wR.js} +2 -2
- package/lab/ui/index.html +1645 -0
- package/lab/ui/room-client.js +520 -0
- package/package.json +4 -2
- package/ts/serve.ts +463 -311
- package/ts/share.ts +49 -17
- package/ts/subcommands.ts +6 -0
- package/dist/SUPPORTED_CLIS-CNO_pj9f.js +0 -8
- package/dist/serve-CKcbVPy6.js +0 -451
package/ts/share.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
// `ay serve --
|
|
2
|
-
// and bridge each browser peer's WebRTC DataChannel to this machine's
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// signaling protocol and lab/ui/index.html for the
|
|
1
|
+
// `ay serve --webrtc` host peer: connect to the signaling server as a room host
|
|
2
|
+
// and bridge each browser peer's WebRTC DataChannel to this machine's `ay serve`
|
|
3
|
+
// API handler, called in-process — no HTTP listener, no port, no tunnel. The
|
|
4
|
+
// browser (agent-yes.com) thus reaches local agents peer-to-peer. See
|
|
5
|
+
// lab/ui/cf/worker.ts for the signaling protocol and lab/ui/index.html for the
|
|
6
|
+
// browser side.
|
|
6
7
|
import { randomBytes } from "crypto";
|
|
8
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import path from "path";
|
|
7
11
|
|
|
8
12
|
const SUB = "ay-signal-1";
|
|
9
13
|
const ICE = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
@@ -11,16 +15,41 @@ const MAX_CHUNK = 15_000; // keep DataChannel messages under the SCTP limit
|
|
|
11
15
|
const DEFAULT_SIGHOST = "s.agent-yes.com";
|
|
12
16
|
|
|
13
17
|
export interface ShareOpts {
|
|
14
|
-
/** webrtc://room:token@host, or undefined to mint a fresh
|
|
18
|
+
/** webrtc://room:token@host, or undefined to mint a fresh (unpersisted)
|
|
19
|
+
* room+token — callers wanting a stable room use loadOrCreateShareRoom() */
|
|
15
20
|
url?: string;
|
|
16
21
|
/** signaling host when minting (default s.agent-yes.com) */
|
|
17
22
|
sighost?: string;
|
|
18
|
-
/** local ay-serve
|
|
19
|
-
|
|
23
|
+
/** the local ay-serve API handler the channel bridges to (called in-process) */
|
|
24
|
+
localFetch: (req: Request) => Promise<Response>;
|
|
20
25
|
/** bearer token for the local ay-serve API */
|
|
21
26
|
apiToken: string;
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
// The room+token persist like the serve token, so the share link (and any
|
|
30
|
+
// browser that saved the room) survives restarts — important for daemons,
|
|
31
|
+
// which would otherwise mint a new link on every restart. Delete the file to
|
|
32
|
+
// rotate the room.
|
|
33
|
+
function shareRoomPath(): string {
|
|
34
|
+
const home = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
35
|
+
return path.join(home, ".share-room");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function loadOrCreateShareRoom(sighost = DEFAULT_SIGHOST): Promise<string> {
|
|
39
|
+
try {
|
|
40
|
+
const url = (await readFile(shareRoomPath(), "utf-8")).trim();
|
|
41
|
+
if (url.startsWith("webrtc://")) return url;
|
|
42
|
+
} catch {
|
|
43
|
+
/* not yet minted */
|
|
44
|
+
}
|
|
45
|
+
const room = "r" + randomBytes(3).toString("hex");
|
|
46
|
+
const token = randomBytes(32).toString("hex");
|
|
47
|
+
const url = `webrtc://${room}:${token}@${sighost}`;
|
|
48
|
+
await mkdir(path.dirname(shareRoomPath()), { recursive: true });
|
|
49
|
+
await writeFile(shareRoomPath(), url, { mode: 0o600 });
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
function parseShareUrl(s: string): { room: string; token: string; host: string } {
|
|
25
54
|
const m = /^webrtc:\/\/([^:@/]+):([^@/]+)@(.+)$/.exec(s);
|
|
26
55
|
if (!m) throw new Error(`bad --share url: ${s} (want webrtc://room:token@host)`);
|
|
@@ -157,15 +186,18 @@ export async function startShare(opts: ShareOpts): Promise<{ room: string; link:
|
|
|
157
186
|
const ac = new AbortController();
|
|
158
187
|
aborts.set(id, ac);
|
|
159
188
|
try {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
189
|
+
// The host part is a placeholder — the handler only routes on the path.
|
|
190
|
+
const res = await opts.localFetch(
|
|
191
|
+
new Request(`http://ay.local${p}`, {
|
|
192
|
+
method,
|
|
193
|
+
headers: {
|
|
194
|
+
Authorization: `Bearer ${opts.apiToken}`,
|
|
195
|
+
...(body ? { "Content-Type": "application/json" } : {}),
|
|
196
|
+
},
|
|
197
|
+
body: body ?? undefined,
|
|
198
|
+
signal: ac.signal,
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
169
201
|
send(dc, { t: "res", id, status: res.status, ct: res.headers.get("content-type") ?? "" });
|
|
170
202
|
const reader = res.body!.getReader();
|
|
171
203
|
const dec = new TextDecoder();
|
package/ts/subcommands.ts
CHANGED
|
@@ -141,6 +141,7 @@ const SUBCOMMANDS = new Set([
|
|
|
141
141
|
"restart",
|
|
142
142
|
"note",
|
|
143
143
|
"serve",
|
|
144
|
+
"setup",
|
|
144
145
|
"remote",
|
|
145
146
|
"help",
|
|
146
147
|
]);
|
|
@@ -190,6 +191,10 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
190
191
|
const { cmdServe } = await import("./serve.ts");
|
|
191
192
|
return cmdServe(rest);
|
|
192
193
|
}
|
|
194
|
+
case "setup": {
|
|
195
|
+
const { cmdSetup } = await import("./setup.ts");
|
|
196
|
+
return cmdSetup(rest);
|
|
197
|
+
}
|
|
193
198
|
case "remote": {
|
|
194
199
|
const { cmdRemote } = await import("./remotes.ts");
|
|
195
200
|
return cmdRemote(rest);
|
|
@@ -225,6 +230,7 @@ export function cmdHelp(): number {
|
|
|
225
230
|
` ay status <keyword> agent status snapshot\n` +
|
|
226
231
|
`\n` +
|
|
227
232
|
`Remote:\n` +
|
|
233
|
+
` ay setup guided setup: pick a workspace, share to agent-yes.com\n` +
|
|
228
234
|
` ay serve [--port N] start HTTP API server (prints token)\n` +
|
|
229
235
|
` ay remote add <alias> http://<token>@<host>:<port>\n` +
|
|
230
236
|
` ay remote ls / rm <alias> manage saved remotes\n` +
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import "./ts-BAc4Jcrw.js";
|
|
2
|
-
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-MNvA73o9.js";
|
|
4
|
-
import "./pidStore-DBjlqzo8.js";
|
|
5
|
-
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-eD-UlqO_.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|
package/dist/serve-CKcbVPy6.js
DELETED
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
import "./ts-BAc4Jcrw.js";
|
|
2
|
-
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-MNvA73o9.js";
|
|
4
|
-
import "./pidStore-DBjlqzo8.js";
|
|
5
|
-
import "./globalPidIndex-yVd3mbsV.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-eD-UlqO_.js";
|
|
7
|
-
import "./remotes-C3xPRtfg.js";
|
|
8
|
-
import { c as readNotes, f as snapshotStatus, l as renderRawLog, m as writeToIpc, o as listRecords, r as controlCodeFromName, u as resolveOne } from "./subcommands-K242usI5.js";
|
|
9
|
-
import yargs from "yargs";
|
|
10
|
-
import { mkdir, open, readFile, writeFile } from "fs/promises";
|
|
11
|
-
import { homedir } from "os";
|
|
12
|
-
import path from "path";
|
|
13
|
-
import { watch } from "node:fs";
|
|
14
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
15
|
-
|
|
16
|
-
//#region ts/serve.ts
|
|
17
|
-
const DEFAULT_PORT = 7432;
|
|
18
|
-
function agentYesHome() {
|
|
19
|
-
return process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
20
|
-
}
|
|
21
|
-
function tokenPath() {
|
|
22
|
-
return path.join(agentYesHome(), ".serve-token");
|
|
23
|
-
}
|
|
24
|
-
async function loadOrCreateToken(tokenFlag) {
|
|
25
|
-
if (tokenFlag) return tokenFlag;
|
|
26
|
-
try {
|
|
27
|
-
return (await readFile(tokenPath(), "utf-8")).trim();
|
|
28
|
-
} catch {
|
|
29
|
-
const token = randomBytes(20).toString("hex");
|
|
30
|
-
await mkdir(agentYesHome(), { recursive: true });
|
|
31
|
-
await writeFile(tokenPath(), token, { mode: 384 });
|
|
32
|
-
return token;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function checkAuth(req, expectedToken) {
|
|
36
|
-
const auth = req.headers.get("authorization") ?? "";
|
|
37
|
-
if (!auth.startsWith("Bearer ")) return false;
|
|
38
|
-
const provided = auth.slice(7);
|
|
39
|
-
const maxLen = Math.max(provided.length, expectedToken.length);
|
|
40
|
-
return timingSafeEqual(Buffer.from(provided.padEnd(maxLen, "\0")), Buffer.from(expectedToken.padEnd(maxLen, "\0"))) && provided.length === expectedToken.length;
|
|
41
|
-
}
|
|
42
|
-
const defaultOpts = (overrides = {}) => ({
|
|
43
|
-
all: false,
|
|
44
|
-
active: false,
|
|
45
|
-
json: true,
|
|
46
|
-
latest: true,
|
|
47
|
-
cwdScope: null,
|
|
48
|
-
...overrides
|
|
49
|
-
});
|
|
50
|
-
const DAEMON_NAME = "agent-yes";
|
|
51
|
-
async function cmdServeDaemon(sub, args) {
|
|
52
|
-
const oxmgrBin = Bun.which("oxmgr");
|
|
53
|
-
if (!oxmgrBin) {
|
|
54
|
-
process.stderr.write("ay serve install: oxmgr not found\n install with: cargo install oxmgr\n or: bun add -g oxmgr\n");
|
|
55
|
-
return 1;
|
|
56
|
-
}
|
|
57
|
-
if (sub === "install") {
|
|
58
|
-
const token = await loadOrCreateToken(void 0);
|
|
59
|
-
const serveCmd = [
|
|
60
|
-
"ay",
|
|
61
|
-
"serve",
|
|
62
|
-
...args
|
|
63
|
-
].join(" ");
|
|
64
|
-
const code = await Bun.spawn([
|
|
65
|
-
oxmgrBin,
|
|
66
|
-
"start",
|
|
67
|
-
serveCmd,
|
|
68
|
-
"--name",
|
|
69
|
-
DAEMON_NAME,
|
|
70
|
-
"--restart",
|
|
71
|
-
"always"
|
|
72
|
-
], { stdio: [
|
|
73
|
-
"ignore",
|
|
74
|
-
"inherit",
|
|
75
|
-
"inherit"
|
|
76
|
-
] }).exited;
|
|
77
|
-
if (code === 0) {
|
|
78
|
-
process.stdout.write(`\ninstalled '${DAEMON_NAME}' as a daemon via oxmgr\n`);
|
|
79
|
-
process.stdout.write(`token: ${token}\n\n`);
|
|
80
|
-
process.stdout.write(` ay ls ${token}@<host>:${DEFAULT_PORT}\n`);
|
|
81
|
-
process.stdout.write(` ay remote add <alias> http://${token}@<host>:${DEFAULT_PORT}\n`);
|
|
82
|
-
process.stdout.write(` ay serve logs # view server logs\n`);
|
|
83
|
-
process.stdout.write(` ay serve uninstall # remove daemon\n`);
|
|
84
|
-
}
|
|
85
|
-
return code ?? 1;
|
|
86
|
-
}
|
|
87
|
-
if (sub === "uninstall") return await Bun.spawn([
|
|
88
|
-
oxmgrBin,
|
|
89
|
-
"delete",
|
|
90
|
-
DAEMON_NAME
|
|
91
|
-
], { stdio: [
|
|
92
|
-
"ignore",
|
|
93
|
-
"inherit",
|
|
94
|
-
"inherit"
|
|
95
|
-
] }).exited ?? 1;
|
|
96
|
-
if (sub === "logs") return await Bun.spawn([
|
|
97
|
-
oxmgrBin,
|
|
98
|
-
"logs",
|
|
99
|
-
DAEMON_NAME,
|
|
100
|
-
...args
|
|
101
|
-
], { stdio: [
|
|
102
|
-
"ignore",
|
|
103
|
-
"inherit",
|
|
104
|
-
"inherit"
|
|
105
|
-
] }).exited ?? 1;
|
|
106
|
-
return 1;
|
|
107
|
-
}
|
|
108
|
-
async function cmdServe(rest) {
|
|
109
|
-
if (rest.includes("-h") || rest.includes("--help")) {
|
|
110
|
-
process.stdout.write(`Usage: ay serve [options]
|
|
111
|
-
|
|
112
|
-
Start an HTTP API server so remote machines can list/tail/send agents.
|
|
113
|
-
|
|
114
|
-
Options:
|
|
115
|
-
--port N Port to listen on (default: ${DEFAULT_PORT})\n --host HOST Interface to bind (default: 127.0.0.1; use 0.0.0.0 to expose)\n --token TOKEN Auth token (auto-generated and saved if omitted)\n --share [URL] Share over WebRTC to agent-yes.com (bare flag mints a room+link)\n --allow-spawn Deprecated no-op — the console can always spawn agents\n --tls-cert FILE TLS certificate PEM\n --tls-key FILE TLS private key PEM\n\nSubcommands:\n ay serve install install as background daemon via oxmgr\n ay serve uninstall remove daemon\n ay serve logs view daemon logs\n\nOnce running, connect from another machine:\n ay ls <token>@<host>:${DEFAULT_PORT}\n ay remote add <alias> http://<token>@<host>:${DEFAULT_PORT}\n`);
|
|
116
|
-
return 0;
|
|
117
|
-
}
|
|
118
|
-
const sub = rest[0];
|
|
119
|
-
if (sub === "install" || sub === "uninstall" || sub === "logs") return cmdServeDaemon(sub, rest.slice(1));
|
|
120
|
-
const argv = await yargs(rest).usage("Usage: ay serve [options]").option("port", {
|
|
121
|
-
type: "number",
|
|
122
|
-
default: DEFAULT_PORT,
|
|
123
|
-
description: "Port to listen on"
|
|
124
|
-
}).option("host", {
|
|
125
|
-
type: "string",
|
|
126
|
-
default: "127.0.0.1",
|
|
127
|
-
description: "Interface to bind (use 0.0.0.0 to expose)"
|
|
128
|
-
}).option("token", {
|
|
129
|
-
type: "string",
|
|
130
|
-
description: "Auth token (auto-generated if omitted)"
|
|
131
|
-
}).option("tls-cert", {
|
|
132
|
-
type: "string",
|
|
133
|
-
description: "TLS certificate file (PEM)"
|
|
134
|
-
}).option("tls-key", {
|
|
135
|
-
type: "string",
|
|
136
|
-
description: "TLS private key file (PEM)"
|
|
137
|
-
}).option("share", {
|
|
138
|
-
type: "string",
|
|
139
|
-
description: "Share over WebRTC: bare flag mints a room+link, or pass webrtc://room:token@host"
|
|
140
|
-
}).option("allow-spawn", {
|
|
141
|
-
type: "boolean",
|
|
142
|
-
default: false,
|
|
143
|
-
description: "Deprecated no-op — the console can always spawn agents"
|
|
144
|
-
}).help(false).version(false).exitProcess(false).parseAsync();
|
|
145
|
-
const port = argv.port ?? DEFAULT_PORT;
|
|
146
|
-
const host = argv.host ?? "127.0.0.1";
|
|
147
|
-
const tokenFlag = typeof argv.token === "string" ? argv.token : void 0;
|
|
148
|
-
const certPath = typeof argv["tls-cert"] === "string" ? argv["tls-cert"] : void 0;
|
|
149
|
-
const keyPath = typeof argv["tls-key"] === "string" ? argv["tls-key"] : void 0;
|
|
150
|
-
if (certPath && !keyPath || !certPath && keyPath) {
|
|
151
|
-
process.stderr.write("ay serve: --tls-cert and --tls-key must both be provided\n");
|
|
152
|
-
return 1;
|
|
153
|
-
}
|
|
154
|
-
const useHttps = !!(certPath && keyPath);
|
|
155
|
-
const scheme = useHttps ? "https" : "http";
|
|
156
|
-
if (host !== "127.0.0.1" && host !== "localhost") process.stderr.write("ay serve: warning: binding to non-loopback — ensure your network is trusted or use Tailscale/VPN\n");
|
|
157
|
-
const token = await loadOrCreateToken(tokenFlag);
|
|
158
|
-
const serverOpts = {
|
|
159
|
-
hostname: host,
|
|
160
|
-
port,
|
|
161
|
-
async fetch(req) {
|
|
162
|
-
if (!checkAuth(req, token)) return new Response("Unauthorized", { status: 401 });
|
|
163
|
-
const url = new URL(req.url);
|
|
164
|
-
const p = url.pathname;
|
|
165
|
-
if (req.method === "GET" && p === "/api/ls") {
|
|
166
|
-
const keyword = url.searchParams.get("keyword") ?? void 0;
|
|
167
|
-
const opts = defaultOpts({
|
|
168
|
-
all: url.searchParams.get("all") === "1",
|
|
169
|
-
active: url.searchParams.get("active") === "1"
|
|
170
|
-
});
|
|
171
|
-
try {
|
|
172
|
-
const records = await listRecords(keyword, opts);
|
|
173
|
-
return Response.json(records);
|
|
174
|
-
} catch (e) {
|
|
175
|
-
return new Response(e.message, { status: 500 });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (req.method === "GET" && p === "/api/notes") {
|
|
179
|
-
const notes = await readNotes();
|
|
180
|
-
return Response.json(Object.fromEntries(notes));
|
|
181
|
-
}
|
|
182
|
-
const statusM = /^\/api\/status\/(.+)$/.exec(p);
|
|
183
|
-
if (req.method === "GET" && statusM) {
|
|
184
|
-
const keyword = decodeURIComponent(statusM[1]);
|
|
185
|
-
try {
|
|
186
|
-
const snap = await snapshotStatus(await resolveOne(keyword, defaultOpts({ all: true })));
|
|
187
|
-
return Response.json(snap);
|
|
188
|
-
} catch (e) {
|
|
189
|
-
return new Response(e.message, { status: 404 });
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const readM = /^\/api\/read\/(.+)$/.exec(p);
|
|
193
|
-
if (req.method === "GET" && readM) {
|
|
194
|
-
const keyword = decodeURIComponent(readM[1]);
|
|
195
|
-
const mode = url.searchParams.get("mode") ?? "tail";
|
|
196
|
-
const n = parseInt(url.searchParams.get("n") ?? "96", 10) || 96;
|
|
197
|
-
try {
|
|
198
|
-
const record = await resolveOne(keyword, defaultOpts());
|
|
199
|
-
if (!record.log_file) return new Response(`pid ${record.pid}: no log_file`, { status: 404 });
|
|
200
|
-
const text = await renderRawLog(await readFile(record.log_file), {
|
|
201
|
-
mode,
|
|
202
|
-
n
|
|
203
|
-
});
|
|
204
|
-
return new Response(text, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
205
|
-
} catch (e) {
|
|
206
|
-
return new Response(e.message, { status: 404 });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
const sizeM = /^\/api\/size\/(.+)$/.exec(p);
|
|
210
|
-
if (req.method === "GET" && sizeM) {
|
|
211
|
-
const keyword = decodeURIComponent(sizeM[1]);
|
|
212
|
-
try {
|
|
213
|
-
const record = await resolveOne(keyword, defaultOpts());
|
|
214
|
-
const ayHome = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
215
|
-
let cols = null;
|
|
216
|
-
let rows = null;
|
|
217
|
-
try {
|
|
218
|
-
const [c, r] = (await readFile(path.join(ayHome, "ptysize", String(record.pid)), "utf-8")).trim().split(/\s+/).map(Number);
|
|
219
|
-
if (c > 0 && r > 0) {
|
|
220
|
-
cols = c;
|
|
221
|
-
rows = r;
|
|
222
|
-
}
|
|
223
|
-
} catch {}
|
|
224
|
-
return Response.json({
|
|
225
|
-
pid: record.pid,
|
|
226
|
-
cols,
|
|
227
|
-
rows
|
|
228
|
-
});
|
|
229
|
-
} catch (e) {
|
|
230
|
-
return new Response(e.message, { status: 404 });
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
const tailM = /^\/api\/tail\/(.+)$/.exec(p);
|
|
234
|
-
if (req.method === "GET" && tailM) {
|
|
235
|
-
const keyword = decodeURIComponent(tailM[1]);
|
|
236
|
-
const raw = url.searchParams.get("raw") === "1";
|
|
237
|
-
try {
|
|
238
|
-
const record = await resolveOne(keyword, defaultOpts());
|
|
239
|
-
if (!record.log_file) return new Response(`pid ${record.pid}: no log_file`, { status: 404 });
|
|
240
|
-
const logPath = record.log_file;
|
|
241
|
-
const stream = new ReadableStream({ async start(ctrl) {
|
|
242
|
-
const enc = new TextEncoder();
|
|
243
|
-
const send = (text) => ctrl.enqueue(enc.encode(`data: ${JSON.stringify(text)}\n\n`));
|
|
244
|
-
const ping = () => ctrl.enqueue(enc.encode(": ping\n\n"));
|
|
245
|
-
const initBuf = await readFile(logPath).catch(() => Buffer.alloc(0));
|
|
246
|
-
if (raw) send(new TextDecoder().decode(initBuf.slice(Math.max(0, initBuf.length - 65536))));
|
|
247
|
-
else send(await renderRawLog(initBuf, {
|
|
248
|
-
mode: "tail",
|
|
249
|
-
n: 96
|
|
250
|
-
}));
|
|
251
|
-
let offset = initBuf.length;
|
|
252
|
-
let closed = false;
|
|
253
|
-
const heartbeat = setInterval(() => {
|
|
254
|
-
if (closed) {
|
|
255
|
-
clearInterval(heartbeat);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
ping();
|
|
259
|
-
}, 15e3);
|
|
260
|
-
const ansiRe = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
|
|
261
|
-
const ctrlRe = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
262
|
-
const fh = await open(logPath, "r").catch(() => null);
|
|
263
|
-
let reading = false;
|
|
264
|
-
const flush = async () => {
|
|
265
|
-
if (closed || reading || !fh) return;
|
|
266
|
-
reading = true;
|
|
267
|
-
try {
|
|
268
|
-
const { size } = await fh.stat();
|
|
269
|
-
if (size < offset) offset = size;
|
|
270
|
-
if (size > offset) {
|
|
271
|
-
const len = size - offset;
|
|
272
|
-
const buf = Buffer.allocUnsafe(len);
|
|
273
|
-
const { bytesRead } = await fh.read(buf, 0, len, offset);
|
|
274
|
-
offset += bytesRead;
|
|
275
|
-
const chunk = buf.subarray(0, bytesRead);
|
|
276
|
-
if (raw) send(new TextDecoder().decode(chunk));
|
|
277
|
-
else {
|
|
278
|
-
const text = new TextDecoder().decode(chunk).replace(ansiRe, "").replace(ctrlRe, "");
|
|
279
|
-
if (text.trim()) send(text.trimStart());
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
} catch {} finally {
|
|
283
|
-
reading = false;
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
let watcher = null;
|
|
287
|
-
try {
|
|
288
|
-
watcher = watch(logPath, () => void flush());
|
|
289
|
-
} catch {}
|
|
290
|
-
const poller = setInterval(() => void flush(), 60);
|
|
291
|
-
req.signal.addEventListener("abort", () => {
|
|
292
|
-
closed = true;
|
|
293
|
-
clearInterval(heartbeat);
|
|
294
|
-
clearInterval(poller);
|
|
295
|
-
try {
|
|
296
|
-
watcher?.close();
|
|
297
|
-
} catch {}
|
|
298
|
-
fh?.close().catch(() => {});
|
|
299
|
-
try {
|
|
300
|
-
ctrl.close();
|
|
301
|
-
} catch {}
|
|
302
|
-
});
|
|
303
|
-
} });
|
|
304
|
-
return new Response(stream, { headers: {
|
|
305
|
-
"Content-Type": "text/event-stream",
|
|
306
|
-
"Cache-Control": "no-cache",
|
|
307
|
-
Connection: "keep-alive"
|
|
308
|
-
} });
|
|
309
|
-
} catch (e) {
|
|
310
|
-
return new Response(e.message, { status: 404 });
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
if (req.method === "POST" && p === "/api/send") {
|
|
314
|
-
let body;
|
|
315
|
-
try {
|
|
316
|
-
body = await req.json();
|
|
317
|
-
} catch {
|
|
318
|
-
return new Response("invalid JSON body", { status: 400 });
|
|
319
|
-
}
|
|
320
|
-
const { keyword, msg = "", code = "enter" } = body;
|
|
321
|
-
if (!keyword || typeof keyword !== "string") return new Response("missing keyword", { status: 400 });
|
|
322
|
-
try {
|
|
323
|
-
const record = await resolveOne(keyword, defaultOpts());
|
|
324
|
-
if (!record.fifo_file) return new Response(`pid ${record.pid}: no fifo_file`, { status: 409 });
|
|
325
|
-
const trailing = controlCodeFromName(code.toLowerCase());
|
|
326
|
-
if (msg && trailing) {
|
|
327
|
-
await writeToIpc(record.fifo_file, msg);
|
|
328
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
329
|
-
await writeToIpc(record.fifo_file, trailing);
|
|
330
|
-
} else await writeToIpc(record.fifo_file, msg + trailing);
|
|
331
|
-
return Response.json({
|
|
332
|
-
ok: true,
|
|
333
|
-
pid: record.pid
|
|
334
|
-
});
|
|
335
|
-
} catch (e) {
|
|
336
|
-
return new Response(e.message, { status: 404 });
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
const resizeM = /^\/api\/resize\/(.+)$/.exec(p);
|
|
340
|
-
if (req.method === "POST" && resizeM) {
|
|
341
|
-
const keyword = decodeURIComponent(resizeM[1]);
|
|
342
|
-
let body;
|
|
343
|
-
try {
|
|
344
|
-
body = await req.json();
|
|
345
|
-
} catch {
|
|
346
|
-
return new Response("invalid JSON body", { status: 400 });
|
|
347
|
-
}
|
|
348
|
-
const cols = Math.max(1, Math.floor(Number(body.cols) || 0));
|
|
349
|
-
const rows = Math.max(1, Math.floor(Number(body.rows) || 0));
|
|
350
|
-
if (!cols || !rows) return new Response("missing cols/rows", { status: 400 });
|
|
351
|
-
try {
|
|
352
|
-
const record = await resolveOne(keyword, defaultOpts());
|
|
353
|
-
const ayHome = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
354
|
-
const winsizeDir = path.join(ayHome, "winsize");
|
|
355
|
-
await mkdir(winsizeDir, { recursive: true });
|
|
356
|
-
await writeFile(path.join(winsizeDir, String(record.pid)), `${cols} ${rows} ${Date.now()}\n`);
|
|
357
|
-
try {
|
|
358
|
-
process.kill(record.pid, "SIGWINCH");
|
|
359
|
-
} catch {}
|
|
360
|
-
return Response.json({
|
|
361
|
-
ok: true,
|
|
362
|
-
pid: record.pid,
|
|
363
|
-
cols,
|
|
364
|
-
rows
|
|
365
|
-
});
|
|
366
|
-
} catch (e) {
|
|
367
|
-
return new Response(e.message, { status: 404 });
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (req.method === "POST" && p === "/api/spawn") {
|
|
371
|
-
let body;
|
|
372
|
-
try {
|
|
373
|
-
body = await req.json();
|
|
374
|
-
} catch {
|
|
375
|
-
return new Response("invalid JSON body", { status: 400 });
|
|
376
|
-
}
|
|
377
|
-
const cli = String(body.cli ?? "claude");
|
|
378
|
-
if (!SUPPORTED_CLIS.includes(cli)) return new Response(`unsupported cli: ${cli}`, { status: 400 });
|
|
379
|
-
const cwd = typeof body.cwd === "string" && body.cwd ? body.cwd : process.cwd();
|
|
380
|
-
const prompt = String(body.prompt ?? "");
|
|
381
|
-
process.stderr.write(`→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd})\n`);
|
|
382
|
-
try {
|
|
383
|
-
const child = Bun.spawn([
|
|
384
|
-
"ay",
|
|
385
|
-
cli,
|
|
386
|
-
...prompt ? ["--", prompt] : []
|
|
387
|
-
], {
|
|
388
|
-
cwd,
|
|
389
|
-
stdin: "ignore",
|
|
390
|
-
stdout: "ignore",
|
|
391
|
-
stderr: "ignore"
|
|
392
|
-
});
|
|
393
|
-
child.unref();
|
|
394
|
-
return Response.json({
|
|
395
|
-
ok: true,
|
|
396
|
-
pid: child.pid,
|
|
397
|
-
cli,
|
|
398
|
-
cwd
|
|
399
|
-
});
|
|
400
|
-
} catch (e) {
|
|
401
|
-
return new Response(e.message, { status: 500 });
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
return new Response("Not Found", { status: 404 });
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
if (useHttps) serverOpts.tls = {
|
|
408
|
-
cert: Bun.file(certPath),
|
|
409
|
-
key: Bun.file(keyPath)
|
|
410
|
-
};
|
|
411
|
-
const server = Bun.serve(serverOpts);
|
|
412
|
-
process.stdout.write(`ay serve ${scheme}://${host}:${port}\n`);
|
|
413
|
-
process.stdout.write(`token: ${token}\n\n`);
|
|
414
|
-
process.stdout.write(`connect from another machine:\n`);
|
|
415
|
-
process.stdout.write(` ay ls ${token}@<host>:${port}\n`);
|
|
416
|
-
process.stdout.write(` ay tail ${token}@<host>:${port}:<keyword>\n`);
|
|
417
|
-
process.stdout.write(` ay send ${token}@<host>:${port}:<keyword> "message"\n\n`);
|
|
418
|
-
process.stdout.write(`save as alias:\n`);
|
|
419
|
-
process.stdout.write(` ay remote add <alias> ${scheme}://${token}@<host>:${port}\n\n`);
|
|
420
|
-
if (!useHttps) process.stdout.write("for HTTPS: ay serve --tls-cert cert.pem --tls-key key.pem\n openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'\n\n");
|
|
421
|
-
if (argv.share !== void 0) {
|
|
422
|
-
const shareUrl = typeof argv.share === "string" && argv.share.startsWith("webrtc://") ? argv.share : void 0;
|
|
423
|
-
try {
|
|
424
|
-
const { startShare } = await import("./share-DUhUA1Pi.js");
|
|
425
|
-
const { link } = await startShare({
|
|
426
|
-
url: shareUrl,
|
|
427
|
-
apiUrl: `http://127.0.0.1:${port}`,
|
|
428
|
-
apiToken: token
|
|
429
|
-
});
|
|
430
|
-
process.stdout.write(`\nshared over WebRTC — open this link (the token is eaten from the URL on open):\n ${link}\n\n`);
|
|
431
|
-
} catch (e) {
|
|
432
|
-
process.stderr.write(`ay serve --share failed: ${e.message}\n`);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
process.stdout.write(`(Ctrl-C to stop)\n`);
|
|
436
|
-
await new Promise((resolve) => {
|
|
437
|
-
process.on("SIGINT", () => {
|
|
438
|
-
server.stop();
|
|
439
|
-
resolve();
|
|
440
|
-
});
|
|
441
|
-
process.on("SIGTERM", () => {
|
|
442
|
-
server.stop();
|
|
443
|
-
resolve();
|
|
444
|
-
});
|
|
445
|
-
});
|
|
446
|
-
return 0;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
//#endregion
|
|
450
|
-
export { cmdServe };
|
|
451
|
-
//# sourceMappingURL=serve-CKcbVPy6.js.map
|