brainiac-basecamp 0.0.17 → 0.0.18
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/lib/brainiac/plugins/basecamp/cli.rb +238 -8
- data/lib/brainiac/plugins/basecamp/comment_responder.rb +380 -0
- data/lib/brainiac/plugins/basecamp/epic_memory.rb +10 -0
- data/lib/brainiac/plugins/basecamp/hooks.rb +94 -270
- data/lib/brainiac/plugins/basecamp/orchestrator.rb +65 -187
- data/lib/brainiac/plugins/basecamp/review_gate.rb +165 -218
- data/lib/brainiac/plugins/basecamp/session_registry.rb +361 -0
- data/lib/brainiac/plugins/basecamp/task_state.rb +86 -0
- data/lib/brainiac/plugins/basecamp/version.rb +1 -1
- data/lib/brainiac/plugins/basecamp/webhook.rb +4 -6
- data/lib/brainiac/plugins/basecamp.rb +128 -492
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c082cb39df24b56239de1b12de0772b25088854641d633afa7352a035e63b721
|
|
4
|
+
data.tar.gz: 2139e2c80a63636e2f3e427b9b784a4237d5f5abf2e52e8018407081d3b6ad23
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16b4848a37f1d4a0b0e0a93940540cace70f020dd11c4cbad84410310f442cb6377a1bf7a8de0204326bf36c894a345aee88278908fffc5c2cd38ba0456b81d9
|
|
7
|
+
data.tar.gz: 2f2caa6a328819e0c27eb5f3b1da73b26e411b5a38f40d1bea2078ad4c3c9dfa1f381d25f83838675941a2cc06b599f50df99f75aec30bc024f891741d892d35
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "open3"
|
|
4
5
|
|
|
5
6
|
module Brainiac
|
|
6
7
|
module Plugins
|
|
@@ -32,11 +33,17 @@ module Brainiac
|
|
|
32
33
|
cmd_set(args)
|
|
33
34
|
when "reset"
|
|
34
35
|
cmd_reset(args)
|
|
36
|
+
when "webhook", "webhooks"
|
|
37
|
+
cmd_webhook(args)
|
|
35
38
|
else
|
|
36
39
|
print_help
|
|
37
40
|
end
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
# The canonical set of webhook event types this plugin needs.
|
|
44
|
+
# Add new types here as features are added — webhook sync will pick them up.
|
|
45
|
+
REQUIRED_WEBHOOK_TYPES = %w[Todo Todolist Comment].freeze
|
|
46
|
+
|
|
40
47
|
private
|
|
41
48
|
|
|
42
49
|
def cmd_setup
|
|
@@ -88,7 +95,8 @@ module Brainiac
|
|
|
88
95
|
puts " 2. Add bot accounts: brainiac basecamp bot add <name> <person-id> <agent>"
|
|
89
96
|
puts " 3. Map projects: brainiac basecamp projects map <brainiac-key> <basecamp-id>"
|
|
90
97
|
puts " 4. Set review gate: brainiac basecamp set review-gate <on_complete|on_pr_merge>"
|
|
91
|
-
puts " 5. Set up webhooks: basecamp webhooks create \"https://your-ngrok/basecamp\"
|
|
98
|
+
puts " 5. Set up webhooks: basecamp webhooks create \"https://your-ngrok/basecamp\" " \
|
|
99
|
+
"--types \"Todo,Todolist,Comment\" --in <project>"
|
|
92
100
|
puts " 6. Restart brainiac: brainiac restart"
|
|
93
101
|
end
|
|
94
102
|
|
|
@@ -217,11 +225,37 @@ module Brainiac
|
|
|
217
225
|
person_id = args.shift
|
|
218
226
|
agent = args.shift
|
|
219
227
|
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
# If person_id is omitted but agent is given, or only agent name given,
|
|
229
|
+
# try to auto-resolve from Basecamp
|
|
230
|
+
if name && !person_id
|
|
231
|
+
# Single arg: treat as agent name, auto-resolve
|
|
232
|
+
agent = name
|
|
233
|
+
person_id = auto_resolve_person_id(agent)
|
|
234
|
+
unless person_id
|
|
235
|
+
puts "❌ Could not find '#{agent}' in Basecamp people list."
|
|
236
|
+
puts " Try: basecamp people list --jq '.data[] | {id, name}'"
|
|
237
|
+
return
|
|
238
|
+
end
|
|
239
|
+
name = agent.downcase
|
|
240
|
+
elsif name && person_id && !agent
|
|
241
|
+
# Two args: name + agent, auto-resolve person_id
|
|
242
|
+
agent = person_id
|
|
243
|
+
person_id = auto_resolve_person_id(agent)
|
|
244
|
+
unless person_id
|
|
245
|
+
puts "❌ Could not find '#{agent}' in Basecamp people list."
|
|
246
|
+
puts " Provide the person_id manually:"
|
|
247
|
+
puts " brainiac basecamp bot add <name> <person-id> <agent>"
|
|
248
|
+
return
|
|
249
|
+
end
|
|
250
|
+
elsif !(name && person_id && agent)
|
|
251
|
+
puts "Usage: brainiac basecamp bot add <agent-name>"
|
|
252
|
+
puts " brainiac basecamp bot add <name> <agent-name>"
|
|
253
|
+
puts " brainiac basecamp bot add <name> <basecamp-person-id> <agent-name>"
|
|
222
254
|
puts ""
|
|
223
|
-
puts "
|
|
224
|
-
puts " brainiac basecamp bot add
|
|
255
|
+
puts "Examples:"
|
|
256
|
+
puts " brainiac basecamp bot add Galen # auto-resolves person_id"
|
|
257
|
+
puts " brainiac basecamp bot add andy-server Galen # auto-resolves person_id"
|
|
258
|
+
puts " brainiac basecamp bot add andy-server 52992796 Galen"
|
|
225
259
|
return
|
|
226
260
|
end
|
|
227
261
|
|
|
@@ -234,7 +268,68 @@ module Brainiac
|
|
|
234
268
|
save_config(config)
|
|
235
269
|
puts "✓ Added bot account '#{name}' (person_id: #{person_id}, agent: #{agent})"
|
|
236
270
|
|
|
237
|
-
when "
|
|
271
|
+
when "sync"
|
|
272
|
+
# Auto-discover agents from ~/.brainiac/agents.json and resolve their
|
|
273
|
+
# Basecamp person IDs. Creates bot_account entries for any that match.
|
|
274
|
+
config = load_config
|
|
275
|
+
config["bot_accounts"] ||= {}
|
|
276
|
+
|
|
277
|
+
agents = load_agents_registry
|
|
278
|
+
if agents.empty?
|
|
279
|
+
puts "No agents found in ~/.brainiac/agents.json"
|
|
280
|
+
return
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
puts "Syncing bot accounts from agents registry..."
|
|
284
|
+
puts "Found #{agents.size} agent(s): #{agents.map { |k, v| v['display_name'] || k }.join(', ')}"
|
|
285
|
+
puts ""
|
|
286
|
+
|
|
287
|
+
added = 0
|
|
288
|
+
updated = 0
|
|
289
|
+
not_found = 0
|
|
290
|
+
|
|
291
|
+
agents.each do |key, agent_config|
|
|
292
|
+
display_name = agent_config["display_name"] || key.capitalize
|
|
293
|
+
# Skip the meta "brainiac" agent
|
|
294
|
+
next if key == "brainiac"
|
|
295
|
+
|
|
296
|
+
resolved_id = auto_resolve_person_id(display_name)
|
|
297
|
+
|
|
298
|
+
if resolved_id
|
|
299
|
+
existing = config["bot_accounts"].find { |_k, v| v["default_agent"] == display_name }
|
|
300
|
+
|
|
301
|
+
if existing
|
|
302
|
+
_existing_key, existing_account = existing
|
|
303
|
+
if existing_account["person_id"].to_s == resolved_id.to_s
|
|
304
|
+
puts " \u00b7 #{display_name}: #{resolved_id} (unchanged)"
|
|
305
|
+
else
|
|
306
|
+
existing_account["person_id"] = resolved_id
|
|
307
|
+
updated += 1
|
|
308
|
+
puts " \u2191 #{display_name}: updated person_id \u2192 #{resolved_id}"
|
|
309
|
+
end
|
|
310
|
+
else
|
|
311
|
+
# Create new bot_account entry using agent key as the account name
|
|
312
|
+
config["bot_accounts"][key] = {
|
|
313
|
+
"person_id" => resolved_id,
|
|
314
|
+
"default_agent" => display_name
|
|
315
|
+
}
|
|
316
|
+
added += 1
|
|
317
|
+
puts " + #{display_name}: #{resolved_id} (new)"
|
|
318
|
+
end
|
|
319
|
+
else
|
|
320
|
+
puts " \u2717 #{display_name}: not found in Basecamp"
|
|
321
|
+
not_found += 1
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
if (added + updated).positive?
|
|
326
|
+
save_config(config)
|
|
327
|
+
puts ""
|
|
328
|
+
puts "\u2713 Added #{added}, updated #{updated} bot account(s)" if added.positive? || updated.positive?
|
|
329
|
+
else
|
|
330
|
+
puts "\nAll bot accounts already up to date."
|
|
331
|
+
end
|
|
332
|
+
puts " (#{not_found} agent(s) not found in Basecamp)" if not_found.positive?
|
|
238
333
|
config = load_config
|
|
239
334
|
bots = config["bot_accounts"] || {}
|
|
240
335
|
if bots.empty?
|
|
@@ -318,6 +413,102 @@ module Brainiac
|
|
|
318
413
|
end
|
|
319
414
|
end
|
|
320
415
|
|
|
416
|
+
def cmd_webhook(args)
|
|
417
|
+
action = args.shift
|
|
418
|
+
|
|
419
|
+
case action
|
|
420
|
+
when "sync"
|
|
421
|
+
cmd_webhook_sync
|
|
422
|
+
else
|
|
423
|
+
puts "Usage: brainiac basecamp webhook <command>"
|
|
424
|
+
puts ""
|
|
425
|
+
puts "Commands:"
|
|
426
|
+
puts " sync Ensure all project webhooks have the required event types"
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def cmd_webhook_sync
|
|
431
|
+
config = load_config
|
|
432
|
+
mappings = config["project_mappings"] || {}
|
|
433
|
+
|
|
434
|
+
if mappings.empty?
|
|
435
|
+
puts "No project mappings configured. Add one first:"
|
|
436
|
+
puts " brainiac basecamp projects map <key> <basecamp-project-id>"
|
|
437
|
+
return
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
updated = 0
|
|
441
|
+
skipped = 0
|
|
442
|
+
not_found = 0
|
|
443
|
+
|
|
444
|
+
mappings.each do |key, mapping|
|
|
445
|
+
project_id = mapping["basecamp_project_id"]
|
|
446
|
+
puts "Checking project '#{key}' (Basecamp #{project_id})..."
|
|
447
|
+
|
|
448
|
+
# List webhooks for this project
|
|
449
|
+
output, status = Open3.capture2("basecamp", "webhooks", "list", "--in", project_id.to_s, "--json")
|
|
450
|
+
unless status.success?
|
|
451
|
+
puts " ✗ Failed to list webhooks"
|
|
452
|
+
not_found += 1
|
|
453
|
+
next
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
data = JSON.parse(output)
|
|
457
|
+
webhooks = data["data"] || []
|
|
458
|
+
|
|
459
|
+
if webhooks.empty?
|
|
460
|
+
puts " ⚠ No webhooks found"
|
|
461
|
+
not_found += 1
|
|
462
|
+
next
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Find webhooks that point to a /basecamp endpoint (ours)
|
|
466
|
+
our_webhooks = webhooks.select { |wh| wh["payload_url"]&.include?("/basecamp") }
|
|
467
|
+
|
|
468
|
+
if our_webhooks.empty?
|
|
469
|
+
puts " ⚠ No webhooks with /basecamp endpoint found"
|
|
470
|
+
not_found += 1
|
|
471
|
+
next
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
our_webhooks.each do |wh|
|
|
475
|
+
current_types = wh["types"] || []
|
|
476
|
+
missing_types = REQUIRED_WEBHOOK_TYPES - current_types
|
|
477
|
+
|
|
478
|
+
if missing_types.empty?
|
|
479
|
+
puts " · Webhook #{wh['id']} (#{wh['payload_url']}): up to date"
|
|
480
|
+
skipped += 1
|
|
481
|
+
next
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
new_types = (current_types + missing_types).uniq
|
|
485
|
+
puts " ↑ Webhook #{wh['id']}: adding #{missing_types.join(', ')}"
|
|
486
|
+
|
|
487
|
+
_, stderr, update_status = Open3.capture3(
|
|
488
|
+
"basecamp", "webhooks", "update", wh["id"].to_s,
|
|
489
|
+
"--types", new_types.join(","),
|
|
490
|
+
"--in", project_id.to_s
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
if update_status.success?
|
|
494
|
+
puts " ✓ Updated: #{new_types.join(', ')}"
|
|
495
|
+
updated += 1
|
|
496
|
+
else
|
|
497
|
+
puts " ✗ Failed: #{stderr.strip}"
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
puts ""
|
|
503
|
+
if updated.positive?
|
|
504
|
+
puts "✓ Updated #{updated} webhook(s)"
|
|
505
|
+
elsif skipped.positive? && updated.zero?
|
|
506
|
+
puts "All webhooks already have required types: #{REQUIRED_WEBHOOK_TYPES.join(', ')}"
|
|
507
|
+
else
|
|
508
|
+
puts "No webhooks updated."
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
321
512
|
def print_help
|
|
322
513
|
puts <<~HELP
|
|
323
514
|
Usage: brainiac basecamp <command>
|
|
@@ -328,7 +519,9 @@ module Brainiac
|
|
|
328
519
|
status Check plugin status
|
|
329
520
|
epics [--all] List active epics (--all for completed too)
|
|
330
521
|
link <card> <url> Link a Fizzy card to a Basecamp todo
|
|
331
|
-
bot add <name>
|
|
522
|
+
bot add <agent-name> Add bot (auto-resolves person_id from Basecamp)
|
|
523
|
+
bot add <name> <person-id> <agent> Add bot with explicit person_id
|
|
524
|
+
bot sync Re-resolve all bot person IDs from Basecamp
|
|
332
525
|
bot list List bot accounts
|
|
333
526
|
bot remove <name> Remove a bot account
|
|
334
527
|
projects map <key> <basecamp-id> Map a Brainiac project to Basecamp
|
|
@@ -340,6 +533,7 @@ module Brainiac
|
|
|
340
533
|
reset task <card-number> [--to <status>] Reset a task to a given status (default: pending)
|
|
341
534
|
reset epic <todolist-id> [--to <status>] Reset entire epic or all tasks within it
|
|
342
535
|
reset gates <card-number> Clear gate approvals and re-dispatch gates
|
|
536
|
+
webhook sync Ensure webhooks have all required event types
|
|
343
537
|
|
|
344
538
|
Config file: ~/.brainiac/basecamp.json
|
|
345
539
|
Epics state: ~/.brainiac/basecamp_epics.json
|
|
@@ -679,6 +873,42 @@ module Brainiac
|
|
|
679
873
|
}))
|
|
680
874
|
end
|
|
681
875
|
|
|
876
|
+
def auto_resolve_person_id(agent_name)
|
|
877
|
+
output, status = Open3.capture2(
|
|
878
|
+
"basecamp", "people", "list", "--jq",
|
|
879
|
+
".data[] | select(.name | ascii_downcase | contains(\"#{agent_name.downcase}\")) | {id, name}"
|
|
880
|
+
)
|
|
881
|
+
return nil unless status.success? && !output.strip.empty?
|
|
882
|
+
|
|
883
|
+
# Parse each JSON line — prefer exact match
|
|
884
|
+
candidates = []
|
|
885
|
+
output.each_line do |line|
|
|
886
|
+
person = JSON.parse(line.strip)
|
|
887
|
+
candidates << person
|
|
888
|
+
rescue JSON::ParserError
|
|
889
|
+
next
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
# Exact name match first
|
|
893
|
+
exact = candidates.find { |p| p["name"]&.downcase == agent_name.downcase }
|
|
894
|
+
return exact["id"].to_s if exact
|
|
895
|
+
|
|
896
|
+
# Otherwise first contains-match
|
|
897
|
+
candidates.first&.dig("id")&.to_s
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# Load the agents registry from ~/.brainiac/agents.json
|
|
901
|
+
#
|
|
902
|
+
# @return [Hash] Agent key => config hash
|
|
903
|
+
def load_agents_registry
|
|
904
|
+
agents_file = File.join(BRAINIAC_DIR, "agents.json")
|
|
905
|
+
return {} unless File.exist?(agents_file)
|
|
906
|
+
|
|
907
|
+
JSON.parse(File.read(agents_file))
|
|
908
|
+
rescue JSON::ParserError
|
|
909
|
+
{}
|
|
910
|
+
end
|
|
911
|
+
|
|
682
912
|
def load_config
|
|
683
913
|
if File.exist?(CONFIG_FILE)
|
|
684
914
|
JSON.parse(File.read(CONFIG_FILE))
|
|
@@ -698,7 +928,7 @@ module Brainiac
|
|
|
698
928
|
end
|
|
699
929
|
|
|
700
930
|
def self.completions
|
|
701
|
-
%w[setup config status epics link bot projects set reset]
|
|
931
|
+
%w[setup config status epics link bot projects set reset webhook]
|
|
702
932
|
end
|
|
703
933
|
end
|
|
704
934
|
end
|