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.
- package/dist/{SUPPORTED_CLIS-DuXIXbBo.js → SUPPORTED_CLIS-DkXclUge.js} +3 -3
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{subcommands-BT4I9SM0.js → subcommands-Vt_yQiEZ.js} +149 -7
- package/dist/{ts-DUtCG3W_.js → ts-DbdWuoGq.js} +2 -2
- package/dist/{versionChecker-CK3Inq3r.js → versionChecker-Ct-4UPeG.js} +2 -2
- package/package.json +1 -1
- package/ts/subcommands.spec.ts +1 -1
- package/ts/subcommands.ts +220 -6
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-DbdWuoGq.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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-
|
|
224
|
+
//# sourceMappingURL=versionChecker-Ct-4UPeG.js.map
|
package/package.json
CHANGED
package/ts/subcommands.spec.ts
CHANGED
|
@@ -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([
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|