agent-yes 1.60.7 → 1.61.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.
@@ -904,7 +904,7 @@ function tryCatch(catchFn, fn) {
904
904
  //#endregion
905
905
  //#region package.json
906
906
  var name = "agent-yes";
907
- var version = "1.60.7";
907
+ var version = "1.61.0";
908
908
 
909
909
  //#endregion
910
910
  //#region ts/pty-fix.ts
@@ -1951,4 +1951,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
1951
1951
 
1952
1952
  //#endregion
1953
1953
  export { AgentContext as a, PidStore as c, config as i, removeControlCharacters as l, CLIS_CONFIG as n, name as o, agentYes as r, version as s, SUPPORTED_CLIS as t };
1954
- //# sourceMappingURL=SUPPORTED_CLIS-gNHv7RnK.js.map
1954
+ //# sourceMappingURL=SUPPORTED_CLIS-C0l1NfFH.js.map
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-gNHv7RnK.js";
2
+ import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C0l1NfFH.js";
3
3
  import { t as logger } from "./logger-CX77vJDA.js";
4
4
  import { argv } from "process";
5
5
  import { spawn } from "child_process";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as AgentContext, i as config, l as removeControlCharacters, n as CLIS_CONFIG, r as agentYes } from "./SUPPORTED_CLIS-gNHv7RnK.js";
1
+ import { a as AgentContext, i as config, l as removeControlCharacters, n as CLIS_CONFIG, r as agentYes } from "./SUPPORTED_CLIS-C0l1NfFH.js";
2
2
  import "./logger-CX77vJDA.js";
3
3
 
4
4
  export { AgentContext, CLIS_CONFIG, config, agentYes as default, removeControlCharacters };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.60.7",
3
+ "version": "1.61.0",
4
4
  "description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "ai",
@@ -80,7 +80,8 @@
80
80
  "prepare": "husky",
81
81
  "release": "standard-version && npm publish",
82
82
  "release:beta": "standard-version && npm publish --tag beta",
83
- "test": "bun test --coverage ts/"
83
+ "test": "vitest run",
84
+ "test:coverage": "vitest run --coverage"
84
85
  },
85
86
  "dependencies": {
86
87
  "@snomiao/bun-pty": "^0.3.4",
@@ -106,6 +107,7 @@
106
107
  "@types/proper-lockfile": "^4.1.4",
107
108
  "@types/yargs": "^17.0.35",
108
109
  "@typescript/native-preview": "^7.0.0-dev.20260124.1",
110
+ "@vitest/coverage-v8": "^4.1.0",
109
111
  "husky": "^9.1.7",
110
112
  "lint-staged": "^16.2.7",
111
113
  "node-pty": "^1.1.0",
@@ -116,7 +118,7 @@
116
118
  "semantic-release": "^25.0.2",
117
119
  "standard-version": "^9.5.0",
118
120
  "tsdown": "^0.20.3",
119
- "vitest": "^4.0.17",
121
+ "vitest": "4.1.0",
120
122
  "zod": "^3.23.0"
121
123
  },
122
124
  "peerDependencies": {
@@ -0,0 +1,195 @@
1
+ import { describe, expect, it, beforeEach, afterEach } from "vitest";
2
+ import { JsonlStore } from "./JsonlStore";
3
+ import { rm, readFile, writeFile, mkdir } from "fs/promises";
4
+ import path from "path";
5
+
6
+ const TEST_DIR = "/tmp/jsonlstore-test-" + process.pid;
7
+ const TEST_FILE = path.join(TEST_DIR, "test.jsonl");
8
+
9
+ describe("JsonlStore", () => {
10
+ let store: JsonlStore;
11
+
12
+ beforeEach(async () => {
13
+ await rm(TEST_DIR, { recursive: true, force: true });
14
+ await mkdir(TEST_DIR, { recursive: true });
15
+ store = new JsonlStore(TEST_FILE);
16
+ });
17
+
18
+ afterEach(async () => {
19
+ await rm(TEST_DIR, { recursive: true, force: true });
20
+ });
21
+
22
+ describe("load", () => {
23
+ it("should return empty map for new file", async () => {
24
+ const docs = await store.load();
25
+ expect(docs.size).toBe(0);
26
+ });
27
+
28
+ it("should load existing JSONL data", async () => {
29
+ await writeFile(TEST_FILE, '{"_id":"1","name":"Alice"}\n{"_id":"2","name":"Bob"}\n');
30
+ const docs = await store.load();
31
+ expect(docs.size).toBe(2);
32
+ expect(docs.get("1")).toEqual({ _id: "1", name: "Alice" });
33
+ });
34
+
35
+ it("should merge duplicate IDs (last wins)", async () => {
36
+ await writeFile(
37
+ TEST_FILE,
38
+ '{"_id":"1","name":"Alice","age":30}\n{"_id":"1","name":"Alice Updated"}\n',
39
+ );
40
+ const docs = await store.load();
41
+ expect(docs.size).toBe(1);
42
+ expect(docs.get("1")).toEqual({ _id: "1", name: "Alice Updated", age: 30 });
43
+ });
44
+
45
+ it("should handle $$deleted tombstones", async () => {
46
+ await writeFile(TEST_FILE, '{"_id":"1","name":"Alice"}\n{"_id":"1","$$deleted":true}\n');
47
+ const docs = await store.load();
48
+ expect(docs.size).toBe(0);
49
+ });
50
+
51
+ it("should skip lines without _id", async () => {
52
+ await writeFile(TEST_FILE, '{"name":"no id"}\n{"_id":"1","name":"valid"}\n');
53
+ const docs = await store.load();
54
+ expect(docs.size).toBe(1);
55
+ });
56
+
57
+ it("should skip corrupt lines gracefully", async () => {
58
+ await writeFile(
59
+ TEST_FILE,
60
+ '{"_id":"1","name":"ok"}\n{corrupt json\n{"_id":"2","name":"also ok"}\n',
61
+ );
62
+ const docs = await store.load();
63
+ expect(docs.size).toBe(2);
64
+ });
65
+
66
+ it("should recover from temp file when main file missing", async () => {
67
+ const tempFile = TEST_FILE + "~";
68
+ await writeFile(tempFile, '{"_id":"1","name":"recovered"}\n');
69
+ const docs = await store.load();
70
+ expect(docs.size).toBe(1);
71
+ expect(docs.get("1")!.name).toBe("recovered");
72
+ });
73
+ });
74
+
75
+ describe("getAll / getById / find / findOne", () => {
76
+ beforeEach(async () => {
77
+ await writeFile(
78
+ TEST_FILE,
79
+ '{"_id":"1","name":"Alice","age":30}\n{"_id":"2","name":"Bob","age":25}\n{"_id":"3","name":"Charlie","age":35}\n',
80
+ );
81
+ await store.load();
82
+ });
83
+
84
+ it("getAll returns all documents", () => {
85
+ expect(store.getAll()).toHaveLength(3);
86
+ });
87
+
88
+ it("getById returns correct doc", () => {
89
+ expect(store.getById("2")).toEqual({ _id: "2", name: "Bob", age: 25 });
90
+ });
91
+
92
+ it("getById returns undefined for missing id", () => {
93
+ expect(store.getById("999")).toBeUndefined();
94
+ });
95
+
96
+ it("find returns matching docs", () => {
97
+ const result = store.find((d) => d.age > 28);
98
+ expect(result).toHaveLength(2);
99
+ });
100
+
101
+ it("findOne returns first match", () => {
102
+ const result = store.findOne((d) => d.age > 28);
103
+ expect(result).toBeDefined();
104
+ expect(result!.age).toBeGreaterThan(28);
105
+ });
106
+
107
+ it("findOne returns undefined when no match", () => {
108
+ expect(store.findOne((d) => d.age > 100)).toBeUndefined();
109
+ });
110
+ });
111
+
112
+ describe("append", () => {
113
+ it("should append and return doc with generated id", async () => {
114
+ await store.load();
115
+ const doc = await store.append({ name: "test" } as any);
116
+ expect(doc._id).toBeDefined();
117
+ expect(doc.name).toBe("test");
118
+ expect(store.getAll()).toHaveLength(1);
119
+ });
120
+
121
+ it("should append with provided _id", async () => {
122
+ await store.load();
123
+ const doc = await store.append({ _id: "custom-id", name: "test" } as any);
124
+ expect(doc._id).toBe("custom-id");
125
+ });
126
+
127
+ it("should merge with existing doc of same _id", async () => {
128
+ await store.load();
129
+ await store.append({ _id: "x", name: "first", value: 1 } as any);
130
+ await store.append({ _id: "x", name: "second" } as any);
131
+ const doc = store.getById("x")!;
132
+ expect(doc.name).toBe("second");
133
+ expect(doc.value).toBe(1);
134
+ });
135
+ });
136
+
137
+ describe("updateById", () => {
138
+ it("should update existing doc", async () => {
139
+ await store.load();
140
+ await store.append({ _id: "u1", name: "original", status: "active" } as any);
141
+ await store.updateById("u1", { status: "done" });
142
+ expect(store.getById("u1")!.status).toBe("done");
143
+ expect(store.getById("u1")!.name).toBe("original");
144
+ });
145
+
146
+ it("should no-op for non-existent id", async () => {
147
+ await store.load();
148
+ await store.updateById("nonexistent", { status: "done" });
149
+ expect(store.getAll()).toHaveLength(0);
150
+ });
151
+ });
152
+
153
+ describe("deleteById", () => {
154
+ it("should remove doc from memory and write tombstone", async () => {
155
+ await store.load();
156
+ await store.append({ _id: "d1", name: "to delete" } as any);
157
+ expect(store.getAll()).toHaveLength(1);
158
+
159
+ await store.deleteById("d1");
160
+ expect(store.getAll()).toHaveLength(0);
161
+ expect(store.getById("d1")).toBeUndefined();
162
+
163
+ // Tombstone should be written to file
164
+ const content = await readFile(TEST_FILE, "utf-8");
165
+ expect(content).toContain('"$$deleted":true');
166
+ });
167
+ });
168
+
169
+ describe("compact", () => {
170
+ it("should deduplicate entries", async () => {
171
+ await store.load();
172
+ await store.append({ _id: "c1", name: "v1" } as any);
173
+ await store.updateById("c1", { name: "v2" });
174
+ await store.updateById("c1", { name: "v3" });
175
+
176
+ // Before compact: 3 lines
177
+ const before = (await readFile(TEST_FILE, "utf-8")).trim().split("\n");
178
+ expect(before).toHaveLength(3);
179
+
180
+ await store.compact();
181
+
182
+ // After compact: 1 line
183
+ const after = (await readFile(TEST_FILE, "utf-8")).trim().split("\n");
184
+ expect(after).toHaveLength(1);
185
+ expect(JSON.parse(after[0]!).name).toBe("v3");
186
+ });
187
+
188
+ it("should handle empty store", async () => {
189
+ await store.load();
190
+ await store.compact();
191
+ const content = await readFile(TEST_FILE, "utf-8");
192
+ expect(content).toBe("");
193
+ });
194
+ });
195
+ });
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { loadCascadingConfig, getConfigPaths, ensureSchemaInConfigFiles } from "./configLoader.ts";
3
3
  import { mkdir, writeFile, readFile, rm } from "node:fs/promises";
4
4
  import path from "node:path";
@@ -187,4 +187,68 @@ configDir: /test
187
187
  expect(result.skipped).toContain(configPath);
188
188
  expect(result.modified).not.toContain(configPath);
189
189
  });
190
+
191
+ it("should handle invalid JSON config gracefully", async () => {
192
+ const configPath = path.join(testDir, ".agent-yes.config.json");
193
+ await writeFile(configPath, "not valid json {{{");
194
+
195
+ const config = await loadCascadingConfig({ projectDir: testDir, homeDir: testDir });
196
+ // Should not throw, returns empty
197
+ expect(config).toEqual({});
198
+ });
199
+
200
+ it("should handle empty YAML config returning null", async () => {
201
+ const configPath = path.join(testDir, ".agent-yes.config.yaml");
202
+ await writeFile(configPath, "");
203
+
204
+ const config = await loadCascadingConfig({ projectDir: testDir, homeDir: testDir });
205
+ expect(config).toEqual({});
206
+ });
207
+
208
+ it("should add schema to YAML with leading comments", async () => {
209
+ const configPath = path.join(testDir, ".agent-yes.config.yaml");
210
+ await writeFile(
211
+ configPath,
212
+ `# My config comment
213
+ # Another comment
214
+ configDir: /test
215
+ `,
216
+ );
217
+
218
+ const result = await ensureSchemaInConfigFiles({ projectDir: testDir, homeDir: testDir });
219
+ expect(result.modified).toContain(configPath);
220
+ const content = await readFile(configPath, "utf-8");
221
+ expect(content).toContain("yaml-language-server:");
222
+ });
223
+
224
+ it("should handle ensureSchema when no config files exist", async () => {
225
+ const emptyDir = path.join(testDir, "empty");
226
+ await mkdir(emptyDir, { recursive: true });
227
+
228
+ const result = await ensureSchemaInConfigFiles({ projectDir: emptyDir, homeDir: emptyDir });
229
+ expect(result.modified).toHaveLength(0);
230
+ expect(result.skipped).toHaveLength(0);
231
+ });
232
+
233
+ it("should handle addSchemaReference for JSON that already has schema", async () => {
234
+ const configPath = path.join(testDir, ".agent-yes.config.json");
235
+ const content = JSON.stringify({ $schema: "existing", configDir: "/test" }, null, 2);
236
+ await writeFile(configPath, content);
237
+
238
+ const result = await ensureSchemaInConfigFiles({ projectDir: testDir, homeDir: testDir });
239
+ expect(result.skipped).toContain(configPath);
240
+ });
241
+
242
+ it("should use defaults for getConfigPaths without options", () => {
243
+ const paths = getConfigPaths();
244
+ expect(paths.length).toBeGreaterThan(0);
245
+ expect(paths.some((p) => p.includes(".agent-yes.config.json"))).toBe(true);
246
+ });
247
+
248
+ it("should use defaults for loadCascadingConfig without options", async () => {
249
+ // This tests the default path (process.cwd() and os.homedir())
250
+ const config = await loadCascadingConfig();
251
+ // Should not throw, may or may not find configs
252
+ expect(config).toBeDefined();
253
+ });
190
254
  });
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { defineCliYesConfig } from "./defineConfig";
3
+
4
+ describe("defineCliYesConfig", () => {
5
+ it("should return a plain config object", async () => {
6
+ const cfg = await defineCliYesConfig({ clis: {} });
7
+ expect(cfg).toEqual({ clis: {} });
8
+ });
9
+
10
+ it("should accept a function that receives the default config", async () => {
11
+ const cfg = await defineCliYesConfig((original) => {
12
+ expect(original).toEqual({ clis: {} });
13
+ return { ...original, clis: { claude: { bin: "claude" } } };
14
+ });
15
+ expect(cfg.clis).toHaveProperty("claude");
16
+ });
17
+
18
+ it("should accept an async function", async () => {
19
+ const cfg = await defineCliYesConfig(async () => {
20
+ return { clis: { test: { bin: "test-cli" } } };
21
+ });
22
+ expect(cfg.clis.test).toEqual({ bin: "test-cli" });
23
+ });
24
+
25
+ it("should accept a promise", async () => {
26
+ const cfg = await defineCliYesConfig(Promise.resolve({ clis: {} }));
27
+ expect(cfg).toEqual({ clis: {} });
28
+ });
29
+ });
@@ -52,4 +52,20 @@ describe("IdleWaiter", () => {
52
52
  const result = waiter.ping().ping().ping();
53
53
  expect(result).toBe(waiter);
54
54
  });
55
+
56
+ it("should wait until idle period has passed", async () => {
57
+ const waiter = new IdleWaiter();
58
+ waiter.checkInterval = 10;
59
+
60
+ // Ping right now so it's not idle
61
+ waiter.ping();
62
+
63
+ const start = Date.now();
64
+ // Wait for 50ms idle period — the wait loop should actually iterate
65
+ await waiter.wait(50);
66
+ const elapsed = Date.now() - start;
67
+
68
+ // Should have waited at least ~50ms
69
+ expect(elapsed).toBeGreaterThanOrEqual(40);
70
+ });
55
71
  });
@@ -115,6 +115,20 @@ describe("PidStore", () => {
115
115
  expect(rec?.exitReason).toBe("crash");
116
116
  expect(rec?.exitCode).toBe(1);
117
117
  });
118
+
119
+ it("should no-op when updating non-existent pid", async () => {
120
+ await store.updateStatus(999888, "exited");
121
+ // Should not throw and records should be unchanged
122
+ expect(store.getAllRecords()).toHaveLength(0);
123
+ });
124
+
125
+ it("should update status without extra params", async () => {
126
+ await store.registerProcess({ pid: 333, cli: "test", args: [], cwd: "/tmp" });
127
+ await store.updateStatus(333, "idle");
128
+ const rec = store.getAllRecords().find((r) => r.pid === 333);
129
+ expect(rec?.status).toBe("idle");
130
+ expect(rec?.exitReason).toBe("");
131
+ });
118
132
  });
119
133
 
120
134
  describe("getAllRecords", () => {
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "bun:test";
1
+ import { describe, expect, it } from "vitest";
2
2
  import { extractSessionId, extractSessionIdFromSessionMeta } from "./resume/codexSessionManager";
3
3
 
4
4
  describe("Session Extraction Test", () => {
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
+ import { compareVersions, fetchLatestVersion, displayVersion } from "./versionChecker";
3
+
4
+ describe("versionChecker", () => {
5
+ describe("compareVersions", () => {
6
+ it("should return 0 for equal versions", () => {
7
+ expect(compareVersions("1.0.0", "1.0.0")).toBe(0);
8
+ expect(compareVersions("2.3.4", "2.3.4")).toBe(0);
9
+ });
10
+
11
+ it("should return 1 when v1 > v2", () => {
12
+ expect(compareVersions("2.0.0", "1.0.0")).toBe(1);
13
+ expect(compareVersions("1.1.0", "1.0.0")).toBe(1);
14
+ expect(compareVersions("1.0.1", "1.0.0")).toBe(1);
15
+ });
16
+
17
+ it("should return -1 when v1 < v2", () => {
18
+ expect(compareVersions("1.0.0", "2.0.0")).toBe(-1);
19
+ expect(compareVersions("1.0.0", "1.1.0")).toBe(-1);
20
+ expect(compareVersions("1.0.0", "1.0.1")).toBe(-1);
21
+ });
22
+
23
+ it("should handle versions with different segment counts", () => {
24
+ expect(compareVersions("1.0", "1.0.0")).toBe(0);
25
+ expect(compareVersions("1.0.0", "1.0")).toBe(0);
26
+ expect(compareVersions("1.0.1", "1.0")).toBe(1);
27
+ expect(compareVersions("1.0", "1.0.1")).toBe(-1);
28
+ });
29
+ });
30
+
31
+ describe("fetchLatestVersion", () => {
32
+ beforeEach(() => {
33
+ vi.stubGlobal("fetch", vi.fn());
34
+ });
35
+
36
+ afterEach(() => {
37
+ vi.restoreAllMocks();
38
+ });
39
+
40
+ it("should return version from npm registry", async () => {
41
+ vi.mocked(fetch).mockResolvedValue({
42
+ ok: true,
43
+ json: async () => ({ version: "1.2.3" }),
44
+ } as Response);
45
+
46
+ const version = await fetchLatestVersion();
47
+ expect(version).toBe("1.2.3");
48
+ });
49
+
50
+ it("should return null on non-ok response", async () => {
51
+ vi.mocked(fetch).mockResolvedValue({
52
+ ok: false,
53
+ } as Response);
54
+
55
+ const version = await fetchLatestVersion();
56
+ expect(version).toBeNull();
57
+ });
58
+
59
+ it("should return null on network error", async () => {
60
+ vi.mocked(fetch).mockRejectedValue(new Error("network error"));
61
+
62
+ const version = await fetchLatestVersion();
63
+ expect(version).toBeNull();
64
+ });
65
+ });
66
+
67
+ describe("displayVersion", () => {
68
+ beforeEach(() => {
69
+ vi.stubGlobal("fetch", vi.fn());
70
+ vi.spyOn(console, "log").mockImplementation(() => {});
71
+ });
72
+
73
+ afterEach(() => {
74
+ vi.restoreAllMocks();
75
+ });
76
+
77
+ it("should log update available when behind", async () => {
78
+ vi.mocked(fetch).mockResolvedValue({
79
+ ok: true,
80
+ json: async () => ({ version: "999.0.0" }),
81
+ } as Response);
82
+
83
+ await displayVersion();
84
+
85
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining("update available"));
86
+ });
87
+
88
+ it("should log latest when versions match", async () => {
89
+ const pkg = await import("../package.json");
90
+ vi.mocked(fetch).mockResolvedValue({
91
+ ok: true,
92
+ json: async () => ({ version: pkg.default.version }),
93
+ } as Response);
94
+
95
+ await displayVersion();
96
+
97
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining("latest"));
98
+ });
99
+
100
+ it("should log latest published when ahead", async () => {
101
+ vi.mocked(fetch).mockResolvedValue({
102
+ ok: true,
103
+ json: async () => ({ version: "0.0.1" }),
104
+ } as Response);
105
+
106
+ await displayVersion();
107
+
108
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining("latest published"));
109
+ });
110
+
111
+ it("should handle fetch failure gracefully", async () => {
112
+ vi.mocked(fetch).mockRejectedValue(new Error("fail"));
113
+
114
+ await displayVersion();
115
+
116
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining("unable to check"));
117
+ });
118
+ });
119
+ });