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.
Files changed (57) hide show
  1. package/dist/{SUPPORTED_CLIS-DD6bhkyI.js → SUPPORTED_CLIS-BBeIFAZD.js} +2 -2
  2. package/dist/SUPPORTED_CLIS-BRgPb6Pz.js +11 -0
  3. package/dist/{agentShare-CoI7IHeq.js → agentShare-90Y_SFqK.js} +4 -4
  4. package/dist/{callback-CbdkyTRw.js → callback-CLzRxjEn.js} +3 -3
  5. package/dist/{callback-DNI__ASS.js → callback-DYQ2jQi2.js} +4 -4
  6. package/dist/cli.js +11 -11
  7. package/dist/{cwdConflictWarn-DZTxTGXq.js → cwdConflictWarn-B_047KCF.js} +2 -2
  8. package/dist/{forkNested-CgJ1rTnT.js → forkNested-CBdhC852.js} +2 -2
  9. package/dist/globalPidIndex-CHA__9Xf.js +4 -0
  10. package/dist/{globalPidIndex-Dq4KY2IV.js → globalPidIndex-pwp7Vb7o.js} +8 -2
  11. package/dist/index.js +4 -4
  12. package/dist/{notifyDaemon-BQ6_UbJS.js → notifyDaemon-D5RtFwMq.js} +4 -4
  13. package/dist/pidStore-1PQ4ARnh.js +6 -0
  14. package/dist/{pidStore-Cmcc8Oej.js → pidStore-DfSm1jog.js} +7 -4
  15. package/dist/{remotes-Dy4Rg8Yp.js → remotes-Cn-wszbK.js} +2 -2
  16. package/dist/{remotes-CNe85Icx.js → remotes-lOhHSoSS.js} +1 -1
  17. package/dist/{rustBinary-orhBmTNo.js → rustBinary-BEscpLLP.js} +2 -2
  18. package/dist/{schedule-BQy6gXYk.js → schedule-Cw5nW_di.js} +6 -6
  19. package/dist/{serve-C2mQMjUR.js → serve-DctJ7tY-.js} +19 -19
  20. package/dist/{setup-EEF1vfhi.js → setup-BA9D1ExO.js} +10 -2
  21. package/dist/{spawnGate-D0E1uRP7.js → spawnGate-D-68B907.js} +2 -2
  22. package/dist/spawnGate-DbSHnfqA.js +5 -0
  23. package/dist/{subcommands-CiX7J7hA.js → subcommands-D_cPHm34.js} +93 -24
  24. package/dist/{subcommands-S7uULM_d.js → subcommands-Dg7KkEi6.js} +3 -3
  25. package/dist/systemPathLink-DTAhFXFx.js +91 -0
  26. package/dist/{terminal-i7IimD_R.js → terminal-CB5K4Jjh.js} +2 -2
  27. package/dist/{todoCli-B2kqgqPh.js → todoCli-DNOk38lq.js} +2 -2
  28. package/dist/{tray-CgkwxtG6.js → tray-wWMvGWeY.js} +1 -1
  29. package/dist/trayApp-B1OhYfj3.js +5 -0
  30. package/dist/{trayApp-4sZgxxP-.js → trayApp-Ck3kWM-7.js} +2 -2
  31. package/dist/{ts-D-E4xqpA.js → ts-Bd_7sgVu.js} +5 -4
  32. package/dist/{versionChecker-YDl8a9il.js → versionChecker-DRijpksb.js} +2 -2
  33. package/dist/{webrtcRemote-C3OlHYSt.js → webrtcRemote-BkZcA5yq.js} +1 -1
  34. package/dist/{widget-7j9RrYZR.js → widget-DiRKbtng.js} +3 -3
  35. package/dist/{ws-DjyQ20AD.js → ws-Bp8eOEoV.js} +2 -2
  36. package/dist/{ws-Clem1WQ-.js → ws-Cqn8mxsS.js} +4 -4
  37. package/package.json +1 -1
  38. package/ts/globalPidIndex.spec.ts +36 -0
  39. package/ts/globalPidIndex.ts +19 -1
  40. package/ts/hist.spec.ts +219 -0
  41. package/ts/hist.ts +136 -0
  42. package/ts/histStore.spec.ts +351 -0
  43. package/ts/histStore.ts +494 -0
  44. package/ts/index.ts +4 -0
  45. package/ts/messageLog.ts +2 -0
  46. package/ts/pidStore.spec.ts +26 -0
  47. package/ts/pidStore.ts +10 -0
  48. package/ts/setup.ts +17 -0
  49. package/ts/subcommands.spec.ts +180 -0
  50. package/ts/subcommands.ts +103 -3
  51. package/ts/systemPathLink.spec.ts +69 -0
  52. package/ts/systemPathLink.ts +134 -0
  53. package/dist/SUPPORTED_CLIS-D1BoydrZ.js +0 -11
  54. package/dist/globalPidIndex-CQX_0JSB.js +0 -4
  55. package/dist/pidStore-BbKqQOS8.js +0 -6
  56. package/dist/spawnGate-DqKBA07k.js +0 -5
  57. package/dist/trayApp-C35IkXnz.js +0 -5
@@ -0,0 +1,219 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
3
+ import { tmpdir } from "os";
4
+ import path from "path";
5
+ import { cmdHist, renderRecord, snippet } from "./hist.ts";
6
+ import { encodeProjectSlug, type HistRecord } from "./histStore.ts";
7
+
8
+ let testHome: string;
9
+
10
+ vi.mock("os", async () => {
11
+ const actual = await vi.importActual<typeof import("os")>("os");
12
+ return {
13
+ ...actual,
14
+ homedir: () => testHome,
15
+ };
16
+ });
17
+
18
+ beforeEach(async () => {
19
+ testHome = await mkdtemp(path.join(tmpdir(), "ay-hist-test-"));
20
+ });
21
+
22
+ afterEach(async () => {
23
+ await rm(testHome, { recursive: true, force: true }).catch(() => null);
24
+ });
25
+
26
+ const rec: HistRecord = {
27
+ ts: "2026-08-06T09:29:37.533Z",
28
+ role: "user",
29
+ text: "hello there",
30
+ source: "claude",
31
+ sessionId: "ff1f38ee-78aa-48a6",
32
+ file: "/tmp/x.jsonl",
33
+ offset: 0,
34
+ };
35
+
36
+ describe("snippet", () => {
37
+ it("leaves short text alone", () => {
38
+ expect(snippet("short", 100)).toBe("short");
39
+ });
40
+
41
+ it("truncates and reports what was withheld", () => {
42
+ const out = snippet("a".repeat(50) + "\n" + "b".repeat(50), 10);
43
+ expect(out.startsWith("a".repeat(10))).toBe(true);
44
+ expect(out).toContain("+91 chars");
45
+ expect(out).toContain("2 more lines");
46
+ });
47
+
48
+ it("treats max 0 as unlimited, for --full", () => {
49
+ const long = "x".repeat(5000);
50
+ expect(snippet(long, 0)).toBe(long);
51
+ });
52
+ });
53
+
54
+ describe("renderRecord", () => {
55
+ it("labels the human as user and the agent by its source", () => {
56
+ expect(renderRecord(rec, { color: false, max: 0 })).toContain("user");
57
+ expect(
58
+ renderRecord({ ...rec, role: "assistant" }, { color: false, max: 0 }),
59
+ ).toContain("claude");
60
+ expect(
61
+ renderRecord({ ...rec, role: "assistant", source: "codex" }, { color: false, max: 0 }),
62
+ ).toContain("codex");
63
+ });
64
+
65
+ it("emits no ANSI when color is off, so pipes stay clean", () => {
66
+ const out = renderRecord(rec, { color: false, max: 0 });
67
+ // eslint-disable-next-line no-control-regex
68
+ expect(/\x1b\[/.test(out)).toBe(false);
69
+ });
70
+
71
+ it("colors user and agent turns differently when color is on", () => {
72
+ const user = renderRecord(rec, { color: true, max: 0 });
73
+ const agent = renderRecord({ ...rec, role: "assistant" }, { color: true, max: 0 });
74
+ expect(user).toContain("\x1b[36m");
75
+ expect(agent).toContain("\x1b[32m");
76
+ });
77
+
78
+ it("indents the body and shows a short session id", () => {
79
+ const out = renderRecord(rec, { color: false, max: 0 });
80
+ expect(out).toContain("\n hello there");
81
+ expect(out).toContain("ff1f38ee");
82
+ expect(out).not.toContain("ff1f38ee-78aa");
83
+ });
84
+
85
+ it("survives a missing timestamp", () => {
86
+ expect(renderRecord({ ...rec, ts: null }, { color: false, max: 0 })).toContain("??:??");
87
+ });
88
+
89
+ it("renders an unparseable timestamp as ??:??", () => {
90
+ expect(renderRecord({ ...rec, ts: "not-a-date" }, { color: false, max: 0 })).toContain("??:??");
91
+ });
92
+
93
+ it("shows a bare HH:MM for a same-day timestamp and MM-DD for older ones", () => {
94
+ const today = renderRecord({ ...rec, ts: new Date().toISOString() }, { color: false, max: 0 });
95
+ expect(today).not.toMatch(/\d{2}-\d{2} \d{2}:\d{2}/);
96
+ const old = renderRecord({ ...rec, ts: "2020-01-02T03:04:05.000Z" }, { color: false, max: 0 });
97
+ expect(old).toMatch(/\d{2}-\d{2} \d{2}:\d{2}/);
98
+ });
99
+ });
100
+
101
+ describe("cmdHist", () => {
102
+ function capture() {
103
+ const out: string[] = [];
104
+ const err: string[] = [];
105
+ const origOut = process.stdout.write.bind(process.stdout);
106
+ const origErr = process.stderr.write.bind(process.stderr);
107
+ (process.stdout as any).write = (s: any) => (out.push(String(s)), true);
108
+ (process.stderr as any).write = (s: any) => (err.push(String(s)), true);
109
+ return {
110
+ out,
111
+ err,
112
+ restore() {
113
+ process.stdout.write = origOut;
114
+ process.stderr.write = origErr;
115
+ },
116
+ };
117
+ }
118
+
119
+ /** Drop a two-turn Claude transcript for `cwd` under the mocked home. */
120
+ async function writeClaudeFixture(cwd: string) {
121
+ const dir = path.join(testHome, ".claude", "projects", encodeProjectSlug(cwd));
122
+ await mkdir(dir, { recursive: true });
123
+ const lines = [
124
+ JSON.stringify({
125
+ type: "user",
126
+ timestamp: "2026-08-06T09:00:00.000Z",
127
+ message: { role: "user", content: "fix the bug" },
128
+ }),
129
+ JSON.stringify({
130
+ type: "assistant",
131
+ timestamp: "2026-08-06T09:00:05.000Z",
132
+ message: { role: "assistant", content: [{ type: "text", text: "done, tests pass" }] },
133
+ }),
134
+ ];
135
+ await writeFile(path.join(dir, "ff1f38ee-78aa-48a6.jsonl"), lines.join("\n") + "\n");
136
+ }
137
+
138
+ it("exits 1 with guidance when the scoped cwd has no history", async () => {
139
+ const cap = capture();
140
+ try {
141
+ expect(await cmdHist(["--cwd", path.join(testHome, "nowhere")])).toBe(1);
142
+ expect(cap.err.join("")).toContain("no agent conversation history for");
143
+ expect(cap.err.join("")).toContain("ay hist --all");
144
+ } finally {
145
+ cap.restore();
146
+ }
147
+ });
148
+
149
+ it("exits 1 without the --all hint when already unscoped", async () => {
150
+ const cap = capture();
151
+ try {
152
+ expect(await cmdHist(["--all"])).toBe(1);
153
+ expect(cap.err.join("")).toContain("on this machine");
154
+ expect(cap.err.join("")).not.toContain("ay hist --all");
155
+ } finally {
156
+ cap.restore();
157
+ }
158
+ });
159
+
160
+ it("--json emits one JSONL record per turn (and exits 1 on empty)", async () => {
161
+ const cap = capture();
162
+ try {
163
+ expect(await cmdHist(["--all", "--json"])).toBe(1);
164
+ expect(cap.out.join("")).toBe("");
165
+ await writeClaudeFixture("/repo/demo");
166
+ expect(await cmdHist(["--all", "--json"])).toBe(0);
167
+ const records = cap.out
168
+ .join("")
169
+ .trim()
170
+ .split("\n")
171
+ .map((l) => JSON.parse(l));
172
+ expect(records.length).toBe(2);
173
+ expect(records.map((r) => r.role).sort()).toEqual(["assistant", "user"]);
174
+ } finally {
175
+ cap.restore();
176
+ }
177
+ });
178
+
179
+ it("renders turns and prints a pagination cursor when older turns remain", async () => {
180
+ await writeClaudeFixture("/repo/demo");
181
+ const cap = capture();
182
+ try {
183
+ expect(await cmdHist(["--cwd", "/repo/demo", "-n", "1"])).toBe(0);
184
+ expect(cap.out.join("")).toContain("done, tests pass");
185
+ expect(cap.err.join("")).toContain("older: ay hist --before ");
186
+ } finally {
187
+ cap.restore();
188
+ }
189
+ });
190
+
191
+ it("--source/--role/--full/--tools narrow and widen the page", async () => {
192
+ await writeClaudeFixture("/repo/demo");
193
+ const cap = capture();
194
+ try {
195
+ expect(
196
+ await cmdHist([
197
+ "--all",
198
+ "--source",
199
+ "claude",
200
+ "--role",
201
+ "user",
202
+ "--full",
203
+ "--tools",
204
+ "--sidechains",
205
+ "--json",
206
+ ]),
207
+ ).toBe(0);
208
+ const records = cap.out
209
+ .join("")
210
+ .trim()
211
+ .split("\n")
212
+ .map((l) => JSON.parse(l));
213
+ expect(records.every((r) => r.role === "user")).toBe(true);
214
+ expect(await cmdHist(["--all", "--source", "codex", "--json"])).toBe(1);
215
+ } finally {
216
+ cap.restore();
217
+ }
218
+ });
219
+ });
package/ts/hist.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * `ay hist` — CLI surface over {@link ./histStore.ts}.
3
+ *
4
+ * Reads past coding-agent conversations (Claude Code, Codex) that already exist
5
+ * on disk. Distinct from `ay tail`, which follows the live PTY log of a process
6
+ * agent-yes spawned; `ay hist` reads the CLI's own durable transcript, so it
7
+ * still works for sessions that already exited.
8
+ */
9
+
10
+ import yargs from "yargs";
11
+ import { histPage, type HistRecord, type HistSource } from "./histStore.ts";
12
+
13
+ const DEFAULT_LIMIT = 6;
14
+ const DEFAULT_SNIPPET = 600;
15
+
16
+ const DIM = "\x1b[2m";
17
+ const CYAN = "\x1b[36m";
18
+ const GREEN = "\x1b[32m";
19
+ const RESET = "\x1b[0m";
20
+
21
+ function shortTime(ts: string | null): string {
22
+ if (!ts) return "??:??";
23
+ const d = new Date(ts);
24
+ if (Number.isNaN(d.getTime())) return "??:??";
25
+ const now = Date.now();
26
+ const sameDay = new Date(now).toDateString() === d.toDateString();
27
+ const hhmm = d.toTimeString().slice(0, 5);
28
+ return sameDay ? hhmm : `${d.toISOString().slice(5, 10)} ${hhmm}`;
29
+ }
30
+
31
+ /** Collapse a turn to `max` chars, noting how much was withheld. */
32
+ export function snippet(text: string, max: number): string {
33
+ const trimmed = text.trim();
34
+ if (max <= 0 || trimmed.length <= max) return trimmed;
35
+ const head = trimmed.slice(0, max);
36
+ const hiddenLines = trimmed.slice(max).split("\n").length;
37
+ return `${head}… ${DIM}(+${trimmed.length - max} chars, ${hiddenLines} more lines)${RESET}`;
38
+ }
39
+
40
+ export function renderRecord(r: HistRecord, opts: { color: boolean; max: number }): string {
41
+ const who = r.role === "user" ? "user" : r.source;
42
+ const tint = opts.color ? (r.role === "user" ? CYAN : GREEN) : "";
43
+ const dim = opts.color ? DIM : "";
44
+ const off = opts.color ? RESET : "";
45
+ const body = snippet(r.text, opts.max)
46
+ .split("\n")
47
+ .map((l) => ` ${l}`)
48
+ .join("\n");
49
+ const head = `${tint}${who}${off} ${dim}${shortTime(r.ts)} ${r.sessionId.slice(0, 8)}${off}`;
50
+ return `${head}\n${body}\n`;
51
+ }
52
+
53
+ export async function cmdHist(rest: string[]): Promise<number> {
54
+ const y = yargs(rest)
55
+ .usage(
56
+ "Usage: ay hist [options]\n\n" +
57
+ "Tail past coding-agent conversations (Claude Code, Codex) from this machine.\n" +
58
+ "Defaults to sessions for the current directory, newest last.\n\n" +
59
+ "Pagination: rerun with --before <cursor> using the cursor printed at the end.",
60
+ )
61
+ .option("n", {
62
+ type: "number",
63
+ default: DEFAULT_LIMIT,
64
+ description: `Number of turns (default ${DEFAULT_LIMIT}; 0 = no cap)`,
65
+ })
66
+ .option("all", {
67
+ type: "boolean",
68
+ default: false,
69
+ description: "All projects, not just the current directory",
70
+ })
71
+ .option("cwd", { type: "string", description: "Scope to this directory instead of $PWD" })
72
+ .option("source", {
73
+ choices: ["claude", "codex"] as const,
74
+ description: "Only one agent's transcripts",
75
+ })
76
+ .option("role", {
77
+ choices: ["user", "assistant"] as const,
78
+ description: "Only my prompts, or only the agent's replies",
79
+ })
80
+ .option("before", {
81
+ type: "string",
82
+ description: "Cursor: only turns older than this ISO timestamp",
83
+ })
84
+ .option("tools", {
85
+ type: "boolean",
86
+ default: false,
87
+ description: "Include tool calls/results and thinking as markers",
88
+ })
89
+ .option("sidechains", {
90
+ type: "boolean",
91
+ default: false,
92
+ description: "Include Claude sub-agent turns",
93
+ })
94
+ .option("full", { type: "boolean", default: false, description: "Do not truncate turns" })
95
+ .option("json", { type: "boolean", default: false, description: "JSONL for scripts/agents" })
96
+ .help();
97
+
98
+ const argv = await y.parse();
99
+
100
+ const cwd = argv.all ? undefined : ((argv.cwd as string | undefined) ?? process.cwd());
101
+ const page = await histPage({
102
+ cwd,
103
+ limit: Number(argv.n),
104
+ before: argv.before as string | undefined,
105
+ role: argv.role as "user" | "assistant" | undefined,
106
+ sources: argv.source ? ([argv.source] as HistSource[]) : undefined,
107
+ includeTools: Boolean(argv.tools),
108
+ includeSidechains: Boolean(argv.sidechains),
109
+ });
110
+
111
+ if (argv.json) {
112
+ for (const r of page.records) process.stdout.write(JSON.stringify(r) + "\n");
113
+ return page.records.length ? 0 : 1;
114
+ }
115
+
116
+ if (!page.records.length) {
117
+ const where = cwd ? `for ${cwd}` : "on this machine";
118
+ process.stderr.write(
119
+ `no agent conversation history ${where} (scanned ${page.scanned} transcript${
120
+ page.scanned === 1 ? "" : "s"
121
+ }).\n` + (cwd ? `try: ay hist --all\n` : ""),
122
+ );
123
+ return 1;
124
+ }
125
+
126
+ const color = process.stdout.isTTY === true;
127
+ const max = argv.full ? 0 : DEFAULT_SNIPPET;
128
+ for (const r of page.records) process.stdout.write(renderRecord(r, { color, max }) + "\n");
129
+
130
+ if (page.cursor) {
131
+ const dim = color ? DIM : "";
132
+ const off = color ? RESET : "";
133
+ process.stderr.write(`${dim}older: ay hist --before ${page.cursor}${off}\n`);
134
+ }
135
+ return 0;
136
+ }