brainiac 0.0.6 → 0.0.8
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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +136 -6
- data/bin/brainiac +472 -44
- data/bin/brainiac-completion.bash +1 -1
- data/lib/brainiac/agents.rb +27 -74
- data/lib/brainiac/brain.rb +6 -6
- data/lib/brainiac/config.rb +40 -76
- data/lib/brainiac/handlers/discord/api.rb +196 -0
- data/lib/brainiac/handlers/discord/config.rb +134 -0
- data/lib/brainiac/handlers/discord/delivery.rb +196 -0
- data/lib/brainiac/handlers/discord/gateway.rb +212 -0
- data/lib/brainiac/handlers/discord/message.rb +933 -0
- data/lib/brainiac/handlers/discord/reactions.rb +215 -0
- data/lib/brainiac/handlers/discord.rb +14 -1892
- data/lib/brainiac/handlers/github.rb +134 -317
- data/lib/brainiac/handlers/shared/git.rb +190 -0
- data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
- data/lib/brainiac/handlers/zoho.rb +103 -153
- data/lib/brainiac/helpers.rb +43 -455
- data/lib/brainiac/hooks.rb +86 -0
- data/lib/brainiac/plugins.rb +154 -0
- data/lib/brainiac/prompts.rb +34 -172
- data/lib/brainiac/restart.rb +112 -0
- data/lib/brainiac/routes/api.rb +411 -0
- data/lib/brainiac/users.rb +1 -7
- data/lib/brainiac/version.rb +1 -1
- data/lib/brainiac/zoho_mail_api.rb +2 -1
- data/lib/brainiac.rb +8 -1
- data/monitor/daemon.rb +4 -27
- data/monitor/shared.rb +247 -0
- data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +17 -89
- data/monitor/waybar/setup.rb +232 -0
- data/monitor/waybar/status.rb +51 -0
- data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
- data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
- data/monitor/xbar/plugin.rb +149 -0
- data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
- data/receiver.rb +44 -551
- data/templates/agents.json.example +1 -2
- data/templates/brainiac.json.example +8 -0
- data/templates/cli-providers/kiro.json.example +8 -2
- data/templates/plugins.json.example +3 -0
- data/templates/users.json.example +0 -3
- metadata +25 -23
- data/lib/brainiac/card_index.rb +0 -389
- data/lib/brainiac/deployments.rb +0 -258
- data/lib/brainiac/handlers/fizzy.rb +0 -1292
- data/lib/brainiac/planning.rb +0 -237
- data/lib/user_registry.rb +0 -159
- data/monitor/menubar.rb +0 -295
- data/monitor/setup-waybar-deploy-envs.rb +0 -121
- data/monitor/setup-waybar-deployments.rb +0 -96
- data/monitor/setup-waybar-module.rb +0 -113
- data/monitor/setup-xbar-plugin.rb +0 -35
- data/monitor/view-logs.rb +0 -119
- data/monitor/waybar-config-updater.rb +0 -56
- data/monitor/waybar-deployments.rb +0 -239
- data/monitor/waybar.rb +0 -146
- data/monitor/xbar.3s.rb +0 -179
- data/templates/fizzy.json.example +0 -24
- /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
- /data/monitor/{view-logs-macos.rb → xbar/view_logs.rb} +0 -0
data/receiver.rb
CHANGED
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
require "sinatra"
|
|
9
9
|
require "json"
|
|
10
10
|
|
|
11
|
+
# The directory this server is running from (supports worktrees)
|
|
12
|
+
SERVER_ROOT = File.expand_path(__dir__)
|
|
13
|
+
|
|
11
14
|
# Load all modules
|
|
15
|
+
require_relative "lib/brainiac/hooks"
|
|
12
16
|
require_relative "lib/brainiac/config"
|
|
13
17
|
require_relative "lib/brainiac/users"
|
|
14
18
|
require_relative "lib/brainiac/agents"
|
|
@@ -16,13 +20,35 @@ require_relative "lib/brainiac/brain"
|
|
|
16
20
|
require_relative "lib/brainiac/skills"
|
|
17
21
|
require_relative "lib/brainiac/sessions"
|
|
18
22
|
require_relative "lib/brainiac/prompts"
|
|
19
|
-
require_relative "lib/brainiac/planning"
|
|
20
23
|
require_relative "lib/brainiac/helpers"
|
|
21
24
|
require_relative "lib/brainiac/cron"
|
|
22
|
-
require_relative "lib/brainiac/
|
|
23
|
-
require_relative "lib/brainiac/
|
|
24
|
-
require_relative "lib/brainiac/
|
|
25
|
-
require_relative "lib/brainiac/
|
|
25
|
+
require_relative "lib/brainiac/restart"
|
|
26
|
+
require_relative "lib/brainiac/plugins"
|
|
27
|
+
require_relative "lib/brainiac/handlers/shared/git"
|
|
28
|
+
require_relative "lib/brainiac/handlers/shared/inline_tags"
|
|
29
|
+
|
|
30
|
+
# Namespace for gem-based plugins (brainiac-whatsapp, brainiac-slack, etc.)
|
|
31
|
+
module Brainiac
|
|
32
|
+
module Plugins
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# --- Conditional handler loading (based on ~/.brainiac/brainiac.json) ---
|
|
37
|
+
|
|
38
|
+
if handler_enabled?("github")
|
|
39
|
+
require_relative "lib/brainiac/handlers/github"
|
|
40
|
+
LOG.info "[Handlers] GitHub handler enabled"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if DISCORD_ENABLED && handler_enabled?("discord")
|
|
44
|
+
require_relative "lib/brainiac/handlers/discord"
|
|
45
|
+
LOG.info "[Handlers] Discord handler enabled"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if DISCORD_ENABLED && handler_enabled?("zoho")
|
|
49
|
+
require_relative "lib/brainiac/handlers/zoho"
|
|
50
|
+
LOG.info "[Handlers] Zoho handler enabled"
|
|
51
|
+
end
|
|
26
52
|
|
|
27
53
|
# Reload hook registry — custom handlers register callbacks here
|
|
28
54
|
module ReloadHooks
|
|
@@ -45,15 +71,21 @@ end
|
|
|
45
71
|
CUSTOM_HANDLERS_DIR = File.join(BRAINIAC_DIR, "handlers")
|
|
46
72
|
if Dir.exist?(CUSTOM_HANDLERS_DIR)
|
|
47
73
|
Dir.glob(File.join(CUSTOM_HANDLERS_DIR, "*.rb")).each do |handler|
|
|
48
|
-
|
|
74
|
+
handler_name = File.basename(handler, ".rb")
|
|
75
|
+
unless handler_enabled?(handler_name)
|
|
76
|
+
LOG.info "[Handlers] Skipping custom handler (disabled): #{handler_name}"
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
LOG.info "[Handlers] Loading custom handler: #{handler_name}"
|
|
49
80
|
require handler
|
|
50
81
|
end
|
|
51
82
|
end
|
|
52
83
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
# --- Load gem-based plugins (brainiac-*) ---
|
|
85
|
+
load_plugins!(Sinatra::Application)
|
|
86
|
+
|
|
87
|
+
# Emit server_started hook — plugins can run startup tasks (backfill, background jobs, etc.)
|
|
88
|
+
Brainiac.emit(:server_started)
|
|
57
89
|
|
|
58
90
|
# --- Sinatra config ---
|
|
59
91
|
set :host_authorization, { permit_all: true }
|
|
@@ -113,90 +145,6 @@ before "/api/*" do
|
|
|
113
145
|
authenticate_dashboard!
|
|
114
146
|
end
|
|
115
147
|
|
|
116
|
-
# --- Fizzy webhook routes ---
|
|
117
|
-
|
|
118
|
-
post "/fizzy/?:board_key?" do
|
|
119
|
-
content_type :json
|
|
120
|
-
request.body.rewind
|
|
121
|
-
payload_body = request.body.read
|
|
122
|
-
board_key = params["board_key"]
|
|
123
|
-
|
|
124
|
-
verify_signature!(request, payload_body, board_key: board_key)
|
|
125
|
-
|
|
126
|
-
payload = JSON.parse(payload_body)
|
|
127
|
-
|
|
128
|
-
event_id = payload["id"]
|
|
129
|
-
action = payload["action"]
|
|
130
|
-
|
|
131
|
-
LOG.info "[Fizzy] Received event #{event_id}: action=#{action}"
|
|
132
|
-
|
|
133
|
-
if already_processed?(event_id)
|
|
134
|
-
LOG.info "Skipping duplicate event #{event_id}"
|
|
135
|
-
halt 200, { status: "duplicate" }.to_json
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
reload_projects!
|
|
139
|
-
reload_agent_registry!
|
|
140
|
-
reload_github_config!
|
|
141
|
-
|
|
142
|
-
case action
|
|
143
|
-
when "card_assigned"
|
|
144
|
-
status_code, body = handle_card_assigned(payload)
|
|
145
|
-
LOG.info "[Fizzy] #{action} response: #{status_code} - #{body}"
|
|
146
|
-
halt status_code, body
|
|
147
|
-
when "comment_created"
|
|
148
|
-
status_code, body = handle_comment(payload)
|
|
149
|
-
LOG.info "[Fizzy] comment_created response: #{status_code} - #{body}"
|
|
150
|
-
halt status_code, body
|
|
151
|
-
when "card_published", "card_triaged"
|
|
152
|
-
eventable = payload["eventable"] || {}
|
|
153
|
-
card_number = eventable["number"]&.to_s
|
|
154
|
-
|
|
155
|
-
# card_triaged never dispatches agents — only card_assigned and @mentions do that.
|
|
156
|
-
# Guards remain as defense-in-depth for any future column-based routing.
|
|
157
|
-
if action == "card_triaged" && card_number
|
|
158
|
-
if self_move_recent?(card_number)
|
|
159
|
-
LOG.info "[Fizzy] Ignoring card_triaged for ##{card_number} — self-move echo"
|
|
160
|
-
halt 200, { status: "ignored", reason: "self_move" }.to_json
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
if card_merged?(card_number)
|
|
164
|
-
LOG.info "[Fizzy] Ignoring card_triaged for ##{card_number} — card already merged"
|
|
165
|
-
halt 200, { status: "ignored", reason: "card_merged" }.to_json
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
card_key = "card-#{card_number}"
|
|
169
|
-
if recently_completed?(card_key)
|
|
170
|
-
LOG.info "[Fizzy] Ignoring card_triaged for ##{card_number} — recently completed"
|
|
171
|
-
halt 200, { status: "ignored", reason: "recently_completed" }.to_json
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Only card_published does duplicate detection — card_triaged skips agent dispatch entirely
|
|
176
|
-
if action == "card_published"
|
|
177
|
-
assignees = eventable["assignees"] || []
|
|
178
|
-
if assignees.any? { |a| local_agent_names.include?(a["name"]) }
|
|
179
|
-
status_code, body = handle_card_assigned(payload)
|
|
180
|
-
LOG.info "[Fizzy] #{action} (with assignee) response: #{status_code} - #{body}"
|
|
181
|
-
halt status_code, body
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
status_code, body = handle_card_published(payload)
|
|
186
|
-
LOG.info "[Fizzy] #{action} response: #{status_code} - #{body}"
|
|
187
|
-
halt status_code, body
|
|
188
|
-
else
|
|
189
|
-
LOG.info "[Fizzy] Ignoring unknown action: #{action}"
|
|
190
|
-
halt 200, { status: "ignored", action: action }.to_json
|
|
191
|
-
end
|
|
192
|
-
rescue JSON::ParserError => e
|
|
193
|
-
LOG.error "Invalid JSON: #{e.message}"
|
|
194
|
-
halt 400, { error: "Invalid JSON" }.to_json
|
|
195
|
-
rescue StandardError => e
|
|
196
|
-
LOG.error "Unhandled error: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
|
|
197
|
-
halt 500, { error: e.message }.to_json
|
|
198
|
-
end
|
|
199
|
-
|
|
200
148
|
post "/github" do
|
|
201
149
|
content_type :json
|
|
202
150
|
request.body.rewind
|
|
@@ -219,7 +167,7 @@ post "/github" do
|
|
|
219
167
|
status_code, body = handle_github_pr_merged(payload)
|
|
220
168
|
halt status_code, body
|
|
221
169
|
elsif action == "opened"
|
|
222
|
-
|
|
170
|
+
track_pr_in_work_items(payload)
|
|
223
171
|
halt 200, { status: "processed", action: "pr_tracked" }.to_json
|
|
224
172
|
elsif action == "synchronize"
|
|
225
173
|
status_code, body = handle_github_pr_synchronized(payload)
|
|
@@ -398,247 +346,7 @@ if DISCORD_ENABLED
|
|
|
398
346
|
end
|
|
399
347
|
|
|
400
348
|
# --- Admin API routes ---
|
|
401
|
-
|
|
402
|
-
get "/api/projects" do
|
|
403
|
-
content_type :json
|
|
404
|
-
reload_projects!
|
|
405
|
-
{ projects: PROJECTS }.to_json
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
get "/api/projects/:key" do
|
|
409
|
-
content_type :json
|
|
410
|
-
reload_projects!
|
|
411
|
-
project_key = params["key"]
|
|
412
|
-
if PROJECTS.key?(project_key)
|
|
413
|
-
{ project: PROJECTS[project_key] }.to_json
|
|
414
|
-
else
|
|
415
|
-
halt 404, { error: "Project not found" }.to_json
|
|
416
|
-
end
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
post "/api/reload" do
|
|
420
|
-
content_type :json
|
|
421
|
-
reload_projects!(force: true)
|
|
422
|
-
reload_agent_registry!(force: true)
|
|
423
|
-
reload_user_registry!(force: true)
|
|
424
|
-
reload_github_config!(force: true)
|
|
425
|
-
reload_deployments_config!(force: true)
|
|
426
|
-
ReloadHooks.run_all!
|
|
427
|
-
{ status: "reloaded", projects: PROJECTS.keys, agents: all_agent_names.to_a, registry: AGENT_REGISTRY.keys,
|
|
428
|
-
users: USER_REGISTRY["users"].size }.to_json
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
get "/api/agents" do
|
|
432
|
-
content_type :json
|
|
433
|
-
{ default: AI_AGENT_NAME, agents: discover_kiro_agents, all_known: all_agent_names.to_a, roster: agent_roster }.to_json
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
get "/api/roles" do
|
|
437
|
-
content_type :json
|
|
438
|
-
roles = []
|
|
439
|
-
if Dir.exist?(ROLES_DIR)
|
|
440
|
-
Dir.glob(File.join(ROLES_DIR, "*.md")).each do |f|
|
|
441
|
-
name = File.basename(f, ".md")
|
|
442
|
-
agents = AGENT_REGISTRY.select { |_, e| e.is_a?(Hash) && Array(e["role"]).include?(name) }.map { |k, e| e["fizzy_name"] || k.capitalize }
|
|
443
|
-
roles << { name: name, agents: agents }
|
|
444
|
-
end
|
|
445
|
-
end
|
|
446
|
-
{ roles: roles, dir: ROLES_DIR }.to_json
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
get "/api/users" do
|
|
450
|
-
content_type :json
|
|
451
|
-
reload_user_registry!
|
|
452
|
-
|
|
453
|
-
filter = params["filter"]
|
|
454
|
-
users = case filter
|
|
455
|
-
when "humans" then human_users
|
|
456
|
-
when "agents" then ai_agents
|
|
457
|
-
else USER_REGISTRY["users"]
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
{ users: users, total: USER_REGISTRY["users"].size, schema_version: USER_REGISTRY["schema_version"] }.to_json
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
get "/api/users/:identifier" do
|
|
464
|
-
content_type :json
|
|
465
|
-
reload_user_registry!
|
|
466
|
-
|
|
467
|
-
identifier = params["identifier"]
|
|
468
|
-
user = find_user(identifier)
|
|
469
|
-
|
|
470
|
-
if user
|
|
471
|
-
{ user: user }.to_json
|
|
472
|
-
else
|
|
473
|
-
halt 404, { error: "User not found", identifier: identifier }.to_json
|
|
474
|
-
end
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
# --- Brain API routes ---
|
|
478
|
-
|
|
479
|
-
get "/api/brain" do
|
|
480
|
-
content_type :json
|
|
481
|
-
agent = params["agent"] || AI_AGENT_NAME
|
|
482
|
-
persona_dir = persona_dir_for(agent)
|
|
483
|
-
persona_col = persona_collection_for(agent)
|
|
484
|
-
|
|
485
|
-
knowledge_files = File.directory?(KNOWLEDGE_DIR) ? Dir.glob(File.join(KNOWLEDGE_DIR, "**", "*.md")).map { |f| f.sub("#{KNOWLEDGE_DIR}/", "") } : []
|
|
486
|
-
persona_files = File.directory?(persona_dir) ? Dir.glob(File.join(persona_dir, "**", "*.md")).map { |f| f.sub("#{persona_dir}/", "") } : []
|
|
487
|
-
|
|
488
|
-
{
|
|
489
|
-
agent: agent,
|
|
490
|
-
knowledge: { dir: KNOWLEDGE_DIR, collection: KNOWLEDGE_COLLECTION, files: knowledge_files },
|
|
491
|
-
persona: { dir: persona_dir, collection: persona_col, files: persona_files }
|
|
492
|
-
}.to_json
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
get "/api/brain/search" do
|
|
496
|
-
content_type :json
|
|
497
|
-
query = params["q"]
|
|
498
|
-
halt 400, { error: "Missing query parameter ?q=" }.to_json unless query && !query.empty?
|
|
499
|
-
|
|
500
|
-
agent = params["agent"] || AI_AGENT_NAME
|
|
501
|
-
scope = (params["scope"] || "knowledge").to_sym
|
|
502
|
-
scope = :knowledge unless %i[knowledge persona].include?(scope)
|
|
503
|
-
results = query_brain(query, agent_name: agent, scope: scope, max_results: (params["n"] || 5).to_i)
|
|
504
|
-
|
|
505
|
-
{ agent: agent, scope: scope, query: query, results: results }.to_json
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
get "/api/skills" do
|
|
509
|
-
content_type :json
|
|
510
|
-
skills = build_skill_index
|
|
511
|
-
{ total: skills.size, skills: skills }.to_json
|
|
512
|
-
end
|
|
513
|
-
|
|
514
|
-
post "/api/skills/curate" do
|
|
515
|
-
content_type :json
|
|
516
|
-
result = curate_skills
|
|
517
|
-
result.to_json
|
|
518
|
-
end
|
|
519
|
-
|
|
520
|
-
get "/api/card-index" do
|
|
521
|
-
content_type :json
|
|
522
|
-
query = params["q"]
|
|
523
|
-
if query && !query.empty?
|
|
524
|
-
similar = CARD_INDEX.find_similar_cards(query)
|
|
525
|
-
{ query: query, matches: similar, total_indexed: CARD_INDEX.size }.to_json
|
|
526
|
-
else
|
|
527
|
-
{ total: CARD_INDEX.size, cards: CARD_INDEX }.to_json
|
|
528
|
-
end
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
get "/api/dispatch-depth" do
|
|
532
|
-
content_type :json
|
|
533
|
-
{
|
|
534
|
-
max_depth: AGENT_DISPATCH_MAX_DEPTH,
|
|
535
|
-
window_seconds: AGENT_DISPATCH_WINDOW,
|
|
536
|
-
cards: AGENT_DISPATCH_DEPTH.transform_values do |v|
|
|
537
|
-
{ count: v[:count], last_human_at: v[:last_human_at]&.iso8601, blocked: v[:count] >= AGENT_DISPATCH_MAX_DEPTH }
|
|
538
|
-
end
|
|
539
|
-
}.to_json
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
get "/api/status" do
|
|
543
|
-
content_type :json
|
|
544
|
-
ACTIVE_SESSIONS_MUTEX.synchronize do
|
|
545
|
-
# Clean up stale sessions first
|
|
546
|
-
ACTIVE_SESSIONS.delete_if do |card_key, info|
|
|
547
|
-
Process.kill(0, info[:pid])
|
|
548
|
-
false # Keep alive sessions
|
|
549
|
-
rescue Errno::ESRCH, Errno::EPERM
|
|
550
|
-
archive_session(card_key, info)
|
|
551
|
-
true # Remove dead sessions
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
sessions = ACTIVE_SESSIONS.map do |card_key, info|
|
|
555
|
-
# Use stored agent_name if available, otherwise try to extract from card_key
|
|
556
|
-
agent_name = if info[:agent_name]
|
|
557
|
-
info[:agent_name]
|
|
558
|
-
else
|
|
559
|
-
# Fallback: extract from card_key for backwards compatibility
|
|
560
|
-
# Formats: "discord-AGENT-CHANNEL-MESSAGE" or "card-ID"
|
|
561
|
-
parts = card_key.split("-")
|
|
562
|
-
agent_key = if parts[0] == "discord" && parts.size >= 4
|
|
563
|
-
parts[1] # Second part is agent name
|
|
564
|
-
else
|
|
565
|
-
"Unknown"
|
|
566
|
-
end
|
|
567
|
-
fizzy_display_name(agent_key)
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
{
|
|
571
|
-
card_key: card_key,
|
|
572
|
-
agent: agent_name,
|
|
573
|
-
pid: info[:pid],
|
|
574
|
-
started_at: info[:started_at].iso8601,
|
|
575
|
-
elapsed_seconds: (Time.now - info[:started_at]).to_i,
|
|
576
|
-
log_file: info[:log_file],
|
|
577
|
-
alive: true,
|
|
578
|
-
children: child_processes_for(info[:pid])
|
|
579
|
-
}
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
recent = RECENT_SESSIONS.map do |s|
|
|
583
|
-
{
|
|
584
|
-
card_key: s[:card_key],
|
|
585
|
-
agent: s[:agent_name] || "Unknown",
|
|
586
|
-
log_file: s[:log_file],
|
|
587
|
-
started_at: s[:started_at]&.iso8601,
|
|
588
|
-
finished_at: s[:finished_at]&.iso8601
|
|
589
|
-
}
|
|
590
|
-
end
|
|
591
|
-
|
|
592
|
-
{ sessions: sessions, count: sessions.size, recent: recent, version: BRAINIAC_VERSION }.to_json
|
|
593
|
-
end
|
|
594
|
-
end
|
|
595
|
-
|
|
596
|
-
# Kill an entire agent session (parent process + all children)
|
|
597
|
-
post "/api/sessions/kill/:card_key" do
|
|
598
|
-
content_type :json
|
|
599
|
-
card_key = params[:card_key]
|
|
600
|
-
halt 400, { error: "missing card_key" }.to_json if card_key.to_s.empty?
|
|
601
|
-
|
|
602
|
-
killed = kill_session(card_key)
|
|
603
|
-
halt 404, { error: "session not found" }.to_json unless killed
|
|
604
|
-
|
|
605
|
-
LOG.info "Killed agent session #{card_key} via API"
|
|
606
|
-
{ killed: card_key }.to_json
|
|
607
|
-
end
|
|
608
|
-
|
|
609
|
-
# Kill a specific child process of an active agent session
|
|
610
|
-
post "/api/sessions/kill-process/:pid" do
|
|
611
|
-
content_type :json
|
|
612
|
-
target_pid = params[:pid].to_i
|
|
613
|
-
halt 400, { error: "invalid pid" }.to_json if target_pid <= 0
|
|
614
|
-
|
|
615
|
-
# Verify the target PID is actually a descendant of an active session
|
|
616
|
-
valid = ACTIVE_SESSIONS_MUTEX.synchronize do
|
|
617
|
-
ACTIVE_SESSIONS.any? do |_, info|
|
|
618
|
-
child_processes_for(info[:pid]).any? { |c| c[:pid] == target_pid }
|
|
619
|
-
end
|
|
620
|
-
end
|
|
621
|
-
halt 403, { error: "pid is not a child of any active agent session" }.to_json unless valid
|
|
622
|
-
|
|
623
|
-
begin
|
|
624
|
-
Process.kill("TERM", target_pid)
|
|
625
|
-
# Give it a moment, then force kill if still alive
|
|
626
|
-
Thread.new do
|
|
627
|
-
sleep 3
|
|
628
|
-
begin
|
|
629
|
-
Process.kill(0, target_pid)
|
|
630
|
-
Process.kill("KILL", target_pid)
|
|
631
|
-
rescue Errno::ESRCH, Errno::EPERM # rubocop:disable Lint/SuppressedException
|
|
632
|
-
end
|
|
633
|
-
end
|
|
634
|
-
LOG.info "Killed child process #{target_pid} (SIGTERM)"
|
|
635
|
-
{ killed: target_pid }.to_json
|
|
636
|
-
rescue Errno::ESRCH
|
|
637
|
-
halt 404, { error: "process not found" }.to_json
|
|
638
|
-
rescue Errno::EPERM
|
|
639
|
-
halt 403, { error: "permission denied" }.to_json
|
|
640
|
-
end
|
|
641
|
-
end
|
|
349
|
+
require_relative "lib/brainiac/routes/api"
|
|
642
350
|
|
|
643
351
|
# --- Dashboard ---
|
|
644
352
|
|
|
@@ -660,25 +368,6 @@ get "/dashboard" do
|
|
|
660
368
|
erb :dashboard, layout: false
|
|
661
369
|
end
|
|
662
370
|
|
|
663
|
-
get "/api/logs" do
|
|
664
|
-
content_type "text/plain"
|
|
665
|
-
log_file = params["file"]
|
|
666
|
-
lines = (params["lines"] || 200).to_i
|
|
667
|
-
|
|
668
|
-
halt 400, "Missing ?file= parameter" unless log_file && !log_file.empty?
|
|
669
|
-
halt 400, "Invalid path" if log_file.include?("..") || !log_file.start_with?("/")
|
|
670
|
-
halt 404, "File not found" unless File.exist?(log_file)
|
|
671
|
-
|
|
672
|
-
# Only allow reading log files from known project tmp dirs or brainiac tmp
|
|
673
|
-
allowed = PROJECTS.values.map { |p| File.join(p["repo_path"], "tmp") }
|
|
674
|
-
allowed << File.join(BRAINIAC_DIR, "tmp")
|
|
675
|
-
halt 403, "Forbidden" unless allowed.any? { |dir| log_file.start_with?(dir) }
|
|
676
|
-
|
|
677
|
-
# Read last N lines and strip ANSI escape codes
|
|
678
|
-
all_lines = File.readlines(log_file).last(lines)
|
|
679
|
-
all_lines.join.gsub(/\e\[[\d;]*[a-zA-Z]/, "").gsub(/\e\[\?[\d;]*[a-zA-Z]/, "")
|
|
680
|
-
end
|
|
681
|
-
|
|
682
371
|
# --- Discord API + startup ---
|
|
683
372
|
|
|
684
373
|
if DISCORD_ENABLED
|
|
@@ -737,199 +426,6 @@ else
|
|
|
737
426
|
end
|
|
738
427
|
end
|
|
739
428
|
|
|
740
|
-
# --- Cron API + startup ---
|
|
741
|
-
|
|
742
|
-
get "/api/cron/script" do
|
|
743
|
-
content_type "text/plain"
|
|
744
|
-
path = params["path"]
|
|
745
|
-
halt 400, "Missing ?path= parameter" unless path && !path.empty?
|
|
746
|
-
halt 400, "Invalid path" if path.include?("..")
|
|
747
|
-
|
|
748
|
-
# Only allow reading scripts that are actually referenced by a cron job
|
|
749
|
-
reload_cron_jobs!
|
|
750
|
-
valid = CRON_JOBS.values.any? do |j|
|
|
751
|
-
j[:script] == path || j["script"] == path ||
|
|
752
|
-
(j[:prompt] || j["prompt"] || "").include?(path)
|
|
753
|
-
end
|
|
754
|
-
halt 403, "Not a registered cron script" unless valid
|
|
755
|
-
halt 404, "File not found" unless File.exist?(path)
|
|
756
|
-
|
|
757
|
-
File.read(path)
|
|
758
|
-
end
|
|
759
|
-
|
|
760
|
-
get "/api/cron" do
|
|
761
|
-
content_type :json
|
|
762
|
-
reload_cron_jobs!
|
|
763
|
-
{
|
|
764
|
-
enabled: true,
|
|
765
|
-
jobs: CRON_JOBS,
|
|
766
|
-
thread_alive: CRON_THREAD[:ref]&.alive? || false
|
|
767
|
-
}.to_json
|
|
768
|
-
end
|
|
769
|
-
|
|
770
|
-
post "/api/cron/add" do
|
|
771
|
-
content_type :json
|
|
772
|
-
request.body.rewind
|
|
773
|
-
payload = JSON.parse(request.body.read)
|
|
774
|
-
|
|
775
|
-
result = add_cron_job(
|
|
776
|
-
id: payload["id"],
|
|
777
|
-
schedule: payload["schedule"],
|
|
778
|
-
agent: payload["agent"],
|
|
779
|
-
project: payload["project"],
|
|
780
|
-
prompt: payload["prompt"],
|
|
781
|
-
script: payload["script"],
|
|
782
|
-
model: payload["model"],
|
|
783
|
-
effort: payload["effort"],
|
|
784
|
-
discord_channel_id: payload["discord_channel_id"],
|
|
785
|
-
forum_title: payload["forum_title"],
|
|
786
|
-
forum_reply_to_latest: payload["forum_reply_to_latest"] || false,
|
|
787
|
-
repeat_count: payload["repeat_count"]
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
result.to_json
|
|
791
|
-
end
|
|
792
|
-
|
|
793
|
-
post "/api/cron/remove" do
|
|
794
|
-
content_type :json
|
|
795
|
-
request.body.rewind
|
|
796
|
-
payload = JSON.parse(request.body.read)
|
|
797
|
-
|
|
798
|
-
result = remove_cron_job(payload["id"])
|
|
799
|
-
result.to_json
|
|
800
|
-
end
|
|
801
|
-
|
|
802
|
-
post "/api/cron/toggle" do
|
|
803
|
-
content_type :json
|
|
804
|
-
request.body.rewind
|
|
805
|
-
payload = JSON.parse(request.body.read)
|
|
806
|
-
|
|
807
|
-
result = toggle_cron_job(payload["id"], payload["enabled"])
|
|
808
|
-
result.to_json
|
|
809
|
-
end
|
|
810
|
-
|
|
811
|
-
post "/api/cron/update" do
|
|
812
|
-
content_type :json
|
|
813
|
-
request.body.rewind
|
|
814
|
-
payload = JSON.parse(request.body.read)
|
|
815
|
-
|
|
816
|
-
result = update_cron_job(
|
|
817
|
-
payload["id"],
|
|
818
|
-
schedule: payload["schedule"],
|
|
819
|
-
discord_channel_id: payload["discord_channel_id"],
|
|
820
|
-
forum_title: payload["forum_title"],
|
|
821
|
-
forum_reply_to_latest: payload["forum_reply_to_latest"]
|
|
822
|
-
)
|
|
823
|
-
result.to_json
|
|
824
|
-
end
|
|
825
|
-
|
|
826
|
-
post "/api/cron/reload" do
|
|
827
|
-
content_type :json
|
|
828
|
-
reload_cron_jobs!
|
|
829
|
-
{ status: "reloaded", jobs: CRON_JOBS.size }.to_json
|
|
830
|
-
end
|
|
831
|
-
|
|
832
|
-
get "/api/cron/logs" do
|
|
833
|
-
content_type :json
|
|
834
|
-
job_id = params["id"]
|
|
835
|
-
halt 400, { error: "Missing ?id= parameter" }.to_json unless job_id && !job_id.empty?
|
|
836
|
-
|
|
837
|
-
logs = []
|
|
838
|
-
PROJECTS.each_value do |proj|
|
|
839
|
-
tmp_dir = File.join(proj["repo_path"], "tmp")
|
|
840
|
-
next unless Dir.exist?(tmp_dir)
|
|
841
|
-
|
|
842
|
-
Dir.glob(File.join(tmp_dir, "{agent-cron,cron-script}-#{job_id}-*.log")).each do |f|
|
|
843
|
-
logs << { file: f, size: File.size(f), modified: File.mtime(f).iso8601 }
|
|
844
|
-
end
|
|
845
|
-
end
|
|
846
|
-
logs.sort_by! { |l| l[:modified] }.reverse!
|
|
847
|
-
logs.first(20).to_json
|
|
848
|
-
end
|
|
849
|
-
|
|
850
|
-
get "/api/gif" do
|
|
851
|
-
content_type :json
|
|
852
|
-
query = params[:q].to_s.strip
|
|
853
|
-
halt 400, { error: "Missing ?q= parameter" }.to_json if query.empty?
|
|
854
|
-
|
|
855
|
-
api_key = DISCORD_CONFIG["giphy_api_key"]
|
|
856
|
-
halt 503, { error: "No giphy_api_key configured in discord.json" }.to_json unless api_key
|
|
857
|
-
|
|
858
|
-
begin
|
|
859
|
-
uri = URI("https://api.giphy.com/v1/gifs/search")
|
|
860
|
-
uri.query = URI.encode_www_form(api_key: api_key, q: query, limit: 5, rating: "pg-13")
|
|
861
|
-
response = Net::HTTP.get_response(uri)
|
|
862
|
-
|
|
863
|
-
if response.code.to_i == 200
|
|
864
|
-
results = JSON.parse(response.body)["data"] || []
|
|
865
|
-
gifs = results.map { |g| { url: g.dig("images", "original", "url") || g["url"], title: g["title"] } }
|
|
866
|
-
{ query: query, results: gifs }.to_json
|
|
867
|
-
else
|
|
868
|
-
LOG.warn "[GIF] Giphy API returned #{response.code}: #{response.body[0..200]}"
|
|
869
|
-
halt 502, { error: "Giphy API error: #{response.code}" }.to_json
|
|
870
|
-
end
|
|
871
|
-
rescue StandardError => e
|
|
872
|
-
LOG.error "[GIF] Search failed: #{e.message}"
|
|
873
|
-
halt 500, { error: e.message }.to_json
|
|
874
|
-
end
|
|
875
|
-
end
|
|
876
|
-
|
|
877
|
-
# --- Deployment environment tracking ---
|
|
878
|
-
|
|
879
|
-
get "/api/deployments" do
|
|
880
|
-
content_type :json
|
|
881
|
-
reload_deployments_config!
|
|
882
|
-
reload_deployment_state!
|
|
883
|
-
{ deployments: deployment_status }.to_json
|
|
884
|
-
end
|
|
885
|
-
|
|
886
|
-
post "/api/deployments/:env" do
|
|
887
|
-
content_type :json
|
|
888
|
-
env_key = params["env"]
|
|
889
|
-
request.body.rewind
|
|
890
|
-
payload = JSON.parse(request.body.read)
|
|
891
|
-
|
|
892
|
-
result = deploy_to_environment(env_key, worktree_path: payload["worktree"], deployed_by: payload["deployed_by"])
|
|
893
|
-
if result[:error]
|
|
894
|
-
halt 404, result.to_json
|
|
895
|
-
else
|
|
896
|
-
{ status: "deployed", env: env_key, deployment: result }.to_json
|
|
897
|
-
end
|
|
898
|
-
rescue JSON::ParserError
|
|
899
|
-
halt 400, { error: "Invalid JSON" }.to_json
|
|
900
|
-
end
|
|
901
|
-
|
|
902
|
-
delete "/api/deployments/:env" do
|
|
903
|
-
content_type :json
|
|
904
|
-
env_key = params["env"]
|
|
905
|
-
state = load_deployment_state
|
|
906
|
-
if state.key?(env_key)
|
|
907
|
-
state[env_key] = { "status" => "available", "cleared_at" => Time.now.iso8601, "last_card" => state[env_key]["card_number"] }
|
|
908
|
-
save_deployment_state(state)
|
|
909
|
-
DEPLOYMENT_STATE.replace(state)
|
|
910
|
-
LOG.info "[Deploy] Manually cleared #{env_key}"
|
|
911
|
-
{ status: "cleared", env: env_key }.to_json
|
|
912
|
-
else
|
|
913
|
-
halt 404, { error: "Unknown environment: #{env_key}" }.to_json
|
|
914
|
-
end
|
|
915
|
-
end
|
|
916
|
-
|
|
917
|
-
post "/api/deployments/:env/deploying" do
|
|
918
|
-
content_type :json
|
|
919
|
-
env_key = params["env"]
|
|
920
|
-
config = DEPLOYMENTS_CONFIG["environments"] || {}
|
|
921
|
-
halt 404, { error: "Unknown environment: #{env_key}" }.to_json unless config.key?(env_key)
|
|
922
|
-
request.body.rewind
|
|
923
|
-
payload = begin
|
|
924
|
-
JSON.parse(request.body.read)
|
|
925
|
-
rescue StandardError
|
|
926
|
-
{}
|
|
927
|
-
end
|
|
928
|
-
mark_deploying(env_key, worktree_path: payload["worktree"] || "")
|
|
929
|
-
LOG.info "[Deploy] #{env_key} marked deploying via API"
|
|
930
|
-
{ status: "deploying", env: env_key }.to_json
|
|
931
|
-
end
|
|
932
|
-
|
|
933
429
|
LOG.info "[Cron] Starting cron thread..."
|
|
934
430
|
start_cron_thread
|
|
935
431
|
|
|
@@ -944,9 +440,6 @@ CURATOR_THREAD = Thread.new do
|
|
|
944
440
|
end
|
|
945
441
|
end
|
|
946
442
|
|
|
947
|
-
LOG.info "[CardIndex] Starting background backfill..."
|
|
948
|
-
CARD_INDEX.backfill
|
|
949
|
-
|
|
950
443
|
LOG.info "[Monitor] Starting daemon..."
|
|
951
444
|
daemon_path = File.join(__dir__, "monitor", "daemon.rb")
|
|
952
445
|
daemon_pid_file = "/tmp/brainiac-daemon.pid"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"binary": "kiro-cli",
|
|
3
|
-
"default_args": "chat --trust-all-tools --no-interactive
|
|
3
|
+
"default_args": "chat --trust-all-tools --no-interactive",
|
|
4
4
|
"agent_flag": "--agent",
|
|
5
5
|
"model_flag": "--model",
|
|
6
6
|
"effort_flag": "--effort",
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
"models": {
|
|
10
10
|
"opus": "claude-opus-4.6",
|
|
11
11
|
"sonnet": "claude-sonnet-4.6",
|
|
12
|
-
"haiku": "claude-haiku-4.5"
|
|
12
|
+
"haiku": "claude-haiku-4.5",
|
|
13
|
+
"deepseek": "deepseek-3.2",
|
|
14
|
+
"minimax": "minimax-m2.5",
|
|
15
|
+
"minimax25": "minimax-m2.5",
|
|
16
|
+
"minimax21": "minimax-m2.1",
|
|
17
|
+
"qwen": "qwen3-coder-next",
|
|
18
|
+
"auto": "auto"
|
|
13
19
|
},
|
|
14
20
|
"efforts": ["low", "medium", "high", "xhigh", "max"]
|
|
15
21
|
}
|