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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +136 -6
  4. data/bin/brainiac +472 -44
  5. data/bin/brainiac-completion.bash +1 -1
  6. data/lib/brainiac/agents.rb +27 -74
  7. data/lib/brainiac/brain.rb +6 -6
  8. data/lib/brainiac/config.rb +40 -76
  9. data/lib/brainiac/handlers/discord/api.rb +196 -0
  10. data/lib/brainiac/handlers/discord/config.rb +134 -0
  11. data/lib/brainiac/handlers/discord/delivery.rb +196 -0
  12. data/lib/brainiac/handlers/discord/gateway.rb +212 -0
  13. data/lib/brainiac/handlers/discord/message.rb +933 -0
  14. data/lib/brainiac/handlers/discord/reactions.rb +215 -0
  15. data/lib/brainiac/handlers/discord.rb +14 -1892
  16. data/lib/brainiac/handlers/github.rb +134 -317
  17. data/lib/brainiac/handlers/shared/git.rb +190 -0
  18. data/lib/brainiac/handlers/shared/inline_tags.rb +89 -0
  19. data/lib/brainiac/handlers/zoho.rb +103 -153
  20. data/lib/brainiac/helpers.rb +43 -455
  21. data/lib/brainiac/hooks.rb +86 -0
  22. data/lib/brainiac/plugins.rb +154 -0
  23. data/lib/brainiac/prompts.rb +34 -172
  24. data/lib/brainiac/restart.rb +112 -0
  25. data/lib/brainiac/routes/api.rb +411 -0
  26. data/lib/brainiac/users.rb +1 -7
  27. data/lib/brainiac/version.rb +1 -1
  28. data/lib/brainiac/zoho_mail_api.rb +2 -1
  29. data/lib/brainiac.rb +8 -1
  30. data/monitor/daemon.rb +4 -27
  31. data/monitor/shared.rb +247 -0
  32. data/monitor/{waybar-deploy-env.rb → waybar/deploy_env.rb} +17 -89
  33. data/monitor/waybar/setup.rb +232 -0
  34. data/monitor/waybar/status.rb +51 -0
  35. data/monitor/{view-logs-rofi.rb → waybar/view_logs.rb} +21 -88
  36. data/monitor/{deploy-env-macos.rb → xbar/deploy_env.rb} +3 -2
  37. data/monitor/xbar/plugin.rb +149 -0
  38. data/monitor/{setup-menubar.rb → xbar/setup.rb} +14 -30
  39. data/receiver.rb +44 -551
  40. data/templates/agents.json.example +1 -2
  41. data/templates/brainiac.json.example +8 -0
  42. data/templates/cli-providers/kiro.json.example +8 -2
  43. data/templates/plugins.json.example +3 -0
  44. data/templates/users.json.example +0 -3
  45. metadata +25 -23
  46. data/lib/brainiac/card_index.rb +0 -389
  47. data/lib/brainiac/deployments.rb +0 -258
  48. data/lib/brainiac/handlers/fizzy.rb +0 -1292
  49. data/lib/brainiac/planning.rb +0 -237
  50. data/lib/user_registry.rb +0 -159
  51. data/monitor/menubar.rb +0 -295
  52. data/monitor/setup-waybar-deploy-envs.rb +0 -121
  53. data/monitor/setup-waybar-deployments.rb +0 -96
  54. data/monitor/setup-waybar-module.rb +0 -113
  55. data/monitor/setup-xbar-plugin.rb +0 -35
  56. data/monitor/view-logs.rb +0 -119
  57. data/monitor/waybar-config-updater.rb +0 -56
  58. data/monitor/waybar-deployments.rb +0 -239
  59. data/monitor/waybar.rb +0 -146
  60. data/monitor/xbar.3s.rb +0 -179
  61. data/templates/fizzy.json.example +0 -24
  62. /data/monitor/{open-action.sh → xbar/open_action.sh} +0 -0
  63. /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/handlers/fizzy"
23
- require_relative "lib/brainiac/handlers/github"
24
- require_relative "lib/brainiac/card_index"
25
- require_relative "lib/brainiac/deployments"
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
- LOG.info "[Handlers] Loading custom handler: #{File.basename(handler)}"
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
- if DISCORD_ENABLED
54
- require_relative "lib/brainiac/handlers/discord"
55
- require_relative "lib/brainiac/handlers/zoho"
56
- end
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
- track_pr_in_card_map(payload)
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,9 +1,8 @@
1
1
  {
2
2
  "galen": {
3
- "fizzy_name": "Galen",
3
+ "display_name": "Galen",
4
4
  "local": true,
5
5
  "env": {
6
- "FIZZY_TOKEN": "your-fizzy-token",
7
6
  "DISCORD_BOT_TOKEN": "your-discord-bot-token"
8
7
  }
9
8
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "default_agent": "Galen",
3
+ "handlers": {
4
+ "github": true,
5
+ "discord": true,
6
+ "zoho": false
7
+ }
8
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "binary": "kiro-cli",
3
- "default_args": "chat --trust-all-tools --no-interactive --trust-tools execute_bash,fs_write,fs_read,code,grep,glob,web_search,web_fetch,use_subagent,use_aws",
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
  }