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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Admin and data API routes.
|
|
4
|
+
#
|
|
5
|
+
# These are read-only (mostly) endpoints for inspecting system state:
|
|
6
|
+
# projects, agents, users, brain, skills, sessions, logs, etc.
|
|
7
|
+
|
|
8
|
+
# --- Session Helpers ---
|
|
9
|
+
|
|
10
|
+
def reap_dead_sessions
|
|
11
|
+
ACTIVE_SESSIONS.delete_if do |card_key, info|
|
|
12
|
+
Process.kill(0, info[:pid])
|
|
13
|
+
false
|
|
14
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
15
|
+
archive_session(card_key, info)
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def format_active_sessions
|
|
21
|
+
ACTIVE_SESSIONS.map do |card_key, info|
|
|
22
|
+
agent_name = resolve_session_agent_name(card_key, info)
|
|
23
|
+
{
|
|
24
|
+
card_key: card_key, agent: agent_name, pid: info[:pid],
|
|
25
|
+
started_at: info[:started_at].iso8601,
|
|
26
|
+
elapsed_seconds: (Time.now - info[:started_at]).to_i,
|
|
27
|
+
log_file: info[:log_file], alive: true,
|
|
28
|
+
children: child_processes_for(info[:pid])
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def resolve_session_agent_name(card_key, info)
|
|
34
|
+
return info[:agent_name] if info[:agent_name]
|
|
35
|
+
|
|
36
|
+
parts = card_key.split("-")
|
|
37
|
+
agent_key = if parts[0] == "discord" && parts.size >= 4
|
|
38
|
+
parts[1]
|
|
39
|
+
else
|
|
40
|
+
"Unknown"
|
|
41
|
+
end
|
|
42
|
+
agent_display_name(agent_key)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_recent_sessions
|
|
46
|
+
RECENT_SESSIONS.map do |s|
|
|
47
|
+
{
|
|
48
|
+
card_key: s[:card_key], agent: s[:agent_name] || "Unknown",
|
|
49
|
+
log_file: s[:log_file], started_at: s[:started_at]&.iso8601,
|
|
50
|
+
finished_at: s[:finished_at]&.iso8601
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def kill_child_process(target_pid)
|
|
56
|
+
Process.kill("TERM", target_pid)
|
|
57
|
+
Thread.new do
|
|
58
|
+
sleep 3
|
|
59
|
+
begin
|
|
60
|
+
Process.kill(0, target_pid)
|
|
61
|
+
Process.kill("KILL", target_pid)
|
|
62
|
+
rescue Errno::ESRCH, Errno::EPERM # rubocop:disable Lint/SuppressedException
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
LOG.info "Killed child process #{target_pid} (SIGTERM)"
|
|
66
|
+
{ killed: target_pid }.to_json
|
|
67
|
+
rescue Errno::ESRCH
|
|
68
|
+
halt 404, { error: "process not found" }.to_json
|
|
69
|
+
rescue Errno::EPERM
|
|
70
|
+
halt 403, { error: "permission denied" }.to_json
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def search_giphy(query, api_key)
|
|
74
|
+
uri = URI("https://api.giphy.com/v1/gifs/search")
|
|
75
|
+
uri.query = URI.encode_www_form(api_key: api_key, q: query, limit: 5, rating: "pg-13")
|
|
76
|
+
response = Net::HTTP.get_response(uri)
|
|
77
|
+
|
|
78
|
+
if response.code.to_i == 200
|
|
79
|
+
results = JSON.parse(response.body)["data"] || []
|
|
80
|
+
results.map { |g| { url: g.dig("images", "original", "url") || g["url"], title: g["title"] } }
|
|
81
|
+
else
|
|
82
|
+
LOG.warn "[GIF] Giphy API returned #{response.code}: #{response.body[0..200]}"
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# --- Projects ---
|
|
88
|
+
|
|
89
|
+
get "/api/projects" do
|
|
90
|
+
content_type :json
|
|
91
|
+
reload_projects!
|
|
92
|
+
{ projects: PROJECTS }.to_json
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
get "/api/projects/:key" do
|
|
96
|
+
content_type :json
|
|
97
|
+
reload_projects!
|
|
98
|
+
project_key = params["key"]
|
|
99
|
+
if PROJECTS.key?(project_key)
|
|
100
|
+
{ project: PROJECTS[project_key] }.to_json
|
|
101
|
+
else
|
|
102
|
+
halt 404, { error: "Project not found" }.to_json
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
post "/api/reload" do
|
|
107
|
+
content_type :json
|
|
108
|
+
reload_projects!(force: true)
|
|
109
|
+
reload_agent_registry!(force: true)
|
|
110
|
+
reload_user_registry!(force: true)
|
|
111
|
+
reload_github_config!(force: true)
|
|
112
|
+
ReloadHooks.run_all!
|
|
113
|
+
{ status: "reloaded", projects: PROJECTS.keys, agents: all_agent_names.to_a, registry: AGENT_REGISTRY.keys,
|
|
114
|
+
users: USER_REGISTRY["users"].size }.to_json
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# --- Agents & Roles ---
|
|
118
|
+
|
|
119
|
+
get "/api/agents" do
|
|
120
|
+
content_type :json
|
|
121
|
+
{ default: AI_AGENT_NAME, agents: discover_kiro_agents, all_known: all_agent_names.to_a, roster: agent_roster }.to_json
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
get "/api/roles" do
|
|
125
|
+
content_type :json
|
|
126
|
+
roles = []
|
|
127
|
+
if Dir.exist?(ROLES_DIR)
|
|
128
|
+
Dir.glob(File.join(ROLES_DIR, "*.md")).each do |f|
|
|
129
|
+
name = File.basename(f, ".md")
|
|
130
|
+
agents = AGENT_REGISTRY.select { |_, e| e.is_a?(Hash) && Array(e["role"]).include?(name) }.map { |k, _e| agent_display_name(k) }
|
|
131
|
+
roles << { name: name, agents: agents }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
{ roles: roles, dir: ROLES_DIR }.to_json
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# --- Users ---
|
|
138
|
+
|
|
139
|
+
get "/api/users" do
|
|
140
|
+
content_type :json
|
|
141
|
+
reload_user_registry!
|
|
142
|
+
|
|
143
|
+
filter = params["filter"]
|
|
144
|
+
users = case filter
|
|
145
|
+
when "humans" then human_users
|
|
146
|
+
when "agents" then ai_agents
|
|
147
|
+
else USER_REGISTRY["users"]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
{ users: users, total: USER_REGISTRY["users"].size, schema_version: USER_REGISTRY["schema_version"] }.to_json
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
get "/api/users/:identifier" do
|
|
154
|
+
content_type :json
|
|
155
|
+
reload_user_registry!
|
|
156
|
+
|
|
157
|
+
identifier = params["identifier"]
|
|
158
|
+
user = find_user(identifier)
|
|
159
|
+
|
|
160
|
+
if user
|
|
161
|
+
{ user: user }.to_json
|
|
162
|
+
else
|
|
163
|
+
halt 404, { error: "User not found", identifier: identifier }.to_json
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# --- Brain ---
|
|
168
|
+
|
|
169
|
+
get "/api/brain" do
|
|
170
|
+
content_type :json
|
|
171
|
+
agent = params["agent"] || AI_AGENT_NAME
|
|
172
|
+
persona_dir = persona_dir_for(agent)
|
|
173
|
+
persona_col = persona_collection_for(agent)
|
|
174
|
+
|
|
175
|
+
knowledge_files = File.directory?(KNOWLEDGE_DIR) ? Dir.glob(File.join(KNOWLEDGE_DIR, "**", "*.md")).map { |f| f.sub("#{KNOWLEDGE_DIR}/", "") } : []
|
|
176
|
+
persona_files = File.directory?(persona_dir) ? Dir.glob(File.join(persona_dir, "**", "*.md")).map { |f| f.sub("#{persona_dir}/", "") } : []
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
agent: agent,
|
|
180
|
+
knowledge: { dir: KNOWLEDGE_DIR, collection: KNOWLEDGE_COLLECTION, files: knowledge_files },
|
|
181
|
+
persona: { dir: persona_dir, collection: persona_col, files: persona_files }
|
|
182
|
+
}.to_json
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
get "/api/brain/search" do
|
|
186
|
+
content_type :json
|
|
187
|
+
query = params["q"]
|
|
188
|
+
halt 400, { error: "Missing query parameter ?q=" }.to_json unless query && !query.empty?
|
|
189
|
+
|
|
190
|
+
agent = params["agent"] || AI_AGENT_NAME
|
|
191
|
+
scope = (params["scope"] || "knowledge").to_sym
|
|
192
|
+
scope = :knowledge unless %i[knowledge persona].include?(scope)
|
|
193
|
+
results = query_brain(query, agent_name: agent, scope: scope, max_results: (params["n"] || 5).to_i)
|
|
194
|
+
|
|
195
|
+
{ agent: agent, scope: scope, query: query, results: results }.to_json
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# --- Skills ---
|
|
199
|
+
|
|
200
|
+
get "/api/skills" do
|
|
201
|
+
content_type :json
|
|
202
|
+
skills = build_skill_index
|
|
203
|
+
{ total: skills.size, skills: skills }.to_json
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
post "/api/skills/curate" do
|
|
207
|
+
content_type :json
|
|
208
|
+
result = curate_skills
|
|
209
|
+
result.to_json
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# --- Dispatch Depth ---
|
|
213
|
+
|
|
214
|
+
get "/api/dispatch-depth" do
|
|
215
|
+
content_type :json
|
|
216
|
+
{
|
|
217
|
+
max_depth: AGENT_DISPATCH_MAX_DEPTH,
|
|
218
|
+
window_seconds: AGENT_DISPATCH_WINDOW,
|
|
219
|
+
cards: AGENT_DISPATCH_DEPTH.transform_values do |v|
|
|
220
|
+
{ count: v[:count], last_human_at: v[:last_human_at]&.iso8601, blocked: v[:count] >= AGENT_DISPATCH_MAX_DEPTH }
|
|
221
|
+
end
|
|
222
|
+
}.to_json
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# --- Sessions ---
|
|
226
|
+
|
|
227
|
+
get "/api/status" do
|
|
228
|
+
content_type :json
|
|
229
|
+
ACTIVE_SESSIONS_MUTEX.synchronize do
|
|
230
|
+
reap_dead_sessions
|
|
231
|
+
|
|
232
|
+
sessions = format_active_sessions
|
|
233
|
+
recent = format_recent_sessions
|
|
234
|
+
|
|
235
|
+
{ sessions: sessions, count: sessions.size, recent: recent, version: BRAINIAC_VERSION,
|
|
236
|
+
server_root: File.expand_path("../../..", __dir__) }.to_json
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
post "/api/sessions/kill/:card_key" do
|
|
241
|
+
content_type :json
|
|
242
|
+
card_key = params[:card_key]
|
|
243
|
+
halt 400, { error: "missing card_key" }.to_json if card_key.to_s.empty?
|
|
244
|
+
|
|
245
|
+
killed = kill_session(card_key)
|
|
246
|
+
halt 404, { error: "session not found" }.to_json unless killed
|
|
247
|
+
|
|
248
|
+
LOG.info "Killed agent session #{card_key} via API"
|
|
249
|
+
{ killed: card_key }.to_json
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
post "/api/sessions/kill-process/:pid" do
|
|
253
|
+
content_type :json
|
|
254
|
+
target_pid = params[:pid].to_i
|
|
255
|
+
halt 400, { error: "invalid pid" }.to_json if target_pid <= 0
|
|
256
|
+
|
|
257
|
+
valid = ACTIVE_SESSIONS_MUTEX.synchronize do
|
|
258
|
+
ACTIVE_SESSIONS.any? do |_, info|
|
|
259
|
+
child_processes_for(info[:pid]).any? { |c| c[:pid] == target_pid }
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
halt 403, { error: "pid is not a child of any active agent session" }.to_json unless valid
|
|
263
|
+
|
|
264
|
+
kill_child_process(target_pid)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# --- Logs ---
|
|
268
|
+
|
|
269
|
+
get "/api/logs" do
|
|
270
|
+
content_type "text/plain"
|
|
271
|
+
log_file = params["file"]
|
|
272
|
+
lines = (params["lines"] || 200).to_i
|
|
273
|
+
|
|
274
|
+
halt 400, "Missing ?file= parameter" unless log_file && !log_file.empty?
|
|
275
|
+
halt 400, "Invalid path" if log_file.include?("..") || !log_file.start_with?("/")
|
|
276
|
+
halt 404, "File not found" unless File.exist?(log_file)
|
|
277
|
+
|
|
278
|
+
allowed = PROJECTS.values.map { |p| File.join(p["repo_path"], "tmp") }
|
|
279
|
+
allowed << File.join(BRAINIAC_DIR, "tmp")
|
|
280
|
+
halt 403, "Forbidden" unless allowed.any? { |dir| log_file.start_with?(dir) }
|
|
281
|
+
|
|
282
|
+
all_lines = File.readlines(log_file).last(lines)
|
|
283
|
+
all_lines.join.gsub(/\e\[[\d;]*[a-zA-Z]/, "").gsub(/\e\[\?[\d;]*[a-zA-Z]/, "")
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# --- GIF ---
|
|
287
|
+
|
|
288
|
+
get "/api/gif" do
|
|
289
|
+
content_type :json
|
|
290
|
+
query = params[:q].to_s.strip
|
|
291
|
+
halt 400, { error: "Missing ?q= parameter" }.to_json if query.empty?
|
|
292
|
+
|
|
293
|
+
api_key = DISCORD_CONFIG["giphy_api_key"]
|
|
294
|
+
halt 503, { error: "No giphy_api_key configured in discord.json" }.to_json unless api_key
|
|
295
|
+
|
|
296
|
+
gifs = search_giphy(query, api_key)
|
|
297
|
+
halt 502, { error: "Giphy API error" }.to_json unless gifs
|
|
298
|
+
|
|
299
|
+
{ query: query, results: gifs }.to_json
|
|
300
|
+
rescue StandardError => e
|
|
301
|
+
LOG.error "[GIF] Search failed: #{e.message}"
|
|
302
|
+
halt 500, { error: e.message }.to_json
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# --- Cron ---
|
|
306
|
+
|
|
307
|
+
get "/api/cron/script" do
|
|
308
|
+
content_type "text/plain"
|
|
309
|
+
path = params["path"]
|
|
310
|
+
halt 400, "Missing ?path= parameter" unless path && !path.empty?
|
|
311
|
+
halt 400, "Invalid path" if path.include?("..")
|
|
312
|
+
|
|
313
|
+
valid = CRON_JOBS.values.any? do |j|
|
|
314
|
+
j[:script] == path || j["script"] == path ||
|
|
315
|
+
(j[:prompt] || j["prompt"] || "").include?(path)
|
|
316
|
+
end
|
|
317
|
+
halt 403, "Not a registered cron script" unless valid
|
|
318
|
+
halt 404, "File not found" unless File.exist?(path)
|
|
319
|
+
|
|
320
|
+
File.read(path)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
get "/api/cron" do
|
|
324
|
+
content_type :json
|
|
325
|
+
reload_cron_jobs!
|
|
326
|
+
{
|
|
327
|
+
enabled: true,
|
|
328
|
+
jobs: CRON_JOBS,
|
|
329
|
+
thread_alive: CRON_THREAD[:ref]&.alive? || false
|
|
330
|
+
}.to_json
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
post "/api/cron/add" do
|
|
334
|
+
content_type :json
|
|
335
|
+
request.body.rewind
|
|
336
|
+
payload = JSON.parse(request.body.read)
|
|
337
|
+
|
|
338
|
+
result = add_cron_job(
|
|
339
|
+
id: payload["id"],
|
|
340
|
+
schedule: payload["schedule"],
|
|
341
|
+
agent: payload["agent"],
|
|
342
|
+
project: payload["project"],
|
|
343
|
+
prompt: payload["prompt"],
|
|
344
|
+
script: payload["script"],
|
|
345
|
+
model: payload["model"],
|
|
346
|
+
effort: payload["effort"],
|
|
347
|
+
discord_channel_id: payload["discord_channel_id"],
|
|
348
|
+
forum_title: payload["forum_title"],
|
|
349
|
+
forum_reply_to_latest: payload["forum_reply_to_latest"] || false,
|
|
350
|
+
repeat_count: payload["repeat_count"]
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
result.to_json
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
post "/api/cron/remove" do
|
|
357
|
+
content_type :json
|
|
358
|
+
request.body.rewind
|
|
359
|
+
payload = JSON.parse(request.body.read)
|
|
360
|
+
|
|
361
|
+
result = remove_cron_job(payload["id"])
|
|
362
|
+
result.to_json
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
post "/api/cron/toggle" do
|
|
366
|
+
content_type :json
|
|
367
|
+
request.body.rewind
|
|
368
|
+
payload = JSON.parse(request.body.read)
|
|
369
|
+
|
|
370
|
+
result = toggle_cron_job(payload["id"], payload["enabled"])
|
|
371
|
+
result.to_json
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
post "/api/cron/update" do
|
|
375
|
+
content_type :json
|
|
376
|
+
request.body.rewind
|
|
377
|
+
payload = JSON.parse(request.body.read)
|
|
378
|
+
|
|
379
|
+
result = update_cron_job(
|
|
380
|
+
payload["id"],
|
|
381
|
+
schedule: payload["schedule"],
|
|
382
|
+
discord_channel_id: payload["discord_channel_id"],
|
|
383
|
+
forum_title: payload["forum_title"],
|
|
384
|
+
forum_reply_to_latest: payload["forum_reply_to_latest"]
|
|
385
|
+
)
|
|
386
|
+
result.to_json
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
post "/api/cron/reload" do
|
|
390
|
+
content_type :json
|
|
391
|
+
reload_cron_jobs!
|
|
392
|
+
{ status: "reloaded", jobs: CRON_JOBS.size }.to_json
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
get "/api/cron/logs" do
|
|
396
|
+
content_type :json
|
|
397
|
+
job_id = params["id"]
|
|
398
|
+
halt 400, { error: "Missing ?id= parameter" }.to_json unless job_id && !job_id.empty?
|
|
399
|
+
|
|
400
|
+
logs = []
|
|
401
|
+
PROJECTS.each_value do |proj|
|
|
402
|
+
tmp_dir = File.join(proj["repo_path"], "tmp")
|
|
403
|
+
next unless Dir.exist?(tmp_dir)
|
|
404
|
+
|
|
405
|
+
Dir.glob(File.join(tmp_dir, "{agent-cron,cron-script}-#{job_id}-*.log")).each do |f|
|
|
406
|
+
logs << { file: f, size: File.size(f), modified: File.mtime(f).iso8601 }
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
logs.sort_by! { |l| l[:modified] }.reverse!
|
|
410
|
+
logs.first(20).to_json
|
|
411
|
+
end
|
data/lib/brainiac/users.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# User identity registry - resolves identities across platforms
|
|
4
|
-
# (Discord, GitHub
|
|
4
|
+
# (Discord, GitHub)
|
|
5
5
|
|
|
6
6
|
USERS_FILE = File.join(BRAINIAC_DIR, "users.json")
|
|
7
7
|
|
|
@@ -40,11 +40,6 @@ def find_user_by_github_username(username)
|
|
|
40
40
|
USER_REGISTRY["users"].find { |u| u.dig("identities", "github", "username") == username.to_s }
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# Find user by Fizzy username
|
|
44
|
-
def find_user_by_fizzy_username(username)
|
|
45
|
-
USER_REGISTRY["users"].find { |u| u.dig("identities", "fizzy", "username") == username.to_s }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
43
|
# Find user by canonical name
|
|
49
44
|
def find_user_by_canonical_name(name)
|
|
50
45
|
USER_REGISTRY["users"].find { |u| u["canonical_name"].downcase == name.downcase }
|
|
@@ -55,7 +50,6 @@ def find_user(identifier)
|
|
|
55
50
|
find_user_by_discord_id(identifier) ||
|
|
56
51
|
find_user_by_discord_username(identifier) ||
|
|
57
52
|
find_user_by_github_username(identifier) ||
|
|
58
|
-
find_user_by_fizzy_username(identifier) ||
|
|
59
53
|
find_user_by_canonical_name(identifier)
|
|
60
54
|
end
|
|
61
55
|
|
data/lib/brainiac/version.rb
CHANGED
|
@@ -94,7 +94,8 @@ end
|
|
|
94
94
|
def extract_text_from_mime(mime)
|
|
95
95
|
# Try to find text/plain part
|
|
96
96
|
if mime =~ %r{Content-Type: text/plain[^\r\n]*\r?\n(?:Content-Transfer-Encoding:[^\r\n]*\r?\n)?(?:\r?\n)(.*?)(?:\r?\n------=_Part|\z)}mi
|
|
97
|
-
return Regexp.last_match(1).gsub("\r\n",
|
|
97
|
+
return Regexp.last_match(1).gsub("\r\n",
|
|
98
|
+
"\n").strip
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
# Fallback: extract text/html part and strip tags
|
data/lib/brainiac.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require_relative "brainiac/hooks"
|
|
1
2
|
require_relative "brainiac/config"
|
|
2
3
|
require_relative "brainiac/users"
|
|
3
4
|
require_relative "brainiac/agents"
|
|
@@ -5,6 +6,12 @@ require_relative "brainiac/brain"
|
|
|
5
6
|
require_relative "brainiac/skills"
|
|
6
7
|
require_relative "brainiac/sessions"
|
|
7
8
|
require_relative "brainiac/prompts"
|
|
8
|
-
require_relative "brainiac/planning"
|
|
9
9
|
require_relative "brainiac/helpers"
|
|
10
10
|
require_relative "brainiac/cron"
|
|
11
|
+
require_relative "brainiac/plugins"
|
|
12
|
+
|
|
13
|
+
# Namespace for gem-based plugins (brainiac-whatsapp, brainiac-slack, etc.)
|
|
14
|
+
module Brainiac
|
|
15
|
+
module Plugins
|
|
16
|
+
end
|
|
17
|
+
end
|
data/monitor/daemon.rb
CHANGED
|
@@ -2,32 +2,13 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
# Brainiac Monitor Daemon
|
|
5
|
-
# Polls /api/status and exposes agent state via Unix socket for waybar
|
|
6
|
-
#
|
|
5
|
+
# Polls /api/status and exposes agent state via Unix socket for waybar/xbar.
|
|
6
|
+
# Modules (waybar/status.rb, xbar/plugin.rb) read from this socket.
|
|
7
7
|
|
|
8
|
-
require "
|
|
9
|
-
|
|
10
|
-
require "socket"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
require_relative "shared"
|
|
11
10
|
|
|
12
|
-
SOCKET_PATH = "/tmp/brainiac-monitor.sock"
|
|
13
|
-
API_URL = "http://localhost:4567/api/status"
|
|
14
11
|
POLL_INTERVAL = 2 # seconds
|
|
15
|
-
CONFIG_PATH = File.expand_path("~/.brainiac/waybar.json")
|
|
16
|
-
|
|
17
|
-
# Load agent configuration from JSON
|
|
18
|
-
def load_agent_config
|
|
19
|
-
config = JSON.parse(File.read(CONFIG_PATH))
|
|
20
|
-
agents = {}
|
|
21
|
-
config["agents"].each do |agent|
|
|
22
|
-
agents[agent["name"]] = { color: agent["color"], emoji: agent["emoji"] }
|
|
23
|
-
end
|
|
24
|
-
agents
|
|
25
|
-
rescue StandardError => e
|
|
26
|
-
warn "Failed to load waybar.json: #{e.message}"
|
|
27
|
-
{}
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
AGENTS = load_agent_config.freeze
|
|
31
12
|
|
|
32
13
|
@state = { sessions: [], count: 0, recent: [], last_update: nil }
|
|
33
14
|
|
|
@@ -67,12 +48,10 @@ def start_server
|
|
|
67
48
|
server = UNIXServer.new(SOCKET_PATH)
|
|
68
49
|
File.chmod(0o666, SOCKET_PATH)
|
|
69
50
|
|
|
70
|
-
# Write PID file
|
|
71
51
|
File.write("/tmp/brainiac-daemon.pid", Process.pid)
|
|
72
52
|
|
|
73
53
|
puts "Monitor daemon started, socket: #{SOCKET_PATH}"
|
|
74
54
|
|
|
75
|
-
# Start polling thread
|
|
76
55
|
poller = Thread.new do
|
|
77
56
|
loop do
|
|
78
57
|
update_state
|
|
@@ -80,10 +59,8 @@ def start_server
|
|
|
80
59
|
end
|
|
81
60
|
end
|
|
82
61
|
|
|
83
|
-
# Initial state fetch
|
|
84
62
|
update_state
|
|
85
63
|
|
|
86
|
-
# Accept client connections
|
|
87
64
|
loop do
|
|
88
65
|
client = server.accept
|
|
89
66
|
Thread.new { handle_client(client) }
|