claude-yes 1.164.0 → 1.165.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-C_J_okdw.js → SUPPORTED_CLIS-H7IIw10E.js} +2 -2
- package/dist/SUPPORTED_CLIS-dBgLp3mi.js +8 -0
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{schedule-B-Ncgklt.js → schedule-BobQwIBS.js} +4 -4
- package/dist/{serve-yVh9Y6ot.js → serve-DzOWMI_8.js} +5 -5
- package/dist/{setup-BmYrvUsz.js → setup-tVfp7djj.js} +2 -2
- package/dist/subcommands-CIFWi9vq.js +9 -0
- package/dist/{subcommands-BbKngO7F.js → subcommands-DtwxPMYe.js} +227 -17
- package/dist/{ts-BzyVSzo1.js → ts-jDEwTsVt.js} +2 -2
- package/dist/{versionChecker-CCHuozmQ.js → versionChecker-C0UJyFcN.js} +2 -2
- package/package.json +1 -1
- package/ts/needsInput.spec.ts +42 -1
- package/ts/needsInput.ts +54 -0
- package/ts/subcommands.spec.ts +33 -1
- package/ts/subcommands.ts +308 -31
- package/dist/SUPPORTED_CLIS-CmJam_wA.js +0 -8
- package/dist/subcommands-C-aCmtre.js +0 -9
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-jDEwTsVt.js";
|
|
2
2
|
|
|
3
3
|
//#region ts/SUPPORTED_CLIS.ts
|
|
4
4
|
const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
5
5
|
|
|
6
6
|
//#endregion
|
|
7
7
|
export { SUPPORTED_CLIS as t };
|
|
8
|
-
//# sourceMappingURL=SUPPORTED_CLIS-
|
|
8
|
+
//# sourceMappingURL=SUPPORTED_CLIS-H7IIw10E.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./ts-jDEwTsVt.js";
|
|
2
|
+
import "./logger-CDIsZ-Pp.js";
|
|
3
|
+
import "./versionChecker-C0UJyFcN.js";
|
|
4
|
+
import "./pidStore-BfoBWUjc.js";
|
|
5
|
+
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-H7IIw10E.js";
|
|
7
|
+
|
|
8
|
+
export { SUPPORTED_CLIS };
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
|
|
3
3
|
import { n as logger } from "./logger-CDIsZ-Pp.js";
|
|
4
|
-
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-
|
|
4
|
+
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-C0UJyFcN.js";
|
|
5
5
|
import { argv } from "process";
|
|
6
6
|
import { execFileSync, spawn } from "child_process";
|
|
7
7
|
import ms from "ms";
|
|
@@ -481,7 +481,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
481
481
|
const rawArg = process.argv[2];
|
|
482
482
|
const managerCommands = !invokedCliName(process.argv);
|
|
483
483
|
const isHelpFlag = rawArg === "-h" || rawArg === "--help";
|
|
484
|
-
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-
|
|
484
|
+
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-CIFWi9vq.js");
|
|
485
485
|
if (isHelpFlag && process.argv.length === 3) {
|
|
486
486
|
cmdHelp(managerCommands);
|
|
487
487
|
process.exit(0);
|
|
@@ -514,7 +514,7 @@ if (config.useRust) {
|
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
516
|
if (rustBinary) {
|
|
517
|
-
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-
|
|
517
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-dBgLp3mi.js");
|
|
518
518
|
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
519
519
|
if (config.verbose) {
|
|
520
520
|
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-jDEwTsVt.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-C0UJyFcN.js";
|
|
4
4
|
import "./pidStore-BfoBWUjc.js";
|
|
5
5
|
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
6
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-jDEwTsVt.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-C0UJyFcN.js";
|
|
4
4
|
import "./pidStore-BfoBWUjc.js";
|
|
5
5
|
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-H7IIw10E.js";
|
|
7
7
|
import { o as resolveSpawnCwd } from "./workspaceConfig-B0Q9-q2B.js";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
|
|
@@ -141,4 +141,4 @@ async function cmdSchedule(rest) {
|
|
|
141
141
|
|
|
142
142
|
//#endregion
|
|
143
143
|
export { cmdSchedule };
|
|
144
|
-
//# sourceMappingURL=schedule-
|
|
144
|
+
//# sourceMappingURL=schedule-BobQwIBS.js.map
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-jDEwTsVt.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
3
|
+
import { r as getInstalledPackage } from "./versionChecker-C0UJyFcN.js";
|
|
4
4
|
import "./pidStore-BfoBWUjc.js";
|
|
5
5
|
import { a as updateGlobalPidStatus } from "./globalPidIndex-DlmmJlO8.js";
|
|
6
6
|
import { t as pgidForWrapper } from "./reaper-C-eWAxIj.js";
|
|
7
7
|
import "./configShared-CEyhl0WH.js";
|
|
8
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
8
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-H7IIw10E.js";
|
|
9
9
|
import "./e2e-Bfw7qL9O.js";
|
|
10
10
|
import "./webrtcLink-CBZkZ-LT.js";
|
|
11
11
|
import "./remotes-DBCvpp3B.js";
|
|
12
12
|
import { a as isProvisionAllowed, i as hasSpawnHook, n as getSpawnHook, o as resolveSpawnCwd, t as getProvisionRoot } from "./workspaceConfig-B0Q9-q2B.js";
|
|
13
|
-
import {
|
|
13
|
+
import { A as writeToIpc, D as snapshotStatus, S as resolveOne, b as renderRawLog, c as extractNeedsInput, h as listRecords, i as controlCodeFromName, l as extractTaskCounts, o as deriveLiveStatus, v as readNotes, y as readPtysize } from "./subcommands-DtwxPMYe.js";
|
|
14
14
|
import yargs from "yargs";
|
|
15
15
|
import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
|
|
16
16
|
import { homedir, hostname, userInfo } from "os";
|
|
@@ -1442,4 +1442,4 @@ Options:
|
|
|
1442
1442
|
|
|
1443
1443
|
//#endregion
|
|
1444
1444
|
export { cmdServe };
|
|
1445
|
-
//# sourceMappingURL=serve-
|
|
1445
|
+
//# sourceMappingURL=serve-DzOWMI_8.js.map
|
|
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
|
|
|
32
32
|
if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
|
|
33
33
|
if (noShare) return 0;
|
|
34
34
|
process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
|
|
35
|
-
const { cmdServe } = await import("./serve-
|
|
35
|
+
const { cmdServe } = await import("./serve-DzOWMI_8.js");
|
|
36
36
|
return cmdServe([
|
|
37
37
|
"install",
|
|
38
38
|
"--share",
|
|
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
|
|
|
42
42
|
|
|
43
43
|
//#endregion
|
|
44
44
|
export { cmdSetup };
|
|
45
|
-
//# sourceMappingURL=setup-
|
|
45
|
+
//# sourceMappingURL=setup-tVfp7djj.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "./logger-CDIsZ-Pp.js";
|
|
2
|
+
import "./globalPidIndex-DlmmJlO8.js";
|
|
3
|
+
import "./configShared-CEyhl0WH.js";
|
|
4
|
+
import "./e2e-Bfw7qL9O.js";
|
|
5
|
+
import "./webrtcLink-CBZkZ-LT.js";
|
|
6
|
+
import "./remotes-DBCvpp3B.js";
|
|
7
|
+
import { A as writeToIpc, C as resolveReadWindow, D as snapshotStatus, E as runSubcommand, O as stopTipForCli, S as resolveOne, T as restartHintLines, _ as menuSelectKeys, a as cursorAbs, b as renderRawLog, c as extractNeedsInput, d as isAgentStuck, f as isExitRequest, g as matchKeyword, h as listRecords, i as controlCodeFromName, k as writeKeysPaced, l as extractTaskCounts, m as isSubcommand, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isPidAlive, r as cmdHelp, s as extractMenu, t as GRACEFUL_EXIT_COMMANDS, u as finalizedLines, v as readNotes, w as resolveResumeArgs, x as renderRawLogLines, y as readPtysize } from "./subcommands-DtwxPMYe.js";
|
|
8
|
+
|
|
9
|
+
export { cmdHelp, isSubcommand, runSubcommand };
|
|
@@ -183,6 +183,43 @@ function classifyNeedsInput(lines, cfg) {
|
|
|
183
183
|
const end = Math.min(lines.length, last + 6);
|
|
184
184
|
return { question: lines.slice(start, end).map((l) => l.trim()).filter((l) => l && !isChromeLine(l)).join(" • ").slice(0, 400) };
|
|
185
185
|
}
|
|
186
|
+
const OPTION_LINE = /^[\s❯›>▶◉○●·*\-]*?(\d+)\.\s/;
|
|
187
|
+
/**
|
|
188
|
+
* Parse the selection menu a `needs_input` agent is parked on into a cursor
|
|
189
|
+
* position + the available option numbers, so a caller can compute how far the
|
|
190
|
+
* cursor must move (Down/Up) to reach a target option. Returns null when the
|
|
191
|
+
* screen isn't a menu (delegates that judgement to {@link classifyNeedsInput},
|
|
192
|
+
* so `working` still wins) or no numbered cursor line is found. Pure — the
|
|
193
|
+
* `ay select` action reuses the exact detection `ay ls` renders with.
|
|
194
|
+
*/
|
|
195
|
+
function parseMenu(lines, cfg) {
|
|
196
|
+
const ni = classifyNeedsInput(lines, cfg);
|
|
197
|
+
if (!ni) return null;
|
|
198
|
+
const patterns = cfg.needsInput ?? [];
|
|
199
|
+
let cursorLine = -1;
|
|
200
|
+
for (let i = 0; i < lines.length; i++) if (patterns.some((re) => reTest(re, lines[i]))) cursorLine = i;
|
|
201
|
+
if (cursorLine < 0) return null;
|
|
202
|
+
const cm = /(\d+)\./.exec(lines[cursorLine]);
|
|
203
|
+
if (!cm) return null;
|
|
204
|
+
const cursor = parseInt(cm[1], 10);
|
|
205
|
+
const start = Math.max(0, cursorLine - 12);
|
|
206
|
+
const end = Math.min(lines.length, cursorLine + 12);
|
|
207
|
+
const options = [];
|
|
208
|
+
for (let i = start; i < end; i++) {
|
|
209
|
+
const m = OPTION_LINE.exec(lines[i]);
|
|
210
|
+
if (m) {
|
|
211
|
+
const v = parseInt(m[1], 10);
|
|
212
|
+
if (!options.includes(v)) options.push(v);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!options.includes(cursor)) options.push(cursor);
|
|
216
|
+
options.sort((a, b) => a - b);
|
|
217
|
+
return {
|
|
218
|
+
cursor,
|
|
219
|
+
options,
|
|
220
|
+
question: ni.question
|
|
221
|
+
};
|
|
222
|
+
}
|
|
186
223
|
/**
|
|
187
224
|
* True when the rendered screen still shows a "busy" marker (config `working`,
|
|
188
225
|
* e.g. claude's `esc to interrupt`). Paired with a long-quiet log this is the
|
|
@@ -500,6 +537,8 @@ const SUBCOMMANDS = new Set([
|
|
|
500
537
|
"tail",
|
|
501
538
|
"head",
|
|
502
539
|
"send",
|
|
540
|
+
"key",
|
|
541
|
+
"select",
|
|
503
542
|
"spawn",
|
|
504
543
|
"attach",
|
|
505
544
|
"stop",
|
|
@@ -549,6 +588,8 @@ async function runSubcommand(argv) {
|
|
|
549
588
|
case "tail": return await cmdRead(rest, { mode: "tail" });
|
|
550
589
|
case "head": return await cmdRead(rest, { mode: "head" });
|
|
551
590
|
case "send": return await cmdSend(rest);
|
|
591
|
+
case "key": return await cmdKey(rest);
|
|
592
|
+
case "select": return await cmdSelect(rest);
|
|
552
593
|
case "spawn": return await cmdSpawn(rest);
|
|
553
594
|
case "attach": return await cmdAttach(rest);
|
|
554
595
|
case "stop": return await cmdStop(rest);
|
|
@@ -556,15 +597,15 @@ async function runSubcommand(argv) {
|
|
|
556
597
|
case "restart": return await cmdRestart(rest);
|
|
557
598
|
case "note": return await cmdNote(rest);
|
|
558
599
|
case "serve": {
|
|
559
|
-
const { cmdServe } = await import("./serve-
|
|
600
|
+
const { cmdServe } = await import("./serve-DzOWMI_8.js");
|
|
560
601
|
return cmdServe(rest);
|
|
561
602
|
}
|
|
562
603
|
case "setup": {
|
|
563
|
-
const { cmdSetup } = await import("./setup-
|
|
604
|
+
const { cmdSetup } = await import("./setup-tVfp7djj.js");
|
|
564
605
|
return cmdSetup(rest);
|
|
565
606
|
}
|
|
566
607
|
case "schedule": {
|
|
567
|
-
const { cmdSchedule } = await import("./schedule-
|
|
608
|
+
const { cmdSchedule } = await import("./schedule-BobQwIBS.js");
|
|
568
609
|
return cmdSchedule(rest);
|
|
569
610
|
}
|
|
570
611
|
case "remote": {
|
|
@@ -585,7 +626,7 @@ async function runSubcommand(argv) {
|
|
|
585
626
|
}
|
|
586
627
|
function cmdHelp(managerCommands = true) {
|
|
587
628
|
const setupLine = managerCommands ? ` ay setup guided setup: pick a workspace, share to agent-yes.com\n` : ``;
|
|
588
|
-
process.stdout.write("ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay tail [-f] [-n N] <keyword> last N lines (96), -f to follow\n ay read <keyword> [page opts] paginate: --last/--head N, --range A:B,\n --before-line L [--limit N]\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 exit <keyword> [reason] graceful shutdown, recording who/why (= 'ay send <kw> exit')\n ay restart <keyword> [--fresh] stop (if live) + relaunch resuming the session; --fresh replays the prompt\n ay status <keyword> agent status snapshot\n ay result <keyword> [--wait] pull an agent's structured result envelope\n ay result set '<json>' (inside an agent) deposit your result envelope\n ay reap kill process groups leaked by dead agents\n\nRemote:\n" + setupLine + " ay schedule <when> <cli> -- <msg> run an agent on a schedule (HH:MM or cron)\n ay serve [--port N] start HTTP API server (prints token)\n ay serve status show serve daemon/server status\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");
|
|
629
|
+
process.stdout.write("ay - agent-yes CLI\n\nManagement:\n ay ls [keyword] list running agents\n ay tail [-f] [-n N] <keyword> last N lines (96), -f to follow\n ay read <keyword> [page opts] paginate: --last/--head N, --range A:B,\n --before-line L [--limit N]\n ay cat <keyword> full log\n ay head <keyword> first N lines\n ay send <keyword> <msg> send a message\n ay key <keyword> <key...> send raw keystrokes (down/up/enter/esc/…) — drives menus\n ay select <keyword> <N> pick option N of a needs_input selection menu\n ay attach <keyword> interactive attach (detach: Ctrl-\\)\n ay stop <keyword> graceful shutdown (/exit for claude/codex)\n ay exit <keyword> [reason] graceful shutdown, recording who/why (= 'ay send <kw> exit')\n ay restart <keyword> [--fresh] stop (if live) + relaunch resuming the session; --fresh replays the prompt\n ay status <keyword> agent status snapshot\n ay result <keyword> [--wait] pull an agent's structured result envelope\n ay result set '<json>' (inside an agent) deposit your result envelope\n ay reap kill process groups leaked by dead agents\n\nRemote:\n" + setupLine + " ay schedule <when> <cli> -- <msg> run an agent on a schedule (HH:MM or cron)\n ay serve [--port N] start HTTP API server (prints token)\n ay serve status show serve daemon/server status\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");
|
|
589
630
|
return 0;
|
|
590
631
|
}
|
|
591
632
|
function matchKeyword(record, keyword) {
|
|
@@ -1895,6 +1936,71 @@ async function cmdSpawn(rest) {
|
|
|
1895
1936
|
prompt: prompt || void 0
|
|
1896
1937
|
});
|
|
1897
1938
|
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Shared safety gate for every command that writes to a live agent's stdin
|
|
1941
|
+
* (`send`, `key`, `select`): refuse a self-targeting loop, and require that THIS
|
|
1942
|
+
* sender actually looked at THIS target recently — an agent is blocked, an
|
|
1943
|
+
* interactive human is only warned — unless `force`. Returns the sender context
|
|
1944
|
+
* so a caller can reuse it (e.g. `send`'s `[from …]` prefix). Extracted from
|
|
1945
|
+
* cmdSend so the action commands enforce the identical guard.
|
|
1946
|
+
*/
|
|
1947
|
+
async function enforceSendGuards(record, force) {
|
|
1948
|
+
const sender = await senderContext();
|
|
1949
|
+
if (sender.agent && sender.agent.pid === record.pid && !force) throw new Error(`refusing to send to yourself (pid ${record.pid}) — pass --force if you really mean it.`);
|
|
1950
|
+
const last = await lastReadAt(sender.key, record.pid);
|
|
1951
|
+
if (!(last !== null && Date.now() - last <= READ_WINDOW_MS) && !force) {
|
|
1952
|
+
const ago = last === null ? "never read" : `last read ${Math.round((Date.now() - last) / 1e3)}s ago`;
|
|
1953
|
+
const what = `pid ${record.pid} (${record.cli}, ${shortenPath(record.cwd)}) — ${ago}, not within ${READ_WINDOW_MS / 1e3}s`;
|
|
1954
|
+
if (sender.agent) throw new Error(`${what}.\n Confirm it's the right agent first: ay tail ${record.pid}\n then resend, or pass --force to override.`);
|
|
1955
|
+
process.stderr.write(`warning: ${what} — make sure this is the agent you meant (ay tail ${record.pid}).\n`);
|
|
1956
|
+
}
|
|
1957
|
+
return sender;
|
|
1958
|
+
}
|
|
1959
|
+
const KEY_PACE_MS = 40;
|
|
1960
|
+
/**
|
|
1961
|
+
* The named-key sequence that moves a menu cursor from `cursor` to option
|
|
1962
|
+
* `target` and confirms: |Δ| Downs (target below) or Ups (target above), then
|
|
1963
|
+
* Enter. Pure so the arrow arithmetic is unit-tested independent of any live PTY.
|
|
1964
|
+
*/
|
|
1965
|
+
function menuSelectKeys(cursor, target) {
|
|
1966
|
+
const delta = target - cursor;
|
|
1967
|
+
return [...Array(Math.abs(delta)).fill(delta > 0 ? "down" : "up"), "enter"];
|
|
1968
|
+
}
|
|
1969
|
+
/** Write each already-encoded key sequence to the FIFO with a pace gap between
|
|
1970
|
+
* them (no gap after the last). Raw bytes, no `[from]` framing, no auto-Enter. */
|
|
1971
|
+
async function writeKeysPaced(fifoPath, byteSeqs, paceMs) {
|
|
1972
|
+
for (let i = 0; i < byteSeqs.length; i++) {
|
|
1973
|
+
if (byteSeqs[i] === "") continue;
|
|
1974
|
+
await writeToIpc(fifoPath, byteSeqs[i]);
|
|
1975
|
+
if (i < byteSeqs.length - 1 && paceMs > 0) await new Promise((r) => setTimeout(r, paceMs));
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* The selection menu a needs_input agent is parked on, or null when it isn't on
|
|
1980
|
+
* one. Mirrors extractNeedsInput (same 32 KB tail render + config patterns) but
|
|
1981
|
+
* returns the cursor position + option numbers so `ay select` can compute the
|
|
1982
|
+
* cursor delta.
|
|
1983
|
+
*/
|
|
1984
|
+
async function extractMenu(logPath, cli) {
|
|
1985
|
+
const cfg = (await cliDefaults())[cli];
|
|
1986
|
+
if (!cfg?.needsInput?.length) return null;
|
|
1987
|
+
const lines = await renderLogTailLines(logPath, 40);
|
|
1988
|
+
if (!lines) return null;
|
|
1989
|
+
return parseMenu(lines, {
|
|
1990
|
+
needsInput: cfg.needsInput,
|
|
1991
|
+
working: cfg.working
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
/** Poll until the agent is no longer parked on a menu (selection accepted → it
|
|
1995
|
+
* resumed / moved on) or the deadline passes. Returns true if it cleared. */
|
|
1996
|
+
async function waitForNeedsInputClear(record, timeoutMs) {
|
|
1997
|
+
const deadline = Date.now() + timeoutMs;
|
|
1998
|
+
while (Date.now() < deadline) {
|
|
1999
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
2000
|
+
if ((await snapshotStatus(record)).state !== "needs_input") return true;
|
|
2001
|
+
}
|
|
2002
|
+
return false;
|
|
2003
|
+
}
|
|
1898
2004
|
async function cmdSend(rest) {
|
|
1899
2005
|
const argv = await yargs(rest).usage("Usage: ay send <keyword> <msg|-> [options]").option("code", {
|
|
1900
2006
|
type: "string",
|
|
@@ -1941,16 +2047,7 @@ async function cmdSend(rest) {
|
|
|
1941
2047
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1942
2048
|
body = Buffer.concat(chunks).toString("utf-8").trimEnd();
|
|
1943
2049
|
} else body = rawMessage;
|
|
1944
|
-
const sender = await
|
|
1945
|
-
const force = Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1";
|
|
1946
|
-
if (sender.agent && sender.agent.pid === record.pid && !force) throw new Error(`refusing to send to yourself (pid ${record.pid}) — pass --force if you really mean it.`);
|
|
1947
|
-
const last = await lastReadAt(sender.key, record.pid);
|
|
1948
|
-
if (!(last !== null && Date.now() - last <= READ_WINDOW_MS) && !force) {
|
|
1949
|
-
const ago = last === null ? "never read" : `last read ${Math.round((Date.now() - last) / 1e3)}s ago`;
|
|
1950
|
-
const what = `pid ${record.pid} (${record.cli}, ${shortenPath(record.cwd)}) — ${ago}, not within ${READ_WINDOW_MS / 1e3}s`;
|
|
1951
|
-
if (sender.agent) throw new Error(`${what}.\n Confirm it's the right agent first: ay tail ${record.pid}\n then resend, or pass --force to override.`);
|
|
1952
|
-
process.stderr.write(`warning: ${what} — make sure this is the agent you meant (ay tail ${record.pid}).\n`);
|
|
1953
|
-
}
|
|
2050
|
+
const sender = await enforceSendGuards(record, Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1");
|
|
1954
2051
|
if (isExitRequest(body)) {
|
|
1955
2052
|
const reason = sender.agent ? `requested by ${sender.agent.cli} #${sender.agent.pid} @ ${shortenPath(sender.agent.cwd)}` : `requested via 'ay send ${keyword} exit'`;
|
|
1956
2053
|
const { strategy } = await gracefulExitAgent(record, reason);
|
|
@@ -1973,6 +2070,104 @@ async function cmdSend(rest) {
|
|
|
1973
2070
|
}
|
|
1974
2071
|
return 0;
|
|
1975
2072
|
}
|
|
2073
|
+
async function resolveWritableAgent(keyword, opts) {
|
|
2074
|
+
const record = await resolveOne(keyword, opts);
|
|
2075
|
+
if (!record.fifo_file) throw new Error(`pid ${record.pid}: no fifo_file recorded — this agent didn't register a stdin FIFO (an older agent, or one not started with --stdpush). Restarting it (ay restart ${record.pid}) re-registers one.`);
|
|
2076
|
+
return record;
|
|
2077
|
+
}
|
|
2078
|
+
async function cmdKey(rest) {
|
|
2079
|
+
const argv = await yargs(rest).usage("Usage: ay key <keyword> <key...> [options]\n\nSend raw named keystrokes to a live agent's TUI — no message framing, no\nauto-Enter. Drives selection menus and other interactive prompts that a\nplain `ay send` (text + Enter) can't. Keys are paced so the CLI registers\neach as a discrete event, not a paste.\n\nKeys: up down left right enter esc tab space backspace delete home end\n pageup pagedown ctrl-c ctrl-d ctrl-y raw:0xNN\n\nExamples:\n ay key 1234 down down enter # move the menu cursor down twice, confirm\n ay key 1234 esc # dismiss a menu\n ay key 1234 raw:0x1b # a literal ESC byte").option("pace", {
|
|
2080
|
+
type: "number",
|
|
2081
|
+
default: KEY_PACE_MS,
|
|
2082
|
+
description: "ms between keystrokes"
|
|
2083
|
+
}).option("all", {
|
|
2084
|
+
type: "boolean",
|
|
2085
|
+
default: false,
|
|
2086
|
+
description: "Include exited agents"
|
|
2087
|
+
}).option("latest", {
|
|
2088
|
+
type: "boolean",
|
|
2089
|
+
default: false,
|
|
2090
|
+
description: "Use most recent match"
|
|
2091
|
+
}).option("cwd", {
|
|
2092
|
+
type: "string",
|
|
2093
|
+
description: "Restrict to agents under this dir"
|
|
2094
|
+
}).option("force", {
|
|
2095
|
+
type: "boolean",
|
|
2096
|
+
default: false,
|
|
2097
|
+
description: "Skip the recency/self-send guard (also: AGENT_YES_FORCE_SEND=1)"
|
|
2098
|
+
}).help(false).version(false).exitProcess(false).parseAsync();
|
|
2099
|
+
const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
|
|
2100
|
+
const keyNames = argv._.slice(1).map(String);
|
|
2101
|
+
if (!keyword || keyNames.length === 0) throw new Error("usage: ay key <keyword> <key...> (e.g. ay key 1234 down down enter)");
|
|
2102
|
+
const byteSeqs = keyNames.map((n) => controlCodeFromName(n.toLowerCase()));
|
|
2103
|
+
const record = await resolveWritableAgent(keyword, {
|
|
2104
|
+
all: argv.all,
|
|
2105
|
+
active: false,
|
|
2106
|
+
json: false,
|
|
2107
|
+
latest: argv.latest,
|
|
2108
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
|
|
2109
|
+
});
|
|
2110
|
+
await enforceSendGuards(record, Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1");
|
|
2111
|
+
await writeKeysPaced(record.fifo_file, byteSeqs, Math.max(0, argv.pace));
|
|
2112
|
+
process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${keyNames.join(" ")}\n`);
|
|
2113
|
+
return 0;
|
|
2114
|
+
}
|
|
2115
|
+
async function cmdSelect(rest) {
|
|
2116
|
+
const argv = await yargs(rest).usage("Usage: ay select <keyword> <N> [options]\n\nPick option N of the selection menu a needs_input agent is parked on.\nRe-parses the live menu (the same ❯-cursor detection `ay ls` uses), computes\nhow far the cursor must move, and sends that many Down/Up keys + Enter — so\nit's robust to a pre-highlighted default (never assumes the cursor starts at 1)\nand doesn't rely on numeric hotkeys (arrow-driven menus ignore them).\n\nExamples:\n ay select 1234 2 # choose option 2\n ay select 1234 2 --wait # …and block until the menu clears").option("pace", {
|
|
2117
|
+
type: "number",
|
|
2118
|
+
default: KEY_PACE_MS,
|
|
2119
|
+
description: "ms between keystrokes"
|
|
2120
|
+
}).option("wait", {
|
|
2121
|
+
type: "boolean",
|
|
2122
|
+
default: false,
|
|
2123
|
+
description: "Block until the agent leaves needs_input (or --timeout)"
|
|
2124
|
+
}).option("timeout", {
|
|
2125
|
+
type: "number",
|
|
2126
|
+
default: 10,
|
|
2127
|
+
description: "Seconds to wait with --wait"
|
|
2128
|
+
}).option("all", {
|
|
2129
|
+
type: "boolean",
|
|
2130
|
+
default: false,
|
|
2131
|
+
description: "Include exited agents"
|
|
2132
|
+
}).option("latest", {
|
|
2133
|
+
type: "boolean",
|
|
2134
|
+
default: false,
|
|
2135
|
+
description: "Use most recent match"
|
|
2136
|
+
}).option("cwd", {
|
|
2137
|
+
type: "string",
|
|
2138
|
+
description: "Restrict to agents under this dir"
|
|
2139
|
+
}).option("force", {
|
|
2140
|
+
type: "boolean",
|
|
2141
|
+
default: false,
|
|
2142
|
+
description: "Skip the recency/self-send guard (also: AGENT_YES_FORCE_SEND=1)"
|
|
2143
|
+
}).help(false).version(false).exitProcess(false).parseAsync();
|
|
2144
|
+
const keyword = argv._[0] !== void 0 ? String(argv._[0]) : void 0;
|
|
2145
|
+
const n = Number(argv._[1]);
|
|
2146
|
+
if (!keyword || !Number.isInteger(n) || n < 1) throw new Error("usage: ay select <keyword> <N> (N = the 1-based option number to choose)");
|
|
2147
|
+
const record = await resolveWritableAgent(keyword, {
|
|
2148
|
+
all: argv.all,
|
|
2149
|
+
active: false,
|
|
2150
|
+
json: false,
|
|
2151
|
+
latest: argv.latest,
|
|
2152
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
|
|
2153
|
+
});
|
|
2154
|
+
if (!record.log_file) throw new Error(`pid ${record.pid}: no log_file recorded — can't read the menu to select from.`);
|
|
2155
|
+
await enforceSendGuards(record, Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1");
|
|
2156
|
+
const menu = await extractMenu(record.log_file, record.cli);
|
|
2157
|
+
if (!menu) throw new Error(`pid ${record.pid} (${record.cli}) is not parked on a selection menu (not needs_input).\n Check with: ay status ${record.pid}`);
|
|
2158
|
+
if (menu.options.length > 0 && !menu.options.includes(n)) throw new Error(`option ${n} is out of range — this menu offers ${menu.options.join(", ")}.`);
|
|
2159
|
+
const byteSeqs = menuSelectKeys(menu.cursor, n).map((k) => controlCodeFromName(k));
|
|
2160
|
+
await writeKeysPaced(record.fifo_file, byteSeqs, Math.max(0, argv.pace));
|
|
2161
|
+
const delta = n - menu.cursor;
|
|
2162
|
+
const moved = delta === 0 ? "cursor already there" : `${Math.abs(delta)}× ${delta > 0 ? "down" : "up"}`;
|
|
2163
|
+
process.stdout.write(`pid ${record.pid} (${record.cli}): selected option ${n} (${moved} + enter)\n`);
|
|
2164
|
+
if (argv.wait) {
|
|
2165
|
+
const ok = await waitForNeedsInputClear(record, Math.max(1, argv.timeout) * 1e3);
|
|
2166
|
+
process.stdout.write(ok ? ` menu cleared — selection accepted.\n` : ` still needs_input after ${argv.timeout}s — re-check with 'ay status ${record.pid}'.\n`);
|
|
2167
|
+
return ok ? 0 : 1;
|
|
2168
|
+
}
|
|
2169
|
+
return 0;
|
|
2170
|
+
}
|
|
1976
2171
|
function stopTipForCli(cli, pid) {
|
|
1977
2172
|
const cmd = GRACEFUL_EXIT_COMMANDS[cli];
|
|
1978
2173
|
if (cmd) return ` tip: ${cli} ignores a single Ctrl+C — try 'ay stop ${pid}' (sends '${cmd}') or double Ctrl+C.\n`;
|
|
@@ -2000,12 +2195,27 @@ function controlCodeFromName(name) {
|
|
|
2000
2195
|
case "ctrl\\":
|
|
2001
2196
|
case "ctrl-backslash": return "";
|
|
2002
2197
|
case "tab": return " ";
|
|
2198
|
+
case "up": return "\x1B[A";
|
|
2199
|
+
case "down": return "\x1B[B";
|
|
2200
|
+
case "right": return "\x1B[C";
|
|
2201
|
+
case "left": return "\x1B[D";
|
|
2202
|
+
case "home": return "\x1B[H";
|
|
2203
|
+
case "end": return "\x1B[F";
|
|
2204
|
+
case "pageup":
|
|
2205
|
+
case "pgup": return "\x1B[5~";
|
|
2206
|
+
case "pagedown":
|
|
2207
|
+
case "pgdn": return "\x1B[6~";
|
|
2208
|
+
case "space": return " ";
|
|
2209
|
+
case "backspace":
|
|
2210
|
+
case "bs": return "";
|
|
2211
|
+
case "delete":
|
|
2212
|
+
case "del": return "\x1B[3~";
|
|
2003
2213
|
case "none":
|
|
2004
2214
|
case "": return "";
|
|
2005
2215
|
default:
|
|
2006
2216
|
const m = /^raw:0x([0-9a-f]+)$/i.exec(name);
|
|
2007
2217
|
if (m) return String.fromCharCode(parseInt(m[1], 16));
|
|
2008
|
-
throw new Error(`unknown
|
|
2218
|
+
throw new Error(`unknown key/code: ${name}`);
|
|
2009
2219
|
}
|
|
2010
2220
|
}
|
|
2011
2221
|
async function writeToIpc(ipcPath, payload) {
|
|
@@ -2794,5 +3004,5 @@ async function cmdResultSet(rest) {
|
|
|
2794
3004
|
}
|
|
2795
3005
|
|
|
2796
3006
|
//#endregion
|
|
2797
|
-
export {
|
|
2798
|
-
//# sourceMappingURL=subcommands-
|
|
3007
|
+
export { writeToIpc as A, resolveReadWindow as C, snapshotStatus as D, runSubcommand as E, stopTipForCli as O, resolveOne as S, restartHintLines as T, menuSelectKeys as _, cursorAbs as a, renderRawLog as b, extractNeedsInput as c, isAgentStuck as d, isExitRequest as f, matchKeyword as g, listRecords as h, controlCodeFromName as i, writeKeysPaced as k, extractTaskCounts as l, isSubcommand as m, READ_PAGE_DEFAULT as n, deriveLiveStatus as o, isPidAlive as p, cmdHelp as r, extractMenu as s, GRACEFUL_EXIT_COMMANDS as t, finalizedLines as u, readNotes as v, resolveResumeArgs as w, renderRawLogLines as x, readPtysize as y };
|
|
3008
|
+
//# sourceMappingURL=subcommands-DtwxPMYe.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-C0UJyFcN.js";
|
|
3
3
|
import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
|
|
4
4
|
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-V4qvXgAw.js";
|
|
5
5
|
import { t as PidStore } from "./pidStore-BfoBWUjc.js";
|
|
@@ -1803,4 +1803,4 @@ function sleep(ms) {
|
|
|
1803
1803
|
|
|
1804
1804
|
//#endregion
|
|
1805
1805
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1806
|
-
//# sourceMappingURL=ts-
|
|
1806
|
+
//# sourceMappingURL=ts-jDEwTsVt.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.165.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -215,4 +215,4 @@ async function displayVersion() {
|
|
|
215
215
|
|
|
216
216
|
//#endregion
|
|
217
217
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
218
|
-
//# sourceMappingURL=versionChecker-
|
|
218
|
+
//# sourceMappingURL=versionChecker-C0UJyFcN.js.map
|
package/package.json
CHANGED
package/ts/needsInput.spec.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "vitest";
|
|
2
|
-
import { classifyNeedsInput, isWorkingScreen } from "./needsInput.ts";
|
|
2
|
+
import { classifyNeedsInput, isWorkingScreen, parseMenu } from "./needsInput.ts";
|
|
3
3
|
import { loadSharedCliDefaults } from "./configShared.ts";
|
|
4
4
|
|
|
5
5
|
// Use the REAL shipped claude/codex patterns so the test guards the actual config.
|
|
@@ -72,3 +72,44 @@ test("detects a codex selection menu (› cursor)", () => {
|
|
|
72
72
|
test("no patterns configured → always null", () => {
|
|
73
73
|
expect(classifyNeedsInput(["❯ 1. anything"], { needsInput: [], working: [] })).toBeNull();
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
test("parseMenu: cursor on option 1, all options collected (for ay select)", () => {
|
|
77
|
+
const screen = [
|
|
78
|
+
"Which auth method should we use?",
|
|
79
|
+
"",
|
|
80
|
+
"❯ 1. Session tokens",
|
|
81
|
+
" 2. JWT",
|
|
82
|
+
" 3. OAuth",
|
|
83
|
+
"",
|
|
84
|
+
"? for shortcuts",
|
|
85
|
+
];
|
|
86
|
+
const menu = parseMenu(screen, claude);
|
|
87
|
+
expect(menu).not.toBeNull();
|
|
88
|
+
expect(menu!.cursor).toBe(1);
|
|
89
|
+
expect(menu!.options).toEqual([1, 2, 3]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("parseMenu: reads a pre-highlighted non-first default (cursor delta, not blind N-1)", () => {
|
|
93
|
+
const screen = ["Proceed?", " 1. Yes", "❯ 2. No, ask again", " 3. Cancel"];
|
|
94
|
+
const menu = parseMenu(screen, claude);
|
|
95
|
+
expect(menu!.cursor).toBe(2);
|
|
96
|
+
expect(menu!.options).toEqual([1, 2, 3]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("parseMenu: codex › cursor", () => {
|
|
100
|
+
const menu = parseMenu(["Pick a branch", " 1. main", "› 2. develop"], codex);
|
|
101
|
+
expect(menu!.cursor).toBe(2);
|
|
102
|
+
expect(menu!.options).toEqual([1, 2]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("parseMenu: null when not on a menu (idle prompt / working)", () => {
|
|
106
|
+
expect(parseMenu(['❯ Try "fix the bug"', "? for shortcuts"], claude)).toBeNull();
|
|
107
|
+
expect(parseMenu(["❯ 1. Session tokens", "✻ Working… (esc to interrupt)"], claude)).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("parseMenu: a 'N.M' version in an option label isn't mistaken for another option", () => {
|
|
111
|
+
const screen = ["Cleanup?", "❯ 1. Delete 3.5GB of cache", " 2. Keep"];
|
|
112
|
+
const menu = parseMenu(screen, claude);
|
|
113
|
+
expect(menu!.cursor).toBe(1);
|
|
114
|
+
expect(menu!.options).toEqual([1, 2]);
|
|
115
|
+
});
|
package/ts/needsInput.ts
CHANGED
|
@@ -67,6 +67,60 @@ export function classifyNeedsInput(
|
|
|
67
67
|
return { question: block.join(" • ").slice(0, 400) };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export interface MenuState {
|
|
71
|
+
/** The 1-based option number the menu cursor (❯/›/>) currently sits on. */
|
|
72
|
+
cursor: number;
|
|
73
|
+
/** Every visible option number, ascending — for range-checking a requested N. */
|
|
74
|
+
options: number[];
|
|
75
|
+
/** Same compact menu rendering as {@link classifyNeedsInput}. */
|
|
76
|
+
question: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// An option row: an optional cursor glyph / bullet, then "N. " (the trailing
|
|
80
|
+
// space rejects version-like "3.5GB" that isn't a menu option).
|
|
81
|
+
const OPTION_LINE = /^[\s❯›>▶◉○●·*\-]*?(\d+)\.\s/;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse the selection menu a `needs_input` agent is parked on into a cursor
|
|
85
|
+
* position + the available option numbers, so a caller can compute how far the
|
|
86
|
+
* cursor must move (Down/Up) to reach a target option. Returns null when the
|
|
87
|
+
* screen isn't a menu (delegates that judgement to {@link classifyNeedsInput},
|
|
88
|
+
* so `working` still wins) or no numbered cursor line is found. Pure — the
|
|
89
|
+
* `ay select` action reuses the exact detection `ay ls` renders with.
|
|
90
|
+
*/
|
|
91
|
+
export function parseMenu(
|
|
92
|
+
lines: string[],
|
|
93
|
+
cfg: { needsInput?: RegExp[]; working?: RegExp[] },
|
|
94
|
+
): MenuState | null {
|
|
95
|
+
const ni = classifyNeedsInput(lines, cfg);
|
|
96
|
+
if (!ni) return null;
|
|
97
|
+
const patterns = cfg.needsInput ?? [];
|
|
98
|
+
// The cursor line is the last one carrying a needsInput match (matches how
|
|
99
|
+
// classifyNeedsInput anchors its question window).
|
|
100
|
+
let cursorLine = -1;
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
if (patterns.some((re) => reTest(re, lines[i]!))) cursorLine = i;
|
|
103
|
+
}
|
|
104
|
+
if (cursorLine < 0) return null;
|
|
105
|
+
const cm = /(\d+)\./.exec(lines[cursorLine]!);
|
|
106
|
+
if (!cm) return null;
|
|
107
|
+
const cursor = parseInt(cm[1]!, 10);
|
|
108
|
+
// Gather option numbers from the rows around the cursor (a menu is contiguous).
|
|
109
|
+
const start = Math.max(0, cursorLine - 12);
|
|
110
|
+
const end = Math.min(lines.length, cursorLine + 12);
|
|
111
|
+
const options: number[] = [];
|
|
112
|
+
for (let i = start; i < end; i++) {
|
|
113
|
+
const m = OPTION_LINE.exec(lines[i]!);
|
|
114
|
+
if (m) {
|
|
115
|
+
const v = parseInt(m[1]!, 10);
|
|
116
|
+
if (!options.includes(v)) options.push(v);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!options.includes(cursor)) options.push(cursor);
|
|
120
|
+
options.sort((a, b) => a - b);
|
|
121
|
+
return { cursor, options, question: ni.question };
|
|
122
|
+
}
|
|
123
|
+
|
|
70
124
|
/**
|
|
71
125
|
* True when the rendered screen still shows a "busy" marker (config `working`,
|
|
72
126
|
* e.g. claude's `esc to interrupt`). Paired with a long-quiet log this is the
|
package/ts/subcommands.spec.ts
CHANGED
|
@@ -43,6 +43,18 @@ describe("subcommands.controlCodeFromName", () => {
|
|
|
43
43
|
expect(controlCodeFromName("")).toBe("");
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
it("maps navigation keys to their ANSI sequences (for ay key / ay select)", async () => {
|
|
47
|
+
const { controlCodeFromName } = await loadModule();
|
|
48
|
+
expect(controlCodeFromName("up")).toBe("\x1b[A");
|
|
49
|
+
expect(controlCodeFromName("down")).toBe("\x1b[B");
|
|
50
|
+
expect(controlCodeFromName("right")).toBe("\x1b[C");
|
|
51
|
+
expect(controlCodeFromName("left")).toBe("\x1b[D");
|
|
52
|
+
expect(controlCodeFromName("space")).toBe(" ");
|
|
53
|
+
expect(controlCodeFromName("backspace")).toBe("\x7f");
|
|
54
|
+
expect(controlCodeFromName("pageup")).toBe("\x1b[5~");
|
|
55
|
+
expect(controlCodeFromName("pagedown")).toBe("\x1b[6~");
|
|
56
|
+
});
|
|
57
|
+
|
|
46
58
|
it("supports raw:0xNN escape", async () => {
|
|
47
59
|
const { controlCodeFromName } = await loadModule();
|
|
48
60
|
expect(controlCodeFromName("raw:0x03")).toBe("\x03");
|
|
@@ -51,7 +63,27 @@ describe("subcommands.controlCodeFromName", () => {
|
|
|
51
63
|
|
|
52
64
|
it("throws on unknown code names", async () => {
|
|
53
65
|
const { controlCodeFromName } = await loadModule();
|
|
54
|
-
expect(() => controlCodeFromName("nope")).toThrow(/unknown
|
|
66
|
+
expect(() => controlCodeFromName("nope")).toThrow(/unknown key\/code/);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("subcommands.menuSelectKeys (ay select cursor arithmetic)", () => {
|
|
71
|
+
it("sends Downs + Enter to reach an option below the cursor", async () => {
|
|
72
|
+
const { menuSelectKeys } = await loadModule();
|
|
73
|
+
expect(menuSelectKeys(1, 3)).toEqual(["down", "down", "enter"]);
|
|
74
|
+
});
|
|
75
|
+
it("sends Ups + Enter to reach an option above the cursor", async () => {
|
|
76
|
+
const { menuSelectKeys } = await loadModule();
|
|
77
|
+
expect(menuSelectKeys(3, 1)).toEqual(["up", "up", "enter"]);
|
|
78
|
+
});
|
|
79
|
+
it("sends only Enter when the cursor already sits on the target", async () => {
|
|
80
|
+
const { menuSelectKeys } = await loadModule();
|
|
81
|
+
expect(menuSelectKeys(2, 2)).toEqual(["enter"]);
|
|
82
|
+
});
|
|
83
|
+
it("encodes to the exact ANSI byte stream via controlCodeFromName", async () => {
|
|
84
|
+
const { menuSelectKeys, controlCodeFromName } = await loadModule();
|
|
85
|
+
const bytes = menuSelectKeys(2, 4).map((k) => controlCodeFromName(k));
|
|
86
|
+
expect(bytes.join("")).toBe("\x1b[B\x1b[B\r"); // down, down, enter
|
|
55
87
|
});
|
|
56
88
|
});
|
|
57
89
|
|
package/ts/subcommands.ts
CHANGED
|
@@ -18,7 +18,13 @@ import path from "path";
|
|
|
18
18
|
import { type GlobalPidRecord, readGlobalPids, updateGlobalPidStatus } from "./globalPidIndex.ts";
|
|
19
19
|
import { buildAgentForest, flattenForest } from "./agentTree.ts";
|
|
20
20
|
import { parseTaskCounts, type TaskCounts } from "./todoParse.ts";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
classifyNeedsInput,
|
|
23
|
+
isWorkingScreen,
|
|
24
|
+
parseMenu,
|
|
25
|
+
type MenuState,
|
|
26
|
+
type NeedsInput,
|
|
27
|
+
} from "./needsInput.ts";
|
|
22
28
|
import { diffLsStates, type LiveState, type LsAgentState } from "./lsWatch.ts";
|
|
23
29
|
import {
|
|
24
30
|
buildStoredResult,
|
|
@@ -251,6 +257,8 @@ const SUBCOMMANDS = new Set([
|
|
|
251
257
|
"tail",
|
|
252
258
|
"head",
|
|
253
259
|
"send",
|
|
260
|
+
"key",
|
|
261
|
+
"select",
|
|
254
262
|
"spawn",
|
|
255
263
|
"attach",
|
|
256
264
|
"stop",
|
|
@@ -326,6 +334,10 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
326
334
|
return await cmdRead(rest, { mode: "head" });
|
|
327
335
|
case "send":
|
|
328
336
|
return await cmdSend(rest);
|
|
337
|
+
case "key":
|
|
338
|
+
return await cmdKey(rest);
|
|
339
|
+
case "select":
|
|
340
|
+
return await cmdSelect(rest);
|
|
329
341
|
case "spawn":
|
|
330
342
|
return await cmdSpawn(rest);
|
|
331
343
|
case "attach":
|
|
@@ -392,6 +404,8 @@ export function cmdHelp(managerCommands = true): number {
|
|
|
392
404
|
` ay cat <keyword> full log\n` +
|
|
393
405
|
` ay head <keyword> first N lines\n` +
|
|
394
406
|
` ay send <keyword> <msg> send a message\n` +
|
|
407
|
+
` ay key <keyword> <key...> send raw keystrokes (down/up/enter/esc/…) — drives menus\n` +
|
|
408
|
+
` ay select <keyword> <N> pick option N of a needs_input selection menu\n` +
|
|
395
409
|
` ay attach <keyword> interactive attach (detach: Ctrl-\\)\n` +
|
|
396
410
|
` ay stop <keyword> graceful shutdown (/exit for claude/codex)\n` +
|
|
397
411
|
` ay exit <keyword> [reason] graceful shutdown, recording who/why (= 'ay send <kw> exit')\n` +
|
|
@@ -2215,9 +2229,112 @@ async function cmdSpawn(rest: string[]): Promise<number> {
|
|
|
2215
2229
|
}
|
|
2216
2230
|
|
|
2217
2231
|
// ---------------------------------------------------------------------------
|
|
2218
|
-
// ay send
|
|
2232
|
+
// ay send / ay key / ay select — inject input into a live agent
|
|
2219
2233
|
// ---------------------------------------------------------------------------
|
|
2220
2234
|
|
|
2235
|
+
/**
|
|
2236
|
+
* Shared safety gate for every command that writes to a live agent's stdin
|
|
2237
|
+
* (`send`, `key`, `select`): refuse a self-targeting loop, and require that THIS
|
|
2238
|
+
* sender actually looked at THIS target recently — an agent is blocked, an
|
|
2239
|
+
* interactive human is only warned — unless `force`. Returns the sender context
|
|
2240
|
+
* so a caller can reuse it (e.g. `send`'s `[from …]` prefix). Extracted from
|
|
2241
|
+
* cmdSend so the action commands enforce the identical guard.
|
|
2242
|
+
*/
|
|
2243
|
+
async function enforceSendGuards(
|
|
2244
|
+
record: GlobalPidRecord,
|
|
2245
|
+
force: boolean,
|
|
2246
|
+
): Promise<{ key: string; agent: GlobalPidRecord | null }> {
|
|
2247
|
+
const sender = await senderContext();
|
|
2248
|
+
|
|
2249
|
+
// Self-send guard: an agent firing at its own pid is almost always a loop.
|
|
2250
|
+
if (sender.agent && sender.agent.pid === record.pid && !force) {
|
|
2251
|
+
throw new Error(
|
|
2252
|
+
`refusing to send to yourself (pid ${record.pid}) — pass --force if you really mean it.`,
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Recency guard: require that THIS sender tailed THIS resolved target within
|
|
2257
|
+
// the window. Catches a fuzzy keyword resolving to an agent you never looked
|
|
2258
|
+
// at. Agents are blocked (override with --force / AGENT_YES_FORCE_SEND=1);
|
|
2259
|
+
// an interactive human shell is only warned.
|
|
2260
|
+
const last = await lastReadAt(sender.key, record.pid);
|
|
2261
|
+
const fresh = last !== null && Date.now() - last <= READ_WINDOW_MS;
|
|
2262
|
+
if (!fresh && !force) {
|
|
2263
|
+
const ago =
|
|
2264
|
+
last === null ? "never read" : `last read ${Math.round((Date.now() - last) / 1000)}s ago`;
|
|
2265
|
+
const what = `pid ${record.pid} (${record.cli}, ${shortenPath(record.cwd)}) — ${ago}, not within ${READ_WINDOW_MS / 1000}s`;
|
|
2266
|
+
if (sender.agent) {
|
|
2267
|
+
throw new Error(
|
|
2268
|
+
`${what}.\n Confirm it's the right agent first: ay tail ${record.pid}\n then resend, or pass --force to override.`,
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
process.stderr.write(
|
|
2272
|
+
`warning: ${what} — make sure this is the agent you meant (ay tail ${record.pid}).\n`,
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
return sender;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// Inter-keystroke pace (ms) for `ay key` / `ay select`. Fast enough to feel
|
|
2279
|
+
// instant, slow enough that the CLI's input loop registers each key as a
|
|
2280
|
+
// discrete event instead of coalescing the burst into a bracketed paste — claude
|
|
2281
|
+
// treats a fast multi-byte blob as pasted text (see the run loop's paste guard),
|
|
2282
|
+
// which would drop arrow keys into the composer instead of moving the menu.
|
|
2283
|
+
const KEY_PACE_MS = 40;
|
|
2284
|
+
|
|
2285
|
+
/**
|
|
2286
|
+
* The named-key sequence that moves a menu cursor from `cursor` to option
|
|
2287
|
+
* `target` and confirms: |Δ| Downs (target below) or Ups (target above), then
|
|
2288
|
+
* Enter. Pure so the arrow arithmetic is unit-tested independent of any live PTY.
|
|
2289
|
+
*/
|
|
2290
|
+
export function menuSelectKeys(cursor: number, target: number): string[] {
|
|
2291
|
+
const delta = target - cursor;
|
|
2292
|
+
const nav = Array(Math.abs(delta)).fill(delta > 0 ? "down" : "up");
|
|
2293
|
+
return [...nav, "enter"];
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/** Write each already-encoded key sequence to the FIFO with a pace gap between
|
|
2297
|
+
* them (no gap after the last). Raw bytes, no `[from]` framing, no auto-Enter. */
|
|
2298
|
+
export async function writeKeysPaced(
|
|
2299
|
+
fifoPath: string,
|
|
2300
|
+
byteSeqs: string[],
|
|
2301
|
+
paceMs: number,
|
|
2302
|
+
): Promise<void> {
|
|
2303
|
+
for (let i = 0; i < byteSeqs.length; i++) {
|
|
2304
|
+
if (byteSeqs[i] === "") continue; // `none`/empty — nothing to send
|
|
2305
|
+
await writeToIpc(fifoPath, byteSeqs[i]!);
|
|
2306
|
+
if (i < byteSeqs.length - 1 && paceMs > 0) {
|
|
2307
|
+
await new Promise((r) => setTimeout(r, paceMs));
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/**
|
|
2313
|
+
* The selection menu a needs_input agent is parked on, or null when it isn't on
|
|
2314
|
+
* one. Mirrors extractNeedsInput (same 32 KB tail render + config patterns) but
|
|
2315
|
+
* returns the cursor position + option numbers so `ay select` can compute the
|
|
2316
|
+
* cursor delta.
|
|
2317
|
+
*/
|
|
2318
|
+
export async function extractMenu(logPath: string, cli: string): Promise<MenuState | null> {
|
|
2319
|
+
const cfg = (await cliDefaults())[cli];
|
|
2320
|
+
if (!cfg?.needsInput?.length) return null;
|
|
2321
|
+
const lines = await renderLogTailLines(logPath, 40);
|
|
2322
|
+
if (!lines) return null;
|
|
2323
|
+
return parseMenu(lines, { needsInput: cfg.needsInput, working: cfg.working });
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
/** Poll until the agent is no longer parked on a menu (selection accepted → it
|
|
2327
|
+
* resumed / moved on) or the deadline passes. Returns true if it cleared. */
|
|
2328
|
+
async function waitForNeedsInputClear(record: GlobalPidRecord, timeoutMs: number): Promise<boolean> {
|
|
2329
|
+
const deadline = Date.now() + timeoutMs;
|
|
2330
|
+
while (Date.now() < deadline) {
|
|
2331
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
2332
|
+
const snap = await snapshotStatus(record);
|
|
2333
|
+
if (snap.state !== "needs_input") return true;
|
|
2334
|
+
}
|
|
2335
|
+
return false;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2221
2338
|
async function cmdSend(rest: string[]): Promise<number> {
|
|
2222
2339
|
const y = yargs(rest)
|
|
2223
2340
|
.usage("Usage: ay send <keyword> <msg|-> [options]")
|
|
@@ -2277,35 +2394,8 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
2277
2394
|
}
|
|
2278
2395
|
|
|
2279
2396
|
// Who's sending, and have they actually looked at this target recently?
|
|
2280
|
-
const sender = await senderContext();
|
|
2281
2397
|
const force = Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1";
|
|
2282
|
-
|
|
2283
|
-
// Self-send guard: an agent firing at its own pid is almost always a loop.
|
|
2284
|
-
if (sender.agent && sender.agent.pid === record.pid && !force) {
|
|
2285
|
-
throw new Error(
|
|
2286
|
-
`refusing to send to yourself (pid ${record.pid}) — pass --force if you really mean it.`,
|
|
2287
|
-
);
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
// Recency guard: require that THIS sender tailed THIS resolved target within
|
|
2291
|
-
// the window. Catches a fuzzy keyword resolving to an agent you never looked
|
|
2292
|
-
// at. Agents are blocked (override with --force / AGENT_YES_FORCE_SEND=1);
|
|
2293
|
-
// an interactive human shell is only warned.
|
|
2294
|
-
const last = await lastReadAt(sender.key, record.pid);
|
|
2295
|
-
const fresh = last !== null && Date.now() - last <= READ_WINDOW_MS;
|
|
2296
|
-
if (!fresh && !force) {
|
|
2297
|
-
const ago =
|
|
2298
|
-
last === null ? "never read" : `last read ${Math.round((Date.now() - last) / 1000)}s ago`;
|
|
2299
|
-
const what = `pid ${record.pid} (${record.cli}, ${shortenPath(record.cwd)}) — ${ago}, not within ${READ_WINDOW_MS / 1000}s`;
|
|
2300
|
-
if (sender.agent) {
|
|
2301
|
-
throw new Error(
|
|
2302
|
-
`${what}.\n Confirm it's the right agent first: ay tail ${record.pid}\n then resend, or pass --force to override.`,
|
|
2303
|
-
);
|
|
2304
|
-
}
|
|
2305
|
-
process.stderr.write(
|
|
2306
|
-
`warning: ${what} — make sure this is the agent you meant (ay tail ${record.pid}).\n`,
|
|
2307
|
-
);
|
|
2308
|
-
}
|
|
2398
|
+
const sender = await enforceSendGuards(record, force);
|
|
2309
2399
|
|
|
2310
2400
|
// A bare "exit" / "/exit" isn't a prompt to type — claude only honours the
|
|
2311
2401
|
// literal `/exit` command, so `ay send <pid> exit` lands as plain text and the
|
|
@@ -2355,6 +2445,163 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
2355
2445
|
return 0;
|
|
2356
2446
|
}
|
|
2357
2447
|
|
|
2448
|
+
// Resolve a keyword to one agent and return it with a writable FIFO, or throw
|
|
2449
|
+
// with the same guidance cmdSend gives. Shared by `ay key` / `ay select`.
|
|
2450
|
+
async function resolveWritableAgent(keyword: string, opts: CommonOpts): Promise<GlobalPidRecord> {
|
|
2451
|
+
const record = await resolveOne(keyword, opts);
|
|
2452
|
+
if (!record.fifo_file) {
|
|
2453
|
+
throw new Error(
|
|
2454
|
+
`pid ${record.pid}: no fifo_file recorded — this agent didn't register a stdin FIFO (an older agent, or one not started with --stdpush). Restarting it (ay restart ${record.pid}) re-registers one.`,
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
return record;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
async function cmdKey(rest: string[]): Promise<number> {
|
|
2461
|
+
const y = yargs(rest)
|
|
2462
|
+
.usage(
|
|
2463
|
+
"Usage: ay key <keyword> <key...> [options]\n\n" +
|
|
2464
|
+
"Send raw named keystrokes to a live agent's TUI — no message framing, no\n" +
|
|
2465
|
+
"auto-Enter. Drives selection menus and other interactive prompts that a\n" +
|
|
2466
|
+
"plain `ay send` (text + Enter) can't. Keys are paced so the CLI registers\n" +
|
|
2467
|
+
"each as a discrete event, not a paste.\n\n" +
|
|
2468
|
+
"Keys: up down left right enter esc tab space backspace delete home end\n" +
|
|
2469
|
+
" pageup pagedown ctrl-c ctrl-d ctrl-y raw:0xNN\n\n" +
|
|
2470
|
+
"Examples:\n" +
|
|
2471
|
+
" ay key 1234 down down enter # move the menu cursor down twice, confirm\n" +
|
|
2472
|
+
" ay key 1234 esc # dismiss a menu\n" +
|
|
2473
|
+
" ay key 1234 raw:0x1b # a literal ESC byte",
|
|
2474
|
+
)
|
|
2475
|
+
.option("pace", { type: "number", default: KEY_PACE_MS, description: "ms between keystrokes" })
|
|
2476
|
+
.option("all", { type: "boolean", default: false, description: "Include exited agents" })
|
|
2477
|
+
.option("latest", { type: "boolean", default: false, description: "Use most recent match" })
|
|
2478
|
+
.option("cwd", { type: "string", description: "Restrict to agents under this dir" })
|
|
2479
|
+
.option("force", {
|
|
2480
|
+
type: "boolean",
|
|
2481
|
+
default: false,
|
|
2482
|
+
description: "Skip the recency/self-send guard (also: AGENT_YES_FORCE_SEND=1)",
|
|
2483
|
+
})
|
|
2484
|
+
.help(false)
|
|
2485
|
+
.version(false)
|
|
2486
|
+
.exitProcess(false);
|
|
2487
|
+
|
|
2488
|
+
const argv = await y.parseAsync();
|
|
2489
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
2490
|
+
const keyNames = argv._.slice(1).map(String);
|
|
2491
|
+
if (!keyword || keyNames.length === 0) {
|
|
2492
|
+
throw new Error("usage: ay key <keyword> <key...> (e.g. ay key 1234 down down enter)");
|
|
2493
|
+
}
|
|
2494
|
+
// Map every key up front so an unknown name fails before we send anything
|
|
2495
|
+
// (a half-sent sequence could leave a menu in a surprising state).
|
|
2496
|
+
const byteSeqs = keyNames.map((n) => controlCodeFromName(n.toLowerCase()));
|
|
2497
|
+
|
|
2498
|
+
const opts: CommonOpts = {
|
|
2499
|
+
all: argv.all,
|
|
2500
|
+
active: false,
|
|
2501
|
+
json: false,
|
|
2502
|
+
latest: argv.latest,
|
|
2503
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
2504
|
+
};
|
|
2505
|
+
const record = await resolveWritableAgent(keyword, opts);
|
|
2506
|
+
const force = Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1";
|
|
2507
|
+
await enforceSendGuards(record, force);
|
|
2508
|
+
|
|
2509
|
+
await writeKeysPaced(record.fifo_file!, byteSeqs, Math.max(0, argv.pace));
|
|
2510
|
+
process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${keyNames.join(" ")}\n`);
|
|
2511
|
+
return 0;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
async function cmdSelect(rest: string[]): Promise<number> {
|
|
2515
|
+
const y = yargs(rest)
|
|
2516
|
+
.usage(
|
|
2517
|
+
"Usage: ay select <keyword> <N> [options]\n\n" +
|
|
2518
|
+
"Pick option N of the selection menu a needs_input agent is parked on.\n" +
|
|
2519
|
+
"Re-parses the live menu (the same ❯-cursor detection `ay ls` uses), computes\n" +
|
|
2520
|
+
"how far the cursor must move, and sends that many Down/Up keys + Enter — so\n" +
|
|
2521
|
+
"it's robust to a pre-highlighted default (never assumes the cursor starts at 1)\n" +
|
|
2522
|
+
"and doesn't rely on numeric hotkeys (arrow-driven menus ignore them).\n\n" +
|
|
2523
|
+
"Examples:\n" +
|
|
2524
|
+
" ay select 1234 2 # choose option 2\n" +
|
|
2525
|
+
" ay select 1234 2 --wait # …and block until the menu clears",
|
|
2526
|
+
)
|
|
2527
|
+
.option("pace", { type: "number", default: KEY_PACE_MS, description: "ms between keystrokes" })
|
|
2528
|
+
.option("wait", {
|
|
2529
|
+
type: "boolean",
|
|
2530
|
+
default: false,
|
|
2531
|
+
description: "Block until the agent leaves needs_input (or --timeout)",
|
|
2532
|
+
})
|
|
2533
|
+
.option("timeout", { type: "number", default: 10, description: "Seconds to wait with --wait" })
|
|
2534
|
+
.option("all", { type: "boolean", default: false, description: "Include exited agents" })
|
|
2535
|
+
.option("latest", { type: "boolean", default: false, description: "Use most recent match" })
|
|
2536
|
+
.option("cwd", { type: "string", description: "Restrict to agents under this dir" })
|
|
2537
|
+
.option("force", {
|
|
2538
|
+
type: "boolean",
|
|
2539
|
+
default: false,
|
|
2540
|
+
description: "Skip the recency/self-send guard (also: AGENT_YES_FORCE_SEND=1)",
|
|
2541
|
+
})
|
|
2542
|
+
.help(false)
|
|
2543
|
+
.version(false)
|
|
2544
|
+
.exitProcess(false);
|
|
2545
|
+
|
|
2546
|
+
const argv = await y.parseAsync();
|
|
2547
|
+
const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
|
|
2548
|
+
const n = Number(argv._[1]);
|
|
2549
|
+
if (!keyword || !Number.isInteger(n) || n < 1) {
|
|
2550
|
+
throw new Error(
|
|
2551
|
+
"usage: ay select <keyword> <N> (N = the 1-based option number to choose)",
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
const opts: CommonOpts = {
|
|
2556
|
+
all: argv.all,
|
|
2557
|
+
active: false,
|
|
2558
|
+
json: false,
|
|
2559
|
+
latest: argv.latest,
|
|
2560
|
+
cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null,
|
|
2561
|
+
};
|
|
2562
|
+
const record = await resolveWritableAgent(keyword, opts);
|
|
2563
|
+
if (!record.log_file) {
|
|
2564
|
+
throw new Error(`pid ${record.pid}: no log_file recorded — can't read the menu to select from.`);
|
|
2565
|
+
}
|
|
2566
|
+
const force = Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1";
|
|
2567
|
+
await enforceSendGuards(record, force);
|
|
2568
|
+
|
|
2569
|
+
const menu = await extractMenu(record.log_file, record.cli);
|
|
2570
|
+
if (!menu) {
|
|
2571
|
+
throw new Error(
|
|
2572
|
+
`pid ${record.pid} (${record.cli}) is not parked on a selection menu (not needs_input).\n Check with: ay status ${record.pid}`,
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
if (menu.options.length > 0 && !menu.options.includes(n)) {
|
|
2576
|
+
throw new Error(
|
|
2577
|
+
`option ${n} is out of range — this menu offers ${menu.options.join(", ")}.`,
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// Move the cursor from where it sits to option N, then confirm. Delta from the
|
|
2582
|
+
// PARSED cursor position (not a blind "N-1 downs") so a non-first default works.
|
|
2583
|
+
const keyNames = menuSelectKeys(menu.cursor, n);
|
|
2584
|
+
const byteSeqs = keyNames.map((k) => controlCodeFromName(k));
|
|
2585
|
+
await writeKeysPaced(record.fifo_file!, byteSeqs, Math.max(0, argv.pace));
|
|
2586
|
+
|
|
2587
|
+
const delta = n - menu.cursor;
|
|
2588
|
+
const moved = delta === 0 ? "cursor already there" : `${Math.abs(delta)}× ${delta > 0 ? "down" : "up"}`;
|
|
2589
|
+
process.stdout.write(
|
|
2590
|
+
`pid ${record.pid} (${record.cli}): selected option ${n} (${moved} + enter)\n`,
|
|
2591
|
+
);
|
|
2592
|
+
|
|
2593
|
+
if (argv.wait) {
|
|
2594
|
+
const ok = await waitForNeedsInputClear(record, Math.max(1, argv.timeout) * 1000);
|
|
2595
|
+
process.stdout.write(
|
|
2596
|
+
ok
|
|
2597
|
+
? ` menu cleared — selection accepted.\n`
|
|
2598
|
+
: ` still needs_input after ${argv.timeout}s — re-check with 'ay status ${record.pid}'.\n`,
|
|
2599
|
+
);
|
|
2600
|
+
return ok ? 0 : 1;
|
|
2601
|
+
}
|
|
2602
|
+
return 0;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2358
2605
|
/// CLIs that ignore a single Ctrl+C and need a more specific shutdown signal.
|
|
2359
2606
|
/// Users hit this every time they try `ay send <pid> "" --code=ctrl-c` and
|
|
2360
2607
|
/// see no effect — print a one-liner pointing them at `ay stop`.
|
|
@@ -2406,6 +2653,36 @@ export function controlCodeFromName(name: string): string {
|
|
|
2406
2653
|
return "\x1c";
|
|
2407
2654
|
case "tab":
|
|
2408
2655
|
return "\t";
|
|
2656
|
+
// Navigation / editing keys — the ANSI/xterm sequences a TUI reads as cursor
|
|
2657
|
+
// moves. Added for `ay key` / `ay select` so a menu can be driven from a
|
|
2658
|
+
// parent agent (up/down + enter picks an option) the same way a human's
|
|
2659
|
+
// arrow keys do in the web terminal.
|
|
2660
|
+
case "up":
|
|
2661
|
+
return "\x1b[A";
|
|
2662
|
+
case "down":
|
|
2663
|
+
return "\x1b[B";
|
|
2664
|
+
case "right":
|
|
2665
|
+
return "\x1b[C";
|
|
2666
|
+
case "left":
|
|
2667
|
+
return "\x1b[D";
|
|
2668
|
+
case "home":
|
|
2669
|
+
return "\x1b[H";
|
|
2670
|
+
case "end":
|
|
2671
|
+
return "\x1b[F";
|
|
2672
|
+
case "pageup":
|
|
2673
|
+
case "pgup":
|
|
2674
|
+
return "\x1b[5~";
|
|
2675
|
+
case "pagedown":
|
|
2676
|
+
case "pgdn":
|
|
2677
|
+
return "\x1b[6~";
|
|
2678
|
+
case "space":
|
|
2679
|
+
return " ";
|
|
2680
|
+
case "backspace":
|
|
2681
|
+
case "bs":
|
|
2682
|
+
return "\x7f";
|
|
2683
|
+
case "delete":
|
|
2684
|
+
case "del":
|
|
2685
|
+
return "\x1b[3~";
|
|
2409
2686
|
case "none":
|
|
2410
2687
|
case "":
|
|
2411
2688
|
return "";
|
|
@@ -2413,7 +2690,7 @@ export function controlCodeFromName(name: string): string {
|
|
|
2413
2690
|
// raw:0xNN form
|
|
2414
2691
|
const m = /^raw:0x([0-9a-f]+)$/i.exec(name);
|
|
2415
2692
|
if (m) return String.fromCharCode(parseInt(m[1]!, 16));
|
|
2416
|
-
throw new Error(`unknown
|
|
2693
|
+
throw new Error(`unknown key/code: ${name}`);
|
|
2417
2694
|
}
|
|
2418
2695
|
}
|
|
2419
2696
|
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import "./ts-BzyVSzo1.js";
|
|
2
|
-
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-CCHuozmQ.js";
|
|
4
|
-
import "./pidStore-BfoBWUjc.js";
|
|
5
|
-
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C_J_okdw.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import "./globalPidIndex-DlmmJlO8.js";
|
|
3
|
-
import "./configShared-CEyhl0WH.js";
|
|
4
|
-
import "./e2e-Bfw7qL9O.js";
|
|
5
|
-
import "./webrtcLink-CBZkZ-LT.js";
|
|
6
|
-
import "./remotes-DBCvpp3B.js";
|
|
7
|
-
import { C as restartHintLines, D as writeToIpc, E as stopTipForCli, S as resolveResumeArgs, T as snapshotStatus, _ as readPtysize, a as cursorAbs, b as resolveOne, c as extractTaskCounts, d as isExitRequest, f as isPidAlive, g as readNotes, h as matchKeyword, i as controlCodeFromName, l as finalizedLines, m as listRecords, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isSubcommand, r as cmdHelp, s as extractNeedsInput, t as GRACEFUL_EXIT_COMMANDS, u as isAgentStuck, v as renderRawLog, w as runSubcommand, x as resolveReadWindow, y as renderRawLogLines } from "./subcommands-BbKngO7F.js";
|
|
8
|
-
|
|
9
|
-
export { cmdHelp, isSubcommand, runSubcommand };
|