claude-yes 1.89.0 → 1.90.1
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 +10 -0
- package/dist/{SUPPORTED_CLIS-yRq_h-h6.js → SUPPORTED_CLIS-BrUEk9Ws.js} +3 -3
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{serve-CHhrOT6F.js → serve-BYPX6VXm.js} +2 -2
- package/dist/subcommands-D5WYWmh-.js +6 -0
- package/dist/{subcommands-Czbgwuy-.js → subcommands-LRmJ4_iJ.js} +258 -4
- package/dist/{ts-CTqyDzUi.js → ts-T6n6lrQG.js} +2 -2
- package/dist/{versionChecker-puPia13W.js → versionChecker-Go0xdcmN.js} +2 -2
- package/package.json +1 -1
- package/ts/subcommands.spec.ts +39 -0
- package/ts/subcommands.ts +363 -1
- package/dist/subcommands-DIxUhK0D.js +0 -6
package/README.md
CHANGED
|
@@ -142,8 +142,18 @@ cy tail <keyword> # render last 96 lines via @xterm/headles
|
|
|
142
142
|
cy read <keyword> # full rendered log
|
|
143
143
|
cy send <keyword> "next: run tests" # append a prompt to that agent's stdin
|
|
144
144
|
cy send <keyword> "" --code=ctrl-c # send a Ctrl+C
|
|
145
|
+
cy attach <keyword> # interactive attach (detach: Ctrl-\)
|
|
146
|
+
cy stop <keyword> # graceful shutdown (claude/codex: /exit)
|
|
145
147
|
```
|
|
146
148
|
|
|
149
|
+
#### Tips
|
|
150
|
+
|
|
151
|
+
- A **single** `--code=ctrl-c` does not stop `claude` / `codex` — they treat it
|
|
152
|
+
as "cancel current turn" rather than "quit". Prefer `cy stop <keyword>` (which
|
|
153
|
+
sends `/exit` for claude/codex and `/quit` for gemini), or send Ctrl+C twice
|
|
154
|
+
in quick succession. The `cy send … --code=ctrl-c` output prints a one-line
|
|
155
|
+
hint pointing at this when it detects one of those CLIs.
|
|
156
|
+
|
|
147
157
|
`cy` (and `ay` / `agent-yes`) writes to a shared registry at
|
|
148
158
|
`~/.agent-yes/pids.jsonl` and a per-pid FIFO at `~/.agent-yes/fifo/<pid>.stdin`,
|
|
149
159
|
so subcommands work whether the target agent is the TS or Rust runtime.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-T6n6lrQG.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-Go0xdcmN.js";
|
|
4
4
|
import "./pidStore-C1JXxoPi.js";
|
|
5
5
|
import "./globalPidIndex-Cr-g75QF.js";
|
|
6
6
|
|
|
@@ -9,4 +9,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
|
9
9
|
|
|
10
10
|
//#endregion
|
|
11
11
|
export { SUPPORTED_CLIS };
|
|
12
|
-
//# sourceMappingURL=SUPPORTED_CLIS-
|
|
12
|
+
//# sourceMappingURL=SUPPORTED_CLIS-BrUEk9Ws.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { n as logger } from "./logger-B9h0djqx.js";
|
|
3
|
-
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-
|
|
3
|
+
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-Go0xdcmN.js";
|
|
4
4
|
import { argv } from "process";
|
|
5
5
|
import { execFileSync, spawn } from "child_process";
|
|
6
6
|
import ms from "ms";
|
|
@@ -482,7 +482,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
482
482
|
{
|
|
483
483
|
const rawArg = process.argv[2];
|
|
484
484
|
const isHelpFlag = rawArg === "-h" || rawArg === "--help";
|
|
485
|
-
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-
|
|
485
|
+
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-D5WYWmh-.js");
|
|
486
486
|
if (isHelpFlag && process.argv.length === 3) {
|
|
487
487
|
cmdHelp();
|
|
488
488
|
process.exit(0);
|
|
@@ -515,7 +515,7 @@ if (config.useRust) {
|
|
|
515
515
|
}
|
|
516
516
|
}
|
|
517
517
|
if (rustBinary) {
|
|
518
|
-
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-
|
|
518
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-BrUEk9Ws.js");
|
|
519
519
|
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
520
520
|
if (config.verbose) {
|
|
521
521
|
console.log(`[rust] Using binary: ${rustBinary}`);
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-T6n6lrQG.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-Go0xdcmN.js";
|
|
4
4
|
import "./pidStore-C1JXxoPi.js";
|
|
5
5
|
import "./globalPidIndex-Cr-g75QF.js";
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "./logger-B9h0djqx.js";
|
|
2
2
|
import "./globalPidIndex-Cr-g75QF.js";
|
|
3
3
|
import "./remotes-Bjp2GYPz.js";
|
|
4
|
-
import {
|
|
4
|
+
import { c as readNotes, f as snapshotStatus, l as renderRawLog, m as writeToIpc, o as listRecords, r as controlCodeFromName, u as resolveOne } from "./subcommands-LRmJ4_iJ.js";
|
|
5
5
|
import yargs from "yargs";
|
|
6
6
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
7
7
|
import { homedir } from "os";
|
|
@@ -312,4 +312,4 @@ Options:
|
|
|
312
312
|
|
|
313
313
|
//#endregion
|
|
314
314
|
export { cmdServe };
|
|
315
|
-
//# sourceMappingURL=serve-
|
|
315
|
+
//# sourceMappingURL=serve-BYPX6VXm.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import "./logger-B9h0djqx.js";
|
|
2
|
+
import "./globalPidIndex-Cr-g75QF.js";
|
|
3
|
+
import "./remotes-Bjp2GYPz.js";
|
|
4
|
+
import { a as isSubcommand, c as readNotes, d as runSubcommand, f as snapshotStatus, i as isPidAlive, l as renderRawLog, m as writeToIpc, n as cmdHelp, o as listRecords, p as stopTipForCli, r as controlCodeFromName, s as matchKeyword, t as GRACEFUL_EXIT_COMMANDS, u as resolveOne } from "./subcommands-LRmJ4_iJ.js";
|
|
5
|
+
|
|
6
|
+
export { cmdHelp, isSubcommand, runSubcommand };
|
|
@@ -126,6 +126,8 @@ const SUBCOMMANDS = new Set([
|
|
|
126
126
|
"tail",
|
|
127
127
|
"head",
|
|
128
128
|
"send",
|
|
129
|
+
"attach",
|
|
130
|
+
"stop",
|
|
129
131
|
"restart",
|
|
130
132
|
"note",
|
|
131
133
|
"serve",
|
|
@@ -155,10 +157,12 @@ async function runSubcommand(argv) {
|
|
|
155
157
|
case "tail": return await cmdRead(rest, { mode: "tail" });
|
|
156
158
|
case "head": return await cmdRead(rest, { mode: "head" });
|
|
157
159
|
case "send": return await cmdSend(rest);
|
|
160
|
+
case "attach": return await cmdAttach(rest);
|
|
161
|
+
case "stop": return await cmdStop(rest);
|
|
158
162
|
case "restart": return await cmdRestart(rest);
|
|
159
163
|
case "note": return await cmdNote(rest);
|
|
160
164
|
case "serve": {
|
|
161
|
-
const { cmdServe } = await import("./serve-
|
|
165
|
+
const { cmdServe } = await import("./serve-BYPX6VXm.js");
|
|
162
166
|
return cmdServe(rest);
|
|
163
167
|
}
|
|
164
168
|
case "remote": {
|
|
@@ -175,7 +179,7 @@ async function runSubcommand(argv) {
|
|
|
175
179
|
}
|
|
176
180
|
}
|
|
177
181
|
function cmdHelp() {
|
|
178
|
-
process.stdout.write("ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay tail [-f] <keyword> stream output (Ctrl-C to stop)\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay send <keyword> <msg> send a message\n ay status <keyword> agent status snapshot\n\nRemote:\n ay serve [--port N] start HTTP API server (prints token)\n ay remote add <alias> http://<token>@<host>:<port>\n ay remote ls / rm <alias> manage saved remotes\n ay ls <token>@<host>:<port> connect inline (no alias needed)\n ay send <token>@<host>:<port>:<kw> <msg>\n\nRun an agent:\n ay [claude|codex|gemini|...] [options] -- [prompt]\n ay claude -- \"fix the bug in auth.ts\"\n ay claude --help full agent-runner options\n\nLabs (examples
|
|
182
|
+
process.stdout.write("ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay tail [-f] <keyword> stream output (Ctrl-C to stop)\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay send <keyword> <msg> send a message\n ay attach <keyword> interactive attach (detach: Ctrl-\\)\n ay stop <keyword> graceful shutdown (/exit for claude/codex)\n ay status <keyword> agent status snapshot\n\nRemote:\n ay serve [--port N] start HTTP API server (prints token)\n ay remote add <alias> http://<token>@<host>:<port>\n ay remote ls / rm <alias> manage saved remotes\n ay ls <token>@<host>:<port> connect inline (no alias needed)\n ay send <token>@<host>:<port>:<kw> <msg>\n\nRun an agent:\n ay [claude|codex|gemini|...] [options] -- [prompt]\n ay claude -- \"fix the bug in auth.ts\"\n ay claude --help full agent-runner options\n\nLabs (examples at https://github.com/snomiao/agent-yes/tree/main/lab):\n local-role-play/ designer + builder on one machine\n http-remote/ ay serve remote access demo\n p2p-pairing/ libp2p P2P (needs: cargo build --features swarm)\n");
|
|
179
183
|
return 0;
|
|
180
184
|
}
|
|
181
185
|
function matchKeyword(record, keyword) {
|
|
@@ -953,8 +957,22 @@ async function cmdSend(rest) {
|
|
|
953
957
|
process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
|
|
954
958
|
const replyHint = sourcePid ? ` ay send ${sourcePid} "..." # reply to sender\n` : "";
|
|
955
959
|
process.stderr.write(`\n` + replyHint + ` ay tail ${record.pid} # watch output\n ay ls # list all agents\n`);
|
|
960
|
+
if (codeName === "ctrl-c" || codeName === "ctrlc") {
|
|
961
|
+
const tip = stopTipForCli(record.cli, record.pid);
|
|
962
|
+
if (tip) process.stderr.write(tip);
|
|
963
|
+
}
|
|
956
964
|
return 0;
|
|
957
965
|
}
|
|
966
|
+
function stopTipForCli(cli, pid) {
|
|
967
|
+
const cmd = GRACEFUL_EXIT_COMMANDS[cli];
|
|
968
|
+
if (cmd) return ` tip: ${cli} ignores a single Ctrl+C — try 'ay stop ${pid}' (sends '${cmd}') or double Ctrl+C.\n`;
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
const GRACEFUL_EXIT_COMMANDS = {
|
|
972
|
+
claude: "/exit",
|
|
973
|
+
codex: "/exit",
|
|
974
|
+
gemini: "/quit"
|
|
975
|
+
};
|
|
958
976
|
function controlCodeFromName(name) {
|
|
959
977
|
switch (name) {
|
|
960
978
|
case "enter":
|
|
@@ -968,6 +986,9 @@ function controlCodeFromName(name) {
|
|
|
968
986
|
case "ctrly": return "";
|
|
969
987
|
case "ctrl-d":
|
|
970
988
|
case "ctrld": return "";
|
|
989
|
+
case "ctrl-\\":
|
|
990
|
+
case "ctrl\\":
|
|
991
|
+
case "ctrl-backslash": return "";
|
|
971
992
|
case "tab": return " ";
|
|
972
993
|
case "none":
|
|
973
994
|
case "": return "";
|
|
@@ -1007,6 +1028,239 @@ async function writeToIpc(ipcPath, payload) {
|
|
|
1007
1028
|
}
|
|
1008
1029
|
}
|
|
1009
1030
|
}
|
|
1031
|
+
async function cmdStop(rest) {
|
|
1032
|
+
const argv = await yargs(rest).usage("Usage: ay stop <keyword> [--method=graceful|double-ctrl-c|auto]").option("method", {
|
|
1033
|
+
type: "string",
|
|
1034
|
+
default: "auto",
|
|
1035
|
+
description: "Shutdown strategy: auto (per-CLI), graceful (/exit-style), double-ctrl-c (force)"
|
|
1036
|
+
}).option("all", {
|
|
1037
|
+
type: "boolean",
|
|
1038
|
+
default: false,
|
|
1039
|
+
description: "Include exited agents"
|
|
1040
|
+
}).option("latest", {
|
|
1041
|
+
type: "boolean",
|
|
1042
|
+
default: false,
|
|
1043
|
+
description: "Use most recent match"
|
|
1044
|
+
}).option("cwd", {
|
|
1045
|
+
type: "string",
|
|
1046
|
+
description: "Restrict to agents under this dir"
|
|
1047
|
+
}).help(false).version(false).exitProcess(false).parseAsync();
|
|
1048
|
+
const opts = {
|
|
1049
|
+
all: argv.all,
|
|
1050
|
+
active: false,
|
|
1051
|
+
json: false,
|
|
1052
|
+
latest: argv.latest,
|
|
1053
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
|
|
1054
|
+
};
|
|
1055
|
+
const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
|
|
1056
|
+
if (!keyword) throw new Error("usage: ay stop <keyword> [--method=auto|graceful|double-ctrl-c]");
|
|
1057
|
+
const record = await resolveOne(keyword, opts);
|
|
1058
|
+
if (!record.fifo_file) throw new Error(`pid ${record.pid}: no fifo_file — cannot send shutdown command`);
|
|
1059
|
+
const method = String(argv.method).toLowerCase();
|
|
1060
|
+
const graceful = GRACEFUL_EXIT_COMMANDS[record.cli];
|
|
1061
|
+
let payload;
|
|
1062
|
+
let strategy;
|
|
1063
|
+
if (method === "double-ctrl-c") {
|
|
1064
|
+
payload = "double-ctrl-c";
|
|
1065
|
+
strategy = `double Ctrl+C (forced)`;
|
|
1066
|
+
} else if (method === "graceful" || method === "auto" && graceful) {
|
|
1067
|
+
if (!graceful) throw new Error(`--method=graceful: no known graceful-exit command for cli "${record.cli}"`);
|
|
1068
|
+
payload = graceful;
|
|
1069
|
+
strategy = `'${graceful}' + Enter`;
|
|
1070
|
+
} else if (method === "auto") {
|
|
1071
|
+
payload = "double-ctrl-c";
|
|
1072
|
+
strategy = `double Ctrl+C (no known /exit for cli "${record.cli}")`;
|
|
1073
|
+
} else throw new Error(`unknown --method=${method}`);
|
|
1074
|
+
const fifoPath = record.fifo_file;
|
|
1075
|
+
if (payload === "double-ctrl-c") {
|
|
1076
|
+
await writeToIpc(fifoPath, "");
|
|
1077
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1078
|
+
await writeToIpc(fifoPath, "");
|
|
1079
|
+
} else {
|
|
1080
|
+
await writeToIpc(fifoPath, payload);
|
|
1081
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1082
|
+
await writeToIpc(fifoPath, "\r");
|
|
1083
|
+
}
|
|
1084
|
+
process.stdout.write(`stopping pid ${record.pid} (${record.cli}) via ${strategy}\n`);
|
|
1085
|
+
process.stderr.write(`\n ay status ${record.pid} # confirm it exited\n ay ls --all # see exit codes\n`);
|
|
1086
|
+
return 0;
|
|
1087
|
+
}
|
|
1088
|
+
async function cmdAttach(rest) {
|
|
1089
|
+
const argv = await yargs(rest).usage("Usage: ay attach <keyword> [--escape ctrl-\\]").option("escape", {
|
|
1090
|
+
type: "string",
|
|
1091
|
+
default: "ctrl-\\",
|
|
1092
|
+
description: "Detach key name (see --code list; default: ctrl-\\)"
|
|
1093
|
+
}).option("all", {
|
|
1094
|
+
type: "boolean",
|
|
1095
|
+
default: false,
|
|
1096
|
+
description: "Include exited agents"
|
|
1097
|
+
}).option("latest", {
|
|
1098
|
+
type: "boolean",
|
|
1099
|
+
default: false,
|
|
1100
|
+
description: "Use most recent match"
|
|
1101
|
+
}).option("cwd", {
|
|
1102
|
+
type: "string",
|
|
1103
|
+
description: "Restrict to agents under this dir"
|
|
1104
|
+
}).help(false).version(false).exitProcess(false).parseAsync();
|
|
1105
|
+
const opts = {
|
|
1106
|
+
all: argv.all,
|
|
1107
|
+
active: false,
|
|
1108
|
+
json: false,
|
|
1109
|
+
latest: argv.latest,
|
|
1110
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
|
|
1111
|
+
};
|
|
1112
|
+
const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
|
|
1113
|
+
if (!keyword) throw new Error("usage: ay attach <keyword> [--escape ctrl-\\]");
|
|
1114
|
+
const escapeName = String(argv.escape).toLowerCase();
|
|
1115
|
+
const detachSeq = controlCodeFromName(escapeName);
|
|
1116
|
+
if (!detachSeq) throw new Error(`--escape must resolve to a non-empty byte sequence (got "${argv.escape}")`);
|
|
1117
|
+
const detachByte = detachSeq.charCodeAt(0);
|
|
1118
|
+
const record = await resolveOne(keyword, opts);
|
|
1119
|
+
if (!record.fifo_file) throw new Error(`pid ${record.pid}: no fifo_file recorded — agent has no input channel`);
|
|
1120
|
+
if (!record.log_file) throw new Error(`pid ${record.pid}: no log_file recorded — cannot stream output`);
|
|
1121
|
+
if (!isPidAlive(record.pid)) throw new Error(`pid ${record.pid}: process is not alive`);
|
|
1122
|
+
const fifoPath = record.fifo_file;
|
|
1123
|
+
const logPath = record.log_file;
|
|
1124
|
+
const REPLAY_CAP_BYTES = 1024 * 1024;
|
|
1125
|
+
let initialOffset = 0;
|
|
1126
|
+
let replay = "";
|
|
1127
|
+
try {
|
|
1128
|
+
const st = await stat(logPath);
|
|
1129
|
+
initialOffset = Number(st.size);
|
|
1130
|
+
if (initialOffset > 0) {
|
|
1131
|
+
const readStart = Math.max(0, initialOffset - REPLAY_CAP_BYTES);
|
|
1132
|
+
const fh = await open(logPath, "r");
|
|
1133
|
+
try {
|
|
1134
|
+
const buf = Buffer.alloc(initialOffset - readStart);
|
|
1135
|
+
await fh.read(buf, 0, buf.length, readStart);
|
|
1136
|
+
replay = await renderRawLog(buf, {
|
|
1137
|
+
mode: "tail",
|
|
1138
|
+
n: process.stdout.rows ?? 50
|
|
1139
|
+
});
|
|
1140
|
+
} finally {
|
|
1141
|
+
await fh.close();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
} catch {}
|
|
1145
|
+
process.stderr.write(`[attaching to pid ${record.pid}: ${record.cli} in ${shortenPath(record.cwd)}]\n[detach: ${escapeName}]\n`);
|
|
1146
|
+
if (replay) {
|
|
1147
|
+
process.stdout.write(replay);
|
|
1148
|
+
if (!replay.endsWith("\n")) process.stdout.write("\n");
|
|
1149
|
+
}
|
|
1150
|
+
const ayHome = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
1151
|
+
const winsizeDir = path.join(ayHome, "winsize");
|
|
1152
|
+
await mkdir(winsizeDir, { recursive: true });
|
|
1153
|
+
const winsizePath = path.join(winsizeDir, String(record.pid));
|
|
1154
|
+
const sendResize = async () => {
|
|
1155
|
+
const cols = process.stdout.columns ?? 80;
|
|
1156
|
+
const rows = process.stdout.rows ?? 24;
|
|
1157
|
+
try {
|
|
1158
|
+
await writeFile(winsizePath, `${cols} ${rows} ${Date.now()}\n`);
|
|
1159
|
+
try {
|
|
1160
|
+
process.kill(record.pid, "SIGWINCH");
|
|
1161
|
+
} catch {}
|
|
1162
|
+
} catch {}
|
|
1163
|
+
};
|
|
1164
|
+
await sendResize();
|
|
1165
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
1166
|
+
const stdinIsTty = !!process.stdin.isTTY;
|
|
1167
|
+
if (stdinIsTty) try {
|
|
1168
|
+
process.stdin.setRawMode(true);
|
|
1169
|
+
} catch {}
|
|
1170
|
+
process.stdin.resume();
|
|
1171
|
+
const onResize = () => {
|
|
1172
|
+
sendResize();
|
|
1173
|
+
};
|
|
1174
|
+
process.stdout.on("resize", onResize);
|
|
1175
|
+
const { openSync, writeSync, closeSync, watch } = await import("fs");
|
|
1176
|
+
let fifoFd = null;
|
|
1177
|
+
try {
|
|
1178
|
+
fifoFd = openSync(fifoPath, "w");
|
|
1179
|
+
} catch (err) {
|
|
1180
|
+
throw new Error(`failed to open FIFO ${fifoPath}: ${err.message}`);
|
|
1181
|
+
}
|
|
1182
|
+
let offset = initialOffset;
|
|
1183
|
+
let detached = false;
|
|
1184
|
+
let pollTimer;
|
|
1185
|
+
let aliveCheck;
|
|
1186
|
+
const flushNew = async () => {
|
|
1187
|
+
if (detached) return;
|
|
1188
|
+
try {
|
|
1189
|
+
const st = await stat(logPath);
|
|
1190
|
+
if (st.size < offset) offset = 0;
|
|
1191
|
+
if (st.size > offset) {
|
|
1192
|
+
const fh = await open(logPath, "r");
|
|
1193
|
+
try {
|
|
1194
|
+
const buf = Buffer.alloc(st.size - offset);
|
|
1195
|
+
await fh.read(buf, 0, buf.length, offset);
|
|
1196
|
+
process.stdout.write(buf);
|
|
1197
|
+
offset = st.size;
|
|
1198
|
+
} finally {
|
|
1199
|
+
await fh.close();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
} catch {}
|
|
1203
|
+
};
|
|
1204
|
+
const watcher = watch(logPath, () => {
|
|
1205
|
+
flushNew();
|
|
1206
|
+
});
|
|
1207
|
+
await flushNew();
|
|
1208
|
+
pollTimer = setInterval(() => {
|
|
1209
|
+
flushNew();
|
|
1210
|
+
}, 100);
|
|
1211
|
+
const triggerDetach = () => {
|
|
1212
|
+
if (detached) return;
|
|
1213
|
+
detached = true;
|
|
1214
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
1215
|
+
if (aliveCheck) clearInterval(aliveCheck);
|
|
1216
|
+
watcher.close();
|
|
1217
|
+
process.stdout.removeListener("resize", onResize);
|
|
1218
|
+
process.stdin.removeListener("data", onStdinData);
|
|
1219
|
+
if (stdinIsTty) try {
|
|
1220
|
+
process.stdin.setRawMode(false);
|
|
1221
|
+
} catch {}
|
|
1222
|
+
process.stdin.pause();
|
|
1223
|
+
if (fifoFd !== null) {
|
|
1224
|
+
try {
|
|
1225
|
+
closeSync(fifoFd);
|
|
1226
|
+
} catch {}
|
|
1227
|
+
fifoFd = null;
|
|
1228
|
+
}
|
|
1229
|
+
process.stderr.write(`\n[detached from pid ${record.pid} — agent still running]\n`);
|
|
1230
|
+
};
|
|
1231
|
+
const onStdinData = (chunk) => {
|
|
1232
|
+
if (detached) return;
|
|
1233
|
+
const idx = chunk.indexOf(detachByte);
|
|
1234
|
+
if (idx === -1) {
|
|
1235
|
+
try {
|
|
1236
|
+
if (fifoFd !== null) writeSync(fifoFd, chunk);
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
process.stderr.write(`\n[fifo write failed: ${err.message}]\n`);
|
|
1239
|
+
triggerDetach();
|
|
1240
|
+
}
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (idx > 0 && fifoFd !== null) try {
|
|
1244
|
+
writeSync(fifoFd, chunk.subarray(0, idx));
|
|
1245
|
+
} catch {}
|
|
1246
|
+
triggerDetach();
|
|
1247
|
+
};
|
|
1248
|
+
process.stdin.on("data", onStdinData);
|
|
1249
|
+
aliveCheck = setInterval(() => {
|
|
1250
|
+
if (!isPidAlive(record.pid)) {
|
|
1251
|
+
process.stderr.write(`\n[pid ${record.pid} exited]\n`);
|
|
1252
|
+
triggerDetach();
|
|
1253
|
+
}
|
|
1254
|
+
}, 1e3);
|
|
1255
|
+
await new Promise((resolve) => {
|
|
1256
|
+
const tick = () => {
|
|
1257
|
+
if (detached) resolve();
|
|
1258
|
+
else setTimeout(tick, 50);
|
|
1259
|
+
};
|
|
1260
|
+
tick();
|
|
1261
|
+
});
|
|
1262
|
+
return 0;
|
|
1263
|
+
}
|
|
1010
1264
|
async function cmdRestart(rest) {
|
|
1011
1265
|
const argv = await yargs(rest).usage("Usage: ay restart <keyword>").option("latest", {
|
|
1012
1266
|
type: "boolean",
|
|
@@ -1192,5 +1446,5 @@ async function cmdStatus(rest) {
|
|
|
1192
1446
|
}
|
|
1193
1447
|
|
|
1194
1448
|
//#endregion
|
|
1195
|
-
export {
|
|
1196
|
-
//# sourceMappingURL=subcommands-
|
|
1449
|
+
export { isSubcommand as a, readNotes as c, runSubcommand as d, snapshotStatus as f, isPidAlive as i, renderRawLog as l, writeToIpc as m, cmdHelp as n, listRecords as o, stopTipForCli as p, controlCodeFromName as r, matchKeyword as s, GRACEFUL_EXIT_COMMANDS as t, resolveOne as u };
|
|
1450
|
+
//# sourceMappingURL=subcommands-LRmJ4_iJ.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-Go0xdcmN.js";
|
|
3
3
|
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-C22d9SRJ.js";
|
|
4
4
|
import { t as PidStore } from "./pidStore-C1JXxoPi.js";
|
|
5
5
|
import { r as readGlobalPids } from "./globalPidIndex-Cr-g75QF.js";
|
|
@@ -1693,4 +1693,4 @@ function sleep(ms) {
|
|
|
1693
1693
|
|
|
1694
1694
|
//#endregion
|
|
1695
1695
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1696
|
-
//# sourceMappingURL=ts-
|
|
1696
|
+
//# sourceMappingURL=ts-T6n6lrQG.js.map
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
//#region package.json
|
|
9
9
|
var name = "claude-yes";
|
|
10
|
-
var version = "1.
|
|
10
|
+
var version = "1.90.1";
|
|
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-Go0xdcmN.js.map
|
package/package.json
CHANGED
package/ts/subcommands.spec.ts
CHANGED
|
@@ -36,6 +36,8 @@ describe("subcommands.controlCodeFromName", () => {
|
|
|
36
36
|
expect(controlCodeFromName("ctrl-c")).toBe("\x03");
|
|
37
37
|
expect(controlCodeFromName("ctrl-y")).toBe("\x19");
|
|
38
38
|
expect(controlCodeFromName("ctrl-d")).toBe("\x04");
|
|
39
|
+
expect(controlCodeFromName("ctrl-\\")).toBe("\x1c");
|
|
40
|
+
expect(controlCodeFromName("ctrl-backslash")).toBe("\x1c");
|
|
39
41
|
expect(controlCodeFromName("tab")).toBe("\t");
|
|
40
42
|
expect(controlCodeFromName("none")).toBe("");
|
|
41
43
|
expect(controlCodeFromName("")).toBe("");
|
|
@@ -53,6 +55,43 @@ describe("subcommands.controlCodeFromName", () => {
|
|
|
53
55
|
});
|
|
54
56
|
});
|
|
55
57
|
|
|
58
|
+
describe("subcommands.isSubcommand", () => {
|
|
59
|
+
it("recognises attach and stop alongside the existing subcommands", async () => {
|
|
60
|
+
const { isSubcommand } = await loadModule();
|
|
61
|
+
expect(isSubcommand("attach")).toBe(true);
|
|
62
|
+
expect(isSubcommand("stop")).toBe(true);
|
|
63
|
+
expect(isSubcommand("tail")).toBe(true);
|
|
64
|
+
expect(isSubcommand("send")).toBe(true);
|
|
65
|
+
expect(isSubcommand("not-a-command")).toBe(false);
|
|
66
|
+
expect(isSubcommand(undefined)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("subcommands.stopTipForCli", () => {
|
|
71
|
+
it("returns a hint for CLIs that ignore single Ctrl+C", async () => {
|
|
72
|
+
const { stopTipForCli } = await loadModule();
|
|
73
|
+
expect(stopTipForCli("claude", 1234)).toMatch(/ay stop 1234/);
|
|
74
|
+
expect(stopTipForCli("claude", 1234)).toMatch(/\/exit/);
|
|
75
|
+
expect(stopTipForCli("codex", 99)).toMatch(/ay stop 99/);
|
|
76
|
+
expect(stopTipForCli("gemini", 7)).toMatch(/\/quit/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns null for CLIs without a known graceful command", async () => {
|
|
80
|
+
const { stopTipForCli } = await loadModule();
|
|
81
|
+
expect(stopTipForCli("qwen", 1)).toBeNull();
|
|
82
|
+
expect(stopTipForCli("copilot", 1)).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("subcommands.GRACEFUL_EXIT_COMMANDS", () => {
|
|
87
|
+
it("maps the three known CLIs to their /exit-style commands", async () => {
|
|
88
|
+
const { GRACEFUL_EXIT_COMMANDS } = await loadModule();
|
|
89
|
+
expect(GRACEFUL_EXIT_COMMANDS["claude"]).toBe("/exit");
|
|
90
|
+
expect(GRACEFUL_EXIT_COMMANDS["codex"]).toBe("/exit");
|
|
91
|
+
expect(GRACEFUL_EXIT_COMMANDS["gemini"]).toBe("/quit");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
56
95
|
describe("subcommands.matchKeyword", () => {
|
|
57
96
|
const baseRecord = {
|
|
58
97
|
pid: 1234,
|
package/ts/subcommands.ts
CHANGED
|
@@ -136,6 +136,8 @@ const SUBCOMMANDS = new Set([
|
|
|
136
136
|
"tail",
|
|
137
137
|
"head",
|
|
138
138
|
"send",
|
|
139
|
+
"attach",
|
|
140
|
+
"stop",
|
|
139
141
|
"restart",
|
|
140
142
|
"note",
|
|
141
143
|
"serve",
|
|
@@ -176,6 +178,10 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
176
178
|
return await cmdRead(rest, { mode: "head" });
|
|
177
179
|
case "send":
|
|
178
180
|
return await cmdSend(rest);
|
|
181
|
+
case "attach":
|
|
182
|
+
return await cmdAttach(rest);
|
|
183
|
+
case "stop":
|
|
184
|
+
return await cmdStop(rest);
|
|
179
185
|
case "restart":
|
|
180
186
|
return await cmdRestart(rest);
|
|
181
187
|
case "note":
|
|
@@ -214,6 +220,8 @@ export function cmdHelp(): number {
|
|
|
214
220
|
` ay cat <keyword> full log\n` +
|
|
215
221
|
` ay head <keyword> first N lines\n` +
|
|
216
222
|
` ay send <keyword> <msg> send a message\n` +
|
|
223
|
+
` ay attach <keyword> interactive attach (detach: Ctrl-\\)\n` +
|
|
224
|
+
` ay stop <keyword> graceful shutdown (/exit for claude/codex)\n` +
|
|
217
225
|
` ay status <keyword> agent status snapshot\n` +
|
|
218
226
|
`\n` +
|
|
219
227
|
`Remote:\n` +
|
|
@@ -228,7 +236,7 @@ export function cmdHelp(): number {
|
|
|
228
236
|
` ay claude -- "fix the bug in auth.ts"\n` +
|
|
229
237
|
` ay claude --help full agent-runner options\n` +
|
|
230
238
|
`\n` +
|
|
231
|
-
`Labs (examples
|
|
239
|
+
`Labs (examples at https://github.com/snomiao/agent-yes/tree/main/lab):\n` +
|
|
232
240
|
` local-role-play/ designer + builder on one machine\n` +
|
|
233
241
|
` http-remote/ ay serve remote access demo\n` +
|
|
234
242
|
` p2p-pairing/ libp2p P2P (needs: cargo build --features swarm)\n`,
|
|
@@ -1328,9 +1336,37 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
1328
1336
|
` ay tail ${record.pid} # watch output\n` +
|
|
1329
1337
|
` ay ls # list all agents\n`,
|
|
1330
1338
|
);
|
|
1339
|
+
if (codeName === "ctrl-c" || codeName === "ctrlc") {
|
|
1340
|
+
const tip = stopTipForCli(record.cli, record.pid);
|
|
1341
|
+
if (tip) process.stderr.write(tip);
|
|
1342
|
+
}
|
|
1331
1343
|
return 0;
|
|
1332
1344
|
}
|
|
1333
1345
|
|
|
1346
|
+
/// CLIs that ignore a single Ctrl+C and need a more specific shutdown signal.
|
|
1347
|
+
/// Users hit this every time they try `ay send <pid> "" --code=ctrl-c` and
|
|
1348
|
+
/// see no effect — print a one-liner pointing them at `ay stop`.
|
|
1349
|
+
export function stopTipForCli(cli: string, pid: number): string | null {
|
|
1350
|
+
const cmd = GRACEFUL_EXIT_COMMANDS[cli];
|
|
1351
|
+
if (cmd) {
|
|
1352
|
+
return ` tip: ${cli} ignores a single Ctrl+C — try 'ay stop ${pid}' (sends '${cmd}') or double Ctrl+C.\n`;
|
|
1353
|
+
}
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/// Per-CLI graceful shutdown commands. Empty fallback = use double Ctrl+C.
|
|
1358
|
+
/// Verified against current upstream CLIs:
|
|
1359
|
+
/// claude — `/exit`
|
|
1360
|
+
/// codex — `/exit`
|
|
1361
|
+
/// gemini — `/quit`
|
|
1362
|
+
/// Other CLIs aren't in the table because their reliable graceful-exit
|
|
1363
|
+
/// command isn't well-known here; `ay stop` falls back to double Ctrl+C.
|
|
1364
|
+
export const GRACEFUL_EXIT_COMMANDS: Record<string, string> = {
|
|
1365
|
+
claude: "/exit",
|
|
1366
|
+
codex: "/exit",
|
|
1367
|
+
gemini: "/quit",
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1334
1370
|
export function controlCodeFromName(name: string): string {
|
|
1335
1371
|
switch (name) {
|
|
1336
1372
|
case "enter":
|
|
@@ -1349,6 +1385,13 @@ export function controlCodeFromName(name: string): string {
|
|
|
1349
1385
|
case "ctrl-d":
|
|
1350
1386
|
case "ctrld":
|
|
1351
1387
|
return "\x04";
|
|
1388
|
+
case "ctrl-\\":
|
|
1389
|
+
case "ctrl\\":
|
|
1390
|
+
case "ctrl-backslash":
|
|
1391
|
+
// FS (file separator); convenient detach key for `ay attach`
|
|
1392
|
+
// because few CLIs send it. Same as SIGQUIT's terminal binding,
|
|
1393
|
+
// but here it's intercepted before reaching any signal handler.
|
|
1394
|
+
return "\x1c";
|
|
1352
1395
|
case "tab":
|
|
1353
1396
|
return "\t";
|
|
1354
1397
|
case "none":
|
|
@@ -1393,6 +1436,325 @@ export async function writeToIpc(ipcPath: string, payload: string): Promise<void
|
|
|
1393
1436
|
}
|
|
1394
1437
|
}
|
|
1395
1438
|
|
|
1439
|
+
// ---------------------------------------------------------------------------
|
|
1440
|
+
// ay stop
|
|
1441
|
+
// ---------------------------------------------------------------------------
|
|
1442
|
+
|
|
1443
|
+
async function cmdStop(rest: string[]): Promise<number> {
|
|
1444
|
+
const y = yargs(rest)
|
|
1445
|
+
.usage("Usage: ay stop <keyword> [--method=graceful|double-ctrl-c|auto]")
|
|
1446
|
+
.option("method", {
|
|
1447
|
+
type: "string",
|
|
1448
|
+
default: "auto",
|
|
1449
|
+
description:
|
|
1450
|
+
"Shutdown strategy: auto (per-CLI), graceful (/exit-style), double-ctrl-c (force)",
|
|
1451
|
+
})
|
|
1452
|
+
.option("all", { type: "boolean", default: false, description: "Include exited agents" })
|
|
1453
|
+
.option("latest", { type: "boolean", default: false, description: "Use most recent match" })
|
|
1454
|
+
.option("cwd", { type: "string", description: "Restrict to agents under this dir" })
|
|
1455
|
+
.help(false)
|
|
1456
|
+
.version(false)
|
|
1457
|
+
.exitProcess(false);
|
|
1458
|
+
|
|
1459
|
+
const argv = await y.parseAsync();
|
|
1460
|
+
const opts: CommonOpts = {
|
|
1461
|
+
all: argv.all,
|
|
1462
|
+
active: false,
|
|
1463
|
+
json: false,
|
|
1464
|
+
latest: argv.latest,
|
|
1465
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
1466
|
+
};
|
|
1467
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
1468
|
+
if (!keyword) throw new Error("usage: ay stop <keyword> [--method=auto|graceful|double-ctrl-c]");
|
|
1469
|
+
|
|
1470
|
+
const record = await resolveOne(keyword, opts);
|
|
1471
|
+
if (!record.fifo_file) {
|
|
1472
|
+
throw new Error(`pid ${record.pid}: no fifo_file — cannot send shutdown command`);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const method = String(argv.method).toLowerCase();
|
|
1476
|
+
const graceful = GRACEFUL_EXIT_COMMANDS[record.cli];
|
|
1477
|
+
|
|
1478
|
+
let payload: string;
|
|
1479
|
+
let strategy: string;
|
|
1480
|
+
if (method === "double-ctrl-c") {
|
|
1481
|
+
payload = "double-ctrl-c";
|
|
1482
|
+
strategy = `double Ctrl+C (forced)`;
|
|
1483
|
+
} else if (method === "graceful" || (method === "auto" && graceful)) {
|
|
1484
|
+
if (!graceful) {
|
|
1485
|
+
throw new Error(`--method=graceful: no known graceful-exit command for cli "${record.cli}"`);
|
|
1486
|
+
}
|
|
1487
|
+
payload = graceful;
|
|
1488
|
+
strategy = `'${graceful}' + Enter`;
|
|
1489
|
+
} else if (method === "auto") {
|
|
1490
|
+
payload = "double-ctrl-c";
|
|
1491
|
+
strategy = `double Ctrl+C (no known /exit for cli "${record.cli}")`;
|
|
1492
|
+
} else {
|
|
1493
|
+
throw new Error(`unknown --method=${method}`);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
const fifoPath = record.fifo_file;
|
|
1497
|
+
if (payload === "double-ctrl-c") {
|
|
1498
|
+
await writeToIpc(fifoPath, "\x03");
|
|
1499
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1500
|
+
await writeToIpc(fifoPath, "\x03");
|
|
1501
|
+
} else {
|
|
1502
|
+
await writeToIpc(fifoPath, payload);
|
|
1503
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
1504
|
+
await writeToIpc(fifoPath, "\r");
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
process.stdout.write(`stopping pid ${record.pid} (${record.cli}) via ${strategy}\n`);
|
|
1508
|
+
process.stderr.write(
|
|
1509
|
+
`\n` +
|
|
1510
|
+
` ay status ${record.pid} # confirm it exited\n` +
|
|
1511
|
+
` ay ls --all # see exit codes\n`,
|
|
1512
|
+
);
|
|
1513
|
+
return 0;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// ---------------------------------------------------------------------------
|
|
1517
|
+
// ay attach
|
|
1518
|
+
// ---------------------------------------------------------------------------
|
|
1519
|
+
|
|
1520
|
+
async function cmdAttach(rest: string[]): Promise<number> {
|
|
1521
|
+
const y = yargs(rest)
|
|
1522
|
+
.usage("Usage: ay attach <keyword> [--escape ctrl-\\]")
|
|
1523
|
+
.option("escape", {
|
|
1524
|
+
type: "string",
|
|
1525
|
+
default: "ctrl-\\",
|
|
1526
|
+
description: "Detach key name (see --code list; default: ctrl-\\)",
|
|
1527
|
+
})
|
|
1528
|
+
.option("all", { type: "boolean", default: false, description: "Include exited agents" })
|
|
1529
|
+
.option("latest", { type: "boolean", default: false, description: "Use most recent match" })
|
|
1530
|
+
.option("cwd", { type: "string", description: "Restrict to agents under this dir" })
|
|
1531
|
+
.help(false)
|
|
1532
|
+
.version(false)
|
|
1533
|
+
.exitProcess(false);
|
|
1534
|
+
|
|
1535
|
+
const argv = await y.parseAsync();
|
|
1536
|
+
const opts: CommonOpts = {
|
|
1537
|
+
all: argv.all,
|
|
1538
|
+
active: false,
|
|
1539
|
+
json: false,
|
|
1540
|
+
latest: argv.latest,
|
|
1541
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
1542
|
+
};
|
|
1543
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
1544
|
+
if (!keyword) throw new Error("usage: ay attach <keyword> [--escape ctrl-\\]");
|
|
1545
|
+
|
|
1546
|
+
const escapeName = String(argv.escape).toLowerCase();
|
|
1547
|
+
const detachSeq = controlCodeFromName(escapeName);
|
|
1548
|
+
if (!detachSeq) {
|
|
1549
|
+
throw new Error(`--escape must resolve to a non-empty byte sequence (got "${argv.escape}")`);
|
|
1550
|
+
}
|
|
1551
|
+
const detachByte = detachSeq.charCodeAt(0);
|
|
1552
|
+
|
|
1553
|
+
const record = await resolveOne(keyword, opts);
|
|
1554
|
+
if (!record.fifo_file) {
|
|
1555
|
+
throw new Error(`pid ${record.pid}: no fifo_file recorded — agent has no input channel`);
|
|
1556
|
+
}
|
|
1557
|
+
if (!record.log_file) {
|
|
1558
|
+
throw new Error(`pid ${record.pid}: no log_file recorded — cannot stream output`);
|
|
1559
|
+
}
|
|
1560
|
+
if (!isPidAlive(record.pid)) {
|
|
1561
|
+
throw new Error(`pid ${record.pid}: process is not alive`);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const fifoPath = record.fifo_file;
|
|
1565
|
+
const logPath = record.log_file;
|
|
1566
|
+
|
|
1567
|
+
// 1. Replay the current screen via @xterm/headless so the user sees a
|
|
1568
|
+
// coherent snapshot instead of half-frame ANSI garbage. Cap input bytes
|
|
1569
|
+
// so multi-MB logs don't stall the attach.
|
|
1570
|
+
const REPLAY_CAP_BYTES = 1024 * 1024;
|
|
1571
|
+
let initialOffset = 0;
|
|
1572
|
+
let replay = "";
|
|
1573
|
+
try {
|
|
1574
|
+
const st = await stat(logPath);
|
|
1575
|
+
initialOffset = Number(st.size);
|
|
1576
|
+
if (initialOffset > 0) {
|
|
1577
|
+
const readStart = Math.max(0, initialOffset - REPLAY_CAP_BYTES);
|
|
1578
|
+
const fh = await open(logPath, "r");
|
|
1579
|
+
try {
|
|
1580
|
+
const buf = Buffer.alloc(initialOffset - readStart);
|
|
1581
|
+
await fh.read(buf, 0, buf.length, readStart);
|
|
1582
|
+
const rows = process.stdout.rows ?? 50;
|
|
1583
|
+
replay = await renderRawLog(buf, { mode: "tail", n: rows });
|
|
1584
|
+
} finally {
|
|
1585
|
+
await fh.close();
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
} catch {
|
|
1589
|
+
/* log unreadable — show nothing */
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
process.stderr.write(
|
|
1593
|
+
`[attaching to pid ${record.pid}: ${record.cli} in ${shortenPath(record.cwd)}]\n` +
|
|
1594
|
+
`[detach: ${escapeName}]\n`,
|
|
1595
|
+
);
|
|
1596
|
+
if (replay) {
|
|
1597
|
+
process.stdout.write(replay);
|
|
1598
|
+
if (!replay.endsWith("\n")) process.stdout.write("\n");
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// 2. Push local winsize → ~/.agent-yes/winsize/<pid>, signal SIGWINCH so
|
|
1602
|
+
// the agent resizes its inner PTY before we start forwarding bytes.
|
|
1603
|
+
const ayHome = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
1604
|
+
const winsizeDir = path.join(ayHome, "winsize");
|
|
1605
|
+
await mkdir(winsizeDir, { recursive: true });
|
|
1606
|
+
const winsizePath = path.join(winsizeDir, String(record.pid));
|
|
1607
|
+
|
|
1608
|
+
const sendResize = async () => {
|
|
1609
|
+
const cols = process.stdout.columns ?? 80;
|
|
1610
|
+
const rows = process.stdout.rows ?? 24;
|
|
1611
|
+
try {
|
|
1612
|
+
await writeFile(winsizePath, `${cols} ${rows} ${Date.now()}\n`);
|
|
1613
|
+
try {
|
|
1614
|
+
process.kill(record.pid, "SIGWINCH");
|
|
1615
|
+
} catch {
|
|
1616
|
+
/* agent died — handled by alive check */
|
|
1617
|
+
}
|
|
1618
|
+
} catch {
|
|
1619
|
+
/* ignore */
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
await sendResize();
|
|
1623
|
+
await new Promise((r) => setTimeout(r, 50)); // let agent redraw
|
|
1624
|
+
|
|
1625
|
+
// 3. Raw TTY so per-keystroke bytes flow through unchanged.
|
|
1626
|
+
const stdinIsTty = !!process.stdin.isTTY;
|
|
1627
|
+
if (stdinIsTty) {
|
|
1628
|
+
try {
|
|
1629
|
+
process.stdin.setRawMode(true);
|
|
1630
|
+
} catch {
|
|
1631
|
+
/* ignore */
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
process.stdin.resume();
|
|
1635
|
+
|
|
1636
|
+
const onResize = () => {
|
|
1637
|
+
void sendResize();
|
|
1638
|
+
};
|
|
1639
|
+
process.stdout.on("resize", onResize);
|
|
1640
|
+
|
|
1641
|
+
// 4. Keep FIFO open across keystrokes so we don't pay open(2) per byte.
|
|
1642
|
+
// Agent's RDWR keepalive means O_WRONLY does not block here.
|
|
1643
|
+
const { openSync, writeSync, closeSync, watch } = await import("fs");
|
|
1644
|
+
let fifoFd: number | null = null;
|
|
1645
|
+
try {
|
|
1646
|
+
fifoFd = openSync(fifoPath, "w");
|
|
1647
|
+
} catch (err) {
|
|
1648
|
+
throw new Error(`failed to open FIFO ${fifoPath}: ${(err as Error).message}`);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// 5. Stream new log bytes → stdout. fs.watch may coalesce on macOS, so
|
|
1652
|
+
// poll every 100ms as a safety net.
|
|
1653
|
+
let offset = initialOffset;
|
|
1654
|
+
let detached = false;
|
|
1655
|
+
let pollTimer: NodeJS.Timeout | undefined;
|
|
1656
|
+
let aliveCheck: NodeJS.Timeout | undefined;
|
|
1657
|
+
|
|
1658
|
+
const flushNew = async () => {
|
|
1659
|
+
if (detached) return;
|
|
1660
|
+
try {
|
|
1661
|
+
const st = await stat(logPath);
|
|
1662
|
+
if (st.size < offset) offset = 0; // truncated
|
|
1663
|
+
if (st.size > offset) {
|
|
1664
|
+
const fh = await open(logPath, "r");
|
|
1665
|
+
try {
|
|
1666
|
+
const buf = Buffer.alloc(st.size - offset);
|
|
1667
|
+
await fh.read(buf, 0, buf.length, offset);
|
|
1668
|
+
process.stdout.write(buf);
|
|
1669
|
+
offset = st.size;
|
|
1670
|
+
} finally {
|
|
1671
|
+
await fh.close();
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
} catch {
|
|
1675
|
+
/* transient — retry */
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
|
|
1679
|
+
const watcher = watch(logPath, () => {
|
|
1680
|
+
void flushNew();
|
|
1681
|
+
});
|
|
1682
|
+
// Race fix: bytes can land between stat() above and watch() install.
|
|
1683
|
+
await flushNew();
|
|
1684
|
+
pollTimer = setInterval(() => {
|
|
1685
|
+
void flushNew();
|
|
1686
|
+
}, 100);
|
|
1687
|
+
|
|
1688
|
+
// 6. Stdin → FIFO, watching for detach byte.
|
|
1689
|
+
const triggerDetach = () => {
|
|
1690
|
+
if (detached) return;
|
|
1691
|
+
detached = true;
|
|
1692
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
1693
|
+
if (aliveCheck) clearInterval(aliveCheck);
|
|
1694
|
+
watcher.close();
|
|
1695
|
+
process.stdout.removeListener("resize", onResize);
|
|
1696
|
+
process.stdin.removeListener("data", onStdinData);
|
|
1697
|
+
if (stdinIsTty) {
|
|
1698
|
+
try {
|
|
1699
|
+
process.stdin.setRawMode(false);
|
|
1700
|
+
} catch {
|
|
1701
|
+
/* ignore */
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
process.stdin.pause();
|
|
1705
|
+
if (fifoFd !== null) {
|
|
1706
|
+
try {
|
|
1707
|
+
closeSync(fifoFd);
|
|
1708
|
+
} catch {
|
|
1709
|
+
/* ignore */
|
|
1710
|
+
}
|
|
1711
|
+
fifoFd = null;
|
|
1712
|
+
}
|
|
1713
|
+
process.stderr.write(`\n[detached from pid ${record.pid} — agent still running]\n`);
|
|
1714
|
+
};
|
|
1715
|
+
|
|
1716
|
+
const onStdinData = (chunk: Buffer) => {
|
|
1717
|
+
if (detached) return;
|
|
1718
|
+
const idx = chunk.indexOf(detachByte);
|
|
1719
|
+
if (idx === -1) {
|
|
1720
|
+
try {
|
|
1721
|
+
if (fifoFd !== null) writeSync(fifoFd, chunk);
|
|
1722
|
+
} catch (err) {
|
|
1723
|
+
process.stderr.write(`\n[fifo write failed: ${(err as Error).message}]\n`);
|
|
1724
|
+
triggerDetach();
|
|
1725
|
+
}
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
if (idx > 0 && fifoFd !== null) {
|
|
1729
|
+
try {
|
|
1730
|
+
writeSync(fifoFd, chunk.subarray(0, idx));
|
|
1731
|
+
} catch {
|
|
1732
|
+
/* ignore */
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
triggerDetach();
|
|
1736
|
+
};
|
|
1737
|
+
process.stdin.on("data", onStdinData);
|
|
1738
|
+
|
|
1739
|
+
// 7. Detach automatically if the agent exits.
|
|
1740
|
+
aliveCheck = setInterval(() => {
|
|
1741
|
+
if (!isPidAlive(record.pid)) {
|
|
1742
|
+
process.stderr.write(`\n[pid ${record.pid} exited]\n`);
|
|
1743
|
+
triggerDetach();
|
|
1744
|
+
}
|
|
1745
|
+
}, 1000);
|
|
1746
|
+
|
|
1747
|
+
await new Promise<void>((resolve) => {
|
|
1748
|
+
const tick = () => {
|
|
1749
|
+
if (detached) resolve();
|
|
1750
|
+
else setTimeout(tick, 50);
|
|
1751
|
+
};
|
|
1752
|
+
tick();
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
return 0;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1396
1758
|
// ---------------------------------------------------------------------------
|
|
1397
1759
|
// ay restart
|
|
1398
1760
|
// ---------------------------------------------------------------------------
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import "./logger-B9h0djqx.js";
|
|
2
|
-
import "./globalPidIndex-Cr-g75QF.js";
|
|
3
|
-
import "./remotes-Bjp2GYPz.js";
|
|
4
|
-
import { a as listRecords, c as renderRawLog, d as snapshotStatus, f as writeToIpc, i as isSubcommand, l as resolveOne, n as controlCodeFromName, o as matchKeyword, r as isPidAlive, s as readNotes, t as cmdHelp, u as runSubcommand } from "./subcommands-Czbgwuy-.js";
|
|
5
|
-
|
|
6
|
-
export { cmdHelp, isSubcommand, runSubcommand };
|