claude-yes 1.82.0 → 1.84.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-WKiG4dyQ.js → SUPPORTED_CLIS-DM0fJTMR.js} +3 -3
- package/dist/cli.js +8 -3
- package/dist/index.js +2 -2
- package/dist/{subcommands-eMrWDGMq.js → subcommands-DjO8lthH.js} +188 -27
- package/dist/{ts-WV8trEPX.js → ts-Bw6gQKyU.js} +17 -3
- package/dist/{versionChecker-BVt2a2mL.js → versionChecker-CspuhOwO.js} +2 -2
- package/package.json +1 -1
- package/ts/index.ts +21 -0
- package/ts/parseCliArgs.ts +7 -0
- package/ts/subcommands.ts +292 -35
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-Bw6gQKyU.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-CspuhOwO.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-DM0fJTMR.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-CspuhOwO.js";
|
|
4
4
|
import { argv } from "process";
|
|
5
5
|
import { execFileSync, spawn } from "child_process";
|
|
6
6
|
import ms from "ms";
|
|
@@ -42,6 +42,10 @@ function parseCliArgs(argv, supportedClis) {
|
|
|
42
42
|
type: "boolean",
|
|
43
43
|
description: "Prepend SKILL.md header from current directory to the prompt (helpful for non-Claude agents)",
|
|
44
44
|
default: false
|
|
45
|
+
}).option("swarm-hint", {
|
|
46
|
+
type: "boolean",
|
|
47
|
+
description: "Inject peer discovery hint into agent system prompt when other agents are running (use --no-swarm-hint to opt out)",
|
|
48
|
+
default: true
|
|
45
49
|
}).option("timeout", {
|
|
46
50
|
type: "string",
|
|
47
51
|
description: "Exit after a period of inactivity, e.g., \"5s\" or \"1m\"",
|
|
@@ -221,6 +225,7 @@ function parseCliArgs(argv, supportedClis) {
|
|
|
221
225
|
verbose: parsedArgv.verbose,
|
|
222
226
|
resume: parsedArgv.continue,
|
|
223
227
|
useSkills: parsedArgv.useSkills,
|
|
228
|
+
swarmHint: parsedArgv.swarmHint,
|
|
224
229
|
appendPrompt: parsedArgv.appendPrompt,
|
|
225
230
|
useStdinAppend: Boolean(parsedArgv.stdpush || parsedArgv.ipc || parsedArgv.fifo),
|
|
226
231
|
showVersion: parsedArgv.version,
|
|
@@ -475,7 +480,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
475
480
|
}
|
|
476
481
|
}
|
|
477
482
|
{
|
|
478
|
-
const { isSubcommand, runSubcommand } = await import("./subcommands-
|
|
483
|
+
const { isSubcommand, runSubcommand } = await import("./subcommands-DjO8lthH.js");
|
|
479
484
|
if (isSubcommand(process.argv[2])) {
|
|
480
485
|
const code = await runSubcommand(process.argv);
|
|
481
486
|
process.exit(code ?? 0);
|
|
@@ -504,7 +509,7 @@ if (config.useRust) {
|
|
|
504
509
|
}
|
|
505
510
|
}
|
|
506
511
|
if (rustBinary) {
|
|
507
|
-
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-
|
|
512
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-DM0fJTMR.js");
|
|
508
513
|
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
509
514
|
if (config.verbose) {
|
|
510
515
|
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-Bw6gQKyU.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-CspuhOwO.js";
|
|
4
4
|
import "./pidStore-C1JXxoPi.js";
|
|
5
5
|
import "./globalPidIndex-Cr-g75QF.js";
|
|
6
6
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import "./logger-B9h0djqx.js";
|
|
2
2
|
import { r as readGlobalPids } from "./globalPidIndex-Cr-g75QF.js";
|
|
3
|
-
import { appendFile, mkdir, readFile, stat, writeFile } 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
|
|
|
7
7
|
//#region ts/subcommands.ts
|
|
8
8
|
/**
|
|
9
|
-
* `
|
|
9
|
+
* `ay ls / read / cat / tail / head / send` subcommand implementations.
|
|
10
10
|
*
|
|
11
11
|
* Mirrors the principles of koho's `terminal-ws-lib.ts` (session list, render
|
|
12
12
|
* via @xterm/headless, keyword-keyed input) — but file-based instead of via
|
|
@@ -61,7 +61,7 @@ async function compactNotes() {
|
|
|
61
61
|
/**
|
|
62
62
|
* Read the per-cwd TS PidStore JSONL and convert to the global record shape,
|
|
63
63
|
* so pre-existing TS agents that were spawned before the global-index mirror
|
|
64
|
-
* shipped still show up in `
|
|
64
|
+
* shipped still show up in `ay ls`. Merging is done in `mergeRecords`.
|
|
65
65
|
*/
|
|
66
66
|
async function readLocalTsPids(cwd) {
|
|
67
67
|
const jsonlPath = path.join(cwd, ".agent-yes", "pid-records.jsonl");
|
|
@@ -118,6 +118,7 @@ const SUBCOMMANDS = new Set([
|
|
|
118
118
|
"ls",
|
|
119
119
|
"list",
|
|
120
120
|
"ps",
|
|
121
|
+
"status",
|
|
121
122
|
"read",
|
|
122
123
|
"cat",
|
|
123
124
|
"tail",
|
|
@@ -126,6 +127,7 @@ const SUBCOMMANDS = new Set([
|
|
|
126
127
|
"restart",
|
|
127
128
|
"note"
|
|
128
129
|
]);
|
|
130
|
+
const IDLE_THRESHOLD_MS = 60 * 1e3;
|
|
129
131
|
function isSubcommand(name) {
|
|
130
132
|
return !!name && SUBCOMMANDS.has(name);
|
|
131
133
|
}
|
|
@@ -142,6 +144,7 @@ async function runSubcommand(argv) {
|
|
|
142
144
|
case "ls":
|
|
143
145
|
case "list":
|
|
144
146
|
case "ps": return await cmdLs(rest);
|
|
147
|
+
case "status": return await cmdStatus(rest);
|
|
145
148
|
case "read":
|
|
146
149
|
case "cat": return await cmdRead(rest, { mode: "cat" });
|
|
147
150
|
case "tail": return await cmdRead(rest, { mode: "tail" });
|
|
@@ -153,7 +156,7 @@ async function runSubcommand(argv) {
|
|
|
153
156
|
}
|
|
154
157
|
} catch (err) {
|
|
155
158
|
const msg = err instanceof Error ? err.message : String(err);
|
|
156
|
-
process.stderr.write(`
|
|
159
|
+
process.stderr.write(`ay ${sub}: ${msg}\n`);
|
|
157
160
|
return 1;
|
|
158
161
|
}
|
|
159
162
|
}
|
|
@@ -173,7 +176,8 @@ function parseArgs(rest) {
|
|
|
173
176
|
"active",
|
|
174
177
|
"follow",
|
|
175
178
|
"json",
|
|
176
|
-
"latest"
|
|
179
|
+
"latest",
|
|
180
|
+
"watch"
|
|
177
181
|
].includes(key) || !next || next.startsWith("-")) flags[key] = true;
|
|
178
182
|
else {
|
|
179
183
|
flags[key] = next;
|
|
@@ -262,7 +266,6 @@ async function cmdLs(rest) {
|
|
|
262
266
|
};
|
|
263
267
|
const fixedWidth = widths.pid + widths.cli + widths.status + widths.age + widths.cwd + 10;
|
|
264
268
|
const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
|
|
265
|
-
const IDLE_THRESHOLD_MS = 60 * 1e3;
|
|
266
269
|
const notes = await readNotes();
|
|
267
270
|
const rows = await Promise.all(records.map(async (r) => {
|
|
268
271
|
let displayStatus;
|
|
@@ -271,8 +274,14 @@ async function cmdLs(rest) {
|
|
|
271
274
|
const mtime = await stat(r.log_file).then((s) => s.mtimeMs).catch(() => null);
|
|
272
275
|
displayStatus = mtime !== null && Date.now() - mtime > IDLE_THRESHOLD_MS ? "idle" : "active";
|
|
273
276
|
} else displayStatus = "active";
|
|
274
|
-
const
|
|
275
|
-
|
|
277
|
+
const note = notes.get(r.pid);
|
|
278
|
+
let label;
|
|
279
|
+
let hasNote = false;
|
|
280
|
+
if (note) {
|
|
281
|
+
label = truncate(note, promptBudget);
|
|
282
|
+
hasNote = true;
|
|
283
|
+
} else if (r.log_file && displayStatus !== "stopped") label = truncate(await extractActivity(r.log_file) ?? (r.prompt ? `→ ${r.prompt}` : ""), promptBudget);
|
|
284
|
+
else label = truncate(r.prompt ? `→ ${r.prompt}` : "", promptBudget);
|
|
276
285
|
return {
|
|
277
286
|
pid: String(r.pid),
|
|
278
287
|
cli: r.cli,
|
|
@@ -306,14 +315,17 @@ async function cmdLs(rest) {
|
|
|
306
315
|
const stopped = rows.find((r) => !r._alive);
|
|
307
316
|
const hints = ["\n"];
|
|
308
317
|
if (alive) {
|
|
309
|
-
hints.push(`
|
|
310
|
-
hints.push(`
|
|
311
|
-
hints.push(`
|
|
312
|
-
hints.push(`
|
|
313
|
-
hints.push(`
|
|
318
|
+
hints.push(` ay status ${alive.pid} # JSON status snapshot\n`);
|
|
319
|
+
hints.push(` ay status ${alive.pid} --watch # stream changes as JSON\n`);
|
|
320
|
+
hints.push(` ay tail ${alive.pid} # view latest output\n`);
|
|
321
|
+
hints.push(` ay tail -f ${alive.pid} # follow live output\n`);
|
|
322
|
+
hints.push(` ay send ${alive.pid} "next: ..." # send a prompt (keyword: pid, cwd, or prompt substring)\n`);
|
|
323
|
+
hints.push(` ay send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
|
|
324
|
+
hints.push(` ay note ${alive.pid} "what it's doing" # set a note\n`);
|
|
325
|
+
hints.push(` ay ls --json # machine-readable list for scripts/agents\n`);
|
|
314
326
|
}
|
|
315
|
-
if (stopped) hints.push(`
|
|
316
|
-
if (!alive && !stopped) hints.push(`
|
|
327
|
+
if (stopped) hints.push(` ay restart ${stopped.pid} # restart stopped agent\n`);
|
|
328
|
+
if (!alive && !stopped) hints.push(` ay ls --all # show exited agents\n`);
|
|
317
329
|
process.stderr.write(hints.join(""));
|
|
318
330
|
}
|
|
319
331
|
return 0;
|
|
@@ -386,8 +398,8 @@ async function cmdRead(rest, { mode }) {
|
|
|
386
398
|
return 0;
|
|
387
399
|
}
|
|
388
400
|
process.stderr.write(`
|
|
389
|
-
|
|
390
|
-
|
|
401
|
+
ay ls # list all agents
|
|
402
|
+
ay tail -f ${record.pid} # follow live output\n ay send ${record.pid} "next: ..." # send a prompt\n ay send ${record.pid} "" --code=ctrl-c # interrupt\n`);
|
|
391
403
|
return 0;
|
|
392
404
|
}
|
|
393
405
|
/**
|
|
@@ -426,12 +438,85 @@ async function renderRawLog(buf, { mode, n }) {
|
|
|
426
438
|
return lines.slice(0, n).join("\n");
|
|
427
439
|
}
|
|
428
440
|
}
|
|
441
|
+
/**
|
|
442
|
+
* Extract a one-line activity summary from a raw log file.
|
|
443
|
+
* Reads only the last 32 KB for speed, renders via xterm for clean output.
|
|
444
|
+
*/
|
|
445
|
+
async function extractActivity(logPath) {
|
|
446
|
+
const TAIL_BYTES = 32 * 1024;
|
|
447
|
+
let buf;
|
|
448
|
+
try {
|
|
449
|
+
const fh = await open(logPath, "r");
|
|
450
|
+
try {
|
|
451
|
+
const { size } = await fh.stat();
|
|
452
|
+
if (size === 0) return null;
|
|
453
|
+
if (size <= TAIL_BYTES) {
|
|
454
|
+
const data = await fh.readFile();
|
|
455
|
+
buf = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
456
|
+
} else {
|
|
457
|
+
const tmp = Buffer.alloc(TAIL_BYTES);
|
|
458
|
+
const { bytesRead } = await fh.read(tmp, 0, TAIL_BYTES, size - TAIL_BYTES);
|
|
459
|
+
buf = new Uint8Array(tmp.buffer, 0, bytesRead);
|
|
460
|
+
}
|
|
461
|
+
} finally {
|
|
462
|
+
await fh.close();
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
return extractActivityFromLines((await renderRawLog(buf, {
|
|
469
|
+
mode: "tail",
|
|
470
|
+
n: 40
|
|
471
|
+
})).split("\n"));
|
|
472
|
+
} catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function extractActivityFromLines(lines) {
|
|
477
|
+
const isChrome = (l) => {
|
|
478
|
+
const s = l.trim();
|
|
479
|
+
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);
|
|
480
|
+
};
|
|
481
|
+
const clean = lines.filter((l) => !isChrome(l));
|
|
482
|
+
const isSpinnerLine = (l) => /^[^\w\s❯>⎿✓✗]\s+[A-Z]\w+[….]/u.test(l.trim()) || /still thinking/i.test(l);
|
|
483
|
+
let lastPromptIdx = -1;
|
|
484
|
+
let lastSpinnerIdx = -1;
|
|
485
|
+
for (let i = clean.length - 1; i >= 0; i--) {
|
|
486
|
+
const l = clean[i].trim();
|
|
487
|
+
if (lastPromptIdx === -1 && l.startsWith("❯")) lastPromptIdx = i;
|
|
488
|
+
if (lastSpinnerIdx === -1 && isSpinnerLine(l)) lastSpinnerIdx = i;
|
|
489
|
+
if (lastPromptIdx !== -1 && lastSpinnerIdx !== -1) break;
|
|
490
|
+
}
|
|
491
|
+
if (lastPromptIdx > lastSpinnerIdx) {
|
|
492
|
+
const text = clean[lastPromptIdx].trim().replace(/^❯\s*/, "").trim();
|
|
493
|
+
return text ? `» ${text}` : null;
|
|
494
|
+
}
|
|
495
|
+
const thinkingLine = clean.find((l) => isSpinnerLine(l));
|
|
496
|
+
if (thinkingLine) {
|
|
497
|
+
const m = /^.\s+(\w+[^(]*)(?:\s*\(|$)/u.exec(thinkingLine.trim());
|
|
498
|
+
return m?.[1] ? `✳ ${m[1].trim()}` : "thinking…";
|
|
499
|
+
}
|
|
500
|
+
const cookIdx = clean.findIndex((l) => /^✻\s+/.test(l.trim()));
|
|
501
|
+
if (cookIdx >= 0) {
|
|
502
|
+
const window = clean.slice(Math.max(0, cookIdx - 8), cookIdx);
|
|
503
|
+
for (let i = window.length - 1; i >= 0; i--) {
|
|
504
|
+
const l = window[i].trim();
|
|
505
|
+
if (l && !/^[✻✢⧉❯]/.test(l) && !isChrome(l)) return l.length > 80 ? l.slice(0, 79) + "…" : l;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (let i = clean.length - 1; i >= 0; i--) {
|
|
509
|
+
const l = clean[i].trim();
|
|
510
|
+
if (l && !/^[─●○◉⧉]/.test(l) && !/^[^\w\s❯>]\s+[A-Z]\w+[….]/u.test(l)) return l.length > 80 ? l.slice(0, 79) + "…" : l;
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
429
514
|
async function cmdSend(rest) {
|
|
430
515
|
const { flags, positional } = parseArgs(rest);
|
|
431
516
|
const opts = commonOpts(flags);
|
|
432
517
|
const keyword = positional[0];
|
|
433
518
|
const rawMessage = positional.slice(1).join(" ");
|
|
434
|
-
if (!keyword) throw new Error("usage:
|
|
519
|
+
if (!keyword) throw new Error("usage: ay send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
|
|
435
520
|
const trailing = controlCodeFromName(typeof flags.code === "string" ? flags.code.toLowerCase() : "enter");
|
|
436
521
|
const record = await resolveOne(keyword, opts);
|
|
437
522
|
const fifoPath = record.fifo_file;
|
|
@@ -442,14 +527,18 @@ async function cmdSend(rest) {
|
|
|
442
527
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
443
528
|
body = Buffer.concat(chunks).toString("utf-8").trimEnd();
|
|
444
529
|
} else body = rawMessage;
|
|
445
|
-
|
|
446
|
-
|
|
530
|
+
const sourcePid = process.env.AGENT_YES_PID ? Number(process.env.AGENT_YES_PID) : null;
|
|
531
|
+
const talkBack = sourcePid ? `\n(from AGENT_YES_PID=${sourcePid} — reply: ay send ${sourcePid} "...")` : "";
|
|
532
|
+
const fullBody = body + talkBack;
|
|
533
|
+
if (fullBody && trailing) {
|
|
534
|
+
await writeToIpc(fifoPath, fullBody);
|
|
447
535
|
await new Promise((r) => setTimeout(r, 200));
|
|
448
536
|
await writeToIpc(fifoPath, trailing);
|
|
449
|
-
} else await writeToIpc(fifoPath,
|
|
537
|
+
} else await writeToIpc(fifoPath, fullBody + trailing);
|
|
450
538
|
const payload = body + trailing;
|
|
451
539
|
process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
|
|
452
|
-
|
|
540
|
+
const replyHint = sourcePid ? ` ay send ${sourcePid} "..." # reply to sender\n` : "";
|
|
541
|
+
process.stderr.write(`\n` + replyHint + ` ay tail ${record.pid} # watch output\n ay ls # list all agents\n`);
|
|
453
542
|
return 0;
|
|
454
543
|
}
|
|
455
544
|
function controlCodeFromName(name) {
|
|
@@ -513,7 +602,7 @@ async function cmdRestart(rest) {
|
|
|
513
602
|
const keyword = positional[0];
|
|
514
603
|
const record = await resolveOne(keyword, opts);
|
|
515
604
|
if (isPidAlive(record.pid)) {
|
|
516
|
-
process.stderr.write(`pid ${record.pid} is still running — stop it first or use
|
|
605
|
+
process.stderr.write(`pid ${record.pid} is still running — stop it first or use ay send\n`);
|
|
517
606
|
return 1;
|
|
518
607
|
}
|
|
519
608
|
const args = ["--cli=" + record.cli];
|
|
@@ -528,7 +617,7 @@ async function cmdRestart(rest) {
|
|
|
528
617
|
]
|
|
529
618
|
});
|
|
530
619
|
process.stdout.write(`restarted ${record.cli} in ${shortenPath(record.cwd)} (new pid: ${proc.pid})\n`);
|
|
531
|
-
process.stderr.write(`\n
|
|
620
|
+
process.stderr.write(`\n ay tail ${proc.pid} # watch output\n ay ls # list all agents\n`);
|
|
532
621
|
return 0;
|
|
533
622
|
}
|
|
534
623
|
async function cmdNote(rest) {
|
|
@@ -536,7 +625,7 @@ async function cmdNote(rest) {
|
|
|
536
625
|
const opts = commonOpts(flags);
|
|
537
626
|
const keyword = positional[0];
|
|
538
627
|
const note = positional.slice(1).join(" ");
|
|
539
|
-
if (!keyword) throw new Error("usage:
|
|
628
|
+
if (!keyword) throw new Error("usage: ay note <keyword> [\"note text\"] (omit text to clear)");
|
|
540
629
|
const record = await resolveOne(keyword, {
|
|
541
630
|
...opts,
|
|
542
631
|
all: true
|
|
@@ -549,10 +638,82 @@ async function cmdNote(rest) {
|
|
|
549
638
|
}
|
|
550
639
|
await writeNote(record.pid, note);
|
|
551
640
|
process.stdout.write(`note set for pid ${record.pid}: ${note}\n`);
|
|
552
|
-
process.stderr.write(`\n
|
|
641
|
+
process.stderr.write(`\n ay ls # see updated note in list\n`);
|
|
642
|
+
return 0;
|
|
643
|
+
}
|
|
644
|
+
async function snapshotStatus(record) {
|
|
645
|
+
const alive = isPidAlive(record.pid);
|
|
646
|
+
let state;
|
|
647
|
+
let logMtimeMs = null;
|
|
648
|
+
if (!alive) state = "stopped";
|
|
649
|
+
else if (record.log_file) {
|
|
650
|
+
logMtimeMs = await stat(record.log_file).then((s) => s.mtimeMs).catch(() => null);
|
|
651
|
+
state = logMtimeMs !== null && Date.now() - logMtimeMs > IDLE_THRESHOLD_MS ? "idle" : "active";
|
|
652
|
+
} else state = "active";
|
|
653
|
+
const activity = state !== "stopped" && record.log_file ? await extractActivity(record.log_file) : null;
|
|
654
|
+
const note = (await readNotes()).get(record.pid) ?? null;
|
|
655
|
+
return {
|
|
656
|
+
pid: record.pid,
|
|
657
|
+
cli: record.cli,
|
|
658
|
+
cwd: record.cwd,
|
|
659
|
+
state,
|
|
660
|
+
activity,
|
|
661
|
+
note,
|
|
662
|
+
log_mtime_ms: logMtimeMs,
|
|
663
|
+
started_at: record.started_at,
|
|
664
|
+
age_ms: Date.now() - record.started_at,
|
|
665
|
+
exit_code: record.exit_code,
|
|
666
|
+
exit_reason: record.exit_reason,
|
|
667
|
+
log_file: record.log_file ?? null
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
async function cmdStatus(rest) {
|
|
671
|
+
const { flags, positional } = parseArgs(rest);
|
|
672
|
+
const opts = {
|
|
673
|
+
...commonOpts(flags),
|
|
674
|
+
all: true
|
|
675
|
+
};
|
|
676
|
+
const keyword = positional[0];
|
|
677
|
+
if (!keyword) throw new Error("usage: ay status <keyword> [--watch] [--interval=N]");
|
|
678
|
+
const watch = !!(flags.watch || flags.w);
|
|
679
|
+
const intervalFlag = typeof flags.interval === "string" ? Number(flags.interval) : 2;
|
|
680
|
+
const intervalMs = Math.max(500, (Number.isFinite(intervalFlag) ? intervalFlag : 2) * 1e3);
|
|
681
|
+
const record = await resolveOne(keyword, opts);
|
|
682
|
+
const emit = (snap, ts) => {
|
|
683
|
+
const out = ts !== void 0 ? {
|
|
684
|
+
ts,
|
|
685
|
+
...snap
|
|
686
|
+
} : snap;
|
|
687
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
688
|
+
};
|
|
689
|
+
if (!watch) {
|
|
690
|
+
emit(await snapshotStatus(record));
|
|
691
|
+
return 0;
|
|
692
|
+
}
|
|
693
|
+
process.stderr.write(`watching pid ${record.pid} every ${intervalMs / 1e3}s… (Ctrl-C to stop)\n`);
|
|
694
|
+
let prev = null;
|
|
695
|
+
const tick = async () => {
|
|
696
|
+
const snap = await snapshotStatus(record);
|
|
697
|
+
if (prev === null || snap.state !== prev.state || snap.activity !== prev.activity || snap.exit_code !== prev.exit_code) {
|
|
698
|
+
emit(snap, Date.now());
|
|
699
|
+
prev = {
|
|
700
|
+
state: snap.state,
|
|
701
|
+
activity: snap.activity,
|
|
702
|
+
exit_code: snap.exit_code
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
await tick();
|
|
707
|
+
await new Promise((resolve) => {
|
|
708
|
+
const timer = setInterval(tick, intervalMs);
|
|
709
|
+
process.on("SIGINT", () => {
|
|
710
|
+
clearInterval(timer);
|
|
711
|
+
resolve();
|
|
712
|
+
});
|
|
713
|
+
});
|
|
553
714
|
return 0;
|
|
554
715
|
}
|
|
555
716
|
|
|
556
717
|
//#endregion
|
|
557
718
|
export { isSubcommand, runSubcommand };
|
|
558
|
-
//# sourceMappingURL=subcommands-
|
|
719
|
+
//# sourceMappingURL=subcommands-DjO8lthH.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
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-CspuhOwO.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
|
+
import { r as readGlobalPids } from "./globalPidIndex-Cr-g75QF.js";
|
|
5
6
|
import { arch, platform } from "process";
|
|
6
7
|
import { execSync } from "child_process";
|
|
7
8
|
import { closeSync, constants, createReadStream, existsSync, mkdirSync, openSync } from "fs";
|
|
@@ -1062,7 +1063,7 @@ const CLIS_CONFIG = config.clis;
|
|
|
1062
1063
|
* });
|
|
1063
1064
|
* ```
|
|
1064
1065
|
*/
|
|
1065
|
-
async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, exitOnIdle, logFile, removeControlCharactersFromStdout = false, verbose = false, queue = false, install = false, resume = false, useSkills = false, useStdinAppend = false, autoYes = true, idleAction }) {
|
|
1066
|
+
async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, exitOnIdle, logFile, removeControlCharactersFromStdout = false, verbose = false, queue = false, install = false, resume = false, useSkills = false, useStdinAppend = false, autoYes = true, idleAction, swarmHint = true }) {
|
|
1066
1067
|
if (!cli) throw new Error(`cli is required`);
|
|
1067
1068
|
const conf = CLIS_CONFIG[cli] || DIE(`Unsupported cli tool: ${cli}, current process.argv: ${process.argv.join(" ")}`);
|
|
1068
1069
|
const workingDir = cwd ?? process.cwd();
|
|
@@ -1139,6 +1140,18 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
|
|
|
1139
1140
|
} catch (error) {
|
|
1140
1141
|
if (verbose) logger.warn("[skills] Failed to inject SKILL.md header:", { error });
|
|
1141
1142
|
}
|
|
1143
|
+
if (swarmHint) try {
|
|
1144
|
+
const peers = await readGlobalPids({ liveOnly: true });
|
|
1145
|
+
if (peers.length > 0) {
|
|
1146
|
+
const hint = `${peers.length} peer agent${peers.length > 1 ? "s are" : " is"} running. Before asking the user for input on any domain-specific topic (design, testing, architecture, etc.), check for relevant peers first: \`ay ls --json\` (see \`prompt\` field for their role). Ask one: \`ay send <pid> <question>\`. Read reply: \`ay tail <pid>\`. Do not use interactive forms or user prompts when a peer can answer.`;
|
|
1147
|
+
if (cli === "claude") cliArgs = [
|
|
1148
|
+
"--append-system-prompt",
|
|
1149
|
+
hint,
|
|
1150
|
+
...cliArgs
|
|
1151
|
+
];
|
|
1152
|
+
prompt = prompt ? `[${hint}]\n\n${prompt}` : hint;
|
|
1153
|
+
}
|
|
1154
|
+
} catch {}
|
|
1142
1155
|
if (resume) if (cli === "codex" && resume) {
|
|
1143
1156
|
const storedSessionId = await getSessionForCwd(workingDir);
|
|
1144
1157
|
if (storedSessionId) {
|
|
@@ -1171,6 +1184,7 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
|
|
|
1171
1184
|
prompt = void 0;
|
|
1172
1185
|
} else logger.warn(`Unknown promptArg format: ${cliConf.promptArg}`);
|
|
1173
1186
|
const ptyEnv = { ...env ?? process.env };
|
|
1187
|
+
ptyEnv.AGENT_YES_PID = String(process.pid);
|
|
1174
1188
|
const ptyOptions = {
|
|
1175
1189
|
name: "xterm-color",
|
|
1176
1190
|
...getTerminalDimensions(),
|
|
@@ -1679,4 +1693,4 @@ function sleep(ms) {
|
|
|
1679
1693
|
|
|
1680
1694
|
//#endregion
|
|
1681
1695
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1682
|
-
//# sourceMappingURL=ts-
|
|
1696
|
+
//# sourceMappingURL=ts-Bw6gQKyU.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.84.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-CspuhOwO.js.map
|
package/package.json
CHANGED
package/ts/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { AgentContext } from "./core/context.ts";
|
|
|
28
28
|
import { createTerminatorStream } from "./core/streamHelpers.ts";
|
|
29
29
|
import { globalAgentRegistry } from "./agentRegistry.ts";
|
|
30
30
|
import { notifyWebhook } from "./webhookNotifier.ts";
|
|
31
|
+
import { readGlobalPids } from "./globalPidIndex.ts";
|
|
31
32
|
|
|
32
33
|
export { removeControlCharacters };
|
|
33
34
|
export { AgentContext };
|
|
@@ -124,6 +125,7 @@ export default async function agentYes({
|
|
|
124
125
|
useStdinAppend = false,
|
|
125
126
|
autoYes = true,
|
|
126
127
|
idleAction,
|
|
128
|
+
swarmHint = true,
|
|
127
129
|
}: {
|
|
128
130
|
cli: keyof typeof CLIS_CONFIG;
|
|
129
131
|
cliArgs?: string[];
|
|
@@ -142,6 +144,7 @@ export default async function agentYes({
|
|
|
142
144
|
useStdinAppend?: boolean; // if true, enable FIFO input stream on Linux, for additional stdin input
|
|
143
145
|
autoYes?: boolean; // if true, auto-yes is enabled (default), toggle with Ctrl+Y during session
|
|
144
146
|
idleAction?: string; // if set, type this message when idle instead of exiting
|
|
147
|
+
swarmHint?: boolean; // if true (default), inject peer discovery hint when other agents are running; --no-swarm-hint to opt out
|
|
145
148
|
}) {
|
|
146
149
|
if (!cli) throw new Error(`cli is required`);
|
|
147
150
|
const conf =
|
|
@@ -274,6 +277,23 @@ export default async function agentYes({
|
|
|
274
277
|
if (verbose) logger.warn("[skills] Failed to inject SKILL.md header:", { error });
|
|
275
278
|
}
|
|
276
279
|
|
|
280
|
+
// Inject peer discovery hint when other agents are running
|
|
281
|
+
if (swarmHint) {
|
|
282
|
+
try {
|
|
283
|
+
const peers = await readGlobalPids({ liveOnly: true });
|
|
284
|
+
if (peers.length > 0) {
|
|
285
|
+
const hint = `${peers.length} peer agent${peers.length > 1 ? "s are" : " is"} running. Before asking the user for input on any domain-specific topic (design, testing, architecture, etc.), check for relevant peers first: \`ay ls --json\` (see \`prompt\` field for their role). Ask one: \`ay send <pid> <question>\`. Read reply: \`ay tail <pid>\`. Do not use interactive forms or user prompts when a peer can answer.`;
|
|
286
|
+
if (cli === "claude") {
|
|
287
|
+
cliArgs = ["--append-system-prompt", hint, ...cliArgs];
|
|
288
|
+
}
|
|
289
|
+
// Prepend to prompt for all CLIs (including claude) so it's read before the task
|
|
290
|
+
prompt = prompt ? `[${hint}]\n\n${prompt}` : hint;
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
// Non-fatal
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
277
297
|
// Handle --continue flag for codex session restoration
|
|
278
298
|
if (resume) {
|
|
279
299
|
if (cli === "codex" && resume) {
|
|
@@ -322,6 +342,7 @@ export default async function agentYes({
|
|
|
322
342
|
|
|
323
343
|
// Spawn the agent CLI process
|
|
324
344
|
const ptyEnv = { ...(env ?? (process.env as Record<string, string>)) };
|
|
345
|
+
ptyEnv.AGENT_YES_PID = String(process.pid);
|
|
325
346
|
const ptyOptions = {
|
|
326
347
|
name: "xterm-color",
|
|
327
348
|
...getTerminalDimensions(),
|
package/ts/parseCliArgs.ts
CHANGED
|
@@ -63,6 +63,12 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
|
|
|
63
63
|
"Prepend SKILL.md header from current directory to the prompt (helpful for non-Claude agents)",
|
|
64
64
|
default: false,
|
|
65
65
|
})
|
|
66
|
+
.option("swarm-hint", {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
description:
|
|
69
|
+
"Inject peer discovery hint into agent system prompt when other agents are running (use --no-swarm-hint to opt out)",
|
|
70
|
+
default: true,
|
|
71
|
+
})
|
|
66
72
|
.option("timeout", {
|
|
67
73
|
type: "string",
|
|
68
74
|
description: 'Exit after a period of inactivity, e.g., "5s" or "1m"',
|
|
@@ -313,6 +319,7 @@ export function parseCliArgs(argv: string[], supportedClis?: readonly string[])
|
|
|
313
319
|
verbose: parsedArgv.verbose,
|
|
314
320
|
resume: parsedArgv.continue, // Note: intentional use resume here to avoid preserved keyword (continue)
|
|
315
321
|
useSkills: parsedArgv.useSkills,
|
|
322
|
+
swarmHint: parsedArgv.swarmHint,
|
|
316
323
|
appendPrompt: parsedArgv.appendPrompt,
|
|
317
324
|
useStdinAppend: Boolean(parsedArgv.stdpush || parsedArgv.ipc || parsedArgv.fifo), // Support --stdpush, --ipc, and --fifo (backward compatibility)
|
|
318
325
|
showVersion: parsedArgv.version,
|
package/ts/subcommands.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
2
|
+
* `ay ls / read / cat / tail / head / send` subcommand implementations.
|
|
3
3
|
*
|
|
4
4
|
* Mirrors the principles of koho's `terminal-ws-lib.ts` (session list, render
|
|
5
5
|
* via @xterm/headless, keyword-keyed input) — but file-based instead of via
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* to the normal agent-spawning flow.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { appendFile, mkdir, readFile, stat, writeFile } 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";
|
|
@@ -66,7 +66,7 @@ async function compactNotes(): Promise<void> {
|
|
|
66
66
|
/**
|
|
67
67
|
* Read the per-cwd TS PidStore JSONL and convert to the global record shape,
|
|
68
68
|
* so pre-existing TS agents that were spawned before the global-index mirror
|
|
69
|
-
* shipped still show up in `
|
|
69
|
+
* shipped still show up in `ay ls`. Merging is done in `mergeRecords`.
|
|
70
70
|
*/
|
|
71
71
|
async function readLocalTsPids(cwd: string): Promise<GlobalPidRecord[]> {
|
|
72
72
|
const jsonlPath = path.join(cwd, ".agent-yes", "pid-records.jsonl");
|
|
@@ -127,6 +127,7 @@ const SUBCOMMANDS = new Set([
|
|
|
127
127
|
"ls",
|
|
128
128
|
"list",
|
|
129
129
|
"ps",
|
|
130
|
+
"status",
|
|
130
131
|
"read",
|
|
131
132
|
"cat",
|
|
132
133
|
"tail",
|
|
@@ -136,6 +137,8 @@ const SUBCOMMANDS = new Set([
|
|
|
136
137
|
"note",
|
|
137
138
|
]);
|
|
138
139
|
|
|
140
|
+
const IDLE_THRESHOLD_MS = 60 * 1000;
|
|
141
|
+
|
|
139
142
|
export function isSubcommand(name: string | undefined): boolean {
|
|
140
143
|
return !!name && SUBCOMMANDS.has(name);
|
|
141
144
|
}
|
|
@@ -156,6 +159,8 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
156
159
|
case "list":
|
|
157
160
|
case "ps":
|
|
158
161
|
return await cmdLs(rest);
|
|
162
|
+
case "status":
|
|
163
|
+
return await cmdStatus(rest);
|
|
159
164
|
case "read":
|
|
160
165
|
case "cat":
|
|
161
166
|
return await cmdRead(rest, { mode: "cat" });
|
|
@@ -174,7 +179,7 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
174
179
|
}
|
|
175
180
|
} catch (err) {
|
|
176
181
|
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
-
process.stderr.write(`
|
|
182
|
+
process.stderr.write(`ay ${sub}: ${msg}\n`);
|
|
178
183
|
return 1;
|
|
179
184
|
}
|
|
180
185
|
}
|
|
@@ -210,7 +215,7 @@ export function parseArgs(rest: string[]): ParsedArgs {
|
|
|
210
215
|
const next = rest[i + 1];
|
|
211
216
|
// Boolean flags: --all, --json, --latest
|
|
212
217
|
if (
|
|
213
|
-
["all", "active", "follow", "json", "latest"].includes(key) ||
|
|
218
|
+
["all", "active", "follow", "json", "latest", "watch"].includes(key) ||
|
|
214
219
|
!next ||
|
|
215
220
|
next.startsWith("-")
|
|
216
221
|
) {
|
|
@@ -322,7 +327,7 @@ async function resolveOne(keyword: string | undefined, opts: CommonOpts): Promis
|
|
|
322
327
|
}
|
|
323
328
|
|
|
324
329
|
// ---------------------------------------------------------------------------
|
|
325
|
-
//
|
|
330
|
+
// ay ls
|
|
326
331
|
// ---------------------------------------------------------------------------
|
|
327
332
|
|
|
328
333
|
async function cmdLs(rest: string[]): Promise<number> {
|
|
@@ -359,7 +364,6 @@ async function cmdLs(rest: string[]): Promise<number> {
|
|
|
359
364
|
const fixedWidth = widths.pid + widths.cli + widths.status + widths.age + widths.cwd + 5 * 2; // 5 separators of " "
|
|
360
365
|
const promptBudget = Math.max(20, termWidth - fixedWidth - 1);
|
|
361
366
|
|
|
362
|
-
const IDLE_THRESHOLD_MS = 60 * 1000;
|
|
363
367
|
const notes = await readNotes();
|
|
364
368
|
const rows = await Promise.all(
|
|
365
369
|
records.map(async (r) => {
|
|
@@ -375,8 +379,18 @@ async function cmdLs(rest: string[]): Promise<number> {
|
|
|
375
379
|
} else {
|
|
376
380
|
displayStatus = "active";
|
|
377
381
|
}
|
|
378
|
-
const
|
|
379
|
-
|
|
382
|
+
const note = notes.get(r.pid);
|
|
383
|
+
let label: string;
|
|
384
|
+
let hasNote = false;
|
|
385
|
+
if (note) {
|
|
386
|
+
label = truncate(note, promptBudget);
|
|
387
|
+
hasNote = true;
|
|
388
|
+
} else if (r.log_file && displayStatus !== "stopped") {
|
|
389
|
+
const activity = await extractActivity(r.log_file);
|
|
390
|
+
label = truncate(activity ?? (r.prompt ? `→ ${r.prompt}` : ""), promptBudget);
|
|
391
|
+
} else {
|
|
392
|
+
label = truncate(r.prompt ? `→ ${r.prompt}` : "", promptBudget);
|
|
393
|
+
}
|
|
380
394
|
return {
|
|
381
395
|
pid: String(r.pid),
|
|
382
396
|
cli: r.cli,
|
|
@@ -419,17 +433,24 @@ async function cmdLs(rest: string[]): Promise<number> {
|
|
|
419
433
|
const stopped = rows.find((r) => !r._alive);
|
|
420
434
|
const hints: string[] = ["\n"];
|
|
421
435
|
if (alive) {
|
|
422
|
-
hints.push(`
|
|
423
|
-
hints.push(`
|
|
424
|
-
hints.push(`
|
|
425
|
-
hints.push(`
|
|
426
|
-
hints.push(
|
|
436
|
+
hints.push(` ay status ${alive.pid} # JSON status snapshot\n`);
|
|
437
|
+
hints.push(` ay status ${alive.pid} --watch # stream changes as JSON\n`);
|
|
438
|
+
hints.push(` ay tail ${alive.pid} # view latest output\n`);
|
|
439
|
+
hints.push(` ay tail -f ${alive.pid} # follow live output\n`);
|
|
440
|
+
hints.push(
|
|
441
|
+
` ay send ${alive.pid} "next: ..." # send a prompt (keyword: pid, cwd, or prompt substring)\n`,
|
|
442
|
+
);
|
|
443
|
+
hints.push(` ay send ${alive.pid} "" --code=ctrl-c # interrupt\n`);
|
|
444
|
+
hints.push(` ay note ${alive.pid} "what it's doing" # set a note\n`);
|
|
445
|
+
hints.push(
|
|
446
|
+
` ay ls --json # machine-readable list for scripts/agents\n`,
|
|
447
|
+
);
|
|
427
448
|
}
|
|
428
449
|
if (stopped) {
|
|
429
|
-
hints.push(`
|
|
450
|
+
hints.push(` ay restart ${stopped.pid} # restart stopped agent\n`);
|
|
430
451
|
}
|
|
431
452
|
if (!alive && !stopped)
|
|
432
|
-
hints.push(`
|
|
453
|
+
hints.push(` ay ls --all # show exited agents\n`);
|
|
433
454
|
process.stderr.write(hints.join(""));
|
|
434
455
|
}
|
|
435
456
|
|
|
@@ -459,7 +480,7 @@ function truncate(s: string, n: number): string {
|
|
|
459
480
|
}
|
|
460
481
|
|
|
461
482
|
// ---------------------------------------------------------------------------
|
|
462
|
-
//
|
|
483
|
+
// ay read / cat / tail / head
|
|
463
484
|
// ---------------------------------------------------------------------------
|
|
464
485
|
|
|
465
486
|
interface ReadOpts {
|
|
@@ -534,10 +555,10 @@ async function cmdRead(rest: string[], { mode }: ReadOpts): Promise<number> {
|
|
|
534
555
|
|
|
535
556
|
process.stderr.write(
|
|
536
557
|
`\n` +
|
|
537
|
-
`
|
|
538
|
-
`
|
|
539
|
-
`
|
|
540
|
-
`
|
|
558
|
+
` ay ls # list all agents\n` +
|
|
559
|
+
` ay tail -f ${record.pid} # follow live output\n` +
|
|
560
|
+
` ay send ${record.pid} "next: ..." # send a prompt\n` +
|
|
561
|
+
` ay send ${record.pid} "" --code=ctrl-c # interrupt\n`,
|
|
541
562
|
);
|
|
542
563
|
return 0;
|
|
543
564
|
}
|
|
@@ -589,7 +610,125 @@ async function renderRawLog(
|
|
|
589
610
|
}
|
|
590
611
|
|
|
591
612
|
// ---------------------------------------------------------------------------
|
|
592
|
-
//
|
|
613
|
+
// activity extraction
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Extract a one-line activity summary from a raw log file.
|
|
618
|
+
* Reads only the last 32 KB for speed, renders via xterm for clean output.
|
|
619
|
+
*/
|
|
620
|
+
async function extractActivity(logPath: string): Promise<string | null> {
|
|
621
|
+
const TAIL_BYTES = 32 * 1024;
|
|
622
|
+
let buf: Uint8Array;
|
|
623
|
+
try {
|
|
624
|
+
const fh = await open(logPath, "r");
|
|
625
|
+
try {
|
|
626
|
+
const { size } = await fh.stat();
|
|
627
|
+
if (size === 0) return null;
|
|
628
|
+
if (size <= TAIL_BYTES) {
|
|
629
|
+
const data = await fh.readFile();
|
|
630
|
+
buf = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
631
|
+
} else {
|
|
632
|
+
const tmp = Buffer.alloc(TAIL_BYTES);
|
|
633
|
+
const { bytesRead } = await fh.read(tmp, 0, TAIL_BYTES, size - TAIL_BYTES);
|
|
634
|
+
buf = new Uint8Array(tmp.buffer, 0, bytesRead);
|
|
635
|
+
}
|
|
636
|
+
} finally {
|
|
637
|
+
await fh.close();
|
|
638
|
+
}
|
|
639
|
+
} catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
try {
|
|
644
|
+
const rendered = await renderRawLog(buf, { mode: "tail", n: 40 });
|
|
645
|
+
return extractActivityFromLines(rendered.split("\n"));
|
|
646
|
+
} catch {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function extractActivityFromLines(lines: string[]): string | null {
|
|
652
|
+
// Claude Code UI chrome: these lines carry no meaningful activity info
|
|
653
|
+
const isChrome = (l: string): boolean => {
|
|
654
|
+
const s = l.trim();
|
|
655
|
+
return (
|
|
656
|
+
!s ||
|
|
657
|
+
/^─+$/.test(s) ||
|
|
658
|
+
s.startsWith("? for shortcuts") ||
|
|
659
|
+
/^esc to interrupt/i.test(s) ||
|
|
660
|
+
/\d+%\s*until auto-compact/i.test(s) ||
|
|
661
|
+
/^\/model\s+/i.test(s) ||
|
|
662
|
+
/^⧉\s+In\s+/i.test(s) ||
|
|
663
|
+
/^●\s+(high|medium|low)\s*[·•]/i.test(s) ||
|
|
664
|
+
/^[·•]\s*\d+\s+(left|request)/i.test(s)
|
|
665
|
+
);
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
const clean = lines.filter((l) => !isChrome(l));
|
|
669
|
+
|
|
670
|
+
const isSpinnerLine = (l: string) =>
|
|
671
|
+
/^[^\w\s❯>⎿✓✗]\s+[A-Z]\w+[….]/u.test(l.trim()) || /still thinking/i.test(l);
|
|
672
|
+
|
|
673
|
+
// Find positions of the last ❯ prompt and last spinner in the rendered output.
|
|
674
|
+
// If ❯ comes after the last spinner, the agent finished and is waiting — show
|
|
675
|
+
// idle state rather than the stale spinner description.
|
|
676
|
+
let lastPromptIdx = -1;
|
|
677
|
+
let lastSpinnerIdx = -1;
|
|
678
|
+
for (let i = clean.length - 1; i >= 0; i--) {
|
|
679
|
+
const l = clean[i]!.trim();
|
|
680
|
+
if (lastPromptIdx === -1 && l.startsWith("❯")) lastPromptIdx = i;
|
|
681
|
+
if (lastSpinnerIdx === -1 && isSpinnerLine(l)) lastSpinnerIdx = i;
|
|
682
|
+
if (lastPromptIdx !== -1 && lastSpinnerIdx !== -1) break;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ❯ appears after (or without) any spinner → agent is idle/waiting for input
|
|
686
|
+
if (lastPromptIdx > lastSpinnerIdx) {
|
|
687
|
+
const text = clean[lastPromptIdx]!.trim()
|
|
688
|
+
.replace(/^❯\s*/, "")
|
|
689
|
+
.trim();
|
|
690
|
+
return text ? `» ${text}` : null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Priority 1: thinking/composing spinner active
|
|
694
|
+
// Claude Code cycles through various Unicode dingbats for its spinner (✢✳✶✻✷…).
|
|
695
|
+
// The format is always: SPINNER_CHAR Verb… (timing…)
|
|
696
|
+
// Require ellipsis after the verb so we don't false-positive on normal text
|
|
697
|
+
// that happens to contain one of these chars mid-sentence.
|
|
698
|
+
const thinkingLine = clean.find((l) => isSpinnerLine(l));
|
|
699
|
+
if (thinkingLine) {
|
|
700
|
+
const m = /^.\s+(\w+[^(]*)(?:\s*\(|$)/u.exec(thinkingLine.trim());
|
|
701
|
+
return m?.[1] ? `✳ ${m[1].trim()}` : "thinking…";
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Priority 3: ✻ spinner just finished — show nearby context
|
|
705
|
+
const cookIdx = clean.findIndex((l) => /^✻\s+/.test(l.trim()));
|
|
706
|
+
if (cookIdx >= 0) {
|
|
707
|
+
const window = clean.slice(Math.max(0, cookIdx - 8), cookIdx);
|
|
708
|
+
for (let i = window.length - 1; i >= 0; i--) {
|
|
709
|
+
const l = window[i]!.trim();
|
|
710
|
+
if (l && !/^[✻✢⧉❯]/.test(l) && !isChrome(l)) {
|
|
711
|
+
return l.length > 80 ? l.slice(0, 79) + "…" : l;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Priority 4: last meaningful non-icon line
|
|
717
|
+
for (let i = clean.length - 1; i >= 0; i--) {
|
|
718
|
+
const l = clean[i]!.trim();
|
|
719
|
+
// Skip lines that look like spinner patterns (caught by priority 1 above)
|
|
720
|
+
// and status dots/separators; everything else (including ⎿ tool sub-output
|
|
721
|
+
// and non-ASCII text like Japanese) is fair game as meaningful content.
|
|
722
|
+
if (l && !/^[─●○◉⧉]/.test(l) && !/^[^\w\s❯>]\s+[A-Z]\w+[….]/u.test(l)) {
|
|
723
|
+
return l.length > 80 ? l.slice(0, 79) + "…" : l;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ---------------------------------------------------------------------------
|
|
731
|
+
// ay send
|
|
593
732
|
// ---------------------------------------------------------------------------
|
|
594
733
|
|
|
595
734
|
async function cmdSend(rest: string[]): Promise<number> {
|
|
@@ -599,7 +738,7 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
599
738
|
const rawMessage = positional.slice(1).join(" ");
|
|
600
739
|
|
|
601
740
|
if (!keyword)
|
|
602
|
-
throw new Error("usage:
|
|
741
|
+
throw new Error("usage: ay send <keyword> <msg|-> [--code=enter|esc|ctrl-c|ctrl-y|tab|none]");
|
|
603
742
|
|
|
604
743
|
const codeName = typeof flags.code === "string" ? flags.code.toLowerCase() : "enter";
|
|
605
744
|
const trailing = controlCodeFromName(codeName);
|
|
@@ -621,20 +760,30 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
621
760
|
body = rawMessage;
|
|
622
761
|
}
|
|
623
762
|
|
|
624
|
-
|
|
625
|
-
|
|
763
|
+
const sourcePid = process.env.AGENT_YES_PID ? Number(process.env.AGENT_YES_PID) : null;
|
|
764
|
+
const talkBack = sourcePid
|
|
765
|
+
? `\n(from AGENT_YES_PID=${sourcePid} — reply: ay send ${sourcePid} "...")`
|
|
766
|
+
: "";
|
|
767
|
+
|
|
768
|
+
const fullBody = body + talkBack;
|
|
769
|
+
if (fullBody && trailing) {
|
|
770
|
+
await writeToIpc(fifoPath, fullBody);
|
|
626
771
|
await new Promise((r) => setTimeout(r, 200));
|
|
627
772
|
await writeToIpc(fifoPath, trailing);
|
|
628
773
|
} else {
|
|
629
|
-
await writeToIpc(fifoPath,
|
|
774
|
+
await writeToIpc(fifoPath, fullBody + trailing);
|
|
630
775
|
}
|
|
631
776
|
const payload = body + trailing;
|
|
632
777
|
process.stdout.write(`sent to pid ${record.pid} (${record.cli}): ${truncate(payload, 80)}\n`);
|
|
633
778
|
|
|
779
|
+
const replyHint = sourcePid
|
|
780
|
+
? ` ay send ${sourcePid} "..." # reply to sender\n`
|
|
781
|
+
: "";
|
|
634
782
|
process.stderr.write(
|
|
635
783
|
`\n` +
|
|
636
|
-
|
|
637
|
-
`
|
|
784
|
+
replyHint +
|
|
785
|
+
` ay tail ${record.pid} # watch output\n` +
|
|
786
|
+
` ay ls # list all agents\n`,
|
|
638
787
|
);
|
|
639
788
|
return 0;
|
|
640
789
|
}
|
|
@@ -702,7 +851,7 @@ async function writeToIpc(ipcPath: string, payload: string): Promise<void> {
|
|
|
702
851
|
}
|
|
703
852
|
|
|
704
853
|
// ---------------------------------------------------------------------------
|
|
705
|
-
//
|
|
854
|
+
// ay restart
|
|
706
855
|
// ---------------------------------------------------------------------------
|
|
707
856
|
|
|
708
857
|
async function cmdRestart(rest: string[]): Promise<number> {
|
|
@@ -712,7 +861,7 @@ async function cmdRestart(rest: string[]): Promise<number> {
|
|
|
712
861
|
const record = await resolveOne(keyword, opts);
|
|
713
862
|
|
|
714
863
|
if (isPidAlive(record.pid)) {
|
|
715
|
-
process.stderr.write(`pid ${record.pid} is still running — stop it first or use
|
|
864
|
+
process.stderr.write(`pid ${record.pid} is still running — stop it first or use ay send\n`);
|
|
716
865
|
return 1;
|
|
717
866
|
}
|
|
718
867
|
|
|
@@ -730,14 +879,14 @@ async function cmdRestart(rest: string[]): Promise<number> {
|
|
|
730
879
|
);
|
|
731
880
|
process.stderr.write(
|
|
732
881
|
`\n` +
|
|
733
|
-
`
|
|
734
|
-
`
|
|
882
|
+
` ay tail ${proc.pid} # watch output\n` +
|
|
883
|
+
` ay ls # list all agents\n`,
|
|
735
884
|
);
|
|
736
885
|
return 0;
|
|
737
886
|
}
|
|
738
887
|
|
|
739
888
|
// ---------------------------------------------------------------------------
|
|
740
|
-
//
|
|
889
|
+
// ay note
|
|
741
890
|
// ---------------------------------------------------------------------------
|
|
742
891
|
|
|
743
892
|
async function cmdNote(rest: string[]): Promise<number> {
|
|
@@ -746,7 +895,7 @@ async function cmdNote(rest: string[]): Promise<number> {
|
|
|
746
895
|
const keyword = positional[0];
|
|
747
896
|
const note = positional.slice(1).join(" ");
|
|
748
897
|
|
|
749
|
-
if (!keyword) throw new Error('usage:
|
|
898
|
+
if (!keyword) throw new Error('usage: ay note <keyword> ["note text"] (omit text to clear)');
|
|
750
899
|
|
|
751
900
|
const record = await resolveOne(keyword, { ...opts, all: true });
|
|
752
901
|
|
|
@@ -760,6 +909,114 @@ async function cmdNote(rest: string[]): Promise<number> {
|
|
|
760
909
|
|
|
761
910
|
await writeNote(record.pid, note);
|
|
762
911
|
process.stdout.write(`note set for pid ${record.pid}: ${note}\n`);
|
|
763
|
-
process.stderr.write(`\n
|
|
912
|
+
process.stderr.write(`\n ay ls # see updated note in list\n`);
|
|
913
|
+
return 0;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ---------------------------------------------------------------------------
|
|
917
|
+
// ay status
|
|
918
|
+
// ---------------------------------------------------------------------------
|
|
919
|
+
|
|
920
|
+
interface StatusSnapshot {
|
|
921
|
+
pid: number;
|
|
922
|
+
cli: string;
|
|
923
|
+
cwd: string;
|
|
924
|
+
state: "active" | "idle" | "stopped";
|
|
925
|
+
activity: string | null;
|
|
926
|
+
note: string | null;
|
|
927
|
+
log_mtime_ms: number | null;
|
|
928
|
+
started_at: number;
|
|
929
|
+
age_ms: number;
|
|
930
|
+
exit_code: number | null;
|
|
931
|
+
exit_reason: string | null;
|
|
932
|
+
log_file: string | null;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async function snapshotStatus(record: GlobalPidRecord): Promise<StatusSnapshot> {
|
|
936
|
+
const alive = isPidAlive(record.pid);
|
|
937
|
+
let state: "active" | "idle" | "stopped";
|
|
938
|
+
let logMtimeMs: number | null = null;
|
|
939
|
+
if (!alive) {
|
|
940
|
+
state = "stopped";
|
|
941
|
+
} else if (record.log_file) {
|
|
942
|
+
logMtimeMs = await stat(record.log_file)
|
|
943
|
+
.then((s) => s.mtimeMs)
|
|
944
|
+
.catch(() => null);
|
|
945
|
+
state = logMtimeMs !== null && Date.now() - logMtimeMs > IDLE_THRESHOLD_MS ? "idle" : "active";
|
|
946
|
+
} else {
|
|
947
|
+
state = "active";
|
|
948
|
+
}
|
|
949
|
+
const activity =
|
|
950
|
+
state !== "stopped" && record.log_file ? await extractActivity(record.log_file) : null;
|
|
951
|
+
const notes = await readNotes();
|
|
952
|
+
const note = notes.get(record.pid) ?? null;
|
|
953
|
+
return {
|
|
954
|
+
pid: record.pid,
|
|
955
|
+
cli: record.cli,
|
|
956
|
+
cwd: record.cwd,
|
|
957
|
+
state,
|
|
958
|
+
activity,
|
|
959
|
+
note,
|
|
960
|
+
log_mtime_ms: logMtimeMs,
|
|
961
|
+
started_at: record.started_at,
|
|
962
|
+
age_ms: Date.now() - record.started_at,
|
|
963
|
+
exit_code: record.exit_code,
|
|
964
|
+
exit_reason: record.exit_reason,
|
|
965
|
+
log_file: record.log_file ?? null,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async function cmdStatus(rest: string[]): Promise<number> {
|
|
970
|
+
const { flags, positional } = parseArgs(rest);
|
|
971
|
+
const opts = { ...commonOpts(flags), all: true };
|
|
972
|
+
const keyword = positional[0];
|
|
973
|
+
|
|
974
|
+
if (!keyword) throw new Error("usage: ay status <keyword> [--watch] [--interval=N]");
|
|
975
|
+
|
|
976
|
+
const watch = !!(flags.watch || flags.w);
|
|
977
|
+
const intervalFlag = typeof flags.interval === "string" ? Number(flags.interval) : 2;
|
|
978
|
+
const intervalMs = Math.max(500, (Number.isFinite(intervalFlag) ? intervalFlag : 2) * 1000);
|
|
979
|
+
|
|
980
|
+
const record = await resolveOne(keyword, opts);
|
|
981
|
+
|
|
982
|
+
const emit = (snap: StatusSnapshot, ts?: number): void => {
|
|
983
|
+
const out = ts !== undefined ? { ts, ...snap } : snap;
|
|
984
|
+
process.stdout.write(JSON.stringify(out) + "\n");
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
if (!watch) {
|
|
988
|
+
emit(await snapshotStatus(record));
|
|
989
|
+
return 0;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
process.stderr.write(
|
|
993
|
+
`watching pid ${record.pid} every ${intervalMs / 1000}s… (Ctrl-C to stop)\n`,
|
|
994
|
+
);
|
|
995
|
+
|
|
996
|
+
let prev: { state: string; activity: string | null; exit_code: number | null } | null = null;
|
|
997
|
+
|
|
998
|
+
const tick = async (): Promise<void> => {
|
|
999
|
+
const snap = await snapshotStatus(record);
|
|
1000
|
+
if (
|
|
1001
|
+
prev === null ||
|
|
1002
|
+
snap.state !== prev.state ||
|
|
1003
|
+
snap.activity !== prev.activity ||
|
|
1004
|
+
snap.exit_code !== prev.exit_code
|
|
1005
|
+
) {
|
|
1006
|
+
emit(snap, Date.now());
|
|
1007
|
+
prev = { state: snap.state, activity: snap.activity, exit_code: snap.exit_code };
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
await tick();
|
|
1012
|
+
|
|
1013
|
+
await new Promise<void>((resolve) => {
|
|
1014
|
+
const timer = setInterval(tick, intervalMs);
|
|
1015
|
+
process.on("SIGINT", () => {
|
|
1016
|
+
clearInterval(timer);
|
|
1017
|
+
resolve();
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
764
1021
|
return 0;
|
|
765
1022
|
}
|