brainiac 0.0.9 → 0.0.10
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 -11
- data/bin/brainiac +946 -316
- data/bin/brainiac-completion.bash +93 -18
- data/brainiac.gemspec +2 -2
- data/lib/brainiac/config.rb +9 -21
- data/lib/brainiac/cron.rb +41 -26
- data/lib/brainiac/handlers/github.rb +3 -28
- data/lib/brainiac/handlers/zoho.rb +25 -33
- data/lib/brainiac/helpers.rb +6 -16
- data/lib/brainiac/notifications.rb +96 -0
- data/lib/brainiac/prompts.rb +2 -102
- data/lib/brainiac/restart.rb +2 -10
- data/lib/brainiac/routes/api.rb +2 -14
- data/lib/brainiac/version.rb +1 -1
- data/receiver.rb +9 -60
- data/skills/brainiac-plugins/SKILL.md +220 -0
- metadata +5 -25
- data/lib/brainiac/handlers/discord/api.rb +0 -196
- data/lib/brainiac/handlers/discord/config.rb +0 -134
- data/lib/brainiac/handlers/discord/delivery.rb +0 -196
- data/lib/brainiac/handlers/discord/gateway.rb +0 -212
- data/lib/brainiac/handlers/discord/message.rb +0 -921
- data/lib/brainiac/handlers/discord/reactions.rb +0 -215
- data/lib/brainiac/handlers/discord.rb +0 -24
- data/templates/discord.json.example +0 -17
data/bin/brainiac
CHANGED
|
@@ -220,6 +220,254 @@ def update_handler_config(name, enabled)
|
|
|
220
220
|
puts " Restart the server for changes to take effect: brainiac restart"
|
|
221
221
|
end
|
|
222
222
|
|
|
223
|
+
# After installing a plugin, load it and run its setup if it provides one.
|
|
224
|
+
# Only runs if the plugin defines .cli and responds to a "setup" subcommand via .configured?
|
|
225
|
+
def run_plugin_setup(name, local_path: nil)
|
|
226
|
+
gem_name = "brainiac-#{name}"
|
|
227
|
+
begin
|
|
228
|
+
if local_path
|
|
229
|
+
lib_path = File.join(local_path, "lib")
|
|
230
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
begin
|
|
234
|
+
require gem_name
|
|
235
|
+
rescue LoadError
|
|
236
|
+
require gem_name.tr("-", "_")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
pascal = name.split(/[-_]/).map(&:capitalize).join
|
|
240
|
+
mod = if defined?(Brainiac::Plugins) && Brainiac::Plugins.const_defined?(pascal)
|
|
241
|
+
Brainiac::Plugins.const_get(pascal)
|
|
242
|
+
else
|
|
243
|
+
Brainiac::Plugins.constants.find { |c| c.to_s.downcase == name.downcase }&.then { |c| Brainiac::Plugins.const_get(c) }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
return unless mod
|
|
247
|
+
|
|
248
|
+
# If plugin reports it's not configured, run setup automatically
|
|
249
|
+
if mod.respond_to?(:configured?) && !mod.configured? && mod.respond_to?(:cli)
|
|
250
|
+
puts ""
|
|
251
|
+
mod.cli(["setup"])
|
|
252
|
+
end
|
|
253
|
+
rescue LoadError
|
|
254
|
+
# Plugin couldn't be loaded — skip setup (will be caught on first use)
|
|
255
|
+
nil
|
|
256
|
+
rescue StandardError => e
|
|
257
|
+
puts " Note: Auto-setup skipped (#{e.message})"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Collect help text from installed plugins for the help command.
|
|
262
|
+
def collect_plugin_help_lines
|
|
263
|
+
plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
|
|
264
|
+
plugins_config = File.exist?(plugins_file) ? JSON.parse(File.read(plugins_file)) : { "plugins" => [] }
|
|
265
|
+
|
|
266
|
+
(plugins_config["plugins"] || []).filter_map do |pentry|
|
|
267
|
+
pname = pentry.is_a?(Hash) ? pentry["name"] : pentry.to_s
|
|
268
|
+
load_plugin_help(pname, pentry)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def load_plugin_help(pname, pentry)
|
|
273
|
+
if pentry.is_a?(Hash) && pentry["path"]
|
|
274
|
+
lib_path = File.join(pentry["path"], "lib")
|
|
275
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Try to load just the metadata file (lightweight, no runtime deps)
|
|
279
|
+
metadata_loaded = try_require_metadata(pname, pentry)
|
|
280
|
+
|
|
281
|
+
# Fall back to loading the full plugin if metadata file doesn't exist
|
|
282
|
+
unless metadata_loaded
|
|
283
|
+
gem_name = "brainiac-#{pname}"
|
|
284
|
+
begin
|
|
285
|
+
require gem_name
|
|
286
|
+
rescue LoadError
|
|
287
|
+
require gem_name.tr("-", "_")
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
mod = resolve_plugin_module_for(pname)
|
|
292
|
+
if mod.respond_to?(:help_text)
|
|
293
|
+
mod.help_text
|
|
294
|
+
elsif mod.respond_to?(:cli)
|
|
295
|
+
" #{pname.ljust(28)} Run 'brainiac #{pname} help' for commands"
|
|
296
|
+
end
|
|
297
|
+
rescue LoadError
|
|
298
|
+
" #{pname.ljust(28)} (could not load — reinstall with: brainiac install #{pname})"
|
|
299
|
+
rescue StandardError
|
|
300
|
+
nil
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def try_require_metadata(pname, pentry)
|
|
304
|
+
if pentry.is_a?(Hash) && pentry["path"]
|
|
305
|
+
metadata_file = File.join(pentry["path"], "lib", "brainiac", "plugins", pname, "metadata.rb")
|
|
306
|
+
if File.exist?(metadata_file)
|
|
307
|
+
load metadata_file
|
|
308
|
+
return true
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Try gem path via standard require
|
|
313
|
+
begin
|
|
314
|
+
require "brainiac/plugins/#{pname}/metadata"
|
|
315
|
+
true
|
|
316
|
+
rescue LoadError
|
|
317
|
+
false
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Load only the plugin's CLI module (metadata + cli) without the full server runtime.
|
|
322
|
+
# Returns true if successfully loaded, false if no CLI file exists.
|
|
323
|
+
def try_require_plugin_cli(pname, pentry)
|
|
324
|
+
plugin_dir = (File.join(pentry["path"], "lib", "brainiac", "plugins", pname) if pentry.is_a?(Hash) && pentry["path"])
|
|
325
|
+
|
|
326
|
+
# Load metadata first (defines module + help_text/configured?)
|
|
327
|
+
try_require_metadata(pname, pentry)
|
|
328
|
+
|
|
329
|
+
# Load CLI file
|
|
330
|
+
if plugin_dir
|
|
331
|
+
cli_file = File.join(plugin_dir, "cli.rb")
|
|
332
|
+
if File.exist?(cli_file)
|
|
333
|
+
load cli_file
|
|
334
|
+
return true
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Try gem path
|
|
339
|
+
begin
|
|
340
|
+
require "brainiac/plugins/#{pname}/cli"
|
|
341
|
+
true
|
|
342
|
+
rescue LoadError
|
|
343
|
+
false
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def resolve_plugin_module_for(name)
|
|
348
|
+
return nil unless defined?(Brainiac::Plugins)
|
|
349
|
+
|
|
350
|
+
pascal = name.split(/[-_]/).map(&:capitalize).join
|
|
351
|
+
if Brainiac::Plugins.const_defined?(pascal)
|
|
352
|
+
Brainiac::Plugins.const_get(pascal)
|
|
353
|
+
else
|
|
354
|
+
match = Brainiac::Plugins.constants.find { |c| c.to_s.downcase == name.downcase }
|
|
355
|
+
match ? Brainiac::Plugins.const_get(match) : nil
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Generate the plugin development SKILL.md content for the scaffold.
|
|
360
|
+
# Uses single-quoted heredoc to avoid interpolation issues with example code.
|
|
361
|
+
def generate_plugin_skill_md(plugin_name, pascal_name)
|
|
362
|
+
<<~SKILL.gsub("__PLUGIN_NAME__", plugin_name).gsub("__PASCAL_NAME__", pascal_name)
|
|
363
|
+
---
|
|
364
|
+
name: brainiac-plugin-development
|
|
365
|
+
description: |
|
|
366
|
+
How to develop Brainiac plugins. Covers the plugin contract, file structure,
|
|
367
|
+
hooks, CLI commands, and architecture constraints.
|
|
368
|
+
triggers:
|
|
369
|
+
- plugin contract
|
|
370
|
+
- plugin development
|
|
371
|
+
- register app
|
|
372
|
+
- brainiac hooks
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
# Brainiac Plugin Development
|
|
376
|
+
|
|
377
|
+
This is a Brainiac plugin gem. Plugins extend Brainiac with new communication
|
|
378
|
+
channels, integrations, or features without modifying core.
|
|
379
|
+
|
|
380
|
+
## Plugin Contract
|
|
381
|
+
|
|
382
|
+
A valid Brainiac plugin MUST:
|
|
383
|
+
|
|
384
|
+
1. **Gem named `brainiac-<name>`**
|
|
385
|
+
2. **Entry file at `lib/brainiac_<name>.rb`** — loaded by RubyGems
|
|
386
|
+
3. **Module at `Brainiac::Plugins::<Name>`** — PascalCase
|
|
387
|
+
4. **Implement `.register(app)`** — receives Sinatra::Application at server startup
|
|
388
|
+
|
|
389
|
+
Optional but recommended:
|
|
390
|
+
|
|
391
|
+
| Method | File | Purpose |
|
|
392
|
+
|--------|------|---------|
|
|
393
|
+
| `.register(app)` | main module | **Required.** Define routes, hooks, threads |
|
|
394
|
+
| `.configured?` | `metadata.rb` | Returns false → auto-runs setup on install |
|
|
395
|
+
| `.help_text` | `metadata.rb` | One-liner for `brainiac help` |
|
|
396
|
+
| `.cli(args)` | `cli.rb` | CLI subcommands via `brainiac <plugin> ...` |
|
|
397
|
+
|
|
398
|
+
## Critical Architecture: CLI vs Server Runtime
|
|
399
|
+
|
|
400
|
+
Plugins are loaded in TWO contexts:
|
|
401
|
+
|
|
402
|
+
1. **Server context** — full plugin via `.register(app)`. Has `LOG`, `AGENT_REGISTRY`, etc.
|
|
403
|
+
2. **CLI context** — only `metadata.rb` + `cli.rb`. NO server runtime.
|
|
404
|
+
|
|
405
|
+
Rules:
|
|
406
|
+
- `metadata.rb` requires only `version.rb`
|
|
407
|
+
- `cli.rb` uses only stdlib (`json`, `net/http`, `fileutils`)
|
|
408
|
+
- Everything else loaded only by `.register(app)`
|
|
409
|
+
|
|
410
|
+
## Available Hooks
|
|
411
|
+
|
|
412
|
+
Subscribe in `.register(app)` via `Brainiac.on(:event)`:
|
|
413
|
+
|
|
414
|
+
| Hook | When |
|
|
415
|
+
|------|------|
|
|
416
|
+
| `:server_started` | All plugins loaded |
|
|
417
|
+
| `:pre_dispatch` | Before agent CLI spawned |
|
|
418
|
+
| `:agent_completed` | Agent session finished |
|
|
419
|
+
| `:agent_crashed` | Agent process crashed |
|
|
420
|
+
| `:build_brain_context` | Building prompt context |
|
|
421
|
+
| `:pr_merged` | GitHub PR merged |
|
|
422
|
+
| `:pr_review_received` | PR review submitted |
|
|
423
|
+
| `:pr_synchronized` | PR updated |
|
|
424
|
+
| `:production_deployed` | Deploy succeeded |
|
|
425
|
+
| `:create_work_item` | Create card/issue/ticket |
|
|
426
|
+
| `:detect_cli_provider` | Detect CLI provider |
|
|
427
|
+
| `:detect_effort` | Detect effort level |
|
|
428
|
+
|
|
429
|
+
## Channel Prompts
|
|
430
|
+
|
|
431
|
+
For communication channel plugins:
|
|
432
|
+
|
|
433
|
+
```ruby
|
|
434
|
+
Brainiac.register_channel_prompt(:my_channel, PROMPT_TEXT, pre_post_check: CHECK_TEXT)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
## Core Functions (Server Context)
|
|
438
|
+
|
|
439
|
+
| Function | Purpose |
|
|
440
|
+
|----------|---------|
|
|
441
|
+
| `agent_display_name(key)` | Display name for agent |
|
|
442
|
+
| `agent_env_for(name)` | Env vars hash for agent |
|
|
443
|
+
| `AGENT_REGISTRY` | All agents |
|
|
444
|
+
| `PROJECTS` | All projects |
|
|
445
|
+
| `LOG` | Logger |
|
|
446
|
+
| `BRAINIAC_DIR` | ~/.brainiac/ path |
|
|
447
|
+
| `register_session(key, pid, **)` | Track active session |
|
|
448
|
+
| `session_active?(key)` | Check if session running |
|
|
449
|
+
| `build_brain_context(...)` | Build brain context |
|
|
450
|
+
| `render_prompt(template, vars, ...)` | Compose full prompt |
|
|
451
|
+
| `detect_model(config, text:)` | Detect model from tags |
|
|
452
|
+
| `parse_inline_tags(text)` | Parse inline tags |
|
|
453
|
+
| `reload_projects!` | Reload projects.json |
|
|
454
|
+
| `brain_push(message:)` | Push brain to git |
|
|
455
|
+
|
|
456
|
+
## Testing
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
rake test # Minitest
|
|
460
|
+
rake rubocop # Linter
|
|
461
|
+
rake # Both
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Reference Implementations
|
|
465
|
+
|
|
466
|
+
- `brainiac-discord` — Communication channel (gateway, messages, delivery, reactions)
|
|
467
|
+
- `brainiac-fizzy` — Card management (webhooks, hooks, duplicate detection, planning)
|
|
468
|
+
SKILL
|
|
469
|
+
end
|
|
470
|
+
|
|
223
471
|
def start_server(daemon: false)
|
|
224
472
|
# Resolve the real path of the brainiac script (follows symlinks)
|
|
225
473
|
receiver_path = File.join(BRAINIAC_ROOT, "receiver.rb")
|
|
@@ -632,7 +880,7 @@ when "setup"
|
|
|
632
880
|
puts ""
|
|
633
881
|
|
|
634
882
|
# Create directory structure
|
|
635
|
-
dirs = %w[brain/knowledge brain/persona brain/memory handlers plans tmp/
|
|
883
|
+
dirs = %w[brain/knowledge brain/persona brain/memory handlers plans tmp/notify/draft tmp/notify/posted]
|
|
636
884
|
dirs.each do |dir|
|
|
637
885
|
path = File.join(BRAINIAC_DIR, dir)
|
|
638
886
|
FileUtils.mkdir_p(path)
|
|
@@ -675,9 +923,7 @@ when "setup"
|
|
|
675
923
|
puts ""
|
|
676
924
|
|
|
677
925
|
available_handlers = {
|
|
678
|
-
|
|
679
926
|
"github" => "GitHub (PR reviews, CI failure handling, deploy tracking)",
|
|
680
|
-
"discord" => "Discord (conversational agent access via bot)",
|
|
681
927
|
"zoho" => "Zoho Mail (email notifications and triage)"
|
|
682
928
|
}
|
|
683
929
|
|
|
@@ -707,6 +953,42 @@ when "setup"
|
|
|
707
953
|
puts " 4. Initialize brain: brainiac brain init"
|
|
708
954
|
puts " 5. Start server: brainiac server"
|
|
709
955
|
|
|
956
|
+
# Shell completion setup
|
|
957
|
+
puts ""
|
|
958
|
+
completion_source = File.join(BRAINIAC_ROOT, "bin", "brainiac-completion.bash")
|
|
959
|
+
completion_target = File.join(BRAINIAC_DIR, "brainiac-completion.bash")
|
|
960
|
+
if File.exist?(completion_source)
|
|
961
|
+
FileUtils.cp(completion_source, completion_target)
|
|
962
|
+
|
|
963
|
+
# Detect shell rc file
|
|
964
|
+
shell = ENV.fetch("SHELL", "/bin/bash")
|
|
965
|
+
rc_file = case shell
|
|
966
|
+
when /zsh/ then File.expand_path("~/.zshrc")
|
|
967
|
+
when /fish/ then nil # fish uses different mechanism
|
|
968
|
+
else File.expand_path("~/.bashrc")
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
source_line = "[[ -r \"#{completion_target}\" ]] && source \"#{completion_target}\""
|
|
972
|
+
|
|
973
|
+
if rc_file && File.exist?(rc_file) && !File.read(rc_file).include?("brainiac-completion")
|
|
974
|
+
print "Add shell completion to #{rc_file}? [Y/n] "
|
|
975
|
+
answer = $stdin.gets&.strip&.downcase
|
|
976
|
+
if answer == "n"
|
|
977
|
+
puts " To add manually:"
|
|
978
|
+
puts " #{source_line}"
|
|
979
|
+
else
|
|
980
|
+
File.open(rc_file, "a") { |f| f.puts("\n# Brainiac CLI completion\n#{source_line}") }
|
|
981
|
+
puts "✓ Added completion to #{rc_file}"
|
|
982
|
+
end
|
|
983
|
+
elsif rc_file && !File.exist?(rc_file)
|
|
984
|
+
puts " Shell completion installed to: #{completion_target}"
|
|
985
|
+
puts " Add to your shell rc file:"
|
|
986
|
+
puts " #{source_line}"
|
|
987
|
+
else
|
|
988
|
+
puts "✓ Shell completion already configured"
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
|
|
710
992
|
when "register", "add"
|
|
711
993
|
OptionParser.new do |opts|
|
|
712
994
|
opts.banner = "Usage: brainiac register [options]"
|
|
@@ -785,7 +1067,7 @@ when "handler", "handlers"
|
|
|
785
1067
|
handlers = config["handlers"] || {}
|
|
786
1068
|
|
|
787
1069
|
# Built-in handlers
|
|
788
|
-
builtins = %w[github
|
|
1070
|
+
builtins = %w[github zoho]
|
|
789
1071
|
puts "Built-in handlers:"
|
|
790
1072
|
builtins.each do |name|
|
|
791
1073
|
enabled = handlers.fetch(name, true)
|
|
@@ -896,165 +1178,6 @@ when "status"
|
|
|
896
1178
|
when "path"
|
|
897
1179
|
puts BRAINIAC_DIR
|
|
898
1180
|
|
|
899
|
-
when "discord"
|
|
900
|
-
discord_cmd = ARGV.shift
|
|
901
|
-
discord_config_file = File.join(BRAINIAC_DIR, "discord.json")
|
|
902
|
-
agent_registry_file = File.join(BRAINIAC_DIR, "agents.json")
|
|
903
|
-
|
|
904
|
-
case discord_cmd
|
|
905
|
-
when "config"
|
|
906
|
-
if File.exist?(discord_config_file)
|
|
907
|
-
puts File.read(discord_config_file)
|
|
908
|
-
else
|
|
909
|
-
puts "No Discord config found at #{discord_config_file}"
|
|
910
|
-
puts "Create one with: brainiac discord map <channel-id> <project>"
|
|
911
|
-
puts "Or copy discord.json.example to #{discord_config_file}"
|
|
912
|
-
end
|
|
913
|
-
|
|
914
|
-
when "map"
|
|
915
|
-
channel_id = ARGV[0]
|
|
916
|
-
project_key = ARGV[1]
|
|
917
|
-
|
|
918
|
-
unless channel_id && project_key
|
|
919
|
-
puts "Usage: brainiac discord map <channel-id> <project-key>"
|
|
920
|
-
exit 1
|
|
921
|
-
end
|
|
922
|
-
|
|
923
|
-
config = File.exist?(discord_config_file) ? JSON.parse(File.read(discord_config_file)) : { "channel_mappings" => {} }
|
|
924
|
-
config["channel_mappings"] ||= {}
|
|
925
|
-
config["channel_mappings"][channel_id] = { "project" => project_key }
|
|
926
|
-
|
|
927
|
-
ensure_brainiac_dir
|
|
928
|
-
File.write(discord_config_file, JSON.pretty_generate(config))
|
|
929
|
-
puts "✓ Mapped channel #{channel_id} → project '#{project_key}'"
|
|
930
|
-
|
|
931
|
-
when "default"
|
|
932
|
-
project_key = ARGV[0]
|
|
933
|
-
|
|
934
|
-
unless project_key
|
|
935
|
-
puts "Usage: brainiac discord default <project-key>"
|
|
936
|
-
exit 1
|
|
937
|
-
end
|
|
938
|
-
|
|
939
|
-
config = File.exist?(discord_config_file) ? JSON.parse(File.read(discord_config_file)) : { "channel_mappings" => {} }
|
|
940
|
-
config["default_project"] = project_key
|
|
941
|
-
|
|
942
|
-
ensure_brainiac_dir
|
|
943
|
-
File.write(discord_config_file, JSON.pretty_generate(config))
|
|
944
|
-
puts "✓ Default project: #{project_key}"
|
|
945
|
-
|
|
946
|
-
when "token"
|
|
947
|
-
agent_key = ARGV[0]
|
|
948
|
-
token = ARGV[1]
|
|
949
|
-
|
|
950
|
-
unless agent_key && token
|
|
951
|
-
puts "Usage: brainiac discord token <agent-key> <bot-token>"
|
|
952
|
-
puts " Sets the DISCORD_BOT_TOKEN env var for an agent in the registry."
|
|
953
|
-
puts " Example: brainiac discord token galen Bot_TOKEN_HERE"
|
|
954
|
-
exit 1
|
|
955
|
-
end
|
|
956
|
-
|
|
957
|
-
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
958
|
-
registry[agent_key] ||= {}
|
|
959
|
-
registry[agent_key]["env"] ||= {}
|
|
960
|
-
registry[agent_key]["env"]["DISCORD_BOT_TOKEN"] = token
|
|
961
|
-
|
|
962
|
-
ensure_brainiac_dir
|
|
963
|
-
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
964
|
-
puts "✓ Set DISCORD_BOT_TOKEN for '#{agent_key}'"
|
|
965
|
-
|
|
966
|
-
when "status"
|
|
967
|
-
config = load_config
|
|
968
|
-
server_url = config["server_url"] || "http://localhost:4567"
|
|
969
|
-
begin
|
|
970
|
-
uri = URI("#{server_url}/api/discord")
|
|
971
|
-
response = Net::HTTP.get_response(uri)
|
|
972
|
-
data = JSON.parse(response.body)
|
|
973
|
-
if data["enabled"]
|
|
974
|
-
bots = data["bots"] || {}
|
|
975
|
-
if bots.empty?
|
|
976
|
-
puts "Discord: enabled but no bots configured"
|
|
977
|
-
else
|
|
978
|
-
puts "Discord bots:"
|
|
979
|
-
bots.each do |agent, info|
|
|
980
|
-
puts " #{agent}: #{info["status"]} (user_id: #{info["user_id"] || "n/a"})"
|
|
981
|
-
end
|
|
982
|
-
end
|
|
983
|
-
puts "Default project: #{data.dig("config", "default_project") || "none"}"
|
|
984
|
-
puts "Channel mappings: #{data.dig("config", "channel_mappings")}"
|
|
985
|
-
else
|
|
986
|
-
puts "Discord: disabled (#{data["reason"]})"
|
|
987
|
-
end
|
|
988
|
-
rescue StandardError => e
|
|
989
|
-
puts "Could not reach server at #{server_url}: #{e.message}"
|
|
990
|
-
puts "Is the server running? Check with: brainiac status"
|
|
991
|
-
end
|
|
992
|
-
|
|
993
|
-
when "agents"
|
|
994
|
-
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
995
|
-
agents_with_tokens = registry.select { |_k, v| v.is_a?(Hash) && (v.dig("env", "DISCORD_BOT_TOKEN") || v["discord_bot_token"]) }
|
|
996
|
-
if agents_with_tokens.empty?
|
|
997
|
-
puts "No agents have Discord bot tokens configured."
|
|
998
|
-
puts "Add one with: brainiac discord token <agent-key> <bot-token>"
|
|
999
|
-
else
|
|
1000
|
-
puts "Agents with Discord bots:"
|
|
1001
|
-
agents_with_tokens.each do |key, entry|
|
|
1002
|
-
display = entry["display_name"] || key.capitalize
|
|
1003
|
-
token = entry.dig("env", "DISCORD_BOT_TOKEN") || entry["discord_bot_token"]
|
|
1004
|
-
token_preview = "#{token[0..10]}..."
|
|
1005
|
-
puts " #{display} (#{key}): #{token_preview}"
|
|
1006
|
-
end
|
|
1007
|
-
end
|
|
1008
|
-
|
|
1009
|
-
when "owner"
|
|
1010
|
-
discord_id = ARGV[0]
|
|
1011
|
-
config = File.exist?(discord_config_file) ? JSON.parse(File.read(discord_config_file)) : {}
|
|
1012
|
-
if discord_id
|
|
1013
|
-
config["owner_discord_id"] = discord_id
|
|
1014
|
-
ensure_brainiac_dir
|
|
1015
|
-
File.write(discord_config_file, JSON.pretty_generate(config))
|
|
1016
|
-
puts "✓ Owner set to #{discord_id}"
|
|
1017
|
-
elsif config["owner_discord_id"]
|
|
1018
|
-
puts "Owner: #{config["owner_discord_id"]}"
|
|
1019
|
-
else
|
|
1020
|
-
puts "No owner set."
|
|
1021
|
-
puts "Usage: brainiac discord owner <discord-user-id>"
|
|
1022
|
-
end
|
|
1023
|
-
|
|
1024
|
-
else
|
|
1025
|
-
puts <<~HELP
|
|
1026
|
-
Usage: brainiac discord <command>
|
|
1027
|
-
|
|
1028
|
-
Commands:
|
|
1029
|
-
config Show Discord config
|
|
1030
|
-
default <project> Set default project for all channels
|
|
1031
|
-
map <channel-id> <project> Map a specific channel to a project
|
|
1032
|
-
owner [<discord-user-id>] Set/show machine owner (for version notifications)
|
|
1033
|
-
token <agent-key> <bot-token> Set Discord bot token for an agent
|
|
1034
|
-
agents List agents with Discord bot tokens
|
|
1035
|
-
status Check Discord bot status (via server API)
|
|
1036
|
-
|
|
1037
|
-
Each agent gets its own Discord bot. Users @mention @Galen or @GLaDOS
|
|
1038
|
-
directly in Discord — no shared bot needed.
|
|
1039
|
-
|
|
1040
|
-
Setup:
|
|
1041
|
-
1. Create a Discord bot per agent at https://discord.com/developers/applications
|
|
1042
|
-
2. Enable MESSAGE CONTENT intent in each bot's settings
|
|
1043
|
-
3. Invite each bot with permissions: Send Messages, Create Public Threads,
|
|
1044
|
-
Send Messages in Threads, Add Reactions, Read Message History
|
|
1045
|
-
4. Register each bot token:
|
|
1046
|
-
brainiac discord token galen "BOT_TOKEN_FOR_GALEN"
|
|
1047
|
-
brainiac discord token glados "BOT_TOKEN_FOR_GLADOS"
|
|
1048
|
-
5. Set a default project:
|
|
1049
|
-
brainiac discord default marketplace
|
|
1050
|
-
6. Start the server (all bots connect automatically):
|
|
1051
|
-
brainiac server
|
|
1052
|
-
|
|
1053
|
-
Config file: #{discord_config_file}
|
|
1054
|
-
Agent registry: #{agent_registry_file}
|
|
1055
|
-
HELP
|
|
1056
|
-
end
|
|
1057
|
-
|
|
1058
1181
|
when "work-items", "card-map"
|
|
1059
1182
|
work_items_file = File.join(BRAINIAC_DIR, "work_items.json")
|
|
1060
1183
|
cm_cmd = ARGV.shift
|
|
@@ -1185,7 +1308,8 @@ when "cron"
|
|
|
1185
1308
|
project = nil
|
|
1186
1309
|
model = nil
|
|
1187
1310
|
effort = nil
|
|
1188
|
-
|
|
1311
|
+
notify_channel = nil
|
|
1312
|
+
notify_target = nil
|
|
1189
1313
|
forum_title = nil
|
|
1190
1314
|
repeat_count = nil
|
|
1191
1315
|
prompt = nil
|
|
@@ -1198,10 +1322,11 @@ when "cron"
|
|
|
1198
1322
|
opts.on("-p", "--project KEY", "Project key") { |v| project = v }
|
|
1199
1323
|
opts.on("-m", "--model MODEL", "Model to use (opus, sonnet, haiku, auto)") { |v| model = v }
|
|
1200
1324
|
opts.on("-e", "--effort LEVEL", "Effort level (low, medium, high, xhigh, max)") { |v| effort = v }
|
|
1201
|
-
opts.on("-d", "--
|
|
1325
|
+
opts.on("-d", "--notify TARGET", "Notification target (channel ID, card number, etc.)") { |v| notify_target = v }
|
|
1326
|
+
opts.on("-c", "--channel CHANNEL", "Notification channel (discord, fizzy — default: discord)") { |v| notify_channel = v }
|
|
1202
1327
|
opts.on("-t", "--title TITLE", "Forum post title (for forum channels)") { |v| forum_title = v }
|
|
1203
1328
|
opts.on("-r", "--repeat COUNT", Integer, "Number of times to repeat (for recurring schedules)") { |v| repeat_count = v }
|
|
1204
|
-
opts.on("--script PATH", "Run script directly (no agent, output
|
|
1329
|
+
opts.on("--script PATH", "Run script directly (no agent, output posted via notify)") { |v| script = v }
|
|
1205
1330
|
end.parse!
|
|
1206
1331
|
|
|
1207
1332
|
prompt = ARGV.join(" ") unless script
|
|
@@ -1241,7 +1366,9 @@ when "cron"
|
|
|
1241
1366
|
payload[:script] = script if script
|
|
1242
1367
|
payload[:model] = model if model
|
|
1243
1368
|
payload[:effort] = effort if effort
|
|
1244
|
-
payload[:discord_channel_id] =
|
|
1369
|
+
payload[:discord_channel_id] = notify_target if notify_target && (!notify_channel || notify_channel == "discord")
|
|
1370
|
+
payload[:notify_channel] = notify_channel if notify_channel
|
|
1371
|
+
payload[:notify_target] = notify_target if notify_target
|
|
1245
1372
|
payload[:forum_title] = forum_title if forum_title
|
|
1246
1373
|
payload[:repeat_count] = repeat_count if repeat_count
|
|
1247
1374
|
req.body = payload.to_json
|
|
@@ -1256,8 +1383,8 @@ when "cron"
|
|
|
1256
1383
|
puts " Project: #{project}"
|
|
1257
1384
|
puts " Model: #{model || "default"}" unless script
|
|
1258
1385
|
puts " Effort: #{effort || "default"}" unless script
|
|
1259
|
-
puts "
|
|
1260
|
-
puts " Forum title: #{forum_title || "auto"}" if
|
|
1386
|
+
puts " Notify: #{notify_target ? "#{notify_channel || "discord"} → #{notify_target}" : "none"}"
|
|
1387
|
+
puts " Forum title: #{forum_title || "auto"}" if notify_target && forum_title
|
|
1261
1388
|
puts " Repeat: #{repeat_count ? "#{repeat_count} times" : "unlimited"}"
|
|
1262
1389
|
puts script ? " Script: #{script}" : " Prompt: #{prompt}"
|
|
1263
1390
|
else
|
|
@@ -1301,7 +1428,9 @@ when "cron"
|
|
|
1301
1428
|
puts " #{status} #{id}"
|
|
1302
1429
|
puts " Schedule: #{schedule_display}"
|
|
1303
1430
|
puts " Agent: #{job["script"] ? "none (script mode)" : job["agent"]} | Project: #{job["project"]}"
|
|
1304
|
-
|
|
1431
|
+
notify_ch = job["notify_channel"] || (job["discord_channel_id"] ? "discord" : nil)
|
|
1432
|
+
notify_tgt = job["notify_target"] || job["discord_channel_id"]
|
|
1433
|
+
puts " Notify: #{notify_ch} → #{notify_tgt}" if notify_tgt
|
|
1305
1434
|
puts job["script"] ? " Script: #{job["script"]}" : " Prompt: #{job["prompt"]}"
|
|
1306
1435
|
puts " Last run: #{last_run}"
|
|
1307
1436
|
puts
|
|
@@ -1390,22 +1519,23 @@ when "cron"
|
|
|
1390
1519
|
when "update"
|
|
1391
1520
|
id = ARGV[0]
|
|
1392
1521
|
schedule = nil
|
|
1393
|
-
|
|
1522
|
+
notify_target = nil
|
|
1523
|
+
notify_channel = nil
|
|
1394
1524
|
forum_title = nil
|
|
1395
1525
|
|
|
1396
1526
|
OptionParser.new do |opts|
|
|
1397
1527
|
opts.banner = "Usage: brainiac cron update <job-id> [options]"
|
|
1398
1528
|
opts.on("-s", "--schedule EXPR", "New cron expression") { |v| schedule = v }
|
|
1399
|
-
opts.on("-
|
|
1529
|
+
opts.on("-d", "--notify TARGET", "New notification target") { |v| notify_target = v }
|
|
1530
|
+
opts.on("-c", "--channel CHANNEL", "New notification channel (discord, fizzy)") { |v| notify_channel = v }
|
|
1400
1531
|
opts.on("-t", "--title TITLE", "New forum post title") { |v| forum_title = v }
|
|
1401
1532
|
end.parse!
|
|
1402
1533
|
|
|
1403
|
-
unless id && (schedule ||
|
|
1534
|
+
unless id && (schedule || notify_target || notify_channel || forum_title)
|
|
1404
1535
|
puts "Error: Missing required arguments"
|
|
1405
1536
|
puts "Usage: brainiac cron update <job-id> -s \"42 13 * * 1-5\""
|
|
1406
|
-
puts " or: brainiac cron update <job-id> -
|
|
1537
|
+
puts " or: brainiac cron update <job-id> -d \"channel-id\" -c discord"
|
|
1407
1538
|
puts " or: brainiac cron update <job-id> -t \"Daily Update\""
|
|
1408
|
-
puts " or: brainiac cron update <job-id> -s \"42 13 * * 1-5\" -c \"1423854179880927274\""
|
|
1409
1539
|
exit 1
|
|
1410
1540
|
end
|
|
1411
1541
|
|
|
@@ -1413,7 +1543,9 @@ when "cron"
|
|
|
1413
1543
|
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1414
1544
|
payload = { id: id }
|
|
1415
1545
|
payload[:schedule] = schedule if schedule
|
|
1416
|
-
payload[:discord_channel_id] =
|
|
1546
|
+
payload[:discord_channel_id] = notify_target if notify_target
|
|
1547
|
+
payload[:notify_channel] = notify_channel if notify_channel
|
|
1548
|
+
payload[:notify_target] = notify_target if notify_target
|
|
1417
1549
|
payload[:forum_title] = forum_title if forum_title
|
|
1418
1550
|
req.body = payload.to_json
|
|
1419
1551
|
|
|
@@ -1423,7 +1555,7 @@ when "cron"
|
|
|
1423
1555
|
if data["success"]
|
|
1424
1556
|
puts "✓ Updated cron job: #{id}"
|
|
1425
1557
|
puts " New schedule: #{schedule}" if schedule
|
|
1426
|
-
puts " New
|
|
1558
|
+
puts " New notify: #{notify_channel || "discord"} → #{notify_target}" if notify_target
|
|
1427
1559
|
puts " New forum title: #{forum_title}" if forum_title
|
|
1428
1560
|
else
|
|
1429
1561
|
puts "Error: #{data["error"]}"
|
|
@@ -2229,6 +2361,9 @@ when "install"
|
|
|
2229
2361
|
puts "✓ Registered plugin '#{name}' from #{local_path}"
|
|
2230
2362
|
puts " Restart the server to activate: brainiac restart"
|
|
2231
2363
|
puts " Note: Source changes take effect on next server restart."
|
|
2364
|
+
|
|
2365
|
+
# Auto-run setup if plugin defines it and isn't already configured
|
|
2366
|
+
run_plugin_setup(name, local_path: local_path)
|
|
2232
2367
|
else
|
|
2233
2368
|
# RubyGems mode
|
|
2234
2369
|
puts "Installing #{gem_name}..."
|
|
@@ -2259,6 +2394,9 @@ when "install"
|
|
|
2259
2394
|
puts ""
|
|
2260
2395
|
puts "✓ Installed plugin '#{name}' (#{gem_name})"
|
|
2261
2396
|
puts " Restart the server to activate: brainiac restart"
|
|
2397
|
+
|
|
2398
|
+
# Auto-run setup if plugin defines it and isn't already configured
|
|
2399
|
+
run_plugin_setup(name)
|
|
2262
2400
|
end
|
|
2263
2401
|
|
|
2264
2402
|
when "uninstall"
|
|
@@ -2356,151 +2494,643 @@ when "plugins"
|
|
|
2356
2494
|
puts " brainiac plugins List installed plugins"
|
|
2357
2495
|
end
|
|
2358
2496
|
|
|
2497
|
+
when "plugin"
|
|
2498
|
+
plugin_cmd = ARGV.shift
|
|
2499
|
+
|
|
2500
|
+
case plugin_cmd
|
|
2501
|
+
when "new"
|
|
2502
|
+
plugin_name = ARGV.shift
|
|
2503
|
+
unless plugin_name
|
|
2504
|
+
puts "Usage: brainiac plugin new <name>"
|
|
2505
|
+
puts ""
|
|
2506
|
+
puts "Generates a new Brainiac plugin gem skeleton."
|
|
2507
|
+
puts ""
|
|
2508
|
+
puts "Example:"
|
|
2509
|
+
puts " brainiac plugin new slack"
|
|
2510
|
+
puts " → Creates ~/Code/brainiac-slack/ with full gem structure"
|
|
2511
|
+
exit 1
|
|
2512
|
+
end
|
|
2513
|
+
|
|
2514
|
+
plugin_name = plugin_name.downcase.gsub(/[^a-z0-9-]/, "-").sub(/^brainiac-/, "")
|
|
2515
|
+
gem_name = "brainiac-#{plugin_name}"
|
|
2516
|
+
pascal_name = plugin_name.split(/[-_]/).map(&:capitalize).join
|
|
2517
|
+
snake_name = plugin_name.tr("-", "_")
|
|
2518
|
+
|
|
2519
|
+
# Determine output directory
|
|
2520
|
+
output_dir = ARGV.shift || File.join(Dir.pwd, gem_name)
|
|
2521
|
+
output_dir = File.expand_path(output_dir)
|
|
2522
|
+
|
|
2523
|
+
if Dir.exist?(output_dir) && !Dir.empty?(output_dir)
|
|
2524
|
+
puts "Error: Directory already exists and is not empty: #{output_dir}"
|
|
2525
|
+
exit 1
|
|
2526
|
+
end
|
|
2527
|
+
|
|
2528
|
+
puts "Creating #{gem_name} at #{output_dir}..."
|
|
2529
|
+
|
|
2530
|
+
# Create directory structure
|
|
2531
|
+
dirs = [
|
|
2532
|
+
"lib/brainiac/plugins/#{plugin_name}",
|
|
2533
|
+
"test",
|
|
2534
|
+
"skills/#{plugin_name}"
|
|
2535
|
+
]
|
|
2536
|
+
dirs.each { |d| FileUtils.mkdir_p(File.join(output_dir, d)) }
|
|
2537
|
+
|
|
2538
|
+
# version.rb
|
|
2539
|
+
File.write(File.join(output_dir, "lib", "brainiac", "plugins", plugin_name, "version.rb"), <<~RUBY)
|
|
2540
|
+
# frozen_string_literal: true
|
|
2541
|
+
|
|
2542
|
+
module Brainiac
|
|
2543
|
+
module Plugins
|
|
2544
|
+
module #{pascal_name}
|
|
2545
|
+
VERSION = "0.0.1"
|
|
2546
|
+
end
|
|
2547
|
+
end
|
|
2548
|
+
end
|
|
2549
|
+
RUBY
|
|
2550
|
+
|
|
2551
|
+
# metadata.rb
|
|
2552
|
+
File.write(File.join(output_dir, "lib", "brainiac", "plugins", plugin_name, "metadata.rb"), <<~RUBY)
|
|
2553
|
+
# frozen_string_literal: true
|
|
2554
|
+
|
|
2555
|
+
# Lightweight metadata — loaded by `brainiac help` without the full plugin runtime.
|
|
2556
|
+
|
|
2557
|
+
require_relative "version"
|
|
2558
|
+
|
|
2559
|
+
module Brainiac
|
|
2560
|
+
module Plugins
|
|
2561
|
+
module #{pascal_name}
|
|
2562
|
+
def self.configured?
|
|
2563
|
+
# TODO: Return true when the plugin has been configured
|
|
2564
|
+
false
|
|
2565
|
+
end
|
|
2566
|
+
|
|
2567
|
+
def self.help_text
|
|
2568
|
+
" brainiac #{plugin_name} <command>#{" " * [1, 20 - plugin_name.length].max}Manage #{pascal_name} integration"
|
|
2569
|
+
end
|
|
2570
|
+
end
|
|
2571
|
+
end
|
|
2572
|
+
end
|
|
2573
|
+
RUBY
|
|
2574
|
+
|
|
2575
|
+
# cli.rb
|
|
2576
|
+
File.write(File.join(output_dir, "lib", "brainiac", "plugins", plugin_name, "cli.rb"), <<~RUBY)
|
|
2577
|
+
# frozen_string_literal: true
|
|
2578
|
+
|
|
2579
|
+
require "json"
|
|
2580
|
+
|
|
2581
|
+
module Brainiac
|
|
2582
|
+
module Plugins
|
|
2583
|
+
module #{pascal_name}
|
|
2584
|
+
module Cli
|
|
2585
|
+
BRAINIAC_DIR = ENV.fetch("BRAINIAC_DIR", File.join(Dir.home, ".brainiac"))
|
|
2586
|
+
|
|
2587
|
+
class << self
|
|
2588
|
+
def run(args)
|
|
2589
|
+
command = args.shift
|
|
2590
|
+
|
|
2591
|
+
case command
|
|
2592
|
+
when "config"
|
|
2593
|
+
cmd_config
|
|
2594
|
+
when "setup"
|
|
2595
|
+
cmd_setup
|
|
2596
|
+
when "status"
|
|
2597
|
+
cmd_status
|
|
2598
|
+
else
|
|
2599
|
+
print_help
|
|
2600
|
+
end
|
|
2601
|
+
end
|
|
2602
|
+
|
|
2603
|
+
private
|
|
2604
|
+
|
|
2605
|
+
def cmd_config
|
|
2606
|
+
puts "TODO: Show #{plugin_name} configuration"
|
|
2607
|
+
end
|
|
2608
|
+
|
|
2609
|
+
def cmd_setup
|
|
2610
|
+
puts "#{pascal_name} Setup"
|
|
2611
|
+
puts "#{"=" * (pascal_name.length + 6)}"
|
|
2612
|
+
puts ""
|
|
2613
|
+
puts "TODO: Walk user through configuration"
|
|
2614
|
+
end
|
|
2615
|
+
|
|
2616
|
+
def cmd_status
|
|
2617
|
+
puts "TODO: Show #{plugin_name} status"
|
|
2618
|
+
end
|
|
2619
|
+
|
|
2620
|
+
def print_help
|
|
2621
|
+
puts <<~HELP
|
|
2622
|
+
Usage: brainiac #{plugin_name} <command>
|
|
2623
|
+
|
|
2624
|
+
Commands:
|
|
2625
|
+
config Show #{pascal_name} config
|
|
2626
|
+
setup Interactive setup guide
|
|
2627
|
+
status Check #{pascal_name} status via server API
|
|
2628
|
+
|
|
2629
|
+
Config file: #{File.join("~/.brainiac", "#{plugin_name}.json")}
|
|
2630
|
+
HELP
|
|
2631
|
+
end
|
|
2632
|
+
end
|
|
2633
|
+
end
|
|
2634
|
+
|
|
2635
|
+
def self.cli(args)
|
|
2636
|
+
Cli.run(args)
|
|
2637
|
+
end
|
|
2638
|
+
|
|
2639
|
+
# Subcommand names for bash completion.
|
|
2640
|
+
def self.completions
|
|
2641
|
+
%w[config setup status]
|
|
2642
|
+
end
|
|
2643
|
+
end
|
|
2644
|
+
end
|
|
2645
|
+
end
|
|
2646
|
+
RUBY
|
|
2647
|
+
|
|
2648
|
+
# Main plugin module
|
|
2649
|
+
File.write(File.join(output_dir, "lib", "brainiac", "plugins", "#{plugin_name}.rb"), <<~RUBY)
|
|
2650
|
+
# frozen_string_literal: true
|
|
2651
|
+
|
|
2652
|
+
require_relative "#{plugin_name}/version"
|
|
2653
|
+
require_relative "#{plugin_name}/metadata"
|
|
2654
|
+
require_relative "#{plugin_name}/cli"
|
|
2655
|
+
|
|
2656
|
+
module Brainiac
|
|
2657
|
+
module Plugins
|
|
2658
|
+
module #{pascal_name}
|
|
2659
|
+
class << self
|
|
2660
|
+
# Called by Brainiac plugin system during server startup.
|
|
2661
|
+
#
|
|
2662
|
+
# @param app [Sinatra::Application] The running Brainiac server
|
|
2663
|
+
def register(app)
|
|
2664
|
+
# TODO: Load config, register hooks, define routes, start threads
|
|
2665
|
+
setup_routes(app)
|
|
2666
|
+
|
|
2667
|
+
LOG.info "[#{pascal_name}] Plugin registered"
|
|
2668
|
+
end
|
|
2669
|
+
|
|
2670
|
+
private
|
|
2671
|
+
|
|
2672
|
+
def setup_routes(app)
|
|
2673
|
+
app.get "/api/#{plugin_name}" do
|
|
2674
|
+
content_type :json
|
|
2675
|
+
{ enabled: true }.to_json
|
|
2676
|
+
end
|
|
2677
|
+
|
|
2678
|
+
# TODO: Define webhook route
|
|
2679
|
+
# app.post "/#{plugin_name}" do
|
|
2680
|
+
# content_type :json
|
|
2681
|
+
# { status: "received" }.to_json
|
|
2682
|
+
# end
|
|
2683
|
+
end
|
|
2684
|
+
end
|
|
2685
|
+
end
|
|
2686
|
+
end
|
|
2687
|
+
end
|
|
2688
|
+
RUBY
|
|
2689
|
+
|
|
2690
|
+
# Entry point (loaded by RubyGems)
|
|
2691
|
+
File.write(File.join(output_dir, "lib", "brainiac_#{snake_name}.rb"), <<~RUBY)
|
|
2692
|
+
# frozen_string_literal: true
|
|
2693
|
+
|
|
2694
|
+
# Brainiac #{pascal_name} Plugin — entry point loaded by RubyGems.
|
|
2695
|
+
require_relative "brainiac/plugins/#{plugin_name}"
|
|
2696
|
+
RUBY
|
|
2697
|
+
|
|
2698
|
+
# Gemspec
|
|
2699
|
+
File.write(File.join(output_dir, "#{gem_name}.gemspec"), <<~RUBY)
|
|
2700
|
+
# frozen_string_literal: true
|
|
2701
|
+
|
|
2702
|
+
require_relative "lib/brainiac/plugins/#{plugin_name}/version"
|
|
2703
|
+
|
|
2704
|
+
Gem::Specification.new do |s|
|
|
2705
|
+
s.name = "#{gem_name}"
|
|
2706
|
+
s.version = Brainiac::Plugins::#{pascal_name}::VERSION
|
|
2707
|
+
s.summary = "#{pascal_name} plugin for Brainiac"
|
|
2708
|
+
s.description = "#{pascal_name} integration for Brainiac — TODO: describe what this plugin does."
|
|
2709
|
+
s.authors = ["Your Name"]
|
|
2710
|
+
s.homepage = "https://github.com/yourorg/#{gem_name}"
|
|
2711
|
+
s.license = "MIT"
|
|
2712
|
+
s.required_ruby_version = ">= 3.4"
|
|
2713
|
+
|
|
2714
|
+
s.files = Dir["lib/**/*.rb", "README.md", "LICENSE"]
|
|
2715
|
+
s.require_paths = ["lib"]
|
|
2716
|
+
|
|
2717
|
+
s.add_dependency "brainiac", ">= #{BRAINIAC_VERSION}"
|
|
2718
|
+
|
|
2719
|
+
s.add_development_dependency "minitest", "~> 5.25"
|
|
2720
|
+
s.add_development_dependency "rake", "~> 13.0"
|
|
2721
|
+
s.add_development_dependency "rubocop", "~> 1.75"
|
|
2722
|
+
s.add_development_dependency "rubocop-performance", "~> 1.25"
|
|
2723
|
+
|
|
2724
|
+
s.metadata["rubygems_mfa_required"] = "true"
|
|
2725
|
+
end
|
|
2726
|
+
RUBY
|
|
2727
|
+
|
|
2728
|
+
# Gemfile
|
|
2729
|
+
File.write(File.join(output_dir, "Gemfile"), <<~RUBY)
|
|
2730
|
+
# frozen_string_literal: true
|
|
2731
|
+
|
|
2732
|
+
source "https://rubygems.org"
|
|
2733
|
+
|
|
2734
|
+
gemspec
|
|
2735
|
+
RUBY
|
|
2736
|
+
|
|
2737
|
+
# Rakefile
|
|
2738
|
+
File.write(File.join(output_dir, "Rakefile"), <<~RUBY)
|
|
2739
|
+
# frozen_string_literal: true
|
|
2740
|
+
|
|
2741
|
+
require "rake/testtask"
|
|
2742
|
+
require "rubocop/rake_task"
|
|
2743
|
+
|
|
2744
|
+
Rake::TestTask.new(:test) do |t|
|
|
2745
|
+
t.libs << "test"
|
|
2746
|
+
t.libs << "lib"
|
|
2747
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
|
2748
|
+
end
|
|
2749
|
+
|
|
2750
|
+
RuboCop::RakeTask.new(:rubocop)
|
|
2751
|
+
|
|
2752
|
+
task default: %i[test rubocop]
|
|
2753
|
+
RUBY
|
|
2754
|
+
|
|
2755
|
+
# .rubocop.yml
|
|
2756
|
+
File.write(File.join(output_dir, ".rubocop.yml"), <<~YAML)
|
|
2757
|
+
plugins:
|
|
2758
|
+
- rubocop-performance
|
|
2759
|
+
|
|
2760
|
+
AllCops:
|
|
2761
|
+
TargetRubyVersion: 3.4
|
|
2762
|
+
NewCops: enable
|
|
2763
|
+
SuggestExtensions: false
|
|
2764
|
+
Exclude:
|
|
2765
|
+
- "vendor/**/*"
|
|
2766
|
+
- "tmp/**/*"
|
|
2767
|
+
|
|
2768
|
+
Metrics/MethodLength:
|
|
2769
|
+
Max: 75
|
|
2770
|
+
|
|
2771
|
+
Metrics/AbcSize:
|
|
2772
|
+
Max: 90
|
|
2773
|
+
|
|
2774
|
+
Metrics/ModuleLength:
|
|
2775
|
+
Max: 600
|
|
2776
|
+
|
|
2777
|
+
Metrics/ParameterLists:
|
|
2778
|
+
Max: 10
|
|
2779
|
+
CountKeywordArgs: false
|
|
2780
|
+
|
|
2781
|
+
Metrics/BlockLength:
|
|
2782
|
+
Exclude:
|
|
2783
|
+
- "test/**/*"
|
|
2784
|
+
- "*.gemspec"
|
|
2785
|
+
|
|
2786
|
+
Style/TopLevelMethodDefinition:
|
|
2787
|
+
Enabled: false
|
|
2788
|
+
|
|
2789
|
+
Style/StringLiterals:
|
|
2790
|
+
EnforcedStyle: double_quotes
|
|
2791
|
+
|
|
2792
|
+
Style/FrozenStringLiteralComment:
|
|
2793
|
+
Enabled: false
|
|
2794
|
+
|
|
2795
|
+
Style/ClassAndModuleChildren:
|
|
2796
|
+
Enabled: false
|
|
2797
|
+
|
|
2798
|
+
Style/Documentation:
|
|
2799
|
+
Enabled: false
|
|
2800
|
+
|
|
2801
|
+
Gemspec/DevelopmentDependencies:
|
|
2802
|
+
Enabled: false
|
|
2803
|
+
|
|
2804
|
+
Layout/LineLength:
|
|
2805
|
+
Max: 150
|
|
2806
|
+
|
|
2807
|
+
Naming/PredicateMethod:
|
|
2808
|
+
Enabled: false
|
|
2809
|
+
|
|
2810
|
+
Naming/FileName:
|
|
2811
|
+
Exclude:
|
|
2812
|
+
- "lib/brainiac/plugins/**/*"
|
|
2813
|
+
YAML
|
|
2814
|
+
|
|
2815
|
+
# Test helper
|
|
2816
|
+
File.write(File.join(output_dir, "test", "test_helper.rb"), <<~RUBY)
|
|
2817
|
+
# frozen_string_literal: true
|
|
2818
|
+
|
|
2819
|
+
require "minitest/autorun"
|
|
2820
|
+
require "json"
|
|
2821
|
+
require "fileutils"
|
|
2822
|
+
require "tmpdir"
|
|
2823
|
+
|
|
2824
|
+
TEST_BRAINIAC_DIR = Dir.mktmpdir("brainiac-#{plugin_name}-test")
|
|
2825
|
+
ENV["BRAINIAC_DIR"] = TEST_BRAINIAC_DIR
|
|
2826
|
+
|
|
2827
|
+
unless defined?(LOG)
|
|
2828
|
+
LOG = Class.new do
|
|
2829
|
+
def info(_msg) = nil
|
|
2830
|
+
def warn(_msg) = nil
|
|
2831
|
+
def error(_msg) = nil
|
|
2832
|
+
def debug(_msg) = nil
|
|
2833
|
+
def debug? = false
|
|
2834
|
+
end.new
|
|
2835
|
+
end
|
|
2836
|
+
|
|
2837
|
+
module Brainiac
|
|
2838
|
+
@hooks = Hash.new { |h, k| h[k] = [] }
|
|
2839
|
+
@channel_prompts = {}
|
|
2840
|
+
@channel_pre_post_checks = {}
|
|
2841
|
+
|
|
2842
|
+
class << self
|
|
2843
|
+
def on(event, &block) = @hooks[event] << block
|
|
2844
|
+
|
|
2845
|
+
def emit(event, **ctx)
|
|
2846
|
+
@hooks[event].filter_map do |h|
|
|
2847
|
+
h.call(ctx)
|
|
2848
|
+
rescue StandardError
|
|
2849
|
+
nil
|
|
2850
|
+
end
|
|
2851
|
+
end
|
|
2852
|
+
|
|
2853
|
+
def register_channel_prompt(channel, prompt, pre_post_check: nil)
|
|
2854
|
+
@channel_prompts[channel] = prompt
|
|
2855
|
+
@channel_pre_post_checks[channel] = pre_post_check if pre_post_check
|
|
2856
|
+
end
|
|
2857
|
+
|
|
2858
|
+
attr_reader :hooks, :channel_prompts, :channel_pre_post_checks
|
|
2859
|
+
|
|
2860
|
+
def reset_hooks!
|
|
2861
|
+
@hooks = Hash.new { |h, k| h[k] = [] }
|
|
2862
|
+
@channel_prompts = {}
|
|
2863
|
+
@channel_pre_post_checks = {}
|
|
2864
|
+
end
|
|
2865
|
+
end
|
|
2866
|
+
|
|
2867
|
+
module Plugins; end
|
|
2868
|
+
end
|
|
2869
|
+
|
|
2870
|
+
AGENT_REGISTRY = {}.freeze
|
|
2871
|
+
PROJECTS = {}.freeze
|
|
2872
|
+
|
|
2873
|
+
require_relative "../lib/brainiac_#{snake_name}"
|
|
2874
|
+
|
|
2875
|
+
Minitest.after_run { FileUtils.rm_rf(TEST_BRAINIAC_DIR) }
|
|
2876
|
+
RUBY
|
|
2877
|
+
|
|
2878
|
+
# Test file
|
|
2879
|
+
File.write(File.join(output_dir, "test", "test_#{snake_name}.rb"), <<~RUBY)
|
|
2880
|
+
# frozen_string_literal: true
|
|
2881
|
+
|
|
2882
|
+
require_relative "test_helper"
|
|
2883
|
+
|
|
2884
|
+
class Test#{pascal_name}Plugin < Minitest::Test
|
|
2885
|
+
def test_register_method_exists
|
|
2886
|
+
assert_respond_to Brainiac::Plugins::#{pascal_name}, :register
|
|
2887
|
+
end
|
|
2888
|
+
|
|
2889
|
+
def test_version_defined
|
|
2890
|
+
assert_match(/\\A\\d+\\.\\d+\\.\\d+\\z/, Brainiac::Plugins::#{pascal_name}::VERSION)
|
|
2891
|
+
end
|
|
2892
|
+
|
|
2893
|
+
def test_cli_method_exists
|
|
2894
|
+
assert_respond_to Brainiac::Plugins::#{pascal_name}, :cli
|
|
2895
|
+
end
|
|
2896
|
+
|
|
2897
|
+
def test_configured_returns_boolean
|
|
2898
|
+
result = Brainiac::Plugins::#{pascal_name}.configured?
|
|
2899
|
+
assert_includes [true, false], result
|
|
2900
|
+
end
|
|
2901
|
+
|
|
2902
|
+
def test_help_text_defined
|
|
2903
|
+
text = Brainiac::Plugins::#{pascal_name}.help_text
|
|
2904
|
+
assert_kind_of String, text
|
|
2905
|
+
assert_includes text, "brainiac #{plugin_name}"
|
|
2906
|
+
end
|
|
2907
|
+
end
|
|
2908
|
+
RUBY
|
|
2909
|
+
|
|
2910
|
+
# README
|
|
2911
|
+
File.write(File.join(output_dir, "README.md"), <<~MD)
|
|
2912
|
+
# #{gem_name}
|
|
2913
|
+
|
|
2914
|
+
#{pascal_name} plugin for [Brainiac](https://github.com/stowzilla/brainiac).
|
|
2915
|
+
|
|
2916
|
+
## Installation
|
|
2917
|
+
|
|
2918
|
+
```bash
|
|
2919
|
+
brainiac install #{plugin_name}
|
|
2920
|
+
brainiac restart
|
|
2921
|
+
```
|
|
2922
|
+
|
|
2923
|
+
Or for local development:
|
|
2924
|
+
|
|
2925
|
+
```bash
|
|
2926
|
+
brainiac install #{plugin_name} --path #{output_dir}
|
|
2927
|
+
brainiac restart
|
|
2928
|
+
```
|
|
2929
|
+
|
|
2930
|
+
## Configuration
|
|
2931
|
+
|
|
2932
|
+
```bash
|
|
2933
|
+
brainiac #{plugin_name} setup
|
|
2934
|
+
```
|
|
2935
|
+
|
|
2936
|
+
## Development
|
|
2937
|
+
|
|
2938
|
+
```bash
|
|
2939
|
+
bundle install
|
|
2940
|
+
rake test
|
|
2941
|
+
rake rubocop
|
|
2942
|
+
```
|
|
2943
|
+
|
|
2944
|
+
## License
|
|
2945
|
+
|
|
2946
|
+
MIT
|
|
2947
|
+
MD
|
|
2948
|
+
|
|
2949
|
+
# Plugin development SKILL.md — stamped into the scaffold so agents
|
|
2950
|
+
# working on this plugin know the contract without needing brain access.
|
|
2951
|
+
File.write(File.join(output_dir, "skills", plugin_name, "SKILL.md"),
|
|
2952
|
+
generate_plugin_skill_md(plugin_name, pascal_name))
|
|
2953
|
+
|
|
2954
|
+
puts ""
|
|
2955
|
+
puts "✓ Created #{gem_name} at #{output_dir}"
|
|
2956
|
+
puts ""
|
|
2957
|
+
puts "Next steps:"
|
|
2958
|
+
puts " cd #{output_dir}"
|
|
2959
|
+
puts " bundle install"
|
|
2960
|
+
puts " rake test # Verify scaffold works"
|
|
2961
|
+
puts " # Edit lib/brainiac/plugins/#{plugin_name}.rb to add your logic"
|
|
2962
|
+
puts " brainiac install #{plugin_name} --path #{output_dir}"
|
|
2963
|
+
puts " brainiac restart"
|
|
2964
|
+
|
|
2965
|
+
else
|
|
2966
|
+
puts "Usage: brainiac plugin <command>"
|
|
2967
|
+
puts ""
|
|
2968
|
+
puts "Commands:"
|
|
2969
|
+
puts " new <name> Generate a new plugin gem skeleton"
|
|
2970
|
+
puts ""
|
|
2971
|
+
puts "Managing installed plugins:"
|
|
2972
|
+
puts " brainiac install <name> Install a plugin"
|
|
2973
|
+
puts " brainiac uninstall <name> Remove a plugin"
|
|
2974
|
+
puts " brainiac plugins List installed plugins"
|
|
2975
|
+
end
|
|
2976
|
+
|
|
2359
2977
|
when "version", "--version", "-v"
|
|
2360
2978
|
puts "brainiac #{BRAINIAC_VERSION}"
|
|
2361
2979
|
|
|
2362
2980
|
when "help", "--help", "-h", nil
|
|
2981
|
+
# Load installed plugin help dynamically
|
|
2982
|
+
plugin_help_lines = collect_plugin_help_lines
|
|
2983
|
+
|
|
2984
|
+
plugin_section = if plugin_help_lines.any?
|
|
2985
|
+
"\n Installed Plugins:\n#{plugin_help_lines.join("\n")}\n"
|
|
2986
|
+
else
|
|
2987
|
+
""
|
|
2988
|
+
end
|
|
2989
|
+
|
|
2363
2990
|
puts <<~HELP
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
brainiac logs
|
|
2461
|
-
#{" "}
|
|
2462
|
-
# Restart the server
|
|
2463
|
-
brainiac restart
|
|
2464
|
-
#{" "}
|
|
2465
|
-
# Stop the server
|
|
2466
|
-
brainiac stop
|
|
2467
|
-
#{" "}
|
|
2468
|
-
# Check server status
|
|
2469
|
-
brainiac status
|
|
2470
|
-
#{" "}
|
|
2471
|
-
# Register current directory
|
|
2472
|
-
cd ~/Code/my-project && brainiac register
|
|
2473
|
-
#{" "}
|
|
2474
|
-
# Register with specific path
|
|
2475
|
-
brainiac register --path ~/Code/my-project
|
|
2476
|
-
#{" "}
|
|
2477
|
-
# List all projects
|
|
2478
|
-
brainiac list
|
|
2479
|
-
brainiac projects list # alternative syntax
|
|
2480
|
-
#{" "}
|
|
2481
|
-
# Set default project
|
|
2482
|
-
brainiac projects default my-project
|
|
2483
|
-
#{" "}
|
|
2484
|
-
# Unregister a project
|
|
2485
|
-
brainiac unregister my-project
|
|
2486
|
-
#{" "}
|
|
2487
|
-
# Show project details
|
|
2488
|
-
brainiac show my-project
|
|
2489
|
-
#{" "}
|
|
2490
|
-
# Initialize brain for your agent
|
|
2491
|
-
brainiac brain init Galen
|
|
2492
|
-
#{" "}
|
|
2493
|
-
# Search the brain
|
|
2494
|
-
brainiac brain search "ruby conventions"
|
|
2495
|
-
|
|
2496
|
-
Configuration:
|
|
2497
|
-
Projects are stored in: #{BRAINIAC_DIR}/projects.json
|
|
2498
|
-
Server config: #{BRAINIAC_DIR}/config.json
|
|
2499
|
-
Brain storage: #{BRAINIAC_DIR}/brain/
|
|
2991
|
+
Brainiac CLI - Manage projects with Brainiac server
|
|
2992
|
+
|
|
2993
|
+
Usage:
|
|
2994
|
+
brainiac setup Bootstrap a new machine (deps, dirs, configs)
|
|
2995
|
+
brainiac server [options] Start the Brainiac webhook server (foreground)
|
|
2996
|
+
brainiac stop Stop the running server
|
|
2997
|
+
brainiac restart Restart the server
|
|
2998
|
+
brainiac logs Tail the server log
|
|
2999
|
+
brainiac status Check if server is running
|
|
3000
|
+
brainiac register [options] Register current directory as a project
|
|
3001
|
+
brainiac unregister <key> Unregister a project
|
|
3002
|
+
brainiac list List all registered projects
|
|
3003
|
+
brainiac projects list List all registered projects (alias)
|
|
3004
|
+
brainiac projects default <key> Set the default project
|
|
3005
|
+
brainiac show <key> Show project configuration
|
|
3006
|
+
brainiac plugin new <name> Generate a new plugin gem skeleton
|
|
3007
|
+
brainiac install <plugin> Install a Brainiac plugin (gem-based handler)
|
|
3008
|
+
brainiac uninstall <plugin> Remove an installed plugin
|
|
3009
|
+
brainiac plugins List installed plugins
|
|
3010
|
+
brainiac brain <command> Manage agent long-term memory (brain)
|
|
3011
|
+
brainiac cron <command> Manage scheduled agent tasks
|
|
3012
|
+
brainiac provider <command> Manage CLI providers
|
|
3013
|
+
brainiac role <command> Manage agent roles
|
|
3014
|
+
brainiac agent <command> Manage agent registry (env, list, show)
|
|
3015
|
+
brainiac config Configure Brainiac CLI
|
|
3016
|
+
brainiac path Show Brainiac config directory
|
|
3017
|
+
brainiac version Show version
|
|
3018
|
+
brainiac help Show this help message
|
|
3019
|
+
#{plugin_section}
|
|
3020
|
+
Server Options:
|
|
3021
|
+
-d, --daemon Run server in background (detached)
|
|
3022
|
+
|
|
3023
|
+
Plugin Commands:
|
|
3024
|
+
plugin new <name> Generate a new plugin gem skeleton
|
|
3025
|
+
install <name> Install a plugin (gem install brainiac-<name>)
|
|
3026
|
+
uninstall <name> Remove a plugin
|
|
3027
|
+
plugins List installed plugins
|
|
3028
|
+
|
|
3029
|
+
Card Map Commands:
|
|
3030
|
+
card-map list Show all card map entries
|
|
3031
|
+
card-map clean Remove stale entries (missing worktrees)
|
|
3032
|
+
|
|
3033
|
+
Brain Commands:
|
|
3034
|
+
brain init [agent] Initialize brain (knowledge + persona)
|
|
3035
|
+
brain status [agent] Show brain status and files
|
|
3036
|
+
brain search [--persona] <q> Search knowledge or persona
|
|
3037
|
+
brain list List knowledge and all personas
|
|
3038
|
+
brain path Show knowledge directory
|
|
3039
|
+
|
|
3040
|
+
Cron Commands:
|
|
3041
|
+
cron add -s <schedule> -p <project> "<prompt>"
|
|
3042
|
+
Add a scheduled task
|
|
3043
|
+
cron list List all cron jobs
|
|
3044
|
+
cron remove <job-id> Remove a cron job
|
|
3045
|
+
cron enable <job-id> Enable a cron job
|
|
3046
|
+
cron disable <job-id> Disable a cron job
|
|
3047
|
+
cron update <job-id> -s <schedule>
|
|
3048
|
+
Update a cron job's schedule
|
|
3049
|
+
|
|
3050
|
+
Provider Commands:
|
|
3051
|
+
provider list List configured CLI providers
|
|
3052
|
+
provider show <name> Show provider configuration
|
|
3053
|
+
provider add <name> Create a new provider config
|
|
3054
|
+
|
|
3055
|
+
Role Commands:
|
|
3056
|
+
role list List configured roles and assignments
|
|
3057
|
+
role show <name> Show role definition
|
|
3058
|
+
role create <name> Create a new role file
|
|
3059
|
+
role assign <agent> <role> Assign a role to an agent (additive)
|
|
3060
|
+
role unassign <agent> <role> Remove a role from an agent
|
|
3061
|
+
|
|
3062
|
+
Agent Commands:
|
|
3063
|
+
agent list List all agents in the registry
|
|
3064
|
+
agent <name> show Show agent configuration (tokens redacted)
|
|
3065
|
+
agent <name> env <KEY> <VALUE> Set an env var for an agent
|
|
3066
|
+
agent <name> env List env vars for an agent
|
|
3067
|
+
agent <name> env --delete <KEY> Remove an env var from an agent
|
|
3068
|
+
|
|
3069
|
+
Examples:
|
|
3070
|
+
# Start the server in the foreground (like rails server)
|
|
3071
|
+
brainiac server
|
|
3072
|
+
brainiac s
|
|
3073
|
+
#{" "}
|
|
3074
|
+
# Register current directory
|
|
3075
|
+
cd ~/Code/my-project && brainiac register
|
|
3076
|
+
#{" "}
|
|
3077
|
+
# Install a plugin
|
|
3078
|
+
brainiac install discord
|
|
3079
|
+
#{" "}
|
|
3080
|
+
# List all projects
|
|
3081
|
+
brainiac list
|
|
3082
|
+
|
|
3083
|
+
Configuration:
|
|
3084
|
+
Projects are stored in: #{BRAINIAC_DIR}/projects.json
|
|
3085
|
+
Server config: #{BRAINIAC_DIR}/config.json
|
|
3086
|
+
Brain storage: #{BRAINIAC_DIR}/brain/
|
|
2500
3087
|
HELP
|
|
2501
3088
|
|
|
2502
3089
|
else
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
3090
|
+
# Check if the subcommand matches an installed plugin (e.g. "brainiac discord ...")
|
|
3091
|
+
plugins_file = File.join(BRAINIAC_DIR, "plugins.json")
|
|
3092
|
+
plugins_config = File.exist?(plugins_file) ? JSON.parse(File.read(plugins_file)) : { "plugins" => [] }
|
|
3093
|
+
(plugins_config["plugins"] || []).map { |p| p.is_a?(Hash) ? p["name"] : p.to_s }
|
|
3094
|
+
plugin_entry = (plugins_config["plugins"] || []).find { |p| (p.is_a?(Hash) ? p["name"] : p.to_s) == subcommand }
|
|
3095
|
+
|
|
3096
|
+
if plugin_entry
|
|
3097
|
+
# Delegate to the plugin's CLI module
|
|
3098
|
+
gem_name = "brainiac-#{subcommand}"
|
|
3099
|
+
begin
|
|
3100
|
+
if plugin_entry.is_a?(Hash) && plugin_entry["path"]
|
|
3101
|
+
lib_path = File.join(plugin_entry["path"], "lib")
|
|
3102
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
|
3103
|
+
end
|
|
3104
|
+
|
|
3105
|
+
# Try lightweight CLI loading first (metadata + cli only, no server runtime)
|
|
3106
|
+
cli_loaded = try_require_plugin_cli(subcommand, plugin_entry)
|
|
3107
|
+
|
|
3108
|
+
# Fall back to full gem load if no standalone CLI file exists
|
|
3109
|
+
unless cli_loaded
|
|
3110
|
+
begin
|
|
3111
|
+
require gem_name
|
|
3112
|
+
rescue LoadError
|
|
3113
|
+
require gem_name.tr("-", "_")
|
|
3114
|
+
end
|
|
3115
|
+
end
|
|
3116
|
+
|
|
3117
|
+
# Resolve the plugin module and call .cli(ARGV)
|
|
3118
|
+
mod = resolve_plugin_module_for(subcommand)
|
|
3119
|
+
|
|
3120
|
+
if mod.respond_to?(:cli)
|
|
3121
|
+
mod.cli(ARGV)
|
|
3122
|
+
else
|
|
3123
|
+
puts "Plugin '#{subcommand}' is installed but does not provide CLI commands."
|
|
3124
|
+
puts "It runs as a server plugin — use 'brainiac server' to activate it."
|
|
3125
|
+
end
|
|
3126
|
+
rescue LoadError => e
|
|
3127
|
+
puts "Plugin '#{subcommand}' is registered but could not be loaded: #{e.message}"
|
|
3128
|
+
puts "Try reinstalling: brainiac uninstall #{subcommand} && brainiac install #{subcommand}"
|
|
3129
|
+
exit 1
|
|
3130
|
+
end
|
|
3131
|
+
else
|
|
3132
|
+
puts "Unknown command: #{subcommand}"
|
|
3133
|
+
puts "Run 'brainiac help' for usage information."
|
|
3134
|
+
exit 1
|
|
3135
|
+
end
|
|
2506
3136
|
end
|