tinker-agent 1.0.45 โ 1.0.47
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/agents.rb +1 -1
- package/lib/tinker_agent/agent.rb +239 -0
- package/lib/tinker_agent/config.rb +78 -0
- package/lib/tinker_agent/docker.rb +72 -0
- package/package.json +2 -1
package/agents.rb
CHANGED
|
@@ -205,7 +205,7 @@ If blocked or workflow is broken:
|
|
|
205
205
|
},
|
|
206
206
|
'researcher' => {
|
|
207
207
|
name: 'tinker-autonomous-researcher',
|
|
208
|
-
skills: ['researcher-
|
|
208
|
+
skills: ['researcher-tactical', 'researcher-strategic', 'researcher-digest', 'memory', 'proposal-execution', 'memory-consolidation', 'retrospective'],
|
|
209
209
|
banner: <<~BANNER
|
|
210
210
|
You are the TINKER RESEARCHER agent operating in FULLY AUTONOMOUS MODE.
|
|
211
211
|
Your role is AUTONOMOUS ANALYSIS and PROPOSAL GENERATION.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TinkerAgent
|
|
4
|
+
module Agent
|
|
5
|
+
def self.run(agent_type, config, agent_configs)
|
|
6
|
+
unless agent_configs.keys.include?(agent_type)
|
|
7
|
+
puts "โ Unknown agent type: #{agent_type}"
|
|
8
|
+
puts " Available: #{agent_configs.keys.join(', ')}"
|
|
9
|
+
exit 1
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
agent_def = agent_configs[agent_type]
|
|
13
|
+
agent_config = config.dig("agents", agent_type) || {}
|
|
14
|
+
container_name = agent_config["container_name"] || agent_def[:name]
|
|
15
|
+
|
|
16
|
+
puts "๐ Starting #{agent_type} agent..."
|
|
17
|
+
|
|
18
|
+
# Stop existing container if running
|
|
19
|
+
system("docker", "rm", "-f", container_name, err: File::NULL, out: File::NULL)
|
|
20
|
+
|
|
21
|
+
# Write banner to a persistent temp file (not auto-deleted)
|
|
22
|
+
banner_path = "/tmp/tinker-agent-banner-#{agent_type}.txt"
|
|
23
|
+
File.write(banner_path, agent_def[:banner])
|
|
24
|
+
|
|
25
|
+
docker_cmd = build_docker_command(container_name, agent_type, config, agent_config, agent_def, banner_path)
|
|
26
|
+
|
|
27
|
+
success = system(*docker_cmd)
|
|
28
|
+
|
|
29
|
+
if success
|
|
30
|
+
puts "โ
Agent started in background"
|
|
31
|
+
puts ""
|
|
32
|
+
puts " Attach: npx tinker-agent attach #{agent_type}"
|
|
33
|
+
puts " Logs: docker logs -f #{container_name}"
|
|
34
|
+
puts " Stop: docker stop #{container_name}"
|
|
35
|
+
else
|
|
36
|
+
puts "โ Failed to start agent"
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.attach(agent_type, config, agent_configs)
|
|
42
|
+
unless agent_configs.keys.include?(agent_type)
|
|
43
|
+
puts "โ Unknown agent type: #{agent_type}"
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
agent_def = agent_configs[agent_type]
|
|
48
|
+
agent_config = config.dig("agents", agent_type) || {}
|
|
49
|
+
container_name = agent_config["container_name"] || agent_def[:name]
|
|
50
|
+
|
|
51
|
+
running = `docker ps --filter name=^#{container_name}$ --format '{{.Names}}'`.strip
|
|
52
|
+
|
|
53
|
+
if running.empty?
|
|
54
|
+
puts "โ ๏ธ #{agent_type} agent is not running. Auto-starting..."
|
|
55
|
+
Docker.build_image(config)
|
|
56
|
+
run(agent_type, config, agent_configs)
|
|
57
|
+
sleep 3
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
puts "๐ Attaching to #{agent_type} agent..."
|
|
61
|
+
|
|
62
|
+
user = detect_agent_user(container_name)
|
|
63
|
+
puts " User: #{user}"
|
|
64
|
+
|
|
65
|
+
# Wait for tmux session to be ready
|
|
66
|
+
10.times do
|
|
67
|
+
if system("docker", "exec", "-u", user, container_name, "tmux", "has-session", "-t", "agent", err: File::NULL, out: File::NULL)
|
|
68
|
+
break
|
|
69
|
+
end
|
|
70
|
+
sleep 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Attach to agent session which has the status bar
|
|
74
|
+
# Must run as agent user since tmux server runs under that user
|
|
75
|
+
exec("docker", "exec", "-it", "-u", user, container_name, "tmux", "attach", "-t", "agent")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def self.build_docker_command(container_name, agent_type, config, agent_config, agent_def, banner_path)
|
|
81
|
+
docker_cmd = [
|
|
82
|
+
"docker", "run", "-d",
|
|
83
|
+
"--name", container_name,
|
|
84
|
+
"--network", "host",
|
|
85
|
+
"--restart", "unless-stopped",
|
|
86
|
+
"--tmpfs", "/rails/tmp",
|
|
87
|
+
"--tmpfs", "/rails/log"
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# Inject custom environment variables from config
|
|
91
|
+
# Merge global env with agent-specific env (agent-specific takes precedence)
|
|
92
|
+
merged_env = {}
|
|
93
|
+
merged_env.merge!(config["env"]) if config["env"]
|
|
94
|
+
merged_env.merge!(agent_config["env"]) if agent_config["env"]
|
|
95
|
+
|
|
96
|
+
if merged_env.any?
|
|
97
|
+
merged_env.each do |k, v|
|
|
98
|
+
docker_cmd += ["-e", "#{k}=#{v}"]
|
|
99
|
+
end
|
|
100
|
+
global_count = config["env"]&.size || 0
|
|
101
|
+
agent_count = agent_config["env"]&.size || 0
|
|
102
|
+
if agent_count > 0
|
|
103
|
+
puts "๐ฟ Injected #{global_count} global + #{agent_count} agent-specific env vars"
|
|
104
|
+
else
|
|
105
|
+
puts "๐ฟ Injected #{global_count} custom env vars from config"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
docker_cmd += [
|
|
110
|
+
# Mount Claude config
|
|
111
|
+
"-v", "#{ENV['HOME']}/.claude.json:/tmp/cfg/claude.json:ro",
|
|
112
|
+
"-v", "#{ENV['HOME']}/.claude:/tmp/cfg/claude_dir:ro",
|
|
113
|
+
"-v", "#{banner_path}:/etc/tinker/system-prompt.txt:ro",
|
|
114
|
+
"-e", "TINKER_VERSION=main",
|
|
115
|
+
"-e", "SKILLS=#{agent_def[:skills]&.join(',')}",
|
|
116
|
+
"-e", "AGENT_TYPE=#{agent_type}",
|
|
117
|
+
"-e", "PROJECT_ID=#{config['project_id']}",
|
|
118
|
+
"-e", "RAILS_WS_URL=#{config['rails_ws_url']}",
|
|
119
|
+
"-e", "RAILS_API_URL=#{config['rails_api_url']}",
|
|
120
|
+
"-e", "RAILS_API_KEY=#{agent_config['mcp_api_key']}"
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
add_github_auth!(docker_cmd, config)
|
|
124
|
+
add_git_config!(docker_cmd, config)
|
|
125
|
+
add_development_mounts!(docker_cmd)
|
|
126
|
+
|
|
127
|
+
local_setup_script = File.join(File.dirname(__FILE__), "..", "..", "setup-agent.rb")
|
|
128
|
+
|
|
129
|
+
if File.exist?(local_setup_script)
|
|
130
|
+
docker_cmd += [Config.image_name(config), "ruby", "/tmp/setup-agent.rb"]
|
|
131
|
+
else
|
|
132
|
+
docker_cmd += [Config.image_name(config)]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
docker_cmd
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.add_github_auth!(docker_cmd, config)
|
|
139
|
+
github = config["github"] || {}
|
|
140
|
+
|
|
141
|
+
if github["method"] == "app"
|
|
142
|
+
key_path = File.expand_path(github["app_private_key_path"].to_s)
|
|
143
|
+
|
|
144
|
+
unless File.exist?(key_path) && !File.directory?(key_path)
|
|
145
|
+
puts "โ Error: GitHub App private key not found at: #{key_path}"
|
|
146
|
+
puts " Please check 'app_private_key_path' in tinker.env.rb"
|
|
147
|
+
exit 1
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
docker_cmd.concat([
|
|
151
|
+
"-e", "GITHUB_APP_CLIENT_ID=#{github['app_client_id']}",
|
|
152
|
+
"-e", "GITHUB_APP_INSTALLATION_ID=#{github['app_installation_id']}",
|
|
153
|
+
"-v", "#{key_path}:/tmp/github-app-privkey.pem:ro"
|
|
154
|
+
])
|
|
155
|
+
puts "๐ Using GitHub App authentication"
|
|
156
|
+
elsif github["token"]
|
|
157
|
+
docker_cmd.concat(["-e", "GH_TOKEN=#{github['token']}"])
|
|
158
|
+
puts "๐ Using GitHub token authentication"
|
|
159
|
+
else
|
|
160
|
+
puts "โ Error: No GitHub authentication configured"
|
|
161
|
+
puts " Please configure 'github' in tinker.env.rb"
|
|
162
|
+
exit 1
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.add_git_config!(docker_cmd, config)
|
|
167
|
+
return unless (git_config = config["git"])
|
|
168
|
+
|
|
169
|
+
docker_cmd.concat(["-e", "GIT_USER_NAME=#{git_config['user_name']}"]) if git_config["user_name"]
|
|
170
|
+
docker_cmd.concat(["-e", "GIT_USER_EMAIL=#{git_config['user_email']}"]) if git_config["user_email"]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.add_development_mounts!(docker_cmd)
|
|
174
|
+
# Check for local setup-agent.rb (for development)
|
|
175
|
+
local_setup_script = File.join(File.dirname(__FILE__), "..", "..", "setup-agent.rb")
|
|
176
|
+
|
|
177
|
+
# Check for local agent-bridge binaries (for development)
|
|
178
|
+
arch = `uname -m`.strip
|
|
179
|
+
linux_arch = (arch == "x86_64") ? "amd64" : "arm64"
|
|
180
|
+
linux_bridge = File.join(Dir.pwd, "tinker-public", "bin", "agent-bridge-linux-#{linux_arch}")
|
|
181
|
+
|
|
182
|
+
local_bridge_default = File.join(Dir.pwd, "bin", "agent-bridge")
|
|
183
|
+
local_tmux = File.join(File.dirname(__FILE__), "..", "..", "bin", "agent-bridge-tmux")
|
|
184
|
+
|
|
185
|
+
if File.exist?(local_setup_script)
|
|
186
|
+
puts "๐ง Using local setup-agent.rb for development"
|
|
187
|
+
docker_cmd.concat(["-v", "#{File.expand_path(local_setup_script)}:/tmp/setup-agent.rb:ro"])
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
if File.exist?(linux_bridge)
|
|
191
|
+
puts "๐ง Using local linux binary: #{linux_bridge}"
|
|
192
|
+
docker_cmd.concat(["-v", "#{linux_bridge}:/tmp/agent-bridge:ro"])
|
|
193
|
+
elsif File.exist?(local_bridge_default)
|
|
194
|
+
# Check if it's a binary or script
|
|
195
|
+
is_script = File.read(local_bridge_default, 4) == "#!/b"
|
|
196
|
+
if is_script
|
|
197
|
+
puts "โ ๏ธ bin/agent-bridge is a host wrapper script. Please run 'bin/build-bridge' to generate linux binaries."
|
|
198
|
+
else
|
|
199
|
+
puts "๐ง Using local agent-bridge binary"
|
|
200
|
+
docker_cmd.concat(["-v", "#{local_bridge_default}:/tmp/agent-bridge:ro"])
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if File.exist?(local_tmux)
|
|
205
|
+
docker_cmd.concat(["-v", "#{File.expand_path(local_tmux)}:/tmp/agent-bridge-tmux:ro"])
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def self.detect_agent_user(container_name)
|
|
210
|
+
# Determine the user to attach as
|
|
211
|
+
# Robust method: find the user running the agent process (tmux or bridge)
|
|
212
|
+
user = `docker exec #{container_name} ps aux | grep "[a]gent-bridge-tmux" | awk '{print $1}' | head -n 1`.strip
|
|
213
|
+
|
|
214
|
+
if user.empty?
|
|
215
|
+
user = `docker exec #{container_name} ps aux | grep "[t]mux new-session" | awk '{print $1}' | head -n 1`.strip
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
if user.empty?
|
|
219
|
+
# Fallback to previous heuristic
|
|
220
|
+
detected_user = `docker exec #{container_name} whoami 2>/dev/null`.strip
|
|
221
|
+
if detected_user == "root" || detected_user.empty?
|
|
222
|
+
uid = Process.uid
|
|
223
|
+
mapped_user = `docker exec #{container_name} getent passwd #{uid} | cut -d: -f1`.strip
|
|
224
|
+
user = mapped_user unless mapped_user.empty?
|
|
225
|
+
else
|
|
226
|
+
user = detected_user
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if user.empty?
|
|
231
|
+
# Final Fallback
|
|
232
|
+
user = "rails"
|
|
233
|
+
puts "โ ๏ธ Could not detect agent user, defaulting to '#{user}'"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
user
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TinkerAgent
|
|
6
|
+
module Config
|
|
7
|
+
def self.load
|
|
8
|
+
rb_config_file = File.join(Dir.pwd, "tinker.env.rb")
|
|
9
|
+
|
|
10
|
+
unless File.exist?(rb_config_file)
|
|
11
|
+
puts "โ Error: tinker.env.rb not found in current directory"
|
|
12
|
+
puts ""
|
|
13
|
+
puts "Create tinker.env.rb:"
|
|
14
|
+
puts " {"
|
|
15
|
+
puts " project_id: 1,"
|
|
16
|
+
puts " rails_ws_url: '...',"
|
|
17
|
+
puts " # ..."
|
|
18
|
+
puts " # Paste your stripped .env content here:"
|
|
19
|
+
puts " dot_env: <<~ENV"
|
|
20
|
+
puts " STRIPE_KEY=sk_test_..."
|
|
21
|
+
puts " OPENAI_KEY=sk-..."
|
|
22
|
+
puts " ENV"
|
|
23
|
+
puts " }"
|
|
24
|
+
puts " echo 'tinker.env.rb' >> .gitignore"
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "โ๏ธ Loading configuration from tinker.env.rb"
|
|
29
|
+
config = eval(File.read(rb_config_file), binding, rb_config_file)
|
|
30
|
+
|
|
31
|
+
# Convert symbols to strings for easier handling before JSON normalization
|
|
32
|
+
config = config.transform_keys(&:to_s)
|
|
33
|
+
|
|
34
|
+
# Parse dot_env heredoc if present
|
|
35
|
+
if (dotenv = config["dot_env"])
|
|
36
|
+
config["env"] ||= {}
|
|
37
|
+
# Ensure env is string-keyed
|
|
38
|
+
config["env"] = config["env"].transform_keys(&:to_s)
|
|
39
|
+
|
|
40
|
+
dotenv.each_line do |line|
|
|
41
|
+
line = line.strip
|
|
42
|
+
next if line.empty? || line.start_with?('#')
|
|
43
|
+
k, v = line.split('=', 2)
|
|
44
|
+
next unless k && v
|
|
45
|
+
# Remove surrounding quotes and trailing comments (simple)
|
|
46
|
+
v = v.strip.gsub(/^['"]|['"]$/, '')
|
|
47
|
+
config["env"][k.strip] = v
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
config.delete("dot_env")
|
|
51
|
+
puts "๐ฟ Parsed dot_env into #{config['env'].size} environment variables"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Normalize per-agent env hashes
|
|
55
|
+
if config["agents"].is_a?(Hash)
|
|
56
|
+
config["agents"].each do |agent_key, agent_config|
|
|
57
|
+
agent_key = agent_key.to_s
|
|
58
|
+
if agent_config.is_a?(Hash) && agent_config["env"].is_a?(Hash)
|
|
59
|
+
agent_config["env"] = agent_config["env"].transform_keys(&:to_s)
|
|
60
|
+
elsif agent_config.is_a?(Hash) && agent_config[:env].is_a?(Hash)
|
|
61
|
+
agent_config["env"] = agent_config[:env].transform_keys(&:to_s)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Normalize symbols to strings for consistency via JSON round-trip
|
|
67
|
+
JSON.parse(JSON.generate(config))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.image_name(config)
|
|
71
|
+
if config["project_id"]
|
|
72
|
+
"tinker-sandbox-#{config['project_id']}"
|
|
73
|
+
else
|
|
74
|
+
"tinker-sandbox"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module TinkerAgent
|
|
6
|
+
module Docker
|
|
7
|
+
def self.check_dockerfile!
|
|
8
|
+
unless File.exist?("Dockerfile.sandbox")
|
|
9
|
+
puts "โ Error: Dockerfile.sandbox not found"
|
|
10
|
+
puts ""
|
|
11
|
+
puts "Please create Dockerfile.sandbox by copying your existing Dockerfile"
|
|
12
|
+
puts "and adding the required agent dependencies."
|
|
13
|
+
puts ""
|
|
14
|
+
puts "See https://github.com/RoM4iK/tinker-public/blob/main/README.md for instructions."
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.build_image(config)
|
|
20
|
+
check_dockerfile!
|
|
21
|
+
|
|
22
|
+
user_id = `id -u`.strip
|
|
23
|
+
group_id = `id -g`.strip
|
|
24
|
+
|
|
25
|
+
puts "๐๏ธ Building Docker image..."
|
|
26
|
+
|
|
27
|
+
# Handle .dockerignore.sandbox
|
|
28
|
+
dockerignore_sandbox = ".dockerignore.sandbox"
|
|
29
|
+
dockerignore_original = ".dockerignore"
|
|
30
|
+
dockerignore_backup = ".dockerignore.bak"
|
|
31
|
+
|
|
32
|
+
has_sandbox_ignore = File.exist?(dockerignore_sandbox)
|
|
33
|
+
has_original_ignore = File.exist?(dockerignore_original)
|
|
34
|
+
|
|
35
|
+
if has_sandbox_ignore
|
|
36
|
+
puts "๐ฆ Swapping .dockerignore with .dockerignore.sandbox..."
|
|
37
|
+
if has_original_ignore
|
|
38
|
+
FileUtils.mv(dockerignore_original, dockerignore_backup)
|
|
39
|
+
end
|
|
40
|
+
FileUtils.cp(dockerignore_sandbox, dockerignore_original)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
success = false
|
|
44
|
+
begin
|
|
45
|
+
success = system(
|
|
46
|
+
"docker", "build",
|
|
47
|
+
"--build-arg", "USER_ID=#{user_id}",
|
|
48
|
+
"--build-arg", "GROUP_ID=#{group_id}",
|
|
49
|
+
"-t", Config.image_name(config),
|
|
50
|
+
"-f", "Dockerfile.sandbox",
|
|
51
|
+
"."
|
|
52
|
+
)
|
|
53
|
+
ensure
|
|
54
|
+
if has_sandbox_ignore
|
|
55
|
+
# Restore original state
|
|
56
|
+
FileUtils.rm(dockerignore_original) if File.exist?(dockerignore_original)
|
|
57
|
+
if has_original_ignore
|
|
58
|
+
FileUtils.mv(dockerignore_backup, dockerignore_original)
|
|
59
|
+
end
|
|
60
|
+
puts "๐งน Restored original .dockerignore"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
unless success
|
|
65
|
+
puts "โ Failed to build Docker image"
|
|
66
|
+
exit 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
puts "โ
Docker image built"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tinker-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.47",
|
|
4
4
|
"description": "Tinker Agent Runner",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tinker-agent": "./run-tinker-agent.rb"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"run-tinker-agent.rb",
|
|
10
10
|
"agents.rb",
|
|
11
11
|
"setup-agent.rb",
|
|
12
|
+
"lib/",
|
|
12
13
|
"bin/agent-bridge-tmux",
|
|
13
14
|
"bin/install-agent.sh"
|
|
14
15
|
],
|