tinker-agent 1.0.60 → 1.0.61
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 +7 -0
- package/lib/tinker_agent/agent.rb +0 -1
- package/package.json +1 -2
- package/setup-agent.rb +0 -961
package/bin/install-agent.sh
CHANGED
|
@@ -85,9 +85,16 @@ chmod +x /etc/profile.d/claude.sh
|
|
|
85
85
|
echo "Installing Github CLI"
|
|
86
86
|
# Install GitHub CLI
|
|
87
87
|
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
|
88
|
+
chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
|
|
88
89
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list
|
|
89
90
|
apt-get update && apt-get install gh -y
|
|
90
91
|
|
|
92
|
+
# Download setup-agent.rb script
|
|
93
|
+
TINKER_VERSION=${TINKER_VERSION:-main}
|
|
94
|
+
echo "Downloading setup-agent.rb (version: ${TINKER_VERSION})..."
|
|
95
|
+
curl -fsSL https://objectstore.fra1.civo.com/tinker/${TINKER_VERSION}/setup-agent.rb -o /usr/local/bin/setup-agent.rb
|
|
96
|
+
chmod +x /usr/local/bin/setup-agent.rb
|
|
97
|
+
|
|
91
98
|
# Create entrypoint
|
|
92
99
|
cat << EOF > /entrypoint.sh
|
|
93
100
|
#!/bin/bash
|
|
@@ -240,7 +240,6 @@ module TinkerAgent
|
|
|
240
240
|
possible_paths = [
|
|
241
241
|
File.join(Dir.pwd, "overrides", "agent-bridge-linux-#{linux_arch}"), # TINKER_SANDBOX/overrides
|
|
242
242
|
File.join(Dir.pwd, "tinker-public", "dist", "agent-bridge-linux-#{linux_arch}"), # Running from metafolder
|
|
243
|
-
File.join(Dir.pwd, "..", "tinker-public", "dist", "agent-bridge-linux-#{linux_arch}"), # Running from TINKER_SANDBOX
|
|
244
243
|
File.join(Dir.pwd, "dist", "agent-bridge-linux-#{linux_arch}"), # Running from tinker-public
|
|
245
244
|
]
|
|
246
245
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinker-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.61",
|
|
4
4
|
"description": "Tinker Agent Runner",
|
|
5
5
|
"bin": "./bin/run-tinker-agent.rb",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin/run-tinker-agent.rb",
|
|
8
8
|
"agents.rb",
|
|
9
|
-
"setup-agent.rb",
|
|
10
9
|
"lib/",
|
|
11
10
|
"bin/agent-bridge-tmux",
|
|
12
11
|
"bin/install-agent.sh"
|
package/setup-agent.rb
DELETED
|
@@ -1,961 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# Tinker Agent - Run inside any Docker container with Ruby
|
|
5
|
-
#
|
|
6
|
-
# This script sets up and runs a Tinker agent. It:
|
|
7
|
-
# 1. Generates MCP config from environment variables
|
|
8
|
-
# 2. Verifies system prompt (agent banner) exists at /etc/tinker/system-prompt.txt
|
|
9
|
-
# 3. Downloads and runs agent-bridge
|
|
10
|
-
#
|
|
11
|
-
# Requirements (install in your container):
|
|
12
|
-
# - Ruby 3.x
|
|
13
|
-
# - Node.js 20+
|
|
14
|
-
# - tmux
|
|
15
|
-
# - git, curl, gh (GitHub CLI)
|
|
16
|
-
# - claude CLI: npm install -g @anthropic-ai/claude-code
|
|
17
|
-
#
|
|
18
|
-
# Environment variables:
|
|
19
|
-
# AGENT_TYPE - worker|planner|reviewer|researcher
|
|
20
|
-
# PROJECT_ID - Your Tinker project ID
|
|
21
|
-
# RAILS_WS_URL - WebSocket URL (wss://tinker.example.com/cable)
|
|
22
|
-
# RAILS_API_URL - API URL (https://tinker.example.com/api/v1)
|
|
23
|
-
# RAILS_API_KEY - MCP API key for this agent type
|
|
24
|
-
# GH_TOKEN - GitHub token (or use GitHub App auth)
|
|
25
|
-
#
|
|
26
|
-
# Usage:
|
|
27
|
-
# curl -fsSL https://raw.githubusercontent.com/RoM4iK/tinker-public/main/tinker-agent.rb | ruby
|
|
28
|
-
|
|
29
|
-
require "json"
|
|
30
|
-
require "fileutils"
|
|
31
|
-
require "open-uri"
|
|
32
|
-
require "openssl"
|
|
33
|
-
require "net/http"
|
|
34
|
-
require "uri"
|
|
35
|
-
require "base64"
|
|
36
|
-
require "time"
|
|
37
|
-
|
|
38
|
-
TINKER_VERSION = ENV["TINKER_VERSION"] || "main"
|
|
39
|
-
TINKER_RAW_URL = "https://raw.githubusercontent.com/RoM4iK/tinker-public/#{TINKER_VERSION}"
|
|
40
|
-
|
|
41
|
-
# Valid agent types
|
|
42
|
-
VALID_AGENT_TYPES = %w[planner worker reviewer researcher]
|
|
43
|
-
|
|
44
|
-
def check_env!
|
|
45
|
-
required = %w[RAILS_WS_URL]
|
|
46
|
-
missing = required.select { |var| ENV[var].to_s.empty? }
|
|
47
|
-
|
|
48
|
-
unless missing.empty?
|
|
49
|
-
puts "❌ Missing environment variables: #{missing.join(', ')}"
|
|
50
|
-
puts ""
|
|
51
|
-
puts "Required:"
|
|
52
|
-
puts " RAILS_WS_URL - WebSocket URL (wss://tinker.example.com/cable)"
|
|
53
|
-
puts ""
|
|
54
|
-
puts "Optional:"
|
|
55
|
-
puts " RAILS_API_URL - API URL for MCP tools"
|
|
56
|
-
puts " RAILS_API_KEY - MCP API key"
|
|
57
|
-
puts " GH_TOKEN - GitHub token"
|
|
58
|
-
exit 1
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
agent_type = ENV["AGENT_TYPE"]
|
|
62
|
-
if agent_type && !VALID_AGENT_TYPES.include?(agent_type)
|
|
63
|
-
puts "❌ Invalid AGENT_TYPE: #{agent_type}"
|
|
64
|
-
puts " Valid types: #{VALID_AGENT_TYPES.join(', ')}"
|
|
65
|
-
exit 1
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def fetch_agent_id!
|
|
70
|
-
rails_api_url = ENV["RAILS_API_URL"]
|
|
71
|
-
rails_api_key = ENV["RAILS_API_KEY"]
|
|
72
|
-
|
|
73
|
-
unless rails_api_url && !rails_api_url.empty? && rails_api_key && !rails_api_key.empty?
|
|
74
|
-
puts "❌ RAILS_API_URL or RAILS_API_KEY not set"
|
|
75
|
-
puts " Agent ID is required for the bridge to connect"
|
|
76
|
-
exit 1
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Fetch agent ID from Rails API using api_key
|
|
80
|
-
uri = URI("#{rails_api_url}/whoami")
|
|
81
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
82
|
-
http.use_ssl = uri.scheme == "https"
|
|
83
|
-
|
|
84
|
-
request = Net::HTTP::Get.new(uri)
|
|
85
|
-
request["X-API-Key"] = rails_api_key
|
|
86
|
-
request["Accept"] = "application/json"
|
|
87
|
-
|
|
88
|
-
begin
|
|
89
|
-
puts "🔍 Fetching agent ID from #{rails_api_url}/whoami..."
|
|
90
|
-
response = http.request(request)
|
|
91
|
-
|
|
92
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
93
|
-
data = JSON.parse(response.body)
|
|
94
|
-
agent_id = data["agent_id"] || data["id"]
|
|
95
|
-
|
|
96
|
-
if agent_id && !agent_id.to_s.empty?
|
|
97
|
-
ENV["AGENT_ID"] = agent_id.to_s
|
|
98
|
-
puts "✅ Agent ID fetched: #{agent_id}"
|
|
99
|
-
else
|
|
100
|
-
puts "❌ Could not find agent_id in API response"
|
|
101
|
-
puts " Response: #{response.body}"
|
|
102
|
-
exit 1
|
|
103
|
-
end
|
|
104
|
-
else
|
|
105
|
-
puts "❌ Failed to fetch agent ID: #{response.code} #{response.message}"
|
|
106
|
-
puts " Response: #{response.body}"
|
|
107
|
-
exit 1
|
|
108
|
-
end
|
|
109
|
-
rescue => e
|
|
110
|
-
puts "❌ Error fetching agent ID: #{e.message}"
|
|
111
|
-
puts " #{e.class}: #{e.backtrace.first}"
|
|
112
|
-
exit 1
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def setup_mcp_config!
|
|
117
|
-
# MCP config is project-specific and should be provided by the Dockerfile
|
|
118
|
-
# or mounted at runtime. This script only checks if it exists.
|
|
119
|
-
|
|
120
|
-
agent_type = ENV["AGENT_TYPE"]
|
|
121
|
-
rails_api_url = ENV["RAILS_API_URL"]
|
|
122
|
-
rails_api_key = ENV["RAILS_API_KEY"]
|
|
123
|
-
|
|
124
|
-
# Load existing config if present
|
|
125
|
-
existing_config = {}
|
|
126
|
-
if File.exist?(".mcp.json")
|
|
127
|
-
begin
|
|
128
|
-
existing_config = JSON.parse(File.read(".mcp.json"))
|
|
129
|
-
puts "✅ Found existing .mcp.json, merging configuration..."
|
|
130
|
-
rescue JSON::ParserError
|
|
131
|
-
puts "⚠️ Existing .mcp.json is invalid, starting fresh"
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Ensure mcpServers key exists
|
|
136
|
-
existing_config["mcpServers"] ||= {}
|
|
137
|
-
|
|
138
|
-
if rails_api_url && !rails_api_url.empty? && rails_api_key && !rails_api_key.empty?
|
|
139
|
-
# Install tinker-mcp locally to ensure we can run it with node (bypassing shebang issues)
|
|
140
|
-
tools_dir = File.expand_path("~/tinker-tools")
|
|
141
|
-
FileUtils.mkdir_p(tools_dir)
|
|
142
|
-
|
|
143
|
-
puts "📦 Installing tinker-mcp..."
|
|
144
|
-
# Redirect output to avoid cluttering logs, unless it fails
|
|
145
|
-
unless system("npm install --prefix #{tools_dir} tinker-mcp > /dev/null 2>&1")
|
|
146
|
-
puts "❌ Failed to install tinker-mcp"
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
script_path = "#{tools_dir}/node_modules/tinker-mcp/dist/index.js"
|
|
150
|
-
|
|
151
|
-
tinker_server_config = {
|
|
152
|
-
"command" => "node",
|
|
153
|
-
"args" => [script_path],
|
|
154
|
-
"env" => {
|
|
155
|
-
"RAILS_API_URL" => rails_api_url,
|
|
156
|
-
"RAILS_API_KEY" => rails_api_key
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
# Add/Update tinker server config
|
|
161
|
-
existing_config["mcpServers"]["tinker-#{agent_type}"] = tinker_server_config
|
|
162
|
-
|
|
163
|
-
File.write(".mcp.json", JSON.pretty_generate(existing_config))
|
|
164
|
-
puts "📝 Updated .mcp.json with tinker-#{agent_type} server (using tinker-mcp)"
|
|
165
|
-
else
|
|
166
|
-
# Only write if we don't have existing config
|
|
167
|
-
if existing_config["mcpServers"].empty?
|
|
168
|
-
File.write(".mcp.json", JSON.generate({ "mcpServers" => {} }))
|
|
169
|
-
puts "ℹ️ No MCP API credentials - MCP tools disabled"
|
|
170
|
-
else
|
|
171
|
-
puts "ℹ️ No MCP API credentials - keeping existing config"
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def setup_claude_config!
|
|
177
|
-
home_claude_json = File.expand_path("~/.claude.json")
|
|
178
|
-
|
|
179
|
-
# First, try to copy from mounted config (from host)
|
|
180
|
-
mounted_paths = ["/tmp/cfg/claude.json"]
|
|
181
|
-
mounted_paths.each do |path|
|
|
182
|
-
if File.exist?(path) && !File.exist?(home_claude_json)
|
|
183
|
-
FileUtils.cp(path, home_claude_json)
|
|
184
|
-
puts "📋 Copied claude.json from #{path}"
|
|
185
|
-
break
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
if File.exist?(home_claude_json)
|
|
190
|
-
puts "🔧 Configuring claude.json..."
|
|
191
|
-
begin
|
|
192
|
-
claude_config = JSON.parse(File.read(home_claude_json))
|
|
193
|
-
|
|
194
|
-
# Add bypass permission at top level
|
|
195
|
-
claude_config["bypassPermissionsModeAccepted"] = true
|
|
196
|
-
|
|
197
|
-
File.write(home_claude_json, JSON.pretty_generate(claude_config))
|
|
198
|
-
puts "✅ claude.json configured with bypass permissions"
|
|
199
|
-
rescue JSON::ParserError
|
|
200
|
-
puts "⚠️ claude.json is invalid, skipping configuration"
|
|
201
|
-
end
|
|
202
|
-
else
|
|
203
|
-
puts "⚠️ claude.json not found at #{home_claude_json}"
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def setup_claude_settings!
|
|
208
|
-
settings_dir = File.expand_path("~/.claude")
|
|
209
|
-
FileUtils.mkdir_p(settings_dir)
|
|
210
|
-
home_settings_json = File.join(settings_dir, "settings.json")
|
|
211
|
-
|
|
212
|
-
puts "🔧 Configuring ~/.claude/settings.json..."
|
|
213
|
-
|
|
214
|
-
settings_config = {}
|
|
215
|
-
|
|
216
|
-
# First, try to load user's existing settings from mounted config
|
|
217
|
-
# We merge both files: settings.json AND claude.json (for theme/appearance settings)
|
|
218
|
-
mounted_settings_paths = [
|
|
219
|
-
"/tmp/cfg/settings.json",
|
|
220
|
-
"/tmp/cfg/claude.json"
|
|
221
|
-
]
|
|
222
|
-
|
|
223
|
-
mounted_settings = {}
|
|
224
|
-
mounted_settings_paths.each do |path|
|
|
225
|
-
if File.exist?(path)
|
|
226
|
-
begin
|
|
227
|
-
file_settings = JSON.parse(File.read(path))
|
|
228
|
-
# Merge settings - later files override earlier ones for conflicting keys
|
|
229
|
-
mounted_settings = mounted_settings.merge(file_settings)
|
|
230
|
-
puts " (loaded from #{path})"
|
|
231
|
-
rescue JSON::ParserError => e
|
|
232
|
-
puts " ⚠️ Failed to parse #{path}: #{e.message}"
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
if mounted_settings.any?
|
|
238
|
-
settings_config = mounted_settings
|
|
239
|
-
elsif File.exist?(home_settings_json)
|
|
240
|
-
begin
|
|
241
|
-
settings_config = JSON.parse(File.read(home_settings_json))
|
|
242
|
-
rescue JSON::ParserError
|
|
243
|
-
puts "⚠️ ~/.claude/settings.json is invalid, starting fresh"
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
# Configure hooks
|
|
248
|
-
settings_config["hooks"] ||= {}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
# Configure SessionStart hook for skill knowledge injection
|
|
252
|
-
settings_config["hooks"]["SessionStart"] ||= []
|
|
253
|
-
# Remove old hook if present
|
|
254
|
-
settings_config["hooks"]["SessionStart"].reject! do |h|
|
|
255
|
-
h["hooks"]&.first&.dig("command")&.include?("inject-skill-knowledge.rb")
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
# Install hook script to ~/.claude/hooks
|
|
259
|
-
hooks_dir = File.join(settings_dir, "hooks")
|
|
260
|
-
FileUtils.mkdir_p(hooks_dir)
|
|
261
|
-
|
|
262
|
-
session_script_name = "inject-skill-knowledge.rb"
|
|
263
|
-
dest_path = File.join(hooks_dir, session_script_name)
|
|
264
|
-
|
|
265
|
-
# Try local copy first (dev mode), otherwise download
|
|
266
|
-
local_hook_paths = [
|
|
267
|
-
File.join(Dir.pwd, "tinker-public", "hooks", session_script_name),
|
|
268
|
-
File.join(Dir.pwd, "hooks", session_script_name)
|
|
269
|
-
]
|
|
270
|
-
|
|
271
|
-
local_hook_content = nil
|
|
272
|
-
local_hook_paths.each do |path|
|
|
273
|
-
if File.exist?(path)
|
|
274
|
-
local_hook_content = File.read(path)
|
|
275
|
-
puts " (using local hook: #{path})"
|
|
276
|
-
break
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
if local_hook_content
|
|
281
|
-
File.write(dest_path, local_hook_content)
|
|
282
|
-
File.chmod(0755, dest_path)
|
|
283
|
-
puts "✅ Installed SessionStart hook (from local) to #{dest_path}"
|
|
284
|
-
else
|
|
285
|
-
puts "📥 Downloading SessionStart hook..."
|
|
286
|
-
hook_url = "#{TINKER_RAW_URL}/hooks/#{session_script_name}"
|
|
287
|
-
begin
|
|
288
|
-
hook_content = URI.open(hook_url).read
|
|
289
|
-
File.write(dest_path, hook_content)
|
|
290
|
-
File.chmod(0755, dest_path)
|
|
291
|
-
puts "✅ Installed SessionStart hook (from remote) to #{dest_path}"
|
|
292
|
-
rescue OpenURI::HTTPError => e
|
|
293
|
-
puts "⚠️ Failed to download hook: #{e.message}"
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
session_hook_to_add = {
|
|
298
|
-
"hooks" => [{ "type" => "command", "command" => "ruby \"#{dest_path}\"" }]
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
settings_config["hooks"]["SessionStart"] << session_hook_to_add
|
|
302
|
-
|
|
303
|
-
# Install sessionstart-input.sh for repository context injection
|
|
304
|
-
# This provides repository information to Claude at session start
|
|
305
|
-
input_script_name = "sessionstart-input.sh"
|
|
306
|
-
input_dest_path = File.join(hooks_dir, input_script_name)
|
|
307
|
-
|
|
308
|
-
local_input_paths = [
|
|
309
|
-
File.join(Dir.pwd, "tinker-public", "hooks", input_script_name),
|
|
310
|
-
File.join(Dir.pwd, "hooks", input_script_name)
|
|
311
|
-
]
|
|
312
|
-
|
|
313
|
-
local_input_content = nil
|
|
314
|
-
local_input_paths.each do |path|
|
|
315
|
-
if File.exist?(path)
|
|
316
|
-
local_input_content = File.read(path)
|
|
317
|
-
puts " (using local input hook: #{path})"
|
|
318
|
-
break
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
if local_input_content
|
|
323
|
-
File.write(input_dest_path, local_input_content)
|
|
324
|
-
File.chmod(0755, input_dest_path)
|
|
325
|
-
puts "✅ Installed SessionStart input hook (from local) to #{input_dest_path}"
|
|
326
|
-
else
|
|
327
|
-
puts "📥 Downloading SessionStart input hook..."
|
|
328
|
-
input_hook_url = "#{TINKER_RAW_URL}/hooks/#{input_script_name}"
|
|
329
|
-
begin
|
|
330
|
-
input_hook_content = URI.open(input_hook_url).read
|
|
331
|
-
File.write(input_dest_path, input_hook_content)
|
|
332
|
-
File.chmod(0755, input_dest_path)
|
|
333
|
-
puts "✅ Installed SessionStart input hook (from remote) to #{input_dest_path}"
|
|
334
|
-
rescue OpenURI::HTTPError => e
|
|
335
|
-
puts "⚠️ Failed to download input hook: #{e.message}"
|
|
336
|
-
end
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
File.write(home_settings_json, JSON.pretty_generate(settings_config))
|
|
340
|
-
puts "✅ ~/.claude/settings.json configured with skill hooks"
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def setup_system_prompt!
|
|
344
|
-
if File.exist?("/etc/tinker/system-prompt.txt")
|
|
345
|
-
puts "✅ System prompt available at /etc/tinker/system-prompt.txt"
|
|
346
|
-
else
|
|
347
|
-
puts "❌ /etc/tinker/system-prompt.txt not found!"
|
|
348
|
-
exit 1
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
# Add repository context to CLAUDE.md for easy reference
|
|
352
|
-
claude_md = File.expand_path("~/CLAUDE.md")
|
|
353
|
-
if ENV["REPOSITORIES_CONTEXT"]
|
|
354
|
-
repo_info = ENV["REPOSITORIES_CONTEXT"].split(',').map do |repo|
|
|
355
|
-
name, path = repo.split(':')
|
|
356
|
-
" - **#{name}**: #{path}"
|
|
357
|
-
end.join("\n")
|
|
358
|
-
|
|
359
|
-
repo_context_md = <<~MARKDOWN
|
|
360
|
-
## Repository Structure
|
|
361
|
-
|
|
362
|
-
This project has #{ENV["REPOSITORIES_CONTEXT"].split(',').count} repositories:
|
|
363
|
-
|
|
364
|
-
#{repo_info}
|
|
365
|
-
|
|
366
|
-
### Repository Switching
|
|
367
|
-
|
|
368
|
-
Use `switch-to-repo.sh <name>` to change working directories:
|
|
369
|
-
|
|
370
|
-
```bash
|
|
371
|
-
switch-to-repo.sh tinker
|
|
372
|
-
switch-to-repo.sh tinker-public
|
|
373
|
-
```
|
|
374
|
-
MARKDOWN
|
|
375
|
-
|
|
376
|
-
# Append to existing CLAUDE.md or create new one
|
|
377
|
-
existing_content = File.exist?(claude_md) ? File.read(claude_md) : ""
|
|
378
|
-
File.write(claude_md, existing_content + "\n" + repo_context_md)
|
|
379
|
-
puts "📝 Repository context added to ~/CLAUDE.md"
|
|
380
|
-
end
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
def setup_skills!
|
|
385
|
-
skills = ENV["SKILLS"].to_s.split(",")
|
|
386
|
-
return if skills.empty?
|
|
387
|
-
|
|
388
|
-
skills_dir = File.expand_path("~/.claude/skills")
|
|
389
|
-
puts "🧠 Installing #{skills.size} skills to #{skills_dir}..."
|
|
390
|
-
FileUtils.mkdir_p(skills_dir)
|
|
391
|
-
|
|
392
|
-
skills.each do |skill|
|
|
393
|
-
puts " - #{skill}"
|
|
394
|
-
|
|
395
|
-
# Try local copy first (dev mode, or if copied into image)
|
|
396
|
-
# Check both PWD/tinker-public/skills or PWD/skills (depending on how image was built)
|
|
397
|
-
local_paths = [
|
|
398
|
-
File.join(Dir.pwd, "tinker-public", "skills", skill, "SKILL.md")
|
|
399
|
-
]
|
|
400
|
-
|
|
401
|
-
skill_content = nil
|
|
402
|
-
|
|
403
|
-
local_paths.each do |path|
|
|
404
|
-
if File.exist?(path)
|
|
405
|
-
skill_content = File.read(path)
|
|
406
|
-
puts " (from local: #{path})"
|
|
407
|
-
break
|
|
408
|
-
end
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
url = "#{TINKER_RAW_URL}/skills/#{skill}/SKILL.md"
|
|
412
|
-
|
|
413
|
-
begin
|
|
414
|
-
unless skill_content
|
|
415
|
-
skill_content = URI.open(url).read
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
# Write to .claude/skills/[skill]/SKILL.md with proper structure
|
|
419
|
-
skill_dir = File.join(skills_dir, skill)
|
|
420
|
-
FileUtils.mkdir_p(skill_dir)
|
|
421
|
-
File.write(File.join(skill_dir, "SKILL.md"), skill_content)
|
|
422
|
-
rescue OpenURI::HTTPError, Errno::ENOENT => e
|
|
423
|
-
puts "⚠️ Failed to download skill #{skill}: #{e.message}"
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
puts "✅ Skills installed"
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
def setup_github_auth!
|
|
431
|
-
app_id = ENV["GITHUB_APP_ID"] || ENV["GITHUB_APP_CLIENT_ID"]
|
|
432
|
-
|
|
433
|
-
# Check for private key in typical locations
|
|
434
|
-
# 1. In the home directory (copied by entrypoint)
|
|
435
|
-
# 2. In /tmp (mounted by docker)
|
|
436
|
-
private_key_path = [
|
|
437
|
-
File.expand_path("~/.github-app-privkey.pem"),
|
|
438
|
-
"/tmp/github-app-privkey.pem"
|
|
439
|
-
].find { |path| File.exist?(path) }
|
|
440
|
-
|
|
441
|
-
if app_id && ENV["GITHUB_APP_INSTALLATION_ID"] && private_key_path
|
|
442
|
-
puts "🔐 Configuring GitHub App authentication..."
|
|
443
|
-
|
|
444
|
-
# Create helper script
|
|
445
|
-
helper_content = <<~RUBY
|
|
446
|
-
#!/usr/bin/env ruby
|
|
447
|
-
require 'openssl'
|
|
448
|
-
require 'json'
|
|
449
|
-
require 'net/http'
|
|
450
|
-
require 'uri'
|
|
451
|
-
require 'base64'
|
|
452
|
-
require 'time'
|
|
453
|
-
|
|
454
|
-
PRIVATE_KEY_PATH = "#{private_key_path}"
|
|
455
|
-
|
|
456
|
-
def generate_jwt(app_id)
|
|
457
|
-
private_key = OpenSSL::PKey::RSA.new(File.read(PRIVATE_KEY_PATH))
|
|
458
|
-
payload = {
|
|
459
|
-
iat: Time.now.to_i - 60,
|
|
460
|
-
exp: Time.now.to_i + 480, # 8 minutes to handle clock skew
|
|
461
|
-
iss: app_id
|
|
462
|
-
}
|
|
463
|
-
header = { alg: 'RS256', typ: 'JWT' }
|
|
464
|
-
segments = [
|
|
465
|
-
Base64.urlsafe_encode64(header.to_json, padding: false),
|
|
466
|
-
Base64.urlsafe_encode64(payload.to_json, padding: false)
|
|
467
|
-
]
|
|
468
|
-
signing_input = segments.join('.')
|
|
469
|
-
signature = private_key.sign(OpenSSL::Digest::SHA256.new, signing_input)
|
|
470
|
-
segments << Base64.urlsafe_encode64(signature, padding: false)
|
|
471
|
-
segments.join('.')
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
def get_installation_token(jwt, installation_id)
|
|
475
|
-
uri = URI("https://api.github.com/app/installations/\#{installation_id}/access_tokens")
|
|
476
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
477
|
-
http.use_ssl = true
|
|
478
|
-
request = Net::HTTP::Post.new(uri)
|
|
479
|
-
request['Authorization'] = "Bearer \#{jwt}"
|
|
480
|
-
request['Accept'] = 'application/vnd.github+json'
|
|
481
|
-
|
|
482
|
-
response = http.request(request)
|
|
483
|
-
data = JSON.parse(response.body)
|
|
484
|
-
|
|
485
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
486
|
-
STDERR.puts "❌ Error getting installation token: \#{response.code} \#{response.message}"
|
|
487
|
-
STDERR.puts " Response: \#{response.body}"
|
|
488
|
-
exit 1
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
{ token: data['token'], expires_at: Time.parse(data['expires_at']) }
|
|
492
|
-
end
|
|
493
|
-
|
|
494
|
-
def find_or_create_cached_token(app_id, installation_id)
|
|
495
|
-
cache_file = '/tmp/github-app-token-cache'
|
|
496
|
-
cached_token = read_cached_token(cache_file)
|
|
497
|
-
return cached_token if cached_token
|
|
498
|
-
|
|
499
|
-
jwt = generate_jwt(app_id)
|
|
500
|
-
token_data = get_installation_token(jwt, installation_id)
|
|
501
|
-
File.write(cache_file, token_data.to_json)
|
|
502
|
-
token_data[:token]
|
|
503
|
-
end
|
|
504
|
-
|
|
505
|
-
def read_cached_token(cache_file)
|
|
506
|
-
return nil unless File.exist?(cache_file)
|
|
507
|
-
cache = JSON.parse(File.read(cache_file))
|
|
508
|
-
return if cache['token'].nil? || cache['expires_at'].nil?
|
|
509
|
-
return cache['token'] if Time.parse(cache['expires_at']) > Time.now + 300
|
|
510
|
-
nil
|
|
511
|
-
rescue
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
app_id = ENV['GITHUB_APP_CLIENT_ID'] || ENV['GITHUB_APP_ID']
|
|
515
|
-
installation_id = ENV['GITHUB_APP_INSTALLATION_ID']
|
|
516
|
-
|
|
517
|
-
puts find_or_create_cached_token(app_id, installation_id)
|
|
518
|
-
RUBY
|
|
519
|
-
|
|
520
|
-
# Install helper via sudo to /usr/local/bin
|
|
521
|
-
helper_path = "/usr/local/bin/git-auth-helper"
|
|
522
|
-
File.write("/tmp/git-auth-helper", helper_content)
|
|
523
|
-
system("sudo mv /tmp/git-auth-helper #{helper_path}")
|
|
524
|
-
system("sudo chmod +x #{helper_path}")
|
|
525
|
-
|
|
526
|
-
# Configure git
|
|
527
|
-
system("git config --global credential.helper '!f() { test \"$1\" = get && echo \"protocol=https\" && echo \"host=github.com\" && echo \"username=x-access-token\" && echo \"password=$(#{helper_path})\"; }; f'")
|
|
528
|
-
|
|
529
|
-
# Configure gh CLI wrapper for auto-refresh and permission controls
|
|
530
|
-
if install_gh_wrapper!(real_gh_path: "/usr/bin/gh", with_token_refresh: true, token_helper_path: helper_path)
|
|
531
|
-
puts "✅ GitHub App authentication configured (with auto-refresh + permission controls)"
|
|
532
|
-
end
|
|
533
|
-
|
|
534
|
-
elsif ENV["GH_TOKEN"] && !ENV["GH_TOKEN"].empty?
|
|
535
|
-
system("echo '#{ENV['GH_TOKEN']}' | gh auth login --with-token 2>/dev/null")
|
|
536
|
-
|
|
537
|
-
# Install wrapper for permission controls even with GH_TOKEN
|
|
538
|
-
if install_gh_wrapper!(real_gh_path: "/usr/bin/gh")
|
|
539
|
-
puts "🔐 GitHub authentication configured (with permission controls)"
|
|
540
|
-
else
|
|
541
|
-
puts "🔐 GitHub authentication configured"
|
|
542
|
-
end
|
|
543
|
-
else
|
|
544
|
-
puts "⚠️ No GH_TOKEN or GitHub App config - GitHub operations may fail"
|
|
545
|
-
end
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
def install_gh_wrapper!(real_gh_path:, with_token_refresh: false, token_helper_path: nil)
|
|
549
|
-
unless File.exist?(real_gh_path)
|
|
550
|
-
puts "⚠️ Could not find 'gh' at #{real_gh_path}, skipping wrapper"
|
|
551
|
-
return false
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
# Move real gh to gh.real if not already done
|
|
555
|
-
unless File.exist?("#{real_gh_path}.real")
|
|
556
|
-
system("sudo mv #{real_gh_path} #{real_gh_path}.real")
|
|
557
|
-
end
|
|
558
|
-
|
|
559
|
-
wrapper_path = "/usr/local/bin/gh"
|
|
560
|
-
|
|
561
|
-
# Build token export line if auto-refresh is enabled
|
|
562
|
-
token_export = with_token_refresh ? "export GH_TOKEN=$(#{token_helper_path})\n\n" : ""
|
|
563
|
-
|
|
564
|
-
wrapper_content = <<~BASH
|
|
565
|
-
#!/bin/bash
|
|
566
|
-
# GitHub CLI wrapper with permission controls for agents
|
|
567
|
-
#{token_export}# Check if caller is an agent (read AGENT_TYPE at runtime)
|
|
568
|
-
if [ -n "$AGENT_TYPE" ] && [[ "$AGENT_TYPE" =~ ^(worker|planner|reviewer|researcher)$ ]]; then
|
|
569
|
-
# Block comment-related commands for agents
|
|
570
|
-
if [[ "$1" == "comment" ]] || ([[ "$1" == "pr" ]] && [[ "$2" == "comment" ]]); then
|
|
571
|
-
echo "❌ You cannot use 'gh $*' commands as an agent." >&2
|
|
572
|
-
echo "" >&2
|
|
573
|
-
echo "Tinker has its own comment system via the add_comment MCP tool." >&2
|
|
574
|
-
echo "Please use the add_comment tool instead of gh commands." >&2
|
|
575
|
-
echo "" >&2
|
|
576
|
-
echo "Example:" >&2
|
|
577
|
-
echo " add_comment(ticket_id: 123, content: \\"Your comment here\\", comment_type: \\"note\\")" >&2
|
|
578
|
-
exit 1
|
|
579
|
-
fi
|
|
580
|
-
fi
|
|
581
|
-
|
|
582
|
-
# Execute real gh binary with all arguments
|
|
583
|
-
exec #{real_gh_path}.real "$@"
|
|
584
|
-
BASH
|
|
585
|
-
|
|
586
|
-
File.write("/tmp/gh-wrapper", wrapper_content)
|
|
587
|
-
system("sudo mv /tmp/gh-wrapper #{wrapper_path}")
|
|
588
|
-
system("sudo chmod +x #{wrapper_path}")
|
|
589
|
-
true
|
|
590
|
-
end
|
|
591
|
-
|
|
592
|
-
def clone_repositories!
|
|
593
|
-
return unless ENV["REPOSITORIES_CONFIG"]
|
|
594
|
-
|
|
595
|
-
begin
|
|
596
|
-
repos = JSON.parse(ENV["REPOSITORIES_CONFIG"])
|
|
597
|
-
return unless repos.is_a?(Array) && repos.any?
|
|
598
|
-
|
|
599
|
-
puts "📚 Cloning #{repos.count} repositories..."
|
|
600
|
-
|
|
601
|
-
repos.each do |repo|
|
|
602
|
-
repo_name = repo["name"]
|
|
603
|
-
github_url = repo["github_url"]
|
|
604
|
-
sandbox_path = repo["path_in_sandbox"] || "/workspace/#{repo_name}"
|
|
605
|
-
|
|
606
|
-
# Create parent directory if needed
|
|
607
|
-
parent_dir = File.dirname(sandbox_path)
|
|
608
|
-
FileUtils.mkdir_p(parent_dir) unless File.exist?(parent_dir)
|
|
609
|
-
|
|
610
|
-
# Clone repository if it doesn't exist
|
|
611
|
-
unless File.exist?(sandbox_path)
|
|
612
|
-
puts " 📥 Cloning #{repo_name} to #{sandbox_path}..."
|
|
613
|
-
system("git", "clone", "--depth", "1", github_url, sandbox_path)
|
|
614
|
-
else
|
|
615
|
-
puts " ✓ #{repo_name} already exists at #{sandbox_path}"
|
|
616
|
-
end
|
|
617
|
-
end
|
|
618
|
-
|
|
619
|
-
puts "✅ Repositories cloned"
|
|
620
|
-
|
|
621
|
-
# Write repository context to a file for hooks to read
|
|
622
|
-
# This is more reliable than environment variables in tmux sessions
|
|
623
|
-
if ENV["REPOSITORIES_CONTEXT"]
|
|
624
|
-
repo_context_file = File.expand_path("~/.claude/repos_context.txt")
|
|
625
|
-
File.write(repo_context_file, ENV["REPOSITORIES_CONTEXT"])
|
|
626
|
-
puts "📝 Repository context written to #{repo_context_file}"
|
|
627
|
-
end
|
|
628
|
-
|
|
629
|
-
# Export REPOSITORIES_CONTEXT to bash profile for shell sessions
|
|
630
|
-
if ENV["REPOSITORIES_CONTEXT"]
|
|
631
|
-
profile_file = File.expand_path("~/.bashrc")
|
|
632
|
-
existing_content = File.exist?(profile_file) ? File.read(profile_file) : ""
|
|
633
|
-
|
|
634
|
-
# Remove old export if present
|
|
635
|
-
existing_content.gsub!(/^export REPOSITORIES_CONTEXT=.*$/, "")
|
|
636
|
-
|
|
637
|
-
# Add new export at the end
|
|
638
|
-
File.write(profile_file, existing_content + "\nexport REPOSITORIES_CONTEXT=\"#{ENV['REPOSITORIES_CONTEXT']}\"\n")
|
|
639
|
-
puts "📝 REPOSITORIES_CONTEXT exported to ~/.bashrc"
|
|
640
|
-
end
|
|
641
|
-
rescue JSON::ParserError => e
|
|
642
|
-
puts "⚠️ Failed to parse REPOSITORIES_CONFIG: #{e.message}"
|
|
643
|
-
end
|
|
644
|
-
end
|
|
645
|
-
|
|
646
|
-
def setup_git_config!
|
|
647
|
-
# Configure identity if provided
|
|
648
|
-
if ENV["GIT_USER_NAME"] && !ENV["GIT_USER_NAME"].empty?
|
|
649
|
-
system("git config --global user.name \"#{ENV['GIT_USER_NAME']}\"")
|
|
650
|
-
puts "✅ Git user.name configured"
|
|
651
|
-
end
|
|
652
|
-
|
|
653
|
-
if ENV["GIT_USER_EMAIL"] && !ENV["GIT_USER_EMAIL"].empty?
|
|
654
|
-
system("git config --global user.email \"#{ENV['GIT_USER_EMAIL']}\"")
|
|
655
|
-
puts "✅ Git user.email configured"
|
|
656
|
-
end
|
|
657
|
-
|
|
658
|
-
# Force HTTPS instead of SSH to ensure our token auth works
|
|
659
|
-
# This fixes "Permission denied (publickey)" when the repo uses git@github.com remote
|
|
660
|
-
system("git config --global url.\"https://github.com/\".insteadOf \"git@github.com:\"")
|
|
661
|
-
puts "✅ Git configured to force HTTPS for GitHub"
|
|
662
|
-
|
|
663
|
-
# Avoid "dubious ownership" errors in containers
|
|
664
|
-
system("git config --global --add safe.directory \"#{Dir.pwd}\"")
|
|
665
|
-
puts "✅ Git configured to trust #{Dir.pwd}"
|
|
666
|
-
end
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
def setup_git_hooks!
|
|
670
|
-
# Install git hooks to encourage agents to use git-workflow skill
|
|
671
|
-
hooks_dest = File.join(Dir.pwd, ".git", "hooks")
|
|
672
|
-
return unless File.directory?(hooks_dest)
|
|
673
|
-
|
|
674
|
-
puts "🪝 Installing git hooks..."
|
|
675
|
-
|
|
676
|
-
# Try local copy first, then download from GitHub
|
|
677
|
-
local_paths = [
|
|
678
|
-
File.join(Dir.pwd, "tinker-public", "hooks", "pre-commit"),
|
|
679
|
-
File.join(Dir.pwd, "hooks", "pre-commit"),
|
|
680
|
-
"/tmp/hooks/pre-commit"
|
|
681
|
-
]
|
|
682
|
-
|
|
683
|
-
hook_content = nil
|
|
684
|
-
local_paths.each do |path|
|
|
685
|
-
if File.exist?(path)
|
|
686
|
-
hook_content = File.read(path)
|
|
687
|
-
puts " (from local: #{path})"
|
|
688
|
-
break
|
|
689
|
-
end
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
unless hook_content
|
|
693
|
-
url = "#{TINKER_RAW_URL}/hooks/pre-commit"
|
|
694
|
-
begin
|
|
695
|
-
hook_content = URI.open(url).read
|
|
696
|
-
rescue OpenURI::HTTPError => e
|
|
697
|
-
puts "⚠️ Failed to download pre-commit hook: #{e.message}"
|
|
698
|
-
return
|
|
699
|
-
end
|
|
700
|
-
end
|
|
701
|
-
|
|
702
|
-
hook_path = File.join(hooks_dest, "pre-commit")
|
|
703
|
-
File.write(hook_path, hook_content)
|
|
704
|
-
File.chmod(0755, hook_path)
|
|
705
|
-
puts "✅ Git hooks installed"
|
|
706
|
-
end
|
|
707
|
-
|
|
708
|
-
def setup_multi_repo_scripts!
|
|
709
|
-
# Install switch-to-repo.sh script and repo-specific setup scripts
|
|
710
|
-
# These are only needed when using multi-repo setup
|
|
711
|
-
repos_dir = File.expand_path("~/repos")
|
|
712
|
-
bin_dir = "/usr/local/bin"
|
|
713
|
-
|
|
714
|
-
puts "📚 Setting up multi-repo scripts..."
|
|
715
|
-
|
|
716
|
-
# Create repos directory for repo-specific setup scripts
|
|
717
|
-
FileUtils.mkdir_p(repos_dir)
|
|
718
|
-
|
|
719
|
-
# Install switch-to-repo.sh script
|
|
720
|
-
switch_script = File.join(bin_dir, "switch-to-repo.sh")
|
|
721
|
-
|
|
722
|
-
# Try local copy first, then download from GitHub
|
|
723
|
-
local_paths = [
|
|
724
|
-
File.join(Dir.pwd, "tinker-public", "hooks", "switch-to-repo.sh"),
|
|
725
|
-
File.join(Dir.pwd, "hooks", "switch-to-repo.sh"),
|
|
726
|
-
"/tmp/hooks/switch-to-repo.sh"
|
|
727
|
-
]
|
|
728
|
-
|
|
729
|
-
script_content = nil
|
|
730
|
-
local_paths.each do |path|
|
|
731
|
-
if File.exist?(path)
|
|
732
|
-
script_content = File.read(path)
|
|
733
|
-
puts " (from local: #{path})"
|
|
734
|
-
break
|
|
735
|
-
end
|
|
736
|
-
end
|
|
737
|
-
|
|
738
|
-
unless script_content
|
|
739
|
-
url = "#{TINKER_RAW_URL}/hooks/switch-to-repo.sh"
|
|
740
|
-
begin
|
|
741
|
-
script_content = URI.open(url).read
|
|
742
|
-
rescue OpenURI::HTTPError => e
|
|
743
|
-
puts "⚠️ Failed to download switch-to-repo.sh: #{e.message}"
|
|
744
|
-
return
|
|
745
|
-
end
|
|
746
|
-
end
|
|
747
|
-
|
|
748
|
-
# Write to temp file first, then sudo mv
|
|
749
|
-
temp_script = "/tmp/switch-to-repo.sh.temp"
|
|
750
|
-
File.write(temp_script, script_content)
|
|
751
|
-
system("sudo mv #{temp_script} #{switch_script}")
|
|
752
|
-
system("sudo chmod +x #{switch_script}")
|
|
753
|
-
puts "✅ Installed switch-to-repo.sh to #{bin_dir}"
|
|
754
|
-
|
|
755
|
-
# Install repo-specific setup scripts
|
|
756
|
-
# These are example scripts that can be customized per repository
|
|
757
|
-
local_repos_dir = File.join(Dir.pwd, "tinker-public", "repos")
|
|
758
|
-
|
|
759
|
-
if File.directory?(local_repos_dir)
|
|
760
|
-
Dir.glob(File.join(local_repos_dir, "*.sh")).each do |script_file|
|
|
761
|
-
script_name = File.basename(script_file)
|
|
762
|
-
dest_script = File.join(repos_dir, script_name)
|
|
763
|
-
FileUtils.cp(script_file, dest_script)
|
|
764
|
-
File.chmod(0755, dest_script)
|
|
765
|
-
puts " - Installed #{script_name}"
|
|
766
|
-
end
|
|
767
|
-
puts "✅ Installed repo-specific setup scripts"
|
|
768
|
-
else
|
|
769
|
-
puts "ℹ️ No repo-specific setup scripts found (skipping)"
|
|
770
|
-
end
|
|
771
|
-
end
|
|
772
|
-
|
|
773
|
-
def download_agent_bridge!
|
|
774
|
-
# Detect architecture
|
|
775
|
-
arch = `uname -m`.strip
|
|
776
|
-
if arch == "x86_64"
|
|
777
|
-
arch = "amd64"
|
|
778
|
-
elsif arch == "aarch64" || arch == "arm64"
|
|
779
|
-
arch = "arm64"
|
|
780
|
-
else
|
|
781
|
-
puts "❌ Unsupported architecture: #{arch}"
|
|
782
|
-
exit 1
|
|
783
|
-
end
|
|
784
|
-
|
|
785
|
-
target_dir = "/usr/local/bin"
|
|
786
|
-
|
|
787
|
-
# 1. Check for local override in /tmp/overrides (development mode)
|
|
788
|
-
override_dir = "/tmp/overrides"
|
|
789
|
-
if Dir.exist?(override_dir)
|
|
790
|
-
local_override = File.join(override_dir, "agent-bridge-linux-#{arch}")
|
|
791
|
-
if File.exist?(local_override)
|
|
792
|
-
puts "🔧 Using local override: #{local_override}"
|
|
793
|
-
system("sudo cp #{local_override} #{target_dir}/agent-bridge")
|
|
794
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
795
|
-
puts "✅ agent-bridge installed from local override"
|
|
796
|
-
return download_agent_bridge_tmux!(target_dir)
|
|
797
|
-
end
|
|
798
|
-
end
|
|
799
|
-
|
|
800
|
-
# 2. Check if binaries are mounted at /tmp (dev mode)
|
|
801
|
-
if File.exist?("/tmp/agent-bridge")
|
|
802
|
-
puts "🔧 Installing mounted agent-bridge..."
|
|
803
|
-
system("sudo cp /tmp/agent-bridge #{target_dir}/agent-bridge")
|
|
804
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
805
|
-
puts "✅ agent-bridge installed from mount"
|
|
806
|
-
return download_agent_bridge_tmux!(target_dir)
|
|
807
|
-
end
|
|
808
|
-
|
|
809
|
-
# 3. Try Civo bucket download (production mode)
|
|
810
|
-
if ENV["CIVO_ACCESS_KEY_ID"] && ENV["CIVO_SECRET_KEY"]
|
|
811
|
-
puts "📥 Downloading agent-bridge from Civo bucket..."
|
|
812
|
-
if download_from_civo!(arch, target_dir)
|
|
813
|
-
return download_agent_bridge_tmux!(target_dir)
|
|
814
|
-
else
|
|
815
|
-
puts "⚠️ Civo download failed, falling back to GitHub..."
|
|
816
|
-
end
|
|
817
|
-
end
|
|
818
|
-
|
|
819
|
-
# 4. Fallback to GitHub raw URLs (legacy)
|
|
820
|
-
bridge_url = "#{TINKER_RAW_URL}/bin/agent-bridge-linux-#{arch}"
|
|
821
|
-
puts "📥 Downloading agent-bridge for linux-#{arch} from GitHub..."
|
|
822
|
-
system("sudo curl -fsSL #{bridge_url} -o #{target_dir}/agent-bridge")
|
|
823
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
824
|
-
|
|
825
|
-
puts "✅ agent-bridge installed to #{target_dir}"
|
|
826
|
-
download_agent_bridge_tmux!(target_dir)
|
|
827
|
-
end
|
|
828
|
-
|
|
829
|
-
def download_from_civo!(arch, target_dir)
|
|
830
|
-
require "aws-sdk-s3"
|
|
831
|
-
|
|
832
|
-
region = ENV["CIVO_REGION"] || "LON1"
|
|
833
|
-
endpoint = ENV["CIVO_ENDPOINT"] || "https://objectstore.fra1.civo.com"
|
|
834
|
-
bucket = ENV["TINKER_BINARIES_BUCKET"] || "tinker"
|
|
835
|
-
version = ENV["AGENT_BRIDGE_VERSION"] || "latest"
|
|
836
|
-
|
|
837
|
-
binary_name = "agent-bridge-linux-#{arch}"
|
|
838
|
-
key = "agent-bridge/#{version}/#{binary_name}"
|
|
839
|
-
|
|
840
|
-
puts " Region: #{region}"
|
|
841
|
-
puts " Bucket: #{bucket}"
|
|
842
|
-
puts " Version: #{version}"
|
|
843
|
-
puts " Binary: #{binary_name}"
|
|
844
|
-
|
|
845
|
-
begin
|
|
846
|
-
# Configure Civo S3-compatible client
|
|
847
|
-
Aws.config.update({
|
|
848
|
-
region: region,
|
|
849
|
-
endpoint: endpoint,
|
|
850
|
-
access_key_id: ENV["CIVO_ACCESS_KEY_ID"],
|
|
851
|
-
secret_access_key: ENV["CIVO_SECRET_KEY"]
|
|
852
|
-
})
|
|
853
|
-
|
|
854
|
-
s3 = Aws::S3::Resource.new
|
|
855
|
-
obj = s3.bucket(bucket).object(key)
|
|
856
|
-
|
|
857
|
-
# Generate presigned URL (valid for 1 hour)
|
|
858
|
-
url = obj.presigned_url(:get, expires_in: 3600)
|
|
859
|
-
|
|
860
|
-
puts "✅ Generated presigned URL"
|
|
861
|
-
|
|
862
|
-
# Download via curl
|
|
863
|
-
system("sudo curl -fsSL #{url} -o #{target_dir}/agent-bridge")
|
|
864
|
-
|
|
865
|
-
unless $?.success?
|
|
866
|
-
puts "❌ Failed to download agent-bridge from Civo"
|
|
867
|
-
return false
|
|
868
|
-
end
|
|
869
|
-
|
|
870
|
-
system("sudo chmod +x #{target_dir}/agent-bridge")
|
|
871
|
-
puts "✅ Downloaded agent-bridge from Civo"
|
|
872
|
-
return true
|
|
873
|
-
|
|
874
|
-
rescue LoadError
|
|
875
|
-
puts "⚠️ aws-sdk-s3 gem not installed"
|
|
876
|
-
return false
|
|
877
|
-
rescue Aws::S3::Errors::NoSuchKey
|
|
878
|
-
puts "❌ Version #{version} not found in bucket"
|
|
879
|
-
return false
|
|
880
|
-
rescue => e
|
|
881
|
-
puts "❌ Civo download failed: #{e.message}"
|
|
882
|
-
return false
|
|
883
|
-
end
|
|
884
|
-
end
|
|
885
|
-
|
|
886
|
-
def download_agent_bridge_tmux!(target_dir)
|
|
887
|
-
bridge_tmux_url = "#{TINKER_RAW_URL}/bin/agent-bridge-tmux"
|
|
888
|
-
|
|
889
|
-
if File.exist?("/tmp/agent-bridge-tmux")
|
|
890
|
-
puts "🔧 Installing mounted agent-bridge-tmux..."
|
|
891
|
-
system("sudo cp /tmp/agent-bridge-tmux #{target_dir}/agent-bridge-tmux")
|
|
892
|
-
system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
|
|
893
|
-
else
|
|
894
|
-
system("sudo curl -fsSL #{bridge_tmux_url} -o #{target_dir}/agent-bridge-tmux")
|
|
895
|
-
system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
|
|
896
|
-
end
|
|
897
|
-
|
|
898
|
-
# Patch agent-bridge-tmux to force INSIDE_TMUX=1
|
|
899
|
-
# Note: This is now fixed in the repo, but we keep this for backward compatibility
|
|
900
|
-
# with older agent-bridge-tmux scripts if cached
|
|
901
|
-
puts "🔧 Patching agent-bridge-tmux to force INSIDE_TMUX=1..."
|
|
902
|
-
|
|
903
|
-
# Read the file (we can read /usr/local/bin files usually)
|
|
904
|
-
content = File.read("#{target_dir}/agent-bridge-tmux")
|
|
905
|
-
|
|
906
|
-
# Replace the command if not already present
|
|
907
|
-
unless content.include?("export INSIDE_TMUX=1")
|
|
908
|
-
new_content = content.gsub(
|
|
909
|
-
"&& agent-bridge\"",
|
|
910
|
-
"&& export INSIDE_TMUX=1 && agent-bridge\""
|
|
911
|
-
)
|
|
912
|
-
|
|
913
|
-
# Write to temp file
|
|
914
|
-
File.write("/tmp/agent-bridge-tmux-patched", new_content)
|
|
915
|
-
|
|
916
|
-
# Move to destination with sudo
|
|
917
|
-
system("sudo mv /tmp/agent-bridge-tmux-patched #{target_dir}/agent-bridge-tmux")
|
|
918
|
-
system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
|
|
919
|
-
end
|
|
920
|
-
|
|
921
|
-
return target_dir
|
|
922
|
-
end
|
|
923
|
-
|
|
924
|
-
def run_agent!(bin_dir)
|
|
925
|
-
agent_type = ENV["AGENT_TYPE"] || "agent"
|
|
926
|
-
puts ""
|
|
927
|
-
puts "🚀 Starting #{agent_type}..."
|
|
928
|
-
puts " Press Ctrl+B then D to detach from tmux"
|
|
929
|
-
puts ""
|
|
930
|
-
|
|
931
|
-
# Build environment variables to pass to agent-bridge-tmux
|
|
932
|
-
# We need to explicitly export AGENT_ID if it was fetched
|
|
933
|
-
env_vars = {}
|
|
934
|
-
env_vars["AGENT_ID"] = ENV["AGENT_ID"] if ENV["AGENT_ID"]
|
|
935
|
-
|
|
936
|
-
# Run agent-bridge-tmux which handles tmux session and status bar
|
|
937
|
-
exec(env_vars, "#{bin_dir}/agent-bridge-tmux")
|
|
938
|
-
end
|
|
939
|
-
|
|
940
|
-
# Main
|
|
941
|
-
puts "🤖 Tinker Agent Setup"
|
|
942
|
-
puts "====================="
|
|
943
|
-
puts ""
|
|
944
|
-
|
|
945
|
-
check_env!
|
|
946
|
-
fetch_agent_id!
|
|
947
|
-
setup_mcp_config!
|
|
948
|
-
setup_system_prompt!
|
|
949
|
-
# Clone repositories BEFORE setting up hooks that depend on them
|
|
950
|
-
setup_github_auth!
|
|
951
|
-
setup_git_config!
|
|
952
|
-
clone_repositories!
|
|
953
|
-
setup_claude_config!
|
|
954
|
-
setup_claude_settings!
|
|
955
|
-
# setup_skill_hooks! # Deprecated in favor of global hooks
|
|
956
|
-
setup_skills!
|
|
957
|
-
setup_git_hooks!
|
|
958
|
-
setup_multi_repo_scripts!
|
|
959
|
-
# prepare_git_state! is now in entrypoint.sh
|
|
960
|
-
bin_dir = download_agent_bridge!
|
|
961
|
-
run_agent!(bin_dir)
|