tinker-agent 1.0.0

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/setup-agent.rb ADDED
@@ -0,0 +1,399 @@
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. Creates CLAUDE.md with role-specific instructions
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|orchestrator|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
+ # Agent banners - role-specific instructions for Claude
42
+ AGENT_BANNERS = {
43
+ "planner" => <<~BANNER,
44
+ ╔════════════════════════════════════════════════════════════════════════════╗
45
+ ║ TINKER PLANNER - ROLE ENFORCEMENT ║
46
+ ╠════════════════════════════════════════════════════════════════════════════╣
47
+ ║ YOUR ROLE: INTERACTIVE PLANNING AND TICKET CREATION ║
48
+ ║ YOUR MODE: CHAT WITH HUMAN - DISCUSS, PLAN, CREATE TICKETS ║
49
+ ╚════════════════════════════════════════════════════════════════════════════╝
50
+
51
+ This session is running as the TINKER PLANNER agent in INTERACTIVE CHAT MODE.
52
+
53
+ CORE RESPONSIBILITIES:
54
+ ✓ Discuss feature ideas and requirements with the human
55
+ ✓ Break down large features into implementable tickets
56
+ ✓ Write clear ticket descriptions with acceptance criteria
57
+ ✓ Create tickets using create_ticket MCP tool when plans are confirmed
58
+ BANNER
59
+
60
+ "worker" => <<~BANNER,
61
+ ╔════════════════════════════════════════════════════════════════════════════╗
62
+ ║ TINKER WORKER - ROLE ENFORCEMENT ║
63
+ ╠════════════════════════════════════════════════════════════════════════════╣
64
+ ║ YOUR ROLE: AUTONOMOUS CODE IMPLEMENTATION ║
65
+ ║ YOUR MODE: WORK AUTONOMOUSLY ON ASSIGNED TICKETS ║
66
+ ╚════════════════════════════════════════════════════════════════════════════╝
67
+
68
+ This session is running as the TINKER WORKER agent in AUTONOMOUS MODE.
69
+
70
+ CORE RESPONSIBILITIES:
71
+ ✓ Check for assigned tickets using get_my_tickets MCP tool
72
+ ✓ Implement code changes according to ticket specifications
73
+ ✓ Create branches, commits, and pull requests
74
+ ✓ Update ticket status as you progress
75
+ BANNER
76
+
77
+ "reviewer" => <<~BANNER,
78
+ ╔════════════════════════════════════════════════════════════════════════════╗
79
+ ║ TINKER REVIEWER - ROLE ENFORCEMENT ║
80
+ ╠════════════════════════════════════════════════════════════════════════════╣
81
+ ║ YOUR ROLE: AUTONOMOUS CODE REVIEW ║
82
+ ║ YOUR MODE: REVIEW PULL REQUESTS AND PROVIDE FEEDBACK ║
83
+ ╚════════════════════════════════════════════════════════════════════════════╝
84
+
85
+ This session is running as the TINKER REVIEWER agent in AUTONOMOUS MODE.
86
+
87
+ CORE RESPONSIBILITIES:
88
+ ✓ Check for PRs awaiting review
89
+ ✓ Review code quality, tests, and documentation
90
+ ✓ Approve or request changes with clear feedback
91
+ BANNER
92
+
93
+ "orchestrator" => <<~BANNER,
94
+ ╔════════════════════════════════════════════════════════════════════════════╗
95
+ ║ TINKER ORCHESTRATOR - ROLE ENFORCEMENT ║
96
+ ╠════════════════════════════════════════════════════════════════════════════╣
97
+ ║ YOUR ROLE: AUTONOMOUS WORK COORDINATION ║
98
+ ║ YOUR MODE: ASSIGN TICKETS AND MANAGE WORKFLOW ║
99
+ ╚════════════════════════════════════════════════════════════════════════════╝
100
+
101
+ This session is running as the TINKER ORCHESTRATOR agent in AUTONOMOUS MODE.
102
+
103
+ CORE RESPONSIBILITIES:
104
+ ✓ Monitor ticket queue and agent availability
105
+ ✓ Assign tickets to workers based on capacity
106
+ ✓ Track progress and handle blockers
107
+ BANNER
108
+
109
+ "researcher" => <<~BANNER
110
+ ╔════════════════════════════════════════════════════════════════════════════╗
111
+ ║ TINKER RESEARCHER - ROLE ENFORCEMENT ║
112
+ ╠════════════════════════════════════════════════════════════════════════════╣
113
+ ║ YOUR ROLE: AUTONOMOUS RESEARCH AND ANALYSIS ║
114
+ ║ YOUR MODE: INVESTIGATE CODEBASE AND DOCUMENT FINDINGS ║
115
+ ╚════════════════════════════════════════════════════════════════════════════╝
116
+
117
+ This session is running as the TINKER RESEARCHER agent in AUTONOMOUS MODE.
118
+
119
+ CORE RESPONSIBILITIES:
120
+ ✓ Analyze codebase architecture and patterns
121
+ ✓ Research best practices and solutions
122
+ ✓ Document findings in memory for other agents
123
+ BANNER
124
+ }
125
+
126
+ def check_requirements!
127
+ missing = []
128
+ missing << "ruby" unless system("which ruby > /dev/null 2>&1")
129
+ missing << "node" unless system("which node > /dev/null 2>&1")
130
+ missing << "tmux" unless system("which tmux > /dev/null 2>&1")
131
+ missing << "git" unless system("which git > /dev/null 2>&1")
132
+ missing << "claude" unless system("which claude > /dev/null 2>&1")
133
+
134
+ unless missing.empty?
135
+ puts "❌ Missing requirements: #{missing.join(', ')}"
136
+ puts ""
137
+ puts "Install with:"
138
+ puts " apt-get install -y tmux git curl"
139
+ puts " npm install -g @anthropic-ai/claude-code"
140
+ exit 1
141
+ end
142
+ end
143
+
144
+ def check_env!
145
+ required = %w[AGENT_TYPE PROJECT_ID RAILS_WS_URL]
146
+ missing = required.select { |var| ENV[var].to_s.empty? }
147
+
148
+ unless missing.empty?
149
+ puts "❌ Missing environment variables: #{missing.join(', ')}"
150
+ puts ""
151
+ puts "Required:"
152
+ puts " AGENT_TYPE - worker|planner|reviewer|orchestrator|researcher"
153
+ puts " PROJECT_ID - Your Tinker project ID"
154
+ puts " RAILS_WS_URL - WebSocket URL (wss://tinker.example.com/cable)"
155
+ puts ""
156
+ puts "Optional:"
157
+ puts " RAILS_API_URL - API URL for MCP tools"
158
+ puts " RAILS_API_KEY - MCP API key"
159
+ puts " GH_TOKEN - GitHub token"
160
+ exit 1
161
+ end
162
+
163
+ agent_type = ENV["AGENT_TYPE"]
164
+ unless AGENT_BANNERS.key?(agent_type)
165
+ puts "❌ Invalid AGENT_TYPE: #{agent_type}"
166
+ puts " Valid types: #{AGENT_BANNERS.keys.join(', ')}"
167
+ exit 1
168
+ end
169
+ end
170
+
171
+ def setup_mcp_config!
172
+ # MCP config is project-specific and should be provided by the Dockerfile
173
+ # or mounted at runtime. This script only checks if it exists.
174
+
175
+ agent_type = ENV["AGENT_TYPE"]
176
+ rails_api_url = ENV["RAILS_API_URL"]
177
+ rails_api_key = ENV["RAILS_API_KEY"]
178
+
179
+ # Load existing config if present
180
+ existing_config = {}
181
+ if File.exist?(".mcp.json")
182
+ begin
183
+ existing_config = JSON.parse(File.read(".mcp.json"))
184
+ puts "✅ Found existing .mcp.json, merging configuration..."
185
+ rescue JSON::ParserError
186
+ puts "⚠️ Existing .mcp.json is invalid, starting fresh"
187
+ end
188
+ end
189
+
190
+ # Ensure mcpServers key exists
191
+ existing_config["mcpServers"] ||= {}
192
+
193
+ if rails_api_url && !rails_api_url.empty? && rails_api_key && !rails_api_key.empty?
194
+ # Use published tinker-mcp package
195
+ tinker_server_config = {
196
+ "command" => "npx",
197
+ "args" => ["-y", "tinker-mcp"],
198
+ "env" => {
199
+ "RAILS_API_URL" => rails_api_url,
200
+ "RAILS_API_KEY" => rails_api_key
201
+ }
202
+ }
203
+
204
+ # Add/Update tinker server config
205
+ existing_config["mcpServers"]["tinker-#{agent_type}"] = tinker_server_config
206
+
207
+ File.write(".mcp.json", JSON.pretty_generate(existing_config))
208
+ puts "📝 Updated .mcp.json with tinker-#{agent_type} server (using tinker-mcp)"
209
+ else
210
+ # Only write if we don't have existing config
211
+ if existing_config["mcpServers"].empty?
212
+ File.write(".mcp.json", JSON.generate({ "mcpServers" => {} }))
213
+ puts "ℹ️ No MCP API credentials - MCP tools disabled"
214
+ else
215
+ puts "ℹ️ No MCP API credentials - keeping existing config"
216
+ end
217
+ end
218
+ end
219
+
220
+ def setup_claude_md!
221
+ agent_type = ENV["AGENT_TYPE"]
222
+ banner = AGENT_BANNERS[agent_type]
223
+
224
+ File.write("CLAUDE.md", banner)
225
+ puts "📝 Created CLAUDE.md with #{agent_type} instructions"
226
+ end
227
+
228
+ def setup_github_auth!
229
+ app_id = ENV["GITHUB_APP_ID"] || ENV["GITHUB_APP_CLIENT_ID"]
230
+
231
+ if app_id && ENV["GITHUB_APP_INSTALLATION_ID"] && ENV["GITHUB_APP_PRIVATE_KEY_PATH"]
232
+ puts "🔐 Configuring GitHub App authentication..."
233
+
234
+ # Create helper script
235
+ helper_path = "/usr/local/bin/git-auth-helper"
236
+
237
+ # We embed the helper script content here
238
+ helper_content = <<~RUBY
239
+ #!/usr/bin/env ruby
240
+ require 'openssl'
241
+ require 'json'
242
+ require 'net/http'
243
+ require 'uri'
244
+ require 'base64'
245
+ require 'time'
246
+
247
+ def generate_jwt(app_id, private_key_path)
248
+ private_key = OpenSSL::PKey::RSA.new(File.read(private_key_path))
249
+ payload = {
250
+ iat: Time.now.to_i - 60,
251
+ exp: Time.now.to_i + 600,
252
+ iss: app_id
253
+ }
254
+ header = { alg: 'RS256', typ: 'JWT' }
255
+ segments = [
256
+ Base64.urlsafe_encode64(header.to_json, padding: false),
257
+ Base64.urlsafe_encode64(payload.to_json, padding: false)
258
+ ]
259
+ signing_input = segments.join('.')
260
+ signature = private_key.sign(OpenSSL::Digest::SHA256.new, signing_input)
261
+ segments << Base64.urlsafe_encode64(signature, padding: false)
262
+ segments.join('.')
263
+ end
264
+
265
+ def get_installation_token(jwt, installation_id)
266
+ uri = URI("https://api.github.com/app/installations/\#{installation_id}/access_tokens")
267
+ http = Net::HTTP.new(uri.host, uri.port)
268
+ http.use_ssl = true
269
+ request = Net::HTTP::Post.new(uri)
270
+ request['Authorization'] = "Bearer \#{jwt}"
271
+ request['Accept'] = 'application/vnd.github+json'
272
+ response = http.request(request)
273
+ data = JSON.parse(response.body)
274
+ { token: data['token'], expires_at: Time.parse(data['expires_at']) }
275
+ end
276
+
277
+ def cached_token(app_id, installation_id, key_path)
278
+ cache_file = '/tmp/github-app-token-cache'
279
+ if File.exist?(cache_file)
280
+ cache = JSON.parse(File.read(cache_file))
281
+ expires_at = Time.parse(cache['expires_at'])
282
+ return cache['token'] if expires_at > Time.now + 300
283
+ end
284
+ jwt = generate_jwt(app_id, key_path)
285
+ token_data = get_installation_token(jwt, installation_id)
286
+ File.write(cache_file, token_data.to_json)
287
+ token_data[:token]
288
+ end
289
+
290
+ app_id = ENV['GITHUB_APP_CLIENT_ID'] || ENV['GITHUB_APP_ID']
291
+ installation_id = ENV['GITHUB_APP_INSTALLATION_ID']
292
+ key_path = ENV['GITHUB_APP_PRIVATE_KEY_PATH']
293
+
294
+ puts cached_token(app_id, installation_id, key_path)
295
+ RUBY
296
+
297
+ # Only write if we have permission (we should as root or if /usr/local/bin is writable)
298
+ # If not, write to /tmp and use that
299
+ if File.writable?("/usr/local/bin")
300
+ File.write(helper_path, helper_content)
301
+ File.chmod(0755, helper_path)
302
+ else
303
+ helper_path = "/tmp/git-auth-helper"
304
+ File.write(helper_path, helper_content)
305
+ File.chmod(0755, helper_path)
306
+ end
307
+
308
+ # Configure git
309
+ 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'")
310
+
311
+ # Configure gh CLI
312
+ token = `#{helper_path}`.strip
313
+ if token.empty?
314
+ puts "❌ Failed to generate GitHub App token"
315
+ else
316
+ IO.popen("gh auth login --with-token 2>/dev/null", "w") { |io| io.puts token }
317
+ puts "✅ GitHub App authentication configured"
318
+ end
319
+
320
+ elsif ENV["GH_TOKEN"] && !ENV["GH_TOKEN"].empty?
321
+ system("echo '#{ENV['GH_TOKEN']}' | gh auth login --with-token 2>/dev/null")
322
+ puts "🔐 GitHub authentication configured"
323
+ else
324
+ puts "⚠️ No GH_TOKEN or GitHub App config - GitHub operations may fail"
325
+ end
326
+ end
327
+
328
+ def download_agent_bridge!
329
+ bridge_url = "#{TINKER_RAW_URL}/bin/agent-bridge"
330
+ bridge_tmux_url = "#{TINKER_RAW_URL}/bin/agent-bridge-tmux"
331
+ target_dir = "/usr/local/bin"
332
+
333
+ # Check if binaries are mounted at /tmp (dev mode)
334
+ if File.exist?("/tmp/agent-bridge")
335
+ puts "🔧 Installing mounted agent-bridge..."
336
+ system("sudo cp /tmp/agent-bridge #{target_dir}/agent-bridge")
337
+ system("sudo chmod +x #{target_dir}/agent-bridge")
338
+ else
339
+ puts "📥 Downloading agent-bridge..."
340
+ system("sudo curl -fsSL #{bridge_url} -o #{target_dir}/agent-bridge")
341
+ system("sudo chmod +x #{target_dir}/agent-bridge")
342
+ end
343
+
344
+ if File.exist?("/tmp/agent-bridge-tmux")
345
+ puts "🔧 Installing mounted agent-bridge-tmux..."
346
+ system("sudo cp /tmp/agent-bridge-tmux #{target_dir}/agent-bridge-tmux")
347
+ system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
348
+ else
349
+ system("sudo curl -fsSL #{bridge_tmux_url} -o #{target_dir}/agent-bridge-tmux")
350
+ system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
351
+ end
352
+
353
+ puts "✅ agent-bridge installed to #{target_dir}"
354
+
355
+ # Patch agent-bridge-tmux to force INSIDE_TMUX=1
356
+ puts "🔧 Patching agent-bridge-tmux to force INSIDE_TMUX=1..."
357
+
358
+ # Read the file (we can read /usr/local/bin files usually)
359
+ content = File.read("#{target_dir}/agent-bridge-tmux")
360
+
361
+ # Replace the command
362
+ new_content = content.gsub(
363
+ "&& agent-bridge\"",
364
+ "&& export INSIDE_TMUX=1 && agent-bridge\""
365
+ )
366
+
367
+ # Write to temp file
368
+ File.write("/tmp/agent-bridge-tmux-patched", new_content)
369
+
370
+ # Move to destination with sudo
371
+ system("sudo mv /tmp/agent-bridge-tmux-patched #{target_dir}/agent-bridge-tmux")
372
+ system("sudo chmod +x #{target_dir}/agent-bridge-tmux")
373
+
374
+ return target_dir
375
+ end
376
+
377
+ def run_agent!(bin_dir)
378
+ agent_type = ENV["AGENT_TYPE"]
379
+ puts ""
380
+ puts "🚀 Starting #{agent_type} agent..."
381
+ puts " Press Ctrl+B then D to detach from tmux"
382
+ puts ""
383
+
384
+ # Run agent-bridge-tmux which handles tmux session and status bar
385
+ exec("#{bin_dir}/agent-bridge-tmux")
386
+ end
387
+
388
+ # Main
389
+ puts "🤖 Tinker Agent Setup"
390
+ puts "====================="
391
+ puts ""
392
+
393
+ check_requirements!
394
+ check_env!
395
+ setup_mcp_config!
396
+ setup_claude_md!
397
+ setup_github_auth!
398
+ bin_dir = download_agent_bridge!
399
+ run_agent!(bin_dir)