claude-yes 1.167.0 → 1.169.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.
Files changed (48) hide show
  1. package/dist/{SUPPORTED_CLIS-DFBATC85.js → SUPPORTED_CLIS-BhtgiqTC.js} +2 -2
  2. package/dist/SUPPORTED_CLIS-D-ea9sTR.js +8 -0
  3. package/dist/cli.js +47 -6
  4. package/dist/{e2e-Bfw7qL9O.js → e2e-BeKjLhmO.js} +1 -1
  5. package/dist/forkNested-UCKPEgSI.js +59 -0
  6. package/dist/index.js +2 -2
  7. package/dist/{openBrowser-CuOlFtow.js → openBrowser-CCF1iuQK.js} +1 -1
  8. package/dist/{remotes-T6nf0t3K.js → remotes-BmAPylU_.js} +3 -3
  9. package/dist/{remotes-ClT1bq16.js → remotes-cx_GDFPj.js} +3 -3
  10. package/dist/{runningLock-V4qvXgAw.js → runningLock-BobVW1_A.js} +2 -1
  11. package/dist/{schedule-C2RMA3qm.js → schedule-CQge8oP_.js} +5 -5
  12. package/dist/{serve-mLqsRwfR.js → serve-tpnNSWeN.js} +34 -15
  13. package/dist/{setup-BtqKZx3q.js → setup-BSAdV0mz.js} +3 -3
  14. package/dist/{share-BjqQBWM-.js → share-D_e2Fwiy.js} +2 -2
  15. package/dist/{spawnGate-UH73I2le.js → spawnGate-CQ1Il3Xk.js} +1 -1
  16. package/dist/{spawnGate-B_VDMXYL.js → spawnGate-DzPfa1PZ.js} +2 -2
  17. package/dist/subcommands-B4Pwp2Z_.js +9 -0
  18. package/dist/{subcommands-DWeo7ZLc.js → subcommands-BV32LcSR.js} +45 -11
  19. package/dist/{tray-CZarCA2Q.js → tray-CWUpaZF4.js} +2 -2
  20. package/dist/{ts-s3wYccKf.js → ts-DBYdQgbR.js} +39 -18
  21. package/dist/{versionChecker-BvR6tV3u.js → versionChecker-CL0RTOKo.js} +2 -2
  22. package/dist/{webrtcLink-CBZkZ-LT.js → webrtcLink-BG0Xc4-W.js} +2 -2
  23. package/dist/{webrtcRemote-GAgF5K45.js → webrtcRemote-SybKurg9.js} +3 -3
  24. package/dist/{workspaceConfig-D3OH7and.js → workspaceConfig-BC03X4Y1.js} +1 -1
  25. package/lab/ui/console-logic.js +16 -0
  26. package/lab/ui/index.html +28 -0
  27. package/package.json +1 -1
  28. package/ts/autoRetry.spec.ts +16 -1
  29. package/ts/autoRetry.ts +23 -0
  30. package/ts/badges.spec.ts +54 -0
  31. package/ts/badges.ts +42 -0
  32. package/ts/cli.ts +46 -0
  33. package/ts/forkNested.spec.ts +34 -0
  34. package/ts/forkNested.ts +64 -0
  35. package/ts/idleWaiter.spec.ts +12 -0
  36. package/ts/idleWaiter.ts +5 -0
  37. package/ts/index.ts +23 -5
  38. package/ts/needsInput.ts +1 -1
  39. package/ts/parseCliArgs.spec.ts +18 -0
  40. package/ts/parseCliArgs.ts +8 -0
  41. package/ts/removeControlCharacters.spec.ts +21 -0
  42. package/ts/removeControlCharacters.ts +32 -6
  43. package/ts/runningLock.ts +1 -0
  44. package/ts/serve.ts +24 -0
  45. package/ts/subcommands.spec.ts +22 -0
  46. package/ts/subcommands.ts +40 -13
  47. package/dist/SUPPORTED_CLIS-DLFqRE8W.js +0 -8
  48. package/dist/subcommands-B-WoBVhk.js +0 -9
package/ts/needsInput.ts CHANGED
@@ -78,7 +78,7 @@ export interface MenuState {
78
78
 
79
79
  // An option row: an optional cursor glyph / bullet, then "N. " (the trailing
80
80
  // space rejects version-like "3.5GB" that isn't a menu option).
81
- const OPTION_LINE = /^[\s❯›>▶◉○●·*\-]*?(\d+)\.\s/;
81
+ const OPTION_LINE = /^[\s❯›>▶◉○●·*-]*?(\d+)\.\s/;
82
82
 
83
83
  /**
84
84
  * Parse the selection menu a `needs_input` agent is parked on into a cursor
@@ -439,4 +439,22 @@ describe("CLI argument parsing", () => {
439
439
  expect(afterCli.useStdinAppend).toBe(true);
440
440
  expect(afterCli.cliArgs).toContain("--no-stdpush");
441
441
  });
442
+
443
+ it("consumes --attach as an agent-yes flag before the CLI positional", () => {
444
+ const result = parseCliArgs(["node", "/path/to/ay", "--attach", "claude", "--", "task"]);
445
+ expect(result.attach).toBe(true);
446
+ });
447
+
448
+ it("defaults attach to false so fork-by-default stays enabled", () => {
449
+ const result = parseCliArgs(["node", "/path/to/ay", "claude"]);
450
+ expect(result.attach).toBe(false);
451
+ });
452
+
453
+ it("does NOT treat --attach after -- as an agent-yes flag (passthrough safety)", () => {
454
+ // Everything after `--` is prompt / CLI passthrough, never an agent-yes flag —
455
+ // so `ay claude -- --attach` sends --attach to the CLI and still forks.
456
+ const result = parseCliArgs(["node", "/path/to/ay", "claude", "--", "--attach"]);
457
+ expect(result.attach).toBe(false);
458
+ expect(result.prompt).toContain("--attach");
459
+ });
442
460
  });
@@ -30,6 +30,13 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
30
30
  description: "re-spawn Claude with --continue if it crashes, only works for claude yet",
31
31
  alias: "r",
32
32
  })
33
+ .option("attach", {
34
+ type: "boolean",
35
+ default: false,
36
+ description:
37
+ "Run the agent in the foreground even when nested inside another agent (disables fork-by-default; also AGENT_YES_ATTACH=1).",
38
+ alias: "foreground",
39
+ })
33
40
  .option("logFile", {
34
41
  type: "string",
35
42
  description: "Rendered log file to write to.",
@@ -307,6 +314,7 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
307
314
  ),
308
315
  queue: parsedArgv.queue,
309
316
  robust: parsedArgv.robust,
317
+ attach: parsedArgv.attach,
310
318
  logFile: parsedArgv.logFile,
311
319
  verbose: parsedArgv.verbose,
312
320
  resume: parsedArgv.continue, // Note: intentional use resume here to avoid preserved keyword (continue)
@@ -70,4 +70,25 @@ describe("removeControlCharacters", () => {
70
70
  const expected = "TextClearLine";
71
71
  expect(removeControlCharacters(input)).toBe(expected);
72
72
  });
73
+
74
+ it("should remove OSC sequences (e.g. window title updates)", () => {
75
+ const input = "Before\u001b]0;window title\u0007After";
76
+ const expected = "BeforeAfter";
77
+ expect(removeControlCharacters(input)).toBe(expected);
78
+ });
79
+
80
+ it("does NOT strip trailing text when an OSC sequence is unterminated (no BEL)", () => {
81
+ const input = "Before\u001b]0;titleAfter";
82
+ expect(removeControlCharacters(input)).toBe(input);
83
+ });
84
+
85
+ it("should remove OSC sequences terminated by ST (ESC backslash), not just BEL", () => {
86
+ const input = "Before\u001b]8;;https://example.com\u001b\\After";
87
+ expect(removeControlCharacters(input)).toBe("BeforeAfter");
88
+ });
89
+
90
+ it("does not let an ST-terminated OSC run on and eat real text before a later BEL", () => {
91
+ const input = "Before\u001b]0;t\u001b\\visible\u0007After";
92
+ expect(removeControlCharacters(input)).toBe("Beforevisible\u0007After");
93
+ });
73
94
  });
@@ -1,8 +1,34 @@
1
+ const ESC = String.fromCharCode(0x1b);
2
+ const C1 = String.fromCharCode(0x9b);
3
+ const BEL = String.fromCharCode(0x07);
4
+ const BACKSLASH = String.fromCharCode(0x5c);
5
+ // String Terminator (ESC \) as REGEX SOURCE text: a literal backslash inside a
6
+ // regex pattern is itself an escape character, so matching one literal
7
+ // backslash requires two backslash characters in the pattern source.
8
+ const ST_PATTERN_SOURCE = ESC + BACKSLASH + BACKSLASH;
9
+
10
+ // OSC sequences (window/tab title updates, hyperlinks, etc.): ESC ] ...
11
+ // terminated by either BEL or ST (ESC \) — both are valid per-spec and used
12
+ // by real terminal apps. Not covered by the CSI pattern below — without this,
13
+ // e.g. a periodic title update would count as "visible" content to callers
14
+ // that gate activity on non-empty output.
15
+ // The terminator is required, not optional: an unterminated ESC]... (the
16
+ // terminator never arrives, e.g. a truncated chunk boundary) must NOT strip
17
+ // through to end-of-string — that would eat real trailing text as if it were
18
+ // part of the title sequence. The body excludes ESC too (not just BEL) so an
19
+ // ST-terminated sequence can't accidentally run on and swallow real text up
20
+ // to some unrelated, later BEL.
21
+ const OSC_PATTERN = new RegExp(
22
+ ESC + "][^" + BEL + ESC + "]*(?:" + BEL + "|" + ST_PATTERN_SOURCE + ")",
23
+ "g",
24
+ );
25
+
26
+ // Matches control characters in the C0 and C1 ranges, including Delete (U+007F)
27
+ const CSI_PATTERN = new RegExp(
28
+ "[" + ESC + C1 + "][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]",
29
+ "g",
30
+ );
31
+
1
32
  export function removeControlCharacters(str: string): string {
2
- // Matches control characters in the C0 and C1 ranges, including Delete (U+007F)
3
- return str.replace(
4
- // eslint-disable-next-line no-control-regex This is a control regex
5
- /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
6
- "",
7
- );
33
+ return str.replace(OSC_PATTERN, "").replace(CSI_PATTERN, "");
8
34
  }
package/ts/runningLock.ts CHANGED
@@ -171,6 +171,7 @@ async function checkLock(cwd: string, _prompt: string): Promise<LockCheckResult>
171
171
 
172
172
  // Find running tasks for this location
173
173
  const blockingTasks = lockFile.tasks.filter((task) => {
174
+ if (task.pid === process.pid) return false; // Never self-block on our own prior entry
174
175
  if (!isProcessRunning(task.pid)) return false; // Skip stale locks
175
176
  if (task.status !== "running") return false; // Only check running tasks
176
177
 
package/ts/serve.ts CHANGED
@@ -9,6 +9,7 @@ import yargs from "yargs";
9
9
  import {
10
10
  controlCodeFromName,
11
11
  deriveLiveStatus,
12
+ extractBadges,
12
13
  extractNeedsInput,
13
14
  extractTaskCounts,
14
15
  listRecords,
@@ -935,6 +936,26 @@ export async function cmdServe(rest: string[]): Promise<number> {
935
936
  }
936
937
  };
937
938
 
939
+ // Per-agent status badges/flags (see badges.ts) matched against the agent's
940
+ // rendered screen — e.g. an active /goal Stop-hook loop. Cached per
941
+ // (size, mtime) exactly like logTasks. Extend BADGE_DEFS in badges.ts to
942
+ // surface more patterns (error banners, other flags); no server changes
943
+ // needed beyond that.
944
+ const badgeCache = new Map<string, { size: number; mtimeMs: number; badges: string[] }>();
945
+ const logBadges = async (logFile: string | null | undefined): Promise<string[]> => {
946
+ if (!logFile) return [];
947
+ try {
948
+ const { size, mtimeMs } = await stat(logFile);
949
+ const hit = badgeCache.get(logFile);
950
+ if (hit && hit.size === size && hit.mtimeMs === mtimeMs) return hit.badges;
951
+ const badges = await extractBadges(logFile);
952
+ badgeCache.set(logFile, { size, mtimeMs, badges });
953
+ return badges;
954
+ } catch {
955
+ return [];
956
+ }
957
+ };
958
+
938
959
  // Per-agent "waiting on you" detection: the agent is parked on an interactive
939
960
  // menu it did NOT auto-resolve (config `needsInput` patterns). Same source and
940
961
  // classifier as `ay ls` / `ay status`, so the console's dot matches the CLI's
@@ -1148,6 +1169,9 @@ export async function cmdServe(rest: string[]): Promise<number> {
1148
1169
  // Task progress from the rendered todo block (null when none detected → no
1149
1170
  // badge). Skipped for exited agents — their screen is no longer live.
1150
1171
  tasks: status === "exited" ? null : await logTasks(r.log_file),
1172
+ // Status flags matched against the rendered screen (see badges.ts) — e.g.
1173
+ // an active /goal loop. [] when none matched or for exited agents.
1174
+ badges: status === "exited" ? [] : await logBadges(r.log_file),
1151
1175
  };
1152
1176
  };
1153
1177
 
@@ -964,6 +964,28 @@ describe("subcommands.isExitRequest", () => {
964
964
  });
965
965
  });
966
966
 
967
+ describe("subcommands.isSlashCommand", () => {
968
+ it("matches a body that starts with a slash command (so it is sent unprefixed)", async () => {
969
+ const { isSlashCommand } = await loadModule();
970
+ for (const s of ["/compact", "/clear", "/model sonnet", "/resume\nmore text"]) {
971
+ expect(isSlashCommand(s)).toBe(true);
972
+ }
973
+ });
974
+ it("does NOT match plain prose, or a slash not at column 0", async () => {
975
+ const { isSlashCommand } = await loadModule();
976
+ for (const s of [
977
+ "please run /compact",
978
+ " /compact", // leading space — the CLI won't parse it as a command either
979
+ "//two slashes... wait, no letter after first slash",
980
+ "/ ",
981
+ "hello",
982
+ "",
983
+ ]) {
984
+ expect(isSlashCommand(s)).toBe(false);
985
+ }
986
+ });
987
+ });
988
+
967
989
  describe("subcommands.writeToIpc reliable delivery", () => {
968
990
  const itUnix = process.platform === "linux" || process.platform === "darwin";
969
991
 
package/ts/subcommands.ts CHANGED
@@ -18,6 +18,7 @@ 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 { matchBadges } from "./badges.ts";
21
22
  import {
22
23
  classifyNeedsInput,
23
24
  isWorkingScreen,
@@ -2128,6 +2129,17 @@ export async function extractNeedsInput(logPath: string, cli: string): Promise<N
2128
2129
  return classifyNeedsInput(lines, { needsInput: cfg.needsInput, working: cfg.working });
2129
2130
  }
2130
2131
 
2132
+ /**
2133
+ * Which badges (see badges.ts) match an agent's current screen — the same 32 KB
2134
+ * tail window `ay tail` renders, no CLI-specific config needed. Returns [] on
2135
+ * any read/render error or an empty log, same failure shape as extractNeedsInput.
2136
+ */
2137
+ export async function extractBadges(logPath: string): Promise<string[]> {
2138
+ const lines = await renderLogTailLines(logPath, 40);
2139
+ if (!lines) return [];
2140
+ return matchBadges(lines);
2141
+ }
2142
+
2131
2143
  /**
2132
2144
  * Whether an alive agent is wedged: its log has been silent for at least
2133
2145
  * STUCK_THRESHOLD_MS yet its screen still shows a `working` busy marker (a live
@@ -2388,7 +2400,10 @@ export async function extractMenu(logPath: string, cli: string): Promise<MenuSta
2388
2400
 
2389
2401
  /** Poll until the agent is no longer parked on a menu (selection accepted → it
2390
2402
  * resumed / moved on) or the deadline passes. Returns true if it cleared. */
2391
- async function waitForNeedsInputClear(record: GlobalPidRecord, timeoutMs: number): Promise<boolean> {
2403
+ async function waitForNeedsInputClear(
2404
+ record: GlobalPidRecord,
2405
+ timeoutMs: number,
2406
+ ): Promise<boolean> {
2392
2407
  const deadline = Date.now() + timeoutMs;
2393
2408
  while (Date.now() < deadline) {
2394
2409
  await new Promise((r) => setTimeout(r, 250));
@@ -2476,10 +2491,15 @@ async function cmdSend(rest: string[]): Promise<number> {
2476
2491
  }
2477
2492
 
2478
2493
  // When an agent sends, prefix one line so the recipient knows who pinged it
2479
- // and exactly how to reply (to a resolvable pid — the sender's own).
2480
- const prefix = sender.agent
2481
- ? `[from ${sender.agent.cli} #${sender.agent.pid} @ ${shortenPath(sender.agent.cwd)} reply: ay send ${sender.agent.pid} "..."]\n`
2482
- : "";
2494
+ // and exactly how to reply (to a resolvable pid — the sender's own). BUT a
2495
+ // slash command is only recognized when `/` is the very first character of the
2496
+ // submitted message; the prefix would bump it to line 2 and the CLI would type
2497
+ // the command as plain text. So skip the prefix for a command body and send it
2498
+ // verbatim — attribution is dropped for the command, but it actually runs.
2499
+ const prefix =
2500
+ sender.agent && !isSlashCommand(body)
2501
+ ? `[from ${sender.agent.cli} #${sender.agent.pid} @ ${shortenPath(sender.agent.cwd)} — reply: ay send ${sender.agent.pid} "..."]\n`
2502
+ : "";
2483
2503
 
2484
2504
  const fullBody = prefix + body;
2485
2505
  if (fullBody && trailing) {
@@ -2610,9 +2630,7 @@ async function cmdSelect(rest: string[]): Promise<number> {
2610
2630
  const keyword = argv._[0] !== undefined ? String(argv._[0]) : undefined;
2611
2631
  const n = Number(argv._[1]);
2612
2632
  if (!keyword || !Number.isInteger(n) || n < 1) {
2613
- throw new Error(
2614
- "usage: ay select <keyword> <N> (N = the 1-based option number to choose)",
2615
- );
2633
+ throw new Error("usage: ay select <keyword> <N> (N = the 1-based option number to choose)");
2616
2634
  }
2617
2635
 
2618
2636
  const opts: CommonOpts = {
@@ -2624,7 +2642,9 @@ async function cmdSelect(rest: string[]): Promise<number> {
2624
2642
  };
2625
2643
  const record = await resolveWritableAgent(keyword, opts);
2626
2644
  if (!record.log_file) {
2627
- throw new Error(`pid ${record.pid}: no log_file recorded — can't read the menu to select from.`);
2645
+ throw new Error(
2646
+ `pid ${record.pid}: no log_file recorded — can't read the menu to select from.`,
2647
+ );
2628
2648
  }
2629
2649
  const force = Boolean(argv.force) || process.env.AGENT_YES_FORCE_SEND === "1";
2630
2650
  await enforceSendGuards(record, force);
@@ -2636,9 +2656,7 @@ async function cmdSelect(rest: string[]): Promise<number> {
2636
2656
  );
2637
2657
  }
2638
2658
  if (menu.options.length > 0 && !menu.options.includes(n)) {
2639
- throw new Error(
2640
- `option ${n} is out of range — this menu offers ${menu.options.join(", ")}.`,
2641
- );
2659
+ throw new Error(`option ${n} is out of range — this menu offers ${menu.options.join(", ")}.`);
2642
2660
  }
2643
2661
 
2644
2662
  // Move the cursor from where it sits to option N, then confirm. Delta from the
@@ -2648,7 +2666,8 @@ async function cmdSelect(rest: string[]): Promise<number> {
2648
2666
  await writeKeysPaced(record.fifo_file!, byteSeqs, Math.max(0, argv.pace));
2649
2667
 
2650
2668
  const delta = n - menu.cursor;
2651
- const moved = delta === 0 ? "cursor already there" : `${Math.abs(delta)}× ${delta > 0 ? "down" : "up"}`;
2669
+ const moved =
2670
+ delta === 0 ? "cursor already there" : `${Math.abs(delta)}× ${delta > 0 ? "down" : "up"}`;
2652
2671
  process.stdout.write(
2653
2672
  `pid ${record.pid} (${record.cli}): selected option ${n} (${moved} + enter)\n`,
2654
2673
  );
@@ -2917,6 +2936,14 @@ export function isExitRequest(body: string): boolean {
2917
2936
  return t === "exit" || t === "/exit";
2918
2937
  }
2919
2938
 
2939
+ /** A body that the CLI will parse as a slash command — `/` as the very first
2940
+ * character (claude requires column 0, no leading whitespace, then a letter).
2941
+ * Such a body must be sent verbatim: any prefix line bumps the `/` off column 0
2942
+ * and the CLI types the command as plain text instead of running it. */
2943
+ export function isSlashCommand(body: string): boolean {
2944
+ return /^\/[A-Za-z]/.test(body);
2945
+ }
2946
+
2920
2947
  /**
2921
2948
  * Gracefully terminate a live agent and record WHY in its note (the audit trail
2922
2949
  * shown by `ay ls`). Sends the CLI's graceful-exit command (e.g. claude's
@@ -1,8 +0,0 @@
1
- import "./ts-s3wYccKf.js";
2
- import "./logger-CDIsZ-Pp.js";
3
- import "./versionChecker-BvR6tV3u.js";
4
- import "./pidStore-BfoBWUjc.js";
5
- import "./globalPidIndex-DlmmJlO8.js";
6
- import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DFBATC85.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-T6nf0t3K.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-DWeo7ZLc.js";
8
-
9
- export { cmdHelp, isSubcommand, runSubcommand };