Class: Clacky::CLI

Inherits:
Thor
  • Object
show all
Defined in:
lib/clacky/cli.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.exit_on_failure?Boolean

Returns:

  • (Boolean)


13
14
15
# File 'lib/clacky/cli.rb', line 13

def self.exit_on_failure?
  true
end

Instance Method Details

#agentObject



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/clacky/cli.rb', line 60

def agent
  # Handle help option
  if options[:help]
    invoke :help, ["agent"]
    return
  end
  agent_config = Clacky::AgentConfig.load

  # Handle session listing
  if options[:list]
    list_sessions
    return
  end

  # Handle Ctrl+C gracefully - raise exception to be caught in the loop
  Signal.trap("INT") do
    Thread.main.raise(Clacky::AgentInterrupted, "Interrupted by user")
  end

  # Validate and get working directory
  working_dir = validate_working_directory(options[:path])

  # Update agent config with CLI options
  agent_config.permission_mode = options[:mode].to_sym if options[:mode]
  agent_config.verbose = options[:verbose] if options[:verbose]

  # Create client for current model
  client = Clacky::Client.new(agent_config.api_key, base_url: agent_config.base_url, model: agent_config.model_name, anthropic_format: agent_config.anthropic_format?)

  # Resolve agent profile name from --agent option
  agent_profile = options[:agent] || "coding"

  # Handle session loading/continuation
  session_manager = Clacky::SessionManager.new
  agent = nil
  is_session_load = false

  if options[:continue]
    agent = load_latest_session(client, agent_config, session_manager, working_dir, profile: agent_profile)
    is_session_load = !agent.nil?
  elsif options[:attach]
    agent = load_session_by_number(client, agent_config, session_manager, working_dir, options[:attach], profile: agent_profile)
    is_session_load = !agent.nil?
  end

  # Create new agent if no session loaded
  if agent.nil?
    agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: nil, profile: agent_profile,
                              session_id: Clacky::SessionManager.generate_id, source: :manual)
    agent.rename("CLI Session")
  end

  # Change to working directory
  original_dir = Dir.pwd
  should_chdir = File.realpath(working_dir) != File.realpath(original_dir)
  Dir.chdir(working_dir) if should_chdir
  begin
    if options[:message]
      file_paths = Array(options[:file]) + Array(options[:image])
      run_non_interactive(agent, options[:message], file_paths, agent_config, session_manager)
    elsif options[:json]
      run_agent_with_json(agent, working_dir, agent_config, session_manager, client, profile: agent_profile)
    else
      run_agent_with_ui2(agent, working_dir, agent_config, session_manager, client, is_session_load: is_session_load)
    end
  ensure
    Dir.chdir(original_dir)
  end
end

#serverObject



836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
# File 'lib/clacky/cli.rb', line 836

def server
  # ── Security gate ──────────────────────────────────────────────────────
  # Binding to 0.0.0.0 exposes the server to the public network.
  # Refuse to start unless CLACKY_ACCESS_KEY env var is set.
  if options[:host] == "0.0.0.0" && ENV.fetch("CLACKY_ACCESS_KEY", "").strip.empty?
    puts <<~MSG
      ╔══════════════════════════════════════════════════════════════╗
      ║  ⚠️  Security Warning: Refusing to start                      ║
      ╠══════════════════════════════════════════════════════════════╣
      ║                                                              ║
      ║  Binding to 0.0.0.0 exposes Clacky to the public network.    ║
      ║  You must set CLACKY_ACCESS_KEY before starting the server.  ║
      ║                                                              ║
      ║  Generate a secure key:                                      ║
      ║    openssl rand -hex 32                                      ║
      ║                                                              ║
      ║  Then export it:                                             ║
      ║    export CLACKY_ACCESS_KEY=<your-generated-key>             ║
      ║                                                              ║
      ╚══════════════════════════════════════════════════════════════╝
    MSG
    exit(1)
  end
  # ─────────────────────────────────────────────────────────────────────

  if ENV["CLACKY_WORKER"] == "1"
    # ── Worker mode ───────────────────────────────────────────────────────
    # Spawned by Master. Inherit the listen socket from the file descriptor
    # passed via CLACKY_INHERIT_FD, and report back to master via CLACKY_MASTER_PID.
    require_relative "server/http_server"

    fd         = ENV["CLACKY_INHERIT_FD"].to_i
    master_pid = ENV["CLACKY_MASTER_PID"].to_i
    # Must use TCPServer.for_fd (not Socket.for_fd) so that accept_nonblock
    # returns a single Socket, not [Socket, Addrinfo] — WEBrick expects the former.
    socket     = TCPServer.for_fd(fd)

    Clacky::Logger.console = true
    Clacky::Logger.info("[cli worker PID=#{Process.pid}] CLACKY_INHERIT_FD=#{fd} CLACKY_MASTER_PID=#{master_pid} socket=#{socket.class} fd=#{socket.fileno}")

    agent_config = Clacky::AgentConfig.load
    agent_config.permission_mode = :confirm_all

    client_factory = lambda do
      Clacky::Client.new(
        agent_config.api_key,
        base_url: agent_config.base_url,
        model: agent_config.model_name,
        anthropic_format: agent_config.anthropic_format?
      )
    end

    Clacky::Server::HttpServer.new(
      host:           options[:host],
      port:           options[:port],
      agent_config:   agent_config,
      client_factory: client_factory,
      brand_test:     options[:brand_test],
      socket:         socket,
      master_pid:     master_pid
    ).start
  else
    # ── Master mode ───────────────────────────────────────────────────────
    # First invocation by the user. Start the Master process which holds the
    # socket and supervises worker processes.
    require_relative "server/server_master"

    if options[:brand_test]
      say "⚡ Brand test mode — license activation uses mock data (no remote API calls).", :yellow
      say ""
      say "  Test license keys (paste any into Settings → Brand & License):", :cyan
      say ""
      say "    00000001-FFFFFFFF-DEADBEEF-CAFEBABE-00000001  →  Brand1"
      say "    00000002-FFFFFFFF-DEADBEEF-CAFEBABE-00000002  →  Brand2"
      say "    00000003-FFFFFFFF-DEADBEEF-CAFEBABE-00000003  →  Brand3"
      say ""
      say "  To reset: rm ~/.clacky/brand.yml", :cyan
      say ""
    end

    extra_flags = []
    extra_flags << "--brand-test" if options[:brand_test]

    Clacky::Logger.console = true

    Clacky::Server::Master.new(
      host:        options[:host],
      port:        options[:port],
      extra_flags: extra_flags
    ).run
  end
end