yatfa 1.0.79 → 1.0.80
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/install-agent.sh +13 -9
- package/lib/yatfa_agent/agent.rb +8 -29
- package/package.json +2 -1
- package/setup/setup-agent.rb +1038 -0
- package/setup/setup-claw-agent.rb +494 -0
- package/setup/setup-github.rb +222 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Yatfa Claw Agent Setup - Loaded by setup-agent.rb when AGENT_TYPE == "claw"
|
|
5
|
+
#
|
|
6
|
+
# This file contains the completely separate setup flow for claw agents:
|
|
7
|
+
# - Validates claw-specific environment variables
|
|
8
|
+
# - Installs OpenClaw gateway via npm
|
|
9
|
+
# - Configures OpenClaw for Telegram + LLM provider
|
|
10
|
+
# - Installs yatfa-tools native OpenClaw plugin
|
|
11
|
+
# - Starts the gateway in tmux
|
|
12
|
+
#
|
|
13
|
+
# Dependencies:
|
|
14
|
+
# - fetch_llm_credentials! is defined by the parent setup-agent.rb
|
|
15
|
+
# - Standard library requires (json, fileutils, pathname, etc.) are loaded
|
|
16
|
+
# by setup-agent.rb before this file is required
|
|
17
|
+
|
|
18
|
+
def check_env_claw!
|
|
19
|
+
required = %w[OPENCLAW_TELEGRAM_BOT_TOKEN]
|
|
20
|
+
missing = required.select { |var| ENV[var].to_s.empty? }
|
|
21
|
+
|
|
22
|
+
unless missing.empty?
|
|
23
|
+
puts "❌ Missing environment variables for claw agent: #{missing.join(', ')}"
|
|
24
|
+
puts ""
|
|
25
|
+
puts "Required:"
|
|
26
|
+
puts " OPENCLAW_TELEGRAM_BOT_TOKEN - Telegram bot token for the Pi agent"
|
|
27
|
+
puts ""
|
|
28
|
+
puts "Optional:"
|
|
29
|
+
puts " OPENCLAW_PORT - Gateway port (default: 18789)"
|
|
30
|
+
puts " OPENCLAW_MODEL - Model to use (default: from openclaw config)"
|
|
31
|
+
puts " OPENCLAW_API_KEY - API key for the model provider"
|
|
32
|
+
puts " OPENCLAW_BASE_URL - Base URL for the model provider"
|
|
33
|
+
puts ""
|
|
34
|
+
puts "Yatfa Integration (optional - enables Pi to interact with Yatfa team):"
|
|
35
|
+
puts " YATFA_API_URL - Yatfa API URL (derived from YATFA_URL)"
|
|
36
|
+
puts " YATFA_API_KEY - API key for yatfa-mcp tools"
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Derive YATFA_API_URL from YATFA_URL if set (optional for claw)
|
|
41
|
+
if ENV["YATFA_URL"] && !ENV["YATFA_API_URL"]
|
|
42
|
+
ENV["YATFA_API_URL"] = "#{ENV['YATFA_URL']}/api/v1"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts "✅ Claw environment validated"
|
|
46
|
+
puts " Telegram Bot Token: #{ENV['OPENCLAW_TELEGRAM_BOT_TOKEN'][0..10]}..."
|
|
47
|
+
puts " Gateway Port: #{ENV['OPENCLAW_PORT'] || 18789}"
|
|
48
|
+
|
|
49
|
+
if ENV["YATFA_API_URL"] && ENV["YATFA_API_KEY"]
|
|
50
|
+
puts " Yatfa Integration: enabled (#{ENV['YATFA_API_URL']})"
|
|
51
|
+
else
|
|
52
|
+
puts " Yatfa Integration: disabled (set YATFA_API_URL and YATFA_API_KEY to enable)"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def install_openclaw!
|
|
57
|
+
puts "📦 Installing OpenClaw gateway..."
|
|
58
|
+
|
|
59
|
+
# Ensure Node.js 22+ is available (required by openclaw)
|
|
60
|
+
node_version = `node --version 2>/dev/null`.strip
|
|
61
|
+
if node_version.empty?
|
|
62
|
+
puts "❌ Node.js is not installed. OpenClaw requires Node.js 22+."
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
node_major = node_version.gsub(/^v/, "").split(".").first.to_i
|
|
67
|
+
if node_major < 22
|
|
68
|
+
puts "❌ Node.js #{node_version} is too old. OpenClaw requires Node.js >= 22.14.0."
|
|
69
|
+
exit 1
|
|
70
|
+
end
|
|
71
|
+
puts " Node.js version: #{node_version}"
|
|
72
|
+
|
|
73
|
+
# Install OpenClaw globally (sudo needed in container for global npm dir)
|
|
74
|
+
install_cmd = "npm install -g openclaw@latest 2>&1"
|
|
75
|
+
install_cmd = "sudo #{install_cmd}" unless Process.uid == 0
|
|
76
|
+
|
|
77
|
+
output = `#{install_cmd} 2>&1`
|
|
78
|
+
unless $?.success?
|
|
79
|
+
puts "❌ Failed to install openclaw via npm:"
|
|
80
|
+
puts output
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Verify installation
|
|
85
|
+
unless system("which openclaw > /dev/null 2>&1")
|
|
86
|
+
puts "❌ openclaw command not found after installation"
|
|
87
|
+
exit 1
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
openclaw_version = `openclaw --version 2>/dev/null`.strip
|
|
91
|
+
puts "✅ OpenClaw installed (version: #{openclaw_version.empty? ? 'unknown' : openclaw_version})"
|
|
92
|
+
|
|
93
|
+
# Fix ownership of ~/.openclaw if sudo created it as root
|
|
94
|
+
openclaw_dir = File.expand_path("~/.openclaw")
|
|
95
|
+
if File.exist?(openclaw_dir) && Process.uid != 0
|
|
96
|
+
system("sudo chown -R #{Process.uid}:#{Process.gid} #{openclaw_dir} 2>/dev/null")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def setup_openclaw_config!(llm_credentials = nil)
|
|
101
|
+
config_dir = File.expand_path("~/.openclaw")
|
|
102
|
+
FileUtils.mkdir_p(config_dir)
|
|
103
|
+
|
|
104
|
+
# Ensure config dir is owned by current user (sudo npm install may create it as root)
|
|
105
|
+
File.chmod(0700, config_dir) if File.exist?(config_dir)
|
|
106
|
+
|
|
107
|
+
config_path = File.join(config_dir, "openclaw.json")
|
|
108
|
+
|
|
109
|
+
# Build config with correct OpenClaw schema:
|
|
110
|
+
# models.providers.<id>.apiKey / baseUrl — provider credentials
|
|
111
|
+
# agents.defaults.model — "provider/model" format
|
|
112
|
+
config = {
|
|
113
|
+
"gateway" => { "mode" => "local" },
|
|
114
|
+
"models" => { "providers" => {} },
|
|
115
|
+
"agents" => { "defaults" => {} },
|
|
116
|
+
"channels" => {
|
|
117
|
+
"telegram" => {
|
|
118
|
+
"botToken" => ENV["OPENCLAW_TELEGRAM_BOT_TOKEN"]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"plugins" => { "enabled" => true }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Resolve API key, base URL, and model — from backend or env vars
|
|
125
|
+
api_key = ENV["OPENCLAW_API_KEY"]
|
|
126
|
+
base_url = ENV["OPENCLAW_BASE_URL"]
|
|
127
|
+
model = ENV["OPENCLAW_MODEL"]
|
|
128
|
+
|
|
129
|
+
if llm_credentials
|
|
130
|
+
api_key ||= llm_credentials["api_key"]
|
|
131
|
+
base_url ||= llm_credentials["base_url"] if llm_credentials["base_url"] && !llm_credentials["base_url"].empty?
|
|
132
|
+
model ||= llm_credentials["model_sonnet"] || llm_credentials["model_opus"] || llm_credentials["model_haiku"]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if api_key
|
|
136
|
+
# Detect API format from base URL (anthropic-messages vs openai-completions)
|
|
137
|
+
api_format = "openai-completions"
|
|
138
|
+
if base_url && (base_url.include?("anthropic") || base_url.include?("claude"))
|
|
139
|
+
api_format = "anthropic-messages"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Register provider with id "yatfa" — requires baseUrl and models array
|
|
143
|
+
provider_entry = {
|
|
144
|
+
"apiKey" => api_key,
|
|
145
|
+
"baseUrl" => base_url || "https://api.anthropic.com",
|
|
146
|
+
"models" => [
|
|
147
|
+
{
|
|
148
|
+
"id" => model || "default",
|
|
149
|
+
"name" => model || "default",
|
|
150
|
+
"api" => api_format
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
config["models"]["providers"]["yatfa"] = provider_entry
|
|
155
|
+
|
|
156
|
+
# Prefix model with provider if not already prefixed
|
|
157
|
+
if model && !model.empty?
|
|
158
|
+
config["agents"]["defaults"]["model"] = model.include?("/") ? model : "yatfa/#{model}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
puts " Provider: yatfa (#{api_format})"
|
|
162
|
+
puts " Backend model: #{model || 'default'}"
|
|
163
|
+
puts " Backend base URL: #{base_url || 'default'}"
|
|
164
|
+
else
|
|
165
|
+
# No credentials — just set model if provided (uses built-in providers)
|
|
166
|
+
if model && !model.empty?
|
|
167
|
+
config["agents"]["defaults"]["model"] = model
|
|
168
|
+
end
|
|
169
|
+
puts " No API key configured — using openclaw defaults"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Remove empty sections
|
|
173
|
+
config["models"].delete("providers") if config["models"]["providers"].empty?
|
|
174
|
+
config.delete("models") if config["models"].empty?
|
|
175
|
+
config["agents"].delete("defaults") if config["agents"]["defaults"].empty?
|
|
176
|
+
config.delete("agents") if config["agents"].empty?
|
|
177
|
+
|
|
178
|
+
# Write config (do not overwrite if file exists and has custom config)
|
|
179
|
+
if File.exist?(config_path)
|
|
180
|
+
begin
|
|
181
|
+
existing = JSON.parse(File.read(config_path))
|
|
182
|
+
# Backend (new) values win for models/agents; existing wins for everything else
|
|
183
|
+
config = existing.merge(config) { |key, old_val, new_val|
|
|
184
|
+
if key == "models" || key == "agents"
|
|
185
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
186
|
+
old_val.merge(new_val) # new (backend) overrides old
|
|
187
|
+
else
|
|
188
|
+
new_val
|
|
189
|
+
end
|
|
190
|
+
elsif old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
191
|
+
new_val.merge(old_val) # old (user) overrides new
|
|
192
|
+
else
|
|
193
|
+
old_val
|
|
194
|
+
end
|
|
195
|
+
}
|
|
196
|
+
puts "📝 Merging with existing OpenClaw config"
|
|
197
|
+
rescue JSON::ParserError
|
|
198
|
+
puts "⚠️ Existing openclaw.json is invalid, overwriting"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
File.write(config_path, JSON.pretty_generate(config))
|
|
203
|
+
puts "✅ OpenClaw config written to #{config_path}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def setup_yatfa_tools_for_claw!
|
|
207
|
+
# Installs the yatfa-tools native OpenClaw plugin so Pi's main agent session
|
|
208
|
+
# can use yatfa tools directly (tickets, memories, knowledge, etc.)
|
|
209
|
+
# Also installs the yatfa-bridge skill for context on how to use them.
|
|
210
|
+
yatfa_api_url = ENV["YATFA_API_URL"]
|
|
211
|
+
yatfa_api_key = ENV["YATFA_API_KEY"]
|
|
212
|
+
|
|
213
|
+
unless yatfa_api_url && !yatfa_api_url.empty? && yatfa_api_key && !yatfa_api_key.empty?
|
|
214
|
+
puts "ℹ️ Yatfa integration disabled (YATFA_API_URL or YATFA_API_KEY not set)"
|
|
215
|
+
return
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
puts "🔗 Setting up Yatfa integration for Pi agent..."
|
|
219
|
+
|
|
220
|
+
# Install native OpenClaw plugin to ~/.openclaw/extensions/yatfa-tools/
|
|
221
|
+
ext_dir = File.expand_path("~/.openclaw/extensions/yatfa-tools")
|
|
222
|
+
FileUtils.mkdir_p(ext_dir)
|
|
223
|
+
|
|
224
|
+
plugin_js = File.join(ext_dir, "index.js")
|
|
225
|
+
plugin_manifest = File.join(ext_dir, "openclaw.plugin.json")
|
|
226
|
+
plugin_pkg = File.join(ext_dir, "package.json")
|
|
227
|
+
|
|
228
|
+
# Resolve path to the openclaw dist directory for the definePluginEntry import
|
|
229
|
+
openclaw_dist = nil
|
|
230
|
+
plugin_entry_file = nil
|
|
231
|
+
["/usr/lib/node_modules/openclaw/dist", "/usr/local/lib/node_modules/openclaw/dist"].each do |candidate|
|
|
232
|
+
next unless File.directory?(candidate)
|
|
233
|
+
matches = Dir.glob(File.join(candidate, "plugin-entry-*.js"))
|
|
234
|
+
if matches.any?
|
|
235
|
+
openclaw_dist = candidate
|
|
236
|
+
plugin_entry_file = File.basename(matches.first)
|
|
237
|
+
break
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
unless openclaw_dist && plugin_entry_file
|
|
242
|
+
puts "⚠️ Cannot find OpenClaw dist directory — skipping plugin install"
|
|
243
|
+
return
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Compute relative path from ext_dir to openclaw dist for the import
|
|
247
|
+
rel_path = Pathname.new(openclaw_dist).relative_path_from(Pathname.new(ext_dir))
|
|
248
|
+
|
|
249
|
+
# Fetch tool definitions from Rails now (during setup) so the plugin can
|
|
250
|
+
# register them synchronously — register() must NOT be async.
|
|
251
|
+
tool_defs_json = File.join(ext_dir, "tools.json")
|
|
252
|
+
begin
|
|
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
|
|
268
|
+
|
|
269
|
+
File.write(plugin_js, <<~JS)
|
|
270
|
+
import { t as definePluginEntry } from "#{rel_path}/#{plugin_entry_file}";
|
|
271
|
+
import { readFileSync } from "node:fs";
|
|
272
|
+
import { fileURLToPath } from "node:url";
|
|
273
|
+
import { dirname, join } from "node:path";
|
|
274
|
+
|
|
275
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
276
|
+
const __dirname = dirname(__filename);
|
|
277
|
+
|
|
278
|
+
const API_BASE_URL = #{yatfa_api_url.inspect};
|
|
279
|
+
const API_KEY = #{yatfa_api_key.inspect};
|
|
280
|
+
|
|
281
|
+
const toolDefs = JSON.parse(readFileSync(join(__dirname, "tools.json"), "utf8"));
|
|
282
|
+
|
|
283
|
+
export default definePluginEntry({
|
|
284
|
+
id: "yatfa-tools",
|
|
285
|
+
name: "Yatfa Tools",
|
|
286
|
+
description: "Yatfa team management tools — tickets, memories, knowledge, agents, and more.",
|
|
287
|
+
|
|
288
|
+
register(api) {
|
|
289
|
+
for (const def of toolDefs) {
|
|
290
|
+
api.registerTool({
|
|
291
|
+
name: def.name,
|
|
292
|
+
description: def.description,
|
|
293
|
+
parameters: def.inputSchema || { type: "object", properties: {} },
|
|
294
|
+
async execute(_id, params) {
|
|
295
|
+
try {
|
|
296
|
+
const url = `${API_BASE_URL}/mcp/execute`;
|
|
297
|
+
const res = await fetch(url, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers: {
|
|
300
|
+
"Content-Type": "application/json",
|
|
301
|
+
"X-API-Key": API_KEY,
|
|
302
|
+
Accept: "application/json",
|
|
303
|
+
},
|
|
304
|
+
body: JSON.stringify({ tool: def.name, params }),
|
|
305
|
+
});
|
|
306
|
+
if (!res.ok) {
|
|
307
|
+
const body = await res.text();
|
|
308
|
+
return { content: [{ type: "text", text: `API Error ${res.status}: ${body}` }], isError: true };
|
|
309
|
+
}
|
|
310
|
+
const data = await res.json();
|
|
311
|
+
return { content: [{ type: "text", text: JSON.stringify(data.result, null, 2) }] };
|
|
312
|
+
} catch (err) {
|
|
313
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
JS
|
|
321
|
+
|
|
322
|
+
File.write(plugin_manifest, <<~JSON)
|
|
323
|
+
{
|
|
324
|
+
"id": "yatfa-tools",
|
|
325
|
+
"name": "Yatfa Tools",
|
|
326
|
+
"description": "Yatfa team management tools — tickets, memories, knowledge, agents, and more.",
|
|
327
|
+
"configSchema": {
|
|
328
|
+
"type": "object",
|
|
329
|
+
"additionalProperties": false,
|
|
330
|
+
"properties": {}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
JSON
|
|
334
|
+
|
|
335
|
+
File.write(plugin_pkg, <<~JSON)
|
|
336
|
+
{
|
|
337
|
+
"name": "@yatfa/openclaw-plugin",
|
|
338
|
+
"version": "1.0.0",
|
|
339
|
+
"type": "module",
|
|
340
|
+
"openclaw": {
|
|
341
|
+
"extensions": ["./index.js"],
|
|
342
|
+
"compat": { "pluginApi": ">=2026.4.10" }
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
JSON
|
|
346
|
+
|
|
347
|
+
puts "✅ yatfa-tools plugin installed to #{ext_dir}"
|
|
348
|
+
|
|
349
|
+
# Install yatfa-bridge skill
|
|
350
|
+
skills_dir = File.expand_path("~/.openclaw/skills/yatfa-bridge")
|
|
351
|
+
FileUtils.mkdir_p(skills_dir)
|
|
352
|
+
|
|
353
|
+
skill_content = <<~SKILL
|
|
354
|
+
# Yatfa Bridge - Pi Agent Integration
|
|
355
|
+
|
|
356
|
+
You have access to the Yatfa team's task management system. You are the human's **front desk** to the Yatfa agent team.
|
|
357
|
+
|
|
358
|
+
## What You Can Do
|
|
359
|
+
|
|
360
|
+
Use the yatfa tools to:
|
|
361
|
+
|
|
362
|
+
- **Check status**: `get_status` — see project overview, ticket counts, agent availability
|
|
363
|
+
- **View tickets**: `list_tickets` — see all tickets with optional filters (status, priority)
|
|
364
|
+
- **Get ticket details**: `get_ticket` — full details of a specific ticket
|
|
365
|
+
- **Create tickets**: `create_ticket` — create new tasks or bugs
|
|
366
|
+
- **Add comments**: `add_comment` — add notes, questions, or decisions to tickets
|
|
367
|
+
- **Search knowledge**: `search_knowledge_articles` — find project documentation
|
|
368
|
+
- **Read knowledge**: `get_knowledge_article` — read a specific article
|
|
369
|
+
- **Search tickets**: `search_tickets` — semantically search for related tickets
|
|
370
|
+
- **Search memories**: `search_memory` — find relevant agent memories
|
|
371
|
+
- **Store memories**: `store_memory` — persist learnings or context for future reference
|
|
372
|
+
|
|
373
|
+
## How to Help the Human
|
|
374
|
+
|
|
375
|
+
When the human messages you on Telegram:
|
|
376
|
+
|
|
377
|
+
1. **Status requests**: "What's the team working on?" → Use `get_status` and `list_tickets`
|
|
378
|
+
2. **Ticket creation**: "Create a task to fix the login bug" → Use `create_ticket`
|
|
379
|
+
3. **Ticket updates**: "What's the status of TINK-42?" → Use `get_ticket`
|
|
380
|
+
4. **Questions**: "Why was this rejected?" → Use `get_ticket` and `list_comments`
|
|
381
|
+
5. **Knowledge**: "What's the deployment process?" → Use `search_knowledge_articles`
|
|
382
|
+
|
|
383
|
+
## Important Guidelines
|
|
384
|
+
|
|
385
|
+
- Always confirm with the human before creating tickets (show title and description first)
|
|
386
|
+
- Use human-friendly language — translate ticket statuses and technical terms
|
|
387
|
+
- Don't modify or transition tickets — that's the agents' job
|
|
388
|
+
- If unsure, ask the human for clarification
|
|
389
|
+
- Keep responses concise for Telegram (avoid overly long messages)
|
|
390
|
+
|
|
391
|
+
## Ticket Statuses
|
|
392
|
+
|
|
393
|
+
- **draft/backlog/todo**: Not yet started
|
|
394
|
+
- **in_progress**: Currently being worked on by a team member
|
|
395
|
+
- **pending_audit**: Awaiting code review
|
|
396
|
+
- **pending_approval**: Ready for final approval
|
|
397
|
+
- **done**: Completed
|
|
398
|
+
- **blocked**: Something is preventing progress
|
|
399
|
+
SKILL
|
|
400
|
+
|
|
401
|
+
File.write(File.join(skills_dir, "SKILL.md"), skill_content)
|
|
402
|
+
puts "✅ yatfa-bridge skill installed to #{skills_dir}"
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def run_openclaw_gateway!
|
|
406
|
+
port = ENV["OPENCLAW_PORT"] || "18789"
|
|
407
|
+
|
|
408
|
+
# Validate port is numeric to prevent command injection
|
|
409
|
+
unless port =~ /\A\d+\z/
|
|
410
|
+
puts "❌ Invalid OPENCLAW_PORT: must be numeric, got '#{port}'"
|
|
411
|
+
exit 1
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
puts ""
|
|
415
|
+
puts "🐾 Starting OpenClaw Gateway..."
|
|
416
|
+
puts " Port: #{port}"
|
|
417
|
+
puts " Press Ctrl+B then D to detach from tmux"
|
|
418
|
+
puts ""
|
|
419
|
+
|
|
420
|
+
# Use tmux for persistent session with status bar
|
|
421
|
+
# Array form of system() prevents shell injection
|
|
422
|
+
system("tmux", "new-session", "-d", "-s", "agent", "-x", "220", "-y", "50",
|
|
423
|
+
"openclaw gateway --port #{port} --verbose",
|
|
424
|
+
err: File::NULL)
|
|
425
|
+
|
|
426
|
+
# Enable mouse mode (scroll, pane selection)
|
|
427
|
+
system("tmux set-option -t agent mouse on 2>/dev/null")
|
|
428
|
+
|
|
429
|
+
# Set up status bar
|
|
430
|
+
system("tmux set-option -t agent status-position bottom 2>/dev/null")
|
|
431
|
+
system("tmux set-option -t agent status-style 'bg=#1a1a2e,fg=#e0e0e0' 2>/dev/null")
|
|
432
|
+
system("tmux set-option -t agent status-left-length 30 2>/dev/null")
|
|
433
|
+
system("tmux set-option -t agent status-left ' 🐾 OpenClaw Gateway ' 2>/dev/null")
|
|
434
|
+
system("tmux set-option -t agent status-right ' %Y-%m-%d %H:%M ' 2>/dev/null")
|
|
435
|
+
|
|
436
|
+
# Wait for gateway to start, then verify the port is listening
|
|
437
|
+
sleep 2
|
|
438
|
+
|
|
439
|
+
gateway_running = false
|
|
440
|
+
5.times do |i|
|
|
441
|
+
begin
|
|
442
|
+
socket = TCPSocket.new("localhost", port.to_i)
|
|
443
|
+
socket.close
|
|
444
|
+
gateway_running = true
|
|
445
|
+
break
|
|
446
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET
|
|
447
|
+
sleep 1
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
if gateway_running
|
|
452
|
+
puts "✅ OpenClaw gateway started in tmux session 'agent'"
|
|
453
|
+
puts " Health check: curl http://localhost:#{port}/healthz"
|
|
454
|
+
else
|
|
455
|
+
puts "⚠️ Gateway port not responding after 7 seconds"
|
|
456
|
+
puts " Check logs: docker exec <container> tmux capture-pane -t agent -p"
|
|
457
|
+
# Don't exit - gateway may still be initializing
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Block: keep the script alive so the container doesn't restart
|
|
461
|
+
# The gateway runs inside tmux — periodically check it's still running
|
|
462
|
+
puts " Keeping container alive (gateway runs in tmux)..."
|
|
463
|
+
loop do
|
|
464
|
+
sleep 60
|
|
465
|
+
# If tmux session died, exit so the container can restart
|
|
466
|
+
unless system("tmux has-session -t agent 2>/dev/null")
|
|
467
|
+
puts "⚠️ tmux session 'agent' died, exiting..."
|
|
468
|
+
break
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def setup_claw_agent!
|
|
474
|
+
puts ""
|
|
475
|
+
puts "🐾 Yatfa Claw Agent Setup"
|
|
476
|
+
puts "=========================="
|
|
477
|
+
puts ""
|
|
478
|
+
|
|
479
|
+
check_env_claw!
|
|
480
|
+
install_openclaw!
|
|
481
|
+
llm_credentials = fetch_llm_credentials!
|
|
482
|
+
if llm_credentials
|
|
483
|
+
puts "✅ Fetched LLM credentials from backend (source: #{llm_credentials['source']})"
|
|
484
|
+
else
|
|
485
|
+
puts "⚠️ No LLM credentials from backend — relying on OPENCLAW_* env vars"
|
|
486
|
+
end
|
|
487
|
+
setup_openclaw_config!(llm_credentials)
|
|
488
|
+
setup_yatfa_tools_for_claw!
|
|
489
|
+
run_openclaw_gateway!
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# ─── Main ─────────────────────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
setup_claw_agent!
|