agent-yes 1.263.0 → 1.264.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-DlxOPg-j.js +11 -0
- package/dist/{SUPPORTED_CLIS-DjFlWHpg.js → SUPPORTED_CLIS-JcVs6jt7.js} +2 -2
- package/dist/{agentShare-BP6wiu_5.js → agentShare-B8h_JkOw.js} +3 -3
- package/dist/{callback-B0PQUZcK.js → callback-C9rkbjgR.js} +3 -3
- package/dist/{callback-DFUFLIZQ.js → callback-DlJ_glcF.js} +3 -3
- package/dist/cli.js +9 -9
- package/dist/{cwdConflictWarn-B_047KCF.js → cwdConflictWarn-DZTxTGXq.js} +2 -2
- package/dist/{forkNested-CBdhC852.js → forkNested-CgJ1rTnT.js} +2 -2
- package/dist/globalPidIndex-CQX_0JSB.js +4 -0
- package/dist/{globalPidIndex-pwp7Vb7o.js → globalPidIndex-Dq4KY2IV.js} +2 -8
- package/dist/index.js +4 -4
- package/dist/{notifyDaemon-BskLhHdg.js → notifyDaemon--TDDJupp.js} +3 -3
- package/dist/pidStore-D-b8I6Qx.js +6 -0
- package/dist/{pidStore-DfSm1jog.js → pidStore-DJqSZPJi.js} +11 -7
- package/dist/{rustBinary-Borvwmya.js → rustBinary-DA-C6PjH.js} +2 -2
- package/dist/{schedule-BSEIVKQ0.js → schedule-CdgYYRti.js} +6 -6
- package/dist/{serve-BEdLsSpv.js → serve-Z8FAjest.js} +20 -20
- package/dist/{setup-DFRvZ_ku.js → setup-D6B85mUe.js} +2 -2
- package/dist/{spawnGate-D-68B907.js → spawnGate-D0E1uRP7.js} +2 -2
- package/dist/spawnGate-DqKBA07k.js +5 -0
- package/dist/{subcommands-B1YlmRaZ.js → subcommands-C2ipqBtq.js} +2 -2
- package/dist/{subcommands-Bo1MSwuC.js → subcommands-C3w4bg6x.js} +134 -90
- package/dist/{terminal-BMQWojIp.js → terminal-iObUHvkj.js} +2 -2
- package/dist/{todoCli-DNOk38lq.js → todoCli-CngDeaT2.js} +2 -2
- package/dist/{trayApp-CjB5QhdV.js → trayApp-BUI7SXiY.js} +2 -2
- package/dist/trayApp-Cb9a-quB.js +5 -0
- package/dist/{ts-BSHR_dga.js → ts-ClHKe4d8.js} +123 -5
- package/dist/{versionChecker-BTLZ3PAb.js → versionChecker-DRAbaxo-.js} +2 -2
- package/dist/{widget-BVss0Z9Y.js → widget-BRBsnbLn.js} +3 -3
- package/dist/{ws-DMudFezi.js → ws-BppNz49U.js} +3 -3
- package/dist/{ws-DxNNwnrc.js → ws-CAFA4oXH.js} +2 -2
- package/package.json +1 -1
- package/ts/globalPidIndex.spec.ts +5 -20
- package/ts/globalPidIndex.ts +9 -19
- package/ts/identity.spec.ts +108 -0
- package/ts/identity.ts +124 -0
- package/ts/index.ts +12 -4
- package/ts/messageLog.ts +0 -2
- package/ts/pidStore.spec.ts +14 -24
- package/ts/pidStore.ts +13 -10
- package/ts/subcommands.spec.ts +11 -120
- package/ts/subcommands.ts +13 -103
- package/ts/titleScanner.spec.ts +65 -0
- package/ts/titleScanner.ts +137 -0
- package/dist/SUPPORTED_CLIS--5E9wO4L.js +0 -11
- package/dist/globalPidIndex-CHA__9Xf.js +0 -4
- package/dist/pidStore-1PQ4ARnh.js +0 -6
- package/dist/spawnGate-DbSHnfqA.js +0 -5
- package/dist/trayApp-DrdktFnW.js +0 -5
package/ts/subcommands.spec.ts
CHANGED
|
@@ -2670,124 +2670,11 @@ describe("subcommands.cmdWhoami", () => {
|
|
|
2670
2670
|
expect(text).toMatch(/agent {5}claude #\d+ {2}\(agent_id aaaa0000bbbb\)/);
|
|
2671
2671
|
expect(text).toMatch(/reply {5}ay send aaaa0000bbbb/);
|
|
2672
2672
|
expect(text).toMatch(
|
|
2673
|
-
/<ay-msg from claude #\d+
|
|
2673
|
+
/<ay-msg from claude [A-Za-z0-9._-]+@[A-Za-z0-9._-]+:\/repo\/alpha#\d+ — reply: ay send aaaa0000bbbb "\.\.\.">/,
|
|
2674
2674
|
);
|
|
2675
2675
|
});
|
|
2676
2676
|
});
|
|
2677
2677
|
|
|
2678
|
-
describe("subcommands.cmdRole", () => {
|
|
2679
|
-
async function registerAgent(over: Record<string, unknown> = {}) {
|
|
2680
|
-
const { appendGlobalPid } = await import("./globalPidIndex.ts");
|
|
2681
|
-
await appendGlobalPid({
|
|
2682
|
-
pid: 910001,
|
|
2683
|
-
cli: "claude",
|
|
2684
|
-
prompt: null,
|
|
2685
|
-
cwd: "/repo/lane",
|
|
2686
|
-
log_file: null,
|
|
2687
|
-
status: "active",
|
|
2688
|
-
exit_code: null,
|
|
2689
|
-
exit_reason: null,
|
|
2690
|
-
started_at: Date.now(),
|
|
2691
|
-
wrapper_pid: 515151,
|
|
2692
|
-
agent_id: "eeee0000ffff",
|
|
2693
|
-
...over,
|
|
2694
|
-
} as any);
|
|
2695
|
-
}
|
|
2696
|
-
|
|
2697
|
-
function capture() {
|
|
2698
|
-
const out: string[] = [];
|
|
2699
|
-
const err: string[] = [];
|
|
2700
|
-
const origOut = process.stdout.write.bind(process.stdout);
|
|
2701
|
-
const origErr = process.stderr.write.bind(process.stderr);
|
|
2702
|
-
(process.stdout as any).write = (s: any) => (out.push(String(s)), true);
|
|
2703
|
-
(process.stderr as any).write = (s: any) => (err.push(String(s)), true);
|
|
2704
|
-
return {
|
|
2705
|
-
out,
|
|
2706
|
-
err,
|
|
2707
|
-
restore() {
|
|
2708
|
-
process.stdout.write = origOut;
|
|
2709
|
-
process.stderr.write = origErr;
|
|
2710
|
-
},
|
|
2711
|
-
};
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
async function withAgentEnv<T>(fn: () => Promise<T>): Promise<T> {
|
|
2715
|
-
const saved = process.env.AGENT_YES_PID;
|
|
2716
|
-
process.env.AGENT_YES_PID = "515151";
|
|
2717
|
-
try {
|
|
2718
|
-
return await fn();
|
|
2719
|
-
} finally {
|
|
2720
|
-
if (saved === undefined) delete process.env.AGENT_YES_PID;
|
|
2721
|
-
else process.env.AGENT_YES_PID = saved;
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
it("self get/set/clear round-trips via agent context", async () => {
|
|
2726
|
-
const { runSubcommand } = await loadModule();
|
|
2727
|
-
await registerAgent();
|
|
2728
|
-
await withAgentEnv(async () => {
|
|
2729
|
-
let cap = capture();
|
|
2730
|
-
try {
|
|
2731
|
-
expect(await runSubcommand(["bun", "cli.js", "role", "pm"])).toBe(0);
|
|
2732
|
-
expect(cap.out.join("")).toContain('role set to "pm"');
|
|
2733
|
-
} finally {
|
|
2734
|
-
cap.restore();
|
|
2735
|
-
}
|
|
2736
|
-
cap = capture();
|
|
2737
|
-
try {
|
|
2738
|
-
expect(await runSubcommand(["bun", "cli.js", "role"])).toBe(0);
|
|
2739
|
-
expect(cap.out.join("")).toBe("pm\n");
|
|
2740
|
-
expect(await runSubcommand(["bun", "cli.js", "role", "--clear"])).toBe(0);
|
|
2741
|
-
expect(await runSubcommand(["bun", "cli.js", "role"])).toBe(0);
|
|
2742
|
-
expect(cap.out.join("")).toContain("role cleared");
|
|
2743
|
-
} finally {
|
|
2744
|
-
cap.restore();
|
|
2745
|
-
}
|
|
2746
|
-
});
|
|
2747
|
-
});
|
|
2748
|
-
|
|
2749
|
-
it("keyword set and keyword clear work from a human shell", async () => {
|
|
2750
|
-
const { runSubcommand } = await loadModule();
|
|
2751
|
-
await registerAgent();
|
|
2752
|
-
let cap = capture();
|
|
2753
|
-
try {
|
|
2754
|
-
expect(await runSubcommand(["bun", "cli.js", "role", "910001", "crm"])).toBe(0);
|
|
2755
|
-
expect(cap.out.join("")).toContain('role set to "crm"');
|
|
2756
|
-
expect(await runSubcommand(["bun", "cli.js", "role", "910001", "--clear"])).toBe(0);
|
|
2757
|
-
expect(cap.out.join("")).toContain("role cleared");
|
|
2758
|
-
} finally {
|
|
2759
|
-
cap.restore();
|
|
2760
|
-
}
|
|
2761
|
-
});
|
|
2762
|
-
|
|
2763
|
-
it("rejects header-forging role values", async () => {
|
|
2764
|
-
const { runSubcommand } = await loadModule();
|
|
2765
|
-
await registerAgent();
|
|
2766
|
-
const cap = capture();
|
|
2767
|
-
try {
|
|
2768
|
-
expect(await runSubcommand(["bun", "cli.js", "role", "910001", "a b>"])).toBe(1);
|
|
2769
|
-
expect(cap.err.join("")).toContain("invalid role");
|
|
2770
|
-
} finally {
|
|
2771
|
-
cap.restore();
|
|
2772
|
-
}
|
|
2773
|
-
});
|
|
2774
|
-
|
|
2775
|
-
it("errors without agent context when no keyword is given", async () => {
|
|
2776
|
-
const { runSubcommand } = await loadModule();
|
|
2777
|
-
const saved = process.env.AGENT_YES_PID;
|
|
2778
|
-
delete process.env.AGENT_YES_PID;
|
|
2779
|
-
const cap = capture();
|
|
2780
|
-
try {
|
|
2781
|
-
expect(await runSubcommand(["bun", "cli.js", "role"])).toBe(1);
|
|
2782
|
-
expect(await runSubcommand(["bun", "cli.js", "role", "somename"])).toBe(1);
|
|
2783
|
-
expect(cap.err.join("")).toContain("no agent context");
|
|
2784
|
-
} finally {
|
|
2785
|
-
cap.restore();
|
|
2786
|
-
if (saved !== undefined) process.env.AGENT_YES_PID = saved;
|
|
2787
|
-
}
|
|
2788
|
-
});
|
|
2789
|
-
});
|
|
2790
|
-
|
|
2791
2678
|
describe("subcommands.cmdSend double-envelope warning", () => {
|
|
2792
2679
|
const itUnix = process.platform === "linux" || process.platform === "darwin";
|
|
2793
2680
|
|
|
@@ -2859,7 +2746,7 @@ describe("subcommands.cmdSend double-envelope warning", () => {
|
|
|
2859
2746
|
const buf = Buffer.alloc(8192);
|
|
2860
2747
|
const n = fs.readSync(rdwrFd, buf, 0, buf.length, null);
|
|
2861
2748
|
const received = buf.subarray(0, n).toString();
|
|
2862
|
-
expect(received).toMatch(/^<ay-msg [0-9a-f]{8} from claude #900001
|
|
2749
|
+
expect(received).toMatch(/^<ay-msg [0-9a-f]{8} from claude [A-Za-z0-9._-]+@[A-Za-z0-9._-]+:\/repo\/beta#900001 — /);
|
|
2863
2750
|
expect(received).toContain("<ay-msg deadbeef");
|
|
2864
2751
|
fs.closeSync(rdwrFd);
|
|
2865
2752
|
} finally {
|
|
@@ -2933,16 +2820,20 @@ describe("subcommands.cmdSend double-envelope warning", () => {
|
|
|
2933
2820
|
}
|
|
2934
2821
|
});
|
|
2935
2822
|
|
|
2936
|
-
it.skipIf(!itUnix)("
|
|
2823
|
+
it.skipIf(!itUnix)("the envelope header carries the standardized identity", async () => {
|
|
2937
2824
|
const { runSubcommand } = await loadModule();
|
|
2938
2825
|
const { appendGlobalPid } = await import("./globalPidIndex.ts");
|
|
2939
2826
|
const { spawnSync } = await import("child_process");
|
|
2940
2827
|
const tmp = await mkdtemp(path.join(tmpdir(), "ay-fifo-"));
|
|
2941
2828
|
try {
|
|
2942
|
-
const fifo = path.join(tmp, "
|
|
2829
|
+
const fifo = path.join(tmp, "ident.fifo");
|
|
2943
2830
|
if (spawnSync("mkfifo", [fifo]).status !== 0) return;
|
|
2944
2831
|
const fs = await import("fs");
|
|
2945
2832
|
const rdwrFd = fs.openSync(fifo, fs.constants.O_RDWR);
|
|
2833
|
+
// Sender cwd IS a git checkout, so the identity carries its branch.
|
|
2834
|
+
const senderCwd = path.join(tmp, "sender-repo");
|
|
2835
|
+
await mkdir(path.join(senderCwd, ".git"), { recursive: true });
|
|
2836
|
+
await writeFile(path.join(senderCwd, ".git", "HEAD"), "ref: refs/heads/crm-lane\n");
|
|
2946
2837
|
await appendGlobalPid({
|
|
2947
2838
|
pid: process.pid,
|
|
2948
2839
|
cli: "claude",
|
|
@@ -2959,7 +2850,7 @@ describe("subcommands.cmdSend double-envelope warning", () => {
|
|
|
2959
2850
|
pid: 900001,
|
|
2960
2851
|
cli: "claude",
|
|
2961
2852
|
prompt: null,
|
|
2962
|
-
cwd:
|
|
2853
|
+
cwd: senderCwd,
|
|
2963
2854
|
log_file: null,
|
|
2964
2855
|
status: "active",
|
|
2965
2856
|
exit_code: null,
|
|
@@ -2967,7 +2858,6 @@ describe("subcommands.cmdSend double-envelope warning", () => {
|
|
|
2967
2858
|
started_at: Date.now(),
|
|
2968
2859
|
wrapper_pid: 424242,
|
|
2969
2860
|
agent_id: "cccc0000dddd",
|
|
2970
|
-
role: "crm",
|
|
2971
2861
|
});
|
|
2972
2862
|
const savedAyPid = process.env.AGENT_YES_PID;
|
|
2973
2863
|
process.env.AGENT_YES_PID = "424242";
|
|
@@ -2991,8 +2881,9 @@ describe("subcommands.cmdSend double-envelope warning", () => {
|
|
|
2991
2881
|
const buf = Buffer.alloc(4096);
|
|
2992
2882
|
const n = fs.readSync(rdwrFd, buf, 0, buf.length, null);
|
|
2993
2883
|
const received = buf.subarray(0, n).toString();
|
|
2884
|
+
// <user>@<host>:<path>:<branch>#<pid> — reply targets the agent_id.
|
|
2994
2885
|
expect(received).toMatch(
|
|
2995
|
-
/^<ay-msg [0-9a-f]{8} from claude #900001
|
|
2886
|
+
/^<ay-msg [0-9a-f]{8} from claude [A-Za-z0-9._-]+@[A-Za-z0-9._-]+:.+:crm-lane#900001 — reply: ay send cccc0000dddd "\.\.\.">/,
|
|
2996
2887
|
);
|
|
2997
2888
|
fs.closeSync(rdwrFd);
|
|
2998
2889
|
} finally {
|
package/ts/subcommands.ts
CHANGED
|
@@ -18,12 +18,8 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
import ms from "ms";
|
|
19
19
|
import { homedir } from "os";
|
|
20
20
|
import path from "path";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
readGlobalPids,
|
|
24
|
-
sanitizeRole,
|
|
25
|
-
updateGlobalPidStatus,
|
|
26
|
-
} from "./globalPidIndex.ts";
|
|
21
|
+
import { type GlobalPidRecord, readGlobalPids, updateGlobalPidStatus } from "./globalPidIndex.ts";
|
|
22
|
+
import { formatIdentity } from "./identity.ts";
|
|
27
23
|
import { buildAgentForest, flattenForest } from "./agentTree.ts";
|
|
28
24
|
import { parseTaskCounts, type TaskCounts } from "./todoParse.ts";
|
|
29
25
|
import { agentYesHome } from "./agentYesHome.ts";
|
|
@@ -328,7 +324,8 @@ async function cmdWhoami(rest: string[]): Promise<number> {
|
|
|
328
324
|
const ageMin = Math.max(0, Math.round((Date.now() - self.started_at) / 60_000));
|
|
329
325
|
const lines = [
|
|
330
326
|
`agent ${self.cli} #${self.pid}${self.agent_id ? ` (agent_id ${self.agent_id})` : ""}`,
|
|
331
|
-
`
|
|
327
|
+
`identity ${formatIdentity({ cwd: self.cwd, pid: self.pid })}`,
|
|
328
|
+
`title ${self.title ?? "-"}`,
|
|
332
329
|
`state ${state}${question ? ` — ${question}` : ""}`,
|
|
333
330
|
`cwd ${self.cwd}`,
|
|
334
331
|
`started ${new Date(self.started_at).toISOString()} (${ageMin}m ago)`,
|
|
@@ -336,7 +333,7 @@ async function cmdWhoami(rest: string[]): Promise<number> {
|
|
|
336
333
|
`log ${self.log_file ?? "-"}`,
|
|
337
334
|
`fifo ${self.fifo_file ?? "-"}`,
|
|
338
335
|
`reply ${reply} "..."`,
|
|
339
|
-
`envelope <ay-msg from ${self.cli}
|
|
336
|
+
`envelope <ay-msg from ${self.cli} ${formatIdentity({ cwd: self.cwd, pid: self.pid })} — reply: ${reply} "...">…</ay-msg>`,
|
|
340
337
|
];
|
|
341
338
|
process.stdout.write(lines.join("\n") + "\n");
|
|
342
339
|
return 0;
|
|
@@ -387,7 +384,7 @@ async function readLocalTsPids(cwd: string): Promise<GlobalPidRecord[]> {
|
|
|
387
384
|
exit_code: d.exitCode ?? null,
|
|
388
385
|
exit_reason: d.exitReason ?? null,
|
|
389
386
|
started_at: d.startedAt ?? 0,
|
|
390
|
-
|
|
387
|
+
title: d.title ?? null,
|
|
391
388
|
}));
|
|
392
389
|
}
|
|
393
390
|
|
|
@@ -422,7 +419,6 @@ const SUBCOMMANDS = new Set([
|
|
|
422
419
|
"head",
|
|
423
420
|
"send",
|
|
424
421
|
"msgs",
|
|
425
|
-
"role",
|
|
426
422
|
"key",
|
|
427
423
|
"select",
|
|
428
424
|
"spawn",
|
|
@@ -593,8 +589,6 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
593
589
|
return await cmdSend(rest);
|
|
594
590
|
case "msgs":
|
|
595
591
|
return await cmdMsgs(rest);
|
|
596
|
-
case "role":
|
|
597
|
-
return await cmdRole(rest);
|
|
598
592
|
case "key":
|
|
599
593
|
return await cmdKey(rest);
|
|
600
594
|
case "select":
|
|
@@ -815,7 +809,6 @@ export async function cmdHelp(managerCommands = true): Promise<number> {
|
|
|
815
809
|
` ay head <keyword> first N lines\n` +
|
|
816
810
|
` ay send <keyword> <msg> send a message (keyword '.' = agent in this cwd)\n` +
|
|
817
811
|
` ay msgs [keyword] [--in|--out] inter-agent message log (sent + received)\n` +
|
|
818
|
-
` ay role [name] | ay role <kw> <name> lane/role name shown in the send envelope\n` +
|
|
819
812
|
` ay ch mk|join|send|read|tail <topic> local-first E2E channels: AI ↔ humans on a topic (ay ch help)\n` +
|
|
820
813
|
` ay term embed <pid> <script> to embed a live read-only agent terminal in a page (ay term help)\n` +
|
|
821
814
|
` ay widget ls | read selection|dom read an opted-in page widget's context (selection/DOM) (ay widget help)\n` +
|
|
@@ -3429,13 +3422,13 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
3429
3422
|
let nonce: string | undefined;
|
|
3430
3423
|
if (sender.agent && !isSlashCommand(body) && !raw) {
|
|
3431
3424
|
nonce = randomBytes(4).toString("hex");
|
|
3432
|
-
// The
|
|
3433
|
-
//
|
|
3434
|
-
//
|
|
3435
|
-
//
|
|
3436
|
-
|
|
3437
|
-
const
|
|
3438
|
-
prefix = `<ay-msg ${nonce} from ${sender.agent.cli}
|
|
3425
|
+
// The standardized identity (<user>@<host>:<path>:<branch>#<pid>) names the
|
|
3426
|
+
// sender in one token: the branch usually carries the lane name for free
|
|
3427
|
+
// (worktree checkouts name their purpose), so recipients don't translate
|
|
3428
|
+
// cwd/pid into a role by hand. Every segment is clamped to a header-safe
|
|
3429
|
+
// charset in identity.ts — the open tag is not nonce-protected.
|
|
3430
|
+
const identity = formatIdentity({ cwd: sender.agent.cwd, pid: sender.agent.pid });
|
|
3431
|
+
prefix = `<ay-msg ${nonce} from ${sender.agent.cli} ${identity} — reply: ay send ${replyTarget} "...">\n`;
|
|
3439
3432
|
suffix = `\n</ay-msg ${nonce}>`;
|
|
3440
3433
|
// A body that itself starts with an <ay-msg …> header is usually an agent
|
|
3441
3434
|
// hand-wrapping what this send is about to wrap again — the recipient then
|
|
@@ -3516,7 +3509,6 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
3516
3509
|
cli: sender.agent.cli,
|
|
3517
3510
|
cwd: sender.agent.cwd,
|
|
3518
3511
|
agent_id: sender.agent.agent_id,
|
|
3519
|
-
role: sender.agent.role ?? undefined,
|
|
3520
3512
|
}
|
|
3521
3513
|
: null,
|
|
3522
3514
|
to: {
|
|
@@ -3524,7 +3516,6 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
3524
3516
|
cli: record.cli,
|
|
3525
3517
|
cwd: record.cwd,
|
|
3526
3518
|
agent_id: record.agent_id,
|
|
3527
|
-
role: record.role ?? undefined,
|
|
3528
3519
|
},
|
|
3529
3520
|
body,
|
|
3530
3521
|
code: trailing === "\r" ? undefined : codeName,
|
|
@@ -3696,87 +3687,6 @@ async function cmdMsgs(rest: string[]): Promise<number> {
|
|
|
3696
3687
|
return 0;
|
|
3697
3688
|
}
|
|
3698
3689
|
|
|
3699
|
-
/**
|
|
3700
|
-
* `ay role [name]` / `ay role <keyword> <name>` — read or set an agent's
|
|
3701
|
-
* self-declared lane/role name ("pm", "crm", …). The role rides in the
|
|
3702
|
-
* `ay send` envelope header (`from claude #123 (pm) @ …`) so recipients don't
|
|
3703
|
-
* translate cwd/pid into a lane by hand — the emergent `[pm→qa]` body prefixes
|
|
3704
|
-
* exist exactly because the envelope lacked this. With no args: print the
|
|
3705
|
-
* calling agent's role. With one arg: set the calling agent's role (requires
|
|
3706
|
-
* agent context). With two args: resolve the keyword and set that agent's role.
|
|
3707
|
-
* `--clear` removes it. Roles are clamped by sanitizeRole (they render into the
|
|
3708
|
-
* non-nonce-protected open tag).
|
|
3709
|
-
*/
|
|
3710
|
-
const ROLE_RESOLVE_OPTS: CommonOpts = {
|
|
3711
|
-
all: false,
|
|
3712
|
-
active: false,
|
|
3713
|
-
cwdScope: null,
|
|
3714
|
-
latest: false,
|
|
3715
|
-
json: false,
|
|
3716
|
-
};
|
|
3717
|
-
|
|
3718
|
-
async function cmdRole(rest: string[]): Promise<number> {
|
|
3719
|
-
const y = yargs(rest)
|
|
3720
|
-
.usage('Usage: ay role [name] | ay role <keyword> <name> | ay role [keyword] --clear')
|
|
3721
|
-
.option("clear", { type: "boolean", default: false, description: "Remove the role" })
|
|
3722
|
-
.help(false)
|
|
3723
|
-
.version(false)
|
|
3724
|
-
.exitProcess(false);
|
|
3725
|
-
const argv = await y.parseAsync();
|
|
3726
|
-
const args = argv._.slice(0).map(String);
|
|
3727
|
-
|
|
3728
|
-
// Figure out target + new value from arity: 0 args = self get (or self clear),
|
|
3729
|
-
// 1 arg = self set (or keyword clear), 2 args = keyword set.
|
|
3730
|
-
let target: GlobalPidRecord | null = null;
|
|
3731
|
-
let newRole: string | undefined;
|
|
3732
|
-
if (args.length === 2) {
|
|
3733
|
-
target = await resolveOne(args[0]!, ROLE_RESOLVE_OPTS);
|
|
3734
|
-
newRole = args[1]!;
|
|
3735
|
-
} else if (args.length === 1 && argv.clear) {
|
|
3736
|
-
target = await resolveOne(args[0]!, ROLE_RESOLVE_OPTS);
|
|
3737
|
-
} else if (args.length === 1) {
|
|
3738
|
-
target = await resolveSender();
|
|
3739
|
-
if (!target) {
|
|
3740
|
-
process.stderr.write(
|
|
3741
|
-
"ay role: no agent context (AGENT_YES_PID unset or unregistered) — from a human shell, name the agent: ay role <keyword> <name>\n",
|
|
3742
|
-
);
|
|
3743
|
-
return 1;
|
|
3744
|
-
}
|
|
3745
|
-
newRole = args[0]!;
|
|
3746
|
-
} else {
|
|
3747
|
-
target = await resolveSender();
|
|
3748
|
-
if (!target) {
|
|
3749
|
-
process.stderr.write(
|
|
3750
|
-
"ay role: no agent context — from a human shell, name the agent: ay role <keyword> [<name>|--clear]\n",
|
|
3751
|
-
);
|
|
3752
|
-
return 1;
|
|
3753
|
-
}
|
|
3754
|
-
}
|
|
3755
|
-
|
|
3756
|
-
if (newRole === undefined && !argv.clear) {
|
|
3757
|
-
process.stdout.write(`${target.role ?? ""}\n`);
|
|
3758
|
-
return 0;
|
|
3759
|
-
}
|
|
3760
|
-
|
|
3761
|
-
let role: string | null = null;
|
|
3762
|
-
if (!argv.clear) {
|
|
3763
|
-
role = sanitizeRole(newRole);
|
|
3764
|
-
if (!role) {
|
|
3765
|
-
process.stderr.write(
|
|
3766
|
-
`ay role: invalid role ${JSON.stringify(newRole)} — use 1-32 chars of [A-Za-z0-9._-]\n`,
|
|
3767
|
-
);
|
|
3768
|
-
return 1;
|
|
3769
|
-
}
|
|
3770
|
-
}
|
|
3771
|
-
await updateGlobalPidStatus(target.pid, { role });
|
|
3772
|
-
process.stdout.write(
|
|
3773
|
-
role
|
|
3774
|
-
? `pid ${target.pid} (${target.cli}): role set to "${role}" — envelopes now read: from ${target.cli} #${target.pid} (${role}) @ ${shortenPath(target.cwd)}\n`
|
|
3775
|
-
: `pid ${target.pid} (${target.cli}): role cleared\n`,
|
|
3776
|
-
);
|
|
3777
|
-
return 0;
|
|
3778
|
-
}
|
|
3779
|
-
|
|
3780
3690
|
// Resolve a keyword to one agent and return it with a writable FIFO, or throw
|
|
3781
3691
|
// with the same guidance cmdSend gives. Shared by `ay key` / `ay select`.
|
|
3782
3692
|
async function resolveWritableAgent(keyword: string, opts: CommonOpts): Promise<GlobalPidRecord> {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { TitlePublisher, TitleScanner } from "./titleScanner.ts";
|
|
3
|
+
|
|
4
|
+
describe("TitleScanner", () => {
|
|
5
|
+
it("parses OSC 0 terminated by BEL", () => {
|
|
6
|
+
const s = new TitleScanner();
|
|
7
|
+
expect(s.feed("\x1b]0;✳ fixing tests\x07")).toBe("✳ fixing tests");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("parses OSC 2 terminated by ST (ESC \\)", () => {
|
|
11
|
+
const s = new TitleScanner();
|
|
12
|
+
expect(s.feed("\x1b]2;my task\x1b\\")).toBe("my task");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("survives a chunk split mid-sequence", () => {
|
|
16
|
+
const s = new TitleScanner();
|
|
17
|
+
expect(s.feed("text \x1b]0;ha")).toBeNull();
|
|
18
|
+
expect(s.feed("lf done\x07 more")).toBe("half done");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns the last title when a chunk has several", () => {
|
|
22
|
+
const s = new TitleScanner();
|
|
23
|
+
expect(s.feed("\x1b]0;one\x07\x1b]2;two\x07")).toBe("two");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("ignores icon-only and unrelated OSC sequences", () => {
|
|
27
|
+
const s = new TitleScanner();
|
|
28
|
+
expect(s.feed("\x1b]1;icon\x07\x1b]52;c;YWJj\x07\x1b]133;A\x07")).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("ignores CSI colors and plain text", () => {
|
|
32
|
+
const s = new TitleScanner();
|
|
33
|
+
expect(s.feed("plain \x1b[31mred\x1b[0m text")).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("caps runaway titles and resyncs", () => {
|
|
37
|
+
const s = new TitleScanner();
|
|
38
|
+
expect(s.feed(`\x1b]0;${"x".repeat(2000)}\x07`)).toBeNull();
|
|
39
|
+
expect(s.feed("\x1b]0;ok\x07")).toBe("ok");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("strips control chars and rejects empty titles", () => {
|
|
43
|
+
const s = new TitleScanner();
|
|
44
|
+
expect(s.feed("\x1b]0;a\tb\x07")).toBe("ab");
|
|
45
|
+
expect(s.feed("\x1b]0;\x07")).toBeNull();
|
|
46
|
+
expect(s.feed("\x1b]0; \x07")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("TitlePublisher", () => {
|
|
51
|
+
it("writes on change, dedupes, and rate-limits within the window", () => {
|
|
52
|
+
const writes: string[] = [];
|
|
53
|
+
const p = new TitlePublisher((t) => writes.push(t), 2000);
|
|
54
|
+
p.observe("a");
|
|
55
|
+
expect(writes).toEqual(["a"]); // first write is immediate
|
|
56
|
+
p.observe("a");
|
|
57
|
+
expect(writes).toEqual(["a"]); // unchanged — no write
|
|
58
|
+
p.observe("b");
|
|
59
|
+
expect(writes).toEqual(["a"]); // changed but throttled
|
|
60
|
+
p.poll(Date.now() + 2500); // window elapsed — pending title lands
|
|
61
|
+
expect(writes).toEqual(["a", "b"]);
|
|
62
|
+
p.poll(Date.now() + 5000);
|
|
63
|
+
expect(writes).toEqual(["a", "b"]); // nothing pending — no write
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental scanner for terminal-title escapes (OSC 0/2) in the child CLI's
|
|
3
|
+
* PTY stream — the TS-runtime mirror of rs/src/title_scanner.rs; keep the two
|
|
4
|
+
* in sync.
|
|
5
|
+
*
|
|
6
|
+
* Coding-agent CLIs (claude, opencode, codex) continuously set the terminal
|
|
7
|
+
* title to a summary of what they are doing (`ESC ] 0 ; <title> BEL`, also
|
|
8
|
+
* terminated by `ESC \`). The wrapper observes the stream anyway, so scanning
|
|
9
|
+
* it yields a live "what is this agent doing" label for free — surfaced as
|
|
10
|
+
* `title` in the pid registry and shown by `ay whoami` / `ay ls --json`.
|
|
11
|
+
*
|
|
12
|
+
* Fed arbitrary chunk boundaries: a sequence split across reads still parses.
|
|
13
|
+
* The scanner only observes — it never modifies the stream.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** A title longer than this is a runaway OSC without terminator — discard. */
|
|
17
|
+
const MAX_TITLE_LEN = 512;
|
|
18
|
+
/** What we store/display. */
|
|
19
|
+
const STORED_TITLE_LEN = 256;
|
|
20
|
+
|
|
21
|
+
const enum State {
|
|
22
|
+
Ground,
|
|
23
|
+
Esc,
|
|
24
|
+
OscParam,
|
|
25
|
+
OscTitle,
|
|
26
|
+
OscOther,
|
|
27
|
+
OscTitleEsc,
|
|
28
|
+
OscOtherEsc,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class TitleScanner {
|
|
32
|
+
private state = State.Ground;
|
|
33
|
+
private param = "";
|
|
34
|
+
private title = "";
|
|
35
|
+
|
|
36
|
+
/** Feed one output chunk; returns the LAST complete title in it, if any. */
|
|
37
|
+
feed(chunk: string): string | null {
|
|
38
|
+
let found: string | null = null;
|
|
39
|
+
for (const c of chunk) {
|
|
40
|
+
switch (this.state) {
|
|
41
|
+
case State.Ground:
|
|
42
|
+
if (c === "\x1b") this.state = State.Esc;
|
|
43
|
+
break;
|
|
44
|
+
case State.Esc:
|
|
45
|
+
if (c === "]") {
|
|
46
|
+
this.param = "";
|
|
47
|
+
this.state = State.OscParam;
|
|
48
|
+
} else if (c !== "\x1b") {
|
|
49
|
+
this.state = State.Ground;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case State.OscParam:
|
|
53
|
+
if (c >= "0" && c <= "9" && this.param.length < 4) {
|
|
54
|
+
this.param += c;
|
|
55
|
+
} else if (c === ";") {
|
|
56
|
+
// OSC 0 (icon+title) and 2 (title) carry the window title; OSC 1
|
|
57
|
+
// is icon-only and everything else (8, 52, 133, …) is unrelated.
|
|
58
|
+
if (this.param === "0" || this.param === "2") {
|
|
59
|
+
this.title = "";
|
|
60
|
+
this.state = State.OscTitle;
|
|
61
|
+
} else {
|
|
62
|
+
this.state = State.OscOther;
|
|
63
|
+
}
|
|
64
|
+
} else if (c === "\x07") {
|
|
65
|
+
this.state = State.Ground;
|
|
66
|
+
} else if (c === "\x1b") {
|
|
67
|
+
this.state = State.OscOtherEsc;
|
|
68
|
+
} else {
|
|
69
|
+
this.state = State.OscOther;
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case State.OscTitle:
|
|
73
|
+
if (c === "\x07") {
|
|
74
|
+
found = cleanTitle(this.title);
|
|
75
|
+
this.state = State.Ground;
|
|
76
|
+
} else if (c === "\x1b") {
|
|
77
|
+
this.state = State.OscTitleEsc;
|
|
78
|
+
} else if (this.title.length >= MAX_TITLE_LEN) {
|
|
79
|
+
this.state = State.OscOther; // runaway — drop and resync
|
|
80
|
+
} else {
|
|
81
|
+
this.title += c;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case State.OscTitleEsc:
|
|
85
|
+
if (c === "\\") found = cleanTitle(this.title);
|
|
86
|
+
this.state = State.Ground; // a bare ESC aborts the OSC either way
|
|
87
|
+
break;
|
|
88
|
+
case State.OscOther:
|
|
89
|
+
if (c === "\x07") this.state = State.Ground;
|
|
90
|
+
else if (c === "\x1b") this.state = State.OscOtherEsc;
|
|
91
|
+
break;
|
|
92
|
+
case State.OscOtherEsc:
|
|
93
|
+
this.state = State.Ground;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return found || null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Strip control chars (they can't be typed into a title bar) and clamp. */
|
|
102
|
+
function cleanTitle(raw: string): string {
|
|
103
|
+
// eslint-disable-next-line no-control-regex
|
|
104
|
+
const trimmed = raw.replace(/[\x00-\x1f\x7f]/g, "").trim();
|
|
105
|
+
return trimmed.slice(0, STORED_TITLE_LEN);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Change-gated + rate-limited publisher: registry writes take the
|
|
110
|
+
* cross-runtime lock, while claude retitles every few seconds — the steady
|
|
111
|
+
* state must cost zero writes, and a retitle burst collapses to one write per
|
|
112
|
+
* window. Call `poll()` from any recurring point in the run loop so the final
|
|
113
|
+
* title still lands within a window after the CLI goes quiet.
|
|
114
|
+
*/
|
|
115
|
+
export class TitlePublisher {
|
|
116
|
+
private latest: string | null = null;
|
|
117
|
+
private written: string | null = null;
|
|
118
|
+
private writtenAt = 0;
|
|
119
|
+
|
|
120
|
+
constructor(
|
|
121
|
+
private readonly write: (title: string) => void,
|
|
122
|
+
private readonly minIntervalMs = 2_000,
|
|
123
|
+
) {}
|
|
124
|
+
|
|
125
|
+
observe(title: string): void {
|
|
126
|
+
this.latest = title;
|
|
127
|
+
this.poll();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
poll(now: number = Date.now()): void {
|
|
131
|
+
if (this.latest === null || this.latest === this.written) return;
|
|
132
|
+
if (now - this.writtenAt < this.minIntervalMs) return;
|
|
133
|
+
this.write(this.latest);
|
|
134
|
+
this.written = this.latest;
|
|
135
|
+
this.writtenAt = now;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import "./versionChecker-BTLZ3PAb.js";
|
|
3
|
-
import "./ts-BSHR_dga.js";
|
|
4
|
-
import "./todoAutomation-BQRW614S.js";
|
|
5
|
-
import "./JsonlStore-CPGIR0Wy.js";
|
|
6
|
-
import "./globalPidIndex-pwp7Vb7o.js";
|
|
7
|
-
import "./pidStore-DfSm1jog.js";
|
|
8
|
-
import "./messageLog-BYFwioFY.js";
|
|
9
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DjFlWHpg.js";
|
|
10
|
-
|
|
11
|
-
export { SUPPORTED_CLIS };
|
package/dist/trayApp-DrdktFnW.js
DELETED