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.
@@ -0,0 +1,95 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Install dependencies
5
+ apt-get update && apt-get install -y \
6
+ git curl tmux sudo unzip wget
7
+
8
+ # Install Node.js (required for Claude CLI)
9
+ if ! command -v node &> /dev/null; then
10
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
11
+ apt-get install -y nodejs
12
+ fi
13
+
14
+ # Install Claude CLI
15
+ npm install -g @anthropic-ai/claude-code
16
+
17
+ # Install GitHub CLI
18
+ curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
19
+ 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
20
+ apt-get update && apt-get install gh -y
21
+
22
+ # Setup User
23
+ # We want to run as the same UID as the host user (passed via build arg)
24
+ # to ensure we can edit mounted files.
25
+ USER_ID=${USER_ID:-1000}
26
+ GROUP_ID=${GROUP_ID:-1000}
27
+ AGENT_USER="claude"
28
+
29
+ # 1. Handle Group
30
+ if getent group ${GROUP_ID} >/dev/null 2>&1; then
31
+ # Group exists (e.g. 'node'), use it
32
+ GROUP_NAME=$(getent group ${GROUP_ID} | cut -d: -f1)
33
+ else
34
+ # Create group
35
+ groupadd -g ${GROUP_ID} ${AGENT_USER}
36
+ GROUP_NAME=${AGENT_USER}
37
+ fi
38
+
39
+ # 2. Handle User
40
+ if getent passwd ${USER_ID} >/dev/null 2>&1; then
41
+ # User exists (e.g. 'node'), use it
42
+ AGENT_USER=$(getent passwd ${USER_ID} | cut -d: -f1)
43
+ # Ensure user is in the group (if different)
44
+ usermod -aG ${GROUP_NAME} ${AGENT_USER}
45
+ else
46
+ # Create user
47
+ useradd -u ${USER_ID} -g ${GROUP_NAME} -m -s /bin/bash ${AGENT_USER}
48
+ fi
49
+
50
+ # 3. Grant Sudo
51
+ echo "${AGENT_USER} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
52
+
53
+ # 4. Determine Home Directory
54
+ AGENT_HOME=$(getent passwd ${AGENT_USER} | cut -d: -f6)
55
+
56
+ # Create entrypoint
57
+ cat << EOF > /entrypoint.sh
58
+ #!/bin/bash
59
+ set -e
60
+
61
+ # Copy config files if they exist in /tmp (mounted volumes)
62
+ if [ -f "/tmp/cfg/claude.json" ]; then
63
+ cp /tmp/cfg/claude.json ${AGENT_HOME}/.claude.json || echo "⚠️ Failed to copy claude.json"
64
+ fi
65
+ if [ -d "/tmp/cfg/claude_dir" ]; then
66
+ rm -rf ${AGENT_HOME}/.claude
67
+ cp -r /tmp/cfg/claude_dir ${AGENT_HOME}/.claude || echo "⚠️ Failed to copy claude_dir"
68
+ fi
69
+ if [ -f "/tmp/github-app-privkey.pem" ]; then
70
+ cp /tmp/github-app-privkey.pem ${AGENT_HOME}/.github-app-privkey.pem || echo "⚠️ Failed to copy github key"
71
+ chmod 600 ${AGENT_HOME}/.github-app-privkey.pem 2>/dev/null || true
72
+ fi
73
+
74
+ # Fix permissions
75
+ sudo chown -R ${AGENT_USER}:${GROUP_NAME} ${AGENT_HOME} || echo "⚠️ Failed to chown home"
76
+
77
+ # Fix permissions of the current directory (project root)
78
+ # This ensures the agent can write CLAUDE.md and .mcp.json
79
+ sudo chown ${AGENT_USER}:${GROUP_NAME} $(pwd) || echo "⚠️ Failed to chown project root"
80
+
81
+ # Set GitHub App Key Path if not set
82
+ if [ -z "\$GITHUB_APP_PRIVATE_KEY_PATH" ]; then
83
+ export GITHUB_APP_PRIVATE_KEY_PATH="${AGENT_HOME}/.github-app-privkey.pem"
84
+ else
85
+ # If it was set to /home/claude/... but we are /home/node, fix it
86
+ if [[ "\$GITHUB_APP_PRIVATE_KEY_PATH" == *"/home/claude/"* ]] && [ "${AGENT_HOME}" != "/home/claude" ]; then
87
+ export GITHUB_APP_PRIVATE_KEY_PATH="${AGENT_HOME}/.github-app-privkey.pem"
88
+ fi
89
+ fi
90
+
91
+ # Execute command as agent user
92
+ exec sudo -E -u ${AGENT_USER} env "HOME=${AGENT_HOME}" "\$@"
93
+ EOF
94
+
95
+ chmod +x /entrypoint.sh
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "tinker-agent",
3
+ "version": "1.0.0",
4
+ "description": "Tinker Agent Runner",
5
+ "bin": {
6
+ "tinker-agent": "./run-tinker-agent.rb"
7
+ },
8
+ "files": [
9
+ "run-tinker-agent.rb",
10
+ "agents.rb",
11
+ "setup-agent.rb",
12
+ "bin/agent-bridge-tmux",
13
+ "bin/install-agent.sh"
14
+ ],
15
+ "scripts": {
16
+ "test": "echo \"Error: no test specified\" && exit 1"
17
+ },
18
+ "author": "",
19
+ "license": "ISC"
20
+ }
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Tinker Agent Runner
5
+ # Usage: npx tinker-agent [worker|planner|reviewer|orchestrator|researcher]
6
+ # npx tinker-agent attach [agent-type]
7
+ #
8
+ # Requirements:
9
+ # - Docker
10
+ # - Ruby
11
+ # - Dockerfile.sandbox in project root
12
+ # - tinker.env.json in project root (gitignored)
13
+
14
+ require "json"
15
+
16
+ # Load agent configs
17
+ require_relative "agents"
18
+
19
+ IMAGE_NAME = "tinker-sandbox"
20
+
21
+ AGENT_TYPES = AGENT_CONFIGS.keys.freeze
22
+
23
+ def load_config
24
+ config_file = File.join(Dir.pwd, "tinker.env.json")
25
+
26
+ unless File.exist?(config_file)
27
+ puts "❌ Error: tinker.env.json not found in current directory"
28
+ puts ""
29
+ puts "Create it:"
30
+ puts " curl -fsSL https://raw.githubusercontent.com/RoM4iK/tinker-public/main/tinker.env.example.json -o tinker.env.json"
31
+ puts " # Edit with your project config"
32
+ puts " echo 'tinker.env.json' >> .gitignore"
33
+ exit 1
34
+ end
35
+
36
+ JSON.parse(File.read(config_file))
37
+ end
38
+
39
+ def check_dockerfile!
40
+ unless File.exist?("Dockerfile.sandbox")
41
+ puts "❌ Error: Dockerfile.sandbox not found"
42
+ puts ""
43
+ puts "Please create Dockerfile.sandbox by copying your existing Dockerfile"
44
+ puts "and adding the required agent dependencies."
45
+ puts ""
46
+ puts "See https://github.com/RoM4iK/tinker-public/blob/main/README.md for instructions."
47
+ exit 1
48
+ end
49
+ end
50
+
51
+ def build_docker_image
52
+ check_dockerfile!
53
+
54
+ user_id = `id -u`.strip
55
+ group_id = `id -g`.strip
56
+
57
+ puts "🏗️ Building Docker image..."
58
+
59
+ success = system(
60
+ "docker", "build",
61
+ "--build-arg", "USER_ID=#{user_id}",
62
+ "--build-arg", "GROUP_ID=#{group_id}",
63
+ "-t", IMAGE_NAME,
64
+ "-f", "Dockerfile.sandbox",
65
+ "."
66
+ )
67
+
68
+ unless success
69
+ puts "❌ Failed to build Docker image"
70
+ exit 1
71
+ end
72
+
73
+ puts "✅ Docker image built"
74
+ end
75
+
76
+ def run_agent(agent_type, config)
77
+ unless AGENT_TYPES.include?(agent_type)
78
+ puts "❌ Unknown agent type: #{agent_type}"
79
+ puts " Available: #{AGENT_TYPES.join(', ')}"
80
+ exit 1
81
+ end
82
+
83
+ agent_def = AGENT_CONFIGS[agent_type]
84
+ agent_config = config.dig("agents", agent_type) || {}
85
+ container_name = agent_config["container_name"] || agent_def[:name]
86
+
87
+ puts "🚀 Starting #{agent_type} agent..."
88
+
89
+ # Stop existing container if running
90
+ system("docker", "rm", "-f", container_name, err: File::NULL, out: File::NULL)
91
+
92
+ # Write banner to a persistent temp file (not auto-deleted)
93
+ banner_path = "/tmp/tinker-agent-banner-#{agent_type}.txt"
94
+ File.write(banner_path, agent_def[:banner])
95
+
96
+ docker_cmd = [
97
+ "docker", "run", "-d",
98
+ "--name", container_name,
99
+ "--network=host",
100
+ # Mount Claude config
101
+ "-v", "#{ENV['HOME']}/.claude.json:/tmp/cfg/claude.json:ro",
102
+ "-v", "#{ENV['HOME']}/.claude:/tmp/cfg/claude_dir:ro",
103
+ # Mount agent banner for CLAUDE.md
104
+ "-v", "#{banner_path}:/tmp/agent-banner.txt:ro",
105
+ # Skills are downloaded inside container (see entrypoint)
106
+ "-e", "TINKER_VERSION=main",
107
+ # Pass config as env vars
108
+ "-e", "AGENT_TYPE=#{agent_type}",
109
+ "-e", "PROJECT_ID=#{config['project_id']}",
110
+ "-e", "RAILS_WS_URL=#{config['rails_ws_url']}",
111
+ "-e", "RAILS_API_URL=#{config['rails_api_url']}",
112
+ "-e", "RAILS_API_KEY=#{agent_config['mcp_api_key']}"
113
+ ]
114
+
115
+ # Add Anthropic config
116
+ if (anthropic = config["anthropic"])
117
+ docker_cmd += ["-e", "ANTHROPIC_BASE_URL=#{anthropic['base_url']}"] if anthropic["base_url"]
118
+ docker_cmd += ["-e", "ANTHROPIC_MODEL=#{anthropic['model']}"] if anthropic["model"]
119
+ end
120
+
121
+ # Add GitHub auth
122
+ github = config["github"] || {}
123
+ if github["method"] == "app"
124
+ docker_cmd += [
125
+ "-e", "GITHUB_APP_CLIENT_ID=#{github['app_client_id']}",
126
+ "-e", "GITHUB_APP_INSTALLATION_ID=#{github['app_installation_id']}",
127
+ # Path is set dynamically in entrypoint.sh based on user home
128
+ "-v", "#{github['app_private_key_path']}:/tmp/github-app-privkey.pem:ro"
129
+ ]
130
+ puts "🔐 Using GitHub App authentication"
131
+ elsif github["token"]
132
+ docker_cmd += ["-e", "GH_TOKEN=#{github['token']}"]
133
+ puts "🔑 Using GitHub token authentication"
134
+ else
135
+ puts "⚠️ Warning: No GitHub authentication configured"
136
+ end
137
+
138
+ # Add git config
139
+ if (git_config = config["git"])
140
+ docker_cmd += ["-e", "GIT_USER_NAME=#{git_config['user_name']}"] if git_config["user_name"]
141
+ docker_cmd += ["-e", "GIT_USER_EMAIL=#{git_config['user_email']}"] if git_config["user_email"]
142
+ end
143
+
144
+ # Check for local setup-agent.rb (for development)
145
+ local_setup_script = File.join(File.dirname(__FILE__), "setup-agent.rb")
146
+
147
+ # Check for local agent-bridge binaries (for development)
148
+ local_bridge = File.join(Dir.pwd, "bin", "agent-bridge")
149
+ local_tmux = File.join(File.dirname(__FILE__), "bin", "agent-bridge-tmux")
150
+
151
+ mounts = []
152
+ if File.exist?(local_setup_script)
153
+ puts "🔧 Using local setup-agent.rb for development"
154
+ mounts += ["-v", "#{File.expand_path(local_setup_script)}:/tmp/setup-agent.rb:ro"]
155
+ end
156
+
157
+ if File.exist?(local_bridge)
158
+ puts "🔧 Using local agent-bridge binary"
159
+ mounts += ["-v", "#{local_bridge}:/tmp/agent-bridge:ro"]
160
+ end
161
+
162
+ if File.exist?(local_tmux)
163
+ mounts += ["-v", "#{File.expand_path(local_tmux)}:/tmp/agent-bridge-tmux:ro"]
164
+ end
165
+
166
+ docker_cmd += mounts
167
+
168
+ if File.exist?(local_setup_script)
169
+ docker_cmd += [IMAGE_NAME, "ruby", "/tmp/setup-agent.rb"]
170
+ else
171
+ docker_cmd += [IMAGE_NAME]
172
+ end
173
+
174
+ success = system(*docker_cmd)
175
+
176
+ if success
177
+ puts "✅ Agent started in background"
178
+ puts ""
179
+ puts " Attach: npx tinker-agent attach #{agent_type}"
180
+ puts " Logs: docker logs -f #{container_name}"
181
+ puts " Stop: docker stop #{container_name}"
182
+ else
183
+ puts "❌ Failed to start agent"
184
+ exit 1
185
+ end
186
+ end
187
+
188
+ def attach_to_agent(agent_type, config)
189
+ unless AGENT_TYPES.include?(agent_type)
190
+ puts "❌ Unknown agent type: #{agent_type}"
191
+ exit 1
192
+ end
193
+
194
+ agent_config = config.dig("agents", agent_type) || {}
195
+ container_name = agent_config["container_name"] || "tinker-#{agent_type}"
196
+
197
+ running = `docker ps --filter name=^#{container_name}$ --format '{{.Names}}'`.strip
198
+
199
+ if running.empty?
200
+ puts "❌ #{agent_type} agent is not running"
201
+ puts " Start with: npx tinker-agent #{agent_type}"
202
+ exit 1
203
+ end
204
+
205
+ puts "📎 Attaching to #{agent_type} agent..."
206
+
207
+ # Determine the user to attach as
208
+ # We assume the agent is running as the user with the same UID as the host user
209
+ # (since that's how we build the image)
210
+ uid = Process.uid
211
+ user = `docker exec #{container_name} getent passwd #{uid} | cut -d: -f1`.strip
212
+
213
+ if user.empty?
214
+ # Fallback to 'claude' if detection fails (e.g. different UID mapping)
215
+ puts "⚠️ Could not detect agent user for UID #{uid}, defaulting to 'claude'"
216
+ user = "claude"
217
+ end
218
+
219
+ puts " User: #{user} (UID: #{uid})"
220
+
221
+ # Attach to agent session which has the status bar
222
+ # Must run as agent user since tmux server runs under that user
223
+ exec("docker", "exec", "-it", "-u", user, container_name, "tmux", "attach", "-t", "agent")
224
+ end
225
+
226
+ def show_usage
227
+ puts "Tinker Agent Runner"
228
+ puts ""
229
+ puts "Usage: npx tinker-agent [worker|planner|reviewer|orchestrator|researcher]"
230
+ puts " npx tinker-agent attach [agent-type]"
231
+ puts ""
232
+ puts "Setup:"
233
+ puts " 1. Create Dockerfile.sandbox (see https://github.com/RoM4iK/tinker-public/blob/main/README.md)"
234
+ puts " 2. curl -fsSL https://raw.githubusercontent.com/RoM4iK/tinker-public/main/tinker.env.example.json -o tinker.env.json"
235
+ puts " 3. Edit tinker.env.json with your config"
236
+ puts " 4. echo 'tinker.env.json' >> .gitignore"
237
+ puts " 5. npx tinker-agent worker"
238
+ exit 1
239
+ end
240
+
241
+ # Main
242
+ show_usage if ARGV.empty?
243
+
244
+ command = ARGV[0].downcase
245
+
246
+ if command == "attach"
247
+ agent_type = ARGV[1]&.downcase
248
+ abort "Usage: npx tinker-agent attach [agent-type]" unless agent_type
249
+ config = load_config
250
+ attach_to_agent(agent_type, config)
251
+ else
252
+ config = load_config
253
+ build_docker_image
254
+ run_agent(command, config)
255
+ end