lib/clacky.rb



# frozen_string_literal: true

# ── Global encoding defaults ──────────────────────────────────────────────────
# Force UTF-8 as the default external/internal encoding for all IO operations
# (File.read, Open3.capture3, HTTP bodies, etc.) so that binary-encoded strings
# from external processes or network I/O never cause "invalid byte sequence in
# UTF-8" errors on Ruby 2.6+.
# Binary-specific operations (File.binread, IO#read with "b" mode, .b) are
# unaffected — they always bypass this setting.
Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8

# ── Ruby < 2.7 polyfills ──────────────────────────────────────────────────────

# Enumerable#filter_map was added in Ruby 2.7.
if RUBY_VERSION < "2.7"
  module Enumerable
    def filter_map(&block)
      return to_enum(:filter_map) unless block

      each_with_object([]) do |item, result|
        mapped = block.call(item)
        result << mapped if mapped
      end
    end
  end
end

# File.absolute_path? was added in Ruby 2.7.
# Polyfill: a path is absolute if it starts with "/" (Unix) or a drive letter (Windows).
unless File.respond_to?(:absolute_path?)
  def File.absolute_path?(path)
    File.expand_path(path) == path.to_s
  end
end

# URI.encode_uri_component was added in Ruby 3.2.
# CGI.escape encodes spaces as '+'; replace with '%20' to match URI encoding.
require "uri"
require "cgi"
unless URI.respond_to?(:encode_uri_component)
  def URI.encode_uri_component(str)
    CGI.escape(str.to_s).gsub("+", "%20")
  end
end

# YAML.safe_load with permitted_classes: keyword was added in Psych 4 (Ruby 3.1).
# On older Ruby, the second positional argument serves the same purpose.
# This helper provides a unified interface across Ruby versions.
module YAMLCompat
  def self.safe_load(yaml_string, permitted_classes: [])
    if Psych::VERSION >= "4.0"
      YAML.safe_load(yaml_string, permitted_classes: permitted_classes)
    else
      YAML.safe_load(yaml_string, permitted_classes)
    end
  end

  def self.load_file(path, permitted_classes: [])
    safe_load(File.read(path), permitted_classes: permitted_classes)
  end
end

require_relative "clacky/version"
require_relative "clacky/message_format/anthropic"
require_relative "clacky/message_format/open_ai"
require_relative "clacky/message_format/bedrock"
require_relative "clacky/bedrock_stream_aggregator"
require_relative "clacky/openai_stream_aggregator"
require_relative "clacky/anthropic_stream_aggregator"
require_relative "clacky/client"
require_relative "clacky/skill"
require_relative "clacky/skill_loader"

# Agent system
require_relative "clacky/message_history"
require_relative "clacky/agent_config"
require_relative "clacky/agent_profile"
require_relative "clacky/providers"
require_relative "clacky/session_manager"
require_relative "clacky/idle_compression_timer"

# Agent modules
require_relative "clacky/agent/message_compressor"
require_relative "clacky/agent/hook_manager"
require_relative "clacky/agent/tool_registry"

# UI modules
require_relative "clacky/ui2/thinking_verbs"
require_relative "clacky/ui2/progress_indicator"

# Utils
require_relative "clacky/utils/logger"
require_relative "clacky/platform_http_client"
require_relative "clacky/utils/encoding"
require_relative "clacky/utils/environment_detector"
require_relative "clacky/utils/browser_detector"
require_relative "clacky/utils/scripts_manager"
require_relative "clacky/utils/model_pricing"
require_relative "clacky/utils/gitignore_parser"
require_relative "clacky/utils/limit_stack"
require_relative "clacky/utils/path_helper"
require_relative "clacky/utils/file_ignore_helper"
require_relative "clacky/utils/string_matcher"
require_relative "clacky/utils/login_shell"
require_relative "clacky/tools/base"
require_relative "clacky/utils/file_processor"

require_relative "clacky/tools/security"
require_relative "clacky/tools/file_reader"
require_relative "clacky/tools/write"
require_relative "clacky/tools/edit"
require_relative "clacky/tools/glob"
require_relative "clacky/tools/grep"
require_relative "clacky/tools/web_search"
require_relative "clacky/tools/web_fetch"
require_relative "clacky/tools/todo_manager"
require_relative "clacky/tools/trash_manager"
require_relative "clacky/tools/request_user_feedback"
require_relative "clacky/tools/invoke_skill"
require_relative "clacky/tools/undo_task"
require_relative "clacky/tools/redo_task"
require_relative "clacky/tools/list_tasks"
require_relative "clacky/tools/browser"
require_relative "clacky/tools/terminal"
require_relative "clacky/telemetry"
require_relative "clacky/agent"

require_relative "clacky/server/session_registry"
require_relative "clacky/server/web_ui_controller"
require_relative "clacky/server/browser_manager"
require_relative "clacky/cli"

module Clacky
  class AgentInterrupted < Exception; end  # Inherit from Exception to bypass rescue StandardError
  class AgentError < StandardError; end
  class BadRequestError < AgentError; end  # 400 errors — our request was malformed, history should be rolled back
  class RetryableError < StandardError; end  # Transient errors that should be retried (5xx, HTML response, rate limit)
  # Upstream (model/router like OpenRouter/Bedrock) returned finish_reason="stop" together with
  # one or more tool_calls whose `arguments` JSON was truncated (empty, "{}" placeholder, or
  # otherwise unparseable). Subclass of RetryableError so it flows through the existing
  # retry/fallback pipeline in LlmCaller#call_llm.
  class UpstreamTruncatedError < RetryableError; end
  class ToolCallError < AgentError; end  # Raised when tool call fails due to invalid parameters
  class BrowserNotReachableError < AgentError; end  # Chrome/Edge not running or remote debugging disabled
  # BrowserManager singleton: Clacky::BrowserManager.instance
end