yatfa 1.0.91 → 1.0.92
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/bin/run-yatfa-agent.rb +3 -6
- package/lib/yatfa_agent/config.rb +32 -26
- package/lib/yatfa_agent/docker.rb +11 -8
- package/lib/yatfa_agent/http.rb +97 -0
- package/lib/yatfa_agent/setup.rb +32 -19
- package/package.json +1 -1
- package/setup/agent_bridge.rb +15 -15
- package/setup/claude_settings.rb +33 -26
- package/setup/http_client.rb +97 -0
- package/setup/setup-agent.rb +58 -54
- package/setup/setup-claw-agent.rb +42 -18
- package/setup/setup-github.rb +5 -5
- package/setup/skills.rb +26 -19
- package/setup/system_prompt.rb +29 -25
package/bin/run-yatfa-agent.rb
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
require_relative "../lib/yatfa_agent/config"
|
|
15
15
|
require_relative "../lib/yatfa_agent/docker"
|
|
16
|
+
require_relative "../lib/yatfa_agent/http"
|
|
16
17
|
require_relative "../lib/yatfa_agent/agent"
|
|
17
18
|
require_relative "../lib/yatfa_agent/setup"
|
|
18
19
|
require_relative "../lib/yatfa_agent/updater"
|
|
@@ -27,12 +28,8 @@ def current_version
|
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def latest_npm_version
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
uri = URI("https://registry.npmjs.org/yatfa/latest")
|
|
33
|
-
response = Net::HTTP.get_response(uri)
|
|
34
|
-
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
35
|
-
JSON.parse(response.body)["version"]
|
|
31
|
+
data = YatfaAgent::HTTP.get("https://registry.npmjs.org/yatfa/latest")
|
|
32
|
+
data&.dig("version")
|
|
36
33
|
rescue StandardError
|
|
37
34
|
nil
|
|
38
35
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "http"
|
|
4
5
|
|
|
5
6
|
module YatfaAgent
|
|
6
7
|
module Config
|
|
@@ -122,49 +123,54 @@ module YatfaAgent
|
|
|
122
123
|
|
|
123
124
|
return unless mcp_api_key
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
require "net/http"
|
|
127
|
-
require "json"
|
|
128
|
-
require "uri"
|
|
126
|
+
auth_headers = { "X-API-Key" => mcp_api_key, "Accept" => "application/json" }
|
|
129
127
|
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
# First, discover which project we belong to via /whoami
|
|
129
|
+
begin
|
|
130
|
+
agent_info = YatfaAgent::HTTP.get("#{api_base}/whoami", headers: auth_headers)
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
# Under the raise-based HTTP contract, a nil result here means an empty
|
|
133
|
+
# 2xx body (real failures raise YatfaAgent::HTTP::Error below).
|
|
134
|
+
unless agent_info
|
|
135
|
+
puts "⚠️ Empty response from /whoami, skipping repository fetch"
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
139
|
+
puts "⚠️ Unable to discover project from /whoami (HTTP #{e.status_code})"
|
|
140
|
+
return
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
puts "⚠️ Unable to discover project from /whoami: #{e.message}"
|
|
135
143
|
return
|
|
136
144
|
end
|
|
137
145
|
|
|
138
|
-
agent_info = JSON.parse(whoami_response.body)
|
|
139
146
|
project_id = agent_info["project_id"]
|
|
140
147
|
project_name = agent_info["name"] rescue "unknown"
|
|
141
148
|
|
|
142
149
|
puts "🔑 Authenticated as #{agent_info["name"]} (project #{project_id})"
|
|
143
150
|
|
|
144
151
|
# Then fetch repositories from Rails API (scoped to our project)
|
|
145
|
-
uri = URI("#{api_base}/repositories")
|
|
146
152
|
begin
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if response.code == "200"
|
|
150
|
-
repositories = JSON.parse(response.body)
|
|
153
|
+
repositories = YatfaAgent::HTTP.get("#{api_base}/repositories", headers: auth_headers)
|
|
151
154
|
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
if repositories.is_a?(Array) && repositories.any?
|
|
156
|
+
config["repositories"] = repositories
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
# Build REPOSITORIES_CONTEXT string: "name:path,name:path"
|
|
159
|
+
repo_context = repositories.map { |r| "#{r['name']}:#{r['path_in_sandbox']}" }.join(",")
|
|
160
|
+
config["env"] ||= {}
|
|
161
|
+
config["env"]["REPOSITORIES_CONTEXT"] = repo_context
|
|
159
162
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
end
|
|
163
|
+
puts "📚 Loaded #{repositories.count} repositories for project #{project_id} (#{project_name})"
|
|
164
|
+
repositories.each do |repo|
|
|
165
|
+
puts " - #{repo['name']}: #{repo['path_in_sandbox']}"
|
|
164
166
|
end
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
elsif repositories.nil?
|
|
168
|
+
# Empty 2xx body — the raise-based contract returns nil only then.
|
|
169
|
+
puts "⚠️ Empty response when fetching repositories"
|
|
167
170
|
end
|
|
171
|
+
# An empty Array ([]) means no repositories are configured; nothing to do.
|
|
172
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
173
|
+
puts "⚠️ Failed to fetch repositories (HTTP #{e.status_code})"
|
|
168
174
|
rescue StandardError => e
|
|
169
175
|
puts "⚠️ Failed to fetch repositories: #{e.message}"
|
|
170
176
|
end
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
-
require "net/http"
|
|
5
4
|
require "json"
|
|
6
5
|
require "time"
|
|
7
|
-
|
|
6
|
+
require_relative "http"
|
|
8
7
|
|
|
9
8
|
module YatfaAgent
|
|
10
9
|
module Docker
|
|
@@ -24,12 +23,16 @@ module YatfaAgent
|
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
def self.fetch_remote_version
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
data = YatfaAgent::HTTP.get(VERSION_URL)
|
|
27
|
+
data&.dig("version")
|
|
28
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
29
|
+
# Non-2xx response: surface the status so a version-fetch failure is
|
|
30
|
+
# distinguishable from "no remote update" (which returns nil on a 2xx).
|
|
31
|
+
warn "⚠️ Remote version check failed (HTTP #{e.status_code})"
|
|
32
|
+
nil
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
# Genuine network/timeout error (no HTTP status code available).
|
|
35
|
+
warn "⚠️ Remote version check failed (#{e.class}: #{e.message})"
|
|
33
36
|
nil
|
|
34
37
|
end
|
|
35
38
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module YatfaAgent
|
|
8
|
+
module HTTP
|
|
9
|
+
DEFAULT_OPEN_TIMEOUT = 5
|
|
10
|
+
DEFAULT_READ_TIMEOUT = 10
|
|
11
|
+
|
|
12
|
+
# Custom error for HTTP failures with response details.
|
|
13
|
+
# Raised for non-2xx responses so callers can access status code and body.
|
|
14
|
+
class Error < StandardError
|
|
15
|
+
attr_reader :status_code, :response_body
|
|
16
|
+
|
|
17
|
+
# @param message [String] error message (e.g., "HTTP 401 Unauthorized")
|
|
18
|
+
# @param status_code [Integer] HTTP status code
|
|
19
|
+
# @param response_body [String] raw response body
|
|
20
|
+
def initialize(message, status_code:, response_body:)
|
|
21
|
+
super(message)
|
|
22
|
+
@status_code = status_code
|
|
23
|
+
@response_body = response_body
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Perform a GET request. Returns parsed JSON body on success.
|
|
28
|
+
# Raises YatfaAgent::HTTP::Error for non-2xx responses.
|
|
29
|
+
# Raises exceptions on network/timeout errors.
|
|
30
|
+
#
|
|
31
|
+
# @param url [String] full URL to request
|
|
32
|
+
# @param headers [Hash] HTTP headers (e.g., { "X-API-Key" => "key" })
|
|
33
|
+
# @param open_timeout [Integer] connection timeout in seconds (default: 5)
|
|
34
|
+
# @param read_timeout [Integer] response timeout in seconds (default: 10)
|
|
35
|
+
# @return [Hash, Array, nil] parsed JSON response, or nil for empty 2xx responses
|
|
36
|
+
# @raise [Error] for non-2xx HTTP responses
|
|
37
|
+
def self.get(url, headers: {}, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT)
|
|
38
|
+
uri = URI(url)
|
|
39
|
+
http = build_http(uri, open_timeout: open_timeout, read_timeout: read_timeout)
|
|
40
|
+
request = Net::HTTP::Get.new(uri)
|
|
41
|
+
headers.each { |k, v| request[k] = v }
|
|
42
|
+
response = http.request(request)
|
|
43
|
+
handle_response(response)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Perform a POST request. Returns parsed JSON body on success.
|
|
47
|
+
# Raises YatfaAgent::HTTP::Error for non-2xx responses.
|
|
48
|
+
# Raises exceptions on network/timeout errors.
|
|
49
|
+
#
|
|
50
|
+
# @param url [String] full URL to request
|
|
51
|
+
# @param body [Hash, String] request body (Hash is auto-serialized to JSON)
|
|
52
|
+
# @param headers [Hash] HTTP headers
|
|
53
|
+
# @param open_timeout [Integer] connection timeout in seconds (default: 5)
|
|
54
|
+
# @param read_timeout [Integer] response timeout in seconds (default: 10)
|
|
55
|
+
# @return [Hash, Array, nil] parsed JSON response, or nil for empty 2xx responses
|
|
56
|
+
# @raise [Error] for non-2xx HTTP responses
|
|
57
|
+
def self.post(url, body:, headers: {}, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT)
|
|
58
|
+
uri = URI(url)
|
|
59
|
+
http = build_http(uri, open_timeout: open_timeout, read_timeout: read_timeout)
|
|
60
|
+
request = Net::HTTP::Post.new(uri)
|
|
61
|
+
request["Content-Type"] = "application/json" unless headers.key?("Content-Type")
|
|
62
|
+
headers.each { |k, v| request[k] = v }
|
|
63
|
+
request.body = body.is_a?(String) ? body : JSON.generate(body)
|
|
64
|
+
response = http.request(request)
|
|
65
|
+
handle_response(response)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class << self
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def build_http(uri, open_timeout:, read_timeout:)
|
|
72
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
73
|
+
http.use_ssl = uri.scheme == "https"
|
|
74
|
+
http.open_timeout = open_timeout
|
|
75
|
+
http.read_timeout = read_timeout
|
|
76
|
+
http
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_json(body)
|
|
80
|
+
return nil if body.nil? || body.empty?
|
|
81
|
+
JSON.parse(body)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def handle_response(response)
|
|
85
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
86
|
+
parse_json(response.body)
|
|
87
|
+
else
|
|
88
|
+
raise Error.new(
|
|
89
|
+
"HTTP #{response.code} #{response.message}",
|
|
90
|
+
status_code: response.code.to_i,
|
|
91
|
+
response_body: response.body
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
package/lib/yatfa_agent/setup.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "net/http"
|
|
4
3
|
require "uri"
|
|
5
4
|
require "json"
|
|
6
5
|
require "readline"
|
|
7
6
|
require "fileutils"
|
|
7
|
+
require_relative "http"
|
|
8
8
|
|
|
9
9
|
module YatfaAgent
|
|
10
10
|
module Setup
|
|
@@ -581,21 +581,25 @@ module YatfaAgent
|
|
|
581
581
|
# Helper methods
|
|
582
582
|
|
|
583
583
|
def validate_api_key(api_key, yatfa_url)
|
|
584
|
-
# Derive API URL
|
|
585
584
|
api_base = "#{yatfa_url}/api/v1"
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
@errors << "
|
|
597
|
-
|
|
585
|
+
result = YatfaAgent::HTTP.get(
|
|
586
|
+
"#{api_base}/whoami",
|
|
587
|
+
headers: { "X-API-Key" => api_key, "Accept" => "application/json" }
|
|
588
|
+
)
|
|
589
|
+
!result.nil?
|
|
590
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
591
|
+
case e.status_code
|
|
592
|
+
when 401, 403
|
|
593
|
+
@errors << "API key is invalid or unauthorized (HTTP #{e.status_code})"
|
|
594
|
+
when 500..599
|
|
595
|
+
@errors << "Yatfa server returned HTTP #{e.status_code}; service may be unavailable, please retry"
|
|
596
|
+
else
|
|
597
|
+
@errors << "API key validation failed (HTTP #{e.status_code})"
|
|
598
598
|
end
|
|
599
|
+
false
|
|
600
|
+
rescue StandardError => e
|
|
601
|
+
@errors << "API key validation failed: #{e.message}"
|
|
602
|
+
false
|
|
599
603
|
end
|
|
600
604
|
|
|
601
605
|
def validate_github_credentials
|
|
@@ -625,16 +629,25 @@ module YatfaAgent
|
|
|
625
629
|
|
|
626
630
|
# If gh CLI is not configured, try direct API call
|
|
627
631
|
unless status.success?
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
{ "Authorization" => "Bearer #{token}", "Accept" => "application/json" }
|
|
632
|
+
result = YatfaAgent::HTTP.get(
|
|
633
|
+
"https://api.github.com/user",
|
|
634
|
+
headers: { "Authorization" => "Bearer #{token}", "Accept" => "application/json" }
|
|
632
635
|
)
|
|
633
636
|
|
|
634
|
-
return
|
|
637
|
+
return !result.nil?
|
|
635
638
|
end
|
|
636
639
|
|
|
637
640
|
true
|
|
641
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
642
|
+
case e.status_code
|
|
643
|
+
when 401, 403
|
|
644
|
+
@errors << "GitHub token is invalid or unauthorized (HTTP #{e.status_code})"
|
|
645
|
+
when 500..599
|
|
646
|
+
@errors << "GitHub API returned HTTP #{e.status_code}; service may be unavailable, please retry"
|
|
647
|
+
else
|
|
648
|
+
@errors << "GitHub token validation failed (HTTP #{e.status_code})"
|
|
649
|
+
end
|
|
650
|
+
false
|
|
638
651
|
end
|
|
639
652
|
|
|
640
653
|
def validate_github_app
|
package/package.json
CHANGED
package/setup/agent_bridge.rb
CHANGED
|
@@ -42,8 +42,8 @@ def download_agent_bridge!
|
|
|
42
42
|
local_override = File.join(override_dir, "agent-bridge-linux-#{arch}")
|
|
43
43
|
if File.exist?(local_override)
|
|
44
44
|
puts "🔧 Using local override: #{local_override}"
|
|
45
|
-
system("sudo cp
|
|
46
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
45
|
+
system("sudo", "cp", local_override, "#{target_dir}/agent-bridge")
|
|
46
|
+
system("sudo", "chmod", "+x", "#{target_dir}/agent-bridge")
|
|
47
47
|
puts "✅ agent-bridge installed from local override"
|
|
48
48
|
return download_agent_bridge_tmux!(target_dir)
|
|
49
49
|
end
|
|
@@ -52,8 +52,8 @@ def download_agent_bridge!
|
|
|
52
52
|
# 2. Check if binaries are mounted at /tmp (dev mode)
|
|
53
53
|
if File.exist?("/tmp/agent-bridge")
|
|
54
54
|
puts "🔧 Installing mounted agent-bridge..."
|
|
55
|
-
system("sudo cp /tmp/agent-bridge #{target_dir}/agent-bridge")
|
|
56
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
55
|
+
system("sudo", "cp", "/tmp/agent-bridge", "#{target_dir}/agent-bridge")
|
|
56
|
+
system("sudo", "chmod", "+x", "#{target_dir}/agent-bridge")
|
|
57
57
|
puts "✅ agent-bridge installed from mount"
|
|
58
58
|
return download_agent_bridge_tmux!(target_dir)
|
|
59
59
|
end
|
|
@@ -71,8 +71,8 @@ def download_agent_bridge!
|
|
|
71
71
|
# 4. Fallback to GitHub raw URLs (legacy)
|
|
72
72
|
bridge_url = "#{YATFA_RAW_URL}/bin/agent-bridge-linux-#{arch}"
|
|
73
73
|
puts "📥 Downloading agent-bridge for linux-#{arch} from GitHub..."
|
|
74
|
-
system("sudo curl -fsSL
|
|
75
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
74
|
+
system("sudo", "curl", "-fsSL", bridge_url, "-o", "#{target_dir}/agent-bridge")
|
|
75
|
+
system("sudo", "chmod", "+x", "#{target_dir}/agent-bridge")
|
|
76
76
|
|
|
77
77
|
puts "✅ agent-bridge installed to #{target_dir}"
|
|
78
78
|
download_agent_bridge_tmux!(target_dir)
|
|
@@ -119,7 +119,7 @@ def download_from_civo!(arch, target_dir)
|
|
|
119
119
|
return false
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
122
|
+
system("sudo", "chmod", "+x", "#{target_dir}/agent-bridge")
|
|
123
123
|
puts "✅ Downloaded agent-bridge from Civo"
|
|
124
124
|
return true
|
|
125
125
|
|
|
@@ -140,11 +140,11 @@ def download_agent_bridge_tmux!(target_dir)
|
|
|
140
140
|
|
|
141
141
|
if File.exist?("/tmp/agent-bridge-tmux")
|
|
142
142
|
puts "🔧 Installing mounted agent-bridge-tmux..."
|
|
143
|
-
system("sudo cp /tmp/agent-bridge-tmux #{target_dir}/agent-bridge-tmux")
|
|
144
|
-
system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
|
|
143
|
+
system("sudo", "cp", "/tmp/agent-bridge-tmux", "#{target_dir}/agent-bridge-tmux")
|
|
144
|
+
system("sudo", "chmod", "+x", "#{target_dir}/agent-bridge-tmux")
|
|
145
145
|
else
|
|
146
|
-
system("sudo curl -fsSL
|
|
147
|
-
system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
|
|
146
|
+
system("sudo", "curl", "-fsSL", bridge_tmux_url, "-o", "#{target_dir}/agent-bridge-tmux")
|
|
147
|
+
system("sudo", "chmod", "+x", "#{target_dir}/agent-bridge-tmux")
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
# Patch agent-bridge-tmux to force INSIDE_TMUX=1
|
|
@@ -166,8 +166,8 @@ def download_agent_bridge_tmux!(target_dir)
|
|
|
166
166
|
File.write("/tmp/agent-bridge-tmux-patched", new_content)
|
|
167
167
|
|
|
168
168
|
# Move to destination with sudo
|
|
169
|
-
system("sudo mv /tmp/agent-bridge-tmux-patched #{target_dir}/agent-bridge-tmux")
|
|
170
|
-
system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
|
|
169
|
+
system("sudo", "mv", "/tmp/agent-bridge-tmux-patched", "#{target_dir}/agent-bridge-tmux")
|
|
170
|
+
system("sudo", "chmod", "+x", "#{target_dir}/agent-bridge-tmux")
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
return target_dir
|
|
@@ -186,8 +186,8 @@ def install_helper_script(bin_dir, script_name)
|
|
|
186
186
|
target = File.join(bin_dir, script_name)
|
|
187
187
|
temp_script = "/tmp/#{script_name}.temp"
|
|
188
188
|
File.write(temp_script, script_content)
|
|
189
|
-
system("sudo mv
|
|
190
|
-
system("sudo chmod +x
|
|
189
|
+
system("sudo", "mv", temp_script, target)
|
|
190
|
+
system("sudo", "chmod", "+x", target)
|
|
191
191
|
puts "✅ Installed #{script_name} to #{bin_dir}"
|
|
192
192
|
true
|
|
193
193
|
end
|
package/setup/claude_settings.rb
CHANGED
|
@@ -24,37 +24,35 @@ def fetch_llm_credentials!
|
|
|
24
24
|
return nil
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
# Fetch LLM credentials from Rails API
|
|
28
|
-
uri = URI("#{rails_api_url}/llm/credentials")
|
|
29
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
30
|
-
http.use_ssl = uri.scheme == "https"
|
|
31
|
-
http.open_timeout = 10
|
|
32
|
-
http.read_timeout = 10
|
|
33
|
-
|
|
34
|
-
request = Net::HTTP::Get.new(uri)
|
|
35
|
-
request["X-API-Key"] = rails_api_key
|
|
36
|
-
request["Accept"] = "application/json"
|
|
37
|
-
|
|
38
27
|
begin
|
|
39
28
|
puts "🔍 Fetching LLM credentials from #{rails_api_url}/llm/credentials..."
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
29
|
+
data = YatfaAgent::HTTP.get(
|
|
30
|
+
"#{rails_api_url}/llm/credentials",
|
|
31
|
+
headers: { "X-API-Key" => rails_api_key, "Accept" => "application/json" },
|
|
32
|
+
open_timeout: 10,
|
|
33
|
+
read_timeout: 10
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Under the raise-based HTTP contract, a nil result means an empty 2xx body
|
|
37
|
+
# (real failures raise YatfaAgent::HTTP::Error below).
|
|
38
|
+
unless data
|
|
39
|
+
puts "❌ Empty response from /llm/credentials; no LLM credentials configured"
|
|
40
|
+
return nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
puts "✅ LLM credentials fetched successfully"
|
|
44
|
+
data
|
|
45
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
46
|
+
case e.status_code
|
|
47
|
+
when 401, 403
|
|
48
|
+
puts "❌ Yatfa API key or token is invalid or unauthorized (HTTP #{e.status_code})"
|
|
49
|
+
when 500..599
|
|
50
|
+
puts "❌ Yatfa server returned HTTP #{e.status_code}; service may be unavailable, please retry"
|
|
49
51
|
else
|
|
50
|
-
puts "❌ Failed to fetch LLM credentials
|
|
51
|
-
puts " Response: #{response.body}"
|
|
52
|
-
nil
|
|
52
|
+
puts "❌ Failed to fetch LLM credentials (HTTP #{e.status_code})"
|
|
53
53
|
end
|
|
54
|
-
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
55
|
-
puts "❌ Timeout fetching LLM credentials: #{e.message}"
|
|
56
54
|
nil
|
|
57
|
-
rescue => e
|
|
55
|
+
rescue StandardError => e
|
|
58
56
|
puts "❌ Error fetching LLM credentials: #{e.message}"
|
|
59
57
|
nil
|
|
60
58
|
end
|
|
@@ -123,6 +121,15 @@ def setup_claude_settings!
|
|
|
123
121
|
hooks_dir = File.join(settings_dir, "hooks")
|
|
124
122
|
FileUtils.mkdir_p(hooks_dir)
|
|
125
123
|
|
|
124
|
+
# Download shared HTTP client for hook scripts
|
|
125
|
+
download_file(
|
|
126
|
+
script_name: "http_client.rb",
|
|
127
|
+
remote_url: "#{YATFA_RAW_URL}/hooks/http_client.rb",
|
|
128
|
+
dest_dir: hooks_dir,
|
|
129
|
+
executable: false,
|
|
130
|
+
label: "HTTP client helper"
|
|
131
|
+
)
|
|
132
|
+
|
|
126
133
|
# SessionStart hook: skill knowledge injection
|
|
127
134
|
install_hook!(
|
|
128
135
|
settings_config, hooks_dir,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module YatfaAgent
|
|
8
|
+
module HTTP
|
|
9
|
+
DEFAULT_OPEN_TIMEOUT = 5
|
|
10
|
+
DEFAULT_READ_TIMEOUT = 10
|
|
11
|
+
|
|
12
|
+
# Custom error for HTTP failures with response details.
|
|
13
|
+
# Raised for non-2xx responses so callers can access status code and body.
|
|
14
|
+
class Error < StandardError
|
|
15
|
+
attr_reader :status_code, :response_body
|
|
16
|
+
|
|
17
|
+
# @param message [String] error message (e.g., "HTTP 401 Unauthorized")
|
|
18
|
+
# @param status_code [Integer] HTTP status code
|
|
19
|
+
# @param response_body [String] raw response body
|
|
20
|
+
def initialize(message, status_code:, response_body:)
|
|
21
|
+
super(message)
|
|
22
|
+
@status_code = status_code
|
|
23
|
+
@response_body = response_body
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Perform a GET request. Returns parsed JSON body on success.
|
|
28
|
+
# Raises YatfaAgent::HTTP::Error for non-2xx responses.
|
|
29
|
+
# Raises exceptions on network/timeout errors.
|
|
30
|
+
#
|
|
31
|
+
# @param url [String] full URL to request
|
|
32
|
+
# @param headers [Hash] HTTP headers (e.g., { "X-API-Key" => "key" })
|
|
33
|
+
# @param open_timeout [Integer] connection timeout in seconds (default: 5)
|
|
34
|
+
# @param read_timeout [Integer] response timeout in seconds (default: 10)
|
|
35
|
+
# @return [Hash, Array, nil] parsed JSON response, or nil for empty 2xx responses
|
|
36
|
+
# @raise [Error] for non-2xx HTTP responses
|
|
37
|
+
def self.get(url, headers: {}, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT)
|
|
38
|
+
uri = URI(url)
|
|
39
|
+
http = build_http(uri, open_timeout: open_timeout, read_timeout: read_timeout)
|
|
40
|
+
request = Net::HTTP::Get.new(uri)
|
|
41
|
+
headers.each { |k, v| request[k] = v }
|
|
42
|
+
response = http.request(request)
|
|
43
|
+
handle_response(response)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Perform a POST request. Returns parsed JSON body on success.
|
|
47
|
+
# Raises YatfaAgent::HTTP::Error for non-2xx responses.
|
|
48
|
+
# Raises exceptions on network/timeout errors.
|
|
49
|
+
#
|
|
50
|
+
# @param url [String] full URL to request
|
|
51
|
+
# @param body [Hash, String] request body (Hash is auto-serialized to JSON)
|
|
52
|
+
# @param headers [Hash] HTTP headers
|
|
53
|
+
# @param open_timeout [Integer] connection timeout in seconds (default: 5)
|
|
54
|
+
# @param read_timeout [Integer] response timeout in seconds (default: 10)
|
|
55
|
+
# @return [Hash, Array, nil] parsed JSON response, or nil for empty 2xx responses
|
|
56
|
+
# @raise [Error] for non-2xx HTTP responses
|
|
57
|
+
def self.post(url, body:, headers: {}, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT)
|
|
58
|
+
uri = URI(url)
|
|
59
|
+
http = build_http(uri, open_timeout: open_timeout, read_timeout: read_timeout)
|
|
60
|
+
request = Net::HTTP::Post.new(uri)
|
|
61
|
+
request["Content-Type"] = "application/json" unless headers.key?("Content-Type")
|
|
62
|
+
headers.each { |k, v| request[k] = v }
|
|
63
|
+
request.body = body.is_a?(String) ? body : JSON.generate(body)
|
|
64
|
+
response = http.request(request)
|
|
65
|
+
handle_response(response)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class << self
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def build_http(uri, open_timeout:, read_timeout:)
|
|
72
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
73
|
+
http.use_ssl = uri.scheme == "https"
|
|
74
|
+
http.open_timeout = open_timeout
|
|
75
|
+
http.read_timeout = read_timeout
|
|
76
|
+
http
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_json(body)
|
|
80
|
+
return nil if body.nil? || body.empty?
|
|
81
|
+
JSON.parse(body)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def handle_response(response)
|
|
85
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
86
|
+
parse_json(response.body)
|
|
87
|
+
else
|
|
88
|
+
raise Error.new(
|
|
89
|
+
"HTTP #{response.code} #{response.message}",
|
|
90
|
+
status_code: response.code.to_i,
|
|
91
|
+
response_body: response.body
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
package/setup/setup-agent.rb
CHANGED
|
@@ -30,12 +30,12 @@ require "fileutils"
|
|
|
30
30
|
require "pathname"
|
|
31
31
|
require "open-uri"
|
|
32
32
|
require "openssl"
|
|
33
|
-
require "net/http"
|
|
34
33
|
require "uri"
|
|
35
34
|
require "base64"
|
|
36
35
|
require "time"
|
|
37
36
|
require "socket"
|
|
38
37
|
require "timeout"
|
|
38
|
+
require_relative "http_client"
|
|
39
39
|
require_relative "setup-github"
|
|
40
40
|
require_relative "repositories"
|
|
41
41
|
require_relative "agent_bridge"
|
|
@@ -126,36 +126,37 @@ def fetch_agent_id!
|
|
|
126
126
|
whoami_url = "#{rails_api_url}/whoami"
|
|
127
127
|
whoami_url += "?version=#{URI.encode_www_form_component(agent_version)}" if agent_version && !agent_version.empty?
|
|
128
128
|
|
|
129
|
-
uri = URI(whoami_url)
|
|
130
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
131
|
-
http.use_ssl = uri.scheme == "https"
|
|
132
|
-
|
|
133
|
-
request = Net::HTTP::Get.new(uri)
|
|
134
|
-
request["X-API-Key"] = rails_api_key
|
|
135
|
-
request["Accept"] = "application/json"
|
|
136
|
-
|
|
137
129
|
begin
|
|
138
130
|
puts "🔍 Fetching agent ID from #{rails_api_url}/whoami..."
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
131
|
+
data = YatfaAgent::HTTP.get(whoami_url, headers: { "X-API-Key" => rails_api_key, "Accept" => "application/json" })
|
|
132
|
+
|
|
133
|
+
# Under the raise-based HTTP contract, a nil result means an empty 2xx body
|
|
134
|
+
# (real failures raise YatfaAgent::HTTP::Error below).
|
|
135
|
+
unless data
|
|
136
|
+
puts "❌ Empty response from /whoami; cannot determine agent ID"
|
|
137
|
+
exit 1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
agent_id = data["agent_id"] || data["id"]
|
|
141
|
+
|
|
142
|
+
if agent_id && !agent_id.to_s.empty?
|
|
143
|
+
ENV["AGENT_ID"] = agent_id.to_s
|
|
144
|
+
puts "✅ Agent ID fetched: #{agent_id}"
|
|
153
145
|
else
|
|
154
|
-
puts "❌
|
|
155
|
-
puts " Response: #{response.body}"
|
|
146
|
+
puts "❌ Could not find agent_id in API response"
|
|
156
147
|
exit 1
|
|
157
148
|
end
|
|
158
|
-
rescue => e
|
|
149
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
150
|
+
case e.status_code
|
|
151
|
+
when 401, 403
|
|
152
|
+
puts "❌ Yatfa API key or token is invalid or unauthorized (HTTP #{e.status_code})"
|
|
153
|
+
when 500..599
|
|
154
|
+
puts "❌ Yatfa server returned HTTP #{e.status_code}; service may be unavailable, please retry"
|
|
155
|
+
else
|
|
156
|
+
puts "❌ Failed to fetch agent ID (HTTP #{e.status_code})"
|
|
157
|
+
end
|
|
158
|
+
exit 1
|
|
159
|
+
rescue StandardError => e
|
|
159
160
|
puts "❌ Error fetching agent ID: #{e.message}"
|
|
160
161
|
puts " #{e.class}: #{e.backtrace.first}"
|
|
161
162
|
exit 1
|
|
@@ -191,7 +192,7 @@ def setup_mcp_config!
|
|
|
191
192
|
|
|
192
193
|
puts "📦 Installing yatfa-mcp..."
|
|
193
194
|
# Redirect output to avoid cluttering logs, unless it fails
|
|
194
|
-
unless system("npm install --prefix
|
|
195
|
+
unless system("npm", "install", "--prefix", tools_dir, "yatfa-mcp", out: "/dev/null", err: "/dev/null")
|
|
195
196
|
puts "❌ Failed to install yatfa-mcp"
|
|
196
197
|
end
|
|
197
198
|
|
|
@@ -261,7 +262,7 @@ def setup_git_config!
|
|
|
261
262
|
|
|
262
263
|
# Avoid "dubious ownership" errors in containers
|
|
263
264
|
# Note: Individual repository paths are added in clone_repositories! after cloning
|
|
264
|
-
system("git config --global --add safe.directory
|
|
265
|
+
system("git", "config", "--global", "--add", "safe.directory", Dir.pwd)
|
|
265
266
|
puts "✅ Git configured to trust #{Dir.pwd}"
|
|
266
267
|
end
|
|
267
268
|
|
|
@@ -308,30 +309,33 @@ end
|
|
|
308
309
|
# ─── Main ─────────────────────────────────────────────────────────────────
|
|
309
310
|
|
|
310
311
|
# Main
|
|
311
|
-
|
|
312
|
-
puts "
|
|
313
|
-
puts ""
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
312
|
+
if __FILE__ == $PROGRAM_NAME
|
|
313
|
+
puts "🤖 Yatfa Agent Setup"
|
|
314
|
+
puts "====================="
|
|
315
|
+
puts ""
|
|
316
|
+
|
|
317
|
+
# Claw agent has its own completely separate setup flow
|
|
318
|
+
if ENV["AGENT_TYPE"] == "claw"
|
|
319
|
+
require_relative "setup-claw-agent"
|
|
320
|
+
setup_claw_agent!
|
|
321
|
+
else
|
|
322
|
+
check_env!
|
|
323
|
+
fetch_agent_id!
|
|
324
|
+
setup_mcp_config!
|
|
325
|
+
setup_system_prompt!
|
|
326
|
+
# Clone repositories BEFORE setting up hooks that depend on them
|
|
327
|
+
setup_github_auth!
|
|
328
|
+
setup_git_config!
|
|
329
|
+
clone_repositories!
|
|
330
|
+
setup_claude_config!
|
|
331
|
+
setup_claude_settings!
|
|
332
|
+
# setup_skill_hooks! # Deprecated in favor of global hooks
|
|
333
|
+
setup_skills!
|
|
334
|
+
setup_community_skills!
|
|
335
|
+
setup_git_hooks!
|
|
336
|
+
setup_helper_scripts!
|
|
337
|
+
# prepare_git_state! is now in entrypoint.sh
|
|
338
|
+
bin_dir = download_agent_bridge!
|
|
339
|
+
run_agent!(bin_dir)
|
|
340
|
+
end
|
|
337
341
|
end
|
|
@@ -93,7 +93,7 @@ def install_openclaw!
|
|
|
93
93
|
# Fix ownership of ~/.openclaw if sudo created it as root
|
|
94
94
|
openclaw_dir = File.expand_path("~/.openclaw")
|
|
95
95
|
if File.exist?(openclaw_dir) && Process.uid != 0
|
|
96
|
-
system("sudo chown -R #{Process.uid}:#{Process.gid}
|
|
96
|
+
system("sudo", "chown", "-R", "#{Process.uid}:#{Process.gid}", openclaw_dir, err: "/dev/null")
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
99
|
|
|
@@ -249,22 +249,7 @@ def setup_yatfa_tools_for_claw!
|
|
|
249
249
|
# Fetch tool definitions from Rails now (during setup) so the plugin can
|
|
250
250
|
# register them synchronously — register() must NOT be async.
|
|
251
251
|
tool_defs_json = File.join(ext_dir, "tools.json")
|
|
252
|
-
|
|
253
|
-
uri = URI("#{yatfa_api_url}/mcp/tools")
|
|
254
|
-
req = Net::HTTP::Get.new(uri)
|
|
255
|
-
req["Content-Type"] = "application/json"
|
|
256
|
-
req["X-API-Key"] = yatfa_api_key
|
|
257
|
-
req["Accept"] = "application/json"
|
|
258
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
259
|
-
http.use_ssl = uri.scheme == "https"
|
|
260
|
-
resp = http.request(req)
|
|
261
|
-
tools_data = JSON.parse(resp.body)
|
|
262
|
-
File.write(tool_defs_json, JSON.pretty_generate(tools_data["tools"] || []))
|
|
263
|
-
puts "✅ Fetched #{(tools_data["tools"] || []).length} tool definitions from Yatfa API"
|
|
264
|
-
rescue => e
|
|
265
|
-
puts "⚠️ Failed to fetch tool definitions: #{e.message}"
|
|
266
|
-
File.write(tool_defs_json, "[]")
|
|
267
|
-
end
|
|
252
|
+
fetch_tool_definitions!(yatfa_api_url: yatfa_api_url, yatfa_api_key: yatfa_api_key, dest_path: tool_defs_json)
|
|
268
253
|
|
|
269
254
|
File.write(plugin_js, <<~JS)
|
|
270
255
|
import { t as definePluginEntry } from "#{rel_path}/#{plugin_entry_file}";
|
|
@@ -402,6 +387,38 @@ def setup_yatfa_tools_for_claw!
|
|
|
402
387
|
puts "✅ yatfa-bridge skill installed to #{skills_dir}"
|
|
403
388
|
end
|
|
404
389
|
|
|
390
|
+
# Fetch MCP tool definitions from the Yatfa backend and write them to +dest_path+
|
|
391
|
+
# as a JSON array. Called during claw setup so the OpenClaw plugin can register
|
|
392
|
+
# tools synchronously.
|
|
393
|
+
#
|
|
394
|
+
# Under the raise-based HTTP contract, a nil tools_data means an empty 2xx body
|
|
395
|
+
# (real failures raise YatfaAgent::HTTP::Error below). On any failure the
|
|
396
|
+
# destination is still written as "[]" so the plugin can boot with no tools.
|
|
397
|
+
def fetch_tool_definitions!(yatfa_api_url:, yatfa_api_key:, dest_path:)
|
|
398
|
+
tools_data = YatfaAgent::HTTP.get(
|
|
399
|
+
"#{yatfa_api_url}/mcp/tools",
|
|
400
|
+
headers: { "Content-Type" => "application/json", "X-API-Key" => yatfa_api_key, "Accept" => "application/json" }
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Empty 2xx body — raise-based contract returns nil only then.
|
|
404
|
+
tools_list = tools_data&.dig("tools") || []
|
|
405
|
+
File.write(dest_path, JSON.pretty_generate(tools_list))
|
|
406
|
+
puts "✅ Fetched #{tools_list.length} tool definitions from Yatfa API"
|
|
407
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
408
|
+
case e.status_code
|
|
409
|
+
when 401, 403
|
|
410
|
+
puts "⚠️ Yatfa API key or token is invalid or unauthorized (HTTP #{e.status_code})"
|
|
411
|
+
when 500..599
|
|
412
|
+
puts "⚠️ Yatfa server returned HTTP #{e.status_code}; service may be unavailable, please retry"
|
|
413
|
+
else
|
|
414
|
+
puts "⚠️ Failed to fetch tool definitions (HTTP #{e.status_code})"
|
|
415
|
+
end
|
|
416
|
+
File.write(dest_path, "[]")
|
|
417
|
+
rescue StandardError => e
|
|
418
|
+
puts "⚠️ Failed to fetch tool definitions: #{e.message}"
|
|
419
|
+
File.write(dest_path, "[]")
|
|
420
|
+
end
|
|
421
|
+
|
|
405
422
|
def run_openclaw_gateway!
|
|
406
423
|
port = ENV["OPENCLAW_PORT"] || "18789"
|
|
407
424
|
|
|
@@ -491,4 +508,11 @@ end
|
|
|
491
508
|
|
|
492
509
|
# ─── Main ─────────────────────────────────────────────────────────────────
|
|
493
510
|
|
|
494
|
-
|
|
511
|
+
# This file is normally required by setup-agent.rb, which invokes
|
|
512
|
+
# setup_claw_agent! explicitly (see the claw branch there). The guard below
|
|
513
|
+
# preserves `ruby setup-claw-agent.rb` as a direct entry point while letting
|
|
514
|
+
# the file be required as a library (e.g. by the spec suite) without
|
|
515
|
+
# auto-running setup.
|
|
516
|
+
if __FILE__ == $PROGRAM_NAME
|
|
517
|
+
setup_claw_agent!
|
|
518
|
+
end
|
package/setup/setup-github.rb
CHANGED
|
@@ -116,8 +116,8 @@ def setup_github_auth!
|
|
|
116
116
|
# Install helper via sudo to /usr/local/bin
|
|
117
117
|
helper_path = "/usr/local/bin/git-auth-helper"
|
|
118
118
|
File.write("/tmp/git-auth-helper", helper_content)
|
|
119
|
-
system("sudo mv /tmp/git-auth-helper
|
|
120
|
-
system("sudo chmod +x
|
|
119
|
+
system("sudo", "mv", "/tmp/git-auth-helper", helper_path)
|
|
120
|
+
system("sudo", "chmod", "+x", helper_path)
|
|
121
121
|
|
|
122
122
|
# Configure git (array form avoids shell interpolation of helper path)
|
|
123
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")
|
|
@@ -167,7 +167,7 @@ def install_gh_wrapper!(real_gh_path:, with_token_refresh: false, token_helper_p
|
|
|
167
167
|
|
|
168
168
|
# Move real gh to gh.real if not already done
|
|
169
169
|
unless File.exist?("#{real_gh_path}.real")
|
|
170
|
-
system("sudo mv
|
|
170
|
+
system("sudo", "mv", real_gh_path, "#{real_gh_path}.real")
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
wrapper_path = "/usr/local/bin/gh"
|
|
@@ -224,7 +224,7 @@ def install_gh_wrapper!(real_gh_path:, with_token_refresh: false, token_helper_p
|
|
|
224
224
|
BASH
|
|
225
225
|
|
|
226
226
|
File.write("/tmp/gh-wrapper", wrapper_content)
|
|
227
|
-
system("sudo mv /tmp/gh-wrapper
|
|
228
|
-
system("sudo chmod +x
|
|
227
|
+
system("sudo", "mv", "/tmp/gh-wrapper", wrapper_path)
|
|
228
|
+
system("sudo", "chmod", "+x", wrapper_path)
|
|
229
229
|
true
|
|
230
230
|
end
|
package/setup/skills.rb
CHANGED
|
@@ -61,28 +61,35 @@ def fetch_community_skills!
|
|
|
61
61
|
return []
|
|
62
62
|
end
|
|
63
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
64
|
begin
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
data = YatfaAgent::HTTP.get(
|
|
66
|
+
"#{rails_api_url}/whoami",
|
|
67
|
+
headers: { "X-API-Key" => rails_api_key, "Accept" => "application/json" }
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Under the raise-based HTTP contract, a nil result means an empty 2xx body
|
|
71
|
+
# (real failures raise YatfaAgent::HTTP::Error below).
|
|
72
|
+
unless data
|
|
73
|
+
puts "⚠️ Empty response from /whoami; no community skills configured"
|
|
74
|
+
return []
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
skills = data["community_skills"] || []
|
|
78
|
+
if skills.any?
|
|
79
|
+
puts "📦 Found #{skills.size} community skills: #{skills.join(', ')}"
|
|
80
|
+
end
|
|
81
|
+
skills
|
|
82
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
83
|
+
case e.status_code
|
|
84
|
+
when 401, 403
|
|
85
|
+
puts "⚠️ Yatfa API key or token is invalid or unauthorized (HTTP #{e.status_code})"
|
|
86
|
+
when 500..599
|
|
87
|
+
puts "⚠️ Yatfa server returned HTTP #{e.status_code}; service may be unavailable, please retry"
|
|
81
88
|
else
|
|
82
|
-
puts "⚠️
|
|
83
|
-
[]
|
|
89
|
+
puts "⚠️ Failed to fetch community skills (HTTP #{e.status_code})"
|
|
84
90
|
end
|
|
85
|
-
|
|
91
|
+
[]
|
|
92
|
+
rescue StandardError => e
|
|
86
93
|
puts "⚠️ Error fetching community skills: #{e.message}"
|
|
87
94
|
[]
|
|
88
95
|
end
|
package/setup/system_prompt.rb
CHANGED
|
@@ -28,41 +28,45 @@ def setup_system_prompt!
|
|
|
28
28
|
# Fetch system prompt from yatfa API
|
|
29
29
|
# Agent prompts are stored in yatfa/prompts/<agent-type>/base.md
|
|
30
30
|
# and loaded into the database by AgentPromptLoader
|
|
31
|
-
prompt_url =
|
|
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"
|
|
31
|
+
prompt_url = "#{rails_api_url}/internal/skill?tags%5B%5D=#{agent_type}&tags%5B%5D=base"
|
|
40
32
|
|
|
41
33
|
banner_content = nil
|
|
42
34
|
begin
|
|
43
35
|
puts "📥 Fetching system prompt for #{agent_type}..."
|
|
44
|
-
|
|
36
|
+
data = YatfaAgent::HTTP.get(
|
|
37
|
+
prompt_url,
|
|
38
|
+
headers: { "X-API-Key" => rails_api_key, "Accept" => "application/json" },
|
|
39
|
+
open_timeout: 10,
|
|
40
|
+
read_timeout: 15
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Under the raise-based HTTP contract, a nil result means an empty 2xx body
|
|
44
|
+
# (real failures raise YatfaAgent::HTTP::Error below).
|
|
45
|
+
unless data
|
|
46
|
+
puts "❌ Empty response from system-prompt endpoint"
|
|
47
|
+
puts " Make sure AgentPromptLoader has loaded prompts/ directory"
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
data = JSON.parse(response.body)
|
|
48
|
-
banner_content = data["content"]
|
|
51
|
+
banner_content = data["content"]
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
unless banner_content && !banner_content.empty?
|
|
54
|
+
puts "❌ System prompt for '#{agent_type}' is empty"
|
|
55
|
+
exit 1
|
|
56
|
+
end
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
puts "✅ Fetched system prompt for #{agent_type}"
|
|
59
|
+
rescue YatfaAgent::HTTP::Error => e
|
|
60
|
+
case e.status_code
|
|
61
|
+
when 401, 403
|
|
62
|
+
puts "❌ Yatfa API key or token is invalid or unauthorized (HTTP #{e.status_code})"
|
|
63
|
+
when 500..599
|
|
64
|
+
puts "❌ Yatfa server returned HTTP #{e.status_code}; service may be unavailable, please retry"
|
|
56
65
|
else
|
|
57
|
-
puts "❌ Failed to fetch system prompt
|
|
58
|
-
puts " Response: #{response.body}"
|
|
59
|
-
puts " Make sure AgentPromptLoader has loaded prompts/ directory"
|
|
60
|
-
exit 1
|
|
66
|
+
puts "❌ Failed to fetch system prompt (HTTP #{e.status_code})"
|
|
61
67
|
end
|
|
62
|
-
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
63
|
-
puts "❌ Timeout fetching system prompt: #{e.message}"
|
|
64
68
|
exit 1
|
|
65
|
-
rescue => e
|
|
69
|
+
rescue StandardError => e
|
|
66
70
|
puts "❌ Error fetching system prompt: #{e.message}"
|
|
67
71
|
exit 1
|
|
68
72
|
end
|