brainiac 0.0.1
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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +2 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +126 -0
- data/README.md +1166 -0
- data/Rakefile +12 -0
- data/bin/brainiac +1521 -0
- data/brainiac.gemspec +30 -0
- data/certs/stowzilla.pem +26 -0
- data/docs/waybar-config.md +96 -0
- data/lib/brainiac/agents.rb +203 -0
- data/lib/brainiac/brain.rb +197 -0
- data/lib/brainiac/card_index.rb +389 -0
- data/lib/brainiac/config.rb +263 -0
- data/lib/brainiac/cron.rb +629 -0
- data/lib/brainiac/deployments.rb +258 -0
- data/lib/brainiac/handlers/discord.rb +1643 -0
- data/lib/brainiac/handlers/fizzy.rb +1249 -0
- data/lib/brainiac/handlers/github.rb +598 -0
- data/lib/brainiac/handlers/zoho.rb +487 -0
- data/lib/brainiac/helpers.rb +760 -0
- data/lib/brainiac/planning.rb +237 -0
- data/lib/brainiac/prompts.rb +620 -0
- data/lib/brainiac/sessions.rb +282 -0
- data/lib/brainiac/skills.rb +276 -0
- data/lib/brainiac/users.rb +76 -0
- data/lib/brainiac/version.rb +6 -0
- data/lib/brainiac/zoho_mail_api.rb +109 -0
- data/lib/brainiac.rb +10 -0
- data/lib/user_registry.rb +159 -0
- data/monitor/daemon.rb +99 -0
- data/monitor/deploy-env-macos.rb +131 -0
- data/monitor/menubar.rb +295 -0
- data/monitor/open-action.sh +15 -0
- data/monitor/setup-menubar.rb +78 -0
- data/monitor/setup-waybar-deploy-envs.rb +121 -0
- data/monitor/setup-waybar-deployments.rb +96 -0
- data/monitor/setup-waybar-module.rb +113 -0
- data/monitor/setup-xbar-plugin.rb +35 -0
- data/monitor/view-logs-macos.rb +210 -0
- data/monitor/view-logs-rofi.rb +194 -0
- data/monitor/view-logs.rb +119 -0
- data/monitor/waybar-config-updater.rb +56 -0
- data/monitor/waybar-deploy-env.rb +206 -0
- data/monitor/waybar-deployments.rb +239 -0
- data/monitor/waybar.rb +146 -0
- data/monitor/xbar.3s.rb +179 -0
- data/receiver.rb +956 -0
- data/templates/agents.json.example +10 -0
- data/templates/discord.json.example +17 -0
- data/templates/fizzy.json.example +24 -0
- data/templates/github.json.example +4 -0
- data/templates/testflight.json.example +8 -0
- data/templates/users.json.example +121 -0
- data/templates/zoho.json.example +27 -0
- data/views/dashboard.erb +437 -0
- data.tar.gz.sig +0 -0
- metadata +235 -0
- metadata.gz.sig +0 -0
data/bin/brainiac
ADDED
|
@@ -0,0 +1,1521 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "net/http"
|
|
8
|
+
require "uri"
|
|
9
|
+
require "time"
|
|
10
|
+
|
|
11
|
+
# Brainiac CLI - Register and manage projects with Brainiac server
|
|
12
|
+
|
|
13
|
+
BRAINIAC_ROOT = File.expand_path("..", __dir__)
|
|
14
|
+
require_relative "../lib/brainiac/version"
|
|
15
|
+
BRAINIAC_VERSION = Brainiac::VERSION
|
|
16
|
+
|
|
17
|
+
BRAINIAC_DIR = File.join(Dir.home, ".brainiac")
|
|
18
|
+
PROJECTS_FILE = File.join(BRAINIAC_DIR, "projects.json")
|
|
19
|
+
CONFIG_FILE = File.join(BRAINIAC_DIR, "config.json")
|
|
20
|
+
PID_FILE = File.join(BRAINIAC_DIR, "server.pid")
|
|
21
|
+
|
|
22
|
+
def ensure_brainiac_dir
|
|
23
|
+
FileUtils.mkdir_p(BRAINIAC_DIR)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def load_projects
|
|
27
|
+
return {} unless File.exist?(PROJECTS_FILE)
|
|
28
|
+
|
|
29
|
+
JSON.parse(File.read(PROJECTS_FILE))
|
|
30
|
+
rescue JSON::ParserError
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def save_projects(projects)
|
|
35
|
+
ensure_brainiac_dir
|
|
36
|
+
File.write(PROJECTS_FILE, JSON.pretty_generate(projects))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def load_config
|
|
40
|
+
return {} unless File.exist?(CONFIG_FILE)
|
|
41
|
+
|
|
42
|
+
JSON.parse(File.read(CONFIG_FILE))
|
|
43
|
+
rescue JSON::ParserError
|
|
44
|
+
{}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def save_config(config)
|
|
48
|
+
ensure_brainiac_dir
|
|
49
|
+
File.write(CONFIG_FILE, JSON.pretty_generate(config))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_server_pid
|
|
53
|
+
# First check PID file
|
|
54
|
+
if File.exist?(PID_FILE)
|
|
55
|
+
pid = File.read(PID_FILE).strip.to_i
|
|
56
|
+
return pid if pid.positive? && process_running?(pid)
|
|
57
|
+
|
|
58
|
+
# Stale PID file
|
|
59
|
+
File.delete(PID_FILE)
|
|
60
|
+
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Fallback: search for running receiver.rb process
|
|
64
|
+
# Try multiple methods to find the process
|
|
65
|
+
|
|
66
|
+
# Method 1: pgrep with full pattern
|
|
67
|
+
output, status = Open3.capture2("pgrep", "-f", 'ruby.*receiver\.rb')
|
|
68
|
+
if status.success? && !output.strip.empty?
|
|
69
|
+
pids = output.strip.split("\n").map(&:to_i)
|
|
70
|
+
return pids.first if pids.any?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Method 2: ps + grep (more portable)
|
|
74
|
+
output, status = Open3.capture2("ps", "aux")
|
|
75
|
+
if status.success?
|
|
76
|
+
output.each_line do |line|
|
|
77
|
+
next unless line.include?("ruby") && line.include?("receiver.rb") && !line.include?("grep")
|
|
78
|
+
|
|
79
|
+
# Extract PID (second column in ps aux output)
|
|
80
|
+
parts = line.split
|
|
81
|
+
pid = parts[1].to_i
|
|
82
|
+
return pid if pid.positive? && process_running?(pid)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def process_running?(pid)
|
|
90
|
+
Process.kill(0, pid)
|
|
91
|
+
true
|
|
92
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def fetch_active_sessions
|
|
97
|
+
uri = URI("http://localhost:4567/api/status")
|
|
98
|
+
response = Net::HTTP.get_response(uri)
|
|
99
|
+
return [] unless response.is_a?(Net::HTTPSuccess)
|
|
100
|
+
|
|
101
|
+
data = JSON.parse(response.body)
|
|
102
|
+
(data["sessions"] || data).select { |s| s["alive"] }
|
|
103
|
+
rescue StandardError
|
|
104
|
+
[]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def wait_for_agents_to_finish
|
|
108
|
+
sessions = fetch_active_sessions
|
|
109
|
+
return true if sessions.empty?
|
|
110
|
+
|
|
111
|
+
agents = sessions.map { |s| s["agent"] || "Unknown" }
|
|
112
|
+
puts "⏳ Waiting for #{sessions.size} active agent(s) to finish: #{agents.join(", ")}"
|
|
113
|
+
puts " (Ctrl+C to restart immediately)"
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
loop do
|
|
117
|
+
sleep 5
|
|
118
|
+
sessions = fetch_active_sessions
|
|
119
|
+
if sessions.empty?
|
|
120
|
+
puts "✓ All agents finished."
|
|
121
|
+
return true
|
|
122
|
+
end
|
|
123
|
+
agents = sessions.map { |s| s["agent"] || "Unknown" }
|
|
124
|
+
elapsed = sessions.map { |s| s["elapsed_seconds"] || 0 }.max
|
|
125
|
+
puts " Still waiting... #{sessions.size} agent(s) active: #{agents.join(", ")} (#{elapsed}s)"
|
|
126
|
+
end
|
|
127
|
+
rescue Interrupt
|
|
128
|
+
puts "\n⚡ Skipping wait, restarting now..."
|
|
129
|
+
true
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def stop_server
|
|
134
|
+
pid = find_server_pid
|
|
135
|
+
|
|
136
|
+
unless pid
|
|
137
|
+
puts "No running Brainiac server found."
|
|
138
|
+
return false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
puts "Stopping Brainiac server (PID: #{pid})..."
|
|
142
|
+
|
|
143
|
+
# Also stop the monitor daemon
|
|
144
|
+
daemon_pid_file = "/tmp/brainiac-daemon.pid"
|
|
145
|
+
if File.exist?(daemon_pid_file)
|
|
146
|
+
daemon_pid = File.read(daemon_pid_file).strip.to_i
|
|
147
|
+
begin
|
|
148
|
+
Process.kill("TERM", daemon_pid)
|
|
149
|
+
puts "✓ Monitor daemon stopped (PID: #{daemon_pid})."
|
|
150
|
+
rescue Errno::ESRCH
|
|
151
|
+
# Already dead
|
|
152
|
+
end
|
|
153
|
+
File.delete(daemon_pid_file)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
begin
|
|
157
|
+
Process.kill("TERM", pid)
|
|
158
|
+
|
|
159
|
+
# Wait up to 5 seconds for graceful shutdown
|
|
160
|
+
5.times do
|
|
161
|
+
sleep 2
|
|
162
|
+
next if process_running?(pid)
|
|
163
|
+
|
|
164
|
+
puts "✓ Server stopped."
|
|
165
|
+
FileUtils.rm_f(PID_FILE)
|
|
166
|
+
return true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Force kill if still running
|
|
170
|
+
puts "Server didn't stop gracefully, forcing..."
|
|
171
|
+
Process.kill("KILL", pid)
|
|
172
|
+
sleep 1
|
|
173
|
+
|
|
174
|
+
FileUtils.rm_f(PID_FILE)
|
|
175
|
+
puts "✓ Server stopped (forced)."
|
|
176
|
+
true
|
|
177
|
+
rescue Errno::ESRCH
|
|
178
|
+
puts "✓ Server already stopped."
|
|
179
|
+
FileUtils.rm_f(PID_FILE)
|
|
180
|
+
true
|
|
181
|
+
rescue Errno::EPERM
|
|
182
|
+
puts "Error: Permission denied. Try running with sudo or check process ownership."
|
|
183
|
+
false
|
|
184
|
+
rescue StandardError => e
|
|
185
|
+
puts "Error stopping server: #{e.message}"
|
|
186
|
+
false
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def start_server(daemon: false)
|
|
191
|
+
# Resolve the real path of the brainiac script (follows symlinks)
|
|
192
|
+
receiver_path = File.join(BRAINIAC_ROOT, "receiver.rb")
|
|
193
|
+
receiver_dir = BRAINIAC_ROOT
|
|
194
|
+
|
|
195
|
+
unless File.exist?(receiver_path)
|
|
196
|
+
puts "Error: receiver.rb not found at #{receiver_path}"
|
|
197
|
+
exit 1
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Check if already running
|
|
201
|
+
if find_server_pid
|
|
202
|
+
puts "Error: Brainiac server is already running."
|
|
203
|
+
puts "Run 'brainiac stop' or 'brainiac restart' to manage it."
|
|
204
|
+
exit 1
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if daemon
|
|
208
|
+
# Daemon mode: start in background (like the old behavior)
|
|
209
|
+
log_dir = File.join(receiver_dir, "tmp")
|
|
210
|
+
FileUtils.mkdir_p(log_dir)
|
|
211
|
+
log_file = File.join(log_dir, "brainiac-server.log")
|
|
212
|
+
|
|
213
|
+
puts "Starting Brainiac server (daemon)..."
|
|
214
|
+
|
|
215
|
+
pid = spawn("ruby", receiver_path,
|
|
216
|
+
chdir: receiver_dir,
|
|
217
|
+
out: log_file,
|
|
218
|
+
err: %i[child out])
|
|
219
|
+
Process.detach(pid)
|
|
220
|
+
|
|
221
|
+
ensure_brainiac_dir
|
|
222
|
+
File.write(PID_FILE, pid.to_s)
|
|
223
|
+
File.write(File.join(BRAINIAC_DIR, "server.root"), receiver_dir)
|
|
224
|
+
|
|
225
|
+
puts "✓ Server started in background (PID: #{pid})"
|
|
226
|
+
puts " Logs: tail -f #{log_file}"
|
|
227
|
+
puts " Stop: brainiac stop"
|
|
228
|
+
puts " Restart: brainiac restart"
|
|
229
|
+
else
|
|
230
|
+
# Foreground mode (default): run in the current process like `rails server`
|
|
231
|
+
puts "Starting Brainiac server (pid: #{Process.pid})..."
|
|
232
|
+
puts " Stop: Ctrl+C"
|
|
233
|
+
puts ""
|
|
234
|
+
|
|
235
|
+
ensure_brainiac_dir
|
|
236
|
+
File.write(PID_FILE, Process.pid.to_s)
|
|
237
|
+
File.write(File.join(BRAINIAC_DIR, "server.root"), receiver_dir)
|
|
238
|
+
|
|
239
|
+
begin
|
|
240
|
+
Dir.chdir(receiver_dir) do
|
|
241
|
+
exec("ruby", receiver_path)
|
|
242
|
+
end
|
|
243
|
+
rescue Interrupt
|
|
244
|
+
puts "\nStopping server..."
|
|
245
|
+
ensure
|
|
246
|
+
FileUtils.rm_f(PID_FILE)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def detect_project_info(path)
|
|
252
|
+
path = File.expand_path(path)
|
|
253
|
+
|
|
254
|
+
# Get git remote URL to extract repo name
|
|
255
|
+
github_repo = nil
|
|
256
|
+
if Dir.exist?(File.join(path, ".git"))
|
|
257
|
+
remote_url, _, status = Open3.capture3("git", "remote", "get-url", "origin", chdir: path)
|
|
258
|
+
if status.success?
|
|
259
|
+
# Parse GitHub repo from URL (supports both HTTPS and SSH)
|
|
260
|
+
if remote_url =~ %r{github\.com[/:](.+?)\.git}
|
|
261
|
+
github_repo = Regexp.last_match(1)
|
|
262
|
+
elsif remote_url =~ %r{github\.com[/:](.+)}
|
|
263
|
+
github_repo = Regexp.last_match(1).chomp
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Suggest project key from directory name
|
|
269
|
+
project_key = File.basename(path).downcase.gsub(/[^a-z0-9-]/, "-")
|
|
270
|
+
|
|
271
|
+
{
|
|
272
|
+
"repo_path" => path,
|
|
273
|
+
"github_repo" => github_repo,
|
|
274
|
+
"project_key" => project_key
|
|
275
|
+
}
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def register_project(options)
|
|
279
|
+
path = options[:path] || Dir.pwd
|
|
280
|
+
info = detect_project_info(path)
|
|
281
|
+
|
|
282
|
+
projects = load_projects
|
|
283
|
+
|
|
284
|
+
# Interactive prompts
|
|
285
|
+
puts "Registering project from: #{info["repo_path"]}"
|
|
286
|
+
puts
|
|
287
|
+
|
|
288
|
+
print "Project key [#{info["project_key"]}]: "
|
|
289
|
+
project_key = $stdin.gets.chomp
|
|
290
|
+
project_key = info["project_key"] if project_key.empty?
|
|
291
|
+
|
|
292
|
+
if projects.key?(project_key) && !options[:force]
|
|
293
|
+
puts "Error: Project '#{project_key}' already exists. Use --force to overwrite."
|
|
294
|
+
exit 1
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
print "Fizzy tags (comma-separated) [#{project_key}]: "
|
|
298
|
+
tags_input = $stdin.gets.chomp
|
|
299
|
+
fizzy_tags = tags_input.empty? ? [project_key] : tags_input.split(",").map(&:strip)
|
|
300
|
+
|
|
301
|
+
print "GitHub repo [#{info["github_repo"]}]: "
|
|
302
|
+
github_repo = $stdin.gets.chomp
|
|
303
|
+
github_repo = info["github_repo"] if github_repo.empty?
|
|
304
|
+
|
|
305
|
+
print "CLI provider [kiro]: "
|
|
306
|
+
cli_provider = $stdin.gets.chomp
|
|
307
|
+
cli_provider = "kiro" if cli_provider.empty?
|
|
308
|
+
|
|
309
|
+
print "Default model [auto]: "
|
|
310
|
+
agent_model = $stdin.gets.chomp
|
|
311
|
+
agent_model = "auto" if agent_model.empty?
|
|
312
|
+
|
|
313
|
+
default_agent_name = ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
314
|
+
print "Agent name [#{default_agent_name}]: "
|
|
315
|
+
agent_name = $stdin.gets.chomp
|
|
316
|
+
agent_name = default_agent_name if agent_name.empty?
|
|
317
|
+
|
|
318
|
+
# Build project config
|
|
319
|
+
project_config = {
|
|
320
|
+
"repo_path" => info["repo_path"],
|
|
321
|
+
"fizzy_tags" => fizzy_tags,
|
|
322
|
+
"github_repo" => github_repo,
|
|
323
|
+
"agent_name" => agent_name,
|
|
324
|
+
"agent_model" => agent_model,
|
|
325
|
+
"cli_provider" => cli_provider
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
projects[project_key] = project_config
|
|
329
|
+
save_projects(projects)
|
|
330
|
+
|
|
331
|
+
puts
|
|
332
|
+
puts "✓ Project '#{project_key}' registered successfully!"
|
|
333
|
+
puts " Repo: #{info["repo_path"]}"
|
|
334
|
+
puts " Tags: #{fizzy_tags.join(", ")}"
|
|
335
|
+
puts " GitHub: #{github_repo}" if github_repo
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def unregister_project(project_key)
|
|
339
|
+
projects = load_projects
|
|
340
|
+
|
|
341
|
+
unless projects.key?(project_key)
|
|
342
|
+
puts "Error: Project '#{project_key}' not found."
|
|
343
|
+
exit 1
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
projects.delete(project_key)
|
|
347
|
+
save_projects(projects)
|
|
348
|
+
|
|
349
|
+
puts "✓ Project '#{project_key}' unregistered."
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def list_projects
|
|
353
|
+
projects = load_projects
|
|
354
|
+
|
|
355
|
+
if projects.empty?
|
|
356
|
+
puts "No projects registered."
|
|
357
|
+
puts "Run 'brainiac register' from a project directory to get started."
|
|
358
|
+
return
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
puts "Registered projects:"
|
|
362
|
+
puts
|
|
363
|
+
|
|
364
|
+
projects.each do |key, config|
|
|
365
|
+
default_marker = config["default"] ? " (default)" : ""
|
|
366
|
+
puts " #{key}#{default_marker}"
|
|
367
|
+
puts " Path: #{config["repo_path"]}"
|
|
368
|
+
puts " Tags: #{config["fizzy_tags"].join(", ")}"
|
|
369
|
+
puts " GitHub: #{config["github_repo"]}" if config["github_repo"]
|
|
370
|
+
puts " Agent: #{config["agent_cli"]} (model: #{config["agent_model"]})"
|
|
371
|
+
puts
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def show_project(project_key)
|
|
376
|
+
projects = load_projects
|
|
377
|
+
|
|
378
|
+
unless projects.key?(project_key)
|
|
379
|
+
puts "Error: Project '#{project_key}' not found."
|
|
380
|
+
exit 1
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
config = projects[project_key]
|
|
384
|
+
puts JSON.pretty_generate(config)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def configure_server(options)
|
|
388
|
+
config = load_config
|
|
389
|
+
|
|
390
|
+
config["server_url"] = options[:server_url] if options[:server_url]
|
|
391
|
+
|
|
392
|
+
if options[:show]
|
|
393
|
+
puts JSON.pretty_generate(config)
|
|
394
|
+
return
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Interactive configuration
|
|
398
|
+
current_url = config["server_url"] || "http://localhost:4567"
|
|
399
|
+
print "Brainiac server URL [#{current_url}]: "
|
|
400
|
+
server_url = $stdin.gets.chomp
|
|
401
|
+
config["server_url"] = server_url.empty? ? current_url : server_url
|
|
402
|
+
|
|
403
|
+
save_config(config)
|
|
404
|
+
puts "✓ Configuration saved."
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# --- Brain management ---
|
|
408
|
+
|
|
409
|
+
BRAIN_BASE_DIR = File.join(BRAINIAC_DIR, "brain")
|
|
410
|
+
KNOWLEDGE_DIR = File.join(BRAIN_BASE_DIR, "knowledge")
|
|
411
|
+
PERSONA_BASE_DIR = File.join(BRAIN_BASE_DIR, "persona")
|
|
412
|
+
MEMORY_BASE_DIR = File.join(BRAIN_BASE_DIR, "memory")
|
|
413
|
+
KNOWLEDGE_COLLECTION = "brainiac-knowledge".freeze
|
|
414
|
+
|
|
415
|
+
def memory_dir_for(agent_name)
|
|
416
|
+
File.join(MEMORY_BASE_DIR, agent_name.downcase.gsub(/[^a-z0-9-]/, "-"))
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def persona_dir_for(agent_name)
|
|
420
|
+
File.join(PERSONA_BASE_DIR, agent_name.downcase.gsub(/[^a-z0-9-]/, "-"))
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def persona_collection_for(agent_name)
|
|
424
|
+
"#{agent_name.downcase.gsub(/[^a-z0-9-]/, "-")}-persona"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def brain_init(agent_name)
|
|
428
|
+
persona_dir = persona_dir_for(agent_name)
|
|
429
|
+
persona_col = persona_collection_for(agent_name)
|
|
430
|
+
|
|
431
|
+
FileUtils.mkdir_p(KNOWLEDGE_DIR)
|
|
432
|
+
FileUtils.mkdir_p(persona_dir)
|
|
433
|
+
FileUtils.mkdir_p(File.join(BRAIN_BASE_DIR, "memory"))
|
|
434
|
+
|
|
435
|
+
# Check if qmd is available
|
|
436
|
+
unless system("which qmd > /dev/null 2>&1")
|
|
437
|
+
puts "✓ Brain directories created:"
|
|
438
|
+
puts " Knowledge: #{KNOWLEDGE_DIR}"
|
|
439
|
+
puts " Persona: #{persona_dir}"
|
|
440
|
+
puts ""
|
|
441
|
+
puts "⚠ qmd is not installed. Install it to enable semantic search:"
|
|
442
|
+
puts " npm install -g @tobilu/qmd"
|
|
443
|
+
puts ""
|
|
444
|
+
puts "Then run: brainiac brain init #{agent_name}"
|
|
445
|
+
return
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Set up shared knowledge collection (idempotent — qmd won't duplicate)
|
|
449
|
+
puts "Setting up shared knowledge collection..."
|
|
450
|
+
system("qmd", "collection", "add", KNOWLEDGE_DIR, "--name", KNOWLEDGE_COLLECTION)
|
|
451
|
+
system("qmd", "context", "add", "qmd://#{KNOWLEDGE_COLLECTION}",
|
|
452
|
+
"Shared technical knowledge: project conventions, coding patterns, architecture decisions, lessons learned")
|
|
453
|
+
|
|
454
|
+
# Set up per-agent persona collection
|
|
455
|
+
puts "Setting up persona collection for #{agent_name}..."
|
|
456
|
+
system("qmd", "collection", "add", persona_dir, "--name", persona_col)
|
|
457
|
+
system("qmd", "context", "add", "qmd://#{persona_col}",
|
|
458
|
+
"Communication style and personality for AI agent #{agent_name}")
|
|
459
|
+
|
|
460
|
+
# Set up card memory collection (per-agent)
|
|
461
|
+
memory_dir = memory_dir_for(agent_name)
|
|
462
|
+
FileUtils.mkdir_p(memory_dir)
|
|
463
|
+
puts "Setting up card memory collection for #{agent_name}..."
|
|
464
|
+
system("qmd", "collection", "add", memory_dir, "--name", "#{agent_name.downcase}-memory")
|
|
465
|
+
system("qmd", "context", "add", "qmd://#{agent_name.downcase}-memory",
|
|
466
|
+
"Per-card session memory for #{agent_name}: decisions, questions, answers, work status, timelines")
|
|
467
|
+
|
|
468
|
+
puts "Indexing..."
|
|
469
|
+
system("qmd", "update")
|
|
470
|
+
|
|
471
|
+
puts ""
|
|
472
|
+
puts "✓ Brain initialized for #{agent_name}"
|
|
473
|
+
puts " Knowledge (shared): #{KNOWLEDGE_DIR}"
|
|
474
|
+
puts " Memory (#{agent_name}): #{memory_dir}"
|
|
475
|
+
puts " Persona (#{agent_name}): #{persona_dir}"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def brain_status(agent_name)
|
|
479
|
+
persona_dir = persona_dir_for(agent_name)
|
|
480
|
+
persona_col = persona_collection_for(agent_name)
|
|
481
|
+
memory_dir = memory_dir_for(agent_name)
|
|
482
|
+
|
|
483
|
+
knowledge_files = File.directory?(KNOWLEDGE_DIR) ? Dir.glob(File.join(KNOWLEDGE_DIR, "**", "*.md")) : []
|
|
484
|
+
memory_files = File.directory?(memory_dir) ? Dir.glob(File.join(memory_dir, "**", "*.md")) : []
|
|
485
|
+
persona_files = File.directory?(persona_dir) ? Dir.glob(File.join(persona_dir, "**", "*.md")) : []
|
|
486
|
+
|
|
487
|
+
if knowledge_files.empty? && memory_files.empty? && persona_files.empty?
|
|
488
|
+
puts "No brain found. Run 'brainiac brain init' to create one."
|
|
489
|
+
return
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
puts "Knowledge (shared across all agents):"
|
|
493
|
+
puts " Directory: #{KNOWLEDGE_DIR}"
|
|
494
|
+
puts " Collection: #{KNOWLEDGE_COLLECTION}"
|
|
495
|
+
puts " Files: #{knowledge_files.size}"
|
|
496
|
+
knowledge_files.each do |f|
|
|
497
|
+
puts " #{f.sub("#{KNOWLEDGE_DIR}/", "")} (#{File.size(f)} bytes)"
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
puts ""
|
|
501
|
+
puts "Memory (#{agent_name}, per-card session history):"
|
|
502
|
+
puts " Directory: #{memory_dir}"
|
|
503
|
+
puts " Collection: #{agent_name.downcase}-memory"
|
|
504
|
+
puts " Files: #{memory_files.size}"
|
|
505
|
+
memory_files.each do |f|
|
|
506
|
+
puts " #{f.sub("#{memory_dir}/", "")} (#{File.size(f)} bytes)"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
puts ""
|
|
510
|
+
puts "Persona (#{agent_name}):"
|
|
511
|
+
puts " Directory: #{persona_dir}"
|
|
512
|
+
puts " Collection: #{persona_col}"
|
|
513
|
+
puts " Files: #{persona_files.size}"
|
|
514
|
+
persona_files.each do |f|
|
|
515
|
+
puts " #{f.sub("#{persona_dir}/", "")} (#{File.size(f)} bytes)"
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def brain_search(query, scope: :knowledge, agent_name: nil)
|
|
520
|
+
unless system("which qmd > /dev/null 2>&1")
|
|
521
|
+
puts "Error: qmd is not installed. Run: npm install -g @tobilu/qmd"
|
|
522
|
+
exit 1
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
collection = case scope
|
|
526
|
+
when :persona
|
|
527
|
+
agent_name ||= ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
528
|
+
persona_collection_for(agent_name)
|
|
529
|
+
else
|
|
530
|
+
KNOWLEDGE_COLLECTION
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
exec("qmd", "search", query, "-c", collection)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def brain_list
|
|
537
|
+
puts "Knowledge (shared):"
|
|
538
|
+
if File.directory?(KNOWLEDGE_DIR)
|
|
539
|
+
files = Dir.glob(File.join(KNOWLEDGE_DIR, "**", "*.md"))
|
|
540
|
+
puts " #{KNOWLEDGE_DIR} (#{files.size} files)"
|
|
541
|
+
else
|
|
542
|
+
puts " Not initialized"
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
puts ""
|
|
546
|
+
puts "Memory (per-agent, per-card):"
|
|
547
|
+
if File.directory?(MEMORY_BASE_DIR)
|
|
548
|
+
agent_dirs = Dir.children(MEMORY_BASE_DIR).select { |d| File.directory?(File.join(MEMORY_BASE_DIR, d)) }
|
|
549
|
+
if agent_dirs.empty?
|
|
550
|
+
puts " None"
|
|
551
|
+
else
|
|
552
|
+
agent_dirs.each do |d|
|
|
553
|
+
agent_memory_dir = File.join(MEMORY_BASE_DIR, d)
|
|
554
|
+
files = Dir.glob(File.join(agent_memory_dir, "**", "*.md"))
|
|
555
|
+
puts " #{d}: #{files.size} files"
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
else
|
|
559
|
+
puts " Not initialized"
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
puts ""
|
|
563
|
+
puts "Personas:"
|
|
564
|
+
if File.directory?(PERSONA_BASE_DIR)
|
|
565
|
+
dirs = Dir.children(PERSONA_BASE_DIR).select { |d| File.directory?(File.join(PERSONA_BASE_DIR, d)) }
|
|
566
|
+
if dirs.empty?
|
|
567
|
+
puts " None"
|
|
568
|
+
else
|
|
569
|
+
dirs.each do |d|
|
|
570
|
+
dir = File.join(PERSONA_BASE_DIR, d)
|
|
571
|
+
files = Dir.glob(File.join(dir, "**", "*.md"))
|
|
572
|
+
puts " #{d} (#{files.size} files) — #{dir}"
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
else
|
|
576
|
+
puts " Not initialized"
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
puts ""
|
|
580
|
+
puts "Run 'brainiac brain init <agent-name>' to set up a new agent."
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Main CLI
|
|
584
|
+
options = {}
|
|
585
|
+
subcommand = ARGV.shift
|
|
586
|
+
|
|
587
|
+
case subcommand
|
|
588
|
+
when "setup"
|
|
589
|
+
puts "🔧 Brainiac Setup"
|
|
590
|
+
puts ""
|
|
591
|
+
|
|
592
|
+
# Create directory structure
|
|
593
|
+
dirs = %w[brain/knowledge brain/persona brain/memory handlers plans tmp/discord/draft tmp/discord/posted]
|
|
594
|
+
dirs.each do |dir|
|
|
595
|
+
path = File.join(BRAINIAC_DIR, dir)
|
|
596
|
+
FileUtils.mkdir_p(path)
|
|
597
|
+
end
|
|
598
|
+
puts "✓ Created ~/.brainiac/ directory structure"
|
|
599
|
+
|
|
600
|
+
# Copy example configs
|
|
601
|
+
templates_dir = File.join(BRAINIAC_ROOT, "templates")
|
|
602
|
+
if Dir.exist?(templates_dir)
|
|
603
|
+
copied = 0
|
|
604
|
+
Dir.glob(File.join(templates_dir, "*.example")).each do |template|
|
|
605
|
+
dest = File.join(BRAINIAC_DIR, File.basename(template, ".example"))
|
|
606
|
+
unless File.exist?(dest)
|
|
607
|
+
FileUtils.cp(template, dest)
|
|
608
|
+
copied += 1
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
puts "✓ Copied #{copied} example config(s) to ~/.brainiac/ (existing files preserved)"
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
puts ""
|
|
615
|
+
puts "Next steps:"
|
|
616
|
+
puts " 1. Edit config files in ~/.brainiac/ with your secrets and IDs"
|
|
617
|
+
puts " 2. Create agent configs in ~/.kiro/agents/"
|
|
618
|
+
puts " 3. Register projects: cd ~/Code/myproject && brainiac register"
|
|
619
|
+
puts " 4. Initialize brain: brainiac brain init"
|
|
620
|
+
puts " 5. Start server: brainiac server"
|
|
621
|
+
|
|
622
|
+
when "register", "add"
|
|
623
|
+
OptionParser.new do |opts|
|
|
624
|
+
opts.banner = "Usage: brainiac register [options]"
|
|
625
|
+
opts.on("-p", "--path PATH", "Project path (default: current directory)")
|
|
626
|
+
opts.on("-f", "--force", "Overwrite existing project")
|
|
627
|
+
end.parse!(into: options)
|
|
628
|
+
|
|
629
|
+
register_project(options)
|
|
630
|
+
|
|
631
|
+
when "unregister", "remove", "rm"
|
|
632
|
+
if ARGV.empty?
|
|
633
|
+
puts "Usage: brainiac unregister <project-key>"
|
|
634
|
+
exit 1
|
|
635
|
+
end
|
|
636
|
+
unregister_project(ARGV[0])
|
|
637
|
+
|
|
638
|
+
when "list", "ls"
|
|
639
|
+
list_projects
|
|
640
|
+
|
|
641
|
+
when "projects"
|
|
642
|
+
# Support "brainiac projects list" as an alias for "brainiac list"
|
|
643
|
+
projects_cmd = ARGV.shift
|
|
644
|
+
case projects_cmd
|
|
645
|
+
when "list", "ls", nil
|
|
646
|
+
list_projects
|
|
647
|
+
when "show"
|
|
648
|
+
if ARGV.empty?
|
|
649
|
+
puts "Usage: brainiac projects show <project-key>"
|
|
650
|
+
exit 1
|
|
651
|
+
end
|
|
652
|
+
show_project(ARGV[0])
|
|
653
|
+
when "default"
|
|
654
|
+
project_key = ARGV[0]
|
|
655
|
+
unless project_key
|
|
656
|
+
puts "Usage: brainiac projects default <project-key>"
|
|
657
|
+
puts " Sets the default project for project-specific setup (e.g., .env copying)"
|
|
658
|
+
exit 1
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
projects = load_projects
|
|
662
|
+
unless projects.key?(project_key)
|
|
663
|
+
puts "Error: Project '#{project_key}' not found."
|
|
664
|
+
puts "Available projects: #{projects.keys.join(", ")}"
|
|
665
|
+
exit 1
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# Remove default flag from all projects, then set it on the target
|
|
669
|
+
projects.each_value { |config| config.delete("default") }
|
|
670
|
+
projects[project_key]["default"] = true
|
|
671
|
+
save_projects(projects)
|
|
672
|
+
|
|
673
|
+
puts "✓ Set '#{project_key}' as the default project"
|
|
674
|
+
else
|
|
675
|
+
puts "Unknown projects command: #{projects_cmd}"
|
|
676
|
+
puts "Available: list, show, default"
|
|
677
|
+
exit 1
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
when "show"
|
|
681
|
+
if ARGV.empty?
|
|
682
|
+
puts "Usage: brainiac show <project-key>"
|
|
683
|
+
exit 1
|
|
684
|
+
end
|
|
685
|
+
show_project(ARGV[0])
|
|
686
|
+
|
|
687
|
+
when "config"
|
|
688
|
+
OptionParser.new do |opts|
|
|
689
|
+
opts.banner = "Usage: brainiac config [options]"
|
|
690
|
+
opts.on("-s", "--server-url URL", "Brainiac server URL")
|
|
691
|
+
opts.on("--show", "Show current configuration")
|
|
692
|
+
end.parse!(into: options)
|
|
693
|
+
|
|
694
|
+
configure_server(options)
|
|
695
|
+
|
|
696
|
+
when "server", "s"
|
|
697
|
+
OptionParser.new do |opts|
|
|
698
|
+
opts.banner = "Usage: brainiac server [options]"
|
|
699
|
+
opts.on("-d", "--daemon", "Run server in background (detached)")
|
|
700
|
+
end.parse!(into: options)
|
|
701
|
+
|
|
702
|
+
start_server(daemon: options[:daemon])
|
|
703
|
+
|
|
704
|
+
when "stop"
|
|
705
|
+
stop_server
|
|
706
|
+
|
|
707
|
+
when "restart"
|
|
708
|
+
puts "Restarting Brainiac server..."
|
|
709
|
+
wait_for_agents_to_finish if find_server_pid
|
|
710
|
+
if stop_server
|
|
711
|
+
# Wait a bit longer to ensure the process is fully dead
|
|
712
|
+
sleep 2
|
|
713
|
+
|
|
714
|
+
# Double-check that the process is really gone
|
|
715
|
+
if find_server_pid
|
|
716
|
+
puts "Warning: Old server process still running, forcing kill..."
|
|
717
|
+
pid = find_server_pid
|
|
718
|
+
begin
|
|
719
|
+
Process.kill("KILL", pid)
|
|
720
|
+
sleep 1
|
|
721
|
+
rescue Errno::ESRCH
|
|
722
|
+
# Already dead
|
|
723
|
+
end
|
|
724
|
+
FileUtils.rm_f(PID_FILE)
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
start_server(daemon: true)
|
|
729
|
+
|
|
730
|
+
when "logs", "log"
|
|
731
|
+
# Tail the server log
|
|
732
|
+
log_file = File.join(BRAINIAC_ROOT, "tmp", "brainiac-server.log")
|
|
733
|
+
|
|
734
|
+
unless File.exist?(log_file)
|
|
735
|
+
puts "No log file found at #{log_file}"
|
|
736
|
+
puts "Is the server running? Check with: brainiac status"
|
|
737
|
+
exit 1
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
exec("tail", "-f", log_file)
|
|
741
|
+
|
|
742
|
+
when "status"
|
|
743
|
+
pid = find_server_pid
|
|
744
|
+
if pid
|
|
745
|
+
puts "Brainiac server is running (PID: #{pid})"
|
|
746
|
+
else
|
|
747
|
+
puts "Brainiac server is not running"
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
when "path"
|
|
751
|
+
puts BRAINIAC_DIR
|
|
752
|
+
|
|
753
|
+
when "discord"
|
|
754
|
+
discord_cmd = ARGV.shift
|
|
755
|
+
discord_config_file = File.join(BRAINIAC_DIR, "discord.json")
|
|
756
|
+
agent_registry_file = File.join(BRAINIAC_DIR, "agents.json")
|
|
757
|
+
|
|
758
|
+
case discord_cmd
|
|
759
|
+
when "config"
|
|
760
|
+
if File.exist?(discord_config_file)
|
|
761
|
+
puts File.read(discord_config_file)
|
|
762
|
+
else
|
|
763
|
+
puts "No Discord config found at #{discord_config_file}"
|
|
764
|
+
puts "Create one with: brainiac discord map <channel-id> <project>"
|
|
765
|
+
puts "Or copy discord.json.example to #{discord_config_file}"
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
when "map"
|
|
769
|
+
channel_id = ARGV[0]
|
|
770
|
+
project_key = ARGV[1]
|
|
771
|
+
|
|
772
|
+
unless channel_id && project_key
|
|
773
|
+
puts "Usage: brainiac discord map <channel-id> <project-key>"
|
|
774
|
+
exit 1
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
config = File.exist?(discord_config_file) ? JSON.parse(File.read(discord_config_file)) : { "channel_mappings" => {} }
|
|
778
|
+
config["channel_mappings"] ||= {}
|
|
779
|
+
config["channel_mappings"][channel_id] = { "project" => project_key }
|
|
780
|
+
|
|
781
|
+
ensure_brainiac_dir
|
|
782
|
+
File.write(discord_config_file, JSON.pretty_generate(config))
|
|
783
|
+
puts "✓ Mapped channel #{channel_id} → project '#{project_key}'"
|
|
784
|
+
|
|
785
|
+
when "default"
|
|
786
|
+
project_key = ARGV[0]
|
|
787
|
+
|
|
788
|
+
unless project_key
|
|
789
|
+
puts "Usage: brainiac discord default <project-key>"
|
|
790
|
+
exit 1
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
config = File.exist?(discord_config_file) ? JSON.parse(File.read(discord_config_file)) : { "channel_mappings" => {} }
|
|
794
|
+
config["default_project"] = project_key
|
|
795
|
+
|
|
796
|
+
ensure_brainiac_dir
|
|
797
|
+
File.write(discord_config_file, JSON.pretty_generate(config))
|
|
798
|
+
puts "✓ Default project: #{project_key}"
|
|
799
|
+
|
|
800
|
+
when "token"
|
|
801
|
+
agent_key = ARGV[0]
|
|
802
|
+
token = ARGV[1]
|
|
803
|
+
|
|
804
|
+
unless agent_key && token
|
|
805
|
+
puts "Usage: brainiac discord token <agent-key> <bot-token>"
|
|
806
|
+
puts " Sets the DISCORD_BOT_TOKEN env var for an agent in the registry."
|
|
807
|
+
puts " Example: brainiac discord token galen Bot_TOKEN_HERE"
|
|
808
|
+
exit 1
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
812
|
+
registry[agent_key] ||= {}
|
|
813
|
+
registry[agent_key]["env"] ||= {}
|
|
814
|
+
registry[agent_key]["env"]["DISCORD_BOT_TOKEN"] = token
|
|
815
|
+
|
|
816
|
+
ensure_brainiac_dir
|
|
817
|
+
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
818
|
+
puts "✓ Set DISCORD_BOT_TOKEN for '#{agent_key}'"
|
|
819
|
+
|
|
820
|
+
when "status"
|
|
821
|
+
config = load_config
|
|
822
|
+
server_url = config["server_url"] || "http://localhost:4567"
|
|
823
|
+
begin
|
|
824
|
+
uri = URI("#{server_url}/api/discord")
|
|
825
|
+
response = Net::HTTP.get_response(uri)
|
|
826
|
+
data = JSON.parse(response.body)
|
|
827
|
+
if data["enabled"]
|
|
828
|
+
bots = data["bots"] || {}
|
|
829
|
+
if bots.empty?
|
|
830
|
+
puts "Discord: enabled but no bots configured"
|
|
831
|
+
else
|
|
832
|
+
puts "Discord bots:"
|
|
833
|
+
bots.each do |agent, info|
|
|
834
|
+
puts " #{agent}: #{info["status"]} (user_id: #{info["user_id"] || "n/a"})"
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
puts "Default project: #{data.dig("config", "default_project") || "none"}"
|
|
838
|
+
puts "Channel mappings: #{data.dig("config", "channel_mappings")}"
|
|
839
|
+
else
|
|
840
|
+
puts "Discord: disabled (#{data["reason"]})"
|
|
841
|
+
end
|
|
842
|
+
rescue StandardError => e
|
|
843
|
+
puts "Could not reach server at #{server_url}: #{e.message}"
|
|
844
|
+
puts "Is the server running? Check with: brainiac status"
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
when "agents"
|
|
848
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
849
|
+
agents_with_tokens = registry.select { |_k, v| v.is_a?(Hash) && (v.dig("env", "DISCORD_BOT_TOKEN") || v["discord_bot_token"]) }
|
|
850
|
+
if agents_with_tokens.empty?
|
|
851
|
+
puts "No agents have Discord bot tokens configured."
|
|
852
|
+
puts "Add one with: brainiac discord token <agent-key> <bot-token>"
|
|
853
|
+
else
|
|
854
|
+
puts "Agents with Discord bots:"
|
|
855
|
+
agents_with_tokens.each do |key, entry|
|
|
856
|
+
display = entry["fizzy_name"] || key.capitalize
|
|
857
|
+
token = entry.dig("env", "DISCORD_BOT_TOKEN") || entry["discord_bot_token"]
|
|
858
|
+
token_preview = "#{token[0..10]}..."
|
|
859
|
+
puts " #{display} (#{key}): #{token_preview}"
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
when "owner"
|
|
864
|
+
discord_id = ARGV[0]
|
|
865
|
+
config = File.exist?(discord_config_file) ? JSON.parse(File.read(discord_config_file)) : {}
|
|
866
|
+
if discord_id
|
|
867
|
+
config["owner_discord_id"] = discord_id
|
|
868
|
+
ensure_brainiac_dir
|
|
869
|
+
File.write(discord_config_file, JSON.pretty_generate(config))
|
|
870
|
+
puts "✓ Owner set to #{discord_id}"
|
|
871
|
+
elsif config["owner_discord_id"]
|
|
872
|
+
puts "Owner: #{config["owner_discord_id"]}"
|
|
873
|
+
else
|
|
874
|
+
puts "No owner set."
|
|
875
|
+
puts "Usage: brainiac discord owner <discord-user-id>"
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
else
|
|
879
|
+
puts <<~HELP
|
|
880
|
+
Usage: brainiac discord <command>
|
|
881
|
+
|
|
882
|
+
Commands:
|
|
883
|
+
config Show Discord config
|
|
884
|
+
default <project> Set default project for all channels
|
|
885
|
+
map <channel-id> <project> Map a specific channel to a project
|
|
886
|
+
owner [<discord-user-id>] Set/show machine owner (for version notifications)
|
|
887
|
+
token <agent-key> <bot-token> Set Discord bot token for an agent
|
|
888
|
+
agents List agents with Discord bot tokens
|
|
889
|
+
status Check Discord bot status (via server API)
|
|
890
|
+
|
|
891
|
+
Each agent gets its own Discord bot. Users @mention @Galen or @GLaDOS
|
|
892
|
+
directly in Discord — no shared bot needed.
|
|
893
|
+
|
|
894
|
+
Setup:
|
|
895
|
+
1. Create a Discord bot per agent at https://discord.com/developers/applications
|
|
896
|
+
2. Enable MESSAGE CONTENT intent in each bot's settings
|
|
897
|
+
3. Invite each bot with permissions: Send Messages, Create Public Threads,
|
|
898
|
+
Send Messages in Threads, Add Reactions, Read Message History
|
|
899
|
+
4. Register each bot token:
|
|
900
|
+
brainiac discord token galen "BOT_TOKEN_FOR_GALEN"
|
|
901
|
+
brainiac discord token glados "BOT_TOKEN_FOR_GLADOS"
|
|
902
|
+
5. Set a default project:
|
|
903
|
+
brainiac discord default marketplace
|
|
904
|
+
6. Start the server (all bots connect automatically):
|
|
905
|
+
brainiac server
|
|
906
|
+
|
|
907
|
+
Config file: #{discord_config_file}
|
|
908
|
+
Agent registry: #{agent_registry_file}
|
|
909
|
+
HELP
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
when "card-map"
|
|
913
|
+
card_map_file = File.join(BRAINIAC_DIR, "card_map.json")
|
|
914
|
+
cm_cmd = ARGV.shift
|
|
915
|
+
case cm_cmd
|
|
916
|
+
when "clean"
|
|
917
|
+
unless File.exist?(card_map_file)
|
|
918
|
+
puts "No card_map.json found."
|
|
919
|
+
exit 0
|
|
920
|
+
end
|
|
921
|
+
map = JSON.parse(File.read(card_map_file))
|
|
922
|
+
before = map.size
|
|
923
|
+
# Remove entries with no valid worktree on disk
|
|
924
|
+
map.reject! { |_id, info| info["worktree"].nil? || !File.directory?(info["worktree"].to_s) }
|
|
925
|
+
after = map.size
|
|
926
|
+
removed = before - after
|
|
927
|
+
File.write(card_map_file, JSON.pretty_generate(map))
|
|
928
|
+
puts "Cleaned card map: removed #{removed} stale entries (#{before} → #{after})"
|
|
929
|
+
when "list", "ls", nil
|
|
930
|
+
unless File.exist?(card_map_file)
|
|
931
|
+
puts "No card_map.json found."
|
|
932
|
+
exit 0
|
|
933
|
+
end
|
|
934
|
+
map = JSON.parse(File.read(card_map_file))
|
|
935
|
+
if map.empty?
|
|
936
|
+
puts "Card map is empty."
|
|
937
|
+
else
|
|
938
|
+
map.each_value do |info|
|
|
939
|
+
num = info["number"] ? "##{info["number"]}" : "(no number)"
|
|
940
|
+
agent = info["agent"] || "(no agent)"
|
|
941
|
+
worktree_exists = info["worktree"] && File.directory?(info["worktree"].to_s) ? "✓" : "✗"
|
|
942
|
+
puts "#{num.ljust(8)} #{agent.ljust(18)} #{worktree_exists} #{info["worktree"] || "(none)"}"
|
|
943
|
+
end
|
|
944
|
+
end
|
|
945
|
+
else
|
|
946
|
+
puts "Usage: brainiac card-map [clean|list]"
|
|
947
|
+
puts " clean Remove stale entries (missing worktrees)"
|
|
948
|
+
puts " list Show all card map entries"
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
when "brain"
|
|
952
|
+
brain_cmd = ARGV.shift
|
|
953
|
+
case brain_cmd
|
|
954
|
+
when "init"
|
|
955
|
+
agent = ARGV[0] || ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
956
|
+
brain_init(agent)
|
|
957
|
+
when "status", "show"
|
|
958
|
+
agent = ARGV[0] || ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
959
|
+
brain_status(agent)
|
|
960
|
+
when "search", "query"
|
|
961
|
+
scope = :knowledge
|
|
962
|
+
agent_name = ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
963
|
+
if ARGV.include?("--persona")
|
|
964
|
+
ARGV.delete("--persona")
|
|
965
|
+
scope = :persona
|
|
966
|
+
end
|
|
967
|
+
if ARGV.include?("--agent")
|
|
968
|
+
idx = ARGV.index("--agent")
|
|
969
|
+
agent_name = ARGV.delete_at(idx + 1)
|
|
970
|
+
ARGV.delete_at(idx)
|
|
971
|
+
end
|
|
972
|
+
if ARGV.empty?
|
|
973
|
+
puts "Usage: brainiac brain search [--persona] [--agent <name>] <query>"
|
|
974
|
+
exit 1
|
|
975
|
+
end
|
|
976
|
+
brain_search(ARGV.join(" "), scope: scope, agent_name: agent_name)
|
|
977
|
+
when "list", "ls"
|
|
978
|
+
brain_list
|
|
979
|
+
when "path"
|
|
980
|
+
what = ARGV[0]
|
|
981
|
+
if what == "persona"
|
|
982
|
+
agent = ARGV[1] || ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
983
|
+
puts persona_dir_for(agent)
|
|
984
|
+
else
|
|
985
|
+
puts KNOWLEDGE_DIR
|
|
986
|
+
end
|
|
987
|
+
else
|
|
988
|
+
puts <<~HELP
|
|
989
|
+
Usage: brainiac brain <command> [options]
|
|
990
|
+
|
|
991
|
+
Commands:
|
|
992
|
+
init [agent-name] Initialize brain (shared knowledge + agent persona)
|
|
993
|
+
status [agent-name] Show brain status and files
|
|
994
|
+
search [--persona] [--agent name] <q> Search knowledge (default) or persona
|
|
995
|
+
list List knowledge and all personas
|
|
996
|
+
path Show knowledge directory
|
|
997
|
+
path persona [agent-name] Show persona directory
|
|
998
|
+
|
|
999
|
+
The brain has two parts:
|
|
1000
|
+
knowledge/ — shared across all agents (project conventions, patterns, lessons)
|
|
1001
|
+
persona/ — per-agent (communication style, tone, personality)
|
|
1002
|
+
|
|
1003
|
+
Knowledge is automatically queried when agents do work.
|
|
1004
|
+
Persona is only queried when agents write comments.
|
|
1005
|
+
|
|
1006
|
+
Examples:
|
|
1007
|
+
brainiac brain init # Init for default agent
|
|
1008
|
+
brainiac brain init Nova # Init persona for Nova
|
|
1009
|
+
brainiac brain search "ruby style" # Search shared knowledge
|
|
1010
|
+
brainiac brain search --persona "tone" # Search persona
|
|
1011
|
+
brainiac brain list # List everything
|
|
1012
|
+
HELP
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
when "cron"
|
|
1016
|
+
cron_cmd = ARGV.shift
|
|
1017
|
+
config = load_config
|
|
1018
|
+
server_url = config["server_url"] || "http://localhost:4567"
|
|
1019
|
+
|
|
1020
|
+
case cron_cmd
|
|
1021
|
+
when "add", "create"
|
|
1022
|
+
# Parse arguments
|
|
1023
|
+
schedule = nil
|
|
1024
|
+
agent = ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
1025
|
+
project = nil
|
|
1026
|
+
model = nil
|
|
1027
|
+
effort = nil
|
|
1028
|
+
discord_channel_id = nil
|
|
1029
|
+
forum_title = nil
|
|
1030
|
+
repeat_count = nil
|
|
1031
|
+
prompt = nil
|
|
1032
|
+
script = nil
|
|
1033
|
+
|
|
1034
|
+
OptionParser.new do |opts|
|
|
1035
|
+
opts.banner = "Usage: brainiac cron add [options] \"<prompt or --script path>\""
|
|
1036
|
+
opts.on("-s", "--schedule EXPR", "Cron expression, @shorthand, or one-time schedule") { |v| schedule = v }
|
|
1037
|
+
opts.on("-a", "--agent NAME", "Agent name (default: $AI_AGENT_NAME)") { |v| agent = v }
|
|
1038
|
+
opts.on("-p", "--project KEY", "Project key") { |v| project = v }
|
|
1039
|
+
opts.on("-m", "--model MODEL", "Model to use (opus, sonnet, haiku, auto)") { |v| model = v }
|
|
1040
|
+
opts.on("-e", "--effort LEVEL", "Effort level (low, medium, high, xhigh, max)") { |v| effort = v }
|
|
1041
|
+
opts.on("-d", "--discord CHANNEL_ID", "Discord channel ID to post results") { |v| discord_channel_id = v }
|
|
1042
|
+
opts.on("-t", "--title TITLE", "Forum post title (for forum channels)") { |v| forum_title = v }
|
|
1043
|
+
opts.on("-r", "--repeat COUNT", Integer, "Number of times to repeat (for recurring schedules)") { |v| repeat_count = v }
|
|
1044
|
+
opts.on("--script PATH", "Run script directly (no agent, output to Discord)") { |v| script = v }
|
|
1045
|
+
end.parse!
|
|
1046
|
+
|
|
1047
|
+
prompt = ARGV.join(" ") unless script
|
|
1048
|
+
|
|
1049
|
+
unless schedule && project && (prompt&.length&.> 0 || script)
|
|
1050
|
+
puts "Error: Missing required arguments"
|
|
1051
|
+
puts ""
|
|
1052
|
+
puts "Usage examples:"
|
|
1053
|
+
puts " # Agent with prompt (recurring)"
|
|
1054
|
+
puts " brainiac cron add -s \"0 9 * * *\" -p marketplace \"Daily standup\""
|
|
1055
|
+
puts ""
|
|
1056
|
+
puts " # Script mode (no agent, direct execution)"
|
|
1057
|
+
puts " brainiac cron add -s \"0 9 * * *\" -p brainiac --script ~/.brainiac/scripts/report.sh -d 1234567890"
|
|
1058
|
+
puts ""
|
|
1059
|
+
puts " # Recurring (shorthand)"
|
|
1060
|
+
puts " brainiac cron add -s @daily -p brainiac \"Security audit\""
|
|
1061
|
+
puts ""
|
|
1062
|
+
puts " # One-time (natural language)"
|
|
1063
|
+
puts " brainiac cron add -s \"tomorrow at 9am\" -p marketplace \"Reminder\""
|
|
1064
|
+
puts " brainiac cron add -s \"in 2 hours\" -p brainiac \"Follow up\""
|
|
1065
|
+
puts " brainiac cron add -s \"next monday at 3pm\" -p marketplace \"Weekly review\""
|
|
1066
|
+
puts ""
|
|
1067
|
+
puts " # Recurring with repeat limit"
|
|
1068
|
+
puts " brainiac cron add -s \"0 9 * * *\" -r 7 -p marketplace \"Daily reminder for 7 days\""
|
|
1069
|
+
puts " brainiac cron add -s @daily -r 3 -p brainiac \"Reminder for 3 days\""
|
|
1070
|
+
exit 1
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
# Generate ID from prompt or script
|
|
1074
|
+
id = (script ? File.basename(script, ".*") : prompt).downcase.gsub(/[^a-z0-9]+/, "-")[0..30]
|
|
1075
|
+
|
|
1076
|
+
# Call server API
|
|
1077
|
+
uri = URI("#{server_url}/api/cron/add")
|
|
1078
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1079
|
+
payload = { id: id, schedule: schedule, agent: agent, project: project }
|
|
1080
|
+
payload[:prompt] = prompt if prompt
|
|
1081
|
+
payload[:script] = script if script
|
|
1082
|
+
payload[:model] = model if model
|
|
1083
|
+
payload[:effort] = effort if effort
|
|
1084
|
+
payload[:discord_channel_id] = discord_channel_id if discord_channel_id
|
|
1085
|
+
payload[:forum_title] = forum_title if forum_title
|
|
1086
|
+
payload[:repeat_count] = repeat_count if repeat_count
|
|
1087
|
+
req.body = payload.to_json
|
|
1088
|
+
|
|
1089
|
+
begin
|
|
1090
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
1091
|
+
data = JSON.parse(response.body)
|
|
1092
|
+
if data["success"]
|
|
1093
|
+
puts "✓ Cron job added: #{id}"
|
|
1094
|
+
puts " Schedule: #{schedule}"
|
|
1095
|
+
puts " Agent: #{script ? "none (script mode)" : agent}"
|
|
1096
|
+
puts " Project: #{project}"
|
|
1097
|
+
puts " Model: #{model || "default"}" unless script
|
|
1098
|
+
puts " Effort: #{effort || "default"}" unless script
|
|
1099
|
+
puts " Discord: #{discord_channel_id || "none"}"
|
|
1100
|
+
puts " Forum title: #{forum_title || "auto"}" if discord_channel_id
|
|
1101
|
+
puts " Repeat: #{repeat_count ? "#{repeat_count} times" : "unlimited"}"
|
|
1102
|
+
puts script ? " Script: #{script}" : " Prompt: #{prompt}"
|
|
1103
|
+
else
|
|
1104
|
+
puts "Error: #{data["error"]}"
|
|
1105
|
+
exit 1
|
|
1106
|
+
end
|
|
1107
|
+
rescue StandardError => e
|
|
1108
|
+
puts "Could not reach server: #{e.message}"
|
|
1109
|
+
puts "Is the server running? Check with: brainiac status"
|
|
1110
|
+
exit 1
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
when "list", "ls"
|
|
1114
|
+
begin
|
|
1115
|
+
uri = URI("#{server_url}/api/cron")
|
|
1116
|
+
response = Net::HTTP.get_response(uri)
|
|
1117
|
+
data = JSON.parse(response.body)
|
|
1118
|
+
|
|
1119
|
+
if data["jobs"].empty?
|
|
1120
|
+
puts "No cron jobs configured."
|
|
1121
|
+
puts "Add one with: brainiac cron add -s \"0 9 * * *\" -p marketplace \"Daily summary\""
|
|
1122
|
+
puts "Or one-time: brainiac cron add -s \"tomorrow at 9am\" -p marketplace \"Reminder\""
|
|
1123
|
+
else
|
|
1124
|
+
puts "Cron jobs:"
|
|
1125
|
+
data["jobs"].each do |id, job|
|
|
1126
|
+
status = job["enabled"] ? "✓" : "✗"
|
|
1127
|
+
last_run = job["last_run"] ? Time.parse(job["last_run"]).strftime("%Y-%m-%d %H:%M") : "never"
|
|
1128
|
+
execution_count = job["execution_count"] || 0
|
|
1129
|
+
|
|
1130
|
+
# Show one-time jobs differently
|
|
1131
|
+
schedule_display = job["schedule"]
|
|
1132
|
+
if job["parsed"] && job["parsed"]["one_time"]
|
|
1133
|
+
target_time = Time.parse(job["parsed"]["timestamp"])
|
|
1134
|
+
schedule_display = "#{job["schedule"]} (#{target_time.strftime("%Y-%m-%d %H:%M")})"
|
|
1135
|
+
schedule_display += " [COMPLETED]" unless job["enabled"]
|
|
1136
|
+
elsif job["repeat_count"]
|
|
1137
|
+
schedule_display = "#{job["schedule"]} (#{execution_count}/#{job["repeat_count"]} runs)"
|
|
1138
|
+
schedule_display += " [COMPLETED]" unless job["enabled"]
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
puts " #{status} #{id}"
|
|
1142
|
+
puts " Schedule: #{schedule_display}"
|
|
1143
|
+
puts " Agent: #{job["script"] ? "none (script mode)" : job["agent"]} | Project: #{job["project"]}"
|
|
1144
|
+
puts " Discord: #{job["discord_channel_id"]}" if job["discord_channel_id"]
|
|
1145
|
+
puts job["script"] ? " Script: #{job["script"]}" : " Prompt: #{job["prompt"]}"
|
|
1146
|
+
puts " Last run: #{last_run}"
|
|
1147
|
+
puts
|
|
1148
|
+
end
|
|
1149
|
+
end
|
|
1150
|
+
rescue StandardError => e
|
|
1151
|
+
puts "Could not reach server: #{e.message}"
|
|
1152
|
+
exit 1
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
when "remove", "rm", "delete"
|
|
1156
|
+
id = ARGV[0]
|
|
1157
|
+
unless id
|
|
1158
|
+
puts "Usage: brainiac cron remove <job-id>"
|
|
1159
|
+
exit 1
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
uri = URI("#{server_url}/api/cron/remove")
|
|
1163
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1164
|
+
req.body = { id: id }.to_json
|
|
1165
|
+
|
|
1166
|
+
begin
|
|
1167
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
1168
|
+
data = JSON.parse(response.body)
|
|
1169
|
+
if data["success"]
|
|
1170
|
+
puts "✓ Removed cron job: #{id}"
|
|
1171
|
+
else
|
|
1172
|
+
puts "Error: #{data["error"]}"
|
|
1173
|
+
exit 1
|
|
1174
|
+
end
|
|
1175
|
+
rescue StandardError => e
|
|
1176
|
+
puts "Could not reach server: #{e.message}"
|
|
1177
|
+
exit 1
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
when "enable"
|
|
1181
|
+
id = ARGV[0]
|
|
1182
|
+
unless id
|
|
1183
|
+
puts "Usage: brainiac cron enable <job-id>"
|
|
1184
|
+
exit 1
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
uri = URI("#{server_url}/api/cron/toggle")
|
|
1188
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1189
|
+
req.body = { id: id, enabled: true }.to_json
|
|
1190
|
+
|
|
1191
|
+
begin
|
|
1192
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
1193
|
+
data = JSON.parse(response.body)
|
|
1194
|
+
if data["success"]
|
|
1195
|
+
puts "✓ Enabled cron job: #{id}"
|
|
1196
|
+
else
|
|
1197
|
+
puts "Error: #{data["error"]}"
|
|
1198
|
+
exit 1
|
|
1199
|
+
end
|
|
1200
|
+
rescue StandardError => e
|
|
1201
|
+
puts "Could not reach server: #{e.message}"
|
|
1202
|
+
exit 1
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
when "disable"
|
|
1206
|
+
id = ARGV[0]
|
|
1207
|
+
unless id
|
|
1208
|
+
puts "Usage: brainiac cron disable <job-id>"
|
|
1209
|
+
exit 1
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
uri = URI("#{server_url}/api/cron/toggle")
|
|
1213
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1214
|
+
req.body = { id: id, enabled: false }.to_json
|
|
1215
|
+
|
|
1216
|
+
begin
|
|
1217
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
1218
|
+
data = JSON.parse(response.body)
|
|
1219
|
+
if data["success"]
|
|
1220
|
+
puts "✓ Disabled cron job: #{id}"
|
|
1221
|
+
else
|
|
1222
|
+
puts "Error: #{data["error"]}"
|
|
1223
|
+
exit 1
|
|
1224
|
+
end
|
|
1225
|
+
rescue StandardError => e
|
|
1226
|
+
puts "Could not reach server: #{e.message}"
|
|
1227
|
+
exit 1
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
when "update"
|
|
1231
|
+
id = ARGV[0]
|
|
1232
|
+
schedule = nil
|
|
1233
|
+
discord_channel_id = nil
|
|
1234
|
+
forum_title = nil
|
|
1235
|
+
|
|
1236
|
+
OptionParser.new do |opts|
|
|
1237
|
+
opts.banner = "Usage: brainiac cron update <job-id> [options]"
|
|
1238
|
+
opts.on("-s", "--schedule EXPR", "New cron expression") { |v| schedule = v }
|
|
1239
|
+
opts.on("-c", "--channel CHANNEL_ID", "New Discord channel ID") { |v| discord_channel_id = v }
|
|
1240
|
+
opts.on("-t", "--title TITLE", "New forum post title") { |v| forum_title = v }
|
|
1241
|
+
end.parse!
|
|
1242
|
+
|
|
1243
|
+
unless id && (schedule || discord_channel_id || forum_title)
|
|
1244
|
+
puts "Error: Missing required arguments"
|
|
1245
|
+
puts "Usage: brainiac cron update <job-id> -s \"42 13 * * 1-5\""
|
|
1246
|
+
puts " or: brainiac cron update <job-id> -c \"1423854179880927274\""
|
|
1247
|
+
puts " or: brainiac cron update <job-id> -t \"Daily Update\""
|
|
1248
|
+
puts " or: brainiac cron update <job-id> -s \"42 13 * * 1-5\" -c \"1423854179880927274\""
|
|
1249
|
+
exit 1
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
uri = URI("#{server_url}/api/cron/update")
|
|
1253
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1254
|
+
payload = { id: id }
|
|
1255
|
+
payload[:schedule] = schedule if schedule
|
|
1256
|
+
payload[:discord_channel_id] = discord_channel_id if discord_channel_id
|
|
1257
|
+
payload[:forum_title] = forum_title if forum_title
|
|
1258
|
+
req.body = payload.to_json
|
|
1259
|
+
|
|
1260
|
+
begin
|
|
1261
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
1262
|
+
data = JSON.parse(response.body)
|
|
1263
|
+
if data["success"]
|
|
1264
|
+
puts "✓ Updated cron job: #{id}"
|
|
1265
|
+
puts " New schedule: #{schedule}" if schedule
|
|
1266
|
+
puts " New Discord channel: #{discord_channel_id}" if discord_channel_id
|
|
1267
|
+
puts " New forum title: #{forum_title}" if forum_title
|
|
1268
|
+
else
|
|
1269
|
+
puts "Error: #{data["error"]}"
|
|
1270
|
+
exit 1
|
|
1271
|
+
end
|
|
1272
|
+
rescue StandardError => e
|
|
1273
|
+
puts "Could not reach server: #{e.message}"
|
|
1274
|
+
exit 1
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
else
|
|
1278
|
+
puts <<~HELP
|
|
1279
|
+
Usage: brainiac cron <command>
|
|
1280
|
+
|
|
1281
|
+
Commands:
|
|
1282
|
+
add -s <schedule> -p <project> [-a <agent>] "<prompt>"
|
|
1283
|
+
Add a new cron job
|
|
1284
|
+
list List all cron jobs
|
|
1285
|
+
remove <job-id> Remove a cron job
|
|
1286
|
+
enable <job-id> Enable a cron job
|
|
1287
|
+
disable <job-id> Disable a cron job
|
|
1288
|
+
update <job-id> [-s <schedule>] [-c <channel>]
|
|
1289
|
+
Update a cron job's schedule and/or Discord channel
|
|
1290
|
+
|
|
1291
|
+
Schedule format:
|
|
1292
|
+
Cron expression: "minute hour day month weekday"
|
|
1293
|
+
Examples:
|
|
1294
|
+
"0 9 * * 1-5" — 9am on weekdays
|
|
1295
|
+
"0 */4 * * *" — Every 4 hours
|
|
1296
|
+
"30 17 * * *" — 5:30pm daily
|
|
1297
|
+
|
|
1298
|
+
Special strings:
|
|
1299
|
+
@hourly — Every hour (minute 0)
|
|
1300
|
+
@daily — Every day at midnight
|
|
1301
|
+
@weekly — Every Sunday at midnight
|
|
1302
|
+
@monthly — First day of month at midnight
|
|
1303
|
+
|
|
1304
|
+
One-time (natural language):
|
|
1305
|
+
"tomorrow at 9am"
|
|
1306
|
+
"in 2 hours"
|
|
1307
|
+
"next monday at 3pm"
|
|
1308
|
+
#{" "}
|
|
1309
|
+
One-time (ISO8601):
|
|
1310
|
+
"2026-02-27T09:00:00-05:00"
|
|
1311
|
+
|
|
1312
|
+
Examples:
|
|
1313
|
+
# Recurring
|
|
1314
|
+
brainiac cron add -s "0 9 * * *" -p marketplace "Summarize yesterday's work"
|
|
1315
|
+
brainiac cron add -s "@daily" -p brainiac -a GLaDOS "Run security audit"
|
|
1316
|
+
#{" "}
|
|
1317
|
+
# One-time
|
|
1318
|
+
brainiac cron add -s "tomorrow at 9am" -p marketplace "Reminder about priorities"
|
|
1319
|
+
brainiac cron add -s "in 30 minutes" -p brainiac "Follow up on PR"
|
|
1320
|
+
#{" "}
|
|
1321
|
+
# Management
|
|
1322
|
+
brainiac cron list
|
|
1323
|
+
brainiac cron update post-morning-message -s "42 13 * * 1-5"
|
|
1324
|
+
brainiac cron disable daily-summary
|
|
1325
|
+
brainiac cron remove daily-summary
|
|
1326
|
+
|
|
1327
|
+
Config file: #{BRAINIAC_DIR}/cron.json
|
|
1328
|
+
HELP
|
|
1329
|
+
end
|
|
1330
|
+
|
|
1331
|
+
when "provider"
|
|
1332
|
+
provider_cmd = ARGV.shift
|
|
1333
|
+
providers_dir = File.join(BRAINIAC_DIR, "cli-providers")
|
|
1334
|
+
|
|
1335
|
+
case provider_cmd
|
|
1336
|
+
when "list", "ls"
|
|
1337
|
+
unless Dir.exist?(providers_dir)
|
|
1338
|
+
puts "No providers configured. Run 'brainiac provider add <name>' to create one."
|
|
1339
|
+
exit 0
|
|
1340
|
+
end
|
|
1341
|
+
files = Dir.glob(File.join(providers_dir, "*.json"))
|
|
1342
|
+
if files.empty?
|
|
1343
|
+
puts "No providers configured."
|
|
1344
|
+
else
|
|
1345
|
+
files.each do |f|
|
|
1346
|
+
name = File.basename(f, ".json")
|
|
1347
|
+
data = JSON.parse(File.read(f))
|
|
1348
|
+
puts "#{name} — #{data["binary"]} (#{data["models"]&.size || 0} models)"
|
|
1349
|
+
end
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
when "show"
|
|
1353
|
+
name = ARGV[0]
|
|
1354
|
+
unless name
|
|
1355
|
+
puts "Usage: brainiac provider show <name>"
|
|
1356
|
+
exit 1
|
|
1357
|
+
end
|
|
1358
|
+
file = File.join(providers_dir, "#{name}.json")
|
|
1359
|
+
unless File.exist?(file)
|
|
1360
|
+
puts "Provider '#{name}' not found."
|
|
1361
|
+
exit 1
|
|
1362
|
+
end
|
|
1363
|
+
puts JSON.pretty_generate(JSON.parse(File.read(file)))
|
|
1364
|
+
|
|
1365
|
+
when "add", "create"
|
|
1366
|
+
name = ARGV[0]
|
|
1367
|
+
unless name
|
|
1368
|
+
puts "Usage: brainiac provider add <name>"
|
|
1369
|
+
exit 1
|
|
1370
|
+
end
|
|
1371
|
+
FileUtils.mkdir_p(providers_dir)
|
|
1372
|
+
file = File.join(providers_dir, "#{name}.json")
|
|
1373
|
+
if File.exist?(file)
|
|
1374
|
+
puts "Provider '#{name}' already exists. Edit #{file} directly."
|
|
1375
|
+
exit 1
|
|
1376
|
+
end
|
|
1377
|
+
scaffold = {
|
|
1378
|
+
"binary" => name,
|
|
1379
|
+
"default_args" => "",
|
|
1380
|
+
"model_flag" => "--model",
|
|
1381
|
+
"list_models_command" => "",
|
|
1382
|
+
"models" => {}
|
|
1383
|
+
}
|
|
1384
|
+
File.write(file, JSON.pretty_generate(scaffold))
|
|
1385
|
+
puts "Created #{file} — edit it to configure the provider."
|
|
1386
|
+
|
|
1387
|
+
else
|
|
1388
|
+
puts <<~HELP
|
|
1389
|
+
Usage: brainiac provider <command>
|
|
1390
|
+
|
|
1391
|
+
Commands:
|
|
1392
|
+
list List configured CLI providers
|
|
1393
|
+
show <name> Show provider configuration
|
|
1394
|
+
add <name> Create a new provider config
|
|
1395
|
+
HELP
|
|
1396
|
+
end
|
|
1397
|
+
|
|
1398
|
+
when "version", "--version", "-v"
|
|
1399
|
+
puts "brainiac #{BRAINIAC_VERSION}"
|
|
1400
|
+
|
|
1401
|
+
when "help", "--help", "-h", nil
|
|
1402
|
+
puts <<~HELP
|
|
1403
|
+
Brainiac CLI - Manage projects with Brainiac server
|
|
1404
|
+
|
|
1405
|
+
Usage:
|
|
1406
|
+
brainiac setup Bootstrap a new machine (deps, dirs, configs)
|
|
1407
|
+
brainiac server [options] Start the Brainiac webhook server (foreground)
|
|
1408
|
+
brainiac stop Stop the running server
|
|
1409
|
+
brainiac restart Restart the server
|
|
1410
|
+
brainiac logs Tail the server log
|
|
1411
|
+
brainiac status Check if server is running
|
|
1412
|
+
brainiac register [options] Register current directory as a project
|
|
1413
|
+
brainiac unregister <key> Unregister a project
|
|
1414
|
+
brainiac list List all registered projects
|
|
1415
|
+
brainiac projects list List all registered projects (alias)
|
|
1416
|
+
brainiac projects default <key> Set the default project
|
|
1417
|
+
brainiac show <key> Show project configuration
|
|
1418
|
+
brainiac brain <command> Manage agent long-term memory (brain)
|
|
1419
|
+
brainiac discord <command> Manage the Discord bot
|
|
1420
|
+
brainiac cron <command> Manage scheduled agent tasks
|
|
1421
|
+
brainiac provider <command> Manage CLI providers
|
|
1422
|
+
brainiac config Configure Brainiac CLI
|
|
1423
|
+
brainiac path Show Brainiac config directory
|
|
1424
|
+
brainiac version Show version
|
|
1425
|
+
brainiac help Show this help message
|
|
1426
|
+
|
|
1427
|
+
Server Options:
|
|
1428
|
+
-d, --daemon Run server in background (detached)
|
|
1429
|
+
|
|
1430
|
+
Card Map Commands:
|
|
1431
|
+
card-map list Show all card map entries
|
|
1432
|
+
card-map clean Remove stale entries (missing worktrees)
|
|
1433
|
+
|
|
1434
|
+
Brain Commands:
|
|
1435
|
+
brain init [agent] Initialize brain (knowledge + persona)
|
|
1436
|
+
brain status [agent] Show brain status and files
|
|
1437
|
+
brain search [--persona] <q> Search knowledge or persona
|
|
1438
|
+
brain list List knowledge and all personas
|
|
1439
|
+
brain path Show knowledge directory
|
|
1440
|
+
|
|
1441
|
+
Cron Commands:
|
|
1442
|
+
cron add -s <schedule> -p <project> "<prompt>"
|
|
1443
|
+
Add a scheduled task
|
|
1444
|
+
cron list List all cron jobs
|
|
1445
|
+
cron remove <job-id> Remove a cron job
|
|
1446
|
+
cron enable <job-id> Enable a cron job
|
|
1447
|
+
cron disable <job-id> Disable a cron job
|
|
1448
|
+
cron update <job-id> -s <schedule>
|
|
1449
|
+
Update a cron job's schedule
|
|
1450
|
+
|
|
1451
|
+
Discord Commands:
|
|
1452
|
+
discord config Show Discord config
|
|
1453
|
+
discord default <proj> Set default project for all channels
|
|
1454
|
+
discord map <ch> <proj> Map a channel to a project
|
|
1455
|
+
discord owner [<id>] Set/show machine owner (version notifications)
|
|
1456
|
+
discord token <agent> <token> Set Discord bot token for an agent
|
|
1457
|
+
discord agents List agents with Discord bot tokens
|
|
1458
|
+
discord status Check bot status via server API
|
|
1459
|
+
|
|
1460
|
+
Provider Commands:
|
|
1461
|
+
provider list List configured CLI providers
|
|
1462
|
+
provider show <name> Show provider configuration
|
|
1463
|
+
provider add <name> Create a new provider config
|
|
1464
|
+
|
|
1465
|
+
Examples:
|
|
1466
|
+
# Start the server in the foreground (like rails server)
|
|
1467
|
+
brainiac server
|
|
1468
|
+
brainiac s
|
|
1469
|
+
#{" "}
|
|
1470
|
+
# Start the server in background (detached)
|
|
1471
|
+
brainiac server --daemon
|
|
1472
|
+
brainiac server -d
|
|
1473
|
+
#{" "}
|
|
1474
|
+
# Tail the server logs
|
|
1475
|
+
brainiac logs
|
|
1476
|
+
#{" "}
|
|
1477
|
+
# Restart the server
|
|
1478
|
+
brainiac restart
|
|
1479
|
+
#{" "}
|
|
1480
|
+
# Stop the server
|
|
1481
|
+
brainiac stop
|
|
1482
|
+
#{" "}
|
|
1483
|
+
# Check server status
|
|
1484
|
+
brainiac status
|
|
1485
|
+
#{" "}
|
|
1486
|
+
# Register current directory
|
|
1487
|
+
cd ~/Code/my-project && brainiac register
|
|
1488
|
+
#{" "}
|
|
1489
|
+
# Register with specific path
|
|
1490
|
+
brainiac register --path ~/Code/my-project
|
|
1491
|
+
#{" "}
|
|
1492
|
+
# List all projects
|
|
1493
|
+
brainiac list
|
|
1494
|
+
brainiac projects list # alternative syntax
|
|
1495
|
+
#{" "}
|
|
1496
|
+
# Set default project
|
|
1497
|
+
brainiac projects default my-project
|
|
1498
|
+
#{" "}
|
|
1499
|
+
# Unregister a project
|
|
1500
|
+
brainiac unregister my-project
|
|
1501
|
+
#{" "}
|
|
1502
|
+
# Show project details
|
|
1503
|
+
brainiac show my-project
|
|
1504
|
+
#{" "}
|
|
1505
|
+
# Initialize brain for your agent
|
|
1506
|
+
brainiac brain init Galen
|
|
1507
|
+
#{" "}
|
|
1508
|
+
# Search the brain
|
|
1509
|
+
brainiac brain search "ruby conventions"
|
|
1510
|
+
|
|
1511
|
+
Configuration:
|
|
1512
|
+
Projects are stored in: #{BRAINIAC_DIR}/projects.json
|
|
1513
|
+
Server config: #{BRAINIAC_DIR}/config.json
|
|
1514
|
+
Brain storage: #{BRAINIAC_DIR}/brain/
|
|
1515
|
+
HELP
|
|
1516
|
+
|
|
1517
|
+
else
|
|
1518
|
+
puts "Unknown command: #{subcommand}"
|
|
1519
|
+
puts "Run 'brainiac help' for usage information."
|
|
1520
|
+
exit 1
|
|
1521
|
+
end
|