claude-yes 1.81.0 → 1.83.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-DUtCG3W_.js";
1
+ import { t as CLIS_CONFIG } from "./ts-DbdWuoGq.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-CK3Inq3r.js";
3
+ import "./versionChecker-Ct-4UPeG.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-DuXIXbBo.js.map
12
+ //# sourceMappingURL=SUPPORTED_CLIS-DkXclUge.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-CK3Inq3r.js";
3
+ import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-Ct-4UPeG.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-BT4I9SM0.js");
478
+ const { isSubcommand, runSubcommand } = await import("./subcommands-Vt_yQiEZ.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-DuXIXbBo.js");
507
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-DkXclUge.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-DUtCG3W_.js";
1
+ import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-DbdWuoGq.js";
2
2
  import "./logger-B9h0djqx.js";
3
- import "./versionChecker-CK3Inq3r.js";
3
+ import "./versionChecker-Ct-4UPeG.js";
4
4
  import "./pidStore-C1JXxoPi.js";
5
5
  import "./globalPidIndex-Cr-g75QF.js";
6
6
 
@@ -1,6 +1,6 @@
1
1
  import "./logger-B9h0djqx.js";
2
2
  import { r as readGlobalPids } from "./globalPidIndex-Cr-g75QF.js";
3
- import { readFile, stat } from "fs/promises";
3
+ import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
4
4
  import { homedir } from "os";
5
5
  import path from "path";
6
6
 
@@ -17,6 +17,47 @@ import path from "path";
17
17
  * Returns null when argv[2] is not a known subcommand so cli.ts falls through
18
18
  * to the normal agent-spawning flow.
19
19
  */
20
+ function notesPath() {
21
+ const dir = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
22
+ return path.join(dir, "notes.jsonl");
23
+ }
24
+ async function readNotes() {
25
+ let raw;
26
+ try {
27
+ raw = await readFile(notesPath(), "utf-8");
28
+ } catch {
29
+ return /* @__PURE__ */ new Map();
30
+ }
31
+ const map = /* @__PURE__ */ new Map();
32
+ for (const line of raw.split("\n")) {
33
+ const t = line.trim();
34
+ if (!t) continue;
35
+ try {
36
+ const { pid, note } = JSON.parse(t);
37
+ if (typeof pid === "number") if (note) map.set(pid, note);
38
+ else map.delete(pid);
39
+ } catch {}
40
+ }
41
+ return map;
42
+ }
43
+ async function writeNote(pid, note) {
44
+ const p = notesPath();
45
+ await mkdir(path.dirname(p), { recursive: true });
46
+ await appendFile(p, JSON.stringify({
47
+ pid,
48
+ note,
49
+ updated_at: Date.now()
50
+ }) + "\n");
51
+ }
52
+ async function compactNotes() {
53
+ const map = await readNotes();
54
+ const lines = Array.from(map.entries()).map(([pid, note]) => JSON.stringify({
55
+ pid,
56
+ note,
57
+ updated_at: Date.now()
58
+ })).join("\n");
59
+ await writeFile(notesPath(), lines ? lines + "\n" : "");
60
+ }
20
61
  /**
21
62
  * Read the per-cwd TS PidStore JSONL and convert to the global record shape,
22
63
  * so pre-existing TS agents that were spawned before the global-index mirror
@@ -82,7 +123,8 @@ const SUBCOMMANDS = new Set([
82
123
  "tail",
83
124
  "head",
84
125
  "send",
85
- "restart"
126
+ "restart",
127
+ "note"
86
128
  ]);
87
129
  function isSubcommand(name) {
88
130
  return !!name && SUBCOMMANDS.has(name);
@@ -106,6 +148,7 @@ async function runSubcommand(argv) {
106
148
  case "head": return await cmdRead(rest, { mode: "head" });
107
149
  case "send": return await cmdSend(rest);
108
150
  case "restart": return await cmdRestart(rest);
151
+ case "note": return await cmdNote(rest);
109
152
  default: return null;
110
153
  }
111
154
  } catch (err) {
@@ -220,6 +263,7 @@ async function cmdLs(rest) {
220
263
  const fixedWidth = widths.pid + widths.cli + widths.status + widths.age + widths.cwd + 10;
221
264
  const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
222
265
  const IDLE_THRESHOLD_MS = 60 * 1e3;
266
+ const notes = await readNotes();
223
267
  const rows = await Promise.all(records.map(async (r) => {
224
268
  let displayStatus;
225
269
  if (!isPidAlive(r.pid)) displayStatus = "stopped";
@@ -227,13 +271,22 @@ async function cmdLs(rest) {
227
271
  const mtime = await stat(r.log_file).then((s) => s.mtimeMs).catch(() => null);
228
272
  displayStatus = mtime !== null && Date.now() - mtime > IDLE_THRESHOLD_MS ? "idle" : "active";
229
273
  } else displayStatus = "active";
274
+ const note = notes.get(r.pid);
275
+ let label;
276
+ let hasNote = false;
277
+ if (note) {
278
+ label = truncate(note, promptBudget);
279
+ hasNote = true;
280
+ } else if (r.log_file && displayStatus !== "stopped") label = truncate(await extractActivity(r.log_file) ?? r.prompt ?? "", promptBudget);
281
+ else label = truncate(r.prompt ?? "", promptBudget);
230
282
  return {
231
283
  pid: String(r.pid),
232
284
  cli: r.cli,
233
285
  status: displayStatus,
234
286
  age: humanizeAge(Date.now() - r.started_at),
235
287
  cwd: shortenPath(r.cwd),
236
- prompt: truncate(r.prompt ?? "", promptBudget),
288
+ label,
289
+ hasNote,
237
290
  _alive: displayStatus !== "stopped"
238
291
  };
239
292
  }));
@@ -243,7 +296,7 @@ async function cmdLs(rest) {
243
296
  "STATUS".padEnd(widths.status),
244
297
  "AGE".padEnd(widths.age),
245
298
  "CWD".padEnd(widths.cwd),
246
- "PROMPT"
299
+ "NOTE/PROMPT"
247
300
  ].join(" ") + "\n";
248
301
  process.stdout.write(header);
249
302
  for (const r of rows) process.stdout.write([
@@ -252,7 +305,7 @@ async function cmdLs(rest) {
252
305
  r.status.padEnd(widths.status),
253
306
  r.age.padEnd(widths.age),
254
307
  r.cwd.padEnd(widths.cwd),
255
- r.prompt
308
+ r.hasNote ? `* ${r.label}` : r.label
256
309
  ].join(" ") + "\n");
257
310
  if (!opts.json && rows.length > 0) {
258
311
  const alive = rows.find((r) => r._alive);
@@ -263,6 +316,7 @@ async function cmdLs(rest) {
263
316
  hints.push(` cy tail -f ${alive.pid} # follow live output\n`);
264
317
  hints.push(` cy send ${alive.pid} "next: ..." # send a prompt\n`);
265
318
  hints.push(` cy send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
319
+ hints.push(` cy note ${alive.pid} "what it's doing" # set a note\n`);
266
320
  }
267
321
  if (stopped) hints.push(` cy restart ${stopped.pid} # restart stopped agent\n`);
268
322
  if (!alive && !stopped) hints.push(` cy ls --all # show exited agents\n`);
@@ -310,7 +364,9 @@ async function cmdRead(rest, { mode }) {
310
364
  mode,
311
365
  n
312
366
  });
313
- process.stderr.write(`[pid ${record.pid} ${shortenPath(record.cwd)}]\n`);
367
+ const noteLabel = (await readNotes()).get(record.pid);
368
+ const header = noteLabel ? `[pid ${record.pid} ${shortenPath(record.cwd)} * ${noteLabel}]` : `[pid ${record.pid} ${shortenPath(record.cwd)}]`;
369
+ process.stderr.write(header + "\n");
314
370
  process.stdout.write(rendered);
315
371
  if (!rendered.endsWith("\n")) process.stdout.write("\n");
316
372
  if (follow) {
@@ -376,6 +432,71 @@ async function renderRawLog(buf, { mode, n }) {
376
432
  return lines.slice(0, n).join("\n");
377
433
  }
378
434
  }
435
+ /**
436
+ * Extract a one-line activity summary from a raw log file.
437
+ * Reads only the last 32 KB for speed, renders via xterm for clean output.
438
+ */
439
+ async function extractActivity(logPath) {
440
+ const TAIL_BYTES = 32 * 1024;
441
+ let buf;
442
+ try {
443
+ const fh = await open(logPath, "r");
444
+ try {
445
+ const { size } = await fh.stat();
446
+ if (size === 0) return null;
447
+ if (size <= TAIL_BYTES) {
448
+ const data = await fh.readFile();
449
+ buf = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
450
+ } else {
451
+ const tmp = Buffer.alloc(TAIL_BYTES);
452
+ const { bytesRead } = await fh.read(tmp, 0, TAIL_BYTES, size - TAIL_BYTES);
453
+ buf = new Uint8Array(tmp.buffer, 0, bytesRead);
454
+ }
455
+ } finally {
456
+ await fh.close();
457
+ }
458
+ } catch {
459
+ return null;
460
+ }
461
+ try {
462
+ return extractActivityFromLines((await renderRawLog(buf, {
463
+ mode: "tail",
464
+ n: 40
465
+ })).split("\n"));
466
+ } catch {
467
+ return null;
468
+ }
469
+ }
470
+ function extractActivityFromLines(lines) {
471
+ const isChrome = (l) => {
472
+ const s = l.trim();
473
+ return !s || /^─+$/.test(s) || s.startsWith("? for shortcuts") || /^esc to interrupt/i.test(s) || /\d+%\s*until auto-compact/i.test(s) || /^\/model\s+/i.test(s) || /^⧉\s+In\s+/i.test(s) || /^●\s+(high|medium|low)\s*[·•]/i.test(s) || /^[·•]\s*\d+\s+(left|request)/i.test(s);
474
+ };
475
+ const clean = lines.filter((l) => !isChrome(l));
476
+ const thinkingLine = clean.find((l) => /^[^\w\s❯>⎿✓✗]\s+[A-Z]\w+[….]/u.test(l.trim()) || /still thinking/i.test(l));
477
+ if (thinkingLine) {
478
+ const m = /^.\s+(\w+[^(]*)(?:\s*\(|$)/u.exec(thinkingLine.trim());
479
+ return m ? `✳ ${m[1].trim()}` : "thinking…";
480
+ }
481
+ const promptLines = clean.filter((l) => /^❯\s+/.test(l.trim()));
482
+ if (promptLines.length > 0) {
483
+ const text = promptLines[promptLines.length - 1].trim().replace(/^❯\s+/, "").trim();
484
+ if (text) return `» ${text}`;
485
+ }
486
+ const cookIdx = clean.findIndex((l) => /^✻\s+/.test(l.trim()));
487
+ if (cookIdx >= 0) {
488
+ const window = clean.slice(Math.max(0, cookIdx - 8), cookIdx);
489
+ for (let i = window.length - 1; i >= 0; i--) {
490
+ const l = window[i].trim();
491
+ if (l && !/^[✻✢⧉❯]/.test(l) && !isChrome(l)) return l.length > 80 ? l.slice(0, 79) + "…" : l;
492
+ }
493
+ }
494
+ for (let i = clean.length - 1; i >= 0; i--) {
495
+ const l = clean[i].trim();
496
+ if (l && !/^[─●○◉⧉]/.test(l) && !/^[^\w\s❯>]\s+[A-Z]\w+[….]/u.test(l)) return l.length > 80 ? l.slice(0, 79) + "…" : l;
497
+ }
498
+ return null;
499
+ }
379
500
  async function cmdSend(rest) {
380
501
  const { flags, positional } = parseArgs(rest);
381
502
  const opts = commonOpts(flags);
@@ -481,7 +602,28 @@ async function cmdRestart(rest) {
481
602
  process.stderr.write(`\n cy tail ${proc.pid} # watch output\n cy ls # list all agents\n`);
482
603
  return 0;
483
604
  }
605
+ async function cmdNote(rest) {
606
+ const { flags, positional } = parseArgs(rest);
607
+ const opts = commonOpts(flags);
608
+ const keyword = positional[0];
609
+ const note = positional.slice(1).join(" ");
610
+ if (!keyword) throw new Error("usage: cy note <keyword> [\"note text\"] (omit text to clear)");
611
+ const record = await resolveOne(keyword, {
612
+ ...opts,
613
+ all: true
614
+ });
615
+ if (!note) {
616
+ await writeNote(record.pid, "");
617
+ await compactNotes();
618
+ process.stdout.write(`cleared note for pid ${record.pid}\n`);
619
+ return 0;
620
+ }
621
+ await writeNote(record.pid, note);
622
+ process.stdout.write(`note set for pid ${record.pid}: ${note}\n`);
623
+ process.stderr.write(`\n cy ls # see updated note in list\n`);
624
+ return 0;
625
+ }
484
626
 
485
627
  //#endregion
486
628
  export { isSubcommand, runSubcommand };
487
- //# sourceMappingURL=subcommands-BT4I9SM0.js.map
629
+ //# sourceMappingURL=subcommands-Vt_yQiEZ.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-CK3Inq3r.js";
2
+ import { r as getInstalledPackage } from "./versionChecker-Ct-4UPeG.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-DUtCG3W_.js.map
1682
+ //# sourceMappingURL=ts-DbdWuoGq.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.81.0";
10
+ var version = "1.83.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-CK3Inq3r.js.map
224
+ //# sourceMappingURL=versionChecker-Ct-4UPeG.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-yes",
3
- "version": "1.81.0",
3
+ "version": "1.83.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",
@@ -307,7 +307,7 @@ describe("subcommands.cmdLs human table", () => {
307
307
  } finally {
308
308
  cap.restore();
309
309
  }
310
- expect(cap.text).toMatch(/PID\s+CLI\s+STATUS\s+AGE\s+CWD\s+PROMPT/);
310
+ expect(cap.text).toMatch(/PID\s+CLI\s+STATUS\s+AGE\s+CWD\s+NOTE\/PROMPT/);
311
311
  expect(cap.text).toMatch(new RegExp(`${process.pid}\\s`));
312
312
  expect(cap.text).toMatch(/claude/);
313
313
  expect(cap.text).toMatch(/table format test/);
package/ts/subcommands.ts CHANGED
@@ -11,11 +11,58 @@
11
11
  * to the normal agent-spawning flow.
12
12
  */
13
13
 
14
- import { readFile, stat } from "fs/promises";
14
+ import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises";
15
15
  import { homedir } from "os";
16
16
  import path from "path";
17
17
  import { type GlobalPidRecord, readGlobalPids } from "./globalPidIndex.ts";
18
18
 
19
+ // ---------------------------------------------------------------------------
20
+ // notes store (~/.agent-yes/notes.jsonl)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ function notesPath(): string {
24
+ const dir = process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
25
+ return path.join(dir, "notes.jsonl");
26
+ }
27
+
28
+ async function readNotes(): Promise<Map<number, string>> {
29
+ let raw: string;
30
+ try {
31
+ raw = await readFile(notesPath(), "utf-8");
32
+ } catch {
33
+ return new Map();
34
+ }
35
+ const map = new Map<number, string>();
36
+ for (const line of raw.split("\n")) {
37
+ const t = line.trim();
38
+ if (!t) continue;
39
+ try {
40
+ const { pid, note } = JSON.parse(t);
41
+ if (typeof pid === "number") {
42
+ if (note) map.set(pid, note);
43
+ else map.delete(pid);
44
+ }
45
+ } catch {
46
+ /* skip */
47
+ }
48
+ }
49
+ return map;
50
+ }
51
+
52
+ async function writeNote(pid: number, note: string): Promise<void> {
53
+ const p = notesPath();
54
+ await mkdir(path.dirname(p), { recursive: true });
55
+ await appendFile(p, JSON.stringify({ pid, note, updated_at: Date.now() }) + "\n");
56
+ }
57
+
58
+ async function compactNotes(): Promise<void> {
59
+ const map = await readNotes();
60
+ const lines = Array.from(map.entries())
61
+ .map(([pid, note]) => JSON.stringify({ pid, note, updated_at: Date.now() }))
62
+ .join("\n");
63
+ await writeFile(notesPath(), lines ? lines + "\n" : "");
64
+ }
65
+
19
66
  /**
20
67
  * Read the per-cwd TS PidStore JSONL and convert to the global record shape,
21
68
  * so pre-existing TS agents that were spawned before the global-index mirror
@@ -76,7 +123,18 @@ function mergeRecords(...buckets: GlobalPidRecord[][]): GlobalPidRecord[] {
76
123
  return Array.from(out.values());
77
124
  }
78
125
 
79
- const SUBCOMMANDS = new Set(["ls", "list", "ps", "read", "cat", "tail", "head", "send", "restart"]);
126
+ const SUBCOMMANDS = new Set([
127
+ "ls",
128
+ "list",
129
+ "ps",
130
+ "read",
131
+ "cat",
132
+ "tail",
133
+ "head",
134
+ "send",
135
+ "restart",
136
+ "note",
137
+ ]);
80
138
 
81
139
  export function isSubcommand(name: string | undefined): boolean {
82
140
  return !!name && SUBCOMMANDS.has(name);
@@ -109,6 +167,8 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
109
167
  return await cmdSend(rest);
110
168
  case "restart":
111
169
  return await cmdRestart(rest);
170
+ case "note":
171
+ return await cmdNote(rest);
112
172
  default:
113
173
  return null;
114
174
  }
@@ -300,6 +360,7 @@ async function cmdLs(rest: string[]): Promise<number> {
300
360
  const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
301
361
 
302
362
  const IDLE_THRESHOLD_MS = 60 * 1000;
363
+ const notes = await readNotes();
303
364
  const rows = await Promise.all(
304
365
  records.map(async (r) => {
305
366
  let displayStatus: string;
@@ -314,13 +375,26 @@ async function cmdLs(rest: string[]): Promise<number> {
314
375
  } else {
315
376
  displayStatus = "active";
316
377
  }
378
+ const note = notes.get(r.pid);
379
+ let label: string;
380
+ let hasNote = false;
381
+ if (note) {
382
+ label = truncate(note, promptBudget);
383
+ hasNote = true;
384
+ } else if (r.log_file && displayStatus !== "stopped") {
385
+ const activity = await extractActivity(r.log_file);
386
+ label = truncate(activity ?? r.prompt ?? "", promptBudget);
387
+ } else {
388
+ label = truncate(r.prompt ?? "", promptBudget);
389
+ }
317
390
  return {
318
391
  pid: String(r.pid),
319
392
  cli: r.cli,
320
393
  status: displayStatus,
321
394
  age: humanizeAge(Date.now() - r.started_at),
322
395
  cwd: shortenPath(r.cwd),
323
- prompt: truncate(r.prompt ?? "", promptBudget),
396
+ label,
397
+ hasNote,
324
398
  _alive: displayStatus !== "stopped",
325
399
  };
326
400
  }),
@@ -333,7 +407,7 @@ async function cmdLs(rest: string[]): Promise<number> {
333
407
  "STATUS".padEnd(widths.status),
334
408
  "AGE".padEnd(widths.age),
335
409
  "CWD".padEnd(widths.cwd),
336
- "PROMPT",
410
+ "NOTE/PROMPT",
337
411
  ].join(" ") + "\n";
338
412
  process.stdout.write(header);
339
413
 
@@ -345,7 +419,7 @@ async function cmdLs(rest: string[]): Promise<number> {
345
419
  r.status.padEnd(widths.status),
346
420
  r.age.padEnd(widths.age),
347
421
  r.cwd.padEnd(widths.cwd),
348
- r.prompt,
422
+ r.hasNote ? `* ${r.label}` : r.label,
349
423
  ].join(" ") + "\n",
350
424
  );
351
425
  }
@@ -359,6 +433,7 @@ async function cmdLs(rest: string[]): Promise<number> {
359
433
  hints.push(` cy tail -f ${alive.pid} # follow live output\n`);
360
434
  hints.push(` cy send ${alive.pid} "next: ..." # send a prompt\n`);
361
435
  hints.push(` cy send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
436
+ hints.push(` cy note ${alive.pid} "what it's doing" # set a note\n`);
362
437
  }
363
438
  if (stopped) {
364
439
  hints.push(` cy restart ${stopped.pid} # restart stopped agent\n`);
@@ -433,7 +508,12 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
433
508
 
434
509
  const buf = await readFile(logPath);
435
510
  const rendered = await renderRawLog(buf, { mode, n });
436
- process.stderr.write(`[pid ${record.pid} ${shortenPath(record.cwd)}]\n`);
511
+ const notes = await readNotes();
512
+ const noteLabel = notes.get(record.pid);
513
+ const header = noteLabel
514
+ ? `[pid ${record.pid} ${shortenPath(record.cwd)} * ${noteLabel}]`
515
+ : `[pid ${record.pid} ${shortenPath(record.cwd)}]`;
516
+ process.stderr.write(header + "\n");
437
517
  process.stdout.write(rendered);
438
518
  if (!rendered.endsWith("\n")) process.stdout.write("\n");
439
519
 
@@ -518,6 +598,112 @@ async function renderRawLog(
518
598
  }
519
599
  }
520
600
 
601
+ // ---------------------------------------------------------------------------
602
+ // activity extraction
603
+ // ---------------------------------------------------------------------------
604
+
605
+ /**
606
+ * Extract a one-line activity summary from a raw log file.
607
+ * Reads only the last 32 KB for speed, renders via xterm for clean output.
608
+ */
609
+ async function extractActivity(logPath: string): Promise<string | null> {
610
+ const TAIL_BYTES = 32 * 1024;
611
+ let buf: Uint8Array;
612
+ try {
613
+ const fh = await open(logPath, "r");
614
+ try {
615
+ const { size } = await fh.stat();
616
+ if (size === 0) return null;
617
+ if (size <= TAIL_BYTES) {
618
+ const data = await fh.readFile();
619
+ buf = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
620
+ } else {
621
+ const tmp = Buffer.alloc(TAIL_BYTES);
622
+ const { bytesRead } = await fh.read(tmp, 0, TAIL_BYTES, size - TAIL_BYTES);
623
+ buf = new Uint8Array(tmp.buffer, 0, bytesRead);
624
+ }
625
+ } finally {
626
+ await fh.close();
627
+ }
628
+ } catch {
629
+ return null;
630
+ }
631
+
632
+ try {
633
+ const rendered = await renderRawLog(buf, { mode: "tail", n: 40 });
634
+ return extractActivityFromLines(rendered.split("\n"));
635
+ } catch {
636
+ return null;
637
+ }
638
+ }
639
+
640
+ function extractActivityFromLines(lines: string[]): string | null {
641
+ // Claude Code UI chrome: these lines carry no meaningful activity info
642
+ const isChrome = (l: string): boolean => {
643
+ const s = l.trim();
644
+ return (
645
+ !s ||
646
+ /^─+$/.test(s) ||
647
+ s.startsWith("? for shortcuts") ||
648
+ /^esc to interrupt/i.test(s) ||
649
+ /\d+%\s*until auto-compact/i.test(s) ||
650
+ /^\/model\s+/i.test(s) ||
651
+ /^⧉\s+In\s+/i.test(s) ||
652
+ /^●\s+(high|medium|low)\s*[·•]/i.test(s) ||
653
+ /^[·•]\s*\d+\s+(left|request)/i.test(s)
654
+ );
655
+ };
656
+
657
+ const clean = lines.filter((l) => !isChrome(l));
658
+
659
+ // Priority 1: thinking/composing spinner active
660
+ // Claude Code cycles through various Unicode dingbats for its spinner (✢✳✶✻✷…).
661
+ // The format is always: SPINNER_CHAR Verb… (timing…)
662
+ // Require ellipsis after the verb so we don't false-positive on normal text
663
+ // that happens to contain one of these chars mid-sentence.
664
+ const thinkingLine = clean.find(
665
+ (l) => /^[^\w\s❯>⎿✓✗]\s+[A-Z]\w+[….]/u.test(l.trim()) || /still thinking/i.test(l),
666
+ );
667
+ if (thinkingLine) {
668
+ const m = /^.\s+(\w+[^(]*)(?:\s*\(|$)/u.exec(thinkingLine.trim());
669
+ return m ? `✳ ${m[1].trim()}` : "thinking…";
670
+ }
671
+
672
+ // Priority 2: last ❯ prompt line means agent is idle, waiting for next input
673
+ const promptLines = clean.filter((l) => /^❯\s+/.test(l.trim()));
674
+ if (promptLines.length > 0) {
675
+ const text = promptLines[promptLines.length - 1]!.trim()
676
+ .replace(/^❯\s+/, "")
677
+ .trim();
678
+ if (text) return `» ${text}`;
679
+ }
680
+
681
+ // Priority 3: ✻ spinner just finished — show nearby context
682
+ const cookIdx = clean.findIndex((l) => /^✻\s+/.test(l.trim()));
683
+ if (cookIdx >= 0) {
684
+ const window = clean.slice(Math.max(0, cookIdx - 8), cookIdx);
685
+ for (let i = window.length - 1; i >= 0; i--) {
686
+ const l = window[i]!.trim();
687
+ if (l && !/^[✻✢⧉❯]/.test(l) && !isChrome(l)) {
688
+ return l.length > 80 ? l.slice(0, 79) + "…" : l;
689
+ }
690
+ }
691
+ }
692
+
693
+ // Priority 4: last meaningful non-icon line
694
+ for (let i = clean.length - 1; i >= 0; i--) {
695
+ const l = clean[i]!.trim();
696
+ // Skip lines that look like spinner patterns (caught by priority 1 above)
697
+ // and status dots/separators; everything else (including ⎿ tool sub-output
698
+ // and non-ASCII text like Japanese) is fair game as meaningful content.
699
+ if (l && !/^[─●○◉⧉]/.test(l) && !/^[^\w\s❯>]\s+[A-Z]\w+[….]/u.test(l)) {
700
+ return l.length > 80 ? l.slice(0, 79) + "…" : l;
701
+ }
702
+ }
703
+
704
+ return null;
705
+ }
706
+
521
707
  // ---------------------------------------------------------------------------
522
708
  // cy send
523
709
  // ---------------------------------------------------------------------------
@@ -665,3 +851,31 @@ async function cmdRestart(rest: string[]): Promise<number> {
665
851
  );
666
852
  return 0;
667
853
  }
854
+
855
+ // ---------------------------------------------------------------------------
856
+ // cy note
857
+ // ---------------------------------------------------------------------------
858
+
859
+ async function cmdNote(rest: string[]): Promise<number> {
860
+ const { flags, positional } = parseArgs(rest);
861
+ const opts = commonOpts(flags);
862
+ const keyword = positional[0];
863
+ const note = positional.slice(1).join(" ");
864
+
865
+ if (!keyword) throw new Error('usage: cy note <keyword> ["note text"] (omit text to clear)');
866
+
867
+ const record = await resolveOne(keyword, { ...opts, all: true });
868
+
869
+ if (!note) {
870
+ // clear
871
+ await writeNote(record.pid, "");
872
+ await compactNotes();
873
+ process.stdout.write(`cleared note for pid ${record.pid}\n`);
874
+ return 0;
875
+ }
876
+
877
+ await writeNote(record.pid, note);
878
+ process.stdout.write(`note set for pid ${record.pid}: ${note}\n`);
879
+ process.stderr.write(`\n cy ls # see updated note in list\n`);
880
+ return 0;
881
+ }