yatfa 1.0.88 → 1.0.90

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.
@@ -119,8 +119,8 @@ def setup_github_auth!
119
119
  system("sudo mv /tmp/git-auth-helper #{helper_path}")
120
120
  system("sudo chmod +x #{helper_path}")
121
121
 
122
- # Configure git
123
- system("git config --global credential.helper '!f() { test \"$1\" = get && echo \"protocol=https\" && echo \"host=github.com\" && echo \"username=x-access-token\" && p=$(#{helper_path}) && echo \"password=$p\"; }; f'")
122
+ # Configure git (array form avoids shell interpolation of helper path)
123
+ system("git", "config", "--global", "credential.helper", "!f() { test \"$1\" = get && echo \"protocol=https\" && echo \"host=github.com\" && echo \"username=x-access-token\" && p=$(#{helper_path}) && echo \"password=$p\"; }; f")
124
124
 
125
125
  # Configure gh CLI wrapper for auto-refresh and permission controls
126
126
  if install_gh_wrapper!(real_gh_path: "/usr/bin/gh", with_token_refresh: true, token_helper_path: helper_path)
@@ -134,10 +134,18 @@ def setup_github_auth!
134
134
  # Authenticate using the real gh binary
135
135
  # On first run: real_gh_path exists, authenticate before wrapper installation
136
136
  # On subsequent runs: real_gh_path.real exists from previous run
137
+ # Use IO.popen with array form to pass token via stdin (avoids credential exposure in /proc/*/cmdline)
138
+ gh_bin = nil
137
139
  if File.exist?("#{real_gh_path}.real")
138
- system("echo '#{ENV['GH_TOKEN']}' | #{real_gh_path}.real auth login --with-token 2>/dev/null")
140
+ gh_bin = "#{real_gh_path}.real"
139
141
  elsif File.exist?(real_gh_path)
140
- system("echo '#{ENV['GH_TOKEN']}' | #{real_gh_path} auth login --with-token 2>/dev/null")
142
+ gh_bin = real_gh_path
143
+ end
144
+
145
+ if gh_bin
146
+ IO.popen([gh_bin, "auth", "login", "--with-token"], "w") do |io|
147
+ io.write(ENV["GH_TOKEN"])
148
+ end
141
149
  end
142
150
 
143
151
  # Install wrapper for permission controls even with GH_TOKEN
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Built-in and community skill installation
4
+ # Extracted from setup-agent.rb for modularity (Round 2)
5
+ #
6
+ # Handles:
7
+ # - Installing built-in skills from the SKILLS env var (Civo-hosted SKILL.md files)
8
+ # - Fetching community skills list from the backend whoami API
9
+ # - Installing community skills from external GitHub repos
10
+ #
11
+ # Dependencies:
12
+ # - YATFA_RAW_URL constant is defined by setup-agent.rb
13
+ # - open-uri is loaded by setup-agent.rb before this file is required
14
+ # - No cross-dependencies with other extracted modules
15
+
16
+ def setup_skills!
17
+ skills = ENV["SKILLS"].to_s.split(",")
18
+ return if skills.empty?
19
+
20
+ skills_dir = File.expand_path("~/.claude/skills")
21
+ puts "🧠 Installing #{skills.size} skills to #{skills_dir}..."
22
+ FileUtils.mkdir_p(skills_dir)
23
+
24
+ skills.each do |skill|
25
+ puts " - #{skill}"
26
+
27
+ url = "#{YATFA_RAW_URL}/skills/#{skill}/SKILL.md"
28
+
29
+ begin
30
+ skill_content = URI.open(url).read
31
+ puts " (from remote: #{url})"
32
+
33
+ # Write to .claude/skills/[skill]/SKILL.md with proper structure
34
+ skill_dir = File.join(skills_dir, skill)
35
+ FileUtils.mkdir_p(skill_dir)
36
+ File.write(File.join(skill_dir, "SKILL.md"), skill_content)
37
+ rescue OpenURI::HTTPError, Errno::ENOENT => e
38
+ puts "⚠️ Failed to download skill #{skill}: #{e.message}"
39
+ end
40
+ end
41
+
42
+ puts "✅ Skills installed"
43
+ end
44
+
45
+ # Community skill source registry
46
+ # Maps skill name to its download source configuration
47
+ COMMUNITY_SKILL_SOURCES = {
48
+ "ui-ux-pro-max" => {
49
+ github_repo: "nextlevelbuilder/ui-ux-pro-max-skill",
50
+ skill_path: ".claude/skills/ui-ux-pro-max/SKILL.md"
51
+ }
52
+ }.freeze
53
+
54
+ # Fetch community skills assigned to this agent from the backend API
55
+ def fetch_community_skills!
56
+ rails_api_url = ENV["YATFA_API_URL"]
57
+ rails_api_key = ENV["YATFA_API_KEY"]
58
+
59
+ unless rails_api_url && !rails_api_url.empty? && rails_api_key && !rails_api_key.empty?
60
+ puts "⚠️ Cannot fetch community skills: API URL or key not set"
61
+ return []
62
+ end
63
+
64
+ uri = URI("#{rails_api_url}/whoami")
65
+ http = Net::HTTP.new(uri.host, uri.port)
66
+ http.use_ssl = uri.scheme == "https"
67
+
68
+ request = Net::HTTP::Get.new(uri)
69
+ request["X-API-Key"] = rails_api_key
70
+ request["Accept"] = "application/json"
71
+
72
+ begin
73
+ response = http.request(request)
74
+ if response.is_a?(Net::HTTPSuccess)
75
+ data = JSON.parse(response.body)
76
+ skills = data["community_skills"] || []
77
+ if skills.any?
78
+ puts "📦 Found #{skills.size} community skills: #{skills.join(', ')}"
79
+ end
80
+ skills
81
+ else
82
+ puts "⚠️ Could not fetch community skills: #{response.code}"
83
+ []
84
+ end
85
+ rescue => e
86
+ puts "⚠️ Error fetching community skills: #{e.message}"
87
+ []
88
+ end
89
+ end
90
+
91
+ # Install community skills from external sources (e.g., GitHub repos)
92
+ def setup_community_skills!
93
+ # Community skills can come from two sources:
94
+ # 1. COMMUNITY_SKILLS env var (set during container creation from agent config)
95
+ # 2. Fetched from the backend API (whoami response)
96
+ env_skills = ENV["COMMUNITY_SKILLS"].to_s.split(",").map(&:strip).reject(&:blank?)
97
+ api_skills = fetch_community_skills!
98
+ community_skills = (env_skills + api_skills).uniq
99
+
100
+ return if community_skills.empty?
101
+
102
+ skills_dir = File.expand_path("~/.claude/skills")
103
+ puts "🌐 Installing #{community_skills.size} community skills..."
104
+ FileUtils.mkdir_p(skills_dir)
105
+
106
+ community_skills.each do |skill_name|
107
+ puts " - #{skill_name}"
108
+
109
+ source = COMMUNITY_SKILL_SOURCES[skill_name]
110
+ unless source
111
+ puts "⚠️ Unknown community skill: #{skill_name} (no source configured)"
112
+ next
113
+ end
114
+
115
+ begin
116
+ # Download SKILL.md from GitHub raw URL
117
+ github_repo = source[:github_repo]
118
+ skill_path = source[:skill_path] || ".claude/skills/#{skill_name}/SKILL.md"
119
+ url = "https://raw.githubusercontent.com/#{github_repo}/main/#{skill_path}"
120
+
121
+ skill_content = URI.open(url, open_timeout: 15, read_timeout: 30).read
122
+ puts " (from GitHub: #{github_repo})"
123
+
124
+ # Write to .claude/skills/[skill_name]/SKILL.md
125
+ skill_dir = File.join(skills_dir, skill_name)
126
+ FileUtils.mkdir_p(skill_dir)
127
+ File.write(File.join(skill_dir, "SKILL.md"), skill_content)
128
+ rescue OpenURI::HTTPError, Errno::ENOENT => e
129
+ puts "⚠️ Failed to download community skill #{skill_name}: #{e.message}"
130
+ rescue => e
131
+ puts "⚠️ Error installing community skill #{skill_name}: #{e.message}"
132
+ end
133
+ end
134
+
135
+ puts "✅ Community skills installed"
136
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ # System prompt fetching and repository context injection
4
+ # Extracted from setup-agent.rb for modularity
5
+ #
6
+ # Handles:
7
+ # - Fetching agent-specific system prompt from the Yatfa API
8
+ # - Injecting repository context into the system prompt
9
+ # - Writing ~/CLAUDE.md with repository structure info
10
+ #
11
+ # Dependencies:
12
+ # - Standard library requires (json, net/http, fileutils, etc.) are loaded
13
+ # by setup-agent.rb before this file is required
14
+
15
+ def setup_system_prompt!
16
+ agent_type = ENV["AGENT_TYPE"]
17
+ system_prompt_path = File.expand_path("~/.yatfa/system-prompt.txt")
18
+
19
+ rails_api_url = ENV["YATFA_API_URL"]
20
+ rails_api_key = ENV["YATFA_API_KEY"]
21
+
22
+ unless rails_api_url && !rails_api_url.empty? && rails_api_key && !rails_api_key.empty?
23
+ puts "❌ YATFA_API_URL or YATFA_API_KEY not set"
24
+ puts " Cannot fetch system prompt without API credentials"
25
+ exit 1
26
+ end
27
+
28
+ # Fetch system prompt from yatfa API
29
+ # Agent prompts are stored in yatfa/prompts/<agent-type>/base.md
30
+ # and loaded into the database by AgentPromptLoader
31
+ prompt_url = URI("#{rails_api_url}/internal/skill?tags%5B%5D=#{agent_type}&tags%5B%5D=base")
32
+ http = Net::HTTP.new(prompt_url.host, prompt_url.port)
33
+ http.use_ssl = prompt_url.scheme == "https"
34
+ http.open_timeout = 10
35
+ http.read_timeout = 15
36
+
37
+ request = Net::HTTP::Get.new(prompt_url)
38
+ request["X-API-Key"] = rails_api_key
39
+ request["Accept"] = "application/json"
40
+
41
+ banner_content = nil
42
+ begin
43
+ puts "📥 Fetching system prompt for #{agent_type}..."
44
+ response = http.request(request)
45
+
46
+ if response.is_a?(Net::HTTPSuccess)
47
+ data = JSON.parse(response.body)
48
+ banner_content = data["content"]
49
+
50
+ unless banner_content && !banner_content.empty?
51
+ puts "❌ System prompt for '#{agent_type}' is empty"
52
+ exit 1
53
+ end
54
+
55
+ puts "✅ Fetched system prompt for #{agent_type}"
56
+ else
57
+ puts "❌ Failed to fetch system prompt: #{response.code} #{response.message}"
58
+ puts " Response: #{response.body}"
59
+ puts " Make sure AgentPromptLoader has loaded prompts/ directory"
60
+ exit 1
61
+ end
62
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
63
+ puts "❌ Timeout fetching system prompt: #{e.message}"
64
+ exit 1
65
+ rescue => e
66
+ puts "❌ Error fetching system prompt: #{e.message}"
67
+ exit 1
68
+ end
69
+
70
+ # Append repository context from REPOSITORIES_CONTEXT env var
71
+ if ENV["REPOSITORIES_CONTEXT"] && !ENV["REPOSITORIES_CONTEXT"].empty?
72
+ repos = ENV["REPOSITORIES_CONTEXT"].split(',').map do |repo|
73
+ name, path = repo.split(':')
74
+ { name: name, path: path || "/workspace/#{name}" }
75
+ end
76
+
77
+ repo_info = repos.map { |r| " - **#{r[:name]}**: #{r[:path]}" }.join("\n")
78
+
79
+ banner_content += "\n\n## REPOSITORY STRUCTURE\n\n"
80
+ banner_content += "This project has #{repos.count} repositories:\n\n"
81
+ banner_content += "#{repo_info}\n\n"
82
+ end
83
+
84
+ # Ensure directory exists and write system prompt
85
+ FileUtils.mkdir_p(File.dirname(system_prompt_path))
86
+ File.write(system_prompt_path, banner_content)
87
+ puts "✅ System prompt written to #{system_prompt_path} (agent: #{agent_type})"
88
+
89
+ # Add repository context to CLAUDE.md for easy reference
90
+ claude_md = File.expand_path("~/CLAUDE.md")
91
+ if ENV["REPOSITORIES_CONTEXT"] && !ENV["REPOSITORIES_CONTEXT"].empty?
92
+ repo_info = ENV["REPOSITORIES_CONTEXT"].split(',').map do |repo|
93
+ name, path = repo.split(':')
94
+ " - **#{name}**: #{path}"
95
+ end.join("\n")
96
+
97
+ repo_context_md = <<~MARKDOWN
98
+ ## Repository Structure
99
+
100
+ This project has #{ENV["REPOSITORIES_CONTEXT"].split(',').count} repositories:
101
+
102
+ #{repo_info}
103
+ MARKDOWN
104
+
105
+ # Append to existing CLAUDE.md or create new one
106
+ existing_content = File.exist?(claude_md) ? File.read(claude_md) : ""
107
+ File.write(claude_md, existing_content + "\n" + repo_context_md)
108
+ puts "📝 Repository context added to ~/CLAUDE.md"
109
+ end
110
+ end