brainiac-basecamp 0.0.17 → 0.0.19

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 632f37b6a52ba851b1f16430cc9f2af1a34b0343adcdc486e950d78470d01eb8
4
- data.tar.gz: 7af23b5ee546eacb7b5d51b0dab4879dc642d7f0e8548ba4d94451e2aadc6518
3
+ metadata.gz: e7d5fd8ee1c180d8083b77c3324d138678276d8bbc471dfa48c51d43ae0cca06
4
+ data.tar.gz: ec08e6a76392e28d5ef6b6a88adac7712bb60887eca9643d02227f556c7d472a
5
5
  SHA512:
6
- metadata.gz: 4d69200fb71f7d04244f0bfcd3d1e8b9ecc97920cb9f1488a96a3fab6da51117af64dd4a47a0ba3a4f8857ce2c36fd5ec23efcf48e35efa5b34f29f86e3af72d
7
- data.tar.gz: c2253c409cdc64c02ec853dfca0623cc483ddd527b6f8135d9af9eb7ee256745a86a5992f998f6fcc73d10175593150f2288beb6437a4fc25e3c2ce7c278d8e1
6
+ metadata.gz: c82d88fd5a6cc8cf77330773b7eff552a3bfb7db291627ab0aef5dc3e8e178cee829389a0f72abd1938e4610c04878f3688e0963091a5361f6ae9dcea9f63b4c
7
+ data.tar.gz: 7ecb2081edefc0da28093bb5eb2b3cd26da316926349093bf51a36c7308073b5387279d3723a0a814c05f3a79dd7a4b17df07c7f479cc132f0fbeb75d5b64f88
@@ -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\" --types \"Todo,Todolist\" --in <project>"
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
- unless name && person_id && agent
221
- puts "Usage: brainiac basecamp bot add <name> <basecamp-person-id> <default-agent>"
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 "Example:"
224
- puts " brainiac basecamp bot add andy-server 12345 Galen"
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 "list"
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> <person-id> <agent> Add a bot account mapping
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