agent-yes 1.47.0 → 1.49.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.
@@ -0,0 +1,163 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/snomiao/agent-yes/blob/main/agent-yes.config.schema.json",
4
+ "title": "agent-yes configuration",
5
+ "description": "Configuration schema for agent-yes - automated interaction wrapper for AI coding assistants",
6
+ "type": "object",
7
+ "properties": {
8
+ "$schema": {
9
+ "type": "string",
10
+ "description": "JSON Schema reference for IDE support"
11
+ },
12
+ "configDir": {
13
+ "type": "string",
14
+ "description": "Directory to store agent-yes config files (e.g., session store)"
15
+ },
16
+ "logsDir": {
17
+ "type": "string",
18
+ "description": "Directory to store agent-yes log files"
19
+ },
20
+ "clis": {
21
+ "type": "object",
22
+ "description": "CLI-specific configurations",
23
+ "additionalProperties": {
24
+ "$ref": "#/definitions/AgentCliConfig"
25
+ },
26
+ "properties": {
27
+ "claude": { "$ref": "#/definitions/AgentCliConfig" },
28
+ "gemini": { "$ref": "#/definitions/AgentCliConfig" },
29
+ "codex": { "$ref": "#/definitions/AgentCliConfig" },
30
+ "copilot": { "$ref": "#/definitions/AgentCliConfig" },
31
+ "cursor": { "$ref": "#/definitions/AgentCliConfig" },
32
+ "grok": { "$ref": "#/definitions/AgentCliConfig" },
33
+ "qwen": { "$ref": "#/definitions/AgentCliConfig" },
34
+ "auggie": { "$ref": "#/definitions/AgentCliConfig" },
35
+ "amp": { "$ref": "#/definitions/AgentCliConfig" },
36
+ "opencode": { "$ref": "#/definitions/AgentCliConfig" }
37
+ }
38
+ }
39
+ },
40
+ "definitions": {
41
+ "AgentCliConfig": {
42
+ "type": "object",
43
+ "description": "Configuration for a specific CLI tool",
44
+ "properties": {
45
+ "install": {
46
+ "description": "Install command(s) for the CLI tool",
47
+ "oneOf": [
48
+ { "type": "string" },
49
+ {
50
+ "type": "object",
51
+ "properties": {
52
+ "powershell": { "type": "string", "description": "PowerShell install command (Windows)" },
53
+ "bash": { "type": "string", "description": "Bash install command (Unix/macOS)" },
54
+ "npm": { "type": "string", "description": "npm install command (fallback)" },
55
+ "unix": { "type": "string", "description": "Unix-specific install command" },
56
+ "windows": { "type": "string", "description": "Windows-specific install command" }
57
+ },
58
+ "additionalProperties": false
59
+ }
60
+ ]
61
+ },
62
+ "version": {
63
+ "type": "string",
64
+ "description": "Command to check if CLI is installed (e.g., 'claude --version')"
65
+ },
66
+ "binary": {
67
+ "type": "string",
68
+ "description": "Actual binary name if different from CLI name (e.g., 'cursor-agent' for cursor)"
69
+ },
70
+ "defaultArgs": {
71
+ "type": "array",
72
+ "items": { "type": "string" },
73
+ "description": "Default arguments to always pass to the CLI"
74
+ },
75
+ "ready": {
76
+ "type": "array",
77
+ "items": { "type": "string" },
78
+ "description": "Regex patterns to detect when CLI is ready for input. Set to [] to disable ready check."
79
+ },
80
+ "fatal": {
81
+ "type": "array",
82
+ "items": { "type": "string" },
83
+ "description": "Regex patterns to detect fatal errors that should stop execution"
84
+ },
85
+ "working": {
86
+ "type": "array",
87
+ "items": { "type": "string" },
88
+ "description": "Regex patterns to detect when CLI is currently processing"
89
+ },
90
+ "exitCommands": {
91
+ "type": "array",
92
+ "items": { "type": "string" },
93
+ "description": "Commands to exit the CLI gracefully (e.g., '/exit', '/quit')"
94
+ },
95
+ "promptArg": {
96
+ "type": "string",
97
+ "description": "How to pass the prompt: 'first-arg', 'last-arg', or a flag like '--prompt'",
98
+ "examples": ["first-arg", "last-arg", "--prompt", "-p"]
99
+ },
100
+ "noEOL": {
101
+ "type": "boolean",
102
+ "description": "If true, don't split lines by newline (for CLIs using cursor movement like codex)"
103
+ },
104
+ "enter": {
105
+ "type": "array",
106
+ "items": { "type": "string" },
107
+ "description": "Regex patterns that trigger automatic Enter key press"
108
+ },
109
+ "enterExclude": {
110
+ "type": "array",
111
+ "items": { "type": "string" },
112
+ "description": "Regex patterns to exclude from auto-enter (even if 'enter' matches)"
113
+ },
114
+ "typingRespond": {
115
+ "type": "object",
116
+ "description": "Map of responses to type when specific patterns are matched",
117
+ "additionalProperties": {
118
+ "type": "array",
119
+ "items": { "type": "string" },
120
+ "description": "Regex patterns that trigger this response"
121
+ },
122
+ "examples": [
123
+ { "y\\n": ["Do you want to continue\\?"] },
124
+ { "1\\n": ["Select an option:"] }
125
+ ]
126
+ },
127
+ "restoreArgs": {
128
+ "type": "array",
129
+ "items": { "type": "string" },
130
+ "description": "Arguments to add when restarting after a crash (e.g., ['--continue'])"
131
+ },
132
+ "restartWithoutContinueArg": {
133
+ "type": "array",
134
+ "items": { "type": "string" },
135
+ "description": "Regex patterns for errors that require restart WITHOUT continue args"
136
+ },
137
+ "bunx": {
138
+ "type": "boolean",
139
+ "description": "Use bunx instead of npx to run the CLI (faster startup)"
140
+ },
141
+ "help": {
142
+ "type": "string",
143
+ "description": "URL to documentation or help page for this CLI"
144
+ },
145
+ "systemPrompt": {
146
+ "type": "string",
147
+ "description": "Flag to pass system prompt (e.g., '--append-system-prompt')"
148
+ },
149
+ "system": {
150
+ "type": "string",
151
+ "description": "System prompt content to inject"
152
+ },
153
+ "updateAvailable": {
154
+ "type": "array",
155
+ "items": { "type": "string" },
156
+ "description": "Regex patterns to detect update available messages"
157
+ }
158
+ },
159
+ "additionalProperties": false
160
+ }
161
+ },
162
+ "additionalProperties": false
163
+ }
@@ -10118,7 +10118,7 @@ const globalAgentRegistry = new AgentRegistry();
10118
10118
 
10119
10119
  //#endregion
10120
10120
  //#region ts/index.ts
10121
- const config = await import("./agent-yes.config-DgkhZ7eQ.js").then((mod) => mod.default || mod);
10121
+ const config = await import("./agent-yes.config-BJbInLnS.js").then((mod) => mod.default || mod);
10122
10122
  const CLIS_CONFIG = config.clis;
10123
10123
  /**
10124
10124
  * Main function to run agent-cli with automatic yes/no responses
@@ -10735,4 +10735,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
10735
10735
 
10736
10736
  //#endregion
10737
10737
  export { AgentContext as a, config as i, CLIS_CONFIG as n, PidStore as o, agentYes as r, removeControlCharacters as s, SUPPORTED_CLIS as t };
10738
- //# sourceMappingURL=SUPPORTED_CLIS-OBl9bioJ.js.map
10738
+ //# sourceMappingURL=SUPPORTED_CLIS-CGHhOLoD.js.map
@@ -1,5 +1,5 @@
1
1
  import { t as logger } from "./logger-DH1Rx9HI.js";
2
- import { access, mkdir, readFile } from "node:fs/promises";
2
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { parse } from "yaml";
@@ -32,6 +32,8 @@ const CONFIG_EXTENSIONS = [
32
32
  ".yml",
33
33
  ".yaml"
34
34
  ];
35
+ const SCHEMA_URL = "https://raw.githubusercontent.com/snomiao/agent-yes/main/agent-yes.config.schema.json";
36
+ const YAML_SCHEMA_COMMENT = `# yaml-language-server: $schema=${SCHEMA_URL}`;
35
37
  /**
36
38
  * Check if a file exists
37
39
  */
@@ -108,10 +110,86 @@ async function loadCascadingConfig(options = {}) {
108
110
  logger.debug("[config] Merged config from", nonEmptyConfigs.length, "sources");
109
111
  return merged;
110
112
  }
113
+ /**
114
+ * Check if a config file has a schema reference
115
+ */
116
+ function hasSchemaReference(content, ext) {
117
+ if (ext === ".json") try {
118
+ return "$schema" in JSON.parse(content);
119
+ } catch {
120
+ return false;
121
+ }
122
+ return content.includes("yaml-language-server:") && content.includes("$schema");
123
+ }
124
+ /**
125
+ * Add schema reference to a config file content
126
+ */
127
+ function addSchemaReference(content, ext) {
128
+ if (ext === ".json") try {
129
+ const parsed = JSON.parse(content);
130
+ if ("$schema" in parsed) return content;
131
+ const withSchema = {
132
+ $schema: SCHEMA_URL,
133
+ ...parsed
134
+ };
135
+ return JSON.stringify(withSchema, null, 2) + "\n";
136
+ } catch {
137
+ return content;
138
+ }
139
+ if (hasSchemaReference(content, ext)) return content;
140
+ const lines = content.split("\n");
141
+ let insertIndex = 0;
142
+ for (let i = 0; i < lines.length; i++) {
143
+ const line = lines[i].trim();
144
+ if (line && !line.startsWith("#")) {
145
+ insertIndex = i;
146
+ break;
147
+ }
148
+ insertIndex = i + 1;
149
+ }
150
+ lines.splice(insertIndex, 0, YAML_SCHEMA_COMMENT, "");
151
+ return lines.join("\n");
152
+ }
153
+ /**
154
+ * Ensure schema reference exists in config files
155
+ * Modifies files in-place if they don't have a schema reference
156
+ */
157
+ async function ensureSchemaInConfigFiles(options = {}) {
158
+ const projectDir = options.projectDir ?? process.cwd();
159
+ const homeDir = options.homeDir ?? os.homedir();
160
+ const modified = [];
161
+ const skipped = [];
162
+ for (const dir of [homeDir, projectDir]) {
163
+ const filepath = await findConfigInDir(dir);
164
+ if (!filepath) continue;
165
+ try {
166
+ const content = await readFile(filepath, "utf-8");
167
+ const ext = path.extname(filepath).toLowerCase();
168
+ if (hasSchemaReference(content, ext)) {
169
+ skipped.push(filepath);
170
+ logger.debug(`[config] Schema already present in: ${filepath}`);
171
+ continue;
172
+ }
173
+ const newContent = addSchemaReference(content, ext);
174
+ if (newContent !== content) {
175
+ await writeFile(filepath, newContent, "utf-8");
176
+ modified.push(filepath);
177
+ logger.info(`[config] Added schema reference to: ${filepath}`);
178
+ }
179
+ } catch (error) {
180
+ logger.warn(`[config] Failed to update schema in ${filepath}:`, error);
181
+ }
182
+ }
183
+ return {
184
+ modified,
185
+ skipped
186
+ };
187
+ }
111
188
 
112
189
  //#endregion
113
190
  //#region agent-yes.config.ts
114
191
  logger.debug("loading cli-yes.config.ts from " + import.meta.url);
192
+ ensureSchemaInConfigFiles().catch(() => {});
115
193
  const configDir = await (async () => {
116
194
  const homeConfigDir = path.resolve(os.homedir(), ".agent-yes");
117
195
  if (await mkdir(homeConfigDir, { recursive: true }).then(() => true).catch(() => false)) {
@@ -252,4 +330,4 @@ function getDefaultConfig() {
252
330
 
253
331
  //#endregion
254
332
  export { agent_yes_config_default as t };
255
- //# sourceMappingURL=agent-yes.config-CUuciqYW.js.map
333
+ //# sourceMappingURL=agent-yes.config-B0st3al_.js.map
@@ -1,4 +1,4 @@
1
1
  import "./logger-DH1Rx9HI.js";
2
- import { t as agent_yes_config_default } from "./agent-yes.config-CUuciqYW.js";
2
+ import { t as agent_yes_config_default } from "./agent-yes.config-B0st3al_.js";
3
3
 
4
4
  export { agent_yes_config_default as default };
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import { c as __toESM, i as __commonJSMin, r as require_ms, t as logger } from "./logger-DH1Rx9HI.js";
3
- import "./agent-yes.config-CUuciqYW.js";
4
- import { o as PidStore, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-OBl9bioJ.js";
3
+ import "./agent-yes.config-B0st3al_.js";
4
+ import { o as PidStore, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-CGHhOLoD.js";
5
5
  import { createRequire } from "node:module";
6
6
  import { argv } from "process";
7
7
  import { spawn } from "child_process";
@@ -4505,7 +4505,7 @@ const Yargs = YargsFactory(esm_default);
4505
4505
  //#endregion
4506
4506
  //#region package.json
4507
4507
  var name = "agent-yes";
4508
- var version = "1.47.0";
4508
+ var version = "1.49.0";
4509
4509
 
4510
4510
  //#endregion
4511
4511
  //#region ts/parseCliArgs.ts
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import "./logger-DH1Rx9HI.js";
2
- import { a as AgentContext, i as config, n as CLIS_CONFIG, r as agentYes, s as removeControlCharacters } from "./SUPPORTED_CLIS-OBl9bioJ.js";
2
+ import { a as AgentContext, i as config, n as CLIS_CONFIG, r as agentYes, s as removeControlCharacters } from "./SUPPORTED_CLIS-CGHhOLoD.js";
3
3
 
4
4
  export { AgentContext, CLIS_CONFIG, config, agentYes as default, removeControlCharacters };
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/snomiao/agent-yes/main/agent-yes.config.schema.json",
3
+ "configDir": "~/.agent-yes",
4
+ "logsDir": "~/.agent-yes/logs",
5
+ "clis": {
6
+ "claude": {
7
+ "defaultArgs": ["--verbose"],
8
+ "ready": ["\\? for shortcuts", "^>[ \\u00A0]"],
9
+ "enter": ["> 1\\. Yes", "Press Enter to continue"]
10
+ },
11
+ "codex": {
12
+ "defaultArgs": ["--search"],
13
+ "ready": ["\\? for shortcuts"]
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,55 @@
1
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/snomiao/agent-yes/main/agent-yes.config.schema.json
2
+
3
+ # agent-yes configuration
4
+ # Place this file in your project root or home directory
5
+
6
+ configDir: ~/.agent-yes
7
+ logsDir: ~/.agent-yes/logs
8
+
9
+ clis:
10
+ # Claude Code configuration
11
+ claude:
12
+ defaultArgs:
13
+ - --verbose
14
+ # Patterns to detect when Claude is ready for input
15
+ ready:
16
+ - "\\? for shortcuts"
17
+ - "^>[ \\u00A0]"
18
+ # Patterns that trigger automatic Enter
19
+ enter:
20
+ - "> 1\\. Yes"
21
+ - "Press Enter to continue"
22
+ # Auto-type responses
23
+ typingRespond:
24
+ "1\n":
25
+ - "Do you want to use this API key\\?"
26
+
27
+ # Codex configuration
28
+ codex:
29
+ defaultArgs:
30
+ - --search
31
+ ready:
32
+ - "\\? for shortcuts"
33
+ enter:
34
+ - "> 1\\. Yes,"
35
+ - "> 1\\. Approve and run now"
36
+
37
+ # Gemini configuration
38
+ gemini:
39
+ ready:
40
+ - "Type your message"
41
+ enter:
42
+ - "│ ● 1\\. Yes, allow once"
43
+ restoreArgs:
44
+ - --resume
45
+
46
+ # Custom CLI example
47
+ # my-custom-cli:
48
+ # binary: my-cli-binary
49
+ # install:
50
+ # npm: "npm install -g my-cli"
51
+ # bash: "curl -fsSL https://example.com/install.sh | bash"
52
+ # ready:
53
+ # - "Ready>"
54
+ # enter:
55
+ # - "Confirm\\? \\[y/N\\]"
@@ -0,0 +1,71 @@
1
+ # GCP Cloud Run Demo
2
+
3
+ ## Quick Start
4
+
5
+ ### 1. Set your API key
6
+
7
+ ```bash
8
+ export ANTHROPIC_API_KEY='your-api-key-here'
9
+ ```
10
+
11
+ ### 2. Run the deployment script
12
+
13
+ ```bash
14
+ cd demo
15
+ ./gcp.sh "hello"
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ The script accepts a command as the first argument:
21
+
22
+ ```bash
23
+ # Run a hello command
24
+ ./gcp.sh "hello"
25
+
26
+ # Run a custom command
27
+ ./gcp.sh "list all files in the current directory"
28
+
29
+ # Default command (if no argument provided)
30
+ ./gcp.sh
31
+ ```
32
+
33
+ ## Configuration
34
+
35
+ You can customize the deployment with environment variables:
36
+
37
+ ```bash
38
+ export GCP_PROJECT_ID="my-project"
39
+ export GCP_REGION="asia-northeast1"
40
+ export GCP_JOB_NAME="my-custom-job"
41
+ export GCP_IMAGE="ghcr.io/snomiao/agent-yes:latest"
42
+
43
+ ./gcp.sh "your command here"
44
+ ```
45
+
46
+ ## How it works
47
+
48
+ 1. **No TTY needed**: The script configures Cloud Run Jobs to run non-interactively
49
+ 2. **API Key authentication**: Uses `ANTHROPIC_API_KEY` environment variable
50
+ 3. **Auto-execution**: Creates/updates the job and executes it immediately
51
+ 4. **Log streaming**: Automatically streams logs from the execution
52
+
53
+ ## Why Cloud Run Jobs instead of Cloud Run Services?
54
+
55
+ - **Cloud Run Services**: Require an HTTP server listening on `$PORT` - not suitable for CLI tools
56
+ - **Cloud Run Jobs**: Run to completion and exit - perfect for CLI tools like `agent-yes`
57
+
58
+ ## Alternative: Quick test on Compute Engine
59
+
60
+ If you need TTY for local testing:
61
+
62
+ ```bash
63
+ # Create a VM
64
+ gcloud compute instances create agent-yes-vm \
65
+ --machine-type=e2-medium \
66
+ --zone=us-central1-a
67
+
68
+ # SSH and run
69
+ gcloud compute ssh agent-yes-vm -- \
70
+ 'docker run -it --rm -e ANTHROPIC_API_KEY=xxx ghcr.io/snomiao/agent-yes:latest claude -- hello'
71
+ ```
@@ -0,0 +1,2 @@
1
+ # play with docker container
2
+ docker compose run --build -it -v ./:/ws --rm --remove-orphans agent-yes claude-yes -- hello, world
@@ -0,0 +1,75 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ # Configuration
6
+ PROJECT_ID="${GCP_PROJECT_ID:-$(gcloud config get-value project)}"
7
+ REGION="${GCP_REGION:-us-central1}"
8
+ JOB_NAME="${GCP_JOB_NAME:-agent-yes-job}"
9
+ IMAGE="${GCP_IMAGE:-ghcr.io/snomiao/agent-yes:latest}"
10
+ COMMAND="${1:-hello}"
11
+
12
+ # Check for ANTHROPIC_API_KEY
13
+ if [ -z "$ANTHROPIC_API_KEY" ]; then
14
+ echo "Error: ANTHROPIC_API_KEY environment variable is not set"
15
+ echo ""
16
+ echo "IMPORTANT: You need an API key from Anthropic Console (NOT OAuth token)"
17
+ echo "1. Go to: https://console.anthropic.com/"
18
+ echo "2. Create an API key (starts with sk-ant-api03-...)"
19
+ echo "3. Export it: export ANTHROPIC_API_KEY='sk-ant-api03-...'"
20
+ echo ""
21
+ echo "Note: The OAuth token from ~/.claude/.credentials.json won't work for Cloud Run"
22
+ echo ""
23
+ exit 1
24
+ fi
25
+
26
+ echo "========================================="
27
+ echo "Google Cloud Run Jobs Deployment"
28
+ echo "========================================="
29
+ echo "Project ID: $PROJECT_ID"
30
+ echo "Region: $REGION"
31
+ echo "Job Name: $JOB_NAME"
32
+ echo "Image: $IMAGE"
33
+ echo "Command: $COMMAND"
34
+ echo "========================================="
35
+
36
+ # Deploy or update Cloud Run Job
37
+ echo ""
38
+ echo "Deploying to Cloud Run Jobs..."
39
+ if gcloud run jobs describe "$JOB_NAME" --region="$REGION" &>/dev/null; then
40
+ echo "Job exists, updating..."
41
+ gcloud run jobs update "$JOB_NAME" \
42
+ --image="$IMAGE" \
43
+ --region="$REGION" \
44
+ --task-timeout=3600 \
45
+ --max-retries=0 \
46
+ --set-env-vars="ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" \
47
+ --args="claude","--","$COMMAND"
48
+ else
49
+ echo "Creating new job..."
50
+ gcloud run jobs create "$JOB_NAME" \
51
+ --image="$IMAGE" \
52
+ --region="$REGION" \
53
+ --task-timeout=3600 \
54
+ --max-retries=0 \
55
+ --set-env-vars="ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY" \
56
+ --args="claude","--","$COMMAND"
57
+ fi
58
+
59
+ # Execute the job
60
+ echo ""
61
+ echo "Executing the job..."
62
+ EXECUTION_NAME=$(gcloud run jobs execute "$JOB_NAME" --region="$REGION" --format="value(metadata.name)")
63
+
64
+ echo ""
65
+ echo "Job execution started: $EXECUTION_NAME"
66
+ echo ""
67
+ echo "Streaming logs..."
68
+ echo "========================================="
69
+ gcloud run jobs executions logs tail "$EXECUTION_NAME" --region="$REGION"
70
+
71
+ echo ""
72
+ echo "========================================="
73
+ echo "View in console:"
74
+ echo " https://console.cloud.google.com/run/jobs/details/$REGION/$JOB_NAME?project=$PROJECT_ID"
75
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-yes",
3
- "version": "1.47.0",
3
+ "version": "1.49.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",
@@ -50,7 +50,9 @@
50
50
  "ts/*.ts",
51
51
  "!dist/**/*.map",
52
52
  "dist/**/*.js",
53
- "bin"
53
+ "bin",
54
+ "agent-yes.config.schema.json",
55
+ "examples"
54
56
  ],
55
57
  "type": "module",
56
58
  "module": "ts/index.ts",
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
- import { loadCascadingConfig, getConfigPaths } from "./configLoader.ts";
3
- import { mkdir, writeFile, rm } from "node:fs/promises";
2
+ import { loadCascadingConfig, getConfigPaths, ensureSchemaInConfigFiles } from "./configLoader.ts";
3
+ import { mkdir, writeFile, readFile, rm } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
6
6
 
@@ -124,4 +124,69 @@ logsDir: /custom/logs
124
124
  expect(config.configDir).toBe("/project/config"); // Project takes precedence
125
125
  expect(config.logsDir).toBe("/home/logs"); // Home is used when not overridden
126
126
  });
127
+
128
+ it("should add schema reference to JSON config without one", async () => {
129
+ const configPath = path.join(testDir, ".agent-yes.config.json");
130
+ await writeFile(
131
+ configPath,
132
+ JSON.stringify({ configDir: "/test" })
133
+ );
134
+
135
+ const result = await ensureSchemaInConfigFiles({ projectDir: testDir, homeDir: testDir });
136
+ expect(result.modified).toContain(configPath);
137
+
138
+ const content = await readFile(configPath, "utf-8");
139
+ const parsed = JSON.parse(content);
140
+ expect(parsed.$schema).toContain("agent-yes.config.schema.json");
141
+ expect(parsed.configDir).toBe("/test"); // Original content preserved
142
+ });
143
+
144
+ it("should add schema comment to YAML config without one", async () => {
145
+ const configPath = path.join(testDir, ".agent-yes.config.yaml");
146
+ await writeFile(
147
+ configPath,
148
+ `configDir: /test
149
+ clis:
150
+ claude:
151
+ defaultArgs:
152
+ - --verbose
153
+ `
154
+ );
155
+
156
+ const result = await ensureSchemaInConfigFiles({ projectDir: testDir, homeDir: testDir });
157
+ expect(result.modified).toContain(configPath);
158
+
159
+ const content = await readFile(configPath, "utf-8");
160
+ expect(content).toContain("yaml-language-server:");
161
+ expect(content).toContain("$schema=");
162
+ expect(content).toContain("configDir: /test"); // Original content preserved
163
+ });
164
+
165
+ it("should skip JSON config that already has schema", async () => {
166
+ const configPath = path.join(testDir, ".agent-yes.config.json");
167
+ const originalContent = JSON.stringify({
168
+ $schema: "https://example.com/schema.json",
169
+ configDir: "/test",
170
+ }, null, 2);
171
+ await writeFile(configPath, originalContent);
172
+
173
+ const result = await ensureSchemaInConfigFiles({ projectDir: testDir, homeDir: testDir });
174
+ expect(result.skipped).toContain(configPath);
175
+ expect(result.modified).not.toContain(configPath);
176
+
177
+ const content = await readFile(configPath, "utf-8");
178
+ expect(content).toBe(originalContent); // Unchanged
179
+ });
180
+
181
+ it("should skip YAML config that already has schema comment", async () => {
182
+ const configPath = path.join(testDir, ".agent-yes.config.yaml");
183
+ const originalContent = `# yaml-language-server: $schema=https://example.com/schema.json
184
+ configDir: /test
185
+ `;
186
+ await writeFile(configPath, originalContent);
187
+
188
+ const result = await ensureSchemaInConfigFiles({ projectDir: testDir, homeDir: testDir });
189
+ expect(result.skipped).toContain(configPath);
190
+ expect(result.modified).not.toContain(configPath);
191
+ });
127
192
  });
@@ -2,7 +2,7 @@
2
2
  //! Supports JSON, YAML, YML formats
3
3
  //! Priority: project-dir > home-dir > package-dir
4
4
 
5
- import { readFile, access } from "node:fs/promises";
5
+ import { readFile, writeFile, access } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import os from "node:os";
8
8
  import { parse as parseYaml } from "yaml";
@@ -12,6 +12,8 @@ import { deepMixin } from "./utils.ts";
12
12
 
13
13
  const CONFIG_FILENAME = ".agent-yes.config";
14
14
  const CONFIG_EXTENSIONS = [".json", ".yml", ".yaml"] as const;
15
+ const SCHEMA_URL = "https://raw.githubusercontent.com/snomiao/agent-yes/main/agent-yes.config.schema.json";
16
+ const YAML_SCHEMA_COMMENT = `# yaml-language-server: $schema=${SCHEMA_URL}`;
15
17
 
16
18
  /**
17
19
  * Check if a file exists
@@ -146,3 +148,103 @@ export function getConfigPaths(options: ConfigLoadOptions = {}): string[] {
146
148
 
147
149
  return paths;
148
150
  }
151
+
152
+ /**
153
+ * Check if a config file has a schema reference
154
+ */
155
+ function hasSchemaReference(content: string, ext: string): boolean {
156
+ if (ext === ".json") {
157
+ try {
158
+ const parsed = JSON.parse(content);
159
+ return "$schema" in parsed;
160
+ } catch {
161
+ return false;
162
+ }
163
+ }
164
+ // YAML/YML - check for yaml-language-server comment
165
+ return content.includes("yaml-language-server:") && content.includes("$schema");
166
+ }
167
+
168
+ /**
169
+ * Add schema reference to a config file content
170
+ */
171
+ function addSchemaReference(content: string, ext: string): string {
172
+ if (ext === ".json") {
173
+ try {
174
+ const parsed = JSON.parse(content);
175
+ if ("$schema" in parsed) {
176
+ return content; // Already has schema
177
+ }
178
+ // Add $schema as first property
179
+ const withSchema = { $schema: SCHEMA_URL, ...parsed };
180
+ return JSON.stringify(withSchema, null, 2) + "\n";
181
+ } catch {
182
+ return content; // Can't parse, return as-is
183
+ }
184
+ }
185
+
186
+ // YAML/YML - prepend comment
187
+ if (hasSchemaReference(content, ext)) {
188
+ return content; // Already has schema
189
+ }
190
+
191
+ // Add schema comment at the top, preserving any existing comments
192
+ const lines = content.split("\n");
193
+
194
+ // Find the first non-empty, non-comment line to insert before actual content
195
+ let insertIndex = 0;
196
+ for (let i = 0; i < lines.length; i++) {
197
+ const line = lines[i].trim();
198
+ if (line && !line.startsWith("#")) {
199
+ insertIndex = i;
200
+ break;
201
+ }
202
+ insertIndex = i + 1;
203
+ }
204
+
205
+ // Insert schema comment before the first content line
206
+ lines.splice(insertIndex, 0, YAML_SCHEMA_COMMENT, "");
207
+ return lines.join("\n");
208
+ }
209
+
210
+ /**
211
+ * Ensure schema reference exists in config files
212
+ * Modifies files in-place if they don't have a schema reference
213
+ */
214
+ export async function ensureSchemaInConfigFiles(
215
+ options: ConfigLoadOptions = {}
216
+ ): Promise<{ modified: string[]; skipped: string[] }> {
217
+ const projectDir = options.projectDir ?? process.cwd();
218
+ const homeDir = options.homeDir ?? os.homedir();
219
+
220
+ const modified: string[] = [];
221
+ const skipped: string[] = [];
222
+
223
+ // Check both project and home directories (not package dir - that's read-only)
224
+ for (const dir of [homeDir, projectDir]) {
225
+ const filepath = await findConfigInDir(dir);
226
+ if (!filepath) continue;
227
+
228
+ try {
229
+ const content = await readFile(filepath, "utf-8");
230
+ const ext = path.extname(filepath).toLowerCase();
231
+
232
+ if (hasSchemaReference(content, ext)) {
233
+ skipped.push(filepath);
234
+ logger.debug(`[config] Schema already present in: ${filepath}`);
235
+ continue;
236
+ }
237
+
238
+ const newContent = addSchemaReference(content, ext);
239
+ if (newContent !== content) {
240
+ await writeFile(filepath, newContent, "utf-8");
241
+ modified.push(filepath);
242
+ logger.info(`[config] Added schema reference to: ${filepath}`);
243
+ }
244
+ } catch (error) {
245
+ logger.warn(`[config] Failed to update schema in ${filepath}:`, error);
246
+ }
247
+ }
248
+
249
+ return { modified, skipped };
250
+ }