yatfa 1.0.66 → 1.0.68
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/README.md +30 -5
- package/agents.rb +36 -1
- package/bin/run-yatfa-agent.rb +39 -7
- package/bin/yatfa +5 -0
- package/lib/yatfa_agent/agent.rb +206 -80
- package/lib/yatfa_agent/config.rb +21 -8
- package/lib/yatfa_agent/setup.rb +56 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -102,23 +102,46 @@ npx yatfa setup
|
|
|
102
102
|
|
|
103
103
|
Launches interactive setup wizard to configure `yatfa.env.rb`.
|
|
104
104
|
|
|
105
|
+
### Check Version
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx yatfa version
|
|
109
|
+
npx yatfa -v
|
|
110
|
+
npx yatfa --version
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Prints the installed yatfa package version (e.g., `yatfa v1.0.67`).
|
|
114
|
+
|
|
105
115
|
### Run Agents
|
|
106
116
|
|
|
117
|
+
When you run an agent, the CLI automatically:
|
|
118
|
+
1. **Checks if the agent is already running** - prompts to Attach, Restart, or Cancel
|
|
119
|
+
2. **Starts the agent** - creates the Docker container
|
|
120
|
+
3. **Prompts to attach** - after starting, asks if you want to attach to the session
|
|
121
|
+
|
|
107
122
|
```bash
|
|
108
|
-
# Start an agent
|
|
109
123
|
npx yatfa worker
|
|
110
124
|
npx yatfa planner
|
|
111
125
|
npx yatfa reviewer
|
|
112
126
|
npx yatfa researcher
|
|
113
127
|
```
|
|
114
128
|
|
|
115
|
-
|
|
129
|
+
#### Non-interactive Mode (for CI/scripts)
|
|
116
130
|
|
|
117
131
|
```bash
|
|
118
|
-
|
|
132
|
+
# Run without prompts
|
|
133
|
+
npx yatfa worker --no-prompt
|
|
134
|
+
# or
|
|
135
|
+
YATFA_NO_PROMPT=1 npx yatfa worker
|
|
119
136
|
```
|
|
120
137
|
|
|
121
|
-
|
|
138
|
+
### Attach to Running Agents (deprecated)
|
|
139
|
+
|
|
140
|
+
The `attach` command still works but is deprecated. Running `npx yatfa <agent-type>` now prompts to attach automatically.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npx yatfa attach worker # Deprecated - use 'npx yatfa worker' instead
|
|
144
|
+
```
|
|
122
145
|
|
|
123
146
|
### List Running Agents
|
|
124
147
|
|
|
@@ -241,8 +264,10 @@ See hook files for implementation details and customization options.
|
|
|
241
264
|
|
|
242
265
|
## Attaching to Running Agent
|
|
243
266
|
|
|
267
|
+
After starting an agent, the CLI will prompt you to attach. You can also attach directly:
|
|
268
|
+
|
|
244
269
|
```bash
|
|
245
|
-
docker exec -it <container> tmux attach -t agent
|
|
270
|
+
docker exec -it <container> tmux attach -t agent
|
|
246
271
|
```
|
|
247
272
|
|
|
248
273
|
Press `Ctrl+B` then `D` to detach.
|
package/agents.rb
CHANGED
|
@@ -144,6 +144,32 @@ If you encounter blocking problems or need to improve workflow:
|
|
|
144
144
|
5. **Context:** Include what you tried, the error/blocker, and suggested fix (e.g., "Escalation: Need additional MCP tools for research").
|
|
145
145
|
ESCALATION
|
|
146
146
|
|
|
147
|
+
CLAW_HEADER = <<~HEADER
|
|
148
|
+
You are the **YATFA CLAW** agent.
|
|
149
|
+
|
|
150
|
+
**ROLE:** OpenClaw Gateway Runner.
|
|
151
|
+
Your purpose is to run an OpenClaw gateway inside this container, providing a Telegram-based chat interface with the Pi agent.
|
|
152
|
+
|
|
153
|
+
### OPERATIONAL MODE
|
|
154
|
+
* **Execution:** The OpenClaw gateway starts automatically and runs persistently.
|
|
155
|
+
* **Telegram Integration:** Chat messages from Telegram are forwarded to the Pi agent via the gateway.
|
|
156
|
+
* **Configuration:** Set via environment variables or `~/.openclaw/openclaw.json`.
|
|
157
|
+
* **Persistence:** Gateway data is stored in a Docker volume at `~/.openclaw/`.
|
|
158
|
+
|
|
159
|
+
### ENVIRONMENT
|
|
160
|
+
* Sandboxed Docker container with ROOT privileges.
|
|
161
|
+
* Network mode: host (port 18789 exposed for gateway).
|
|
162
|
+
* OpenClaw installed globally via npm.
|
|
163
|
+
HEADER
|
|
164
|
+
|
|
165
|
+
CLAW_BOUNDARIES = <<~BOUNDARIES
|
|
166
|
+
### ROLE BOUNDARIES
|
|
167
|
+
* This agent runs the OpenClaw gateway - it does NOT run Claude Code.
|
|
168
|
+
* No Yatfa MCP tools are available (that is a future enhancement).
|
|
169
|
+
* No git repositories are mounted.
|
|
170
|
+
* No skills are loaded.
|
|
171
|
+
BOUNDARIES
|
|
172
|
+
|
|
147
173
|
# Dynamic banner builder
|
|
148
174
|
def build_banner(header:, boundaries:, escalation: nil, extra_sections: [])
|
|
149
175
|
[
|
|
@@ -178,7 +204,7 @@ AGENT_CONFIGS = {
|
|
|
178
204
|
|
|
179
205
|
'reviewer' => {
|
|
180
206
|
name: 'yatfa-autonomous-reviewer',
|
|
181
|
-
skills: ['reviewer-workflow', 'memory', 'proposal-reviewer'],
|
|
207
|
+
skills: ['reviewer-workflow', 'qa-workflow', 'memory', 'proposal-reviewer'],
|
|
182
208
|
banner: build_banner(
|
|
183
209
|
header: REVIEWER_HEADER,
|
|
184
210
|
boundaries: REVIEWER_BOUNDARIES,
|
|
@@ -194,5 +220,14 @@ AGENT_CONFIGS = {
|
|
|
194
220
|
boundaries: RESEARCHER_BOUNDARIES,
|
|
195
221
|
escalation: RESEARCHER_ESCALATION
|
|
196
222
|
)
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
'claw' => {
|
|
226
|
+
name: 'yatfa-openclaw',
|
|
227
|
+
skills: [],
|
|
228
|
+
banner: build_banner(
|
|
229
|
+
header: CLAW_HEADER,
|
|
230
|
+
boundaries: CLAW_BOUNDARIES
|
|
231
|
+
)
|
|
197
232
|
}
|
|
198
233
|
}.freeze
|
package/bin/run-yatfa-agent.rb
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
# YATFA Agent Runner (Yet Another Tool For Agents)
|
|
5
|
-
# Usage: npx yatfa [worker|planner|reviewer|researcher]
|
|
6
|
-
# npx yatfa attach [agent-type]
|
|
5
|
+
# Usage: npx yatfa [worker|planner|reviewer|researcher|claw]
|
|
7
6
|
# npx yatfa setup
|
|
8
7
|
#
|
|
9
8
|
# Requirements:
|
|
@@ -18,13 +17,26 @@ require_relative "../lib/yatfa_agent/agent"
|
|
|
18
17
|
require_relative "../lib/yatfa_agent/setup"
|
|
19
18
|
require_relative "../agents"
|
|
20
19
|
|
|
20
|
+
def show_version
|
|
21
|
+
require "json"
|
|
22
|
+
real_path = File.realpath(__FILE__)
|
|
23
|
+
package_json_path = File.join(File.dirname(real_path), "..", "package.json")
|
|
24
|
+
version = JSON.parse(File.read(package_json_path))["version"]
|
|
25
|
+
puts "yatfa v#{version}"
|
|
26
|
+
exit 0
|
|
27
|
+
end
|
|
28
|
+
|
|
21
29
|
def show_usage
|
|
22
30
|
puts "YATFA Agent Runner (Yet Another Tool For Agents)"
|
|
23
31
|
puts ""
|
|
24
32
|
puts "Usage:"
|
|
25
|
-
puts " npx yatfa
|
|
26
|
-
puts " npx yatfa
|
|
27
|
-
puts " npx yatfa
|
|
33
|
+
puts " npx yatfa version # Show version"
|
|
34
|
+
puts " npx yatfa setup # Interactive setup wizard"
|
|
35
|
+
puts " npx yatfa [worker|planner|reviewer|researcher|claw] # Run agent (prompts to attach)"
|
|
36
|
+
puts ""
|
|
37
|
+
puts "Options:"
|
|
38
|
+
puts " --no-prompt, -d Run without prompts (for CI/scripts)"
|
|
39
|
+
puts " YATFA_NO_PROMPT=1 Environment variable alternative"
|
|
28
40
|
puts ""
|
|
29
41
|
puts "Setup:"
|
|
30
42
|
puts " Run 'npx yatfa setup' for interactive configuration"
|
|
@@ -42,6 +54,13 @@ show_usage if ARGV.empty?
|
|
|
42
54
|
|
|
43
55
|
command = ARGV[0].downcase
|
|
44
56
|
|
|
57
|
+
# Set process title so ps/top shows the agent type instead of "node" (from npx)
|
|
58
|
+
agent_type_for_title = %w[worker planner reviewer researcher claw].include?(command) ? command : "yatfa"
|
|
59
|
+
Process.setproctitle("yatfa-#{agent_type_for_title}")
|
|
60
|
+
|
|
61
|
+
# Handle version command
|
|
62
|
+
show_version if %w[version -v --version].include?(command)
|
|
63
|
+
|
|
45
64
|
# Handle setup command
|
|
46
65
|
if command == "setup"
|
|
47
66
|
wizard = YatfaAgent::Setup::SetupWizard.new
|
|
@@ -49,15 +68,28 @@ if command == "setup"
|
|
|
49
68
|
exit 0
|
|
50
69
|
end
|
|
51
70
|
|
|
52
|
-
#
|
|
71
|
+
# Resolve project root by walking up the directory tree.
|
|
72
|
+
# This allows running `npx yatfa` from subfolders inside the project.
|
|
73
|
+
# Skipped for setup (which creates new config in cwd).
|
|
74
|
+
project_root = YatfaAgent::Config.find_project_root
|
|
75
|
+
if project_root && project_root != Dir.pwd
|
|
76
|
+
puts "📂 Running from subfolder, using config from #{project_root}"
|
|
77
|
+
Dir.chdir(project_root)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Handle attach command (deprecated - will still work)
|
|
53
81
|
if command == "attach"
|
|
82
|
+
puts ""
|
|
83
|
+
puts "⚠️ Deprecation: The 'attach' command is deprecated"
|
|
84
|
+
puts " Running `npx yatfa <agent-type>` now prompts to attach automatically."
|
|
85
|
+
puts ""
|
|
54
86
|
agent_type = ARGV[1]&.downcase
|
|
55
87
|
abort "Usage: npx yatfa attach [agent-type]" unless agent_type
|
|
56
88
|
config = YatfaAgent::Config.load
|
|
57
89
|
YatfaAgent::Config.fetch_repositories!(config)
|
|
58
90
|
YatfaAgent::Agent.attach(agent_type, config, AGENT_CONFIGS)
|
|
59
91
|
else
|
|
60
|
-
# Handle run commands (worker, planner, reviewer, researcher)
|
|
92
|
+
# Handle run commands (worker, planner, reviewer, researcher, claw)
|
|
61
93
|
config = YatfaAgent::Config.load
|
|
62
94
|
# Fetch repositories from Rails API before building
|
|
63
95
|
YatfaAgent::Config.fetch_repositories!(config)
|
package/bin/yatfa
ADDED
package/lib/yatfa_agent/agent.rb
CHANGED
|
@@ -1,8 +1,113 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require 'fileutils'
|
|
3
2
|
|
|
4
3
|
module YatfaAgent
|
|
5
4
|
module Agent
|
|
5
|
+
# Check if container is currently running
|
|
6
|
+
def self.container_running?(container_name)
|
|
7
|
+
result = IO.popen(["docker", "ps", "--filter", "name=^#{container_name}$", "--format", "{{.Names}}"], err: File::NULL, &:read).strip
|
|
8
|
+
!result.empty?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Check if container exists (running or stopped)
|
|
12
|
+
def self.container_exists?(container_name)
|
|
13
|
+
result = IO.popen(["docker", "ps", "-a", "--filter", "name=^#{container_name}$", "--format", "{{.Names}}"], err: File::NULL, &:read).strip
|
|
14
|
+
!result.empty?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Prompt user for input with a default value
|
|
18
|
+
def self.prompt(message, default: nil, options: nil)
|
|
19
|
+
loop do
|
|
20
|
+
print message
|
|
21
|
+
input = $stdin.gets&.strip&.downcase || ""
|
|
22
|
+
|
|
23
|
+
# Handle default on empty input
|
|
24
|
+
if input.empty? && default
|
|
25
|
+
return default
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# If options specified, validate input
|
|
29
|
+
if options
|
|
30
|
+
valid = options.any? { |opt| opt.downcase == input || opt.downcase.start_with?(input) }
|
|
31
|
+
if valid
|
|
32
|
+
# Return the full option that matches
|
|
33
|
+
return options.find { |opt| opt.downcase.start_with?(input) }
|
|
34
|
+
end
|
|
35
|
+
puts " Invalid option. Please choose: #{options.join('/')}"
|
|
36
|
+
else
|
|
37
|
+
return input
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check if running in non-interactive mode
|
|
43
|
+
def self.non_interactive?
|
|
44
|
+
!$stdin.tty? || ENV['YATFA_NO_PROMPT'] == '1' || ARGV.include?('--no-prompt') || ARGV.include?('-d')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Handle running container: prompt user for action
|
|
48
|
+
def self.handle_running_container(agent_type, container_name, config, agent_configs)
|
|
49
|
+
puts "⚠️ #{agent_type} agent is already running (#{container_name})"
|
|
50
|
+
puts ""
|
|
51
|
+
|
|
52
|
+
if non_interactive?
|
|
53
|
+
puts " Running in non-interactive mode. Run 'npx yatfa #{agent_type}' to attach, or stop the container first."
|
|
54
|
+
puts " docker stop #{container_name}"
|
|
55
|
+
exit 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
choice = prompt(" [A]ttach / [R]estart / [C]ancel? [A/r/c]: ", default: "A", options: %w[A R C])
|
|
59
|
+
|
|
60
|
+
case choice
|
|
61
|
+
when "A"
|
|
62
|
+
puts ""
|
|
63
|
+
attach(agent_type, config, agent_configs)
|
|
64
|
+
when "R"
|
|
65
|
+
puts ""
|
|
66
|
+
puts "🔄 Stopping existing container..."
|
|
67
|
+
system("docker", "stop", container_name, err: File::NULL, out: File::NULL)
|
|
68
|
+
system("docker", "rm", "-f", container_name, err: File::NULL, out: File::NULL)
|
|
69
|
+
return true # Continue with start
|
|
70
|
+
when "C"
|
|
71
|
+
puts " Cancelled."
|
|
72
|
+
exit 0
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Banner file is no longer generated on the host.
|
|
77
|
+
# System prompt is generated inside the container by setup-agent.rb
|
|
78
|
+
# using AGENT_TYPE and REPOSITORIES_CONTEXT env vars.
|
|
79
|
+
|
|
80
|
+
# Handle stopped container: prompt user for action
|
|
81
|
+
def self.handle_stopped_container(agent_type, container_name, config, agent_configs)
|
|
82
|
+
puts "⚠️ #{agent_type} agent container exists but is stopped (#{container_name})"
|
|
83
|
+
puts ""
|
|
84
|
+
|
|
85
|
+
if non_interactive?
|
|
86
|
+
puts " Non-interactive mode: starting existing container."
|
|
87
|
+
system("docker", "start", container_name, out: File::NULL)
|
|
88
|
+
puts "✅ Agent started in background"
|
|
89
|
+
exit 0
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
choice = prompt(" [S]tart existing / [R]ecreate / [C]ancel? [S/r/c]: ", default: "S", options: %w[S R C])
|
|
93
|
+
|
|
94
|
+
case choice
|
|
95
|
+
when "S"
|
|
96
|
+
puts ""
|
|
97
|
+
puts "🔄 Starting existing container..."
|
|
98
|
+
system("docker", "start", container_name, out: File::NULL)
|
|
99
|
+
puts "✅ Agent started in background"
|
|
100
|
+
exit 0
|
|
101
|
+
when "R"
|
|
102
|
+
puts ""
|
|
103
|
+
puts "🔄 Removing stopped container..."
|
|
104
|
+
system("docker", "rm", "-f", container_name, err: File::NULL, out: File::NULL)
|
|
105
|
+
when "C"
|
|
106
|
+
puts " Cancelled."
|
|
107
|
+
exit 0
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
6
111
|
def self.run(agent_type, config, agent_configs)
|
|
7
112
|
unless agent_configs.keys.include?(agent_type)
|
|
8
113
|
puts "❌ Unknown agent type: #{agent_type}"
|
|
@@ -14,48 +119,65 @@ module YatfaAgent
|
|
|
14
119
|
agent_config = config.dig("agents", agent_type) || {}
|
|
15
120
|
container_name = agent_config["container_name"] || agent_def[:name]
|
|
16
121
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Append repository context to banner
|
|
27
|
-
banner_content = agent_def[:banner].dup
|
|
28
|
-
if config["repositories"]&.any?
|
|
29
|
-
sorted_repos = config["repositories"].sort_by { |r| r["position"] || 0 }
|
|
30
|
-
repo_info = sorted_repos.map do |repo|
|
|
31
|
-
name = repo["name"]
|
|
32
|
-
path = repo["path_in_sandbox"] || "/workspace/#{name}"
|
|
33
|
-
" - **#{name}**: #{path}"
|
|
34
|
-
end.join("\n")
|
|
35
|
-
|
|
36
|
-
banner_content += "\n\n## REPOSITORY STRUCTURE\n\n"
|
|
37
|
-
banner_content += "This project has #{sorted_repos.count} repositories:\n\n"
|
|
38
|
-
banner_content += "#{repo_info}\n\n"
|
|
122
|
+
# Check if container is already running and handle interactively
|
|
123
|
+
if container_running?(container_name)
|
|
124
|
+
should_continue = handle_running_container(agent_type, container_name, config, agent_configs)
|
|
125
|
+
# If user chose Attach or Cancel, the method calls exec/exit, so we never reach here
|
|
126
|
+
# If user chose Restart, should_continue is true and we proceed to start a new container
|
|
127
|
+
return unless should_continue
|
|
128
|
+
elsif container_exists?(container_name)
|
|
129
|
+
handle_stopped_container(agent_type, container_name, config, agent_configs)
|
|
39
130
|
end
|
|
40
131
|
|
|
41
|
-
|
|
132
|
+
puts "🚀 Starting #{agent_type} agent..."
|
|
42
133
|
|
|
43
|
-
docker_cmd = build_docker_command(container_name, agent_type, config, agent_config, agent_def
|
|
134
|
+
docker_cmd = build_docker_command(container_name, agent_type, config, agent_config, agent_def)
|
|
44
135
|
|
|
45
136
|
success = system(*docker_cmd)
|
|
46
137
|
|
|
47
138
|
if success
|
|
48
139
|
puts "✅ Agent started in background"
|
|
49
140
|
puts ""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
141
|
+
|
|
142
|
+
# Prompt to attach (interactive mode only)
|
|
143
|
+
prompt_to_attach(agent_type, container_name, config, agent_configs)
|
|
53
144
|
else
|
|
54
145
|
puts "❌ Failed to start agent"
|
|
55
146
|
exit 1
|
|
56
147
|
end
|
|
57
148
|
end
|
|
58
149
|
|
|
150
|
+
# Prompt user to attach after starting a new agent
|
|
151
|
+
def self.prompt_to_attach(agent_type, container_name, config, agent_configs)
|
|
152
|
+
if non_interactive?
|
|
153
|
+
# Non-interactive: just print helpful info
|
|
154
|
+
puts " Logs: docker logs -f #{container_name}"
|
|
155
|
+
puts " Stop: docker stop #{container_name}"
|
|
156
|
+
puts " Attach: docker exec -it #{container_name} tmux attach -t agent"
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
choice = prompt(" Agent started. Attach to session? [Y/n]: ", default: "Y", options: %w[Y N])
|
|
161
|
+
|
|
162
|
+
if choice == "Y"
|
|
163
|
+
puts ""
|
|
164
|
+
# Wait briefly for container to initialize
|
|
165
|
+
print " Waiting for agent to initialize"
|
|
166
|
+
5.times do
|
|
167
|
+
print "."
|
|
168
|
+
sleep 1
|
|
169
|
+
end
|
|
170
|
+
puts ""
|
|
171
|
+
attach(agent_type, config, agent_configs)
|
|
172
|
+
else
|
|
173
|
+
puts ""
|
|
174
|
+
puts " Agent is running in background."
|
|
175
|
+
puts " Logs: docker logs -f #{container_name}"
|
|
176
|
+
puts " Stop: docker stop #{container_name}"
|
|
177
|
+
puts " Attach: docker exec -it #{container_name} tmux attach -t agent"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
59
181
|
def self.attach(agent_type, config, agent_configs)
|
|
60
182
|
unless agent_configs.keys.include?(agent_type)
|
|
61
183
|
puts "❌ Unknown agent type: #{agent_type}"
|
|
@@ -66,7 +188,7 @@ module YatfaAgent
|
|
|
66
188
|
agent_config = config.dig("agents", agent_type) || {}
|
|
67
189
|
container_name = agent_config["container_name"] || agent_def[:name]
|
|
68
190
|
|
|
69
|
-
running =
|
|
191
|
+
running = IO.popen(["docker", "ps", "--filter", "name=^#{container_name}$", "--format", "{{.Names}}"], err: File::NULL, &:read).strip
|
|
70
192
|
|
|
71
193
|
if running.empty?
|
|
72
194
|
puts "⚠️ #{agent_type} agent is not running. Auto-starting..."
|
|
@@ -95,7 +217,7 @@ module YatfaAgent
|
|
|
95
217
|
|
|
96
218
|
private
|
|
97
219
|
|
|
98
|
-
def self.build_docker_command(container_name, agent_type, config, agent_config, agent_def
|
|
220
|
+
def self.build_docker_command(container_name, agent_type, config, agent_config, agent_def)
|
|
99
221
|
docker_cmd = [
|
|
100
222
|
"docker", "run", "-d",
|
|
101
223
|
"--name", container_name,
|
|
@@ -110,7 +232,7 @@ module YatfaAgent
|
|
|
110
232
|
merged_env = {}
|
|
111
233
|
merged_env.merge!(config["env"]) if config["env"]
|
|
112
234
|
merged_env.merge!(agent_config["env"]) if agent_config["env"]
|
|
113
|
-
|
|
235
|
+
|
|
114
236
|
if merged_env.any?
|
|
115
237
|
merged_env.each do |k, v|
|
|
116
238
|
docker_cmd += ["-e", "#{k}=#{v}"]
|
|
@@ -124,22 +246,51 @@ module YatfaAgent
|
|
|
124
246
|
end
|
|
125
247
|
end
|
|
126
248
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
249
|
+
if agent_type == "claw"
|
|
250
|
+
# Claw-specific Docker command setup
|
|
251
|
+
docker_cmd += [
|
|
252
|
+
"-e", "AGENT_TYPE=#{agent_type}",
|
|
253
|
+
"-e", "YATFA_VERSION=main"
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
# Add persistence volume for OpenClaw config and data (scoped per container)
|
|
257
|
+
docker_cmd += ["-v", "#{container_name}-openclaw-data:/home/claude/.openclaw"]
|
|
258
|
+
|
|
259
|
+
# Auto-inject telegram_bot_token as env var so users don't need to duplicate in env block
|
|
260
|
+
if agent_config["telegram_bot_token"]
|
|
261
|
+
docker_cmd += ["-e", "OPENCLAW_TELEGRAM_BOT_TOKEN=#{agent_config['telegram_bot_token']}"]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Pass YATFA_URL if configured (optional for claw)
|
|
265
|
+
if config["yatfa_url"]
|
|
266
|
+
docker_cmd += ["-e", "YATFA_URL=#{config['yatfa_url']}"]
|
|
267
|
+
docker_cmd += ["-e", "YATFA_API_URL=#{config['yatfa_url']}/api/v1"]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Pass MCP API key if configured (for future MCP integration)
|
|
271
|
+
if agent_config["mcp_api_key"]
|
|
272
|
+
docker_cmd += ["-e", "YATFA_API_KEY=#{agent_config['mcp_api_key']}"]
|
|
273
|
+
end
|
|
135
274
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
275
|
+
add_development_mounts!(docker_cmd)
|
|
276
|
+
else
|
|
277
|
+
# Standard agent setup (worker, planner, reviewer, researcher)
|
|
278
|
+
docker_cmd += [
|
|
279
|
+
"-e", "YATFA_VERSION=main",
|
|
280
|
+
"-e", "SKILLS=#{agent_def[:skills]&.join(',')}",
|
|
281
|
+
"-e", "AGENT_TYPE=#{agent_type}",
|
|
282
|
+
"-e", "YATFA_URL=#{YatfaAgent::Config.yatfa_url(config)}",
|
|
283
|
+
"-e", "YATFA_API_KEY=#{agent_config['mcp_api_key']}"
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
add_github_auth!(docker_cmd, config)
|
|
287
|
+
add_git_config!(docker_cmd, config)
|
|
288
|
+
add_repository_mounts!(docker_cmd, config)
|
|
289
|
+
add_development_mounts!(docker_cmd)
|
|
290
|
+
end
|
|
140
291
|
|
|
141
292
|
local_setup_script = File.join(File.dirname(__FILE__), "..", "..", "setup-agent.rb")
|
|
142
|
-
|
|
293
|
+
|
|
143
294
|
if File.exist?(local_setup_script)
|
|
144
295
|
docker_cmd += [Config.image_name(config), "ruby", "/tmp/setup-agent.rb"]
|
|
145
296
|
else
|
|
@@ -208,52 +359,27 @@ module YatfaAgent
|
|
|
208
359
|
|
|
209
360
|
def self.add_development_mounts!(docker_cmd)
|
|
210
361
|
# Check for local setup-agent.rb (for development)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
# Try multiple possible paths for the binary (built Go binaries in dist/)
|
|
218
|
-
possible_paths = [
|
|
219
|
-
File.join(Dir.pwd, "overrides", "agent-bridge-linux-#{linux_arch}"), # YATFA_SANDBOX/overrides
|
|
220
|
-
File.join(Dir.pwd, "yatfa-public", "dist", "agent-bridge-linux-#{linux_arch}"), # Running from metafolder
|
|
221
|
-
File.join(Dir.pwd, "dist", "agent-bridge-linux-#{linux_arch}"), # Running from yatfa-public
|
|
362
|
+
# Try multiple paths: relative to gem, relative to cwd
|
|
363
|
+
setup_script_paths = [
|
|
364
|
+
File.join(File.dirname(__FILE__), "..", "..", "setup-agent.rb"), # Relative to gem
|
|
365
|
+
File.join(Dir.pwd, "tinker-public", "setup-agent.rb"), # Running from metafolder
|
|
366
|
+
File.join(Dir.pwd, "setup-agent.rb"), # Running from yatfa-public
|
|
222
367
|
]
|
|
223
368
|
|
|
224
|
-
|
|
225
|
-
|
|
369
|
+
local_setup_script = nil
|
|
370
|
+
setup_script_paths.each do |path|
|
|
226
371
|
if File.exist?(path)
|
|
227
|
-
|
|
372
|
+
local_setup_script = path
|
|
228
373
|
break
|
|
229
374
|
end
|
|
230
375
|
end
|
|
231
376
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if File.exist?(local_setup_script)
|
|
377
|
+
# Mount local setup-agent.rb for development if present
|
|
378
|
+
# Binaries are always fetched from bucket by setup-agent.rb
|
|
379
|
+
if local_setup_script
|
|
236
380
|
puts "🔧 Using local setup-agent.rb for development"
|
|
237
381
|
docker_cmd.concat(["-v", "#{File.expand_path(local_setup_script)}:/tmp/setup-agent.rb:ro"])
|
|
238
382
|
end
|
|
239
|
-
|
|
240
|
-
if linux_bridge && File.exist?(linux_bridge)
|
|
241
|
-
puts "🔧 Using local linux binary: #{linux_bridge}"
|
|
242
|
-
docker_cmd.concat(["-v", "#{File.expand_path(linux_bridge)}:/tmp/agent-bridge:ro"])
|
|
243
|
-
elsif File.exist?(local_bridge_default)
|
|
244
|
-
# Check if it's a binary or script
|
|
245
|
-
is_script = File.read(local_bridge_default, 4) == "#!/b"
|
|
246
|
-
if is_script
|
|
247
|
-
puts "⚠️ bin/agent-bridge is a host wrapper script. Please run 'bin/build-bridge' to generate linux binaries."
|
|
248
|
-
else
|
|
249
|
-
puts "🔧 Using local agent-bridge binary"
|
|
250
|
-
docker_cmd.concat(["-v", "#{local_bridge_default}:/tmp/agent-bridge:ro"])
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
if File.exist?(local_tmux)
|
|
255
|
-
docker_cmd.concat(["-v", "#{File.expand_path(local_tmux)}:/tmp/agent-bridge-tmux:ro"])
|
|
256
|
-
end
|
|
257
383
|
end
|
|
258
384
|
|
|
259
385
|
def self.detect_agent_user(container_name)
|
|
@@ -4,17 +4,31 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module YatfaAgent
|
|
6
6
|
module Config
|
|
7
|
+
# Walk up the directory tree from +start_dir+ (default: Dir.pwd)
|
|
8
|
+
# looking for yatfa.env.rb. Returns the directory containing the
|
|
9
|
+
# config file, or nil if not found.
|
|
10
|
+
def self.find_project_root(start_dir: Dir.pwd)
|
|
11
|
+
dir = File.expand_path(start_dir)
|
|
12
|
+
previous = nil
|
|
13
|
+
|
|
14
|
+
while dir != previous
|
|
15
|
+
return dir if File.exist?(File.join(dir, "yatfa.env.rb"))
|
|
16
|
+
previous = dir
|
|
17
|
+
dir = File.dirname(dir)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
7
23
|
def self.load
|
|
8
|
-
|
|
24
|
+
config_file = File.join(Dir.pwd, "yatfa.env.rb")
|
|
9
25
|
|
|
10
|
-
unless File.exist?(
|
|
26
|
+
unless File.exist?(config_file)
|
|
11
27
|
puts "❌ Error: yatfa.env.rb not found in current directory"
|
|
12
28
|
puts ""
|
|
13
29
|
puts "Create yatfa.env.rb:"
|
|
14
30
|
puts " {"
|
|
15
31
|
puts " yatfa_url: 'https://yatfa.example.com', # Required"
|
|
16
|
-
puts " # OR legacy name:"
|
|
17
|
-
puts " rails_api_url: 'https://yatfa.example.com/api/v1',"
|
|
18
32
|
puts " # ..."
|
|
19
33
|
puts " # Paste your stripped .env content here:"
|
|
20
34
|
puts " dot_env: <<~ENV"
|
|
@@ -23,11 +37,11 @@ module YatfaAgent
|
|
|
23
37
|
puts " ENV"
|
|
24
38
|
puts " }"
|
|
25
39
|
puts " echo 'yatfa.env.rb' >> .gitignore"
|
|
26
|
-
|
|
40
|
+
exit 1
|
|
27
41
|
end
|
|
28
42
|
|
|
29
43
|
puts "⚙️ Loading configuration from yatfa.env.rb"
|
|
30
|
-
config = eval(File.read(
|
|
44
|
+
config = eval(File.read(config_file), binding, config_file)
|
|
31
45
|
|
|
32
46
|
# Convert symbols to strings for easier handling before JSON normalization
|
|
33
47
|
config = config.transform_keys(&:to_s)
|
|
@@ -69,8 +83,7 @@ module YatfaAgent
|
|
|
69
83
|
end
|
|
70
84
|
|
|
71
85
|
def self.yatfa_url(config)
|
|
72
|
-
|
|
73
|
-
config["yatfa_url"] || config["yafta_url"] || config["rails_api_url"]
|
|
86
|
+
config["yatfa_url"] || config["rails_api_url"]
|
|
74
87
|
end
|
|
75
88
|
|
|
76
89
|
def self.api_url(config)
|
package/lib/yatfa_agent/setup.rb
CHANGED
|
@@ -106,8 +106,13 @@ module YatfaAgent
|
|
|
106
106
|
|
|
107
107
|
@config[:agents] = {}
|
|
108
108
|
|
|
109
|
-
agent_types = ["worker", "planner", "reviewer", "researcher"]
|
|
109
|
+
agent_types = ["worker", "planner", "reviewer", "researcher", "claw"]
|
|
110
110
|
agent_types.each do |agent_type|
|
|
111
|
+
if agent_type == "claw"
|
|
112
|
+
prompt_claw_agent
|
|
113
|
+
next
|
|
114
|
+
end
|
|
115
|
+
|
|
111
116
|
print "#{agent_type.capitalize} API key (press Enter to skip): "
|
|
112
117
|
key = STDIN.noecho(&:gets).chomp.strip
|
|
113
118
|
puts ""
|
|
@@ -136,6 +141,43 @@ module YatfaAgent
|
|
|
136
141
|
end
|
|
137
142
|
end
|
|
138
143
|
|
|
144
|
+
def prompt_claw_agent
|
|
145
|
+
puts ""
|
|
146
|
+
puts "🐾 Claw Agent (OpenClaw Gateway)"
|
|
147
|
+
puts " This runs an OpenClaw gateway with Telegram integration."
|
|
148
|
+
puts " You'll need a Telegram Bot Token from @BotFather."
|
|
149
|
+
puts ""
|
|
150
|
+
|
|
151
|
+
print "Setup Claw agent? (press Enter to skip): "
|
|
152
|
+
confirm = Readline.readline.strip.downcase
|
|
153
|
+
return unless confirm == "y"
|
|
154
|
+
|
|
155
|
+
print "Telegram Bot Token: "
|
|
156
|
+
bot_token = STDIN.noecho(&:gets).chomp.strip
|
|
157
|
+
puts ""
|
|
158
|
+
|
|
159
|
+
if bot_token.empty?
|
|
160
|
+
puts "⊘ Skipped Claw agent (no token provided)"
|
|
161
|
+
return
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
agent_config = {
|
|
165
|
+
container_name: "#{@config[:project_name]}-claw",
|
|
166
|
+
telegram_bot_token: bot_token
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Optionally ask for MCP API key (for future Yatfa integration)
|
|
170
|
+
print "YATFA API key for MCP integration (optional, press Enter to skip): "
|
|
171
|
+
mcp_key = STDIN.noecho(&:gets).chomp.strip
|
|
172
|
+
puts ""
|
|
173
|
+
agent_config[:mcp_api_key] = mcp_key unless mcp_key.empty?
|
|
174
|
+
|
|
175
|
+
@config[:agents][:claw] = agent_config
|
|
176
|
+
|
|
177
|
+
puts "✅ Claw agent configured"
|
|
178
|
+
puts ""
|
|
179
|
+
end
|
|
180
|
+
|
|
139
181
|
def prompt_git_config
|
|
140
182
|
# Try to read from existing yatfa.env.rb
|
|
141
183
|
existing_config = File.join(Dir.pwd, "yatfa.env.rb")
|
|
@@ -373,8 +415,19 @@ module YatfaAgent
|
|
|
373
415
|
lines << " agents: {"
|
|
374
416
|
@config[:agents].each_with_index do |(agent_type, agent_config), idx|
|
|
375
417
|
lines << " #{agent_type}: {"
|
|
376
|
-
|
|
377
|
-
|
|
418
|
+
if agent_type.to_s == "claw"
|
|
419
|
+
lines << " container_name: #{agent_config[:container_name].inspect},"
|
|
420
|
+
lines << " telegram_bot_token: #{agent_config[:telegram_bot_token].inspect},"
|
|
421
|
+
lines << " env: {"
|
|
422
|
+
lines << " \"OPENCLAW_TELEGRAM_BOT_TOKEN\" => #{agent_config[:telegram_bot_token].inspect},"
|
|
423
|
+
if agent_config[:mcp_api_key]
|
|
424
|
+
lines << " \"YATFA_API_KEY\" => #{agent_config[:mcp_api_key].inspect},"
|
|
425
|
+
end
|
|
426
|
+
lines << " }"
|
|
427
|
+
else
|
|
428
|
+
lines << " mcp_api_key: #{agent_config[:mcp_api_key].inspect},"
|
|
429
|
+
lines << " container_name: #{agent_config[:container_name].inspect}"
|
|
430
|
+
end
|
|
378
431
|
lines << " }#{',' if idx < @config[:agents].size - 1}"
|
|
379
432
|
end
|
|
380
433
|
lines << " }"
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yatfa",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.68",
|
|
4
4
|
"description": "YATFA - Yet Another Tool For Agents",
|
|
5
|
-
"bin": "./bin/
|
|
5
|
+
"bin": "./bin/yatfa",
|
|
6
6
|
"files": [
|
|
7
|
+
"bin/yatfa",
|
|
7
8
|
"bin/run-yatfa-agent.rb",
|
|
8
9
|
"agents.rb",
|
|
9
10
|
"lib/",
|