claude-yes 1.153.2 → 1.154.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 (38) hide show
  1. package/README.md +1 -0
  2. package/agent-yes.config.schema.json +4 -0
  3. package/default.config.yaml +27 -0
  4. package/dist/SUPPORTED_CLIS-CmM_zBrM.js +8 -0
  5. package/dist/{SUPPORTED_CLIS-DsOUGcZj.js → SUPPORTED_CLIS-Dam11bMz.js} +2 -2
  6. package/dist/{agent-yes.config-BKtCmmy9.js → agent-yes.config-Bl08kMpl.js} +2 -2
  7. package/dist/cli.js +5 -5
  8. package/dist/{configShared-C1C04bbq.js → configShared-CEyhl0WH.js} +3 -2
  9. package/dist/{e2e-ClOI_aqV.js → e2e-Bfw7qL9O.js} +1 -1
  10. package/dist/index.js +2 -2
  11. package/dist/openBrowser-CuOlFtow.js +71 -0
  12. package/dist/{remotes-qK6uozO4.js → remotes-DBCvpp3B.js} +3 -3
  13. package/dist/{remotes-5lHLAAqn.js → remotes-DkNuq417.js} +3 -3
  14. package/dist/{schedule-Bxei036B.js → schedule-BPFBe7R5.js} +5 -5
  15. package/dist/{serve-CoUlCHxT.js → serve-BYc2J-2F.js} +25 -18
  16. package/dist/{setup-CSgScgPD.js → setup-D9emVJYz.js} +3 -3
  17. package/dist/{share-ShLKJTUE.js → share-Bq_tDXQU.js} +2 -2
  18. package/dist/{subcommands-CapfwHNa.js → subcommands-C4tMi9NX.js} +102 -18
  19. package/dist/subcommands-DLvC6kto.js +9 -0
  20. package/dist/{tray-5Hezw9uj.js → tray-BMzpUSfa.js} +1 -1
  21. package/dist/{ts-C4ZmPc5P.js → ts-U3AE4u72.js} +3 -3
  22. package/dist/{versionChecker-CqRHax_g.js → versionChecker-B-ToHoJ2.js} +2 -2
  23. package/dist/{webrtcLink-BWhuA74k.js → webrtcLink-CBZkZ-LT.js} +2 -2
  24. package/dist/{webrtcRemote-jGM3ZHK3.js → webrtcRemote-Ccdzmuc-.js} +3 -3
  25. package/dist/{workspaceConfig-B9VX8tVO.js → workspaceConfig-DT3cCBZK.js} +1 -1
  26. package/lab/ui/index.html +1 -1
  27. package/package.json +1 -1
  28. package/ts/buildRustArgs.spec.ts +5 -1
  29. package/ts/configShared.ts +4 -0
  30. package/ts/globalPidIndex.ts +5 -0
  31. package/ts/index.ts +1 -0
  32. package/ts/openBrowser.spec.ts +130 -0
  33. package/ts/openBrowser.ts +67 -0
  34. package/ts/serve.ts +19 -6
  35. package/ts/subcommands.spec.ts +119 -2
  36. package/ts/subcommands.ts +104 -7
  37. package/dist/SUPPORTED_CLIS-BsfuhkaH.js +0 -8
  38. package/dist/subcommands-6M1qZllB.js +0 -9
@@ -1,9 +1,9 @@
1
1
  import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
2
2
  import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
3
3
  import { a as updateGlobalPidStatus, i as readGlobalPids } from "./globalPidIndex-DlmmJlO8.js";
4
- import { t as loadSharedCliDefaults } from "./configShared-C1C04bbq.js";
5
- import { n as isWebrtcSpec } from "./webrtcLink-BWhuA74k.js";
6
- import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-qK6uozO4.js";
4
+ import { t as loadSharedCliDefaults } from "./configShared-CEyhl0WH.js";
5
+ import { n as isWebrtcSpec } from "./webrtcLink-CBZkZ-LT.js";
6
+ import { a as resolveRemoteSpec, i as readRemotes } from "./remotes-DBCvpp3B.js";
7
7
  import ms from "ms";
8
8
  import yargs from "yargs";
9
9
  import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
@@ -554,19 +554,19 @@ async function runSubcommand(argv) {
554
554
  case "restart": return await cmdRestart(rest);
555
555
  case "note": return await cmdNote(rest);
556
556
  case "serve": {
557
- const { cmdServe } = await import("./serve-CoUlCHxT.js");
557
+ const { cmdServe } = await import("./serve-BYc2J-2F.js");
558
558
  return cmdServe(rest);
559
559
  }
560
560
  case "setup": {
561
- const { cmdSetup } = await import("./setup-CSgScgPD.js");
561
+ const { cmdSetup } = await import("./setup-D9emVJYz.js");
562
562
  return cmdSetup(rest);
563
563
  }
564
564
  case "schedule": {
565
- const { cmdSchedule } = await import("./schedule-Bxei036B.js");
565
+ const { cmdSchedule } = await import("./schedule-BPFBe7R5.js");
566
566
  return cmdSchedule(rest);
567
567
  }
568
568
  case "remote": {
569
- const { cmdRemote } = await import("./remotes-5lHLAAqn.js");
569
+ const { cmdRemote } = await import("./remotes-DkNuq417.js");
570
570
  return cmdRemote(rest);
571
571
  }
572
572
  case "reap":
@@ -583,7 +583,7 @@ async function runSubcommand(argv) {
583
583
  }
584
584
  function cmdHelp(managerCommands = true) {
585
585
  const setupLine = managerCommands ? ` ay setup guided setup: pick a workspace, share to agent-yes.com\n` : ``;
586
- 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 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");
586
+ 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");
587
587
  return 0;
588
588
  }
589
589
  function matchKeyword(record, keyword) {
@@ -899,7 +899,7 @@ async function fetchRemoteRecordsRaw(url, token, opts) {
899
899
  let base = url;
900
900
  let bearer = token;
901
901
  if (isWebrtcSpec(url)) {
902
- const { startWebrtcBridge } = await import("./webrtcRemote-jGM3ZHK3.js");
902
+ const { startWebrtcBridge } = await import("./webrtcRemote-Ccdzmuc-.js");
903
903
  bridge = await startWebrtcBridge(url);
904
904
  base = bridge.baseUrl;
905
905
  bearer = bridge.token;
@@ -1002,6 +1002,10 @@ async function deriveLiveState(r) {
1002
1002
  state: "stopped",
1003
1003
  question: null
1004
1004
  };
1005
+ if (r.unresponsive) return {
1006
+ state: "stuck",
1007
+ question: null
1008
+ };
1005
1009
  if (r.log_file) {
1006
1010
  const ni = await extractNeedsInput(r.log_file, r.cli);
1007
1011
  if (ni) return {
@@ -2283,14 +2287,74 @@ async function cmdAttach(rest) {
2283
2287
  });
2284
2288
  return 0;
2285
2289
  }
2290
+ /**
2291
+ * Decide how to relaunch an agent on `ay restart`. Pure (no I/O) so it's unit
2292
+ * testable. Precedence:
2293
+ * - `fresh`: replay the original prompt (the old behaviour), no resume.
2294
+ * - else if the CLI printed a resume command its `resumeCommand` regex matches
2295
+ * in the captured log (capture group 1 = the arg string), relaunch with those
2296
+ * whitespace-split args.
2297
+ * - else fall back to `restoreArgs` (e.g. `--continue`) so the wrapper's own
2298
+ * resume plumbing (claude --continue, codex stored-session) kicks in.
2299
+ */
2300
+ function resolveResumeArgs(conf, logText, opts) {
2301
+ if (opts.fresh) return opts.prompt ? {
2302
+ args: [opts.prompt],
2303
+ strategy: "fresh (replay original prompt)"
2304
+ } : {
2305
+ args: [],
2306
+ strategy: "fresh (no prompt)"
2307
+ };
2308
+ const re = conf?.resumeCommand;
2309
+ if (re) {
2310
+ const captured = (re.global ? new RegExp(re.source, re.flags.replace(/g/g, "")) : re).exec(logText)?.[1]?.trim();
2311
+ if (captured) {
2312
+ const parts = captured.split(/\s+/).filter(Boolean);
2313
+ if (parts.length) return {
2314
+ args: parts,
2315
+ strategy: `printed resume command: ${captured}`
2316
+ };
2317
+ }
2318
+ }
2319
+ const restore = conf?.restoreArgs;
2320
+ if (restore && restore.length) return {
2321
+ args: [...restore],
2322
+ strategy: `restoreArgs (${restore.join(" ")})`
2323
+ };
2324
+ return {
2325
+ args: ["--continue"],
2326
+ strategy: "--continue (fallback)"
2327
+ };
2328
+ }
2329
+ /**
2330
+ * Wait for a pid to exit. No cross-process exit event exists (the agent is owned
2331
+ * by its own wrapper), so poll `isPidAlive` — checked once immediately, then with
2332
+ * golden-ratio backoff (1.0, 1.6, 2.6…s, capped) up to `timeoutMs`. Returns true
2333
+ * once the pid is gone.
2334
+ */
2335
+ async function waitForExit(pid, timeoutMs) {
2336
+ if (!isPidAlive(pid)) return true;
2337
+ const start = Date.now();
2338
+ let delay = 1e3;
2339
+ while (Date.now() - start < timeoutMs) {
2340
+ await new Promise((r) => setTimeout(r, delay));
2341
+ if (!isPidAlive(pid)) return true;
2342
+ delay = Math.min(Math.round(delay * 1.618), 8e3);
2343
+ }
2344
+ return !isPidAlive(pid);
2345
+ }
2286
2346
  async function cmdRestart(rest) {
2287
- const argv = await yargs(rest).usage("Usage: ay restart <keyword>").option("latest", {
2347
+ const argv = await yargs(rest).usage("Usage: ay restart <keyword> [--fresh]").option("latest", {
2288
2348
  type: "boolean",
2289
2349
  default: false,
2290
2350
  description: "Use most recent match"
2291
2351
  }).option("cwd", {
2292
2352
  type: "string",
2293
2353
  description: "Restrict to agents under this dir"
2354
+ }).option("fresh", {
2355
+ type: "boolean",
2356
+ default: false,
2357
+ description: "Replay the original prompt instead of resuming the session"
2294
2358
  }).help(false).version(false).exitProcess(false).parseAsync();
2295
2359
  const opts = {
2296
2360
  all: true,
@@ -2300,13 +2364,32 @@ async function cmdRestart(rest) {
2300
2364
  cwdScope: typeof argv.cwd === "string" ? path.resolve(argv.cwd) : null
2301
2365
  };
2302
2366
  const record = await resolveOne(argv._[0] !== void 0 ? String(argv._[0]) : void 0, opts);
2367
+ const fresh = Boolean(argv.fresh);
2303
2368
  if (isPidAlive(record.pid)) {
2304
- process.stderr.write(`pid ${record.pid} is still running — stop it first or use ay send\n`);
2305
- return 1;
2369
+ await gracefulExitAgent(record, "restart");
2370
+ process.stdout.write(`stopping pid ${record.pid} (${record.cli}) before restart…\n`);
2371
+ let exited = await waitForExit(record.pid, 3e4);
2372
+ if (!exited) {
2373
+ try {
2374
+ process.kill(record.pid, "SIGKILL");
2375
+ } catch {}
2376
+ exited = await waitForExit(record.pid, 5e3);
2377
+ }
2378
+ if (!exited) {
2379
+ process.stderr.write(`pid ${record.pid} did not exit — aborting restart (try: ay stop ${record.pid} --method=double-ctrl-c)\n`);
2380
+ return 1;
2381
+ }
2306
2382
  }
2307
- const args = ["--cli=" + record.cli];
2308
- if (record.prompt) args.push(record.prompt);
2309
- const proc = Bun.spawn(["agent-yes", ...args], {
2383
+ const conf = (await cliDefaults())[record.cli];
2384
+ const { args: resumeArgs, strategy } = resolveResumeArgs(conf, !fresh && record.log_file ? await readFile(record.log_file, "utf8").catch(() => "") : "", {
2385
+ fresh,
2386
+ prompt: record.prompt
2387
+ });
2388
+ const proc = Bun.spawn([
2389
+ "agent-yes",
2390
+ "--cli=" + record.cli,
2391
+ ...resumeArgs
2392
+ ], {
2310
2393
  cwd: record.cwd,
2311
2394
  detached: true,
2312
2395
  stdio: [
@@ -2315,7 +2398,7 @@ async function cmdRestart(rest) {
2315
2398
  "ignore"
2316
2399
  ]
2317
2400
  });
2318
- process.stdout.write(`restarted ${record.cli} in ${shortenPath(record.cwd)} (new pid: ${proc.pid})\n`);
2401
+ process.stdout.write(`restarted ${record.cli} in ${shortenPath(record.cwd)} via ${strategy} (new pid: ${proc.pid})\n`);
2319
2402
  process.stderr.write(`\n ay tail ${proc.pid} # watch output\n ay ls # list all agents\n`);
2320
2403
  return 0;
2321
2404
  }
@@ -2360,6 +2443,7 @@ async function snapshotStatus(record) {
2360
2443
  question = ni.question;
2361
2444
  } else if (state === "idle" && await isAgentStuck(record, logMtimeMs)) state = "stuck";
2362
2445
  }
2446
+ if (alive && record.unresponsive) state = "stuck";
2363
2447
  const note = (await readNotes()).get(record.pid) ?? null;
2364
2448
  return {
2365
2449
  pid: record.pid,
@@ -2625,5 +2709,5 @@ async function cmdResultSet(rest) {
2625
2709
  }
2626
2710
 
2627
2711
  //#endregion
2628
- export { snapshotStatus as C, runSubcommand as S, writeToIpc as T, readPtysize as _, cursorAbs as a, resolveOne as b, extractTaskCounts as c, isExitRequest as d, isPidAlive as f, readNotes as g, matchKeyword as h, controlCodeFromName as i, finalizedLines as l, listRecords as m, READ_PAGE_DEFAULT as n, deriveLiveStatus as o, isSubcommand as p, cmdHelp as r, extractNeedsInput as s, GRACEFUL_EXIT_COMMANDS as t, isAgentStuck as u, renderRawLog as v, stopTipForCli as w, resolveReadWindow as x, renderRawLogLines as y };
2629
- //# sourceMappingURL=subcommands-CapfwHNa.js.map
2712
+ export { runSubcommand as C, writeToIpc as E, resolveResumeArgs as S, stopTipForCli as T, readPtysize as _, cursorAbs as a, resolveOne as b, extractTaskCounts as c, isExitRequest as d, isPidAlive as f, readNotes as g, matchKeyword as h, controlCodeFromName as i, finalizedLines as l, listRecords as m, READ_PAGE_DEFAULT as n, deriveLiveStatus as o, isSubcommand as p, cmdHelp as r, extractNeedsInput as s, GRACEFUL_EXIT_COMMANDS as t, isAgentStuck as u, renderRawLog as v, snapshotStatus as w, resolveReadWindow as x, renderRawLogLines as y };
2713
+ //# sourceMappingURL=subcommands-C4tMi9NX.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 { C as runSubcommand, E as writeToIpc, S as resolveResumeArgs, T as stopTipForCli, _ 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 snapshotStatus, x as resolveReadWindow, y as renderRawLogLines } from "./subcommands-C4tMi9NX.js";
8
+
9
+ export { cmdHelp, isSubcommand, runSubcommand };
@@ -187,4 +187,4 @@ async function startTray() {
187
187
 
188
188
  //#endregion
189
189
  export { ensureTray, startTray };
190
- //# sourceMappingURL=tray-5Hezw9uj.js.map
190
+ //# sourceMappingURL=tray-BMzpUSfa.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-CqRHax_g.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-B-ToHoJ2.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-fqXqTKkh.js";
@@ -1060,7 +1060,7 @@ async function notifyWebhook(status, details, cwd = process.cwd()) {
1060
1060
 
1061
1061
  //#endregion
1062
1062
  //#region ts/index.ts
1063
- const config = await import("./agent-yes.config-BKtCmmy9.js").then((mod) => mod.default || mod);
1063
+ const config = await import("./agent-yes.config-Bl08kMpl.js").then((mod) => mod.default || mod);
1064
1064
  const CLIS_CONFIG = config.clis;
1065
1065
  /**
1066
1066
  * Main function to run agent-cli with automatic yes/no responses
@@ -1802,4 +1802,4 @@ function sleep(ms) {
1802
1802
 
1803
1803
  //#endregion
1804
1804
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1805
- //# sourceMappingURL=ts-C4ZmPc5P.js.map
1805
+ //# sourceMappingURL=ts-U3AE4u72.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.153.2";
10
+ var version = "1.154.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-CqRHax_g.js.map
218
+ //# sourceMappingURL=versionChecker-B-ToHoJ2.js.map
@@ -1,4 +1,4 @@
1
- import { u as parseSecret } from "./e2e-ClOI_aqV.js";
1
+ import { u as parseSecret } from "./e2e-Bfw7qL9O.js";
2
2
 
3
3
  //#region ts/webrtcLink.ts
4
4
  const SIGNAL_SUBPROTOCOL = "ay-signal-1";
@@ -42,4 +42,4 @@ function isWebrtcSpec(spec) {
42
42
 
43
43
  //#endregion
44
44
  export { isWebrtcSpec as n, parseWebrtcLink as r, SIGNAL_SUBPROTOCOL as t };
45
- //# sourceMappingURL=webrtcLink-BWhuA74k.js.map
45
+ //# sourceMappingURL=webrtcLink-CBZkZ-LT.js.map
@@ -1,5 +1,5 @@
1
- import { a as computeTranscriptHash, c as open, d as randomHex, f as seal, l as packEnvelope, n as FLAG_CONFIRM, o as deriveAuthToken, p as unpackEnvelope, s as deriveDirKeys } from "./e2e-ClOI_aqV.js";
2
- import { n as isWebrtcSpec, r as parseWebrtcLink, t as SIGNAL_SUBPROTOCOL } from "./webrtcLink-BWhuA74k.js";
1
+ import { a as computeTranscriptHash, c as open, d as randomHex, f as seal, l as packEnvelope, n as FLAG_CONFIRM, o as deriveAuthToken, p as unpackEnvelope, s as deriveDirKeys } from "./e2e-Bfw7qL9O.js";
2
+ import { n as isWebrtcSpec, r as parseWebrtcLink, t as SIGNAL_SUBPROTOCOL } from "./webrtcLink-CBZkZ-LT.js";
3
3
  import { RTCPeerConnection } from "node-datachannel/polyfill";
4
4
 
5
5
  //#region ts/webrtcRemote.ts
@@ -256,4 +256,4 @@ async function startWebrtcBridge(link) {
256
256
 
257
257
  //#endregion
258
258
  export { startWebrtcBridge };
259
- //# sourceMappingURL=webrtcRemote-jGM3ZHK3.js.map
259
+ //# sourceMappingURL=webrtcRemote-Ccdzmuc-.js.map
@@ -88,4 +88,4 @@ function resolveSpawnCwd(input) {
88
88
 
89
89
  //#endregion
90
90
  export { setWorkspaceRoot as a, resolveSpawnCwd as i, getWorkspaceRoot as n, isProvisionAllowed as r, getProvisionRoot as t };
91
- //# sourceMappingURL=workspaceConfig-B9VX8tVO.js.map
91
+ //# sourceMappingURL=workspaceConfig-DT3cCBZK.js.map
package/lab/ui/index.html CHANGED
@@ -1332,7 +1332,7 @@
1332
1332
  <span class="mag">⌕</span>
1333
1333
  <input
1334
1334
  id="q"
1335
- placeholder="filter… repo:agent-yes claude symval (space = AND)"
1335
+ placeholder="filter… repo:agent-yes claude (space = AND)"
1336
1336
  autocomplete="off"
1337
1337
  autofocus
1338
1338
  />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-yes",
3
- "version": "1.153.2",
3
+ "version": "1.154.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",
@@ -167,7 +167,11 @@ describe("buildRustArgs", () => {
167
167
  });
168
168
 
169
169
  it("does not treat a supported-CLI word after -- as the CLI selector", () => {
170
- const result = buildRustArgs(argv("--cwd", "/tmp/x", "--", "ask", "claude"), "claude", SUPPORTED_CLIS);
170
+ const result = buildRustArgs(
171
+ argv("--cwd", "/tmp/x", "--", "ask", "claude"),
172
+ "claude",
173
+ SUPPORTED_CLIS,
174
+ );
171
175
  expect(result).toEqual(["--cli=claude", "--cwd", "/tmp/x", "--", "ask", "claude"]);
172
176
  });
173
177
  });
@@ -19,6 +19,7 @@ type RawCliConfig = Omit<
19
19
  | "exitCommands"
20
20
  | "autoRetry"
21
21
  | "needsInput"
22
+ | "resumeCommand"
22
23
  > & {
23
24
  ready?: RegexSource[];
24
25
  fatal?: RegexSource[];
@@ -30,6 +31,7 @@ type RawCliConfig = Omit<
30
31
  updateAvailable?: RegexSource[];
31
32
  autoRetry?: RegexSource[];
32
33
  needsInput?: RegexSource[];
34
+ resumeCommand?: RegexSource;
33
35
  exitCommands?: string[];
34
36
  exitCommand?: string[];
35
37
  };
@@ -84,6 +86,7 @@ export function normalizeCliConfig(raw: RawCliConfig): AgentCliConfig {
84
86
  updateAvailable,
85
87
  autoRetry,
86
88
  needsInput,
89
+ resumeCommand,
87
90
  exitCommands,
88
91
  exitCommand,
89
92
  ...rest
@@ -101,6 +104,7 @@ export function normalizeCliConfig(raw: RawCliConfig): AgentCliConfig {
101
104
  updateAvailable: compileRegexList(updateAvailable),
102
105
  autoRetry: compileRegexList(autoRetry),
103
106
  needsInput: compileRegexList(needsInput),
107
+ resumeCommand: resumeCommand === undefined ? undefined : compileRegexSource(resumeCommand),
104
108
  exitCommands: exitCommands ?? exitCommand,
105
109
  };
106
110
  }
@@ -33,6 +33,11 @@ export interface GlobalPidRecord {
33
33
  log_file: string | null;
34
34
  fifo_file?: string | null;
35
35
  status: "active" | "idle" | "exited";
36
+ // Set by the Rust supervisor when the agent produced no PTY output after a
37
+ // high-signal poke / while a "working" spinner is frozen — i.e. it looks
38
+ // wedged. Orthogonal to `status` (which stays "active"); cleared on recovery
39
+ // and on exit. The ls/status live-state derivation surfaces it as `stuck`.
40
+ unresponsive?: boolean;
36
41
  exit_code: number | null;
37
42
  exit_reason: string | null;
38
43
  started_at: number;
package/ts/index.ts CHANGED
@@ -75,6 +75,7 @@ export type AgentCliConfig = {
75
75
  // crash/resuming-session behaviour
76
76
  restoreArgs?: string[]; // arguments to continue the session when crashed
77
77
  restartWithoutContinueArg?: RegExp[]; // array of regex to match for errors that require restart without continue args
78
+ resumeCommand?: RegExp; // optional: scraped from the agent's log on `ay restart`. Capture group 1 is the argument string (whitespace-split) to relaunch with — i.e. a resume command the CLI printed, like "--resume <id>". When absent or unmatched, restart falls back to restoreArgs (e.g. --continue).
78
79
  };
79
80
  export type AgentYesConfig = {
80
81
  configDir?: string; // directory to store agent-yes config files, e.g. session store
@@ -0,0 +1,130 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+
3
+ const spawn = vi.fn();
4
+ const createInterface = vi.fn();
5
+ vi.mock("node:child_process", () => ({ spawn: (...a: unknown[]) => spawn(...a) }));
6
+ vi.mock("node:readline", () => ({
7
+ createInterface: (...a: unknown[]) => createInterface(...a),
8
+ }));
9
+
10
+ import { resolveOpener, openInBrowser, offerOpenInBrowser } from "./openBrowser.ts";
11
+
12
+ const URL = "https://agent-yes.com/w/#room:tok";
13
+
14
+ // A fake child whose .on()/.unref() are inert — enough for openInBrowser's
15
+ // fire-and-forget launch.
16
+ const fakeChild = () => ({ on: vi.fn(), unref: vi.fn() });
17
+ // A fake readline that answers `reply` to the next question() call.
18
+ const fakeRl = (reply: string) => ({
19
+ question: (_q: string, cb: (a: string) => void) => cb(reply),
20
+ close: vi.fn(),
21
+ });
22
+
23
+ // Force a resolvable opener regardless of the host platform: on Linux runners
24
+ // (incl. headless CI) resolveOpener needs a display, so pin one. macOS/Windows
25
+ // resolve an opener anyway, so this is harmless there.
26
+ let savedDisplay: string | undefined;
27
+ beforeEach(() => {
28
+ spawn.mockReset();
29
+ createInterface.mockReset();
30
+ savedDisplay = process.env.DISPLAY;
31
+ process.env.DISPLAY = ":99";
32
+ });
33
+ afterEach(() => {
34
+ if (savedDisplay === undefined) delete process.env.DISPLAY;
35
+ else process.env.DISPLAY = savedDisplay;
36
+ });
37
+
38
+ // resolveOpener is the pure resolution table — the only branching worth pinning.
39
+ describe("resolveOpener", () => {
40
+ it("uses `open` on macOS", () => {
41
+ expect(resolveOpener(URL, "darwin", {})).toEqual({ cmd: "open", args: [URL] });
42
+ });
43
+
44
+ it("uses cmd /c start with an empty title arg on Windows", () => {
45
+ expect(resolveOpener(URL, "win32", {})).toEqual({
46
+ cmd: "cmd",
47
+ args: ["/c", "start", "", URL],
48
+ });
49
+ });
50
+
51
+ it("uses xdg-open on Linux when a display is present (X11 or Wayland)", () => {
52
+ expect(resolveOpener(URL, "linux", { DISPLAY: ":0" })).toEqual({
53
+ cmd: "xdg-open",
54
+ args: [URL],
55
+ });
56
+ expect(resolveOpener(URL, "linux", { WAYLAND_DISPLAY: "wayland-0" })).toEqual({
57
+ cmd: "xdg-open",
58
+ args: [URL],
59
+ });
60
+ });
61
+
62
+ it("returns null on a headless Linux box (no display → nothing to open)", () => {
63
+ expect(resolveOpener(URL, "linux", {})).toBeNull();
64
+ });
65
+ });
66
+
67
+ describe("openInBrowser", () => {
68
+ it("spawns the platform opener detached and returns true when one exists", () => {
69
+ spawn.mockReturnValue(fakeChild());
70
+ expect(openInBrowser(URL)).toBe(true);
71
+ expect(spawn).toHaveBeenCalledOnce();
72
+ });
73
+
74
+ it("returns false (and never spawns) when spawn throws", () => {
75
+ spawn.mockImplementation(() => {
76
+ throw new Error("ENOENT");
77
+ });
78
+ expect(openInBrowser(URL)).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe("offerOpenInBrowser", () => {
83
+ const withTTY = async (fn: () => Promise<void>) => {
84
+ const [si, so] = [process.stdin.isTTY, process.stdout.isTTY];
85
+ process.stdin.isTTY = true;
86
+ process.stdout.isTTY = true;
87
+ try {
88
+ await fn();
89
+ } finally {
90
+ process.stdin.isTTY = si;
91
+ process.stdout.isTTY = so;
92
+ }
93
+ };
94
+
95
+ it("no-ops without prompting when stdin is not a TTY (curl|sh / CI / daemon)", async () => {
96
+ const orig = process.stdin.isTTY;
97
+ process.stdin.isTTY = false;
98
+ try {
99
+ expect(await offerOpenInBrowser(URL)).toBe(false);
100
+ expect(createInterface).not.toHaveBeenCalled();
101
+ } finally {
102
+ process.stdin.isTTY = orig;
103
+ }
104
+ });
105
+
106
+ it("opens on an empty answer (default yes)", async () => {
107
+ await withTTY(async () => {
108
+ createInterface.mockReturnValue(fakeRl(""));
109
+ spawn.mockReturnValue(fakeChild());
110
+ expect(await offerOpenInBrowser(URL)).toBe(true);
111
+ expect(spawn).toHaveBeenCalledOnce();
112
+ });
113
+ });
114
+
115
+ it("opens on an explicit yes", async () => {
116
+ await withTTY(async () => {
117
+ createInterface.mockReturnValue(fakeRl("Y"));
118
+ spawn.mockReturnValue(fakeChild());
119
+ expect(await offerOpenInBrowser(URL)).toBe(true);
120
+ });
121
+ });
122
+
123
+ it("does not open when the operator declines", async () => {
124
+ await withTTY(async () => {
125
+ createInterface.mockReturnValue(fakeRl("n"));
126
+ expect(await offerOpenInBrowser(URL)).toBe(false);
127
+ expect(spawn).not.toHaveBeenCalled();
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,67 @@
1
+ // Open a URL in the operator's default browser, gated behind an interactive
2
+ // prompt. Used after `ay serve --share` prints a WebRTC console link so the
3
+ // operator can jump straight into the console — but only when there's a real
4
+ // terminal to ask on AND a graphical session to open into, so SSH/CI/daemon
5
+ // runs never try to launch a browser they can't show (they just print the link).
6
+ import { spawn } from "node:child_process";
7
+ import { createInterface } from "node:readline";
8
+
9
+ /**
10
+ * The platform command that opens `url` in the default browser, or null when we
11
+ * have no way to (a headless Linux box with no display). Pure — platform/env are
12
+ * injectable so the resolution table is unit-testable.
13
+ */
14
+ export function resolveOpener(
15
+ url: string,
16
+ platform: NodeJS.Platform = process.platform,
17
+ env: NodeJS.ProcessEnv = process.env,
18
+ ): { cmd: string; args: string[] } | null {
19
+ if (platform === "darwin") return { cmd: "open", args: [url] };
20
+ // `start` is a cmd builtin; the empty "" is its title arg so a URL with spaces
21
+ // isn't mistaken for the window title.
22
+ if (platform === "win32") return { cmd: "cmd", args: ["/c", "start", "", url] };
23
+ // Linux/BSD: opening a browser only makes sense inside a graphical session.
24
+ if (!env.DISPLAY && !env.WAYLAND_DISPLAY) return null;
25
+ return { cmd: "xdg-open", args: [url] };
26
+ }
27
+
28
+ /** Fire-and-forget launch of the browser. Returns false when we can't open. */
29
+ export function openInBrowser(url: string): boolean {
30
+ const opener = resolveOpener(url);
31
+ if (!opener) return false;
32
+ try {
33
+ const child = spawn(opener.cmd, opener.args, { stdio: "ignore", detached: true });
34
+ child.on("error", () => {}); // missing opener binary → swallow, never crash the caller
35
+ child.unref();
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Offer to open `url` in the browser, defaulting to yes. No-ops silently unless
44
+ * BOTH stdin/stdout are a TTY (so we can ask) AND a browser opener exists for
45
+ * this platform (so "yes" can actually do something). Non-interactive contexts
46
+ * — `curl | sh`, SSH, CI, the daemon — fall through and just leave the link
47
+ * printed, as the operator chose.
48
+ */
49
+ export async function offerOpenInBrowser(
50
+ url: string,
51
+ prompt = "Open in browser now? [Y/n] ",
52
+ ): Promise<boolean> {
53
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
54
+ if (!resolveOpener(url)) return false;
55
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
56
+ try {
57
+ const answer = (
58
+ await new Promise<string>((resolve) => rl.question(prompt, resolve))
59
+ )
60
+ .trim()
61
+ .toLowerCase();
62
+ if (answer !== "" && answer !== "y" && answer !== "yes") return false;
63
+ } finally {
64
+ rl.close();
65
+ }
66
+ return openInBrowser(url);
67
+ }
package/ts/serve.ts CHANGED
@@ -388,9 +388,9 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
388
388
  /* best effort — fall back to the .share-link file hint in emitShareLink */
389
389
  }
390
390
  }
391
- const emitShareLink = () => {
391
+ const emitShareLink = async () => {
392
392
  if (!webrtcDaemon) return;
393
- if (shareLink)
393
+ if (shareLink) {
394
394
  process.stdout.write(
395
395
  `\nshared over WebRTC — open this link (the token is eaten from the URL on open):\n` +
396
396
  ` ${shareLink}\n` +
@@ -398,7 +398,11 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
398
398
  ? ` (persistent room — same link across restarts; delete ~/.agent-yes/.share-room to rotate)\n`
399
399
  : ``),
400
400
  );
401
- else
401
+ // Offer to jump straight into the console (default yes); no-ops on a
402
+ // non-TTY or headless box, leaving the link printed above.
403
+ const { offerOpenInBrowser } = await import("./openBrowser.ts");
404
+ await offerOpenInBrowser(shareLink);
405
+ } else
402
406
  process.stdout.write(
403
407
  `\nthe WebRTC share link carries a secret, so the daemon does NOT log it —\n` +
404
408
  `read it from ~/.agent-yes/.share-link (mode 0600). The room persists in\n` +
@@ -418,7 +422,7 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
418
422
  if (runningVer === current && sameConfig) {
419
423
  await ensureBootAutostart(mgr);
420
424
  process.stdout.write(`'${DAEMON_NAME}' already running v${current} (up to date)\n`);
421
- emitShareLink();
425
+ await emitShareLink();
422
426
  return 0;
423
427
  }
424
428
  // Outdated, unreachable, or reconfigured → graceful roll-forward. `stop` sends
@@ -528,7 +532,7 @@ async function cmdServeDaemon(sub: string, args: string[]): Promise<number> {
528
532
  }
529
533
  process.stdout.write(` ay serve logs # view server logs\n`);
530
534
  process.stdout.write(` ay serve uninstall # remove daemon\n`);
531
- emitShareLink();
535
+ await emitShareLink();
532
536
  }
533
537
  return code ?? 1;
534
538
  }
@@ -983,7 +987,10 @@ export async function cmdServe(rest: string[]): Promise<number> {
983
987
  const status = await deriveLiveStatus(r);
984
988
  return {
985
989
  ...r,
986
- status,
990
+ // The Rust supervisor's unresponsive flag is an authoritative wedge signal —
991
+ // surface it as `stuck` so the console's dot matches `ay ls`. (A dead agent
992
+ // is never unresponsive — Rust clears the flag on exit.)
993
+ status: status !== "exited" && r.unresponsive ? "stuck" : status,
987
994
  title: await logTitle(r.log_file),
988
995
  git: status === "exited" ? null : await gitStatus(r.cwd),
989
996
  // Task progress from the rendered todo block (null when none detected → no
@@ -1770,6 +1777,12 @@ export async function cmdServe(rest: string[]): Promise<number> {
1770
1777
  ? "\n"
1771
1778
  : ` (persistent room — same link across restarts; delete ~/.agent-yes/.share-room to rotate)\n\n`;
1772
1779
  process.stdout.write(`${wantHttp ? "\n" : ""}${lead}:\n ${link}\n` + persistNote);
1780
+ // Offer to open the console (default yes) on the FIRST share only —
1781
+ // an auto-rotation shouldn't pop a fresh tab from under the operator.
1782
+ if (!rotated) {
1783
+ const { offerOpenInBrowser } = await import("./openBrowser.ts");
1784
+ await offerOpenInBrowser(link);
1785
+ }
1773
1786
  } else {
1774
1787
  // Non-TTY (daemon/journal/CI): the link embeds the room secret S, so never
1775
1788
  // write it to a log stream. Stash it in a 0600 file and point there instead.