agent-yes 1.72.4 → 1.73.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.72.4",
3
+ "version": "1.73.2",
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",
@@ -49,6 +49,7 @@
49
49
  "files": [
50
50
  "agent-yes.config.schema.json",
51
51
  "bin",
52
+ "default.config.yaml",
52
53
  "examples",
53
54
  "scripts",
54
55
  "ts/*.ts",
@@ -81,10 +82,12 @@
81
82
  "release": "standard-version && npm publish",
82
83
  "release:beta": "standard-version && npm publish --tag beta",
83
84
  "test": "vitest run",
84
- "test:coverage": "vitest run --coverage"
85
+ "test:coverage": "vitest run --coverage",
86
+ "test:ui": "vitest run --config tests/ui-test/vitest.config.ts"
85
87
  },
86
88
  "dependencies": {
87
89
  "@snomiao/bun-pty": "^0.3.4",
90
+ "@xterm/headless": "^6.0.0",
88
91
  "bun-pty": "^0.4.8",
89
92
  "execa": "^9.6.1",
90
93
  "from-node-stream": "^0.2.0",
@@ -92,12 +95,12 @@
92
95
  "phpdie": "^1.7.0",
93
96
  "proper-lockfile": "^4.1.2",
94
97
  "sflow": "^1.27.0",
95
- "terminal-render": "^1.5.1",
96
98
  "winston": "^3.19.0",
97
99
  "yaml": "^2.8.2",
98
100
  "yargs": "^18.0.0"
99
101
  },
100
102
  "devDependencies": {
103
+ "@google/generative-ai": "^0.24.1",
101
104
  "@semantic-release/exec": "^7.1.0",
102
105
  "@semantic-release/git": "^10.0.1",
103
106
  "@types/bun": "^1.3.6",
@@ -105,6 +108,7 @@
105
108
  "@types/ms": "^2.1.0",
106
109
  "@types/node": "^25.0.10",
107
110
  "@types/proper-lockfile": "^4.1.4",
111
+ "@types/ws": "^8.18.1",
108
112
  "@types/yargs": "^17.0.35",
109
113
  "@typescript/native-preview": "^7.0.0-dev.20260124.1",
110
114
  "@vitest/coverage-v8": "4.1.0",
@@ -114,11 +118,13 @@
114
118
  "oxfmt": "^0.26.0",
115
119
  "oxlint": "^1.41.0",
116
120
  "patch-package": "^8.0.1",
121
+ "playwright": "^1.58.2",
117
122
  "rambda": "^11.0.1",
118
123
  "semantic-release": "^25.0.2",
119
124
  "standard-version": "^9.5.0",
120
125
  "tsdown": "^0.20.3",
121
126
  "vitest": "4.1.0",
127
+ "ws": "^8.20.0",
122
128
  "zod": "^3.23.0"
123
129
  },
124
130
  "peerDependencies": {
package/ts/cli.ts CHANGED
@@ -2,18 +2,27 @@
2
2
  import { argv } from "process";
3
3
  import { spawn } from "child_process";
4
4
  import { parseCliArgs } from "./parseCliArgs.ts";
5
- import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
6
5
  import { logger } from "./logger.ts";
7
- import { PidStore } from "./pidStore.ts";
8
- import { checkAndAutoUpdate, displayVersion } from "./versionChecker.ts";
6
+ import { checkAndAutoUpdate, displayVersion, versionString } from "./versionChecker.ts";
9
7
  import { getRustBinary } from "./rustBinary.ts";
10
8
  import { buildRustArgs } from "./buildRustArgs.ts";
11
9
 
10
+ // Ultra-fast path: skip heavy init for --version/-v
11
+ {
12
+ const rawArgs = process.argv.slice(2);
13
+ if (rawArgs[0] === "-v" || rawArgs.includes("--version")) {
14
+ console.log(versionString());
15
+ process.exit(0);
16
+ }
17
+ }
18
+
12
19
  // Check for updates before starting — installs & re-execs if a newer version exists.
13
20
  // Fast path: cached result (no network), so this adds near-zero latency most of the time.
14
21
  await checkAndAutoUpdate();
15
22
 
16
- // Parse CLI arguments
23
+ logger.info(versionString());
24
+
25
+ // Parse CLI arguments (no choices validation — SUPPORTED_CLIS loaded lazily below)
17
26
  const config = parseCliArgs(process.argv);
18
27
 
19
28
  // Handle --tray: show system tray icon and block
@@ -45,6 +54,7 @@ if (config.useRust) {
45
54
  }
46
55
 
47
56
  if (rustBinary) {
57
+ const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS.ts");
48
58
  const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
49
59
 
50
60
  if (config.verbose) {
@@ -81,7 +91,7 @@ if (config.useRust) {
81
91
  }
82
92
  }
83
93
 
84
- // Handle --version: display version and exit
94
+ // Handle --version: display version and exit (also reached when --version comes after other flags)
85
95
  if (config.showVersion) {
86
96
  await displayVersion();
87
97
  process.exit(0);
@@ -89,6 +99,7 @@ if (config.showVersion) {
89
99
 
90
100
  // Handle --append-prompt: write to active IPC (FIFO/Named Pipe) and exit
91
101
  if (config.appendPrompt) {
102
+ const { PidStore } = await import("./pidStore.ts");
92
103
  const ipcPath = await PidStore.findActiveFifo(process.cwd());
93
104
  if (!ipcPath) {
94
105
  console.error("No active agent with IPC found in current directory.");
@@ -134,12 +145,8 @@ if (config.appendPrompt) {
134
145
 
135
146
  // Validate CLI name
136
147
  if (!config.cli) {
137
- // logger.error(process.argv);
138
148
  config.cli = "claude"; // default to claude, for smooth UX
139
149
  logger.warn("Warning: No CLI name provided. Using default 'claude'.");
140
- // throw new Error(
141
- // `missing cli def, available clis: ${Object.keys((await cliYesConfig).clis).join(", ")}`,
142
- // );
143
150
  }
144
151
 
145
152
  // console.log(`Using CLI: ${config.cli}`);
@@ -52,6 +52,25 @@ clis:
52
52
  expect(config.clis?.gemini?.defaultArgs).toEqual(["--resume"]);
53
53
  });
54
54
 
55
+ it("should compile regex sources from YAML config", async () => {
56
+ const configPath = path.join(testDir, ".agent-yes.config.yaml");
57
+ await writeFile(
58
+ configPath,
59
+ `
60
+ clis:
61
+ codex:
62
+ ready:
63
+ - pattern: '^› '
64
+ flags: m
65
+ `,
66
+ );
67
+
68
+ const config = await loadCascadingConfig({ projectDir: testDir, homeDir: testDir });
69
+ expect(config.clis?.codex?.ready?.[0]).toBeInstanceOf(RegExp);
70
+ expect(config.clis?.codex?.ready?.[0]?.flags).toContain("m");
71
+ expect(config.clis?.codex?.ready?.[0]?.test("› ")).toBe(true);
72
+ });
73
+
55
74
  it("should load YML config", async () => {
56
75
  const configPath = path.join(testDir, ".agent-yes.config.yml");
57
76
  await writeFile(
@@ -8,6 +8,7 @@ import os from "node:os";
8
8
  import { parse as parseYaml } from "yaml";
9
9
  import { logger } from "./logger.ts";
10
10
  import type { AgentYesConfig } from "./index.ts";
11
+ import { normalizeAgentYesConfig } from "./configShared.ts";
11
12
  import { deepMixin } from "./utils.ts";
12
13
 
13
14
  const CONFIG_FILENAME = ".agent-yes.config";
@@ -34,16 +35,21 @@ async function fileExists(filepath: string): Promise<boolean> {
34
35
  async function parseConfigFile(filepath: string): Promise<Partial<AgentYesConfig>> {
35
36
  const content = await readFile(filepath, "utf-8");
36
37
  const ext = path.extname(filepath).toLowerCase();
38
+ let parsed: Partial<AgentYesConfig>;
37
39
 
38
40
  switch (ext) {
39
41
  case ".json":
40
- return JSON.parse(content);
42
+ parsed = JSON.parse(content);
43
+ break;
41
44
  case ".yml":
42
45
  case ".yaml":
43
- return parseYaml(content) ?? {};
46
+ parsed = parseYaml(content) ?? {};
47
+ break;
44
48
  default:
45
49
  throw new Error(`Unsupported config file extension: ${ext}`);
46
50
  }
51
+
52
+ return normalizeAgentYesConfig(parsed as Partial<AgentYesConfig>);
47
53
  }
48
54
 
49
55
  /**
@@ -0,0 +1,97 @@
1
+ import { mkdir, rm, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import {
7
+ compileRegexSource,
8
+ findSharedCliDefaultsPath,
9
+ isRegexSource,
10
+ loadSharedCliDefaults,
11
+ normalizeAgentYesConfig,
12
+ normalizeCliConfig,
13
+ } from "./configShared.ts";
14
+
15
+ describe("configShared", () => {
16
+ const tempRoots: string[] = [];
17
+
18
+ afterEach(async () => {
19
+ await Promise.all(tempRoots.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
20
+ });
21
+
22
+ it("compiles structured regex sources with flags", () => {
23
+ const regex = compileRegexSource({ pattern: "^foo$", flags: "m" });
24
+ expect(regex).toBeInstanceOf(RegExp);
25
+ expect(regex.flags).toContain("m");
26
+ });
27
+
28
+ it("returns RegExp instances unchanged", () => {
29
+ const regex = /foo/;
30
+ expect(compileRegexSource(regex)).toBe(regex);
31
+ });
32
+
33
+ it("normalizes legacy exitCommand to exitCommands", () => {
34
+ const config = normalizeCliConfig({ exitCommand: ["/quit"] });
35
+ expect(config.exitCommands).toEqual(["/quit"]);
36
+ });
37
+
38
+ it("normalizes configDir, logsDir, and regex arrays", () => {
39
+ const config = normalizeAgentYesConfig({
40
+ configDir: "/cfg",
41
+ logsDir: "/logs",
42
+ clis: {
43
+ claude: {
44
+ ready: [{ pattern: "^ready$", flags: "m" }],
45
+ typingRespond: {
46
+ "1\n": ["^confirm$"],
47
+ },
48
+ },
49
+ },
50
+ });
51
+
52
+ expect(config.configDir).toBe("/cfg");
53
+ expect(config.logsDir).toBe("/logs");
54
+ expect(config.clis?.claude.ready?.[0]).toBeInstanceOf(RegExp);
55
+ expect(config.clis?.claude.typingRespond?.["1\n"]?.[0]).toBeInstanceOf(RegExp);
56
+ });
57
+
58
+ it("loads shared YAML defaults for codex", async () => {
59
+ const clis = await loadSharedCliDefaults(import.meta.url);
60
+ expect(clis.codex).toBeDefined();
61
+ expect(clis.codex.ready?.some((regex) => regex.test("› "))).toBe(true);
62
+ expect(clis.codex.ready?.some((regex) => regex.test("⏎ send"))).toBe(true);
63
+ });
64
+
65
+ it("finds the shared defaults file by walking upward", async () => {
66
+ const found = await findSharedCliDefaultsPath(import.meta.url);
67
+ expect(found.endsWith("default.config.yaml")).toBe(true);
68
+ });
69
+
70
+ it("throws when no shared defaults file exists in parent directories", async () => {
71
+ const tempDir = path.join(os.tmpdir(), `agent-yes-config-shared-${Date.now()}-missing`);
72
+ tempRoots.push(tempDir);
73
+ await mkdir(tempDir, { recursive: true });
74
+
75
+ await expect(
76
+ findSharedCliDefaultsPath(pathToFileURL(path.join(tempDir, "entry.js")).href),
77
+ ).rejects.toThrow("Unable to locate");
78
+ });
79
+
80
+ it("throws when the located shared defaults file is not an object", async () => {
81
+ const tempDir = path.join(os.tmpdir(), `agent-yes-config-shared-${Date.now()}-invalid`);
82
+ tempRoots.push(tempDir);
83
+ await mkdir(tempDir, { recursive: true });
84
+ await writeFile(path.join(tempDir, "default.config.yaml"), "123\n");
85
+
86
+ await expect(
87
+ loadSharedCliDefaults(pathToFileURL(path.join(tempDir, "entry.js")).href),
88
+ ).rejects.toThrow("Invalid shared CLI defaults file");
89
+ });
90
+
91
+ it("recognizes valid and invalid regex source shapes", () => {
92
+ expect(isRegexSource("^ready$")).toBe(true);
93
+ expect(isRegexSource({ pattern: "^ready$", flags: "m" })).toBe(true);
94
+ expect(isRegexSource({ pattern: 123 })).toBe(false);
95
+ expect(isRegexSource({ pattern: "^ready$", flags: 1 })).toBe(false);
96
+ });
97
+ });
@@ -0,0 +1,158 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parse as parseYaml } from "yaml";
5
+ import type { AgentCliConfig, AgentYesConfig } from "./index.ts";
6
+
7
+ export type RegexSource = string | { pattern: string; flags?: string };
8
+
9
+ type RawCliConfig = Omit<
10
+ AgentCliConfig,
11
+ | "ready"
12
+ | "fatal"
13
+ | "working"
14
+ | "enter"
15
+ | "enterExclude"
16
+ | "typingRespond"
17
+ | "restartWithoutContinueArg"
18
+ | "updateAvailable"
19
+ | "exitCommands"
20
+ > & {
21
+ ready?: RegexSource[];
22
+ fatal?: RegexSource[];
23
+ working?: RegexSource[];
24
+ enter?: RegexSource[];
25
+ enterExclude?: RegexSource[];
26
+ typingRespond?: Record<string, RegexSource[]>;
27
+ restartWithoutContinueArg?: RegexSource[];
28
+ updateAvailable?: RegexSource[];
29
+ exitCommands?: string[];
30
+ exitCommand?: string[];
31
+ };
32
+
33
+ type RawAgentYesConfig = {
34
+ configDir?: string;
35
+ logsDir?: string;
36
+ clis?: Record<string, RawCliConfig>;
37
+ };
38
+
39
+ function isRegexSourceObject(value: unknown): value is { pattern: string; flags?: string } {
40
+ return (
41
+ !!value &&
42
+ typeof value === "object" &&
43
+ "pattern" in value &&
44
+ typeof (value as { pattern?: unknown }).pattern === "string" &&
45
+ (!("flags" in value) || typeof (value as { flags?: unknown }).flags === "string")
46
+ );
47
+ }
48
+
49
+ export function compileRegexSource(source: RegexSource | RegExp): RegExp {
50
+ if (source instanceof RegExp) return source;
51
+ if (typeof source === "string") return new RegExp(source);
52
+ return new RegExp(source.pattern, source.flags ?? "");
53
+ }
54
+
55
+ function compileRegexList(sources?: (RegexSource | RegExp)[]): RegExp[] | undefined {
56
+ return sources?.map((source) => compileRegexSource(source));
57
+ }
58
+
59
+ function compileTypingRespond(
60
+ typingRespond?: Record<string, RegexSource[]>,
61
+ ): Record<string, RegExp[]> | undefined {
62
+ if (!typingRespond) return undefined;
63
+ return Object.fromEntries(
64
+ Object.entries(typingRespond).map(([message, patterns]) => [
65
+ message,
66
+ patterns.map(compileRegexSource),
67
+ ]),
68
+ );
69
+ }
70
+
71
+ export function normalizeCliConfig(raw: RawCliConfig): AgentCliConfig {
72
+ const {
73
+ ready,
74
+ fatal,
75
+ working,
76
+ enter,
77
+ enterExclude,
78
+ typingRespond,
79
+ restartWithoutContinueArg,
80
+ updateAvailable,
81
+ exitCommands,
82
+ exitCommand,
83
+ ...rest
84
+ } = raw;
85
+
86
+ return {
87
+ ...rest,
88
+ ready: compileRegexList(ready),
89
+ fatal: compileRegexList(fatal),
90
+ working: compileRegexList(working),
91
+ enter: compileRegexList(enter),
92
+ enterExclude: compileRegexList(enterExclude),
93
+ typingRespond: compileTypingRespond(typingRespond),
94
+ restartWithoutContinueArg: compileRegexList(restartWithoutContinueArg),
95
+ updateAvailable: compileRegexList(updateAvailable),
96
+ exitCommands: exitCommands ?? exitCommand,
97
+ };
98
+ }
99
+
100
+ export function normalizeAgentYesConfig(raw: RawAgentYesConfig): Partial<AgentYesConfig> {
101
+ const normalized: Partial<AgentYesConfig> = {};
102
+
103
+ if (raw.configDir !== undefined) normalized.configDir = raw.configDir;
104
+ if (raw.logsDir !== undefined) normalized.logsDir = raw.logsDir;
105
+
106
+ if (raw.clis) {
107
+ normalized.clis = Object.fromEntries(
108
+ Object.entries(raw.clis).map(([name, cliConfig]) => [name, normalizeCliConfig(cliConfig)]),
109
+ );
110
+ }
111
+
112
+ return normalized;
113
+ }
114
+
115
+ async function fileExists(filepath: string) {
116
+ try {
117
+ await access(filepath);
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ export async function findSharedCliDefaultsPath(
125
+ fromUrl: string = import.meta.url,
126
+ ): Promise<string> {
127
+ let currentDir = path.dirname(fileURLToPath(fromUrl));
128
+
129
+ while (true) {
130
+ const candidate = path.resolve(currentDir, "default.config.yaml");
131
+ if (await fileExists(candidate)) return candidate;
132
+
133
+ const parent = path.dirname(currentDir);
134
+ if (parent === currentDir) break;
135
+ currentDir = parent;
136
+ }
137
+
138
+ throw new Error("Unable to locate default.config.yaml from current package path");
139
+ }
140
+
141
+ export async function loadSharedCliDefaults(
142
+ fromUrl: string = import.meta.url,
143
+ ): Promise<Record<string, AgentCliConfig>> {
144
+ const filepath = await findSharedCliDefaultsPath(fromUrl);
145
+ const content = await readFile(filepath, "utf8");
146
+ const parsed = parseYaml(content);
147
+
148
+ if (!parsed || typeof parsed !== "object") {
149
+ throw new Error(`Invalid shared CLI defaults file: ${filepath}`);
150
+ }
151
+
152
+ const normalized = normalizeAgentYesConfig(parsed as RawAgentYesConfig);
153
+ return normalized.clis ?? {};
154
+ }
155
+
156
+ export function isRegexSource(value: unknown): value is RegexSource {
157
+ return typeof value === "string" || isRegexSourceObject(value);
158
+ }