claude-yes 1.89.0 → 1.90.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 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-CTqyDzUi.js";
1
+ import { t as CLIS_CONFIG } from "./ts-D1iQ6T16.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-puPia13W.js";
3
+ import "./versionChecker-BK69HagP.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-yRq_h-h6.js.map
12
+ //# sourceMappingURL=SUPPORTED_CLIS-BYNrRXLa.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-puPia13W.js";
3
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-BK69HagP.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-DIxUhK0D.js");
485
+ const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-Bz2WIEDl.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-yRq_h-h6.js");
518
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-BYNrRXLa.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-CTqyDzUi.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-D1iQ6T16.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-puPia13W.js";
3
+ import "./versionChecker-BK69HagP.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 { a as listRecords, c as renderRawLog, d as snapshotStatus, f as writeToIpc, l as resolveOne, n as controlCodeFromName, s as readNotes } from "./subcommands-Czbgwuy-.js";
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-DUUMZWHZ.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-CHhrOT6F.js.map
315
+ //# sourceMappingURL=serve-CKVANskv.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-DUUMZWHZ.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-CHhrOT6F.js");
165
+ const { cmdServe } = await import("./serve-CKVANskv.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 in ./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");
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 in ./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 { listRecords as a, renderRawLog as c, snapshotStatus as d, writeToIpc as f, isSubcommand as i, resolveOne as l, controlCodeFromName as n, matchKeyword as o, isPidAlive as r, readNotes as s, cmdHelp as t, runSubcommand as u };
1196
- //# sourceMappingURL=subcommands-Czbgwuy-.js.map
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-DUUMZWHZ.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-puPia13W.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-BK69HagP.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-CTqyDzUi.js.map
1696
+ //# sourceMappingURL=ts-D1iQ6T16.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.89.0";
10
+ var version = "1.90.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-puPia13W.js.map
224
+ //# sourceMappingURL=versionChecker-BK69HagP.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-yes",
3
- "version": "1.89.0",
3
+ "version": "1.90.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",
@@ -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` +
@@ -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 };