claude-yes 1.78.0 → 1.80.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.
@@ -1,6 +1,6 @@
1
- import { t as CLIS_CONFIG } from "./ts-B2M_r506.js";
1
+ import { t as CLIS_CONFIG } from "./ts-DJTdHpX6.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-CoV20GQ7.js";
3
+ import "./versionChecker-iWnXmXoS.js";
4
4
  import "./pidStore-C1JXxoPi.js";
5
5
  import "./globalPidIndex-Cr-g75QF.js";
6
6
 
@@ -9,4 +9,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
9
9
 
10
10
  //#endregion
11
11
  export { SUPPORTED_CLIS };
12
- //# sourceMappingURL=SUPPORTED_CLIS-eyTIFAJl.js.map
12
+ //# sourceMappingURL=SUPPORTED_CLIS-ujFlzMra.js.map
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { n as logger } from "./logger-B9h0djqx.js";
3
- import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-CoV20GQ7.js";
3
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-iWnXmXoS.js";
4
4
  import { argv } from "process";
5
5
  import { execFileSync, spawn } from "child_process";
6
6
  import ms from "ms";
@@ -475,7 +475,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
475
475
  }
476
476
  }
477
477
  {
478
- const { isSubcommand, runSubcommand } = await import("./subcommands-Ctgm4cEn.js");
478
+ const { isSubcommand, runSubcommand } = await import("./subcommands-BVcxPfly.js");
479
479
  if (isSubcommand(process.argv[2])) {
480
480
  const code = await runSubcommand(process.argv);
481
481
  process.exit(code ?? 0);
@@ -504,7 +504,7 @@ if (config.useRust) {
504
504
  }
505
505
  }
506
506
  if (rustBinary) {
507
- const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-eyTIFAJl.js");
507
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-ujFlzMra.js");
508
508
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
509
509
  if (config.verbose) {
510
510
  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-B2M_r506.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-DJTdHpX6.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-CoV20GQ7.js";
3
+ import "./versionChecker-iWnXmXoS.js";
4
4
  import "./pidStore-C1JXxoPi.js";
5
5
  import "./globalPidIndex-Cr-g75QF.js";
6
6
 
@@ -81,7 +81,8 @@ const SUBCOMMANDS = new Set([
81
81
  "cat",
82
82
  "tail",
83
83
  "head",
84
- "send"
84
+ "send",
85
+ "restart"
85
86
  ]);
86
87
  function isSubcommand(name) {
87
88
  return !!name && SUBCOMMANDS.has(name);
@@ -104,6 +105,7 @@ async function runSubcommand(argv) {
104
105
  case "tail": return await cmdRead(rest, { mode: "tail" });
105
106
  case "head": return await cmdRead(rest, { mode: "head" });
106
107
  case "send": return await cmdSend(rest);
108
+ case "restart": return await cmdRestart(rest);
107
109
  default: return null;
108
110
  }
109
111
  } catch (err) {
@@ -125,6 +127,8 @@ function parseArgs(rest) {
125
127
  const next = rest[i + 1];
126
128
  if ([
127
129
  "all",
130
+ "active",
131
+ "follow",
128
132
  "json",
129
133
  "latest"
130
134
  ].includes(key) || !next || next.startsWith("-")) flags[key] = true;
@@ -147,6 +151,7 @@ function parseArgs(rest) {
147
151
  function commonOpts(flags) {
148
152
  return {
149
153
  all: !!flags.all,
154
+ active: !!flags.active,
150
155
  cwdScope: typeof flags.cwd === "string" ? path.resolve(flags.cwd) : flags.cwd === true ? process.cwd() : null,
151
156
  latest: !!flags.latest,
152
157
  json: !!flags.json
@@ -163,7 +168,8 @@ function matchKeyword(record, keyword) {
163
168
  }
164
169
  async function listRecords(keyword, opts) {
165
170
  let records = mergeRecords(await readLocalTsPids(process.cwd()), opts.cwdScope ? await readLocalTsPids(opts.cwdScope) : [], await readGlobalPids());
166
- if (!opts.all) records = records.filter((r) => r.status !== "exited" && isPidAlive(r.pid));
171
+ if (!opts.all) records = records.filter((r) => r.status !== "exited");
172
+ if (opts.active) records = records.filter((r) => isPidAlive(r.pid));
167
173
  if (opts.cwdScope) {
168
174
  const scope = opts.cwdScope;
169
175
  records = records.filter((r) => r.cwd === scope || r.cwd.startsWith(scope + path.sep));
@@ -183,7 +189,7 @@ function isPidAlive(pid) {
183
189
  async function resolveOne(keyword, opts) {
184
190
  if (!keyword) throw new Error("keyword required (pid, cwd substring, cli name, or prompt substring)");
185
191
  const matches = await listRecords(keyword, opts);
186
- if (matches.length === 0) throw new Error(`no running agent matched "${keyword}"`);
192
+ if (matches.length === 0) throw new Error(`no agent matched "${keyword}"`);
187
193
  if (matches.length === 1) return matches[0];
188
194
  if (opts.latest) return matches[0];
189
195
  const lines = matches.slice(0, 10).map((r) => ` ${r.pid} ${r.cli} ${r.cwd}`).join("\n");
@@ -215,18 +221,20 @@ async function cmdLs(rest) {
215
221
  const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
216
222
  const IDLE_THRESHOLD_MS = 60 * 1e3;
217
223
  const rows = await Promise.all(records.map(async (r) => {
218
- let displayStatus = r.status;
219
- if (r.status === "active" && r.log_file) {
224
+ let displayStatus;
225
+ if (!isPidAlive(r.pid)) displayStatus = "stopped";
226
+ else if (r.log_file) {
220
227
  const mtime = await stat(r.log_file).then((s) => s.mtimeMs).catch(() => null);
221
- if (mtime !== null && Date.now() - mtime > IDLE_THRESHOLD_MS) displayStatus = "idle";
222
- }
228
+ displayStatus = mtime !== null && Date.now() - mtime > IDLE_THRESHOLD_MS ? "idle" : "active";
229
+ } else displayStatus = "active";
223
230
  return {
224
231
  pid: String(r.pid),
225
232
  cli: r.cli,
226
233
  status: displayStatus,
227
234
  age: humanizeAge(Date.now() - r.started_at),
228
235
  cwd: shortenPath(r.cwd),
229
- prompt: truncate(r.prompt ?? "", promptBudget)
236
+ prompt: truncate(r.prompt ?? "", promptBudget),
237
+ _alive: displayStatus !== "stopped"
230
238
  };
231
239
  }));
232
240
  const header = [
@@ -246,9 +254,19 @@ async function cmdLs(rest) {
246
254
  r.cwd.padEnd(widths.cwd),
247
255
  r.prompt
248
256
  ].join(" ") + "\n");
249
- if (!opts.json && records.length > 0) {
250
- const example = records[0].pid;
251
- process.stderr.write(`\n cy tail ${example} # view latest output\n cy read ${example} # full rendered log\n cy send ${example} "next: ..." # send a prompt\n cy send ${example} "" --code=ctrl-c # interrupt\n`);
257
+ if (!opts.json && rows.length > 0) {
258
+ const alive = rows.find((r) => r._alive);
259
+ const stopped = rows.find((r) => !r._alive);
260
+ const hints = ["\n"];
261
+ if (alive) {
262
+ hints.push(` cy tail ${alive.pid} # view latest output\n`);
263
+ hints.push(` cy tail -f ${alive.pid} # follow live output\n`);
264
+ hints.push(` cy send ${alive.pid} "next: ..." # send a prompt\n`);
265
+ hints.push(` cy send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
266
+ }
267
+ if (stopped) hints.push(` cy restart ${stopped.pid} # restart stopped agent\n`);
268
+ if (!alive && !stopped) hints.push(` cy ls --all # show exited agents\n`);
269
+ process.stderr.write(hints.join(""));
252
270
  }
253
271
  return 0;
254
272
  }
@@ -274,6 +292,7 @@ async function cmdRead(rest, { mode }) {
274
292
  const { flags, positional } = parseArgs(rest);
275
293
  const opts = commonOpts(flags);
276
294
  const keyword = positional[0];
295
+ const follow = !!(flags.f || flags.follow);
277
296
  const nFlag = typeof flags.n === "string" ? Number(flags.n) : void 0;
278
297
  const n = nFlag !== void 0 && Number.isFinite(nFlag) && nFlag > 0 ? Math.floor(nFlag) : mode === "cat" ? 0 : 96;
279
298
  const record = await resolveOne(keyword, opts);
@@ -286,16 +305,39 @@ async function cmdRead(rest, { mode }) {
286
305
  throw new Error(`pid ${record.pid}: log file not found at ${logPath}`);
287
306
  }
288
307
  if (!stats.isFile()) throw new Error(`pid ${record.pid}: log path is not a file: ${logPath}`);
289
- const rendered = await renderRawLog(await readFile(logPath), {
308
+ const buf = await readFile(logPath);
309
+ const rendered = await renderRawLog(buf, {
290
310
  mode,
291
311
  n
292
312
  });
293
313
  process.stderr.write(`[pid ${record.pid} ${shortenPath(record.cwd)}]\n`);
294
314
  process.stdout.write(rendered);
295
315
  if (!rendered.endsWith("\n")) process.stdout.write("\n");
316
+ if (follow) {
317
+ process.stderr.write(`following... (Ctrl-C to stop)\n`);
318
+ let offset = buf.length;
319
+ const { watch } = await import("fs");
320
+ const ansiRe = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
321
+ const ctrlRe = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
322
+ await new Promise((resolve) => {
323
+ const watcher = watch(logPath, async () => {
324
+ const full = await readFile(logPath);
325
+ if (full.length <= offset) return;
326
+ const chunk = full.slice(offset);
327
+ offset = full.length;
328
+ const text = new TextDecoder().decode(chunk).replace(ansiRe, "").replace(ctrlRe, "");
329
+ if (text.trim()) process.stdout.write(text.trimStart());
330
+ });
331
+ process.on("SIGINT", () => {
332
+ watcher.close();
333
+ resolve();
334
+ });
335
+ });
336
+ return 0;
337
+ }
296
338
  process.stderr.write(`
297
339
  cy ls # list all agents
298
- cy send ${record.pid} "next: ..." # send a prompt\n cy send ${record.pid} "" --code=ctrl-c # interrupt\n`);
340
+ cy tail -f ${record.pid} # follow live output\n cy send ${record.pid} "next: ..." # send a prompt\n cy send ${record.pid} "" --code=ctrl-c # interrupt\n`);
299
341
  return 0;
300
342
  }
301
343
  /**
@@ -344,8 +386,13 @@ async function cmdSend(rest) {
344
386
  const record = await resolveOne(keyword, opts);
345
387
  const fifoPath = record.fifo_file;
346
388
  if (!fifoPath) throw new Error(`pid ${record.pid}: no fifo_file recorded — agent was not started with --stdpush (or was spawned by Rust which doesn't yet support FIFO IPC; see ROADMAP item 10)`);
347
- const payload = (message ?? "") + trailing;
348
- await writeToIpc(fifoPath, payload);
389
+ const body = message ?? "";
390
+ if (body && trailing) {
391
+ await writeToIpc(fifoPath, body);
392
+ await new Promise((r) => setTimeout(r, 200));
393
+ await writeToIpc(fifoPath, trailing);
394
+ } else await writeToIpc(fifoPath, body + trailing);
395
+ const payload = body + trailing;
349
396
  process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
350
397
  process.stderr.write(`\n cy tail ${record.pid} # watch output\n cy ls # list all agents\n`);
351
398
  return 0;
@@ -402,7 +449,34 @@ async function writeToIpc(ipcPath, payload) {
402
449
  }
403
450
  }
404
451
  }
452
+ async function cmdRestart(rest) {
453
+ const { flags, positional } = parseArgs(rest);
454
+ const opts = {
455
+ ...commonOpts(flags),
456
+ all: true
457
+ };
458
+ const keyword = positional[0];
459
+ const record = await resolveOne(keyword, opts);
460
+ if (isPidAlive(record.pid)) {
461
+ process.stderr.write(`pid ${record.pid} is still running — stop it first or use cy send\n`);
462
+ return 1;
463
+ }
464
+ const args = ["--cli=" + record.cli];
465
+ if (record.prompt) args.push(record.prompt);
466
+ const proc = Bun.spawn(["agent-yes", ...args], {
467
+ cwd: record.cwd,
468
+ detached: true,
469
+ stdio: [
470
+ "ignore",
471
+ "ignore",
472
+ "ignore"
473
+ ]
474
+ });
475
+ process.stdout.write(`restarted ${record.cli} in ${shortenPath(record.cwd)} (new pid: ${proc.pid})\n`);
476
+ process.stderr.write(`\n cy tail ${proc.pid} # watch output\n cy ls # list all agents\n`);
477
+ return 0;
478
+ }
405
479
 
406
480
  //#endregion
407
481
  export { isSubcommand, runSubcommand };
408
- //# sourceMappingURL=subcommands-Ctgm4cEn.js.map
482
+ //# sourceMappingURL=subcommands-BVcxPfly.js.map
@@ -1,5 +1,5 @@
1
1
  import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
2
- import { r as getInstalledPackage } from "./versionChecker-CoV20GQ7.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-iWnXmXoS.js";
3
3
  import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-C22d9SRJ.js";
4
4
  import { t as PidStore } from "./pidStore-C1JXxoPi.js";
5
5
  import { arch, platform } from "process";
@@ -1679,4 +1679,4 @@ function sleep(ms) {
1679
1679
 
1680
1680
  //#endregion
1681
1681
  export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
1682
- //# sourceMappingURL=ts-B2M_r506.js.map
1682
+ //# sourceMappingURL=ts-DJTdHpX6.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.78.0";
10
+ var version = "1.80.0";
11
11
 
12
12
  //#endregion
13
13
  //#region ts/versionChecker.ts
@@ -221,4 +221,4 @@ async function displayVersion() {
221
221
 
222
222
  //#endregion
223
223
  export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
224
- //# sourceMappingURL=versionChecker-CoV20GQ7.js.map
224
+ //# sourceMappingURL=versionChecker-iWnXmXoS.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-yes",
3
- "version": "1.78.0",
3
+ "version": "1.80.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",
@@ -206,7 +206,7 @@ describe("subcommands.runSubcommand routing", () => {
206
206
  try {
207
207
  const code = await runSubcommand(["bun", "cli.js", "read", "no-such-agent-keyword"]);
208
208
  expect(code).toBe(1);
209
- expect(stderr.join("")).toMatch(/no running agent matched/);
209
+ expect(stderr.join("")).toMatch(/no agent matched/);
210
210
  } finally {
211
211
  process.stderr.write = orig;
212
212
  }
package/ts/subcommands.ts CHANGED
@@ -76,7 +76,7 @@ function mergeRecords(...buckets: GlobalPidRecord[][]): GlobalPidRecord[] {
76
76
  return Array.from(out.values());
77
77
  }
78
78
 
79
- const SUBCOMMANDS = new Set(["ls", "list", "ps", "read", "cat", "tail", "head", "send"]);
79
+ const SUBCOMMANDS = new Set(["ls", "list", "ps", "read", "cat", "tail", "head", "send", "restart"]);
80
80
 
81
81
  export function isSubcommand(name: string | undefined): boolean {
82
82
  return !!name && SUBCOMMANDS.has(name);
@@ -107,6 +107,8 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
107
107
  return await cmdRead(rest, { mode: "head" });
108
108
  case "send":
109
109
  return await cmdSend(rest);
110
+ case "restart":
111
+ return await cmdRestart(rest);
110
112
  default:
111
113
  return null;
112
114
  }
@@ -123,6 +125,7 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
123
125
 
124
126
  interface CommonOpts {
125
127
  all: boolean;
128
+ active: boolean;
126
129
  cwdScope: string | null;
127
130
  latest: boolean;
128
131
  json: boolean;
@@ -146,7 +149,11 @@ export function parseArgs(rest: string[]): ParsedArgs {
146
149
  const key = arg.slice(2);
147
150
  const next = rest[i + 1];
148
151
  // Boolean flags: --all, --json, --latest
149
- if (["all", "json", "latest"].includes(key) || !next || next.startsWith("-")) {
152
+ if (
153
+ ["all", "active", "follow", "json", "latest"].includes(key) ||
154
+ !next ||
155
+ next.startsWith("-")
156
+ ) {
150
157
  flags[key] = true;
151
158
  } else {
152
159
  flags[key] = next;
@@ -171,6 +178,7 @@ export function parseArgs(rest: string[]): ParsedArgs {
171
178
  function commonOpts(flags: Record<string, string | boolean>): CommonOpts {
172
179
  return {
173
180
  all: !!flags.all,
181
+ active: !!flags.active,
174
182
  cwdScope:
175
183
  typeof flags.cwd === "string"
176
184
  ? path.resolve(flags.cwd)
@@ -210,7 +218,10 @@ async function listRecords(
210
218
  let records = mergeRecords(local, scopeLocal, global);
211
219
 
212
220
  if (!opts.all) {
213
- records = records.filter((r) => r.status !== "exited" && isPidAlive(r.pid));
221
+ records = records.filter((r) => r.status !== "exited");
222
+ }
223
+ if (opts.active) {
224
+ records = records.filter((r) => isPidAlive(r.pid));
214
225
  }
215
226
  if (opts.cwdScope) {
216
227
  const scope = opts.cwdScope;
@@ -237,7 +248,7 @@ async function resolveOne(keyword: string | undefined, opts: CommonOpts): Promis
237
248
  }
238
249
  const matches = await listRecords(keyword, opts);
239
250
  if (matches.length === 0) {
240
- throw new Error(`no running agent matched "${keyword}"`);
251
+ throw new Error(`no agent matched "${keyword}"`);
241
252
  }
242
253
  if (matches.length === 1) return matches[0]!;
243
254
  if (opts.latest) return matches[0]!; // already sorted newest-first
@@ -291,12 +302,17 @@ async function cmdLs(rest: string[]): Promise<number> {
291
302
  const IDLE_THRESHOLD_MS = 60 * 1000;
292
303
  const rows = await Promise.all(
293
304
  records.map(async (r) => {
294
- let displayStatus = r.status;
295
- if (r.status === "active" && r.log_file) {
305
+ let displayStatus: string;
306
+ if (!isPidAlive(r.pid)) {
307
+ displayStatus = "stopped";
308
+ } else if (r.log_file) {
296
309
  const mtime = await stat(r.log_file)
297
310
  .then((s) => s.mtimeMs)
298
311
  .catch(() => null);
299
- if (mtime !== null && Date.now() - mtime > IDLE_THRESHOLD_MS) displayStatus = "idle";
312
+ displayStatus =
313
+ mtime !== null && Date.now() - mtime > IDLE_THRESHOLD_MS ? "idle" : "active";
314
+ } else {
315
+ displayStatus = "active";
300
316
  }
301
317
  return {
302
318
  pid: String(r.pid),
@@ -305,6 +321,7 @@ async function cmdLs(rest: string[]): Promise<number> {
305
321
  age: humanizeAge(Date.now() - r.started_at),
306
322
  cwd: shortenPath(r.cwd),
307
323
  prompt: truncate(r.prompt ?? "", promptBudget),
324
+ _alive: displayStatus !== "stopped",
308
325
  };
309
326
  }),
310
327
  );
@@ -333,15 +350,22 @@ async function cmdLs(rest: string[]): Promise<number> {
333
350
  );
334
351
  }
335
352
 
336
- if (!opts.json && records.length > 0) {
337
- const example = records[0]!.pid;
338
- process.stderr.write(
339
- `\n` +
340
- ` cy tail ${example} # view latest output\n` +
341
- ` cy read ${example} # full rendered log\n` +
342
- ` cy send ${example} "next: ..." # send a prompt\n` +
343
- ` cy send ${example} "" --code=ctrl-c # interrupt\n`,
344
- );
353
+ if (!opts.json && rows.length > 0) {
354
+ const alive = rows.find((r) => r._alive);
355
+ const stopped = rows.find((r) => !r._alive);
356
+ const hints: string[] = ["\n"];
357
+ if (alive) {
358
+ hints.push(` cy tail ${alive.pid} # view latest output\n`);
359
+ hints.push(` cy tail -f ${alive.pid} # follow live output\n`);
360
+ hints.push(` cy send ${alive.pid} "next: ..." # send a prompt\n`);
361
+ hints.push(` cy send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
362
+ }
363
+ if (stopped) {
364
+ hints.push(` cy restart ${stopped.pid} # restart stopped agent\n`);
365
+ }
366
+ if (!alive && !stopped)
367
+ hints.push(` cy ls --all # show exited agents\n`);
368
+ process.stderr.write(hints.join(""));
345
369
  }
346
370
 
347
371
  return 0;
@@ -381,6 +405,7 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
381
405
  const { flags, positional } = parseArgs(rest);
382
406
  const opts = commonOpts(flags);
383
407
  const keyword = positional[0];
408
+ const follow = !!(flags.f || flags.follow);
384
409
 
385
410
  const nFlag = typeof flags.n === "string" ? Number(flags.n) : undefined;
386
411
  const n =
@@ -412,9 +437,35 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
412
437
  process.stdout.write(rendered);
413
438
  if (!rendered.endsWith("\n")) process.stdout.write("\n");
414
439
 
440
+ if (follow) {
441
+ process.stderr.write(`following... (Ctrl-C to stop)\n`);
442
+ let offset = buf.length;
443
+ const { watch } = await import("fs");
444
+ // oxlint-disable-next-line no-control-regex -- intentional: strip ANSI/control
445
+ const ansiRe = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_]/g;
446
+ // oxlint-disable-next-line no-control-regex -- intentional: strip control chars
447
+ const ctrlRe = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
448
+ await new Promise<void>((resolve) => {
449
+ const watcher = watch(logPath, async () => {
450
+ const full = await readFile(logPath);
451
+ if (full.length <= offset) return;
452
+ const chunk = full.slice(offset);
453
+ offset = full.length;
454
+ const text = new TextDecoder().decode(chunk).replace(ansiRe, "").replace(ctrlRe, "");
455
+ if (text.trim()) process.stdout.write(text.trimStart());
456
+ });
457
+ process.on("SIGINT", () => {
458
+ watcher.close();
459
+ resolve();
460
+ });
461
+ });
462
+ return 0;
463
+ }
464
+
415
465
  process.stderr.write(
416
466
  `\n` +
417
467
  ` cy ls # list all agents\n` +
468
+ ` cy tail -f ${record.pid} # follow live output\n` +
418
469
  ` cy send ${record.pid} "next: ..." # send a prompt\n` +
419
470
  ` cy send ${record.pid} "" --code=ctrl-c # interrupt\n`,
420
471
  );
@@ -491,8 +542,15 @@ async function cmdSend(rest: string[]): Promise<number> {
491
542
  );
492
543
  }
493
544
 
494
- const payload = (message ?? "") + trailing;
495
- await writeToIpc(fifoPath, payload);
545
+ const body = message ?? "";
546
+ if (body && trailing) {
547
+ await writeToIpc(fifoPath, body);
548
+ await new Promise((r) => setTimeout(r, 200));
549
+ await writeToIpc(fifoPath, trailing);
550
+ } else {
551
+ await writeToIpc(fifoPath, body + trailing);
552
+ }
553
+ const payload = body + trailing;
496
554
  process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
497
555
 
498
556
  process.stderr.write(
@@ -564,3 +622,38 @@ async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
564
622
  }
565
623
  }
566
624
  }
625
+
626
+ // ---------------------------------------------------------------------------
627
+ // cy restart
628
+ // ---------------------------------------------------------------------------
629
+
630
+ async function cmdRestart(rest: string[]): Promise<number> {
631
+ const { flags, positional } = parseArgs(rest);
632
+ const opts = { ...commonOpts(flags), all: true }; // search stopped agents too
633
+ const keyword = positional[0];
634
+ const record = await resolveOne(keyword, opts);
635
+
636
+ if (isPidAlive(record.pid)) {
637
+ process.stderr.write(`pid ${record.pid} is still running — stop it first or use cy send\n`);
638
+ return 1;
639
+ }
640
+
641
+ const args = ["--cli=" + record.cli];
642
+ if (record.prompt) args.push(record.prompt);
643
+
644
+ const proc = Bun.spawn(["agent-yes", ...args], {
645
+ cwd: record.cwd,
646
+ detached: true,
647
+ stdio: ["ignore", "ignore", "ignore"],
648
+ });
649
+
650
+ process.stdout.write(
651
+ `restarted ${record.cli} in ${shortenPath(record.cwd)} (new pid: ${proc.pid})\n`,
652
+ );
653
+ process.stderr.write(
654
+ `\n` +
655
+ ` cy tail ${proc.pid} # watch output\n` +
656
+ ` cy ls # list all agents\n`,
657
+ );
658
+ return 0;
659
+ }