yatfa 1.0.66 → 1.0.67

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
@@ -104,21 +104,34 @@ Launches interactive setup wizard to configure `yatfa.env.rb`.
104
104
 
105
105
  ### Run Agents
106
106
 
107
+ When you run an agent, the CLI automatically:
108
+ 1. **Checks if the agent is already running** - prompts to Attach, Restart, or Cancel
109
+ 2. **Starts the agent** - creates the Docker container
110
+ 3. **Prompts to attach** - after starting, asks if you want to attach to the session
111
+
107
112
  ```bash
108
- # Start an agent
109
113
  npx yatfa worker
110
114
  npx yatfa planner
111
115
  npx yatfa reviewer
112
116
  npx yatfa researcher
113
117
  ```
114
118
 
115
- ### Attach to Running Agents
119
+ #### Non-interactive Mode (for CI/scripts)
116
120
 
117
121
  ```bash
118
- npx yatfa attach worker
122
+ # Run without prompts
123
+ npx yatfa worker --no-prompt
124
+ # or
125
+ YATFA_NO_PROMPT=1 npx yatfa worker
119
126
  ```
120
127
 
121
- Opens tmux session to view agent activity.
128
+ ### Attach to Running Agents (deprecated)
129
+
130
+ The `attach` command still works but is deprecated. Running `npx yatfa <agent-type>` now prompts to attach automatically.
131
+
132
+ ```bash
133
+ npx yatfa attach worker # Deprecated - use 'npx yatfa worker' instead
134
+ ```
122
135
 
123
136
  ### List Running Agents
124
137
 
@@ -241,8 +254,10 @@ See hook files for implementation details and customization options.
241
254
 
242
255
  ## Attaching to Running Agent
243
256
 
257
+ After starting an agent, the CLI will prompt you to attach. You can also attach directly:
258
+
244
259
  ```bash
245
- docker exec -it <container> tmux attach -t agent-wrapper
260
+ docker exec -it <container> tmux attach -t agent
246
261
  ```
247
262
 
248
263
  Press `Ctrl+B` then `D` to detach.
@@ -3,7 +3,6 @@
3
3
 
4
4
  # YATFA Agent Runner (Yet Another Tool For Agents)
5
5
  # Usage: npx yatfa [worker|planner|reviewer|researcher]
6
- # npx yatfa attach [agent-type]
7
6
  # npx yatfa setup
8
7
  #
9
8
  # Requirements:
@@ -22,9 +21,12 @@ def show_usage
22
21
  puts "YATFA Agent Runner (Yet Another Tool For Agents)"
23
22
  puts ""
24
23
  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"
24
+ puts " npx yatfa setup # Interactive setup wizard"
25
+ puts " npx yatfa [worker|planner|reviewer|researcher] # Run agent (prompts to attach)"
26
+ puts ""
27
+ puts "Options:"
28
+ puts " --no-prompt, -d Run without prompts (for CI/scripts)"
29
+ puts " YATFA_NO_PROMPT=1 Environment variable alternative"
28
30
  puts ""
29
31
  puts "Setup:"
30
32
  puts " Run 'npx yatfa setup' for interactive configuration"
@@ -49,8 +51,12 @@ if command == "setup"
49
51
  exit 0
50
52
  end
51
53
 
52
- # Handle attach command
54
+ # Handle attach command (deprecated - will still work)
53
55
  if command == "attach"
56
+ puts ""
57
+ puts "⚠️ Deprecation: The 'attach' command is deprecated"
58
+ puts " Running `npx yatfa <agent-type>` now prompts to attach automatically."
59
+ puts ""
54
60
  agent_type = ARGV[1]&.downcase
55
61
  abort "Usage: npx yatfa attach [agent-type]" unless agent_type
56
62
  config = YatfaAgent::Config.load
@@ -3,6 +3,72 @@ require 'fileutils'
3
3
 
4
4
  module YatfaAgent
5
5
  module Agent
6
+ # Check if container is currently running
7
+ def self.container_running?(container_name)
8
+ # Use IO.popen with array arguments to avoid shell injection
9
+ result = IO.popen(["docker", "ps", "--filter", "name=^#{container_name}$", "--format", "{{.Names}}"], err: File::NULL, &:read).strip
10
+ !result.empty?
11
+ end
12
+
13
+ # Prompt user for input with a default value
14
+ def self.prompt(message, default: nil, options: nil)
15
+ loop do
16
+ print message
17
+ input = $stdin.gets&.strip&.downcase || ""
18
+
19
+ # Handle default on empty input
20
+ if input.empty? && default
21
+ return default
22
+ end
23
+
24
+ # If options specified, validate input
25
+ if options
26
+ valid = options.any? { |opt| opt.downcase == input || opt.downcase.start_with?(input) }
27
+ if valid
28
+ # Return the full option that matches
29
+ return options.find { |opt| opt.downcase.start_with?(input) }
30
+ end
31
+ puts " Invalid option. Please choose: #{options.join('/')}"
32
+ else
33
+ return input
34
+ end
35
+ end
36
+ end
37
+
38
+ # Check if running in non-interactive mode
39
+ def self.non_interactive?
40
+ !$stdin.tty? || ENV['YATFA_NO_PROMPT'] == '1' || ARGV.include?('--no-prompt') || ARGV.include?('-d')
41
+ end
42
+
43
+ # Handle running container: prompt user for action
44
+ def self.handle_running_container(agent_type, container_name, config, agent_configs)
45
+ puts "⚠️ #{agent_type} agent is already running (#{container_name})"
46
+ puts ""
47
+
48
+ if non_interactive?
49
+ puts " Running in non-interactive mode. Run 'npx yatfa #{agent_type}' to attach, or stop the container first."
50
+ puts " docker stop #{container_name}"
51
+ exit 0
52
+ end
53
+
54
+ choice = prompt(" [A]ttach / [R]estart / [C]ancel? [A/r/c]: ", default: "A", options: %w[A R C])
55
+
56
+ case choice
57
+ when "A"
58
+ puts ""
59
+ attach(agent_type, config, agent_configs)
60
+ when "R"
61
+ puts ""
62
+ puts "🔄 Stopping existing container..."
63
+ system("docker", "stop", container_name, err: File::NULL, out: File::NULL)
64
+ system("docker", "rm", "-f", container_name, err: File::NULL, out: File::NULL)
65
+ return true # Continue with start
66
+ when "C"
67
+ puts " Cancelled."
68
+ exit 0
69
+ end
70
+ end
71
+
6
72
  def self.run(agent_type, config, agent_configs)
7
73
  unless agent_configs.keys.include?(agent_type)
8
74
  puts "❌ Unknown agent type: #{agent_type}"
@@ -14,10 +80,15 @@ module YatfaAgent
14
80
  agent_config = config.dig("agents", agent_type) || {}
15
81
  container_name = agent_config["container_name"] || agent_def[:name]
16
82
 
17
- puts "🚀 Starting #{agent_type} agent..."
83
+ # Check if container is already running and handle interactively
84
+ if container_running?(container_name)
85
+ should_continue = handle_running_container(agent_type, container_name, config, agent_configs)
86
+ # If user chose Attach or Cancel, the method calls exec/exit, so we never reach here
87
+ # If user chose Restart, should_continue is true and we proceed to start a new container
88
+ return unless should_continue
89
+ end
18
90
 
19
- # Stop existing container if running
20
- system("docker", "rm", "-f", container_name, err: File::NULL, out: File::NULL)
91
+ puts "🚀 Starting #{agent_type} agent..."
21
92
 
22
93
  # Write banner to a persistent temp file (not auto-deleted)
23
94
  banner_path = "/tmp/yatfa-banner-#{agent_type}.txt"
@@ -47,15 +118,46 @@ module YatfaAgent
47
118
  if success
48
119
  puts "✅ Agent started in background"
49
120
  puts ""
50
- puts " Attach: npx yatfa attach #{agent_type}"
51
- puts " Logs: docker logs -f #{container_name}"
52
- puts " Stop: docker stop #{container_name}"
121
+
122
+ # Prompt to attach (interactive mode only)
123
+ prompt_to_attach(agent_type, container_name, config, agent_configs)
53
124
  else
54
125
  puts "❌ Failed to start agent"
55
126
  exit 1
56
127
  end
57
128
  end
58
129
 
130
+ # Prompt user to attach after starting a new agent
131
+ def self.prompt_to_attach(agent_type, container_name, config, agent_configs)
132
+ if non_interactive?
133
+ # Non-interactive: just print helpful info
134
+ puts " Logs: docker logs -f #{container_name}"
135
+ puts " Stop: docker stop #{container_name}"
136
+ puts " Attach: docker exec -it #{container_name} tmux attach -t agent"
137
+ return
138
+ end
139
+
140
+ choice = prompt(" Agent started. Attach to session? [Y/n]: ", default: "Y", options: %w[Y N])
141
+
142
+ if choice == "Y"
143
+ puts ""
144
+ # Wait briefly for container to initialize
145
+ print " Waiting for agent to initialize"
146
+ 5.times do
147
+ print "."
148
+ sleep 1
149
+ end
150
+ puts ""
151
+ attach(agent_type, config, agent_configs)
152
+ else
153
+ puts ""
154
+ puts " Agent is running in background."
155
+ puts " Logs: docker logs -f #{container_name}"
156
+ puts " Stop: docker stop #{container_name}"
157
+ puts " Attach: docker exec -it #{container_name} tmux attach -t agent"
158
+ end
159
+ end
160
+
59
161
  def self.attach(agent_type, config, agent_configs)
60
162
  unless agent_configs.keys.include?(agent_type)
61
163
  puts "❌ Unknown agent type: #{agent_type}"
@@ -66,7 +168,7 @@ module YatfaAgent
66
168
  agent_config = config.dig("agents", agent_type) || {}
67
169
  container_name = agent_config["container_name"] || agent_def[:name]
68
170
 
69
- running = `docker ps --filter name=^#{container_name}$ --format '{{.Names}}'`.strip
171
+ running = IO.popen(["docker", "ps", "--filter", "name=^#{container_name}$", "--format", "{{.Names}}"], err: File::NULL, &:read).strip
70
172
 
71
173
  if running.empty?
72
174
  puts "⚠️ #{agent_type} agent is not running. Auto-starting..."
@@ -208,52 +310,27 @@ module YatfaAgent
208
310
 
209
311
  def self.add_development_mounts!(docker_cmd)
210
312
  # 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
313
+ # Try multiple paths: relative to gem, relative to cwd
314
+ setup_script_paths = [
315
+ File.join(File.dirname(__FILE__), "..", "..", "setup-agent.rb"), # Relative to gem
316
+ File.join(Dir.pwd, "tinker-public", "setup-agent.rb"), # Running from metafolder
317
+ File.join(Dir.pwd, "setup-agent.rb"), # Running from yatfa-public
222
318
  ]
223
319
 
224
- linux_bridge = nil
225
- possible_paths.each do |path|
320
+ local_setup_script = nil
321
+ setup_script_paths.each do |path|
226
322
  if File.exist?(path)
227
- linux_bridge = path
323
+ local_setup_script = path
228
324
  break
229
325
  end
230
326
  end
231
327
 
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)
328
+ # Mount local setup-agent.rb for development if present
329
+ # Binaries are always fetched from bucket by setup-agent.rb
330
+ if local_setup_script
236
331
  puts "🔧 Using local setup-agent.rb for development"
237
332
  docker_cmd.concat(["-v", "#{File.expand_path(local_setup_script)}:/tmp/setup-agent.rb:ro"])
238
333
  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
334
  end
258
335
 
259
336
  def self.detect_agent_user(container_name)
@@ -5,16 +5,14 @@ require "json"
5
5
  module YatfaAgent
6
6
  module Config
7
7
  def self.load
8
- rb_config_file = File.join(Dir.pwd, "yatfa.env.rb")
8
+ config_file = File.join(Dir.pwd, "yatfa.env.rb")
9
9
 
10
- unless File.exist?(rb_config_file)
10
+ unless File.exist?(config_file)
11
11
  puts "❌ Error: yatfa.env.rb not found in current directory"
12
12
  puts ""
13
13
  puts "Create yatfa.env.rb:"
14
14
  puts " {"
15
15
  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
16
  puts " # ..."
19
17
  puts " # Paste your stripped .env content here:"
20
18
  puts " dot_env: <<~ENV"
@@ -23,11 +21,11 @@ module YatfaAgent
23
21
  puts " ENV"
24
22
  puts " }"
25
23
  puts " echo 'yatfa.env.rb' >> .gitignore"
26
- puts " exit 1"
24
+ exit 1
27
25
  end
28
26
 
29
27
  puts "⚙️ Loading configuration from yatfa.env.rb"
30
- config = eval(File.read(rb_config_file), binding, rb_config_file)
28
+ config = eval(File.read(config_file), binding, config_file)
31
29
 
32
30
  # Convert symbols to strings for easier handling before JSON normalization
33
31
  config = config.transform_keys(&:to_s)
@@ -69,8 +67,7 @@ module YatfaAgent
69
67
  end
70
68
 
71
69
  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"]
70
+ config["yatfa_url"] || config["rails_api_url"]
74
71
  end
75
72
 
76
73
  def self.api_url(config)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yatfa",
3
- "version": "1.0.66",
3
+ "version": "1.0.67",
4
4
  "description": "YATFA - Yet Another Tool For Agents",
5
5
  "bin": "./bin/run-yatfa-agent.rb",
6
6
  "files": [