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/dist/SUPPORTED_CLIS-CNO_pj9f.js +8 -0
- package/dist/SUPPORTED_CLIS-eD-UlqO_.js +8 -0
- package/dist/cli.js +6 -6
- package/dist/index.js +3 -3
- package/dist/pidStore-9b3YTuf4.js +5 -0
- package/dist/{pidStore-DTzl6zeh.js → pidStore-DBjlqzo8.js} +22 -22
- package/dist/{remotes-Bjp2GYPz.js → remotes-C3xPRtfg.js} +1 -1
- package/dist/{remotes-oNI1fR7_.js → remotes-C9WMt5PY.js} +1 -1
- package/dist/{runningLock-C22d9SRJ.js → runningLock-CJxsoGdb.js} +1 -1
- package/dist/{serve-CixOwENN.js → serve-CKcbVPy6.js} +155 -19
- package/dist/share-DUhUA1Pi.js +184 -0
- package/dist/{subcommands-DjrOWqD9.js → subcommands-B4gXEu5I.js} +2 -2
- package/dist/{subcommands-D9wmaZ3U.js → subcommands-K242usI5.js} +4 -4
- package/dist/{tray-DHuD0nEk.js → tray-CWQe9DMY.js} +2 -2
- package/dist/{ts-BvWaEGsr.js → ts-BAc4Jcrw.js} +13 -4
- package/dist/{versionChecker-DfIPG9ui.js → versionChecker-MNvA73o9.js} +2 -2
- package/package.json +6 -2
- package/scripts/cf.ts +51 -0
- package/ts/index.ts +16 -0
- package/ts/rustBinary.ts +10 -35
- package/ts/serve.ts +184 -19
- package/ts/share.ts +190 -0
- package/dist/SUPPORTED_CLIS-CrlcmUcE.js +0 -12
- package/dist/pidStore-iJY3JFTn.js +0 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
3
|
-
import {
|
|
4
|
-
import { t as
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-MNvA73o9.js";
|
|
3
|
+
import { n as agentYesHome, t as PidStore } from "./pidStore-DBjlqzo8.js";
|
|
4
|
+
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-CJxsoGdb.js";
|
|
5
5
|
import { i as readGlobalPids } from "./globalPidIndex-yVd3mbsV.js";
|
|
6
6
|
import { arch, platform } from "process";
|
|
7
7
|
import { execSync } from "child_process";
|
|
@@ -1423,10 +1423,19 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
|
|
|
1423
1423
|
notifyWebhook("EXIT", `${exitReason} exitCode=${exitCode ?? "?"}`, workingDir).catch(() => null);
|
|
1424
1424
|
return pendingExitCode.resolve(exitCode);
|
|
1425
1425
|
});
|
|
1426
|
+
const writeCurrentPtysize = (cols, rows) => {
|
|
1427
|
+
const dir = path.join(agentYesHome(), "ptysize");
|
|
1428
|
+
mkdir(dir, { recursive: true }).then(() => writeFile(path.join(dir, String(process.pid)), `${cols} ${rows}\n`)).catch(() => null);
|
|
1429
|
+
};
|
|
1430
|
+
{
|
|
1431
|
+
const { cols, rows } = getTerminalDimensions();
|
|
1432
|
+
writeCurrentPtysize(cols, rows);
|
|
1433
|
+
}
|
|
1426
1434
|
process.stdout.on("resize", () => {
|
|
1427
1435
|
const { cols, rows } = getTerminalDimensions();
|
|
1428
1436
|
shell.resize(cols, rows);
|
|
1429
1437
|
xtermProxy.resize(cols, rows);
|
|
1438
|
+
writeCurrentPtysize(cols, rows);
|
|
1430
1439
|
});
|
|
1431
1440
|
const isStillWorkingQ = () => {
|
|
1432
1441
|
const rendered = xtermProxy.tail(24).replace(/\s+/g, " ");
|
|
@@ -1705,4 +1714,4 @@ function sleep(ms) {
|
|
|
1705
1714
|
|
|
1706
1715
|
//#endregion
|
|
1707
1716
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1708
|
-
//# sourceMappingURL=ts-
|
|
1717
|
+
//# sourceMappingURL=ts-BAc4Jcrw.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.97.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -221,4 +221,4 @@ async function displayVersion() {
|
|
|
221
221
|
|
|
222
222
|
//#endregion
|
|
223
223
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
224
|
-
//# sourceMappingURL=versionChecker-
|
|
224
|
+
//# sourceMappingURL=versionChecker-MNvA73o9.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-yes",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.97.0",
|
|
4
4
|
"description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsdown",
|
|
72
|
+
"cf": "bun scripts/cf.ts",
|
|
72
73
|
"build:rs": "cargo install --path rs --features swarm",
|
|
73
74
|
"postbuild": "bun ./ts/postbuild.ts",
|
|
74
75
|
"demo": "bun run build && bun link && claude-yes -- demo",
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
"execa": "^9.6.1",
|
|
93
94
|
"from-node-stream": "^0.2.0",
|
|
94
95
|
"ms": "^2.1.3",
|
|
96
|
+
"node-datachannel": "^0.32.3",
|
|
95
97
|
"phpdie": "^1.7.0",
|
|
96
98
|
"proper-lockfile": "^4.1.2",
|
|
97
99
|
"sflow": "^1.27.0",
|
|
@@ -142,5 +144,7 @@
|
|
|
142
144
|
"engines": {
|
|
143
145
|
"node": ">=22.0.0"
|
|
144
146
|
},
|
|
145
|
-
"trustedDependencies": [
|
|
147
|
+
"trustedDependencies": [
|
|
148
|
+
"node-datachannel"
|
|
149
|
+
]
|
|
146
150
|
}
|
package/scripts/cf.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Thin wrapper that runs `wrangler` against the SNOLAB Cloudflare account using
|
|
3
|
+
// the API token saved in .env.local — so we never depend on `wrangler login`
|
|
4
|
+
// state (which points at a different account) and never pass the token on the
|
|
5
|
+
// CLI. Usage: bun scripts/cf.ts <wrangler args...>
|
|
6
|
+
// e.g. bun scripts/cf.ts whoami
|
|
7
|
+
// bun scripts/cf.ts pages deploy ./dist --project-name agent-yes
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
// SNOLAB account — agent-yes.com lives here. Account id is not a secret.
|
|
14
|
+
const SNOLAB_ACCOUNT_ID = "0beef4cd2d2da6befa47d8d149d6e157";
|
|
15
|
+
|
|
16
|
+
const root = path.join(import.meta.dir, "..");
|
|
17
|
+
const env: Record<string, string | undefined> = { ...process.env };
|
|
18
|
+
|
|
19
|
+
// Load .env.local (bun also auto-loads it, but be explicit so this works from
|
|
20
|
+
// any cwd and is obvious to a reader).
|
|
21
|
+
try {
|
|
22
|
+
for (const line of readFileSync(path.join(root, ".env.local"), "utf8").split("\n")) {
|
|
23
|
+
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*?)\s*$/);
|
|
24
|
+
if (m && !(m[1] in env)) env[m[1]] = m[2];
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
/* no .env.local — fall through to the check below */
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!env.CLOUDFLARE_API_TOKEN) {
|
|
31
|
+
console.error("CLOUDFLARE_API_TOKEN is missing — add it to .env.local (see scripts/cf.ts).");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
env.CLOUDFLARE_ACCOUNT_ID = SNOLAB_ACCOUNT_ID;
|
|
35
|
+
|
|
36
|
+
// wrangler otherwise prefers a stored OAuth login over CLOUDFLARE_API_TOKEN and
|
|
37
|
+
// pins the OAuth account (Axon), ignoring CLOUDFLARE_ACCOUNT_ID. Two sources to
|
|
38
|
+
// neutralise: the global OAuth config (~/.wrangler) and a project-level account
|
|
39
|
+
// cache (.wrangler/wrangler-account.json) that pins whatever account first
|
|
40
|
+
// deployed. Drop the cache, move the OAuth config aside for the run, restore it.
|
|
41
|
+
rmSync(path.join(root, ".wrangler/wrangler-account.json"), { force: true });
|
|
42
|
+
const oauthCfg = path.join(homedir(), ".wrangler/config/default.toml");
|
|
43
|
+
const oauthBak = oauthCfg + ".cf-bak";
|
|
44
|
+
const hadOauth = existsSync(oauthCfg);
|
|
45
|
+
if (hadOauth) renameSync(oauthCfg, oauthBak);
|
|
46
|
+
try {
|
|
47
|
+
const r = spawnSync("bunx", ["wrangler", ...process.argv.slice(2)], { stdio: "inherit", env });
|
|
48
|
+
process.exitCode = r.status ?? 1;
|
|
49
|
+
} finally {
|
|
50
|
+
if (hadOauth && existsSync(oauthBak)) renameSync(oauthBak, oauthCfg);
|
|
51
|
+
}
|
package/ts/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import path from "path";
|
|
|
5
5
|
import DIE from "phpdie";
|
|
6
6
|
import sflow from "sflow";
|
|
7
7
|
import { XtermProxy } from "./xterm-proxy.ts";
|
|
8
|
+
import { agentYesHome } from "./agentYesHome.ts";
|
|
8
9
|
import {
|
|
9
10
|
extractSessionId,
|
|
10
11
|
getSessionForCwd,
|
|
@@ -643,11 +644,26 @@ export default async function agentYes({
|
|
|
643
644
|
return pendingExitCode.resolve(exitCode);
|
|
644
645
|
});
|
|
645
646
|
|
|
647
|
+
// Record the agent's current PTY size to ~/.agent-yes/ptysize/<pid> so `ay serve`
|
|
648
|
+
// / the web console can render the existing buffer at the agent's real width
|
|
649
|
+
// before adapting. Mirrors the Rust runtime (rs/src/pty_spawner.rs).
|
|
650
|
+
const writeCurrentPtysize = (cols: number, rows: number) => {
|
|
651
|
+
const dir = path.join(agentYesHome(), "ptysize");
|
|
652
|
+
void mkdir(dir, { recursive: true })
|
|
653
|
+
.then(() => writeFile(path.join(dir, String(process.pid)), `${cols} ${rows}\n`))
|
|
654
|
+
.catch(() => null);
|
|
655
|
+
};
|
|
656
|
+
{
|
|
657
|
+
const { cols, rows } = getTerminalDimensions();
|
|
658
|
+
writeCurrentPtysize(cols, rows);
|
|
659
|
+
}
|
|
660
|
+
|
|
646
661
|
// when current tty resized, resize both pty and xterm proxy
|
|
647
662
|
process.stdout.on("resize", () => {
|
|
648
663
|
const { cols, rows } = getTerminalDimensions();
|
|
649
664
|
shell.resize(cols, rows);
|
|
650
665
|
xtermProxy.resize(cols, rows);
|
|
666
|
+
writeCurrentPtysize(cols, rows);
|
|
651
667
|
});
|
|
652
668
|
|
|
653
669
|
const isStillWorkingQ = () => {
|
package/ts/rustBinary.ts
CHANGED
|
@@ -41,10 +41,7 @@ export function getBinaryName(): string {
|
|
|
41
41
|
*/
|
|
42
42
|
export function getBinDir(): string {
|
|
43
43
|
// First check for binaries in the npm package
|
|
44
|
-
const packageBinDir = path.resolve(
|
|
45
|
-
import.meta.dirname ?? import.meta.dir,
|
|
46
|
-
"../bin",
|
|
47
|
-
);
|
|
44
|
+
const packageBinDir = path.resolve(import.meta.dirname ?? import.meta.dir, "../bin");
|
|
48
45
|
if (existsSync(packageBinDir)) {
|
|
49
46
|
return packageBinDir;
|
|
50
47
|
}
|
|
@@ -61,8 +58,7 @@ export function getBinDir(): string {
|
|
|
61
58
|
const cacheDir =
|
|
62
59
|
process.env.AGENT_YES_CACHE_DIR ||
|
|
63
60
|
path.join(
|
|
64
|
-
process.env.XDG_CACHE_HOME ||
|
|
65
|
-
path.join(process.env.HOME || "/tmp", ".cache"),
|
|
61
|
+
process.env.XDG_CACHE_HOME || path.join(process.env.HOME || "/tmp", ".cache"),
|
|
66
62
|
"agent-yes",
|
|
67
63
|
);
|
|
68
64
|
|
|
@@ -78,14 +74,8 @@ export function findRustBinary(verbose = false): string | undefined {
|
|
|
78
74
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
79
75
|
const searchPaths = [
|
|
80
76
|
// 1. Check relative to this script (in the repo during development)
|
|
81
|
-
path.resolve(
|
|
82
|
-
|
|
83
|
-
`../rs/target/release/agent-yes${ext}`,
|
|
84
|
-
),
|
|
85
|
-
path.resolve(
|
|
86
|
-
import.meta.dirname ?? import.meta.dir,
|
|
87
|
-
`../rs/target/debug/agent-yes${ext}`,
|
|
88
|
-
),
|
|
77
|
+
path.resolve(import.meta.dirname ?? import.meta.dir, `../rs/target/release/agent-yes${ext}`),
|
|
78
|
+
path.resolve(import.meta.dirname ?? import.meta.dir, `../rs/target/debug/agent-yes${ext}`),
|
|
89
79
|
|
|
90
80
|
// 2. Check in npm package bin directory
|
|
91
81
|
path.join(getBinDir(), binaryName),
|
|
@@ -149,9 +139,7 @@ export async function downloadBinary(verbose = false): Promise<string> {
|
|
|
149
139
|
|
|
150
140
|
const response = await fetch(url);
|
|
151
141
|
if (!response.ok) {
|
|
152
|
-
throw new Error(
|
|
153
|
-
`Failed to download binary: ${response.status} ${response.statusText}`,
|
|
154
|
-
);
|
|
142
|
+
throw new Error(`Failed to download binary: ${response.status} ${response.statusText}`);
|
|
155
143
|
}
|
|
156
144
|
|
|
157
145
|
const isWindows = process.platform === "win32";
|
|
@@ -243,19 +231,14 @@ function getRustBinaryVersion(binaryPath: string): string | null {
|
|
|
243
231
|
*/
|
|
244
232
|
function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
|
|
245
233
|
// Only auto-rebuild for local dev builds (target/release or target/debug)
|
|
246
|
-
if (
|
|
247
|
-
!binaryPath.includes("/target/release") &&
|
|
248
|
-
!binaryPath.includes("/target/debug")
|
|
249
|
-
) {
|
|
234
|
+
if (!binaryPath.includes("/target/release") && !binaryPath.includes("/target/debug")) {
|
|
250
235
|
return true; // not a dev build, skip
|
|
251
236
|
}
|
|
252
237
|
|
|
253
238
|
const binaryVersion = getRustBinaryVersion(binaryPath);
|
|
254
239
|
const pkgVersion = getInstalledPackage().version;
|
|
255
240
|
if (verbose) {
|
|
256
|
-
console.log(
|
|
257
|
-
`[rust] Binary version: ${binaryVersion}, package version: ${pkgVersion}`,
|
|
258
|
-
);
|
|
241
|
+
console.log(`[rust] Binary version: ${binaryVersion}, package version: ${pkgVersion}`);
|
|
259
242
|
}
|
|
260
243
|
|
|
261
244
|
if (binaryVersion === pkgVersion) {
|
|
@@ -263,15 +246,9 @@ function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
|
|
|
263
246
|
}
|
|
264
247
|
|
|
265
248
|
// Find the rs/ directory relative to the binary (binary is at rs/target/release/agent-yes)
|
|
266
|
-
const rsDir = binaryPath.replace(
|
|
267
|
-
/\/target\/(release|debug)\/agent-yes.*$/,
|
|
268
|
-
"",
|
|
269
|
-
);
|
|
249
|
+
const rsDir = binaryPath.replace(/\/target\/(release|debug)\/agent-yes.*$/, "");
|
|
270
250
|
if (!existsSync(path.join(rsDir, "Cargo.toml"))) {
|
|
271
|
-
if (verbose)
|
|
272
|
-
console.log(
|
|
273
|
-
`[rust] Cannot find Cargo.toml at ${rsDir}, skipping rebuild`,
|
|
274
|
-
);
|
|
251
|
+
if (verbose) console.log(`[rust] Cannot find Cargo.toml at ${rsDir}, skipping rebuild`);
|
|
275
252
|
return true; // can't rebuild, use as-is
|
|
276
253
|
}
|
|
277
254
|
|
|
@@ -299,9 +276,7 @@ function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
|
|
|
299
276
|
process.stderr.write(`\x1b[32m[rust] Rebuild complete\x1b[0m\n`);
|
|
300
277
|
return true;
|
|
301
278
|
} catch {
|
|
302
|
-
process.stderr.write(
|
|
303
|
-
`\x1b[31m[rust] Auto-rebuild failed, using outdated binary\x1b[0m\n`,
|
|
304
|
-
);
|
|
279
|
+
process.stderr.write(`\x1b[31m[rust] Auto-rebuild failed, using outdated binary\x1b[0m\n`);
|
|
305
280
|
return true; // still usable, just old
|
|
306
281
|
}
|
|
307
282
|
}
|
package/ts/serve.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1
|
+
import { mkdir, open, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { watch } from "node:fs";
|
|
2
3
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
3
4
|
import { homedir } from "os";
|
|
4
5
|
import path from "path";
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
writeToIpc,
|
|
14
15
|
type CommonOpts,
|
|
15
16
|
} from "./subcommands.ts";
|
|
17
|
+
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
16
18
|
|
|
17
19
|
const DEFAULT_PORT = 7432;
|
|
18
20
|
|
|
@@ -123,6 +125,8 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
123
125
|
` --port N Port to listen on (default: ${DEFAULT_PORT})\n` +
|
|
124
126
|
` --host HOST Interface to bind (default: 127.0.0.1; use 0.0.0.0 to expose)\n` +
|
|
125
127
|
` --token TOKEN Auth token (auto-generated and saved if omitted)\n` +
|
|
128
|
+
` --share [URL] Share over WebRTC to agent-yes.com (bare flag mints a room+link)\n` +
|
|
129
|
+
` --allow-spawn Deprecated no-op — the console can always spawn agents\n` +
|
|
126
130
|
` --tls-cert FILE TLS certificate PEM\n` +
|
|
127
131
|
` --tls-key FILE TLS private key PEM\n\n` +
|
|
128
132
|
`Subcommands:\n` +
|
|
@@ -153,6 +157,16 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
153
157
|
.option("token", { type: "string", description: "Auth token (auto-generated if omitted)" })
|
|
154
158
|
.option("tls-cert", { type: "string", description: "TLS certificate file (PEM)" })
|
|
155
159
|
.option("tls-key", { type: "string", description: "TLS private key file (PEM)" })
|
|
160
|
+
.option("share", {
|
|
161
|
+
type: "string",
|
|
162
|
+
description:
|
|
163
|
+
"Share over WebRTC: bare flag mints a room+link, or pass webrtc://room:token@host",
|
|
164
|
+
})
|
|
165
|
+
.option("allow-spawn", {
|
|
166
|
+
type: "boolean",
|
|
167
|
+
default: false,
|
|
168
|
+
description: "Deprecated no-op — the console can always spawn agents",
|
|
169
|
+
})
|
|
156
170
|
.help(false)
|
|
157
171
|
.version(false)
|
|
158
172
|
.exitProcess(false);
|
|
@@ -178,6 +192,11 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
const token = await loadOrCreateToken(tokenFlag);
|
|
195
|
+
// Spawning is always allowed: a connected console already has full read-write
|
|
196
|
+
// control over every running agent (it writes straight to their stdin), so it
|
|
197
|
+
// can already make an agent do anything — gating /api/spawn behind a flag or a
|
|
198
|
+
// y/N prompt bought no real safety. We just log each spawn so the host sees it.
|
|
199
|
+
// (--allow-spawn is still accepted as a no-op for older invocations.)
|
|
181
200
|
|
|
182
201
|
const serverOpts: any = {
|
|
183
202
|
hostname: host,
|
|
@@ -242,10 +261,39 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
242
261
|
}
|
|
243
262
|
}
|
|
244
263
|
|
|
264
|
+
// GET /api/size/:keyword — the agent's current PTY size, so the console can
|
|
265
|
+
// render the existing buffer at the agent's real width before adapting.
|
|
266
|
+
const sizeM = /^\/api\/size\/(.+)$/.exec(p);
|
|
267
|
+
if (req.method === "GET" && sizeM) {
|
|
268
|
+
const keyword = decodeURIComponent(sizeM[1]!);
|
|
269
|
+
try {
|
|
270
|
+
const record = await resolveOne(keyword, defaultOpts());
|
|
271
|
+
const ayHome = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
272
|
+
let cols: number | null = null;
|
|
273
|
+
let rows: number | null = null;
|
|
274
|
+
try {
|
|
275
|
+
const txt = await readFile(path.join(ayHome, "ptysize", String(record.pid)), "utf-8");
|
|
276
|
+
const [c, r] = txt.trim().split(/\s+/).map(Number);
|
|
277
|
+
if (c > 0 && r > 0) {
|
|
278
|
+
cols = c;
|
|
279
|
+
rows = r;
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
/* no ptysize sidecar (older agent or not yet written) */
|
|
283
|
+
}
|
|
284
|
+
return Response.json({ pid: record.pid, cols, rows });
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return new Response((e as Error).message, { status: 404 });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
245
290
|
// GET /api/tail/:keyword — SSE streaming
|
|
246
291
|
const tailM = /^\/api\/tail\/(.+)$/.exec(p);
|
|
247
292
|
if (req.method === "GET" && tailM) {
|
|
248
293
|
const keyword = decodeURIComponent(tailM[1]!);
|
|
294
|
+
// raw=1 streams the unmodified PTY bytes (ANSI/cursor control intact) so a
|
|
295
|
+
// browser xterm.js can render the real terminal; default stays ANSI-stripped.
|
|
296
|
+
const raw = url.searchParams.get("raw") === "1";
|
|
249
297
|
try {
|
|
250
298
|
const record = await resolveOne(keyword, defaultOpts());
|
|
251
299
|
if (!record.log_file)
|
|
@@ -259,10 +307,12 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
259
307
|
ctrl.enqueue(enc.encode(`data: ${JSON.stringify(text)}\n\n`));
|
|
260
308
|
const ping = () => ctrl.enqueue(enc.encode(": ping\n\n"));
|
|
261
309
|
|
|
262
|
-
// Initial tail
|
|
310
|
+
// Initial tail. Raw: replay the last ~64 KB of PTY bytes (enough to
|
|
311
|
+
// contain a recent full-screen redraw so xterm converges fast).
|
|
263
312
|
const initBuf = await readFile(logPath).catch(() => Buffer.alloc(0));
|
|
264
|
-
|
|
265
|
-
|
|
313
|
+
if (raw)
|
|
314
|
+
send(new TextDecoder().decode(initBuf.slice(Math.max(0, initBuf.length - 65536))));
|
|
315
|
+
else send(await renderRawLog(initBuf, { mode: "tail", n: 96 }));
|
|
266
316
|
|
|
267
317
|
let offset = initBuf.length;
|
|
268
318
|
let closed = false;
|
|
@@ -281,30 +331,59 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
281
331
|
// eslint-disable-next-line no-control-regex
|
|
282
332
|
const ctrlRe = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
283
333
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
334
|
+
// Stream only the bytes appended since `offset` (incremental read,
|
|
335
|
+
// not a full re-read), driven by fs.watch for near-instant echo with
|
|
336
|
+
// a short fallback poll in case the watcher misses an event. The old
|
|
337
|
+
// 300 ms full-file poll was the dominant typing-echo latency.
|
|
338
|
+
const fh = await open(logPath, "r").catch(() => null);
|
|
339
|
+
let reading = false;
|
|
340
|
+
const flush = async () => {
|
|
341
|
+
if (closed || reading || !fh) return;
|
|
342
|
+
reading = true;
|
|
289
343
|
try {
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
.
|
|
296
|
-
|
|
297
|
-
.
|
|
298
|
-
|
|
344
|
+
const { size } = await fh.stat();
|
|
345
|
+
if (size < offset) offset = size; // truncated/rotated
|
|
346
|
+
if (size > offset) {
|
|
347
|
+
const len = size - offset;
|
|
348
|
+
const buf = Buffer.allocUnsafe(len);
|
|
349
|
+
const { bytesRead } = await fh.read(buf, 0, len, offset);
|
|
350
|
+
offset += bytesRead;
|
|
351
|
+
const chunk = buf.subarray(0, bytesRead);
|
|
352
|
+
if (raw) {
|
|
353
|
+
send(new TextDecoder().decode(chunk));
|
|
354
|
+
} else {
|
|
355
|
+
const text = new TextDecoder()
|
|
356
|
+
.decode(chunk)
|
|
357
|
+
.replace(ansiRe, "")
|
|
358
|
+
.replace(ctrlRe, "");
|
|
359
|
+
if (text.trim()) send(text.trimStart());
|
|
360
|
+
}
|
|
361
|
+
}
|
|
299
362
|
} catch {
|
|
300
363
|
/* log gone */
|
|
364
|
+
} finally {
|
|
365
|
+
reading = false;
|
|
301
366
|
}
|
|
302
|
-
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
let watcher: ReturnType<typeof watch> | null = null;
|
|
370
|
+
try {
|
|
371
|
+
watcher = watch(logPath, () => void flush());
|
|
372
|
+
} catch {
|
|
373
|
+
/* fs.watch unsupported — the fallback poll below still works */
|
|
374
|
+
}
|
|
375
|
+
const poller = setInterval(() => void flush(), 60);
|
|
303
376
|
|
|
304
377
|
req.signal.addEventListener("abort", () => {
|
|
305
378
|
closed = true;
|
|
306
379
|
clearInterval(heartbeat);
|
|
307
380
|
clearInterval(poller);
|
|
381
|
+
try {
|
|
382
|
+
watcher?.close();
|
|
383
|
+
} catch {
|
|
384
|
+
/* already closed */
|
|
385
|
+
}
|
|
386
|
+
void fh?.close().catch(() => {});
|
|
308
387
|
try {
|
|
309
388
|
ctrl.close();
|
|
310
389
|
} catch {
|
|
@@ -356,6 +435,71 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
356
435
|
}
|
|
357
436
|
}
|
|
358
437
|
|
|
438
|
+
// POST /api/resize/:keyword body {cols, rows} — drive the agent's PTY size.
|
|
439
|
+
// Mirrors `ay attach`: write ~/.agent-yes/winsize/<pid> then SIGWINCH; the
|
|
440
|
+
// agent's resize listener picks it up and reflows its TUI to that width.
|
|
441
|
+
const resizeM = /^\/api\/resize\/(.+)$/.exec(p);
|
|
442
|
+
if (req.method === "POST" && resizeM) {
|
|
443
|
+
const keyword = decodeURIComponent(resizeM[1]!);
|
|
444
|
+
let body: { cols?: number; rows?: number };
|
|
445
|
+
try {
|
|
446
|
+
body = await req.json();
|
|
447
|
+
} catch {
|
|
448
|
+
return new Response("invalid JSON body", { status: 400 });
|
|
449
|
+
}
|
|
450
|
+
const cols = Math.max(1, Math.floor(Number(body.cols) || 0));
|
|
451
|
+
const rows = Math.max(1, Math.floor(Number(body.rows) || 0));
|
|
452
|
+
if (!cols || !rows) return new Response("missing cols/rows", { status: 400 });
|
|
453
|
+
try {
|
|
454
|
+
const record = await resolveOne(keyword, defaultOpts());
|
|
455
|
+
const ayHome = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
456
|
+
const winsizeDir = path.join(ayHome, "winsize");
|
|
457
|
+
await mkdir(winsizeDir, { recursive: true });
|
|
458
|
+
await writeFile(
|
|
459
|
+
path.join(winsizeDir, String(record.pid)),
|
|
460
|
+
`${cols} ${rows} ${Date.now()}\n`,
|
|
461
|
+
);
|
|
462
|
+
try {
|
|
463
|
+
process.kill(record.pid, "SIGWINCH");
|
|
464
|
+
} catch {
|
|
465
|
+
/* agent gone */
|
|
466
|
+
}
|
|
467
|
+
return Response.json({ ok: true, pid: record.pid, cols, rows });
|
|
468
|
+
} catch (e) {
|
|
469
|
+
return new Response((e as Error).message, { status: 404 });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// POST /api/spawn body {cli, cwd, prompt} — launch a new agent
|
|
474
|
+
if (req.method === "POST" && p === "/api/spawn") {
|
|
475
|
+
let body: { cli?: string; cwd?: string; prompt?: string };
|
|
476
|
+
try {
|
|
477
|
+
body = await req.json();
|
|
478
|
+
} catch {
|
|
479
|
+
return new Response("invalid JSON body", { status: 400 });
|
|
480
|
+
}
|
|
481
|
+
const cli = String(body.cli ?? "claude");
|
|
482
|
+
if (!SUPPORTED_CLIS.includes(cli as never))
|
|
483
|
+
return new Response(`unsupported cli: ${cli}`, { status: 400 });
|
|
484
|
+
const cwd = typeof body.cwd === "string" && body.cwd ? body.cwd : process.cwd();
|
|
485
|
+
const prompt = String(body.prompt ?? "");
|
|
486
|
+
process.stderr.write(
|
|
487
|
+
`→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd})\n`,
|
|
488
|
+
);
|
|
489
|
+
try {
|
|
490
|
+
const child = Bun.spawn(["ay", cli, ...(prompt ? ["--", prompt] : [])], {
|
|
491
|
+
cwd,
|
|
492
|
+
stdin: "ignore",
|
|
493
|
+
stdout: "ignore",
|
|
494
|
+
stderr: "ignore",
|
|
495
|
+
});
|
|
496
|
+
child.unref();
|
|
497
|
+
return Response.json({ ok: true, pid: child.pid, cli, cwd });
|
|
498
|
+
} catch (e) {
|
|
499
|
+
return new Response((e as Error).message, { status: 500 });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
359
503
|
return new Response("Not Found", { status: 404 });
|
|
360
504
|
},
|
|
361
505
|
};
|
|
@@ -380,6 +524,27 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
380
524
|
` openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj '/CN=localhost'\n\n`,
|
|
381
525
|
);
|
|
382
526
|
}
|
|
527
|
+
// --share: bridge this local server to a WebRTC room so the agent-yes.com
|
|
528
|
+
// console can reach it peer-to-peer. Bare flag mints a room; a webrtc:// value
|
|
529
|
+
// joins an explicit one.
|
|
530
|
+
if (argv.share !== undefined) {
|
|
531
|
+
const shareUrl =
|
|
532
|
+
typeof argv.share === "string" && argv.share.startsWith("webrtc://") ? argv.share : undefined;
|
|
533
|
+
try {
|
|
534
|
+
const { startShare } = await import("./share.ts");
|
|
535
|
+
const { link } = await startShare({
|
|
536
|
+
url: shareUrl,
|
|
537
|
+
apiUrl: `http://127.0.0.1:${port}`,
|
|
538
|
+
apiToken: token,
|
|
539
|
+
});
|
|
540
|
+
process.stdout.write(
|
|
541
|
+
`\nshared over WebRTC — open this link (the token is eaten from the URL on open):\n ${link}\n\n`,
|
|
542
|
+
);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
process.stderr.write(`ay serve --share failed: ${(e as Error).message}\n`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
383
548
|
process.stdout.write(`(Ctrl-C to stop)\n`);
|
|
384
549
|
|
|
385
550
|
await new Promise<void>((resolve) => {
|