class RabbotMeta
RabbotMeta#run_plugin_build calls unregister_plugin_class on the plugin instance; delegate to the class method so enhance/reload registration succeeds.
Constants
- AGENT_MODEL
- AgentResult
- BUILD_PROGRESS_SNIPPET_LIMIT
- BUILD_PROGRESS_UPDATE_LIMIT
- BuildBackup
- BuildStats
- IRC_STYLE_GUIDANCE
- IRC_TAG
- MAX_DESCRIPTION_LENGTH
- MAX_FIX_ATTEMPTS
- MAX_QUIT_MESSAGE_LENGTH
- PLUGIN_ALIASES
- PROGRESS_GUTTER_MIN_LINES
- QUEUE_KICK_INTERVAL_SEC
- QUIT_ACK_DELAY
- RABBOT_ROOT
- SECURITY_GUIDANCE
- STYLE_TITLE
- THEME_GUIDANCE
- TWO_DAYS_SEC
- USAGE_KIND_STYLES
- USAGE_LABELS
Public Class Methods
Source
# File plugins/rabbot_meta.rb, line 579 def self.ai_cooldown_exempt?(network:, channel:, nick:, store: RabbotDb) RabbotMetaAiCooldown.exempt?(network: network, channel: channel, nick: nick, store: store) end
Source
# File plugins/rabbot_meta.rb, line 583 def self.ai_cooldown_exempt_nick?(nick) RabbotMetaAiCooldown.exempt_nick?(nick) end
Source
# File plugins/rabbot_meta.rb, line 591 def self.ai_guard_false_positive?(question) RabbotMetaAiGuardBypass.false_positive?(question) end
Source
# File plugins/rabbot_meta.rb, line 595 def self.ai_guard_should_grant_bypass?(question:) RabbotMetaAiGuardBypass.should_grant_bypass?(question: question) end
Source
# File plugins/rabbot_meta.rb, line 1001 def self.backup_from_h(data) BuildBackup.new( plugin_path: data["plugin_path"], test_path: data["test_path"], plugin_content: data["plugin_content"], test_content: data["test_content"], plugin_existed: data["plugin_existed"], test_existed: data["test_existed"], class_name: data["class_name"], related_files: data["related_files"].is_a?(Hash) ? data["related_files"] : {} ) end
Source
# File plugins/rabbot_meta.rb, line 984 def self.backup_storage_key(plugin_name) "build_backup:#{snake_case_name(plugin_name)}" end
Source
# File plugins/rabbot_meta.rb, line 988 def self.backup_to_h(backup) { "plugin_path" => backup.plugin_path, "test_path" => backup.test_path, "plugin_content" => backup.plugin_content, "test_content" => backup.test_content, "plugin_existed" => backup.plugin_existed, "test_existed" => backup.test_existed, "class_name" => backup.class_name, "related_files" => backup.related_files.is_a?(Hash) ? backup.related_files : {} } end
Source
# File plugins/rabbot_meta.rb, line 464 def self.botgroup_usage_message RabbotMetaBotgroupReport.format_usage end
Source
# File plugins/rabbot_meta.rb, line 166 def self.build_agent_args(prompt:, model:, workspace: RABBOT_ROOT, session_id: nil, mode: :plan) RabbotMetaAgent.build_args( prompt: prompt, model: model, workspace: workspace, session_id: session_id, mode: mode ) end
Source
# File plugins/rabbot_meta.rb, line 707 def self.build_enhance_prompt(plugin_name, plugin_path, test_path, description) rel_plugin = plugin_path.sub("#{RABBOT_ROOT}/", "") rel_test = test_path.sub("#{RABBOT_ROOT}/", "") <<~PROMPT Enhance an existing IRCinch plugin for the Rabbot IRC bot using test-driven development. Plugin: #{plugin_name} Plugin file: #{rel_plugin} Test file: #{rel_test} Enhancement request: #{description} Requirements: 1. TDD order: update #{rel_test} FIRST with tests for the new behavior, then modify #{rel_plugin} 2. Preserve existing behavior unless the request explicitly changes it 3. Extract core logic into testable methods (no IRC connection in tests) 4. Use FloodSafe + FloodSafeTheme; prefer themed_flood_safe_reply(m, text) for long or multi-line replies 5. Stay DRY — read lib/CONVENTIONS.md; reuse shared lib helpers instead of duplicating irc_color, HTTP fetch, or normalization 5b. #{IRC_STYLE_GUIDANCE} Match the plugin's existing style where possible; full guide: lib/CONVENTIONS.md § IRC presentation 5c. #{THEME_GUIDANCE} When enhancing styled output, migrate flood_safe_reply → themed_flood_safe_reply and add PaintSamples if missing Theme sprint checklist: docs/THEME_MIGRATION.md; column helpers: lib/reply_layout.rb, lib/text_align.rb 5d. Preserve or document responder scope in `# Help:` (`— responders: owner|room_reader|nick_targeted|all_loaded`). If not `all_loaded`, gate in `execute` with `BotIdentity`. See lib/CONVENTIONS.md § Responder scope. 5e. #{SECURITY_GUIDANCE} 5f. #{file_structure_guidance(enhance: true)} 6. Add any new gems to Gemfile if needed 7. Do NOT modify rabbot.rb or other plugins. Modify #{rel_plugin}, #{rel_test}, and lib/ files only when refactoring shared helpers into smaller modules 8. Run tests and fix until they pass before finishing 9. Print exactly on success: PLUGIN_FILE: #{rel_plugin} TEST_FILE: #{rel_test} PROMPT end
Source
# File plugins/rabbot_meta.rb, line 743 def self.build_fix_prompt(description, error_context, plugin_path, test_path, build_kind: :new) rel_plugin = plugin_path ? plugin_path.to_s.sub("#{RABBOT_ROOT}/", "") : "unknown" rel_test = test_path ? test_path.to_s.sub("#{RABBOT_ROOT}/", "") : "unknown" if build_kind.to_sym == :enhance <<~PROMPT Fix the failing Rabbot IRCinch plugin enhancement. Original request: #{description} Error: #{error_context} Plugin file: #{rel_plugin} Test file: #{rel_test} #{IRC_STYLE_GUIDANCE} #{THEME_GUIDANCE} #{file_structure_guidance(enhance: true)} Fix the code and tests until they pass. Do not modify rabbot.rb or other plugins. Modify #{rel_plugin}, #{rel_test}, and lib/ files only when refactoring shared helpers into smaller modules. Run tests and fix until they pass before finishing. Print exactly on success: PLUGIN_FILE: #{rel_plugin} TEST_FILE: #{rel_test} PROMPT else <<~PROMPT Fix the failing Rabbot IRCinch plugin. Original request: #{description} Error: #{error_context} Plugin file: #{plugin_path || "unknown"} Test file: #{test_path || "unknown"} #{IRC_STYLE_GUIDANCE} #{THEME_GUIDANCE} Fix the code and tests until they pass. Do not modify rabbot.rb, lib/, or other existing plugins. Run tests and fix until they pass before finishing. Print exactly on success: PLUGIN_FILE: plugins/<name>.rb TEST_FILE: test/plugins/<name>_test.rb PROMPT end end
Source
# File plugins/rabbot_meta.rb, line 176 def self.build_implement_prompt(task_prompt, plan_text:, admin_decisions: nil) decisions = admin_decisions.to_s.strip decision_block = if decisions.empty? "" else "Admin decisions:\n#{decisions}\n\n" end <<~PROMPT Implement the approved plugin build plan. Write and run tests until they pass — do not ask clarifying questions. Task: #{task_prompt.to_s.strip} #{decision_block}Approved plan: #{plan_text.to_s.strip} PROMPT end
Source
# File plugins/rabbot_meta.rb, line 660 def self.build_prompt(description) <<~PROMPT Create a new IRCinch plugin for the Rabbot IRC bot using test-driven development. User request: #{description} Requirements: 1. TDD order: write test/plugins/<name>_test.rb FIRST, then plugins/<name>.rb 2. Extract core logic into testable methods (no IRC connection in tests) 3. Follow plugins/google.rb structure: require "ircinch", include Cinch::Plugin, match + execute 4. Include FloodSafe and FloodSafeTheme; use themed_flood_safe_reply(m, text) when output may be long 5. Pick a sensible single-word command trigger for match() (e.g. duck for DuckDuckGo search) 5b. Add `# Help: short IRC-visible description` near the top of the plugin (after frozen_string_literal) for !help 5c. Stay DRY — read lib/CONVENTIONS.md; reuse IrcReply, IrcFormat, RabbotHttp, Normalize, ListCommand, RabbotDb instead of copying helpers 5d. #{IRC_STYLE_GUIDANCE} Common Rabbot pairs: title (0,4), win (0,3), nick (0,10), count (1,8), body (15), muted (15,14), sep (14) Full palette and patterns: lib/CONVENTIONS.md § IRC presentation 5e. #{THEME_GUIDANCE} Theme sprint checklist: docs/THEME_MIGRATION.md; column helpers: lib/reply_layout.rb, lib/text_align.rb 5f. Declare responder scope in `# Help:` (`— responders: owner|room_reader|nick_targeted|all_loaded`). If not `all_loaded`, gate in `execute` with `BotIdentity` (silent early return). See lib/CONVENTIONS.md § Responder scope. 5g. #{SECURITY_GUIDANCE} 5h. #{file_structure_guidance(enhance: false)} 6. Add any new gems to Gemfile if needed 7. Do NOT modify rabbot.rb or other existing plugins (only the new plugin, its lib/ modules, and tests) 8. Run tests and fix until they pass before finishing 9. Print exactly on success: PLUGIN_FILE: plugins/<name>.rb TEST_FILE: test/plugins/<name>_test.rb PROMPT end
Source
# File plugins/rabbot_meta.rb, line 1228 def self.build_resolve_paths(build_kind:, plugin_snapshot: nil, test_snapshot: nil) case build_kind.to_sym when :enhance lambda do |combined, canonical_plugin_path, canonical_test_path| marker_plugin = parse_marker(combined, "PLUGIN_FILE") marker_test = parse_marker(combined, "TEST_FILE") resolved_plugin = resolve_enhance_path(marker_plugin, canonical_plugin_path) resolved_test = resolve_enhance_path(marker_test, canonical_test_path) missing_plugin = "Agent finished but plugin file was not updated.\n#{combined.strip[0, 800]}" missing_test = "Plugin updated but test file was not found.\n#{combined.strip[0, 800]}" [resolved_plugin, resolved_test, missing_plugin, missing_test] end else snapshot_plugins = plugin_snapshot || snapshot_files(File.join(RABBOT_ROOT, "plugins", "*.rb")) snapshot_tests = test_snapshot || snapshot_files(File.join(RABBOT_ROOT, "test", "plugins", "*_test.rb")) lambda do |combined, plugin_path, test_path| plugin_path = parse_marker(combined, "PLUGIN_FILE") test_path = parse_marker(combined, "TEST_FILE") new_plugins = snapshot_files(File.join(RABBOT_ROOT, "plugins", "*.rb")) - snapshot_plugins new_tests = snapshot_files(File.join(RABBOT_ROOT, "test", "plugins", "*_test.rb")) - snapshot_tests plugin_path = File.expand_path(plugin_path, RABBOT_ROOT) if plugin_path test_path = File.expand_path(test_path, RABBOT_ROOT) if test_path plugin_path ||= new_plugins.first test_path ||= new_tests.first missing_plugin = "Agent finished but no new plugin file was created.\n#{combined.strip[0, 800]}" missing_test = "Plugin file created but no test file found.\n#{combined.strip[0, 800]}" [plugin_path, test_path, missing_plugin, missing_test] end end end
Source
# File plugins/rabbot_meta.rb, line 342 def self.build_state_from_h(data) stats_data = data["stats"].is_a?(Hash) ? data["stats"] : {} stats = BuildStats.new( duration_ms: stats_data["duration_ms"].to_i, input_tokens: stats_data["input_tokens"].to_i, output_tokens: stats_data["output_tokens"].to_i, attempts: stats_data["attempts"].to_i ) backup = data["backup"].is_a?(Hash) ? backup_from_h(data["backup"]) : nil { build_kind: data["build_kind"].to_s, description: data["description"].to_s, progress_label: data["progress_label"].to_s, attempt: data["attempt"].to_i, plugin_path: data["plugin_path"], test_path: data["test_path"], plugin_name: data["plugin_name"].to_s, undo_plugin: data["undo_plugin"].to_s, backup: backup, stats: stats, initial_prompt: data["initial_prompt"].to_s, plugin_snapshot: data["plugin_snapshot"].to_a.map(&:to_s).to_set, test_snapshot: data["test_snapshot"].to_a.map(&:to_s).to_set } end
Source
# File plugins/rabbot_meta.rb, line 317 def self.build_state_to_h(build_kind:, description:, progress_label:, attempt:, stats:, plugin_path: nil, test_path: nil, plugin_name: nil, undo_plugin: nil, backup: nil, initial_prompt: nil, plugin_snapshot: nil, test_snapshot: nil) { "build_kind" => build_kind.to_s, "description" => description.to_s, "progress_label" => progress_label.to_s, "attempt" => attempt.to_i, "plugin_path" => plugin_path, "test_path" => test_path, "plugin_name" => plugin_name.to_s, "undo_plugin" => undo_plugin.to_s, "backup" => backup ? backup_to_h(backup) : nil, "stats" => { "duration_ms" => stats.duration_ms.to_i, "input_tokens" => stats.input_tokens.to_i, "output_tokens" => stats.output_tokens.to_i, "attempts" => stats.attempts.to_i }, "initial_prompt" => initial_prompt.to_s, "plugin_snapshot" => Array(plugin_snapshot).map(&:to_s), "test_snapshot" => Array(test_snapshot).map(&:to_s) } end
Source
# File plugins/rabbot_meta.rb, line 957 def self.capture_build_backup(plugin_path, test_path) plugin_name = File.basename(plugin_path.to_s, ".rb") BuildBackup.new( plugin_path: File.expand_path(plugin_path), test_path: File.expand_path(test_path), plugin_content: read_file_if_exists(plugin_path), test_content: read_file_if_exists(test_path), plugin_existed: File.file?(plugin_path), test_existed: File.file?(test_path), class_name: class_name_from_file(plugin_path), related_files: snapshot_related_files(plugin_name) ) end
Source
# File plugins/rabbot_meta.rb, line 122 def self.chunk_delay_sec_for(logical_lines:) RabbotMetaFlood.chunk_delay_sec_for(logical_lines: logical_lines) end
Source
# File plugins/rabbot_meta.rb, line 798 def self.class_name_from_file(path) content = File.read(path) return Regexp.last_match(1) if content =~ /class\s+(\w+)/ end
Source
# File plugins/rabbot_meta.rb, line 1032 def self.clear_build_backup(plugin_name) RabbotDb.set_plugin_value( plugin: "rabbot_meta", key: backup_storage_key(plugin_name), value: "" ) end
Source
# File plugins/rabbot_meta.rb, line 427 def self.colors_usage_message "Usage: !rabbot colors [code|fg,bg|group|pair|pairs|themes|theme <id>|spectrum] — extended mIRC palette 16–98" end
Source
# File plugins/rabbot_meta.rb, line 549 def self.download_transcripts(target, **opts) CharacterTranscripts.download(target, **opts) end
Source
# File plugins/rabbot_meta.rb, line 1437 def self.drain_build_queue RabbotMetaQueue.drain end
Source
# File plugins/rabbot_meta.rb, line 195 def self.enhance_files_unchanged?(backup, plugin_path, test_path) return false unless backup plugin_same = File.file?(plugin_path) && File.read(plugin_path) == backup.plugin_content.to_s test_same = if backup.test_existed File.file?(test_path) && File.read(test_path) == backup.test_content.to_s else !File.file?(test_path) end return false unless plugin_same && test_same return true if backup.related_files.nil? plugin_name = File.basename(plugin_path.to_s, ".rb") snapshot = backup.related_files.is_a?(Hash) ? backup.related_files : {} !related_files_changed?(snapshot, plugin_name) end
Source
# File plugins/rabbot_meta.rb, line 146 def self.enhance_missing_description_message "Please provide an enhancement description." end
Source
# File plugins/rabbot_meta.rb, line 142 def self.enhance_usage_message "Usage: !rabbot pluginenhance <plugin> <description>" end
Source
# File plugins/rabbot_meta.rb, line 1415 def self.enqueue_build(schedule_drain: method(:schedule_build_drain_async), on_fail: nil, build_kind: nil, plugin_name: nil, description: nil, &block) RabbotMetaQueue.followup_scheduler = schedule_drain unless schedule_drain == false status, position = RabbotMetaQueue.enqueue( on_fail: on_fail, build_kind: build_kind, plugin_name: plugin_name, description: description, &block ) schedule_drain.call unless schedule_drain == false [status, position] end
Source
# File plugins/rabbot_meta.rb, line 1117 def self.fetch_usage_percents(token: Cursorusage.session_token, fetch: Cursorusage.method(:default_fetch), cache_ttl: Cursorusage::DEFAULT_CACHE_TTL_SEC, now: Time.now, use_cache: true) token = token.to_s.strip return nil if token.empty? response = Cursorusage.fetch_usage_summary( token: token, fetch: fetch, cache_ttl: cache_ttl, now: now, use_cache: use_cache ) return nil if response[:status] == 401 summary = Cursorusage.parse_usage_summary(response[:body]) return nil unless summary[:success] { auto_percent_used: summary[:auto_percent_used], api_percent_used: summary[:api_percent_used], total_percent_used: summary[:total_percent_used], billing_cycle_end: summary[:billing_cycle_end] } rescue StandardError nil end
Source
# File plugins/rabbot_meta.rb, line 622 def self.file_structure_guidance(enhance: false) lines = [ "File structure — prefer many small files over monoliths:", "- Split plugins into a thin IRCinch entry (plugins/<name>.rb) plus focused lib/ modules", "- One responsibility per file; target ~100–200 lines unless the domain is inherently tiny", "- Name lib modules clearly (lib/<topic>.rb or lib/<plugin>_<concern>.rb) and require_relative them from the plugin", "- Add matching tests under test/lib/ for new lib modules (test/plugins/ for the plugin file)", "- When shared logic outgrows a file, extract submodules instead of growing a single class" ] lines << "- Refactor bloated plugin or lib/ code into multiple smaller files as part of the enhancement" if enhance lines.join("\n") end
Source
# File plugins/rabbot_meta.rb, line 1403 def self.filter_build_progress_text(text) RabbotMetaBuild.filter_progress_text(text) end
Source
# File plugins/rabbot_meta.rb, line 1172 def self.format_agent_report(agent_result, include_summary: true) CursorAgentOutput.format_irc_report( CursorAgentOutput::StreamResult.new( success: agent_result.success?, text: agent_result.text, duration_ms: agent_result.duration_ms, model: agent_result.model, input_tokens: agent_result.input_tokens, output_tokens: agent_result.output_tokens, error: agent_result.error, todos: agent_result.todos || [] ), include_summary: include_summary ) end
Source
# File plugins/rabbot_meta.rb, line 368 def self.format_args_error(message) RabbotMetaBuild.tagged_line(message.to_s) end
Source
# File plugins/rabbot_meta.rb, line 527 def self.format_botgroup_reply(action, payload = nil, network: nil, channel: nil) case action when :list data = payload.is_a?(Hash) ? payload : { groups: Array(payload), network: network, channel: channel } RabbotMetaBotgroupReport.format_list( groups: data[:groups] || data["groups"] || [], network: data[:network] || data["network"] || network, channel: data[:channel] || data["channel"] || channel ) when :add group, bot_id = payload RabbotMetaBotgroupReport.format_action("Added #{bot_id} to #{group[:name]}.") when :rem group, bot_id = payload RabbotMetaBotgroupReport.format_action("Removed #{bot_id} from #{group[:name]}.") when :postgres RabbotMetaBotgroupReport.format_postgres_help(extensions: payload) else format_args_error(botgroup_usage_message) end end
Source
# File plugins/rabbot_meta.rb, line 1395 def self.format_build_agent_done_reply(progress_label:, todos: nil) RabbotMetaBuild.format_agent_done_reply(progress_label: progress_label, todos: todos) end
Source
# File plugins/rabbot_meta.rb, line 265 def self.format_build_conflict_reply(plugin_name:, reason:) RabbotMetaBuild.format_build_conflict_reply(plugin_name: plugin_name, reason: reason) end
Source
# File plugins/rabbot_meta.rb, line 1377 def self.format_build_failure(stats, model, error_summary, usage_fetch: method(:fetch_usage_percents), now: Time.now) usage_percents = usage_fetch.call "Plugin build failed after #{stats.attempts} attempt(s) " \ "#{format_cursor_summary(model: model, total_tokens: stats.total_tokens, duration_s: stats.duration_s, usage_percents: usage_percents, now: now)}: #{error_summary}" end
Source
# File plugins/rabbot_meta.rb, line 1399 def self.format_build_plan_ready_reply(progress_label:) RabbotMetaBuild.format_plan_ready_reply(progress_label: progress_label) end
Source
# File plugins/rabbot_meta.rb, line 1407 def self.format_build_progress_update(text) RabbotMetaBuild.format_progress_update(text) end
Source
# File plugins/rabbot_meta.rb, line 1517 def self.format_build_queue_failure(error) RabbotMetaBuildJob.format_queue_failure(error) end
Source
# File plugins/rabbot_meta.rb, line 1411 def self.format_build_queued_reply(position:) RabbotMetaQueue.format_queued_reply(position: position) end
Source
# File plugins/rabbot_meta.rb, line 1385 def self.format_build_start_reply(progress_label:, attempt:, max_attempts:, model:, target: nil) RabbotMetaBuild.format_start_reply( progress_label: progress_label, attempt: attempt, max_attempts: max_attempts, model: model, target: target ) end
Source
# File plugins/rabbot_meta.rb, line 1364 def self.format_build_success(stats, model, _command_hint, plugin_path) format_build_success_reply(stats: stats, model: model, plugin_path: plugin_path) end
Backward-compatible 4-arg entry point (matches AiRabbotMetaClassExtensions when ai.rb is loaded).
Source
# File plugins/rabbot_meta.rb, line 1368 def self.format_build_success_irc_reply(stats:, model:, plugin_path:, undo_plugin: nil, usage_fetch: method(:fetch_usage_percents), now: Time.now, agent_text: nil) format_build_success_reply( stats: stats, model: model, plugin_path: plugin_path, undo_plugin: undo_plugin, usage_fetch: usage_fetch, now: now, agent_text: agent_text ) end
Source
# File plugins/rabbot_meta.rb, line 1346 def self.format_build_success_reply(stats:, model:, plugin_path:, undo_plugin: nil, usage_fetch: method(:fetch_usage_percents), now: Time.now, agent_text: nil) reload_libs! filename = File.basename(plugin_path) usage_percents = usage_fetch.call stats_message = "Plugin #{filename} built in #{stats.attempts} attempt(s) " \ "#{format_cursor_summary(model: model, total_tokens: stats.total_tokens, duration_s: stats.duration_s, usage_percents: usage_percents, now: now)}" RabbotMetaBuild.format_success_reply( stats_message: stats_message, agent_text: agent_text, undo_hint: undo_plugin ? format_undo_hint(undo_plugin) : nil ) end
Source
# File plugins/rabbot_meta.rb, line 280 def self.format_cancel_reply(vetor:, cleared_pending:, cleared_queued:, worker_running:) RabbotMetaBuild.format_cancel_reply( vetor: vetor, cleared_pending: cleared_pending, cleared_queued: cleared_queued, worker_running: worker_running ) end
Source
# File plugins/rabbot_meta.rb, line 438 def self.format_colors_reply(parsed) case parsed[0] when :help RabbotMetaColorsReport.format_help when :code RabbotMetaColorsReport.format_code(parsed[1]) when :pair RabbotMetaColorsReport.format_pair(parsed[1], parsed[2]) when :pairs RabbotMetaColorsReport.format_pairs_list when :themes RabbotMetaColorsThemes.format_themes_list when :theme RabbotMetaColorsThemes.format_theme_palette(parsed[1]) || format_args_error("Unknown theme: #{parsed[1]}") when :spectrum RabbotMetaColorsThemes.format_spectrum when :named_pair RabbotMetaColorsReport.format_named_pair(parsed[1], parsed[2], parsed[3]) when :group RabbotMetaColorsReport.format_group(parsed[1]) else format_args_error(colors_usage_message) end end
Source
# File plugins/rabbot_meta.rb, line 1109 def self.format_cursor_summary(model:, total_tokens:, duration_s:, usage_percents: nil, now: Time.now) percent_segment = format_stats_percentages(usage_percents) cycle_segment = format_cycle_remaining_segment(usage_percents, now: now) "#{AiSummary::STYLE_META}#{AiSummary::STYLE_MODEL}#{model}#{AiSummary::IRC_RESET}#{AiSummary::STYLE_META} · " \ "#{AiSummary::STYLE_TOKENS}#{total_tokens} tokens#{AiSummary::IRC_RESET}#{AiSummary::STYLE_META} · " \ "#{AiSummary::STYLE_DURATION}#{duration_s}s#{AiSummary::IRC_RESET}#{percent_segment}#{cycle_segment}#{AiSummary::IRC_RESET}" end
Source
# File plugins/rabbot_meta.rb, line 1085 def self.format_cycle_remaining(cycle_end, now: Time.now) return "" if cycle_end.to_s.strip.empty? end_time = Time.parse(cycle_end).utc remaining = end_time - now.utc return "0m" if remaining <= 0 return "#{ (remaining / 86_400).floor }d" if remaining >= TWO_DAYS_SEC return "#{ (remaining / 3600).floor }h" if remaining >= 3600 minutes = [(remaining / 60).floor, 1].max "#{minutes}m" rescue ArgumentError, TypeError "" end
Source
# File plugins/rabbot_meta.rb, line 1100 def self.format_cycle_remaining_segment(usage_percents, now: Time.now) return "" unless usage_percents.is_a?(Hash) remaining = format_cycle_remaining(usage_percents[:billing_cycle_end], now: now) return "" if remaining.empty? " #{AiSummary::STYLE_DURATION}#{remaining}#{AiSummary::IRC_RESET}" end
Source
# File plugins/rabbot_meta.rb, line 1072 def self.format_labeled_usage_percent(value, kind) format("#{USAGE_KIND_STYLES[kind]}#{USAGE_LABELS[kind]}%d%%#{AiSummary::IRC_RESET}", value.to_i) end
Source
# File plugins/rabbot_meta.rb, line 313 def self.format_plan_question_reply(prompt:, options:, title: nil) RabbotMetaBuild.format_plan_question_reply(prompt: prompt, options: options, title: title) end
Source
# File plugins/rabbot_meta.rb, line 1513 def self.format_queued_build_start_notice(preview) RabbotMetaBuildJob.format_queued_start_notice(preview) end
Source
# File plugins/rabbot_meta.rb, line 1076 def self.format_stats_percentages(usage_percents) return "" if usage_percents.nil? auto = format_labeled_usage_percent(usage_percents[:auto_percent_used], :auto) api = format_labeled_usage_percent(usage_percents[:api_percent_used], :api) total = format_labeled_usage_percent(usage_percents[:total_percent_used], :total) "#{AiSummary::STYLE_META} · #{AiSummary::IRC_RESET}#{auto}#{api}#{total}" end
Source
# File plugins/rabbot_meta.rb, line 305 def self.format_stop_reply(vetor:, cleared_waiters:, active_plugin:) RabbotMetaBuild.format_stop_reply( vetor: vetor, cleared_waiters: cleared_waiters, active_plugin: active_plugin ) end
Source
# File plugins/rabbot_meta.rb, line 553 def self.format_transcripts_reply(result) unless result.ok? return [IrcFormat.tag(IRC_TAG, style: STYLE_TITLE), result.error.to_s].join("\n") end header = IrcFormat.report_header( tag: IRC_TAG, title: "Transcripts — #{result.character}", style: STYLE_TITLE ) body = "Downloaded #{result.chunk_count} transcript file(s) (#{result.bytes} bytes), split and sorted." footer = IrcFormat.report_footer(result.character, "#{result.chunk_count} files", "fandom.com") [header, body, footer].join("\n") end
Source
# File plugins/rabbot_meta.rb, line 1067 def self.format_undo_hint(plugin_name) name = snake_case_name(plugin_name) "Undo: !rabbot pluginundo #{name}" end
Source
# File plugins/rabbot_meta.rb, line 138 def self.generate_missing_description_message "Please provide a plugin description." end
Source
# File plugins/rabbot_meta.rb, line 1441 def self.kick_build_queue_if_needed RabbotMetaQueue.reconcile_worker! return unless RabbotMetaQueue.kick_needed? schedule_build_drain_async end
Source
# File plugins/rabbot_meta.rb, line 1022 def self.load_build_backup(plugin_name) data = RabbotDb.get_plugin_json( plugin: "rabbot_meta", key: backup_storage_key(plugin_name) ) return nil if data.nil? backup_from_h(data) end
Source
# File plugins/rabbot_meta.rb, line 971 def self.new_plugin_backup(plugin_path, test_path) BuildBackup.new( plugin_path: File.expand_path(plugin_path), test_path: File.expand_path(test_path), plugin_content: nil, test_content: nil, plugin_existed: false, test_existed: false, class_name: nil, related_files: {} ) end
Source
# File plugins/rabbot_meta.rb, line 587 def self.note_ai_guard_message(network:, channel:, nick:, text:) RabbotMetaAiGuardBypass.note_message(network: network, channel: channel, nick: nick, text: text) end
Source
# File plugins/rabbot_meta.rb, line 1158 def self.parse_agent_json(stdout, stderr, status, model: AGENT_MODEL) result = CursorAgentOutput.parse(stdout, stderr, status, model: model) AgentResult.new( success: result.success?, text: result.text, duration_ms: result.duration_ms, model: model, input_tokens: result.input_tokens, output_tokens: result.output_tokens, error: result.error, todos: result.todos || [] ) end
Source
# File plugins/rabbot_meta.rb, line 691 def self.parse_enhance_args(args) text = args.to_s.strip return [nil, ""] if text.empty? match = text.match(/\A(\S+)\s+(.+)\z/m) return [text, ""] unless match [match[1], sanitize_description(match[2])] end
Source
# File plugins/rabbot_meta.rb, line 791 def self.parse_marker(output, marker) output.to_s.each_line do |line| return Regexp.last_match(1).strip if line.strip =~ /\A#{marker}:\s*(.+)\z/ end nil end
Source
# File plugins/rabbot_meta.rb, line 126 def self.piece_delay_sec_for(piece_index:, chunk_delay_sec:) RabbotMetaFlood.piece_delay_sec(piece_index: piece_index, chunk_delay_sec: chunk_delay_sec) end
Source
# File plugins/rabbot_meta.rb, line 162 def self.plan_prompt(prompt) RabbotMetaAgent.plan_prompt(prompt) end
Source
# File plugins/rabbot_meta.rb, line 811 def self.plugin_path_for(name) raw = name.to_s.strip return nil if raw.empty? alias_target = PLUGIN_ALIASES[raw.downcase] return plugin_path_for(alias_target) if alias_target if raw.end_with?(".rb") candidates = [ File.expand_path(raw, RABBOT_ROOT), File.join(RABBOT_ROOT, "plugins", File.basename(raw)) ] return candidates.find { |path| File.file?(path) } end snake = snake_case_name(raw) candidates = [ File.join(RABBOT_ROOT, "plugins", "#{raw.downcase}.rb"), File.join(RABBOT_ROOT, "plugins", "#{snake}.rb") ] candidates.find { |path| File.file?(path) } end
Source
# File plugins/rabbot_meta.rb, line 834 def self.plugin_path_for_class(klass) name = klass.name @plugin_path_by_class ||= {} return @plugin_path_by_class[name] if @plugin_path_by_class.key?(name) path = Dir[File.join(RABBOT_ROOT, "plugins", "*.rb")].find { |candidate| class_name_from_file(candidate) == name } @plugin_path_by_class[name] = path path end
Source
# File plugins/rabbot_meta.rb, line 231 def self.plugincancel_in_flight_message "A plugin build is in progress; !rabbot plugincancel cannot stop an in-flight agent." end
Source
# File plugins/rabbot_meta.rb, line 227 def self.plugincancel_no_pending_message "No pending plugin build to cancel in this channel." end
Source
# File plugins/rabbot_meta.rb, line 468 def self.prepare_botgroup(args) text = args.to_s.strip return [:ok, :list] if text.empty? || text.casecmp?("list") return [:ok, :postgres] if text.casecmp?("postgres") if (match = text.match(/\Aadd\s+(\S+)\s+(\S+)\z/i)) return [:ok, :add, match[1], match[2]] end if (match = text.match(/\Arem(?:ove)?\s+(\S+)\s+(\S+)\z/i)) return [:ok, :rem, match[1], match[2]] end [:error, botgroup_usage_message] end
Source
# File plugins/rabbot_meta.rb, line 494 def self.prepare_botgroup_add(network:, channel:, nick:, is_op:, group_name:, bot_id:) unless BotMesh.admin_allowed?(network: network, channel: channel, nick: nick, is_op: is_op) return [:error, BotMesh.admin_denial_message] end group = BotMesh::GroupResolver.find_group(name: group_name, network: network, channel: channel) return [:error, "Unknown bot group: #{group_name}"] unless group added = BotMesh.add_member(group_id: group[:id], bot_id: bot_id, added_by: nick) return [:error, "#{bot_id} is already in #{group_name}"] unless added [:ok, group, bot_id] end
Source
# File plugins/rabbot_meta.rb, line 485 def self.prepare_botgroup_list(network:, channel:, nick:, is_op:) unless sensitive_command_allowed?(network: network, channel: channel, nick: nick, is_op: is_op) return [:error, sensitive_command_denied_message] end groups = BotMesh.groups_for(network: network, channel: channel) [:ok, { groups: groups, network: network, channel: channel }] end
Source
# File plugins/rabbot_meta.rb, line 523 def self.prepare_botgroup_postgres [:ok, BotMesh.recommended_plugins] end
Source
# File plugins/rabbot_meta.rb, line 508 def self.prepare_botgroup_rem(network:, channel:, nick:, is_op:, group_name:, bot_id:) unless BotMesh.admin_allowed?(network: network, channel: channel, nick: nick, is_op: is_op) return [:error, BotMesh.admin_denial_message] end group = BotMesh::GroupResolver.find_group(name: group_name, network: network, channel: channel) return [:error, "Unknown bot group: #{group_name}"] unless group return [:error, "Cannot remove bots from implicit owner groups"] if group[:implicit] removed = BotMesh.remove_member(group_id: group[:id], bot_id: bot_id) return [:error, "#{bot_id} is not in #{group_name}"] unless removed [:ok, group, bot_id] end
Source
# File plugins/rabbot_meta.rb, line 235 def self.prepare_cancel_build(network:, channel:, nick:, is_op:) unless RabbotMetaPlanSession.admin_allowed?( network: network, channel: channel, nick: nick, is_op: is_op ) return [:error, RabbotMetaPlanSession.admin_denial_message] end pending = RabbotMetaPlanSession.pending(network: network, channel: channel) queued = RabbotMetaQueue.pending_count worker_running = RabbotMetaQueue.worker_running? if pending.nil? && queued.zero? && !worker_running return [:error, plugincancel_no_pending_message] end cleared_pending = !pending.nil? RabbotMetaPlanSession.clear_pending!(network: network, channel: channel) if cleared_pending cleared_queued = RabbotMetaQueue.clear_waiting! if cleared_pending || cleared_queued.positive? return [:ok, { cleared_pending: cleared_pending, cleared_queued: cleared_queued, worker_running: worker_running }] end [:error, plugincancel_in_flight_message] end
Source
# File plugins/rabbot_meta.rb, line 431 def self.prepare_colors(args) parsed = IrcExtendedColors.parse_query(args) return [:error, parsed[1]] if parsed[0] == :error [:ok, parsed] end
Source
# File plugins/rabbot_meta.rb, line 387 def self.prepare_enhance(args) plugin_name, description = parse_enhance_args(args) return [:error, enhance_usage_message] if plugin_name.nil? || plugin_name.to_s.empty? return [:error, enhance_missing_description_message] if description.to_s.empty? [:ok, plugin_name, description] end
Source
# File plugins/rabbot_meta.rb, line 269 def self.prepare_enhance_enqueue(plugin_name:, plugin_path:, test_path:, description: nil, queue: RabbotMetaQueue) RabbotMetaBuildTarget.prepare_enhance_enqueue( plugin_name: plugin_name, plugin_path: plugin_path, test_path: test_path, description: description, queue: queue ) end
Source
# File plugins/rabbot_meta.rb, line 372 def self.prepare_generate(description) sanitized = sanitize_description(description) return [:error, generate_missing_description_message] if sanitized.empty? if (hint = reload_typo_hint(sanitized)) return [:error, hint] end if sanitized.match?(/\Acancel\z/i) return [:error, "To abort a pending build, use: !rabbot plugincancel"] end [:ok, sanitized] end
Source
# File plugins/rabbot_meta.rb, line 213 def self.prepare_plan_answer(text, network:, channel:, nick:, is_op:) pending = RabbotMetaPlanSession.pending(network: network, channel: channel) return [:error, "No pending build plan question in this channel."] unless pending unless RabbotMetaPlanSession.admin_allowed?( network: network, channel: channel, nick: nick, is_op: is_op ) return [:error, RabbotMetaPlanSession.admin_denial_message] end answer = RabbotMetaPlanSession.resolve_answer(text, questions: pending[:questions]) [:ok, answer, pending] end
Source
# File plugins/rabbot_meta.rb, line 409 def self.prepare_quit(message) quit_message = sanitize_quit_message(message) return [:error, quit_usage_message] if quit_message.empty? [:ok, quit_message] end
Source
# File plugins/rabbot_meta.rb, line 395 def self.prepare_reload(name) plugin_name = name.to_s.strip return [:error, reload_usage_message] if plugin_name.empty? [:ok, plugin_name] end
Source
# File plugins/rabbot_meta.rb, line 289 def self.prepare_stop_output_queue(network:, channel:, nick:, is_op:, channel_key:, bot:, queue_class: nil) unless sensitive_command_allowed?( network: network, channel: channel, nick: nick, is_op: is_op ) return [:error, sensitive_command_denied_message] end details = RabbotMetaOutputQueue.clear_for_bot( bot: bot, channel_key: channel_key, queue_class: queue_class ) [:ok, details] end
Source
# File plugins/rabbot_meta.rb, line 420 def self.prepare_transcripts(args) target = args.to_s.strip return [:error, transcripts_usage_message] if target.empty? [:ok, target] end
Source
# File plugins/rabbot_meta.rb, line 402 def self.prepare_undo(name) plugin_name = name.to_s.strip return [:error, undo_usage_message] if plugin_name.empty? [:ok, plugin_name] end
Source
# File plugins/rabbot_meta.rb, line 118 def self.progress_gutter_needed?(logical_lines:, pieces:) logical_lines.to_i >= PROGRESS_GUTTER_MIN_LINES || pieces.to_i >= PROGRESS_GUTTER_MIN_LINES end
Source
# File plugins/rabbot_meta.rb, line 158 def self.quit_usage_message "Usage: !rabbot quit <quit message>" end
Source
# File plugins/rabbot_meta.rb, line 907 def self.read_file_if_exists(path) return nil unless File.file?(path) File.read(path) end
Source
# File plugins/rabbot_meta.rb, line 850 def self.register_reloaded_plugin(bot, old_klass, new_klass = nil) new_klass ||= old_klass unregister_plugin_class(bot, old_klass) bot.plugins.register_plugin(new_klass) PluginTimers.start(bot.plugins.last) end
Source
# File plugins/rabbot_meta.rb, line 1342 def self.reload_agent_libs! reload_libs! end
Backward-compatible alias used by plugin builds and tests.
Source
# File plugins/rabbot_meta.rb, line 1328 def self.reload_lib_file(path) return :missing unless File.file?(path) verbose = $VERBOSE $VERBOSE = nil load path :ok rescue ScriptError, StandardError => e [:error, e.message] ensure $VERBOSE = verbose if defined?(verbose) end
Like LibReload.reload_file but also tolerates ScriptError (e.g. a SyntaxError in one lib file) so a single broken file cannot abort the whole reload.
Source
# File plugins/rabbot_meta.rb, line 1306 def self.reload_libs! reloaded = [] errors = [] LibReload::PASSES.times do LibReload.lib_paths.each do |path| case reload_lib_file(path) in :ok reloaded << LibReload.rel_path(path) in [:error, message] errors << "#{LibReload.rel_path(path)}: #{message}" else nil end end end { reloaded: reloaded.uniq, errors: errors.uniq } end
Source
# File plugins/rabbot_meta.rb, line 857 def self.reload_plugin(name, allow_rabbot_meta: false) reload_libs! path = plugin_path_for(name) return [:error, "Plugin not found: #{name}"] unless path class_name = class_name_from_file(path) return [:error, "Could not determine plugin class in #{path}"] unless class_name if class_name == "RabbotMeta" && !allow_rabbot_meta return [:error, "Only registered owners can reload RabbotMeta"] end @plugin_path_by_class&.delete(class_name) load path klass = Object.const_get(class_name) [:ok, klass, path] rescue NameError, StandardError => e [:error, "Reload failed: #{e.message}"] end
Source
# File plugins/rabbot_meta.rb, line 876 def self.reload_registered_plugins(bot) lib_result = reload_libs! reloaded = [] errors = lib_result[:errors].dup bot.plugins.dup.each do |plugin| old_klass = plugin.class next if old_klass.name == "RabbotMeta" path = plugin_path_for_class(old_klass) unless path errors << "#{old_klass.name}: plugin file not found" next end class_name = class_name_from_file(path) load path new_klass = Object.const_get(class_name) register_reloaded_plugin(bot, old_klass, new_klass) reloaded << new_klass.name rescue StandardError => e errors << "#{old_klass.name}: #{e.message}" end { reloaded: reloaded, reloaded_libs: lib_result[:reloaded], errors: errors } end
Source
# File plugins/rabbot_meta.rb, line 610 def self.reload_typo_hint(description) text = description.to_s.strip return nil if text.empty? if (match = text.match(/\A(?:re|ra)?load\s+(.+)\z/i)) plugin = match[1].strip return "That looks like a reload request. Use: !rabbot pluginreload #{plugin}" unless plugin.empty? end nil end
Source
# File plugins/rabbot_meta.rb, line 150 def self.reload_usage_message "Usage: !rabbot pluginreload <plugin> (e.g. yt, Yt, plugins/yt.rb)" end
Source
# File plugins/rabbot_meta.rb, line 949 def self.resolve_enhance_path(marker, canonical_path) return canonical_path if marker.to_s.strip.empty? return canonical_path if canonical_path.to_s.strip.empty? candidate = File.expand_path(marker.strip, RABBOT_ROOT) File.file?(candidate) ? candidate : canonical_path end
Source
# File plugins/rabbot_meta.rb, line 1048 def self.restore_build_backup(backup) restore_file(backup.plugin_path, backup.plugin_content, backup.plugin_existed) restore_file(backup.test_path, backup.test_content, backup.test_existed) snapshot = backup.related_files.is_a?(Hash) ? backup.related_files : {} plugin_name = File.basename(backup.plugin_path.to_s, ".rb") current_paths = related_files_for_plugin(plugin_name).to_set snapshot_paths = snapshot.keys.map { |path| File.expand_path(path) }.to_set snapshot.each do |path, state| data = state.is_a?(Hash) ? state : {} restore_file(File.expand_path(path), data["content"], data["existed"]) end (current_paths - snapshot_paths).each do |path| File.delete(path) if File.file?(path) end end
Source
# File plugins/rabbot_meta.rb, line 1040 def self.restore_file(path, content, existed) if existed File.write(path, content) elsif File.file?(path) File.delete(path) end end
Source
# File plugins/rabbot_meta.rb, line 1188 def self.run_agent(prompt, model: RabbotMeta::AGENT_MODEL, on_assistant_text: nil, runner: nil, session_id: nil, mode: :plan) result = RabbotMetaAgent.run( prompt, model: model, workspace: RABBOT_ROOT, session_id: session_id, on_assistant_text: on_assistant_text, runner: runner, mode: mode ) AgentResult.new( success: result.success?, text: result.text, duration_ms: result.duration_ms, model: model, input_tokens: result.input_tokens, output_tokens: result.output_tokens, error: result.error, todos: result.todos || [], session_id: result.session_id, questions: result.questions, needs_input: result.needs_input? ) end
Source
# File plugins/rabbot_meta.rb, line 1267 def self.run_tests(test_path) Open3.capture3( "ruby", "-I#{File.join(RABBOT_ROOT, "lib")}:#{File.join(RABBOT_ROOT, "test")}", test_path ) end
Source
# File plugins/rabbot_meta.rb, line 130 def self.sanitize_description(description) description.to_s.strip.gsub(/[\x00-\x1f]/, "")[0, MAX_DESCRIPTION_LENGTH] end
Source
# File plugins/rabbot_meta.rb, line 134 def self.sanitize_quit_message(message) GracefulQuit.sanitize_message(message) end
Source
# File plugins/rabbot_meta.rb, line 1429 def self.schedule_build_drain_async(label: "rabbot_meta_queue", runner: nil) if runner runner.call else AsyncJob.run(label: label) { drain_build_queue } end end
Source
# File plugins/rabbot_meta.rb, line 599 def self.scrub_educational_paths(text) RabbotMetaAiGuardBypass.scrub_educational_paths(text) end
Source
# File plugins/rabbot_meta.rb, line 566 def self.sensitive_command_allowed?(network:, channel:, nick:, is_op:) ChannelTrust.human_admin_nick?( network: network, channel: channel, nick: nick, is_op: is_op ) end
Source
# File plugins/rabbot_meta.rb, line 575 def self.sensitive_command_denied_message "Only registered human admins can use this command (bots cannot build plugins)." end
Source
# File plugins/rabbot_meta.rb, line 803 def self.snake_case_name(name) name.to_s .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') .gsub(/([a-z\d])([A-Z])/, '\1_\2') .tr("-", "_") .downcase end
Source
# File plugins/rabbot_meta.rb, line 903 def self.snapshot_files(glob) Dir[glob].map { |path| File.expand_path(path) }.to_set end
Source
# File plugins/rabbot_meta.rb, line 1014 def self.store_build_backup(plugin_name, backup) RabbotDb.set_plugin_json( plugin: "rabbot_meta", key: backup_storage_key(plugin_name), value: backup_to_h(backup) ) end
Source
# File plugins/rabbot_meta.rb, line 1214 def self.store_pending_build_plan(network:, channel:, asker:, agent_result:, model:, description:, progress_label:, attempt:, stats:, build_kind:, build_state:) RabbotMetaPlanSession.save_pending!( network: network, channel: channel, session_id: agent_result.session_id, asker: asker, description: description, questions: agent_result.questions, model: model, build_state: build_state ) end
Source
# File plugins/rabbot_meta.rb, line 1297 def self.summarize_build_error(error) text = error.to_s if text.start_with?("Tests failed:\n") return summarize_test_output(text.sub(/\ATests failed:\n/, ""), "") end text.lines.first&.strip || text[0, 200].to_s end
Source
# File plugins/rabbot_meta.rb, line 1275 def self.summarize_test_output(stdout, stderr) combined = [stdout, stderr].map(&:to_s).join("\n").strip return "Tests failed (no output)" if combined.empty? stats = combined.match(/(\d+) runs?, (\d+) assertions?, (\d+) failures?, (\d+) errors?, (\d+) skips?/) failures = [] combined.scan(/^\s*\d+\)\s+(?:Failure|Error):\s*\n\s*(\S+#\S+)/m) do |name| failures << name end parts = [] if stats runs, _assertions, failure_count, error_count, _skips = stats.captures.map(&:to_i) failure_label = failure_count == 1 ? "failure" : "failures" error_label = error_count == 1 ? "error" : "errors" parts << "#{runs} runs, #{failure_count} #{failure_label}, #{error_count} #{error_label}" end parts << failures.first(3).join(", ") if failures.any? "Tests failed: #{parts.join(' — ')}" end
Source
# File plugins/rabbot_meta.rb, line 701 def self.test_path_for(plugin_path) basename = File.basename(plugin_path, ".rb") path = File.join(RABBOT_ROOT, "test", "plugins", "#{basename}_test.rb") File.file?(path) ? path : nil end
Source
# File plugins/rabbot_meta.rb, line 416 def self.transcripts_usage_message "Usage: !rabbot transcripts <fandom-url|character>" end
Source
# File plugins/rabbot_meta.rb, line 1144 def self.undo_plugin(plugin_name, plugin_path_resolver: method(:plugin_path_for)) backup = load_build_backup(plugin_name) return [:error, "No undo backup for #{plugin_name}"] unless backup restore_build_backup(backup) clear_build_backup(plugin_name) resolved_path = plugin_path_resolver.call(plugin_name) || backup.plugin_path class_name = backup.class_name class_name ||= class_name_from_file(resolved_path) if File.file?(resolved_path) [:ok, backup, class_name] rescue StandardError => e [:error, "Undo failed: #{e.message}"] end
Source
# File plugins/rabbot_meta.rb, line 154 def self.undo_usage_message "Usage: !rabbot pluginundo <plugin> (e.g. pizza)" end
Source
# File plugins/rabbot_meta.rb, line 844 def self.unregister_plugin_class(bot, klass) bot.plugins.dup.each do |plugin| bot.plugins.unregister_plugin(plugin) if plugin.class == klass end end
Public Instance Methods
Source
# File plugins/rabbot_meta.rb, line 1637 def botgroup_command(m, args) case self.class.prepare_botgroup(args) in [:ok, :list] case self.class.prepare_botgroup_list( network: bot.config.server, channel: m.channel.name, nick: m.user.nick, is_op: m.channel.opped?(m.user) ) in [:ok, data] themed_flood_safe_reply(m, self.class.format_botgroup_reply(:list, data)) in [:error, message] flood_safe_reply(m, self.class.format_args_error(message), with_progress: false) end in [:ok, :postgres] case self.class.prepare_botgroup_postgres in [:ok, extensions] themed_flood_safe_reply(m, self.class.format_botgroup_reply(:postgres, extensions)) end in [:ok, :add, group_name, bot_id] case self.class.prepare_botgroup_add( network: bot.config.server, channel: m.channel.name, nick: m.user.nick, is_op: m.channel.opped?(m.user), group_name: group_name, bot_id: bot_id ) in [:ok, group, added_bot] themed_flood_safe_reply(m, self.class.format_botgroup_reply(:add, [group, added_bot])) in [:error, message] flood_safe_reply(m, self.class.format_args_error(message), with_progress: false) end in [:ok, :rem, group_name, bot_id] case self.class.prepare_botgroup_rem( network: bot.config.server, channel: m.channel.name, nick: m.user.nick, is_op: m.channel.opped?(m.user), group_name: group_name, bot_id: bot_id ) in [:ok, group, removed_bot] themed_flood_safe_reply(m, self.class.format_botgroup_reply(:rem, [group, removed_bot])) in [:error, message] flood_safe_reply(m, self.class.format_args_error(message), with_progress: false) end in [:error, message] flood_safe_reply(m, self.class.format_args_error(message), with_progress: false) end end
Source
# File plugins/rabbot_meta.rb, line 1502 def capture_ai_guard_context(m) return unless m.channel self.class.note_ai_guard_message( network: bot.config.server, channel: m.channel.name, nick: m.user.nick, text: m.message ) end
Source
# File plugins/rabbot_meta.rb, line 1628 def colors_command(m, args) case self.class.prepare_colors(args) in [:ok, parsed] themed_flood_safe_reply(m, self.class.format_colors_reply(parsed)) in [:error, message] flood_safe_reply(m, self.class.format_args_error(message), with_progress: false) end end
Source
# File plugins/rabbot_meta.rb, line 1551 def enhance_plugin_command(m, args) unless sensitive_command_allowed?(m) m.reply self.class.sensitive_command_denied_message return end if handle_pending_plan_answer(m, args) return end case self.class.prepare_enhance(args) in [:ok, plugin_name, description] plugin_path = self.class.plugin_path_for(plugin_name) test_path = plugin_path ? self.class.test_path_for(plugin_path) : nil if RabbotMetaBuildTarget.normalize_plugin_name(plugin_name) == "rabbot_meta" && !ChannelTrust.registered_owner_nick?( network: bot.config.server, channel: m.channel.name, nick: m.user.nick ) m.reply "Only registered owners can enhance RabbotMeta" return end if plugin_path && !test_path rel_plugin = plugin_path.sub("#{self.class::RABBOT_ROOT}/", "") m.reply "No test file found for #{rel_plugin} (expected test/plugins/#{File.basename(plugin_path, ".rb")}_test.rb)" return end case self.class.prepare_enhance_enqueue( plugin_name: plugin_name, plugin_path: plugin_path, test_path: test_path, description: description ) in [:ok, plugin_name, description, plugin_path, test_path] preview = description.length > 80 ? "#{description[0, 77]}..." : description normalized = RabbotMetaBuildTarget.normalize_plugin_name(plugin_name) target_resolver = lambda do resolved = plugin_path || self.class.plugin_path_for(plugin_name) resolved&.sub("#{self.class::RABBOT_ROOT}/", "") end enqueue_plugin_build_job( m, preview: preview, build_kind: :enhance, plugin_name: normalized, description: description, message_kind: :enhance, target_label: target_resolver ) do resolved_plugin = plugin_path || self.class.plugin_path_for(plugin_name) resolved_test = test_path || self.class.test_path_for(resolved_plugin) unless resolved_plugin && resolved_test themed_flood_safe_reply(m, self.class.format_args_error("Plugin not found: #{plugin_name}")) next end backup = self.class.capture_build_backup(resolved_plugin, resolved_test) enhance_plugin(m, plugin_name, description, resolved_plugin, resolved_test, backup: backup) end in [:error, message] reply = if message.start_with?("An enhancement for") self.class.format_build_conflict_reply(plugin_name: plugin_name, reason: message) else self.class.format_args_error(message) end m.reply reply end in [:error, message] m.reply self.class.format_args_error(message) end end
Source
# File plugins/rabbot_meta.rb, line 1478 def flood_safe_reply(message, text, with_progress: :auto, skip_prefix_lines: 0, line_byte_limit: nil, chunk_delay_sec: FloodSafe::CHUNK_DELAY_SEC) target = reply_target(message) pieces = reply_pieces( text, message: target, with_progress: with_progress, skip_prefix_lines: skip_prefix_lines, line_byte_limit: line_byte_limit ) return if pieces.empty? pieces.each_with_index do |piece, index| delay = self.class.piece_delay_sec_for(piece_index: index, chunk_delay_sec: chunk_delay_sec) if delay.zero? target.reply(piece) else Timer(delay, shots: 1) do target.reply(piece) end end end end
Source
# File plugins/rabbot_meta.rb, line 1521 def generate_plugin_command(m, description) unless sensitive_command_allowed?(m) m.reply self.class.sensitive_command_denied_message return end if handle_pending_plan_answer(m, description) return end case self.class.prepare_generate(description) in [:ok, sanitized] preview = sanitized.length > 80 ? "#{sanitized[0, 77]}..." : sanitized plugin_snapshot = self.class.snapshot_files(File.join(RABBOT_ROOT, "plugins", "*.rb")) test_snapshot = self.class.snapshot_files(File.join(RABBOT_ROOT, "test", "plugins", "*_test.rb")) enqueue_plugin_build_job( m, preview: preview, build_kind: :new, description: sanitized, message_kind: :generate ) do generate_plugin(m, sanitized, plugin_snapshot, test_snapshot) end in [:error, message] m.reply self.class.format_args_error(message) end end
Source
# File plugins/rabbot_meta.rb, line 1448 def kick_build_queue_if_needed self.class.kick_build_queue_if_needed end
Source
# File plugins/rabbot_meta.rb, line 1725 def plugincancel_command(m) unless sensitive_command_allowed?(m) m.reply self.class.sensitive_command_denied_message return end case self.class.prepare_cancel_build( network: bot.config.server, channel: m.channel.name, nick: m.user.nick, is_op: m.channel.opped?(m.user) ) in [:ok, details] m.reply self.class.format_cancel_reply(vetor: m.user.nick, **details) in [:error, message] m.reply self.class.format_args_error(message) end end
Source
# File plugins/rabbot_meta.rb, line 1707 def quit_command(m, message) unless sensitive_command_allowed?(m) m.reply self.class.sensitive_command_denied_message return end case self.class.prepare_quit(message) in [:ok, quit_message] m.reply "Signing off — #{quit_message}" AsyncJob.run(label: "rabbot_quit") do sleep QUIT_ACK_DELAY GracefulQuit.perform!(bot, message: quit_message) end in [:error, usage] m.reply self.class.format_args_error(usage) end end
Source
# File plugins/rabbot_meta.rb, line 1790 def reload_plugin_command(m, name) unless sensitive_command_allowed?(m) m.reply self.class.sensitive_command_denied_message return end case self.class.prepare_reload(name) in [:ok, plugin_name] allow_rabbot_meta = ChannelTrust.registered_owner_nick?( network: bot.config.server, channel: m.channel.name, nick: m.user.nick ) case self.class.reload_plugin(plugin_name, allow_rabbot_meta: allow_rabbot_meta) in [:ok, klass, path] self.class.register_reloaded_plugin(bot, klass) rel_path = path.sub("#{RABBOT_ROOT}/", "") m.reply "Reloaded #{klass.name} (#{rel_path})" in [:error, message] m.reply message end in [:error, message] m.reply self.class.format_args_error(message) end end
Source
# File plugins/rabbot_meta.rb, line 1744 def stop_command(m) unless sensitive_command_allowed?(m) m.reply self.class.sensitive_command_denied_message return end channel_key = if defined?(Bot::PluginOutputQueue) Bot::PluginOutputQueue.channel_key(m) else m.channel.name.to_s.downcase end case self.class.prepare_stop_output_queue( network: bot.config.server, channel: m.channel.name, nick: m.user.nick, is_op: m.channel.opped?(m.user), channel_key: channel_key, bot: bot ) in [:ok, details] m.reply self.class.format_stop_reply(vetor: m.user.nick, **details) in [:error, message] m.reply self.class.format_args_error(message) end end
Source
# File plugins/rabbot_meta.rb, line 1452 def themed_flood_safe_reply(m, text, plugin: nil) plugin_name = (plugin || self.class.name).to_s.downcase network = if bot && !bot.is_a?(Class) && bot.respond_to?(:config) && bot.config bot.config.server end channel = reply_channel_name(m) message = if network && channel ThemeOutput.apply( network: network, channel: channel, plugin: plugin_name, text: text ) else text.to_s end target = reply_target(m) pieces, logical_line_count = collect_reply_pieces(message, message: target) with_progress = self.class.progress_gutter_needed?( logical_lines: logical_line_count, pieces: pieces.length ) chunk_delay_sec = self.class.chunk_delay_sec_for(logical_lines: logical_line_count) flood_safe_reply(m, message, with_progress: with_progress, chunk_delay_sec: chunk_delay_sec) end
Source
# File plugins/rabbot_meta.rb, line 1689 def transcripts_command(m, args) unless sensitive_command_allowed?(m) m.reply self.class.sensitive_command_denied_message return end case self.class.prepare_transcripts(args) in [:ok, target] m.reply "Fetching transcripts for #{target} (requested by #{m.user.nick})..." AsyncJob.run(label: "rabbot_meta_transcripts") do result = self.class.download_transcripts(target) themed_flood_safe_reply(m, self.class.format_transcripts_reply(result)) end in [:error, message] m.reply self.class.format_args_error(message) end end
Source
# File plugins/rabbot_meta.rb, line 1771 def undo_plugin_command(m, name) unless sensitive_command_allowed?(m) m.reply self.class.sensitive_command_denied_message return end case self.class.prepare_undo(name) in [:ok, plugin_name] case self.class.undo_plugin(plugin_name) in [:ok, backup, class_name] apply_undo_reload(m, backup, class_name, plugin_name) in [:error, message] m.reply message end in [:error, message] m.reply self.class.format_args_error(message) end end
Source
# File plugins/distance.rb, line 530 def unregister_plugin_class(klass) self.class.unregister_plugin_class(bot, klass) end