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.
- package/lib/yatfa_agent/agent.rb +12 -374
- 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-github.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
140
|
+
gh_bin = "#{real_gh_path}.real"
|
|
139
141
|
elsif File.exist?(real_gh_path)
|
|
140
|
-
|
|
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
|
package/setup/skills.rb
ADDED
|
@@ -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
|