agent-yes 1.205.0 → 1.206.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/SUPPORTED_CLIS-CQ6e94ln.js +9 -0
- package/dist/{SUPPORTED_CLIS-C8-qh98a.js → SUPPORTED_CLIS-DQSgURGW.js} +2 -2
- package/dist/{agentShare-99IGGOkv.js → agentShare-k8224qiZ.js} +3 -3
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-DK1HOMjj.js → notifyDaemon-BbVnNvlo.js} +2 -2
- package/dist/{rustBinary-Tgn948GR.js → rustBinary-BjRvvEs8.js} +2 -2
- package/dist/{schedule-CBoM67Vy.js → schedule-DJ-v4yVa.js} +4 -4
- package/dist/{serve-IvOlaL-7.js → serve-crLypY_c.js} +162 -39
- package/dist/{setup-BaKdS-3i.js → setup-BPNkeSmY.js} +2 -2
- package/dist/{share-Ciw1mWVN.js → share-_QYjPN7q.js} +1 -1
- package/dist/{share-BfIU8t_h.js → share-vchIyVd8.js} +98 -5
- package/dist/{subcommands-DyDHZ5YI.js → subcommands-DCGZjr4t.js} +1 -1
- package/dist/{subcommands-Bs7wJRBB.js → subcommands-DkrMRVNh.js} +7 -7
- package/dist/{ts-Dm6jjLwC.js → ts-DnqRreF4.js} +2 -2
- package/dist/{versionChecker-BomfUpSo.js → versionChecker-d5c6_vgS.js} +2 -2
- package/dist/{ws-CSkP2mkD.js → ws-BmHpWaG_.js} +2 -2
- package/lab/ui/console-logic.js +3 -1
- package/lab/ui/index.html +39 -5
- package/package.json +1 -1
- package/ts/serve.spec.ts +13 -1
- package/ts/serve.ts +158 -28
- package/ts/share.ts +80 -1
- package/ts/statusText.spec.ts +17 -0
- package/ts/statusText.ts +19 -0
- package/dist/SUPPORTED_CLIS-D3XTFfS7.js +0 -9
package/ts/serve.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from "./subcommands.ts";
|
|
39
39
|
import { TYPING_BADGE } from "./badges.ts";
|
|
40
40
|
import { isTerminalReply } from "./terminalReply.ts";
|
|
41
|
+
import { parseStatusText } from "./statusText.ts";
|
|
41
42
|
import { ensureNodeRuntime, liveEnv } from "./nodeRuntime.ts";
|
|
42
43
|
import { acquireWebrtcHostLock, type ServeLockOwner } from "./serveLock.ts";
|
|
43
44
|
import { type MailParty, recordInbox } from "./messageLog.ts";
|
|
@@ -58,7 +59,56 @@ import {
|
|
|
58
59
|
resolveSpawnCwd,
|
|
59
60
|
} from "./workspaceConfig.ts";
|
|
60
61
|
|
|
61
|
-
const DEFAULT_PORT =
|
|
62
|
+
const DEFAULT_PORT = 0;
|
|
63
|
+
const PORTLESS_APP_NAME = "agent-yes";
|
|
64
|
+
const PORTLESS_CHILD_ENV = "AGENT_YES_PORTLESS_CHILD";
|
|
65
|
+
|
|
66
|
+
export function portlessConsoleUrl(token?: string): string {
|
|
67
|
+
return portlessConsoleUrlForOrigin(
|
|
68
|
+
process.env.PORTLESS_URL?.replace(/\/$/, "") || `https://${PORTLESS_APP_NAME}.localhost`,
|
|
69
|
+
token,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function portlessConsoleUrlForOrigin(origin: string, token?: string): string {
|
|
74
|
+
const fragment = token ? `/#k=${encodeURIComponent(token)}` : "/";
|
|
75
|
+
return `${origin}${fragment}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function resolvedPortlessConsoleUrl(token?: string): Promise<string> {
|
|
79
|
+
if (process.env.PORTLESS_URL) return portlessConsoleUrl(token);
|
|
80
|
+
try {
|
|
81
|
+
const port = Number(
|
|
82
|
+
(await readFile(path.join(homedir(), ".portless", "proxy.port"), "utf-8")).trim(),
|
|
83
|
+
);
|
|
84
|
+
if (Number.isInteger(port) && port > 0)
|
|
85
|
+
return portlessConsoleUrlForOrigin(
|
|
86
|
+
`https://${PORTLESS_APP_NAME}.localhost${port === 443 ? "" : `:${port}`}`,
|
|
87
|
+
token,
|
|
88
|
+
);
|
|
89
|
+
} catch {
|
|
90
|
+
/* Portless has not created state yet. */
|
|
91
|
+
}
|
|
92
|
+
return portlessConsoleUrl(token);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function runWithPortless(rest: string[]): Promise<number> {
|
|
96
|
+
const portless = Bun.which("portless");
|
|
97
|
+
if (!portless) {
|
|
98
|
+
process.stderr.write(
|
|
99
|
+
"ay serve: Portless is required for local HTTPS. Install it with: npm install -g portless\n",
|
|
100
|
+
);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const script = process.argv[1];
|
|
104
|
+
const self =
|
|
105
|
+
script && /\.[cm]?[jt]s$/.test(script) ? [process.execPath, script] : [process.execPath];
|
|
106
|
+
const proc = Bun.spawn([portless, PORTLESS_APP_NAME, ...self, "serve", ...rest], {
|
|
107
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
108
|
+
env: { ...liveEnv(), [PORTLESS_CHILD_ENV]: "1" },
|
|
109
|
+
});
|
|
110
|
+
return (await proc.exited) ?? 1;
|
|
111
|
+
}
|
|
62
112
|
|
|
63
113
|
/**
|
|
64
114
|
* Normalize a user-supplied GitHub-ish source into the standard
|
|
@@ -704,9 +754,28 @@ async function readDaemonServeArgs(mgr: DaemonManager): Promise<string[] | null>
|
|
|
704
754
|
}
|
|
705
755
|
}
|
|
706
756
|
|
|
707
|
-
function portFromArgs(args: string[]): number {
|
|
757
|
+
function portFromArgs(args: string[]): number | null {
|
|
708
758
|
const m = /--port[=\s](\d+)/.exec(args.join(" "));
|
|
709
|
-
return m ? Number(m[1]) :
|
|
759
|
+
return m ? Number(m[1]) : null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function argsUsePortless(args: string[]): boolean {
|
|
763
|
+
const wantsHttp =
|
|
764
|
+
args.some((a) => a.startsWith("--http") || a.startsWith("--share")) ||
|
|
765
|
+
!args.some((a) => a.startsWith("--webrtc"));
|
|
766
|
+
return wantsHttp && portFromArgs(args) === null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function portlessAppPort(): Promise<number | null> {
|
|
770
|
+
try {
|
|
771
|
+
const routes = JSON.parse(
|
|
772
|
+
await readFile(path.join(homedir(), ".portless", "routes.json"), "utf-8"),
|
|
773
|
+
) as { hostname?: string; port?: number }[];
|
|
774
|
+
const route = routes.find((r) => r.hostname === `${PORTLESS_APP_NAME}.localhost`);
|
|
775
|
+
return typeof route?.port === "number" ? route.port : null;
|
|
776
|
+
} catch {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
710
779
|
}
|
|
711
780
|
|
|
712
781
|
async function warnStrayServeProcesses(): Promise<void> {
|
|
@@ -741,7 +810,8 @@ function explicitWebrtcUrl(args: string[]): string | undefined {
|
|
|
741
810
|
// Ask the live daemon its version over the local HTTP API. null if it's not
|
|
742
811
|
// listening (webrtc-only) or too old to expose /api/version — both of which we
|
|
743
812
|
// treat as "outdated" so a re-install rolls it forward.
|
|
744
|
-
async function fetchDaemonVersion(port: number, token: string): Promise<string | null> {
|
|
813
|
+
async function fetchDaemonVersion(port: number | null, token: string): Promise<string | null> {
|
|
814
|
+
if (port === null) return null;
|
|
745
815
|
try {
|
|
746
816
|
const r = await fetch(`http://127.0.0.1:${port}/api/version`, {
|
|
747
817
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -889,7 +959,8 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
889
959
|
// share link for a WebRTC bridge that isn't actually running. A bare re-run
|
|
890
960
|
// passes no args, so effArgs === priorArgs and this stays a no-op as before.
|
|
891
961
|
const sameConfig = JSON.stringify(effArgs) === JSON.stringify(priorArgs);
|
|
892
|
-
const
|
|
962
|
+
const daemonPort = argsUsePortless(effArgs) ? await portlessAppPort() : portFromArgs(effArgs);
|
|
963
|
+
const runningVer = await fetchDaemonVersion(daemonPort, token);
|
|
893
964
|
if (runningVer === current && sameConfig) {
|
|
894
965
|
await ensureBootAutostart(mgr);
|
|
895
966
|
process.stdout.write(`'${DAEMON_NAME}' already running v${current} (up to date)\n`);
|
|
@@ -982,7 +1053,8 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
982
1053
|
const code = await proc.exited;
|
|
983
1054
|
if (code === 0) {
|
|
984
1055
|
const onBoot = await ensureBootAutostart(mgr);
|
|
985
|
-
const port = portFromArgs(effArgs);
|
|
1056
|
+
const port = argsUsePortless(effArgs) ? await portlessAppPort() : portFromArgs(effArgs);
|
|
1057
|
+
const localUrl = argsUsePortless(effArgs) ? await resolvedPortlessConsoleUrl(token) : null;
|
|
986
1058
|
// Mirror cmdServe's mode resolution: webrtc-only daemons open no HTTP port.
|
|
987
1059
|
const httpish =
|
|
988
1060
|
effArgs.some((a) => a.startsWith("--http") || a.startsWith("--share")) ||
|
|
@@ -1020,8 +1092,12 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
|
|
|
1020
1092
|
);
|
|
1021
1093
|
process.stdout.write(`token: ${token}\n\n`);
|
|
1022
1094
|
if (httpish) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1095
|
+
if (argsUsePortless(effArgs)) {
|
|
1096
|
+
process.stdout.write(` web console: ${localUrl}\n`);
|
|
1097
|
+
} else if (port !== null) {
|
|
1098
|
+
process.stdout.write(` ay ls ${token}@<host>:${port}\n`);
|
|
1099
|
+
process.stdout.write(` ay remote add <alias> http://${token}@<host>:${port}\n`);
|
|
1100
|
+
}
|
|
1025
1101
|
}
|
|
1026
1102
|
process.stdout.write(` ay serve logs # view server logs\n`);
|
|
1027
1103
|
process.stdout.write(` ay serve uninstall # remove daemon\n`);
|
|
@@ -1054,7 +1130,9 @@ async function cmdServeStatus(args: string[]): Promise<number> {
|
|
|
1054
1130
|
const daemonArgs = mgr ? await readDaemonServeArgs(mgr) : null;
|
|
1055
1131
|
const installed = daemonArgs !== null;
|
|
1056
1132
|
const a = daemonArgs ?? [];
|
|
1057
|
-
const
|
|
1133
|
+
const local = argsUsePortless(a);
|
|
1134
|
+
const port = local ? await portlessAppPort() : portFromArgs(a);
|
|
1135
|
+
const localUrl = local ? await resolvedPortlessConsoleUrl(token ?? undefined) : null;
|
|
1058
1136
|
// Mirror cmdServe/install mode resolution: webrtc when --webrtc/--share; http
|
|
1059
1137
|
// when --http/--share OR no --webrtc at all (http is the implicit default).
|
|
1060
1138
|
const webrtcish = a.some((x) => x.startsWith("--webrtc") || x.startsWith("--share"));
|
|
@@ -1076,6 +1154,7 @@ async function cmdServeStatus(args: string[]): Promise<number> {
|
|
|
1076
1154
|
installed,
|
|
1077
1155
|
mode,
|
|
1078
1156
|
port: httpish ? port : null,
|
|
1157
|
+
localUrl: httpish ? localUrl : null,
|
|
1079
1158
|
reachable: runningVersion !== null,
|
|
1080
1159
|
runningVersion,
|
|
1081
1160
|
currentVersion: current,
|
|
@@ -1096,25 +1175,32 @@ async function cmdServeStatus(args: string[]): Promise<number> {
|
|
|
1096
1175
|
w(`manager: ${mgr ? mgr.id : "none — install pm2 or oxmgr to daemonize"}`);
|
|
1097
1176
|
if (installed) {
|
|
1098
1177
|
w(`installed: yes (via ${mgr!.id})`);
|
|
1099
|
-
w(`mode: ${mode}${httpish ? ` (port ${port})` : ""}`);
|
|
1178
|
+
w(`mode: ${mode}${httpish ? (local ? ` (${localUrl})` : ` (port ${port})`) : ""}`);
|
|
1100
1179
|
if (a.length) w(`args: ${a.join(" ")}`);
|
|
1101
1180
|
} else {
|
|
1102
1181
|
w(`installed: no — start a daemon with: ay serve install [--share]`);
|
|
1103
1182
|
}
|
|
1104
1183
|
if (runningVersion !== null) {
|
|
1105
1184
|
const tag = runningVersion === current ? "up to date" : `outdated (current v${current})`;
|
|
1106
|
-
w(
|
|
1185
|
+
w(
|
|
1186
|
+
`http api: reachable ${local ? `via ${localUrl}` : `on 127.0.0.1:${port}`} — v${runningVersion} (${tag})`,
|
|
1187
|
+
);
|
|
1107
1188
|
} else if (mode === "webrtc") {
|
|
1108
1189
|
w(`http api: none (webrtc-only)`);
|
|
1109
1190
|
} else {
|
|
1110
|
-
w(
|
|
1191
|
+
w(
|
|
1192
|
+
`http api: not reachable ${local ? `via ${localUrl}` : `on 127.0.0.1:${port}`} (not running)`,
|
|
1193
|
+
);
|
|
1111
1194
|
}
|
|
1112
1195
|
w(`token: ${token ?? "(none yet — created on first serve)"}`);
|
|
1113
1196
|
if (shareLink) w(`share link: ${shareLink}`);
|
|
1114
1197
|
if (token && httpish) {
|
|
1115
1198
|
w();
|
|
1116
|
-
w(`
|
|
1117
|
-
|
|
1199
|
+
if (local) w(`console: ${localUrl}`);
|
|
1200
|
+
else if (port !== null) {
|
|
1201
|
+
w(`connect: ay ls ${token}@<host>:${port}`);
|
|
1202
|
+
w(` ay remote add <alias> http://${token}@<host>:${port}`);
|
|
1203
|
+
}
|
|
1118
1204
|
}
|
|
1119
1205
|
return 0;
|
|
1120
1206
|
}
|
|
@@ -1138,11 +1224,13 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1138
1224
|
` (stable link across restarts; delete the file to rotate).\n` +
|
|
1139
1225
|
` --share [URL] Legacy alias for --http --webrtc\n\n` +
|
|
1140
1226
|
`Options:\n` +
|
|
1141
|
-
` --port N
|
|
1227
|
+
` --port N Bypass Portless and listen on a fixed HTTP port\n` +
|
|
1142
1228
|
` --host HOST Interface to bind (default: 127.0.0.1; use 0.0.0.0 to expose)\n` +
|
|
1143
1229
|
` --token TOKEN Auth token (auto-generated and saved if omitted)\n` +
|
|
1144
1230
|
` -d, --daemon Install these flags as a background daemon (pm2/oxmgr)\n` +
|
|
1145
1231
|
` (same as: ay serve install <flags>)\n` +
|
|
1232
|
+
` --local Explicitly use Portless (the default for HTTP mode)\n` +
|
|
1233
|
+
` ${portlessConsoleUrl()}\n` +
|
|
1146
1234
|
` --allow-spawn Deprecated no-op — the console can always spawn agents\n` +
|
|
1147
1235
|
` --tls-cert FILE TLS certificate PEM\n` +
|
|
1148
1236
|
` --tls-key FILE TLS private key PEM\n\n` +
|
|
@@ -1152,8 +1240,8 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1152
1240
|
` ay serve uninstall remove daemon\n` +
|
|
1153
1241
|
` ay serve logs view daemon logs\n\n` +
|
|
1154
1242
|
`Once running, connect from another machine:\n` +
|
|
1155
|
-
` ay ls <token>@<host
|
|
1156
|
-
` ay remote add <alias> http://<token>@<host
|
|
1243
|
+
` ay ls <token>@<host>:<port>\n` +
|
|
1244
|
+
` ay remote add <alias> http://<token>@<host>:<port>\n`,
|
|
1157
1245
|
);
|
|
1158
1246
|
return 0;
|
|
1159
1247
|
}
|
|
@@ -1189,7 +1277,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1189
1277
|
|
|
1190
1278
|
const y = yargs(rest)
|
|
1191
1279
|
.usage("Usage: ay serve [options]")
|
|
1192
|
-
.option("port", { type: "number",
|
|
1280
|
+
.option("port", { type: "number", description: "Bypass Portless and listen on a fixed port" })
|
|
1193
1281
|
.option("host", {
|
|
1194
1282
|
type: "string",
|
|
1195
1283
|
default: "127.0.0.1",
|
|
@@ -1217,6 +1305,11 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1217
1305
|
default: false,
|
|
1218
1306
|
description: "Install as a background daemon (same as: ay serve install <flags>)",
|
|
1219
1307
|
})
|
|
1308
|
+
.option("local", {
|
|
1309
|
+
type: "boolean",
|
|
1310
|
+
default: false,
|
|
1311
|
+
description: `Use Portless at ${portlessConsoleUrl()} (default for HTTP mode)`,
|
|
1312
|
+
})
|
|
1220
1313
|
.option("allow-spawn", {
|
|
1221
1314
|
type: "boolean",
|
|
1222
1315
|
default: false,
|
|
@@ -1225,7 +1318,8 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1225
1318
|
.option("takeover", {
|
|
1226
1319
|
type: "boolean",
|
|
1227
1320
|
default: false,
|
|
1228
|
-
description:
|
|
1321
|
+
description:
|
|
1322
|
+
"If another ay serve already hosts this home's WebRTC room, stop it and take the room",
|
|
1229
1323
|
})
|
|
1230
1324
|
.help(false)
|
|
1231
1325
|
.version(false)
|
|
@@ -1240,6 +1334,18 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1240
1334
|
return cmdServeDaemon("install", fwd);
|
|
1241
1335
|
}
|
|
1242
1336
|
|
|
1337
|
+
const wantWebrtc = argv.webrtc !== undefined || argv.share !== undefined;
|
|
1338
|
+
const wantHttp = argv.http === true || argv.share !== undefined || argv.webrtc === undefined;
|
|
1339
|
+
const fixedPort = typeof argv.port === "number";
|
|
1340
|
+
const usePortless = wantHttp && !fixedPort;
|
|
1341
|
+
if (argv.local === true && fixedPort) {
|
|
1342
|
+
process.stderr.write("ay serve: --local and --port cannot be used together\n");
|
|
1343
|
+
return 1;
|
|
1344
|
+
}
|
|
1345
|
+
if (usePortless && process.env[PORTLESS_CHILD_ENV] !== "1" && process.env.PORTLESS !== "0") {
|
|
1346
|
+
return runWithPortless(rest);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1243
1349
|
// `ay serve` takes only flags (plus the install/uninstall/logs subcommands
|
|
1244
1350
|
// handled above). A bare word like `ay serve share` is silently dropped into
|
|
1245
1351
|
// argv._ by yargs and would otherwise start in the wrong mode — most often
|
|
@@ -1260,7 +1366,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1260
1366
|
// top-level agents regardless of the spawn path.
|
|
1261
1367
|
delete process.env.AGENT_YES_PID;
|
|
1262
1368
|
|
|
1263
|
-
const port = (argv.port as number) ?? DEFAULT_PORT;
|
|
1369
|
+
const port = (argv.port as number | undefined) ?? (Number(process.env.PORT) || DEFAULT_PORT);
|
|
1264
1370
|
const host = (argv.host as string) ?? "127.0.0.1";
|
|
1265
1371
|
const tokenFlag = typeof argv.token === "string" ? argv.token : undefined;
|
|
1266
1372
|
const certPath = typeof argv["tls-cert"] === "string" ? argv["tls-cert"] : undefined;
|
|
@@ -1276,8 +1382,6 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1276
1382
|
// Modes: --http (HTTP listener + web console), --webrtc (port-free WebRTC
|
|
1277
1383
|
// share), or both. Bare `ay serve` stays HTTP-only; --share keeps its old
|
|
1278
1384
|
// meaning (HTTP + WebRTC) for existing invocations.
|
|
1279
|
-
const wantWebrtc = argv.webrtc !== undefined || argv.share !== undefined;
|
|
1280
|
-
const wantHttp = argv.http === true || argv.share !== undefined || argv.webrtc === undefined;
|
|
1281
1385
|
|
|
1282
1386
|
// Singleton WebRTC host per ~/.agent-yes: a second host joining the SAME
|
|
1283
1387
|
// persisted room (~/.agent-yes/.share-room) fights the first over every
|
|
@@ -1435,6 +1539,28 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1435
1539
|
}
|
|
1436
1540
|
};
|
|
1437
1541
|
|
|
1542
|
+
// Per-agent live status description from the rendered screen. Claude Code
|
|
1543
|
+
// paints a spinner/status line while working; surfacing it lets the left panel
|
|
1544
|
+
// show "what it's doing" without opening the terminal.
|
|
1545
|
+
const statusTextCache = new Map<
|
|
1546
|
+
string,
|
|
1547
|
+
{ size: number; mtimeMs: number; statusText: string | null }
|
|
1548
|
+
>();
|
|
1549
|
+
const logStatusText = async (logFile: string | null | undefined): Promise<string | null> => {
|
|
1550
|
+
if (!logFile) return null;
|
|
1551
|
+
try {
|
|
1552
|
+
const { size, mtimeMs } = await stat(logFile);
|
|
1553
|
+
const hit = statusTextCache.get(logFile);
|
|
1554
|
+
if (hit && hit.size === size && hit.mtimeMs === mtimeMs) return hit.statusText;
|
|
1555
|
+
const lines = await renderLogTailLines(logFile, 40);
|
|
1556
|
+
const statusText = lines ? parseStatusText(lines) : null;
|
|
1557
|
+
statusTextCache.set(logFile, { size, mtimeMs, statusText });
|
|
1558
|
+
return statusText;
|
|
1559
|
+
} catch {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1438
1564
|
// Per-agent "waiting on you" detection: the agent is parked on an interactive
|
|
1439
1565
|
// menu it did NOT auto-resolve (config `needsInput` patterns). Same source and
|
|
1440
1566
|
// classifier as `ay ls` / `ay status`, so the console's dot matches the CLI's
|
|
@@ -1701,6 +1827,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1701
1827
|
// WHAT the agent is waiting on. Null otherwise.
|
|
1702
1828
|
question,
|
|
1703
1829
|
title: await logTitle(r.log_file),
|
|
1830
|
+
status_text: status === "exited" ? null : await logStatusText(r.log_file),
|
|
1704
1831
|
git: status === "exited" ? null : await gitStatus(r.cwd),
|
|
1705
1832
|
// Task progress from the rendered todo block (null when none detected → no
|
|
1706
1833
|
// badge). Skipped for exited agents — their screen is no longer live.
|
|
@@ -3208,17 +3335,20 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
3208
3335
|
// server is null only when we degraded to WebRTC-only above (port wedged);
|
|
3209
3336
|
// skip the HTTP connection banner since nothing is listening on the port.
|
|
3210
3337
|
if (server) {
|
|
3338
|
+
const listenPort = server.port;
|
|
3211
3339
|
const uiHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
|
|
3212
|
-
process.stdout.write(`ay serve ${scheme}://${host}:${
|
|
3340
|
+
process.stdout.write(`ay serve ${scheme}://${host}:${listenPort}\n`);
|
|
3213
3341
|
process.stdout.write(`token: ${token}\n\n`);
|
|
3214
3342
|
process.stdout.write(`web console (token in the # is eaten on open):\n`);
|
|
3215
|
-
process.stdout.write(
|
|
3343
|
+
process.stdout.write(
|
|
3344
|
+
` ${usePortless ? portlessConsoleUrl(token) : `${scheme}://${uiHost}:${listenPort}/#k=${token}`}\n\n`,
|
|
3345
|
+
);
|
|
3216
3346
|
process.stdout.write(`connect from another machine:\n`);
|
|
3217
|
-
process.stdout.write(` ay ls ${token}@<host>:${
|
|
3218
|
-
process.stdout.write(` ay tail ${token}@<host>:${
|
|
3219
|
-
process.stdout.write(` ay send ${token}@<host>:${
|
|
3347
|
+
process.stdout.write(` ay ls ${token}@<host>:${listenPort}\n`);
|
|
3348
|
+
process.stdout.write(` ay tail ${token}@<host>:${listenPort}:<keyword>\n`);
|
|
3349
|
+
process.stdout.write(` ay send ${token}@<host>:${listenPort}:<keyword> "message"\n\n`);
|
|
3220
3350
|
process.stdout.write(`save as alias:\n`);
|
|
3221
|
-
process.stdout.write(` ay remote add <alias> ${scheme}://${token}@<host>:${
|
|
3351
|
+
process.stdout.write(` ay remote add <alias> ${scheme}://${token}@<host>:${listenPort}\n\n`);
|
|
3222
3352
|
if (!useHttps) {
|
|
3223
3353
|
process.stdout.write(
|
|
3224
3354
|
`for HTTPS: ay serve --tls-cert cert.pem --tls-key key.pem\n` +
|
package/ts/share.ts
CHANGED
|
@@ -66,10 +66,28 @@ const MAX_PEER_JOIN_QUEUE = 50;
|
|
|
66
66
|
const IDLE_RESTART_UPTIME_MS = 25 * 60_000;
|
|
67
67
|
const HARD_RESTART_UPTIME_MS = 45 * 60_000;
|
|
68
68
|
const IDLE_RESTART_CHECK_MS = 60_000;
|
|
69
|
+
const PERF_SLOW_MS = Number(process.env.AGENT_YES_WEBRTC_PERF_SLOW_MS) || 750;
|
|
70
|
+
const PERF_BUFFERED_BYTES = Number(process.env.AGENT_YES_WEBRTC_PERF_BUFFERED_BYTES) || 1_000_000;
|
|
71
|
+
const PERF_VERBOSE = process.env.AGENT_YES_WEBRTC_PERF === "1";
|
|
69
72
|
|
|
70
73
|
type IceServer = { urls: string | string[]; username?: string; credential?: string };
|
|
71
74
|
const STUN: IceServer[] = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
72
75
|
|
|
76
|
+
function perfLog(event: string, data: Record<string, unknown>, force = false) {
|
|
77
|
+
if (!force && !PERF_VERBOSE) return;
|
|
78
|
+
process.stderr.write(`[share:perf] ${JSON.stringify({ t: Date.now(), event, ...data })}\n`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function maybeSlow(event: string, startedAt: number, data: Record<string, unknown> = {}) {
|
|
82
|
+
const ms = Math.round(performance.now() - startedAt);
|
|
83
|
+
perfLog(event, { ms, ...data }, ms >= PERF_SLOW_MS);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function dcBufferedAmount(dc: any): number {
|
|
87
|
+
const n = Number(dc?.bufferedAmount ?? 0);
|
|
88
|
+
return Number.isFinite(n) ? n : 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
// Short-lived Cloudflare TURN credentials, minted from a long-term TURN key, so
|
|
74
92
|
// browsers can RELAY when a direct P2P path is impossible (symmetric NAT /
|
|
75
93
|
// CGNAT — the main cause of "rooms offline"). Set CF_TURN_KEY_ID +
|
|
@@ -428,6 +446,7 @@ export async function startShare(
|
|
|
428
446
|
if (closed) return; // a reconnect timer queued before close() must not revive it
|
|
429
447
|
const ws = new WebSocket(`${wsScheme}://${host}/${room}`, [SUB]);
|
|
430
448
|
currentWs = ws;
|
|
449
|
+
const signalStartedAt = performance.now();
|
|
431
450
|
let ready = false;
|
|
432
451
|
let lastRecv = Date.now();
|
|
433
452
|
let hb: ReturnType<typeof setInterval> | undefined;
|
|
@@ -443,6 +462,7 @@ export async function startShare(
|
|
|
443
462
|
}
|
|
444
463
|
};
|
|
445
464
|
ws.onopen = () => {
|
|
465
|
+
maybeSlow("signal.open", signalStartedAt, { room, host });
|
|
446
466
|
ws.send(JSON.stringify({ type: "hello", role: "host", v: 2, token: authToken }));
|
|
447
467
|
ready = true;
|
|
448
468
|
lastRecv = Date.now();
|
|
@@ -480,6 +500,12 @@ export async function startShare(
|
|
|
480
500
|
const m = JSON.parse(ev.data as string);
|
|
481
501
|
if (m.type === "pong") return; // heartbeat ack — liveness already recorded
|
|
482
502
|
if (m.type === "peer-join") {
|
|
503
|
+
perfLog("peer.join", {
|
|
504
|
+
room,
|
|
505
|
+
peer: String(m.peer),
|
|
506
|
+
queue: peerJoinQueue.length,
|
|
507
|
+
peers: peers.size,
|
|
508
|
+
});
|
|
483
509
|
// Serialized in drainPeerJoins() to avoid a storm. Skip dupes (already
|
|
484
510
|
// queued or already an active peer) and cap the queue so a pathological
|
|
485
511
|
// burst can't grow it unboundedly — dropped joins retry via the browser.
|
|
@@ -554,6 +580,7 @@ export async function startShare(
|
|
|
554
580
|
};
|
|
555
581
|
|
|
556
582
|
async function startPeer(ws: WebSocket, peerId: string) {
|
|
583
|
+
const startedAt = performance.now();
|
|
557
584
|
const iceServers = await getIceServers();
|
|
558
585
|
const pc = new RTCPeerConnection({ iceServers });
|
|
559
586
|
let resolveKeys!: () => void;
|
|
@@ -584,6 +611,11 @@ export async function startShare(
|
|
|
584
611
|
dc.binaryType = "arraybuffer";
|
|
585
612
|
dc.onopen = async () => {
|
|
586
613
|
try {
|
|
614
|
+
maybeSlow("peer.dc_open", startedAt, {
|
|
615
|
+
room,
|
|
616
|
+
peer: peerId,
|
|
617
|
+
buffered: dcBufferedAmount(dc),
|
|
618
|
+
});
|
|
587
619
|
await peer.keysReady; // keys derived in the answer handler
|
|
588
620
|
// Open the mandatory bidirectional key-confirmation handshake. Nothing
|
|
589
621
|
// the peer sends is acted on until BOTH directions confirm (see onFrame).
|
|
@@ -624,6 +656,7 @@ export async function startShare(
|
|
|
624
656
|
ws.send(
|
|
625
657
|
JSON.stringify({ type: "offer", to: peerId, sdp: pc.localDescription.sdp, iceServers }),
|
|
626
658
|
);
|
|
659
|
+
maybeSlow("peer.offer", startedAt, { room, peer: peerId, iceServers: iceServers.length });
|
|
627
660
|
}
|
|
628
661
|
|
|
629
662
|
function closePeer(peerId: string) {
|
|
@@ -642,8 +675,10 @@ export async function startShare(
|
|
|
642
675
|
// Seal an envelope and send it, serialized per peer so the wire order matches
|
|
643
676
|
// the nonce-counter order (so the receiver's monotonic check never trips).
|
|
644
677
|
function enqueueSeal(peerId: string, dc: any, peer: Peer, flags: number, obj: object) {
|
|
678
|
+
const queuedAt = performance.now();
|
|
645
679
|
peer.sendChain = peer.sendChain.then(async () => {
|
|
646
680
|
if (dc.readyState !== "open" || !peer.keyH2C || !peer.th) return;
|
|
681
|
+
const queuedMs = Math.round(performance.now() - queuedAt);
|
|
647
682
|
let frame: ArrayBuffer;
|
|
648
683
|
try {
|
|
649
684
|
frame = await e2eSeal(peer.keyH2C, peer.send, flags, peer.th, packEnvelope(obj));
|
|
@@ -653,6 +688,19 @@ export async function startShare(
|
|
|
653
688
|
}
|
|
654
689
|
try {
|
|
655
690
|
dc.send(frame);
|
|
691
|
+
const buffered = dcBufferedAmount(dc);
|
|
692
|
+
perfLog(
|
|
693
|
+
"send",
|
|
694
|
+
{
|
|
695
|
+
room,
|
|
696
|
+
peer: peerId,
|
|
697
|
+
kind: (obj as any)?.t,
|
|
698
|
+
queuedMs,
|
|
699
|
+
buffered,
|
|
700
|
+
bytes: frame.byteLength,
|
|
701
|
+
},
|
|
702
|
+
queuedMs >= PERF_SLOW_MS || buffered >= PERF_BUFFERED_BYTES,
|
|
703
|
+
);
|
|
656
704
|
} catch {
|
|
657
705
|
/* peer vanished mid-send; dropping the frame is correct */
|
|
658
706
|
}
|
|
@@ -703,6 +751,8 @@ export async function startShare(
|
|
|
703
751
|
}
|
|
704
752
|
if (req.t !== "req") return;
|
|
705
753
|
const { id, method, path: p, body } = req;
|
|
754
|
+
const startedAt = performance.now();
|
|
755
|
+
perfLog("req.start", { room, peer: peerId, id, method, path: p });
|
|
706
756
|
const ac = new AbortController();
|
|
707
757
|
peer.aborts.set(id, ac);
|
|
708
758
|
try {
|
|
@@ -718,6 +768,14 @@ export async function startShare(
|
|
|
718
768
|
signal: ac.signal,
|
|
719
769
|
}),
|
|
720
770
|
);
|
|
771
|
+
maybeSlow("req.head", startedAt, {
|
|
772
|
+
room,
|
|
773
|
+
peer: peerId,
|
|
774
|
+
id,
|
|
775
|
+
method,
|
|
776
|
+
path: p,
|
|
777
|
+
status: res.status,
|
|
778
|
+
});
|
|
721
779
|
enqueueSeal(peerId, dc, peer, 0, {
|
|
722
780
|
t: "res",
|
|
723
781
|
id,
|
|
@@ -727,9 +785,11 @@ export async function startShare(
|
|
|
727
785
|
const reader = res.body!.getReader();
|
|
728
786
|
const dec = new TextDecoder();
|
|
729
787
|
let seq = 0;
|
|
788
|
+
let bytes = 0;
|
|
730
789
|
for (;;) {
|
|
731
790
|
const { done, value } = await reader.read();
|
|
732
791
|
if (done) break;
|
|
792
|
+
bytes += value.byteLength;
|
|
733
793
|
const text = dec.decode(value, { stream: true });
|
|
734
794
|
// Slice on UTF-16 boundaries: JSON round-trips lone surrogates as \uXXXX,
|
|
735
795
|
// so the receiver reassembles the exact text by concatenating in seq order.
|
|
@@ -742,13 +802,32 @@ export async function startShare(
|
|
|
742
802
|
});
|
|
743
803
|
}
|
|
744
804
|
enqueueSeal(peerId, dc, peer, 0, { t: "end", id, seq });
|
|
805
|
+
maybeSlow("req.end", startedAt, {
|
|
806
|
+
room,
|
|
807
|
+
peer: peerId,
|
|
808
|
+
id,
|
|
809
|
+
method,
|
|
810
|
+
path: p,
|
|
811
|
+
status: res.status,
|
|
812
|
+
chunks: seq,
|
|
813
|
+
bytes,
|
|
814
|
+
});
|
|
745
815
|
} catch (e) {
|
|
746
|
-
if ((e as Error).name !== "AbortError")
|
|
816
|
+
if ((e as Error).name !== "AbortError") {
|
|
817
|
+
maybeSlow("req.error", startedAt, {
|
|
818
|
+
room,
|
|
819
|
+
peer: peerId,
|
|
820
|
+
id,
|
|
821
|
+
method,
|
|
822
|
+
path: p,
|
|
823
|
+
error: String((e as Error).message ?? e),
|
|
824
|
+
});
|
|
747
825
|
enqueueSeal(peerId, dc, peer, 0, {
|
|
748
826
|
t: "end",
|
|
749
827
|
id,
|
|
750
828
|
error: String((e as Error).message ?? e),
|
|
751
829
|
});
|
|
830
|
+
}
|
|
752
831
|
} finally {
|
|
753
832
|
peer.aborts.delete(id);
|
|
754
833
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseStatusText } from "./statusText.ts";
|
|
3
|
+
|
|
4
|
+
describe("parseStatusText", () => {
|
|
5
|
+
it("returns the latest Claude spinner/status line", () => {
|
|
6
|
+
expect(
|
|
7
|
+
parseStatusText([
|
|
8
|
+
"older output",
|
|
9
|
+
"✶ Verifying calendar meetings with real data… (6m 30s · ↓ 19.5k tokens)",
|
|
10
|
+
]),
|
|
11
|
+
).toBe("✶ Verifying calendar meetings with real data… (6m 30s · ↓ 19.5k tokens)");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("ignores non-status terminal prose", () => {
|
|
15
|
+
expect(parseStatusText(["hello", "Done", ""])).toBe(null);
|
|
16
|
+
});
|
|
17
|
+
});
|
package/ts/statusText.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a short "what is the agent doing right now?" line from the rendered
|
|
3
|
+
* terminal screen. Claude Code paints this as a spinner/status line, e.g.
|
|
4
|
+
* "✶ Verifying calendar meetings with real data… (6m 30s · ↓ 19.5k tokens)".
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const SPINNER_PREFIX = /^[\u2800-\u28ff✶✻✢✳✽✦✧✩✷✸✹✺✼·•●◐◓◒◑]\s+/u;
|
|
8
|
+
const CONTROL = /[\x00-\x1f\x7f-\x9f]/g;
|
|
9
|
+
|
|
10
|
+
export function parseStatusText(lines: string[]): string | null {
|
|
11
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
12
|
+
const line = lines[i]?.replace(CONTROL, "").trim();
|
|
13
|
+
if (!line || line.length < 3) continue;
|
|
14
|
+
if (!SPINNER_PREFIX.test(line)) continue;
|
|
15
|
+
if (/^(?:[•·]\s*)?(?:esc|ctrl|enter|return|shift|tab)\b/i.test(line)) continue;
|
|
16
|
+
return line.slice(0, 220).trim();
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import "./ts-Dm6jjLwC.js";
|
|
2
|
-
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-BomfUpSo.js";
|
|
4
|
-
import "./pidStore-BIvsBQ8X.js";
|
|
5
|
-
import "./globalPidIndex-CoNr7tS8.js";
|
|
6
|
-
import "./messageLog-CxrKJj77.js";
|
|
7
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C8-qh98a.js";
|
|
8
|
-
|
|
9
|
-
export { SUPPORTED_CLIS };
|