yatfa 1.0.89 → 1.0.91
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/agents.rb +1 -1
- package/lib/yatfa_agent/agent.rb +12 -356
- package/lib/yatfa_agent/command_builder.rb +159 -0
- package/lib/yatfa_agent/container.rb +65 -0
- package/lib/yatfa_agent/interaction.rb +163 -0
- package/package.json +1 -1
- package/setup/agent_bridge.rb +228 -0
- package/setup/claude_settings.rb +217 -0
- package/setup/repositories.rb +98 -0
- package/setup/setup-agent.rb +25 -743
- package/setup/setup-github.rb +12 -4
- package/setup/skills.rb +136 -0
- package/setup/system_prompt.rb +110 -0
package/setup/setup-agent.rb
CHANGED
|
@@ -37,16 +37,17 @@ require "time"
|
|
|
37
37
|
require "socket"
|
|
38
38
|
require "timeout"
|
|
39
39
|
require_relative "setup-github"
|
|
40
|
+
require_relative "repositories"
|
|
41
|
+
require_relative "agent_bridge"
|
|
42
|
+
require_relative "claude_settings"
|
|
43
|
+
require_relative "system_prompt"
|
|
44
|
+
require_relative "skills"
|
|
40
45
|
|
|
41
46
|
SCRIPT_DIR = File.dirname(File.expand_path(__FILE__))
|
|
42
47
|
YATFA_VERSION = ENV["YATFA_VERSION"] || "main"
|
|
43
48
|
YATFA_SCRIPTS_BASE_URL = "https://objectstore.fra1.civo.com/yatfa"
|
|
44
49
|
YATFA_RAW_URL = "#{YATFA_SCRIPTS_BASE_URL}/#{YATFA_VERSION}"
|
|
45
50
|
|
|
46
|
-
# Helper scripts installed into /usr/local/bin by setup_helper_scripts!
|
|
47
|
-
# Single source of truth — deploy/deploy reads this list too.
|
|
48
|
-
HELPER_SCRIPTS = %w[fetch-skill-variant.sh fetch-review-context.sh attach-screenshot.sh].freeze
|
|
49
|
-
|
|
50
51
|
# Valid agent types
|
|
51
52
|
VALID_AGENT_TYPES = %w[planner worker reviewer researcher claw]
|
|
52
53
|
|
|
@@ -132,15 +133,15 @@ def fetch_agent_id!
|
|
|
132
133
|
request = Net::HTTP::Get.new(uri)
|
|
133
134
|
request["X-API-Key"] = rails_api_key
|
|
134
135
|
request["Accept"] = "application/json"
|
|
135
|
-
|
|
136
|
+
|
|
136
137
|
begin
|
|
137
138
|
puts "🔍 Fetching agent ID from #{rails_api_url}/whoami..."
|
|
138
139
|
response = http.request(request)
|
|
139
|
-
|
|
140
|
+
|
|
140
141
|
if response.is_a?(Net::HTTPSuccess)
|
|
141
142
|
data = JSON.parse(response.body)
|
|
142
143
|
agent_id = data["agent_id"] || data["id"]
|
|
143
|
-
|
|
144
|
+
|
|
144
145
|
if agent_id && !agent_id.to_s.empty?
|
|
145
146
|
ENV["AGENT_ID"] = agent_id.to_s
|
|
146
147
|
puts "✅ Agent ID fetched: #{agent_id}"
|
|
@@ -164,11 +165,11 @@ end
|
|
|
164
165
|
def setup_mcp_config!
|
|
165
166
|
# MCP config is project-specific and should be provided by the Dockerfile
|
|
166
167
|
# or mounted at runtime. This script only checks if it exists.
|
|
167
|
-
|
|
168
|
+
|
|
168
169
|
agent_type = ENV["AGENT_TYPE"]
|
|
169
170
|
rails_api_url = ENV["YATFA_API_URL"]
|
|
170
171
|
rails_api_key = ENV["YATFA_API_KEY"]
|
|
171
|
-
|
|
172
|
+
|
|
172
173
|
# Load existing config if present
|
|
173
174
|
existing_config = {}
|
|
174
175
|
if File.exist?(".mcp.json")
|
|
@@ -179,7 +180,7 @@ def setup_mcp_config!
|
|
|
179
180
|
puts "⚠️ Existing .mcp.json is invalid, starting fresh"
|
|
180
181
|
end
|
|
181
182
|
end
|
|
182
|
-
|
|
183
|
+
|
|
183
184
|
# Ensure mcpServers key exists
|
|
184
185
|
existing_config["mcpServers"] ||= {}
|
|
185
186
|
|
|
@@ -187,13 +188,13 @@ def setup_mcp_config!
|
|
|
187
188
|
# Install yatfa-mcp locally to ensure we can run it with node (bypassing shebang issues)
|
|
188
189
|
tools_dir = File.expand_path("~/yatfa-tools")
|
|
189
190
|
FileUtils.mkdir_p(tools_dir)
|
|
190
|
-
|
|
191
|
+
|
|
191
192
|
puts "📦 Installing yatfa-mcp..."
|
|
192
193
|
# Redirect output to avoid cluttering logs, unless it fails
|
|
193
194
|
unless system("npm install --prefix #{tools_dir} yatfa-mcp > /dev/null 2>&1")
|
|
194
195
|
puts "❌ Failed to install yatfa-mcp"
|
|
195
196
|
end
|
|
196
|
-
|
|
197
|
+
|
|
197
198
|
script_path = "#{tools_dir}/node_modules/yatfa-mcp/dist/index.js"
|
|
198
199
|
|
|
199
200
|
yatfa_server_config = {
|
|
@@ -204,10 +205,10 @@ def setup_mcp_config!
|
|
|
204
205
|
"YATFA_API_KEY" => rails_api_key
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
|
-
|
|
208
|
+
|
|
208
209
|
# Add/Update yatfa server config
|
|
209
210
|
existing_config["mcpServers"]["yatfa-#{agent_type}"] = yatfa_server_config
|
|
210
|
-
|
|
211
|
+
|
|
211
212
|
File.write(".mcp.json", JSON.pretty_generate(existing_config))
|
|
212
213
|
puts "📝 Updated .mcp.json with yatfa-#{agent_type} server (using yatfa-mcp)"
|
|
213
214
|
else
|
|
@@ -241,523 +242,15 @@ def setup_claude_config!
|
|
|
241
242
|
puts "✅ Created minimal claude.json with bypass permissions and workspace trust"
|
|
242
243
|
end
|
|
243
244
|
|
|
244
|
-
def fetch_llm_credentials!
|
|
245
|
-
rails_api_url = ENV["YATFA_API_URL"]
|
|
246
|
-
rails_api_key = ENV["YATFA_API_KEY"]
|
|
247
|
-
|
|
248
|
-
unless rails_api_url && !rails_api_url.empty? && rails_api_key && !rails_api_key.empty?
|
|
249
|
-
puts "❌ YATFA_API_URL or YATFA_API_KEY not set"
|
|
250
|
-
return nil
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
# Fetch LLM credentials from Rails API
|
|
254
|
-
uri = URI("#{rails_api_url}/llm/credentials")
|
|
255
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
256
|
-
http.use_ssl = uri.scheme == "https"
|
|
257
|
-
http.open_timeout = 10
|
|
258
|
-
http.read_timeout = 10
|
|
259
|
-
|
|
260
|
-
request = Net::HTTP::Get.new(uri)
|
|
261
|
-
request["X-API-Key"] = rails_api_key
|
|
262
|
-
request["Accept"] = "application/json"
|
|
263
|
-
|
|
264
|
-
begin
|
|
265
|
-
puts "🔍 Fetching LLM credentials from #{rails_api_url}/llm/credentials..."
|
|
266
|
-
response = http.request(request)
|
|
267
|
-
|
|
268
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
269
|
-
data = JSON.parse(response.body)
|
|
270
|
-
puts "✅ LLM credentials fetched successfully"
|
|
271
|
-
data
|
|
272
|
-
elsif response.is_a?(Net::HTTPNotFound)
|
|
273
|
-
puts "❌ No LLM credentials configured in backend"
|
|
274
|
-
nil
|
|
275
|
-
else
|
|
276
|
-
puts "❌ Failed to fetch LLM credentials: #{response.code} #{response.message}"
|
|
277
|
-
puts " Response: #{response.body}"
|
|
278
|
-
nil
|
|
279
|
-
end
|
|
280
|
-
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
281
|
-
puts "❌ Timeout fetching LLM credentials: #{e.message}"
|
|
282
|
-
nil
|
|
283
|
-
rescue => e
|
|
284
|
-
puts "❌ Error fetching LLM credentials: #{e.message}"
|
|
285
|
-
nil
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def setup_claude_settings!
|
|
290
|
-
settings_dir = File.expand_path("~/.claude")
|
|
291
|
-
FileUtils.mkdir_p(settings_dir)
|
|
292
|
-
home_settings_json = File.join(settings_dir, "settings.json")
|
|
293
|
-
|
|
294
|
-
puts "🔧 Configuring ~/.claude/settings.json..."
|
|
295
|
-
|
|
296
|
-
settings_config = {}
|
|
297
|
-
|
|
298
|
-
# Fetch LLM credentials from Rails backend (BYOK support)
|
|
299
|
-
llm_credentials = fetch_llm_credentials!
|
|
300
|
-
|
|
301
|
-
if llm_credentials
|
|
302
|
-
puts "✅ Fetched LLM credentials from backend (source: #{llm_credentials['source']})"
|
|
303
|
-
|
|
304
|
-
# Configure Claude CLI with LLM provider environment variables
|
|
305
|
-
settings_config["env"] ||= {}
|
|
306
|
-
settings_config["env"]["ANTHROPIC_AUTH_TOKEN"] = llm_credentials["api_key"]
|
|
307
|
-
|
|
308
|
-
if llm_credentials["base_url"] && !llm_credentials["base_url"].empty?
|
|
309
|
-
settings_config["env"]["ANTHROPIC_BASE_URL"] = llm_credentials["base_url"]
|
|
310
|
-
puts " Custom base URL: #{llm_credentials['base_url']}"
|
|
311
|
-
else
|
|
312
|
-
settings_config["env"].delete("ANTHROPIC_BASE_URL") if settings_config["env"]["ANTHROPIC_BASE_URL"]
|
|
313
|
-
puts " Using default Anthropic base URL"
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
# Configure model selection if specified
|
|
317
|
-
# Each model tier can be configured independently
|
|
318
|
-
%w[haiku sonnet opus].each do |tier|
|
|
319
|
-
key = "model_#{tier}"
|
|
320
|
-
if llm_credentials[key] && !llm_credentials[key].empty?
|
|
321
|
-
settings_config["env"]["ANTHROPIC_DEFAULT_#{tier.upcase}_MODEL"] = llm_credentials[key]
|
|
322
|
-
puts " #{tier.capitalize} model: #{llm_credentials[key]}"
|
|
323
|
-
end
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
# Configure context window if specified (value is in k-units, append 'k' suffix)
|
|
327
|
-
if llm_credentials["context_window"] && !llm_credentials["context_window"].to_s.empty?
|
|
328
|
-
context_window_value = llm_credentials["context_window"].to_i
|
|
329
|
-
if context_window_value > 0
|
|
330
|
-
settings_config["env"]["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = "#{context_window_value}000"
|
|
331
|
-
puts " Context window: #{context_window_value}k tokens"
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
unless %w[haiku sonnet opus].any? { |tier| llm_credentials["model_#{tier}"] }
|
|
336
|
-
puts " Using default Anthropic models"
|
|
337
|
-
end
|
|
338
|
-
else
|
|
339
|
-
puts "❌ No LLM credentials found in backend!"
|
|
340
|
-
puts " Please configure LLM API key for this agent or its project in the Yatfa dashboard."
|
|
341
|
-
puts " Agent will fail to start without credentials."
|
|
342
|
-
exit 1
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
# No settings loaded from host - agent is fully self-contained
|
|
346
|
-
|
|
347
|
-
# Configure hooks
|
|
348
|
-
settings_config["hooks"] ||= {}
|
|
349
|
-
hooks_dir = File.join(settings_dir, "hooks")
|
|
350
|
-
FileUtils.mkdir_p(hooks_dir)
|
|
351
|
-
|
|
352
|
-
# SessionStart hook: skill knowledge injection
|
|
353
|
-
install_hook!(
|
|
354
|
-
settings_config, hooks_dir,
|
|
355
|
-
hook_type: "SessionStart",
|
|
356
|
-
script_name: "inject-skill-knowledge.rb",
|
|
357
|
-
match_substring: "inject-skill-knowledge.rb",
|
|
358
|
-
command_prefix: "ruby"
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
# Install sessionstart-input.sh for repository context injection
|
|
362
|
-
download_file(
|
|
363
|
-
script_name: "sessionstart-input.sh",
|
|
364
|
-
remote_url: "#{YATFA_RAW_URL}/hooks/sessionstart-input.sh",
|
|
365
|
-
dest_dir: hooks_dir,
|
|
366
|
-
label: "SessionStart input hook"
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
# Add agent-specific settings
|
|
370
|
-
settings_config["skipDangerousModePermissionPrompt"] = true
|
|
371
|
-
|
|
372
|
-
# Stop hook: session-end learning capture
|
|
373
|
-
install_hook!(
|
|
374
|
-
settings_config, hooks_dir,
|
|
375
|
-
hook_type: "Stop",
|
|
376
|
-
script_name: "sessionend-output.sh",
|
|
377
|
-
match_substring: "sessionend-output.sh",
|
|
378
|
-
command_prefix: "bash"
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
# Download and register project hooks wrapper
|
|
382
|
-
# The wrapper fetches user-defined hooks from the API at runtime (with caching),
|
|
383
|
-
# executes them, and reports results back for observability.
|
|
384
|
-
setup_project_hooks_wrapper!(settings_config, hooks_dir)
|
|
385
|
-
|
|
386
|
-
File.write(home_settings_json, JSON.pretty_generate(settings_config))
|
|
387
|
-
puts "✅ ~/.claude/settings.json configured with skill hooks"
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
# Install a Claude settings hook by downloading the script and registering it.
|
|
391
|
-
#
|
|
392
|
-
# Handles the common pattern: ensure hook array → remove old entry → download → append.
|
|
393
|
-
#
|
|
394
|
-
# @param settings_config [Hash] the settings.json hash being built
|
|
395
|
-
# @param hooks_dir [String] directory to download hook scripts into
|
|
396
|
-
# @param hook_type [String] Claude hook type (e.g. "SessionStart", "Stop")
|
|
397
|
-
# @param script_name [String] filename of the hook script to download
|
|
398
|
-
# @param match_substring [String] substring to match when removing old hook entries
|
|
399
|
-
# @param command_prefix [String] command prefix ("ruby" or "bash")
|
|
400
|
-
def install_hook!(settings_config, hooks_dir, hook_type:, script_name:, match_substring:, command_prefix:)
|
|
401
|
-
settings_config["hooks"][hook_type] ||= []
|
|
402
|
-
# Remove old hook entry if present (idempotent re-runs)
|
|
403
|
-
settings_config["hooks"][hook_type].reject! do |h|
|
|
404
|
-
h["hooks"]&.first&.dig("command")&.include?(match_substring)
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
dest_path = download_file(
|
|
408
|
-
script_name: script_name,
|
|
409
|
-
remote_url: "#{YATFA_RAW_URL}/hooks/#{script_name}",
|
|
410
|
-
dest_dir: hooks_dir,
|
|
411
|
-
label: "#{hook_type} hook"
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
if dest_path
|
|
415
|
-
settings_config["hooks"][hook_type] << {
|
|
416
|
-
"hooks" => [{ "type" => "command", "command" => "#{command_prefix} \"#{dest_path}\"" }]
|
|
417
|
-
}
|
|
418
|
-
else
|
|
419
|
-
puts "⚠️ Skipping #{hook_type} hook configuration (download failed)"
|
|
420
|
-
end
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
# Download and register the project hooks wrapper as a SessionStart hook.
|
|
424
|
-
# The wrapper handles fetching user-defined hooks from the API at runtime,
|
|
425
|
-
# executing them, and reporting results. Hook content is managed by users
|
|
426
|
-
# via the Yatfa config UI and stored as ProjectConfig records.
|
|
427
|
-
def setup_project_hooks_wrapper!(settings_config, hooks_dir)
|
|
428
|
-
wrapper_path = download_file(
|
|
429
|
-
script_name: "project-hooks-wrapper.rb",
|
|
430
|
-
remote_url: "#{YATFA_RAW_URL}/hooks/project-hooks-wrapper.rb",
|
|
431
|
-
dest_dir: hooks_dir,
|
|
432
|
-
label: "Project hooks wrapper"
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
if wrapper_path
|
|
436
|
-
settings_config["hooks"]["SessionStart"] << {
|
|
437
|
-
"hooks" => [{ "type" => "command", "command" => "ruby \"#{wrapper_path}\"" }]
|
|
438
|
-
}
|
|
439
|
-
puts "✅ Registered project hooks wrapper"
|
|
440
|
-
else
|
|
441
|
-
puts "⚠️ Skipping project hooks wrapper (download failed)"
|
|
442
|
-
end
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
def setup_system_prompt!
|
|
446
|
-
agent_type = ENV["AGENT_TYPE"]
|
|
447
|
-
system_prompt_path = File.expand_path("~/.yatfa/system-prompt.txt")
|
|
448
|
-
|
|
449
|
-
rails_api_url = ENV["YATFA_API_URL"]
|
|
450
|
-
rails_api_key = ENV["YATFA_API_KEY"]
|
|
451
|
-
|
|
452
|
-
unless rails_api_url && !rails_api_url.empty? && rails_api_key && !rails_api_key.empty?
|
|
453
|
-
puts "❌ YATFA_API_URL or YATFA_API_KEY not set"
|
|
454
|
-
puts " Cannot fetch system prompt without API credentials"
|
|
455
|
-
exit 1
|
|
456
|
-
end
|
|
457
|
-
|
|
458
|
-
# Fetch system prompt from yatfa API
|
|
459
|
-
# Agent prompts are stored in yatfa/prompts/<agent-type>/base.md
|
|
460
|
-
# and loaded into the database by AgentPromptLoader
|
|
461
|
-
prompt_url = URI("#{rails_api_url}/internal/skill?tags%5B%5D=#{agent_type}&tags%5B%5D=base")
|
|
462
|
-
http = Net::HTTP.new(prompt_url.host, prompt_url.port)
|
|
463
|
-
http.use_ssl = prompt_url.scheme == "https"
|
|
464
|
-
http.open_timeout = 10
|
|
465
|
-
http.read_timeout = 15
|
|
466
|
-
|
|
467
|
-
request = Net::HTTP::Get.new(prompt_url)
|
|
468
|
-
request["X-API-Key"] = rails_api_key
|
|
469
|
-
request["Accept"] = "application/json"
|
|
470
|
-
|
|
471
|
-
banner_content = nil
|
|
472
|
-
begin
|
|
473
|
-
puts "📥 Fetching system prompt for #{agent_type}..."
|
|
474
|
-
response = http.request(request)
|
|
475
|
-
|
|
476
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
477
|
-
data = JSON.parse(response.body)
|
|
478
|
-
banner_content = data["content"]
|
|
479
|
-
|
|
480
|
-
unless banner_content && !banner_content.empty?
|
|
481
|
-
puts "❌ System prompt for '#{agent_type}' is empty"
|
|
482
|
-
exit 1
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
puts "✅ Fetched system prompt for #{agent_type}"
|
|
486
|
-
else
|
|
487
|
-
puts "❌ Failed to fetch system prompt: #{response.code} #{response.message}"
|
|
488
|
-
puts " Response: #{response.body}"
|
|
489
|
-
puts " Make sure AgentPromptLoader has loaded prompts/ directory"
|
|
490
|
-
exit 1
|
|
491
|
-
end
|
|
492
|
-
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
493
|
-
puts "❌ Timeout fetching system prompt: #{e.message}"
|
|
494
|
-
exit 1
|
|
495
|
-
rescue => e
|
|
496
|
-
puts "❌ Error fetching system prompt: #{e.message}"
|
|
497
|
-
exit 1
|
|
498
|
-
end
|
|
499
|
-
|
|
500
|
-
# Append repository context from REPOSITORIES_CONTEXT env var
|
|
501
|
-
if ENV["REPOSITORIES_CONTEXT"] && !ENV["REPOSITORIES_CONTEXT"].empty?
|
|
502
|
-
repos = ENV["REPOSITORIES_CONTEXT"].split(',').map do |repo|
|
|
503
|
-
name, path = repo.split(':')
|
|
504
|
-
{ name: name, path: path || "/workspace/#{name}" }
|
|
505
|
-
end
|
|
506
|
-
|
|
507
|
-
repo_info = repos.map { |r| " - **#{r[:name]}**: #{r[:path]}" }.join("\n")
|
|
508
|
-
|
|
509
|
-
banner_content += "\n\n## REPOSITORY STRUCTURE\n\n"
|
|
510
|
-
banner_content += "This project has #{repos.count} repositories:\n\n"
|
|
511
|
-
banner_content += "#{repo_info}\n\n"
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
# Ensure directory exists and write system prompt
|
|
515
|
-
FileUtils.mkdir_p(File.dirname(system_prompt_path))
|
|
516
|
-
File.write(system_prompt_path, banner_content)
|
|
517
|
-
puts "✅ System prompt written to #{system_prompt_path} (agent: #{agent_type})"
|
|
518
|
-
|
|
519
|
-
# Add repository context to CLAUDE.md for easy reference
|
|
520
|
-
claude_md = File.expand_path("~/CLAUDE.md")
|
|
521
|
-
if ENV["REPOSITORIES_CONTEXT"] && !ENV["REPOSITORIES_CONTEXT"].empty?
|
|
522
|
-
repo_info = ENV["REPOSITORIES_CONTEXT"].split(',').map do |repo|
|
|
523
|
-
name, path = repo.split(':')
|
|
524
|
-
" - **#{name}**: #{path}"
|
|
525
|
-
end.join("\n")
|
|
526
|
-
|
|
527
|
-
repo_context_md = <<~MARKDOWN
|
|
528
|
-
## Repository Structure
|
|
529
|
-
|
|
530
|
-
This project has #{ENV["REPOSITORIES_CONTEXT"].split(',').count} repositories:
|
|
531
|
-
|
|
532
|
-
#{repo_info}
|
|
533
|
-
MARKDOWN
|
|
534
|
-
|
|
535
|
-
# Append to existing CLAUDE.md or create new one
|
|
536
|
-
existing_content = File.exist?(claude_md) ? File.read(claude_md) : ""
|
|
537
|
-
File.write(claude_md, existing_content + "\n" + repo_context_md)
|
|
538
|
-
puts "📝 Repository context added to ~/CLAUDE.md"
|
|
539
|
-
end
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
def setup_skills!
|
|
544
|
-
skills = ENV["SKILLS"].to_s.split(",")
|
|
545
|
-
return if skills.empty?
|
|
546
|
-
|
|
547
|
-
skills_dir = File.expand_path("~/.claude/skills")
|
|
548
|
-
puts "🧠 Installing #{skills.size} skills to #{skills_dir}..."
|
|
549
|
-
FileUtils.mkdir_p(skills_dir)
|
|
550
|
-
|
|
551
|
-
skills.each do |skill|
|
|
552
|
-
puts " - #{skill}"
|
|
553
|
-
|
|
554
|
-
url = "#{YATFA_RAW_URL}/skills/#{skill}/SKILL.md"
|
|
555
|
-
|
|
556
|
-
begin
|
|
557
|
-
skill_content = URI.open(url).read
|
|
558
|
-
puts " (from remote: #{url})"
|
|
559
|
-
|
|
560
|
-
# Write to .claude/skills/[skill]/SKILL.md with proper structure
|
|
561
|
-
skill_dir = File.join(skills_dir, skill)
|
|
562
|
-
FileUtils.mkdir_p(skill_dir)
|
|
563
|
-
File.write(File.join(skill_dir, "SKILL.md"), skill_content)
|
|
564
|
-
rescue OpenURI::HTTPError, Errno::ENOENT => e
|
|
565
|
-
puts "⚠️ Failed to download skill #{skill}: #{e.message}"
|
|
566
|
-
end
|
|
567
|
-
end
|
|
568
|
-
|
|
569
|
-
puts "✅ Skills installed"
|
|
570
|
-
end
|
|
571
|
-
|
|
572
|
-
# Community skill source registry
|
|
573
|
-
# Maps skill name to its download source configuration
|
|
574
|
-
COMMUNITY_SKILL_SOURCES = {
|
|
575
|
-
"ui-ux-pro-max" => {
|
|
576
|
-
github_repo: "nextlevelbuilder/ui-ux-pro-max-skill",
|
|
577
|
-
skill_path: ".claude/skills/ui-ux-pro-max/SKILL.md"
|
|
578
|
-
}
|
|
579
|
-
}.freeze
|
|
580
|
-
|
|
581
|
-
# Fetch community skills assigned to this agent from the backend API
|
|
582
|
-
def fetch_community_skills!
|
|
583
|
-
rails_api_url = ENV["YATFA_API_URL"]
|
|
584
|
-
rails_api_key = ENV["YATFA_API_KEY"]
|
|
585
|
-
|
|
586
|
-
unless rails_api_url && !rails_api_url.empty? && rails_api_key && !rails_api_key.empty?
|
|
587
|
-
puts "⚠️ Cannot fetch community skills: API URL or key not set"
|
|
588
|
-
return []
|
|
589
|
-
end
|
|
590
|
-
|
|
591
|
-
uri = URI("#{rails_api_url}/whoami")
|
|
592
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
593
|
-
http.use_ssl = uri.scheme == "https"
|
|
594
|
-
|
|
595
|
-
request = Net::HTTP::Get.new(uri)
|
|
596
|
-
request["X-API-Key"] = rails_api_key
|
|
597
|
-
request["Accept"] = "application/json"
|
|
598
|
-
|
|
599
|
-
begin
|
|
600
|
-
response = http.request(request)
|
|
601
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
602
|
-
data = JSON.parse(response.body)
|
|
603
|
-
skills = data["community_skills"] || []
|
|
604
|
-
if skills.any?
|
|
605
|
-
puts "📦 Found #{skills.size} community skills: #{skills.join(', ')}"
|
|
606
|
-
end
|
|
607
|
-
skills
|
|
608
|
-
else
|
|
609
|
-
puts "⚠️ Could not fetch community skills: #{response.code}"
|
|
610
|
-
[]
|
|
611
|
-
end
|
|
612
|
-
rescue => e
|
|
613
|
-
puts "⚠️ Error fetching community skills: #{e.message}"
|
|
614
|
-
[]
|
|
615
|
-
end
|
|
616
|
-
end
|
|
617
|
-
|
|
618
|
-
# Install community skills from external sources (e.g., GitHub repos)
|
|
619
|
-
def setup_community_skills!
|
|
620
|
-
# Community skills can come from two sources:
|
|
621
|
-
# 1. COMMUNITY_SKILLS env var (set during container creation from agent config)
|
|
622
|
-
# 2. Fetched from the backend API (whoami response)
|
|
623
|
-
env_skills = ENV["COMMUNITY_SKILLS"].to_s.split(",").map(&:strip).reject(&:blank?)
|
|
624
|
-
api_skills = fetch_community_skills!
|
|
625
|
-
community_skills = (env_skills + api_skills).uniq
|
|
626
|
-
|
|
627
|
-
return if community_skills.empty?
|
|
628
|
-
|
|
629
|
-
skills_dir = File.expand_path("~/.claude/skills")
|
|
630
|
-
puts "🌐 Installing #{community_skills.size} community skills..."
|
|
631
|
-
FileUtils.mkdir_p(skills_dir)
|
|
632
|
-
|
|
633
|
-
community_skills.each do |skill_name|
|
|
634
|
-
puts " - #{skill_name}"
|
|
635
|
-
|
|
636
|
-
source = COMMUNITY_SKILL_SOURCES[skill_name]
|
|
637
|
-
unless source
|
|
638
|
-
puts "⚠️ Unknown community skill: #{skill_name} (no source configured)"
|
|
639
|
-
next
|
|
640
|
-
end
|
|
641
|
-
|
|
642
|
-
begin
|
|
643
|
-
# Download SKILL.md from GitHub raw URL
|
|
644
|
-
github_repo = source[:github_repo]
|
|
645
|
-
skill_path = source[:skill_path] || ".claude/skills/#{skill_name}/SKILL.md"
|
|
646
|
-
url = "https://raw.githubusercontent.com/#{github_repo}/main/#{skill_path}"
|
|
647
|
-
|
|
648
|
-
skill_content = URI.open(url, open_timeout: 15, read_timeout: 30).read
|
|
649
|
-
puts " (from GitHub: #{github_repo})"
|
|
650
|
-
|
|
651
|
-
# Write to .claude/skills/[skill_name]/SKILL.md
|
|
652
|
-
skill_dir = File.join(skills_dir, skill_name)
|
|
653
|
-
FileUtils.mkdir_p(skill_dir)
|
|
654
|
-
File.write(File.join(skill_dir, "SKILL.md"), skill_content)
|
|
655
|
-
rescue OpenURI::HTTPError, Errno::ENOENT => e
|
|
656
|
-
puts "⚠️ Failed to download community skill #{skill_name}: #{e.message}"
|
|
657
|
-
rescue => e
|
|
658
|
-
puts "⚠️ Error installing community skill #{skill_name}: #{e.message}"
|
|
659
|
-
end
|
|
660
|
-
end
|
|
661
|
-
|
|
662
|
-
puts "✅ Community skills installed"
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
def clone_repositories!
|
|
666
|
-
return unless ENV["REPOSITORIES_CONFIG"]
|
|
667
|
-
|
|
668
|
-
begin
|
|
669
|
-
repos = JSON.parse(ENV["REPOSITORIES_CONFIG"])
|
|
670
|
-
return unless repos.is_a?(Array) && repos.any?
|
|
671
|
-
|
|
672
|
-
puts "📚 Cloning #{repos.count} repositories..."
|
|
673
|
-
|
|
674
|
-
repos.each do |repo|
|
|
675
|
-
repo_name = repo["name"]
|
|
676
|
-
github_url = repo["github_url"]
|
|
677
|
-
sandbox_path = repo["path_in_sandbox"] || "/workspace/#{repo_name}"
|
|
678
|
-
|
|
679
|
-
# Create parent directory if needed
|
|
680
|
-
parent_dir = File.dirname(sandbox_path)
|
|
681
|
-
FileUtils.mkdir_p(parent_dir) unless File.exist?(parent_dir)
|
|
682
|
-
|
|
683
|
-
# Clone repository if it doesn't exist
|
|
684
|
-
# Do NOT use --depth to allow full branch access for PR workflows
|
|
685
|
-
unless File.exist?(sandbox_path)
|
|
686
|
-
puts " 📥 Cloning #{repo_name} to #{sandbox_path}..."
|
|
687
|
-
clone_timeout = ENV.fetch("GIT_CLONE_TIMEOUT", "180").to_i
|
|
688
|
-
clone_success = begin
|
|
689
|
-
Timeout.timeout(clone_timeout) do
|
|
690
|
-
system("git", "clone", github_url, sandbox_path)
|
|
691
|
-
end
|
|
692
|
-
rescue Timeout::Error
|
|
693
|
-
# Kill orphan git clone subprocess (Timeout.timeout doesn't kill children)
|
|
694
|
-
system("pkill", "-f", "git clone #{github_url}", err: File::NULL)
|
|
695
|
-
# Clean up partial clone directory so restart doesn't skip it
|
|
696
|
-
FileUtils.rm_rf(sandbox_path) if File.exist?(sandbox_path)
|
|
697
|
-
puts "❌ git clone of #{repo_name} timed out after #{clone_timeout}s"
|
|
698
|
-
puts " The container will restart automatically."
|
|
699
|
-
exit 1
|
|
700
|
-
end
|
|
701
|
-
|
|
702
|
-
unless clone_success
|
|
703
|
-
puts "❌ git clone of #{repo_name} failed"
|
|
704
|
-
puts " The container will restart automatically."
|
|
705
|
-
exit 1
|
|
706
|
-
end
|
|
707
|
-
else
|
|
708
|
-
puts " ✓ #{repo_name} already exists at #{sandbox_path}"
|
|
709
|
-
# Fetch all remotes to ensure branches are up to date
|
|
710
|
-
Dir.chdir(sandbox_path) do
|
|
711
|
-
system("git", "fetch", "--all")
|
|
712
|
-
puts " ✓ Fetched all remotes for #{repo_name}"
|
|
713
|
-
end
|
|
714
|
-
end
|
|
715
|
-
|
|
716
|
-
# Add repository path to git safe.directory to avoid "dubious ownership" errors
|
|
717
|
-
system("git", "config", "--global", "--add", "safe.directory", sandbox_path)
|
|
718
|
-
puts " ✓ Added #{sandbox_path} to git safe.directory"
|
|
719
|
-
end
|
|
720
|
-
|
|
721
|
-
puts "✅ Repositories cloned"
|
|
722
|
-
|
|
723
|
-
# Track if we have a single repository for later use
|
|
724
|
-
single_repo_name = repos.count == 1 ? repos.first['name'] : nil
|
|
725
|
-
|
|
726
|
-
# Write repository context to a file for hooks to read
|
|
727
|
-
# This is more reliable than environment variables in tmux sessions
|
|
728
|
-
if ENV["REPOSITORIES_CONTEXT"]
|
|
729
|
-
repo_context_file = File.expand_path("~/.claude/repos_context.txt")
|
|
730
|
-
File.write(repo_context_file, ENV["REPOSITORIES_CONTEXT"])
|
|
731
|
-
puts "📝 Repository context written to #{repo_context_file}"
|
|
732
|
-
end
|
|
733
|
-
|
|
734
|
-
# Export REPOSITORIES_CONTEXT to bash profile for shell sessions
|
|
735
|
-
if ENV["REPOSITORIES_CONTEXT"]
|
|
736
|
-
profile_file = File.expand_path("~/.bashrc")
|
|
737
|
-
existing_content = File.exist?(profile_file) ? File.read(profile_file) : ""
|
|
738
|
-
|
|
739
|
-
# Remove old export line if present
|
|
740
|
-
existing_content.gsub!(/^export REPOSITORIES_CONTEXT=.*$/, "")
|
|
741
|
-
|
|
742
|
-
# Add new export at the end
|
|
743
|
-
additions = "\nexport REPOSITORIES_CONTEXT=\"#{ENV['REPOSITORIES_CONTEXT']}\"\n"
|
|
744
|
-
File.write(profile_file, existing_content + additions)
|
|
745
|
-
puts "📝 REPOSITORIES_CONTEXT exported to ~/.bashrc"
|
|
746
|
-
end
|
|
747
|
-
rescue JSON::ParserError => e
|
|
748
|
-
puts "⚠️ Failed to parse REPOSITORIES_CONFIG: #{e.message}"
|
|
749
|
-
end
|
|
750
|
-
end
|
|
751
|
-
|
|
752
245
|
def setup_git_config!
|
|
753
246
|
# Configure identity if provided
|
|
754
247
|
if ENV["GIT_USER_NAME"] && !ENV["GIT_USER_NAME"].empty?
|
|
755
|
-
system("git config --global user.name
|
|
248
|
+
system("git", "config", "--global", "user.name", ENV["GIT_USER_NAME"])
|
|
756
249
|
puts "✅ Git user.name configured"
|
|
757
250
|
end
|
|
758
251
|
|
|
759
252
|
if ENV["GIT_USER_EMAIL"] && !ENV["GIT_USER_EMAIL"].empty?
|
|
760
|
-
system("git config --global user.email
|
|
253
|
+
system("git", "config", "--global", "user.email", ENV["GIT_USER_EMAIL"])
|
|
761
254
|
puts "✅ Git user.email configured"
|
|
762
255
|
end
|
|
763
256
|
|
|
@@ -794,211 +287,6 @@ def setup_git_hooks!
|
|
|
794
287
|
puts "✅ Git hooks installed"
|
|
795
288
|
end
|
|
796
289
|
|
|
797
|
-
def download_agent_bridge!
|
|
798
|
-
# Detect architecture
|
|
799
|
-
arch = `uname -m`.strip
|
|
800
|
-
if arch == "x86_64"
|
|
801
|
-
arch = "amd64"
|
|
802
|
-
elsif arch == "aarch64" || arch == "arm64"
|
|
803
|
-
arch = "arm64"
|
|
804
|
-
else
|
|
805
|
-
puts "❌ Unsupported architecture: #{arch}"
|
|
806
|
-
exit 1
|
|
807
|
-
end
|
|
808
|
-
|
|
809
|
-
target_dir = "/usr/local/bin"
|
|
810
|
-
|
|
811
|
-
# 1. Check for local override in /tmp/overrides (development mode)
|
|
812
|
-
override_dir = "/tmp/overrides"
|
|
813
|
-
if Dir.exist?(override_dir)
|
|
814
|
-
local_override = File.join(override_dir, "agent-bridge-linux-#{arch}")
|
|
815
|
-
if File.exist?(local_override)
|
|
816
|
-
puts "🔧 Using local override: #{local_override}"
|
|
817
|
-
system("sudo cp #{local_override} #{target_dir}/agent-bridge")
|
|
818
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
819
|
-
puts "✅ agent-bridge installed from local override"
|
|
820
|
-
return download_agent_bridge_tmux!(target_dir)
|
|
821
|
-
end
|
|
822
|
-
end
|
|
823
|
-
|
|
824
|
-
# 2. Check if binaries are mounted at /tmp (dev mode)
|
|
825
|
-
if File.exist?("/tmp/agent-bridge")
|
|
826
|
-
puts "🔧 Installing mounted agent-bridge..."
|
|
827
|
-
system("sudo cp /tmp/agent-bridge #{target_dir}/agent-bridge")
|
|
828
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
829
|
-
puts "✅ agent-bridge installed from mount"
|
|
830
|
-
return download_agent_bridge_tmux!(target_dir)
|
|
831
|
-
end
|
|
832
|
-
|
|
833
|
-
# 3. Try Civo bucket download (production mode)
|
|
834
|
-
if ENV["CIVO_ACCESS_KEY_ID"] && ENV["CIVO_SECRET_KEY"]
|
|
835
|
-
puts "📥 Downloading agent-bridge from Civo bucket..."
|
|
836
|
-
if download_from_civo!(arch, target_dir)
|
|
837
|
-
return download_agent_bridge_tmux!(target_dir)
|
|
838
|
-
else
|
|
839
|
-
puts "⚠️ Civo download failed, falling back to GitHub..."
|
|
840
|
-
end
|
|
841
|
-
end
|
|
842
|
-
|
|
843
|
-
# 4. Fallback to GitHub raw URLs (legacy)
|
|
844
|
-
bridge_url = "#{YATFA_RAW_URL}/bin/agent-bridge-linux-#{arch}"
|
|
845
|
-
puts "📥 Downloading agent-bridge for linux-#{arch} from GitHub..."
|
|
846
|
-
system("sudo curl -fsSL #{bridge_url} -o #{target_dir}/agent-bridge")
|
|
847
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
848
|
-
|
|
849
|
-
puts "✅ agent-bridge installed to #{target_dir}"
|
|
850
|
-
download_agent_bridge_tmux!(target_dir)
|
|
851
|
-
end
|
|
852
|
-
|
|
853
|
-
def download_from_civo!(arch, target_dir)
|
|
854
|
-
require "aws-sdk-s3"
|
|
855
|
-
|
|
856
|
-
region = ENV["CIVO_REGION"] || "LON1"
|
|
857
|
-
endpoint = ENV["CIVO_ENDPOINT"] || "https://objectstore.fra1.civo.com"
|
|
858
|
-
bucket = ENV["YATFA_BINARIES_BUCKET"] || "yatfa"
|
|
859
|
-
version = ENV["AGENT_BRIDGE_VERSION"] || "latest"
|
|
860
|
-
|
|
861
|
-
binary_name = "agent-bridge-linux-#{arch}"
|
|
862
|
-
key = "agent-bridge/#{version}/#{binary_name}"
|
|
863
|
-
|
|
864
|
-
puts " Region: #{region}"
|
|
865
|
-
puts " Bucket: #{bucket}"
|
|
866
|
-
puts " Version: #{version}"
|
|
867
|
-
puts " Binary: #{binary_name}"
|
|
868
|
-
|
|
869
|
-
begin
|
|
870
|
-
# Configure Civo S3-compatible client
|
|
871
|
-
Aws.config.update({
|
|
872
|
-
region: region,
|
|
873
|
-
endpoint: endpoint,
|
|
874
|
-
access_key_id: ENV["CIVO_ACCESS_KEY_ID"],
|
|
875
|
-
secret_access_key: ENV["CIVO_SECRET_KEY"]
|
|
876
|
-
})
|
|
877
|
-
|
|
878
|
-
s3 = Aws::S3::Resource.new
|
|
879
|
-
obj = s3.bucket(bucket).object(key)
|
|
880
|
-
|
|
881
|
-
# Generate presigned URL (valid for 1 hour)
|
|
882
|
-
url = obj.presigned_url(:get, expires_in: 3600)
|
|
883
|
-
|
|
884
|
-
puts "✅ Generated presigned URL"
|
|
885
|
-
|
|
886
|
-
# Download via curl
|
|
887
|
-
system("sudo curl -fsSL #{url} -o #{target_dir}/agent-bridge")
|
|
888
|
-
|
|
889
|
-
unless $?.success?
|
|
890
|
-
puts "❌ Failed to download agent-bridge from Civo"
|
|
891
|
-
return false
|
|
892
|
-
end
|
|
893
|
-
|
|
894
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
895
|
-
puts "✅ Downloaded agent-bridge from Civo"
|
|
896
|
-
return true
|
|
897
|
-
|
|
898
|
-
rescue LoadError
|
|
899
|
-
puts "⚠️ aws-sdk-s3 gem not installed"
|
|
900
|
-
return false
|
|
901
|
-
rescue Aws::S3::Errors::NoSuchKey
|
|
902
|
-
puts "❌ Version #{version} not found in bucket"
|
|
903
|
-
return false
|
|
904
|
-
rescue => e
|
|
905
|
-
puts "❌ Civo download failed: #{e.message}"
|
|
906
|
-
return false
|
|
907
|
-
end
|
|
908
|
-
end
|
|
909
|
-
|
|
910
|
-
def download_agent_bridge_tmux!(target_dir)
|
|
911
|
-
bridge_tmux_url = "#{YATFA_RAW_URL}/bin/agent-bridge-tmux"
|
|
912
|
-
|
|
913
|
-
if File.exist?("/tmp/agent-bridge-tmux")
|
|
914
|
-
puts "🔧 Installing mounted agent-bridge-tmux..."
|
|
915
|
-
system("sudo cp /tmp/agent-bridge-tmux #{target_dir}/agent-bridge-tmux")
|
|
916
|
-
system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
|
|
917
|
-
else
|
|
918
|
-
system("sudo curl -fsSL #{bridge_tmux_url} -o #{target_dir}/agent-bridge-tmux")
|
|
919
|
-
system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
|
|
920
|
-
end
|
|
921
|
-
|
|
922
|
-
# Patch agent-bridge-tmux to force INSIDE_TMUX=1
|
|
923
|
-
# Note: This is now fixed in the repo, but we keep this for backward compatibility
|
|
924
|
-
# with older agent-bridge-tmux scripts if cached
|
|
925
|
-
puts "🔧 Patching agent-bridge-tmux to force INSIDE_TMUX=1..."
|
|
926
|
-
|
|
927
|
-
# Read the file (we can read /usr/local/bin files usually)
|
|
928
|
-
content = File.read("#{target_dir}/agent-bridge-tmux")
|
|
929
|
-
|
|
930
|
-
# Replace the command if not already present
|
|
931
|
-
unless content.include?("export INSIDE_TMUX=1")
|
|
932
|
-
new_content = content.gsub(
|
|
933
|
-
"&& agent-bridge\"",
|
|
934
|
-
"&& export INSIDE_TMUX=1 && agent-bridge\""
|
|
935
|
-
)
|
|
936
|
-
|
|
937
|
-
# Write to temp file
|
|
938
|
-
File.write("/tmp/agent-bridge-tmux-patched", new_content)
|
|
939
|
-
|
|
940
|
-
# Move to destination with sudo
|
|
941
|
-
system("sudo mv /tmp/agent-bridge-tmux-patched #{target_dir}/agent-bridge-tmux")
|
|
942
|
-
system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
|
|
943
|
-
end
|
|
944
|
-
|
|
945
|
-
return target_dir
|
|
946
|
-
end
|
|
947
|
-
|
|
948
|
-
def install_helper_script(bin_dir, script_name)
|
|
949
|
-
url = "#{YATFA_RAW_URL}/bin/#{script_name}"
|
|
950
|
-
begin
|
|
951
|
-
script_content = URI.open(url).read
|
|
952
|
-
puts " (from remote: #{url})"
|
|
953
|
-
rescue OpenURI::HTTPError, Errno::ENOENT => e
|
|
954
|
-
puts "⚠️ Failed to download #{script_name}: #{e.message}"
|
|
955
|
-
return false
|
|
956
|
-
end
|
|
957
|
-
|
|
958
|
-
target = File.join(bin_dir, script_name)
|
|
959
|
-
temp_script = "/tmp/#{script_name}.temp"
|
|
960
|
-
File.write(temp_script, script_content)
|
|
961
|
-
system("sudo mv #{temp_script} #{target}")
|
|
962
|
-
system("sudo chmod +x #{target}")
|
|
963
|
-
puts "✅ Installed #{script_name} to #{bin_dir}"
|
|
964
|
-
true
|
|
965
|
-
end
|
|
966
|
-
|
|
967
|
-
# Download a file from remote URL to a destination directory
|
|
968
|
-
# @param script_name [String] filename to download
|
|
969
|
-
# @param remote_url [String] full URL to download from
|
|
970
|
-
# @param dest_dir [String] directory to save into
|
|
971
|
-
# @param executable [Boolean] whether to chmod 0755 (default: true)
|
|
972
|
-
# @param label [String] human-readable label for log messages (default: script_name)
|
|
973
|
-
# @return [String, nil] downloaded file path, or nil on failure
|
|
974
|
-
def download_file(script_name:, remote_url:, dest_dir:, executable: true, label: nil)
|
|
975
|
-
label ||= script_name
|
|
976
|
-
dest_path = File.join(dest_dir, script_name)
|
|
977
|
-
|
|
978
|
-
puts "📥 Downloading #{label}..."
|
|
979
|
-
begin
|
|
980
|
-
content = URI.open(remote_url).read
|
|
981
|
-
File.write(dest_path, content)
|
|
982
|
-
File.chmod(0755, dest_path) if executable
|
|
983
|
-
puts "✅ Installed #{label} to #{dest_path}"
|
|
984
|
-
dest_path
|
|
985
|
-
rescue OpenURI::HTTPError => e
|
|
986
|
-
puts "⚠️ Failed to download #{label}: #{e.message}"
|
|
987
|
-
nil
|
|
988
|
-
end
|
|
989
|
-
end
|
|
990
|
-
|
|
991
|
-
def setup_helper_scripts!
|
|
992
|
-
# Install helper scripts that skills use for command injection
|
|
993
|
-
bin_dir = "/usr/local/bin"
|
|
994
|
-
|
|
995
|
-
puts "🛠️ Setting up helper scripts..."
|
|
996
|
-
|
|
997
|
-
HELPER_SCRIPTS.each do |script_name|
|
|
998
|
-
install_helper_script(bin_dir, script_name)
|
|
999
|
-
end
|
|
1000
|
-
end
|
|
1001
|
-
|
|
1002
290
|
def run_agent!(bin_dir)
|
|
1003
291
|
agent_type = ENV["AGENT_TYPE"] || "agent"
|
|
1004
292
|
puts ""
|
|
@@ -1006,21 +294,15 @@ def run_agent!(bin_dir)
|
|
|
1006
294
|
puts " Press Ctrl+B then D to detach from tmux"
|
|
1007
295
|
puts ""
|
|
1008
296
|
|
|
1009
|
-
#
|
|
1010
|
-
#
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
env_cmd << "YATFA_API_KEY=#{ENV['YATFA_API_KEY']}" if ENV["YATFA_API_KEY"]
|
|
1017
|
-
|
|
1018
|
-
# Build the full command
|
|
1019
|
-
full_cmd = env_cmd.join(" ") + " #{bin_dir}/agent-bridge-tmux"
|
|
297
|
+
# Pass environment variables via env hash instead of command string
|
|
298
|
+
# to avoid exposing credentials in /proc/*/cmdline (visible via ps)
|
|
299
|
+
env = {}
|
|
300
|
+
env["AGENT_ID"] = ENV["AGENT_ID"] if ENV["AGENT_ID"]
|
|
301
|
+
env["YATFA_WS_URL"] = ENV["YATFA_WS_URL"] if ENV["YATFA_WS_URL"]
|
|
302
|
+
env["YATFA_API_URL"] = ENV["YATFA_API_URL"] if ENV["YATFA_API_URL"]
|
|
303
|
+
env["YATFA_API_KEY"] = ENV["YATFA_API_KEY"] if ENV["YATFA_API_KEY"]
|
|
1020
304
|
|
|
1021
|
-
#
|
|
1022
|
-
puts "🔧 Debug: Running #{full_cmd}"
|
|
1023
|
-
system(full_cmd)
|
|
305
|
+
system(env, "#{bin_dir}/agent-bridge-tmux")
|
|
1024
306
|
end
|
|
1025
307
|
|
|
1026
308
|
# ─── Main ─────────────────────────────────────────────────────────────────
|