agent-yes 1.258.3 → 1.259.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-DD6bhkyI.js → SUPPORTED_CLIS-BBeIFAZD.js} +2 -2
- package/dist/SUPPORTED_CLIS-BRgPb6Pz.js +11 -0
- package/dist/{agentShare-CoI7IHeq.js → agentShare-90Y_SFqK.js} +4 -4
- package/dist/{callback-CbdkyTRw.js → callback-CLzRxjEn.js} +3 -3
- package/dist/{callback-DNI__ASS.js → callback-DYQ2jQi2.js} +4 -4
- package/dist/cli.js +11 -11
- package/dist/{cwdConflictWarn-DZTxTGXq.js → cwdConflictWarn-B_047KCF.js} +2 -2
- package/dist/{forkNested-CgJ1rTnT.js → forkNested-CBdhC852.js} +2 -2
- package/dist/globalPidIndex-CHA__9Xf.js +4 -0
- package/dist/{globalPidIndex-Dq4KY2IV.js → globalPidIndex-pwp7Vb7o.js} +8 -2
- package/dist/index.js +4 -4
- package/dist/{notifyDaemon-BQ6_UbJS.js → notifyDaemon-D5RtFwMq.js} +4 -4
- package/dist/pidStore-1PQ4ARnh.js +6 -0
- package/dist/{pidStore-Cmcc8Oej.js → pidStore-DfSm1jog.js} +7 -4
- package/dist/{remotes-Dy4Rg8Yp.js → remotes-Cn-wszbK.js} +2 -2
- package/dist/{remotes-CNe85Icx.js → remotes-lOhHSoSS.js} +1 -1
- package/dist/{rustBinary-orhBmTNo.js → rustBinary-BEscpLLP.js} +2 -2
- package/dist/{schedule-BQy6gXYk.js → schedule-Cw5nW_di.js} +6 -6
- package/dist/{serve-C2mQMjUR.js → serve-DctJ7tY-.js} +19 -19
- package/dist/{setup-EEF1vfhi.js → setup-BA9D1ExO.js} +10 -2
- package/dist/{spawnGate-D0E1uRP7.js → spawnGate-D-68B907.js} +2 -2
- package/dist/spawnGate-DbSHnfqA.js +5 -0
- package/dist/{subcommands-CiX7J7hA.js → subcommands-D_cPHm34.js} +93 -24
- package/dist/{subcommands-S7uULM_d.js → subcommands-Dg7KkEi6.js} +3 -3
- package/dist/systemPathLink-DTAhFXFx.js +91 -0
- package/dist/{terminal-i7IimD_R.js → terminal-CB5K4Jjh.js} +2 -2
- package/dist/{todoCli-B2kqgqPh.js → todoCli-DNOk38lq.js} +2 -2
- package/dist/{tray-CgkwxtG6.js → tray-wWMvGWeY.js} +1 -1
- package/dist/trayApp-B1OhYfj3.js +5 -0
- package/dist/{trayApp-4sZgxxP-.js → trayApp-Ck3kWM-7.js} +2 -2
- package/dist/{ts-D-E4xqpA.js → ts-Bd_7sgVu.js} +5 -4
- package/dist/{versionChecker-YDl8a9il.js → versionChecker-DRijpksb.js} +2 -2
- package/dist/{webrtcRemote-C3OlHYSt.js → webrtcRemote-BkZcA5yq.js} +1 -1
- package/dist/{widget-7j9RrYZR.js → widget-DiRKbtng.js} +3 -3
- package/dist/{ws-DjyQ20AD.js → ws-Bp8eOEoV.js} +2 -2
- package/dist/{ws-Clem1WQ-.js → ws-Cqn8mxsS.js} +4 -4
- package/package.json +1 -1
- package/ts/globalPidIndex.spec.ts +36 -0
- package/ts/globalPidIndex.ts +19 -1
- package/ts/hist.spec.ts +219 -0
- package/ts/hist.ts +136 -0
- package/ts/histStore.spec.ts +351 -0
- package/ts/histStore.ts +494 -0
- package/ts/index.ts +4 -0
- package/ts/messageLog.ts +2 -0
- package/ts/pidStore.spec.ts +26 -0
- package/ts/pidStore.ts +10 -0
- package/ts/setup.ts +17 -0
- package/ts/subcommands.spec.ts +180 -0
- package/ts/subcommands.ts +103 -3
- package/ts/systemPathLink.spec.ts +69 -0
- package/ts/systemPathLink.ts +134 -0
- package/dist/SUPPORTED_CLIS-D1BoydrZ.js +0 -11
- package/dist/globalPidIndex-CQX_0JSB.js +0 -4
- package/dist/pidStore-BbKqQOS8.js +0 -6
- package/dist/spawnGate-DqKBA07k.js +0 -5
- package/dist/trayApp-C35IkXnz.js +0 -5
package/ts/subcommands.spec.ts
CHANGED
|
@@ -2675,6 +2675,119 @@ describe("subcommands.cmdWhoami", () => {
|
|
|
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
|
+
|
|
2678
2791
|
describe("subcommands.cmdSend double-envelope warning", () => {
|
|
2679
2792
|
const itUnix = process.platform === "linux" || process.platform === "darwin";
|
|
2680
2793
|
|
|
@@ -2819,4 +2932,71 @@ describe("subcommands.cmdSend double-envelope warning", () => {
|
|
|
2819
2932
|
await rm(tmp, { recursive: true, force: true }).catch(() => null);
|
|
2820
2933
|
}
|
|
2821
2934
|
});
|
|
2935
|
+
|
|
2936
|
+
it.skipIf(!itUnix)("a sender with a role gets it rendered into the envelope header", async () => {
|
|
2937
|
+
const { runSubcommand } = await loadModule();
|
|
2938
|
+
const { appendGlobalPid } = await import("./globalPidIndex.ts");
|
|
2939
|
+
const { spawnSync } = await import("child_process");
|
|
2940
|
+
const tmp = await mkdtemp(path.join(tmpdir(), "ay-fifo-"));
|
|
2941
|
+
try {
|
|
2942
|
+
const fifo = path.join(tmp, "role.fifo");
|
|
2943
|
+
if (spawnSync("mkfifo", [fifo]).status !== 0) return;
|
|
2944
|
+
const fs = await import("fs");
|
|
2945
|
+
const rdwrFd = fs.openSync(fifo, fs.constants.O_RDWR);
|
|
2946
|
+
await appendGlobalPid({
|
|
2947
|
+
pid: process.pid,
|
|
2948
|
+
cli: "claude",
|
|
2949
|
+
prompt: null,
|
|
2950
|
+
cwd: "/repo/alpha",
|
|
2951
|
+
log_file: null,
|
|
2952
|
+
fifo_file: fifo,
|
|
2953
|
+
status: "active",
|
|
2954
|
+
exit_code: null,
|
|
2955
|
+
exit_reason: null,
|
|
2956
|
+
started_at: Date.now(),
|
|
2957
|
+
});
|
|
2958
|
+
await appendGlobalPid({
|
|
2959
|
+
pid: 900001,
|
|
2960
|
+
cli: "claude",
|
|
2961
|
+
prompt: null,
|
|
2962
|
+
cwd: "/repo/beta",
|
|
2963
|
+
log_file: null,
|
|
2964
|
+
status: "active",
|
|
2965
|
+
exit_code: null,
|
|
2966
|
+
exit_reason: null,
|
|
2967
|
+
started_at: Date.now(),
|
|
2968
|
+
wrapper_pid: 424242,
|
|
2969
|
+
agent_id: "cccc0000dddd",
|
|
2970
|
+
role: "crm",
|
|
2971
|
+
});
|
|
2972
|
+
const savedAyPid = process.env.AGENT_YES_PID;
|
|
2973
|
+
process.env.AGENT_YES_PID = "424242";
|
|
2974
|
+
const origOut = process.stdout.write.bind(process.stdout);
|
|
2975
|
+
(process.stdout as any).write = () => true;
|
|
2976
|
+
try {
|
|
2977
|
+
const code = await runSubcommand([
|
|
2978
|
+
"bun",
|
|
2979
|
+
"cli.js",
|
|
2980
|
+
"send",
|
|
2981
|
+
String(process.pid),
|
|
2982
|
+
"hello from crm",
|
|
2983
|
+
"--force",
|
|
2984
|
+
]);
|
|
2985
|
+
expect(code).toBe(0);
|
|
2986
|
+
} finally {
|
|
2987
|
+
process.stdout.write = origOut;
|
|
2988
|
+
if (savedAyPid === undefined) delete process.env.AGENT_YES_PID;
|
|
2989
|
+
else process.env.AGENT_YES_PID = savedAyPid;
|
|
2990
|
+
}
|
|
2991
|
+
const buf = Buffer.alloc(4096);
|
|
2992
|
+
const n = fs.readSync(rdwrFd, buf, 0, buf.length, null);
|
|
2993
|
+
const received = buf.subarray(0, n).toString();
|
|
2994
|
+
expect(received).toMatch(
|
|
2995
|
+
/^<ay-msg [0-9a-f]{8} from claude #900001 \(crm\) @ \/repo\/beta — reply: ay send cccc0000dddd "\.\.\.">/,
|
|
2996
|
+
);
|
|
2997
|
+
fs.closeSync(rdwrFd);
|
|
2998
|
+
} finally {
|
|
2999
|
+
await rm(tmp, { recursive: true, force: true }).catch(() => null);
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
2822
3002
|
});
|
package/ts/subcommands.ts
CHANGED
|
@@ -16,7 +16,12 @@ import { appendFile, mkdir, open, readFile, stat, writeFile } from "fs/promises"
|
|
|
16
16
|
import ms from "ms";
|
|
17
17
|
import { homedir } from "os";
|
|
18
18
|
import path from "path";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type GlobalPidRecord,
|
|
21
|
+
readGlobalPids,
|
|
22
|
+
sanitizeRole,
|
|
23
|
+
updateGlobalPidStatus,
|
|
24
|
+
} from "./globalPidIndex.ts";
|
|
20
25
|
import { buildAgentForest, flattenForest } from "./agentTree.ts";
|
|
21
26
|
import { parseTaskCounts, type TaskCounts } from "./todoParse.ts";
|
|
22
27
|
import { agentYesHome } from "./agentYesHome.ts";
|
|
@@ -321,6 +326,7 @@ async function cmdWhoami(rest: string[]): Promise<number> {
|
|
|
321
326
|
const ageMin = Math.max(0, Math.round((Date.now() - self.started_at) / 60_000));
|
|
322
327
|
const lines = [
|
|
323
328
|
`agent ${self.cli} #${self.pid}${self.agent_id ? ` (agent_id ${self.agent_id})` : ""}`,
|
|
329
|
+
`role ${self.role ?? "- (set with: ay role <name>)"}`,
|
|
324
330
|
`state ${state}${question ? ` — ${question}` : ""}`,
|
|
325
331
|
`cwd ${self.cwd}`,
|
|
326
332
|
`started ${new Date(self.started_at).toISOString()} (${ageMin}m ago)`,
|
|
@@ -328,7 +334,7 @@ async function cmdWhoami(rest: string[]): Promise<number> {
|
|
|
328
334
|
`log ${self.log_file ?? "-"}`,
|
|
329
335
|
`fifo ${self.fifo_file ?? "-"}`,
|
|
330
336
|
`reply ${reply} "..."`,
|
|
331
|
-
`envelope <ay-msg from ${self.cli} #${self.pid} @ ${self.cwd} — reply: ${reply} "...">…</ay-msg>`,
|
|
337
|
+
`envelope <ay-msg from ${self.cli} #${self.pid}${self.role ? ` (${self.role})` : ""} @ ${self.cwd} — reply: ${reply} "...">…</ay-msg>`,
|
|
332
338
|
];
|
|
333
339
|
process.stdout.write(lines.join("\n") + "\n");
|
|
334
340
|
return 0;
|
|
@@ -379,6 +385,7 @@ async function readLocalTsPids(cwd: string): Promise<GlobalPidRecord[]> {
|
|
|
379
385
|
exit_code: d.exitCode ?? null,
|
|
380
386
|
exit_reason: d.exitReason ?? null,
|
|
381
387
|
started_at: d.startedAt ?? 0,
|
|
388
|
+
role: d.role ?? null,
|
|
382
389
|
}));
|
|
383
390
|
}
|
|
384
391
|
|
|
@@ -413,6 +420,7 @@ const SUBCOMMANDS = new Set([
|
|
|
413
420
|
"head",
|
|
414
421
|
"send",
|
|
415
422
|
"msgs",
|
|
423
|
+
"role",
|
|
416
424
|
"key",
|
|
417
425
|
"select",
|
|
418
426
|
"spawn",
|
|
@@ -545,6 +553,8 @@ export async function runSubcommand(argv: string[]): Promise<number | null> {
|
|
|
545
553
|
return await cmdSend(rest);
|
|
546
554
|
case "msgs":
|
|
547
555
|
return await cmdMsgs(rest);
|
|
556
|
+
case "role":
|
|
557
|
+
return await cmdRole(rest);
|
|
548
558
|
case "key":
|
|
549
559
|
return await cmdKey(rest);
|
|
550
560
|
case "select":
|
|
@@ -721,6 +731,7 @@ export async function cmdHelp(managerCommands = true): Promise<number> {
|
|
|
721
731
|
` ay head <keyword> first N lines\n` +
|
|
722
732
|
` ay send <keyword> <msg> send a message (keyword '.' = agent in this cwd)\n` +
|
|
723
733
|
` ay msgs [keyword] [--in|--out] inter-agent message log (sent + received)\n` +
|
|
734
|
+
` ay role [name] | ay role <kw> <name> lane/role name shown in the send envelope\n` +
|
|
724
735
|
` ay ch mk|join|send|read|tail <topic> local-first E2E channels: AI ↔ humans on a topic (ay ch help)\n` +
|
|
725
736
|
` ay term embed <pid> <script> to embed a live read-only agent terminal in a page (ay term help)\n` +
|
|
726
737
|
` ay widget ls | read selection|dom read an opted-in page widget's context (selection/DOM) (ay widget help)\n` +
|
|
@@ -3310,7 +3321,13 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
3310
3321
|
let nonce: string | undefined;
|
|
3311
3322
|
if (sender.agent && !isSlashCommand(body) && !raw) {
|
|
3312
3323
|
nonce = randomBytes(4).toString("hex");
|
|
3313
|
-
|
|
3324
|
+
// The sender's self-declared role ("pm", "crm", …) rides along when set —
|
|
3325
|
+
// recipients read dozens of these a day and a pid/cwd doesn't carry the
|
|
3326
|
+
// lane name (cwd → role isn't always derivable). sanitizeRole guarantees
|
|
3327
|
+
// the value can't forge header syntax.
|
|
3328
|
+
const senderRole = sanitizeRole(sender.agent.role);
|
|
3329
|
+
const roleTag = senderRole ? ` (${senderRole})` : "";
|
|
3330
|
+
prefix = `<ay-msg ${nonce} from ${sender.agent.cli} #${sender.agent.pid}${roleTag} @ ${shortenPath(sender.agent.cwd)} — reply: ay send ${replyTarget} "...">\n`;
|
|
3314
3331
|
suffix = `\n</ay-msg ${nonce}>`;
|
|
3315
3332
|
// A body that itself starts with an <ay-msg …> header is usually an agent
|
|
3316
3333
|
// hand-wrapping what this send is about to wrap again — the recipient then
|
|
@@ -3391,6 +3408,7 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
3391
3408
|
cli: sender.agent.cli,
|
|
3392
3409
|
cwd: sender.agent.cwd,
|
|
3393
3410
|
agent_id: sender.agent.agent_id,
|
|
3411
|
+
role: sender.agent.role ?? undefined,
|
|
3394
3412
|
}
|
|
3395
3413
|
: null,
|
|
3396
3414
|
to: {
|
|
@@ -3398,6 +3416,7 @@ async function cmdSend(rest: string[]): Promise<number> {
|
|
|
3398
3416
|
cli: record.cli,
|
|
3399
3417
|
cwd: record.cwd,
|
|
3400
3418
|
agent_id: record.agent_id,
|
|
3419
|
+
role: record.role ?? undefined,
|
|
3401
3420
|
},
|
|
3402
3421
|
body,
|
|
3403
3422
|
code: trailing === "\r" ? undefined : codeName,
|
|
@@ -3569,6 +3588,87 @@ async function cmdMsgs(rest: string[]): Promise<number> {
|
|
|
3569
3588
|
return 0;
|
|
3570
3589
|
}
|
|
3571
3590
|
|
|
3591
|
+
/**
|
|
3592
|
+
* `ay role [name]` / `ay role <keyword> <name>` — read or set an agent's
|
|
3593
|
+
* self-declared lane/role name ("pm", "crm", …). The role rides in the
|
|
3594
|
+
* `ay send` envelope header (`from claude #123 (pm) @ …`) so recipients don't
|
|
3595
|
+
* translate cwd/pid into a lane by hand — the emergent `[pm→qa]` body prefixes
|
|
3596
|
+
* exist exactly because the envelope lacked this. With no args: print the
|
|
3597
|
+
* calling agent's role. With one arg: set the calling agent's role (requires
|
|
3598
|
+
* agent context). With two args: resolve the keyword and set that agent's role.
|
|
3599
|
+
* `--clear` removes it. Roles are clamped by sanitizeRole (they render into the
|
|
3600
|
+
* non-nonce-protected open tag).
|
|
3601
|
+
*/
|
|
3602
|
+
const ROLE_RESOLVE_OPTS: CommonOpts = {
|
|
3603
|
+
all: false,
|
|
3604
|
+
active: false,
|
|
3605
|
+
cwdScope: null,
|
|
3606
|
+
latest: false,
|
|
3607
|
+
json: false,
|
|
3608
|
+
};
|
|
3609
|
+
|
|
3610
|
+
async function cmdRole(rest: string[]): Promise<number> {
|
|
3611
|
+
const y = yargs(rest)
|
|
3612
|
+
.usage('Usage: ay role [name] | ay role <keyword> <name> | ay role [keyword] --clear')
|
|
3613
|
+
.option("clear", { type: "boolean", default: false, description: "Remove the role" })
|
|
3614
|
+
.help(false)
|
|
3615
|
+
.version(false)
|
|
3616
|
+
.exitProcess(false);
|
|
3617
|
+
const argv = await y.parseAsync();
|
|
3618
|
+
const args = argv._.slice(0).map(String);
|
|
3619
|
+
|
|
3620
|
+
// Figure out target + new value from arity: 0 args = self get (or self clear),
|
|
3621
|
+
// 1 arg = self set (or keyword clear), 2 args = keyword set.
|
|
3622
|
+
let target: GlobalPidRecord | null = null;
|
|
3623
|
+
let newRole: string | undefined;
|
|
3624
|
+
if (args.length === 2) {
|
|
3625
|
+
target = await resolveOne(args[0]!, ROLE_RESOLVE_OPTS);
|
|
3626
|
+
newRole = args[1]!;
|
|
3627
|
+
} else if (args.length === 1 && argv.clear) {
|
|
3628
|
+
target = await resolveOne(args[0]!, ROLE_RESOLVE_OPTS);
|
|
3629
|
+
} else if (args.length === 1) {
|
|
3630
|
+
target = await resolveSender();
|
|
3631
|
+
if (!target) {
|
|
3632
|
+
process.stderr.write(
|
|
3633
|
+
"ay role: no agent context (AGENT_YES_PID unset or unregistered) — from a human shell, name the agent: ay role <keyword> <name>\n",
|
|
3634
|
+
);
|
|
3635
|
+
return 1;
|
|
3636
|
+
}
|
|
3637
|
+
newRole = args[0]!;
|
|
3638
|
+
} else {
|
|
3639
|
+
target = await resolveSender();
|
|
3640
|
+
if (!target) {
|
|
3641
|
+
process.stderr.write(
|
|
3642
|
+
"ay role: no agent context — from a human shell, name the agent: ay role <keyword> [<name>|--clear]\n",
|
|
3643
|
+
);
|
|
3644
|
+
return 1;
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
if (newRole === undefined && !argv.clear) {
|
|
3649
|
+
process.stdout.write(`${target.role ?? ""}\n`);
|
|
3650
|
+
return 0;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
let role: string | null = null;
|
|
3654
|
+
if (!argv.clear) {
|
|
3655
|
+
role = sanitizeRole(newRole);
|
|
3656
|
+
if (!role) {
|
|
3657
|
+
process.stderr.write(
|
|
3658
|
+
`ay role: invalid role ${JSON.stringify(newRole)} — use 1-32 chars of [A-Za-z0-9._-]\n`,
|
|
3659
|
+
);
|
|
3660
|
+
return 1;
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
await updateGlobalPidStatus(target.pid, { role });
|
|
3664
|
+
process.stdout.write(
|
|
3665
|
+
role
|
|
3666
|
+
? `pid ${target.pid} (${target.cli}): role set to "${role}" — envelopes now read: from ${target.cli} #${target.pid} (${role}) @ ${shortenPath(target.cwd)}\n`
|
|
3667
|
+
: `pid ${target.pid} (${target.cli}): role cleared\n`,
|
|
3668
|
+
);
|
|
3669
|
+
return 0;
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3572
3672
|
// Resolve a keyword to one agent and return it with a writable FIFO, or throw
|
|
3573
3673
|
// with the same guidance cmdSend gives. Shared by `ay key` / `ay select`.
|
|
3574
3674
|
async function resolveWritableAgent(keyword: string, opts: CommonOpts): Promise<GlobalPidRecord> {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, lstatSync, readlinkSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { linkBunBinsToSystemPath } from "./systemPathLink.ts";
|
|
6
|
+
|
|
7
|
+
// linkBunBinsToSystemPath is POSIX-only by design (it returns null on win32 —
|
|
8
|
+
// Windows uses a different PATH model), so the behavioral tests only run where
|
|
9
|
+
// the code path exists.
|
|
10
|
+
const itPosix = it.skipIf(process.platform === "win32");
|
|
11
|
+
|
|
12
|
+
describe("systemPathLink.linkBunBinsToSystemPath", () => {
|
|
13
|
+
let root: string;
|
|
14
|
+
let bunDir: string;
|
|
15
|
+
let target: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
root = mkdtempSync(path.join(tmpdir(), "ay-pathlink-"));
|
|
19
|
+
bunDir = path.join(root, "bun", "bin");
|
|
20
|
+
target = path.join(root, "usr-local-bin");
|
|
21
|
+
mkdirSync(bunDir, { recursive: true });
|
|
22
|
+
for (const name of ["ay", "cy", "qq"]) writeFileSync(path.join(bunDir, name), "#!/bin/sh\n");
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => rmSync(root, { recursive: true, force: true }));
|
|
25
|
+
|
|
26
|
+
itPosix("symlinks every bun bin into the target dir", () => {
|
|
27
|
+
const r = linkBunBinsToSystemPath({ bunDir, target })!;
|
|
28
|
+
expect(r.linked.sort()).toEqual(["ay", "cy", "qq"]);
|
|
29
|
+
for (const name of ["ay", "cy", "qq"]) {
|
|
30
|
+
const dest = path.join(target, name);
|
|
31
|
+
expect(lstatSync(dest).isSymbolicLink()).toBe(true);
|
|
32
|
+
expect(path.resolve(target, readlinkSync(dest))).toBe(path.join(bunDir, name));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
itPosix("never clobbers a foreign (real) binary already in the target", () => {
|
|
37
|
+
mkdirSync(target, { recursive: true });
|
|
38
|
+
writeFileSync(path.join(target, "cy"), "#!/bin/sh\n# a different real cy\n");
|
|
39
|
+
const r = linkBunBinsToSystemPath({ bunDir, target })!;
|
|
40
|
+
expect(r.linked.sort()).toEqual(["ay", "qq"]); // cy left alone
|
|
41
|
+
expect(r.skipped).toContain("cy");
|
|
42
|
+
expect(lstatSync(path.join(target, "cy")).isSymbolicLink()).toBe(false); // still the real file
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
itPosix("is idempotent — re-running skips links it already owns", () => {
|
|
46
|
+
linkBunBinsToSystemPath({ bunDir, target });
|
|
47
|
+
const r2 = linkBunBinsToSystemPath({ bunDir, target })!;
|
|
48
|
+
expect(r2.linked).toEqual([]);
|
|
49
|
+
expect(r2.skipped.sort()).toEqual(["ay", "cy", "qq"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
itPosix("refreshes a dangling symlink it owns (name points into bunDir but target gone)", () => {
|
|
53
|
+
mkdirSync(target, { recursive: true });
|
|
54
|
+
// a stale link named "ay" pointing at a now-missing path INSIDE bunDir
|
|
55
|
+
symlinkSync(path.join(bunDir, "ay-old-removed"), path.join(target, "ay"));
|
|
56
|
+
const r = linkBunBinsToSystemPath({ bunDir, target })!;
|
|
57
|
+
expect(r.refreshed).toContain("ay");
|
|
58
|
+
expect(path.resolve(target, readlinkSync(path.join(target, "ay")))).toBe(path.join(bunDir, "ay"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns null when the bun dir is absent", () => {
|
|
62
|
+
expect(linkBunBinsToSystemPath({ bunDir: path.join(root, "nope"), target })).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns null on win32 regardless of inputs", () => {
|
|
66
|
+
if (process.platform !== "win32") return; // covered implicitly elsewhere
|
|
67
|
+
expect(linkBunBinsToSystemPath({ bunDir, target })).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Make bun-global CLIs (ay, cy, the *-yes family, qq, rech, …) resolvable from
|
|
2
|
+
// EVERY shell context, including the hard case: non-interactive non-login shells
|
|
3
|
+
// and Claude Code's Bash tool.
|
|
4
|
+
//
|
|
5
|
+
// Why this is needed: Claude Code's Bash tool sources a per-session shell
|
|
6
|
+
// *snapshot* that statically `export`s a PATH captured at session start. If that
|
|
7
|
+
// PATH didn't include `~/.bun/bin` (e.g. the session was spawned from a context
|
|
8
|
+
// whose PATH lacked it), every bun-global CLI is "command not found" — even
|
|
9
|
+
// though an interactive login shell would have them. There is no bash file that
|
|
10
|
+
// reliably augments PATH for non-interactive non-login shells.
|
|
11
|
+
//
|
|
12
|
+
// The robust fix: symlink the bun-global bins into `/usr/local/bin`, which is on
|
|
13
|
+
// PATH in EVERY context — the bare non-login default
|
|
14
|
+
// (`/usr/local/bin:/usr/local/sbin:/usr/bin:…`), every snapshot, and login
|
|
15
|
+
// shells. Non-clobbering (never overwrites an existing entry) and best-effort
|
|
16
|
+
// (a scheduling/permission hiccup must never break setup). POSIX only — Windows
|
|
17
|
+
// uses a different PATH model and isn't the reported failure surface.
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
existsSync,
|
|
21
|
+
lstatSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
readlinkSync,
|
|
25
|
+
symlinkSync,
|
|
26
|
+
unlinkSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import { homedir } from "node:os";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
|
|
31
|
+
/** Did a (now-dangling) symlink at `linkPath` point into `dir`? Used to adopt
|
|
32
|
+
* only stale links we previously created, never a foreign broken link. */
|
|
33
|
+
function danglingPointsInto(linkPath: string, dir: string): boolean {
|
|
34
|
+
try {
|
|
35
|
+
const oldTarget = path.resolve(path.dirname(linkPath), readlinkSync(linkPath));
|
|
36
|
+
return path.resolve(path.dirname(oldTarget)) === path.resolve(dir);
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The directory bun installs global binaries into, or null if absent. */
|
|
43
|
+
export function bunBinDir(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
44
|
+
const install = env.BUN_INSTALL || path.join(homedir(), ".bun");
|
|
45
|
+
const dir = path.join(install, "bin");
|
|
46
|
+
return existsSync(dir) ? dir : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface LinkResult {
|
|
50
|
+
target: string;
|
|
51
|
+
linked: string[]; // names newly symlinked
|
|
52
|
+
skipped: string[]; // names already present (not ours to touch)
|
|
53
|
+
refreshed: string[]; // our own dangling/stale symlinks repointed
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Symlink each entry of the bun-global bin dir into `target` (default
|
|
58
|
+
* /usr/local/bin). Skips a name that already exists UNLESS it's a symlink we
|
|
59
|
+
* previously created into the same bun dir (those we refresh, so a rebuilt/moved
|
|
60
|
+
* bun bin stays valid). Returns null on an unsupported platform or when neither
|
|
61
|
+
* the bun dir nor the target is usable. Never throws.
|
|
62
|
+
*/
|
|
63
|
+
export function linkBunBinsToSystemPath(opts?: {
|
|
64
|
+
bunDir?: string | null;
|
|
65
|
+
target?: string;
|
|
66
|
+
env?: NodeJS.ProcessEnv;
|
|
67
|
+
}): LinkResult | null {
|
|
68
|
+
if (process.platform === "win32") return null;
|
|
69
|
+
const env = opts?.env ?? process.env;
|
|
70
|
+
const bunDir = opts?.bunDir ?? bunBinDir(env);
|
|
71
|
+
if (!bunDir) return null;
|
|
72
|
+
const target = opts?.target ?? "/usr/local/bin";
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
if (!existsSync(target)) mkdirSync(target, { recursive: true });
|
|
76
|
+
} catch {
|
|
77
|
+
return null; // can't create the target dir (permissions) — best-effort give up
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result: LinkResult = { target, linked: [], skipped: [], refreshed: [] };
|
|
81
|
+
let entries: string[];
|
|
82
|
+
try {
|
|
83
|
+
entries = readdirSync(bunDir);
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const name of entries) {
|
|
89
|
+
const src = path.join(bunDir, name);
|
|
90
|
+
const dest = path.join(target, name);
|
|
91
|
+
let existing: ReturnType<typeof lstatSync> | null = null;
|
|
92
|
+
try {
|
|
93
|
+
existing = lstatSync(dest);
|
|
94
|
+
} catch {
|
|
95
|
+
existing = null; // dest absent
|
|
96
|
+
}
|
|
97
|
+
if (existing) {
|
|
98
|
+
// Only touch a symlink WE own (points back into this bun dir). Anything
|
|
99
|
+
// else (a real binary, or a link elsewhere) is not ours to replace.
|
|
100
|
+
let ours = false;
|
|
101
|
+
if (existing.isSymbolicLink()) {
|
|
102
|
+
try {
|
|
103
|
+
ours = path.resolve(target, readlinkSync(dest)) === src;
|
|
104
|
+
} catch {
|
|
105
|
+
ours = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (ours) {
|
|
109
|
+
result.skipped.push(name); // already correctly linked — nothing to do
|
|
110
|
+
} else if (existing.isSymbolicLink() && !existsSync(dest) && danglingPointsInto(dest, bunDir)) {
|
|
111
|
+
// A dangling symlink that USED to point into our bun dir (a bin that
|
|
112
|
+
// moved/rebuilt) — repoint it. We check the old target dir so we never
|
|
113
|
+
// hijack an unrelated broken link that merely shares a name.
|
|
114
|
+
try {
|
|
115
|
+
unlinkSync(dest);
|
|
116
|
+
symlinkSync(src, dest);
|
|
117
|
+
result.refreshed.push(name);
|
|
118
|
+
} catch {
|
|
119
|
+
result.skipped.push(name);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
result.skipped.push(name); // a real/foreign entry — never clobber
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
symlinkSync(src, dest);
|
|
128
|
+
result.linked.push(name);
|
|
129
|
+
} catch {
|
|
130
|
+
result.skipped.push(name); // permission/race — best effort
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import "./versionChecker-YDl8a9il.js";
|
|
3
|
-
import "./ts-D-E4xqpA.js";
|
|
4
|
-
import "./todoAutomation-BQRW614S.js";
|
|
5
|
-
import "./JsonlStore-CPGIR0Wy.js";
|
|
6
|
-
import "./globalPidIndex-Dq4KY2IV.js";
|
|
7
|
-
import "./pidStore-Cmcc8Oej.js";
|
|
8
|
-
import "./messageLog-BYFwioFY.js";
|
|
9
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DD6bhkyI.js";
|
|
10
|
-
|
|
11
|
-
export { SUPPORTED_CLIS };
|
package/dist/trayApp-C35IkXnz.js
DELETED