Class: Ralph::CLI

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

Instance Method Summary collapse

Constructor Details

#initialize(argv = ARGV) ⇒ CLI

Returns a new instance of CLI.



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/ralph/cli.rb', line 5

def initialize(argv = ARGV)
  @config = Config.new

  @parser = OptionParser.new do |o|
    o.banner = <<~BANNER
      Ralph Wiggum Loop - Iterative AI development with AI agents

      Usage:
        ralph "<prompt>" [options]

      Commands:
        --status            Show current Ralph loop status and history
        --add-context TEXT  Add context for the next iteration
        --clear-context     Clear any pending context
        --list-tasks        Display the current task list with indices
        --add-task "desc"   Add a new task to the list
        --remove-task N     Remove task at index N (including subtasks)

      Options:
    BANNER

    o.on("--agent AGENT", Agents.valid_agent_names, "AI agent: #{Agents.valid_agent_names.join(', ')} (default: opencode)") do |v|
      @config.chosen_agent = v
    end

    o.on("--min-iterations N", Integer, "Minimum iterations before completion (default: 1)") do |v|
      @config.min_iterations = v
    end

    o.on("--max-iterations N", Integer, "Maximum iterations before stopping (default: unlimited)") do |v|
      @config.max_iterations = v
    end

    o.on("--completion-promise TEXT", "Phrase that signals completion (default: COMPLETE)") do |v|
      @config.completion_promise = v
    end

    o.on("-t", "--tasks", "Enable Tasks Mode for structured task tracking") do
      @config.tasks_mode = true
    end

    o.on("--task-promise TEXT", "Phrase that signals task completion (default: READY_FOR_NEXT_TASK)") do |v|
      @config.task_promise = v
    end

    o.on("--model MODEL", "Model to use (agent-specific)") do |v|
      @config.model = v
    end

    o.on("--[no-]stream", "Stream agent output in real-time (default: on)") do |v|
      @config.stream_output = v
    end

    o.on("--verbose-tools", "Print every tool line (disable compact summary)") do
      @config.verbose_tools = true
    end

    o.on("--no-plugins", "Disable non-auth OpenCode plugins (opencode only)") do
      @config.disable_plugins = true
    end

    o.on("--[no-]allow-all", "Auto-approve all tool permissions (default: on)") do |v|
      @config.allow_all_permissions = v
    end

    # Subcommands -- these set a command to dispatch after parsing
    o.on("-v", "--version", "Show version") do
      puts "ralph #{VERSION}"
      exit 0
    end

    o.on("--status", "Show current loop status and history") do
      Output::Status.call(options: @config.to_h)
      exit 0
    end

    o.on("--add-context TEXT", "Add context for the next iteration") do |context_text|

      Storage::Context.new.append(
        "\n## Context added at #{Time.now.utc.iso8601}\n#{context_text}\n"
      )

      puts "✅ Context added for next iteration"
      puts "   File: #{Storage::Context.new.path}"

      Storage::State.load.then do |state|
        if state&.active
          puts "   Will be picked up in iteration #{state.iteration + 1}"
        else
          puts "   Will be used when loop starts"
        end
      end

      exit 0
    end

    o.on("--clear-context", "Clear any pending context") do
      Storage::Context.new.then do |context|
        if context.present?
          context.clear
          puts "✅ Context cleared"
        else
          puts "ℹ️  No pending context to clear"
        end
      end

      exit 0
    end

    o.on("--list-tasks", "Display the current task list") do
      begin
        Storage::Tasks.new.load_tasks.then do |tasks|
          if tasks
            tasks.display_with_indices
          else
            puts "No tasks file found. Use --add-task to create your first task."
          end
        end
      rescue StandardError => e
        $stderr.puts "Error reading tasks file: #{e}"
        exit 1
      end

      exit 0
    end

    o.on("--add-task DESC", "Add a new task to the list") do |description|
      begin
        Storage::Tasks.new.add_task(description)
        puts "✅ Task added: \"#{description}\""
      rescue StandardError => e
        $stderr.puts "Error adding task: #{e}"
        exit 1
      end

      exit 0
    end

    o.on("--remove-task N", Integer, "Remove task at index N") do |task_index|
      begin
        Storage::Tasks.new.remove_task(task_index)
        puts "✅ Removed task #{task_index} and its subtasks"
      rescue IndexError => e
        $stderr.puts "Error: #{e.message}"
        exit 1
      rescue RuntimeError => e
        $stderr.puts "Error: #{e.message}"
        exit 1
      rescue StandardError => e
        $stderr.puts "Error removing task: #{e}"
        exit 1
      end

      exit 0
    end

    o.separator ""
    o.separator "Examples:"
    o.separator '  ralph "Build a REST API for todos"'
    o.separator '  ralph "Fix the auth bug" --max-iterations 10'
    o.separator '  ralph "Add tests" --completion-promise "ALL TESTS PASS" --model openai/gpt-5.1'
    o.separator '  ralph --status'
    o.separator '  ralph --add-context "Focus on the auth module first"'
    o.separator ""
    o.separator "How it works:"
    o.separator "  1. Sends your prompt to the selected AI agent"
    o.separator "  2. AI agent works on the task"
    o.separator "  3. Checks output for completion promise"
    o.separator "  4. If not complete, repeats with same prompt"
    o.separator "  5. AI sees its previous work in files"
    o.separator "  6. Continues until promise detected or max iterations"
    o.separator ""
    o.separator "To stop manually: Ctrl+C"
    o.separator "Learn more: https://ghuntley.com/ralph/"
  end

end

Instance Method Details

#run(argv = ARGV.dup) ⇒ Object



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/ralph/cli.rb', line 183

def run(argv = ARGV.dup)
  @user_prompt = @parser.parse(argv).then do |remaining_args|
    if $stdin.tty?
      remaining_args.join(" ").strip
    else
      [$stdin.read, remaining_args.join(" ")].join("\n").strip
    end
  end

  if @user_prompt.empty?
    abort "
      Error: No prompt provided
      Usage: ralph 'Your task description' [options]
      Run 'ralph --help' for more information
    "
  end

  tasks   = Storage::Tasks.new
  context = Storage::Context.new

  PromptTemplate.inject(@user_prompt, context: context, tasks: tasks).then do |prompt|
    @config.prompt = prompt

    if @config.max_iterations > 0 && @config.min_iterations > @config.max_iterations
      abort "Error: --min-iterations (#{@config.min_iterations}) cannot be greater than --max-iterations (#{@config.max_iterations})"
    end

    state   = Storage::State.from_config(@config, prompt: prompt)
    history = Storage::History.new

    Ralph::Loop.new(@config, state, history, context, tasks).run
  end

rescue StandardError => e
  $stderr.puts "Fatal error: #{e}"
  Storage::State.clear
  exit 1
end