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 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
- ### Attach to Running Agents
129
+ #### Non-interactive Mode (for CI/scripts)
116
130
 
117
131
  ```bash
118
- npx yatfa attach worker
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
- Opens tmux session to view agent activity.
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-wrapper
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
@@ -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 setup # Interactive setup wizard"
26
- puts " npx yatfa [worker|planner|reviewer|researcher] # Run agent"
27
- puts " npx yatfa attach [agent-type] # Attach to running agent"
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
- # Handle attach command
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # yatfa - Agent runner wrapper
3
+ # Sets process name to yatfa-<agent-type> so ps/top/Activity Monitor shows it clearly.
4
+ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ exec -a "yatfa-${1:-yatfa}" ruby "$DIR/run-yatfa-agent.rb" "$@"
@@ -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
- puts "🚀 Starting #{agent_type} agent..."
18
-
19
- # Stop existing container if running
20
- system("docker", "rm", "-f", container_name, err: File::NULL, out: File::NULL)
21
-
22
- # Write banner to a persistent temp file (not auto-deleted)
23
- banner_path = "/tmp/yatfa-banner-#{agent_type}.txt"
24
- FileUtils.rm_rf(banner_path)
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
- File.write(banner_path, banner_content)
132
+ puts "🚀 Starting #{agent_type} agent..."
42
133
 
43
- docker_cmd = build_docker_command(container_name, agent_type, config, agent_config, agent_def, banner_path)
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
- puts " Attach: npx yatfa attach #{agent_type}"
51
- puts " Logs: docker logs -f #{container_name}"
52
- puts " Stop: docker stop #{container_name}"
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 = `docker ps --filter name=^#{container_name}$ --format '{{.Names}}'`.strip
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, banner_path)
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
- docker_cmd += [
128
- "-v", "#{banner_path}:/etc/yatfa/system-prompt.txt:ro",
129
- "-e", "YATFA_VERSION=main",
130
- "-e", "SKILLS=#{agent_def[:skills]&.join(',')}",
131
- "-e", "AGENT_TYPE=#{agent_type}",
132
- "-e", "YATFA_URL=#{YatfaAgent::Config.yatfa_url(config)}",
133
- "-e", "YATFA_API_KEY=#{agent_config['mcp_api_key']}"
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
- add_github_auth!(docker_cmd, config)
137
- add_git_config!(docker_cmd, config)
138
- add_repository_mounts!(docker_cmd, config)
139
- add_development_mounts!(docker_cmd)
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
- local_setup_script = File.join(File.dirname(__FILE__), "..", "..", "setup-agent.rb")
212
-
213
- # Check for local agent-bridge binaries (for development)
214
- arch = `uname -m`.strip
215
- linux_arch = (arch == "x86_64") ? "amd64" : "arm64"
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
- linux_bridge = nil
225
- possible_paths.each do |path|
369
+ local_setup_script = nil
370
+ setup_script_paths.each do |path|
226
371
  if File.exist?(path)
227
- linux_bridge = path
372
+ local_setup_script = path
228
373
  break
229
374
  end
230
375
  end
231
376
 
232
- local_bridge_default = File.join(Dir.pwd, "bin", "agent-bridge")
233
- local_tmux = File.join(File.dirname(__FILE__), "..", "..", "bin", "agent-bridge-tmux")
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
- rb_config_file = File.join(Dir.pwd, "yatfa.env.rb")
24
+ config_file = File.join(Dir.pwd, "yatfa.env.rb")
9
25
 
10
- unless File.exist?(rb_config_file)
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
- puts " exit 1"
40
+ exit 1
27
41
  end
28
42
 
29
43
  puts "⚙️ Loading configuration from yatfa.env.rb"
30
- config = eval(File.read(rb_config_file), binding, rb_config_file)
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
- # Support both new name and legacy name for backwards compatibility
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)
@@ -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
- lines << " mcp_api_key: #{agent_config[:mcp_api_key].inspect},"
377
- lines << " container_name: #{agent_config[:container_name].inspect}"
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.66",
3
+ "version": "1.0.68",
4
4
  "description": "YATFA - Yet Another Tool For Agents",
5
- "bin": "./bin/run-yatfa-agent.rb",
5
+ "bin": "./bin/yatfa",
6
6
  "files": [
7
+ "bin/yatfa",
7
8
  "bin/run-yatfa-agent.rb",
8
9
  "agents.rb",
9
10
  "lib/",