rubyn-code 0.2.2 → 0.3.0
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 +4 -4
- data/README.md +91 -3
- data/lib/rubyn_code/agent/background_job_handler.rb +71 -0
- data/lib/rubyn_code/agent/conversation.rb +55 -56
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +99 -0
- data/lib/rubyn_code/agent/feedback_handler.rb +49 -0
- data/lib/rubyn_code/agent/llm_caller.rb +149 -0
- data/lib/rubyn_code/agent/loop.rb +175 -683
- data/lib/rubyn_code/agent/loop_detector.rb +50 -11
- data/lib/rubyn_code/agent/prompts.rb +109 -0
- data/lib/rubyn_code/agent/response_modes.rb +111 -0
- data/lib/rubyn_code/agent/response_parser.rb +111 -0
- data/lib/rubyn_code/agent/system_prompt_builder.rb +205 -0
- data/lib/rubyn_code/agent/tool_processor.rb +158 -0
- data/lib/rubyn_code/agent/usage_tracker.rb +59 -0
- data/lib/rubyn_code/auth/oauth.rb +80 -64
- data/lib/rubyn_code/auth/server.rb +21 -24
- data/lib/rubyn_code/auth/token_store.rb +31 -44
- data/lib/rubyn_code/autonomous/daemon.rb +29 -18
- data/lib/rubyn_code/autonomous/idle_poller.rb +4 -4
- data/lib/rubyn_code/autonomous/task_claimer.rb +36 -40
- data/lib/rubyn_code/background/worker.rb +64 -76
- data/lib/rubyn_code/cli/app.rb +128 -114
- data/lib/rubyn_code/cli/commands/model.rb +75 -18
- data/lib/rubyn_code/cli/commands/new_session.rb +45 -0
- data/lib/rubyn_code/cli/daemon_runner.rb +28 -11
- data/lib/rubyn_code/cli/renderer.rb +109 -60
- data/lib/rubyn_code/cli/repl.rb +42 -373
- data/lib/rubyn_code/cli/repl_commands.rb +176 -0
- data/lib/rubyn_code/cli/repl_lifecycle.rb +75 -0
- data/lib/rubyn_code/cli/repl_setup.rb +145 -0
- data/lib/rubyn_code/cli/setup.rb +6 -2
- data/lib/rubyn_code/cli/stream_formatter.rb +56 -49
- data/lib/rubyn_code/cli/version_check.rb +28 -11
- data/lib/rubyn_code/config/defaults.rb +10 -0
- data/lib/rubyn_code/config/project_profile.rb +185 -0
- data/lib/rubyn_code/config/settings.rb +100 -1
- data/lib/rubyn_code/context/auto_compact.rb +1 -1
- data/lib/rubyn_code/context/context_budget.rb +167 -0
- data/lib/rubyn_code/context/decision_compactor.rb +99 -0
- data/lib/rubyn_code/context/manager.rb +7 -5
- data/lib/rubyn_code/context/micro_compact.rb +29 -19
- data/lib/rubyn_code/context/schema_filter.rb +64 -0
- data/lib/rubyn_code/db/connection.rb +31 -26
- data/lib/rubyn_code/db/migrator.rb +44 -28
- data/lib/rubyn_code/hooks/built_in.rb +14 -10
- data/lib/rubyn_code/index/codebase_index.rb +245 -0
- data/lib/rubyn_code/learning/extractor.rb +65 -82
- data/lib/rubyn_code/learning/injector.rb +22 -23
- data/lib/rubyn_code/learning/instinct.rb +71 -42
- data/lib/rubyn_code/learning/shortcut.rb +95 -0
- data/lib/rubyn_code/llm/adapters/anthropic.rb +270 -0
- data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +215 -0
- data/lib/rubyn_code/llm/adapters/base.rb +35 -0
- data/lib/rubyn_code/llm/adapters/json_parsing.rb +21 -0
- data/lib/rubyn_code/llm/adapters/openai.rb +246 -0
- data/lib/rubyn_code/llm/adapters/openai_compatible.rb +46 -0
- data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +90 -0
- data/lib/rubyn_code/llm/adapters/openai_streaming.rb +141 -0
- data/lib/rubyn_code/llm/adapters/prompt_caching.rb +60 -0
- data/lib/rubyn_code/llm/client.rb +55 -252
- data/lib/rubyn_code/llm/model_router.rb +237 -0
- data/lib/rubyn_code/llm/streaming.rb +4 -227
- data/lib/rubyn_code/mcp/client.rb +1 -1
- data/lib/rubyn_code/mcp/config.rb +9 -12
- data/lib/rubyn_code/mcp/sse_transport.rb +15 -13
- data/lib/rubyn_code/mcp/stdio_transport.rb +16 -18
- data/lib/rubyn_code/mcp/tool_bridge.rb +31 -62
- data/lib/rubyn_code/memory/session_persistence.rb +59 -58
- data/lib/rubyn_code/memory/store.rb +42 -55
- data/lib/rubyn_code/observability/budget_enforcer.rb +46 -32
- data/lib/rubyn_code/observability/cost_calculator.rb +32 -8
- data/lib/rubyn_code/observability/skill_analytics.rb +116 -0
- data/lib/rubyn_code/observability/token_analytics.rb +130 -0
- data/lib/rubyn_code/observability/usage_reporter.rb +79 -61
- data/lib/rubyn_code/output/diff_renderer.rb +102 -77
- data/lib/rubyn_code/output/formatter.rb +11 -11
- data/lib/rubyn_code/permissions/policy.rb +11 -13
- data/lib/rubyn_code/permissions/prompter.rb +8 -9
- data/lib/rubyn_code/protocols/plan_approval.rb +25 -20
- data/lib/rubyn_code/skills/document.rb +33 -29
- data/lib/rubyn_code/skills/ttl_manager.rb +100 -0
- data/lib/rubyn_code/sub_agents/runner.rb +20 -25
- data/lib/rubyn_code/tasks/dag.rb +25 -24
- data/lib/rubyn_code/tools/ask_user.rb +44 -0
- data/lib/rubyn_code/tools/background_run.rb +2 -1
- data/lib/rubyn_code/tools/base.rb +26 -32
- data/lib/rubyn_code/tools/bash.rb +2 -1
- data/lib/rubyn_code/tools/edit_file.rb +74 -18
- data/lib/rubyn_code/tools/executor.rb +74 -24
- data/lib/rubyn_code/tools/file_cache.rb +95 -0
- data/lib/rubyn_code/tools/git_commit.rb +12 -10
- data/lib/rubyn_code/tools/git_log.rb +12 -10
- data/lib/rubyn_code/tools/glob.rb +23 -7
- data/lib/rubyn_code/tools/grep.rb +2 -1
- data/lib/rubyn_code/tools/load_skill.rb +13 -6
- data/lib/rubyn_code/tools/memory_search.rb +14 -13
- data/lib/rubyn_code/tools/memory_write.rb +2 -1
- data/lib/rubyn_code/tools/output_compressor.rb +185 -0
- data/lib/rubyn_code/tools/read_file.rb +11 -6
- data/lib/rubyn_code/tools/review_pr.rb +127 -80
- data/lib/rubyn_code/tools/run_specs.rb +26 -15
- data/lib/rubyn_code/tools/schema.rb +4 -10
- data/lib/rubyn_code/tools/spawn_agent.rb +113 -82
- data/lib/rubyn_code/tools/spawn_teammate.rb +107 -64
- data/lib/rubyn_code/tools/spec_output_parser.rb +118 -0
- data/lib/rubyn_code/tools/task.rb +17 -17
- data/lib/rubyn_code/tools/web_fetch.rb +62 -47
- data/lib/rubyn_code/tools/web_search.rb +66 -48
- data/lib/rubyn_code/tools/write_file.rb +59 -1
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +40 -1
- data/skills/rubyn_self_test.md +121 -0
- metadata +53 -1
|
@@ -35,9 +35,9 @@ module RubynCode
|
|
|
35
35
|
return :shutdown if monotonic_now >= deadline
|
|
36
36
|
|
|
37
37
|
# Messages always take priority over tasks.
|
|
38
|
-
return :resume if
|
|
38
|
+
return :resume if pending_messages?
|
|
39
39
|
|
|
40
|
-
return :resume if
|
|
40
|
+
return :resume if claimable_task?
|
|
41
41
|
|
|
42
42
|
remaining = deadline - monotonic_now
|
|
43
43
|
return :shutdown if remaining <= 0
|
|
@@ -76,7 +76,7 @@ module RubynCode
|
|
|
76
76
|
private
|
|
77
77
|
|
|
78
78
|
# @return [Boolean]
|
|
79
|
-
def
|
|
79
|
+
def pending_messages?
|
|
80
80
|
messages = @mailbox.pending_for(@agent_name)
|
|
81
81
|
messages.is_a?(Array) ? !messages.empty? : false
|
|
82
82
|
rescue StandardError
|
|
@@ -84,7 +84,7 @@ module RubynCode
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# @return [Boolean]
|
|
87
|
-
def
|
|
87
|
+
def claimable_task?
|
|
88
88
|
rows = @task_manager.db.query(<<~SQL).to_a
|
|
89
89
|
SELECT 1 FROM tasks
|
|
90
90
|
WHERE status = 'pending'
|
|
@@ -15,47 +15,9 @@ module RubynCode
|
|
|
15
15
|
# @return [Tasks::Task, nil] the claimed task, or nil if none available
|
|
16
16
|
def self.call(task_manager:, agent_name:)
|
|
17
17
|
db = task_manager.db
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# ensure that only pending tasks with no current owner are touched,
|
|
21
|
-
# avoiding race conditions with other agents.
|
|
22
|
-
db.execute(<<~SQL, [agent_name])
|
|
23
|
-
UPDATE tasks
|
|
24
|
-
SET owner = ?,
|
|
25
|
-
status = 'in_progress',
|
|
26
|
-
updated_at = datetime('now')
|
|
27
|
-
WHERE id = (
|
|
28
|
-
SELECT id FROM tasks
|
|
29
|
-
WHERE status = 'pending'
|
|
30
|
-
AND (owner IS NULL OR owner = '')
|
|
31
|
-
ORDER BY priority DESC, created_at ASC
|
|
32
|
-
LIMIT 1
|
|
33
|
-
)
|
|
34
|
-
AND status = 'pending'
|
|
35
|
-
AND (owner IS NULL OR owner = '')
|
|
36
|
-
SQL
|
|
37
|
-
|
|
38
|
-
# Fetch the task we just claimed. Using owner + status filters
|
|
39
|
-
# ensures we only retrieve a task that *this* agent successfully
|
|
40
|
-
# claimed (another agent cannot have flipped it in between).
|
|
41
|
-
rows = db.query(<<~SQL, [agent_name]).to_a
|
|
42
|
-
SELECT id, session_id, title, description, status,
|
|
43
|
-
priority, owner, result, metadata, created_at, updated_at
|
|
44
|
-
FROM tasks
|
|
45
|
-
WHERE owner = ?
|
|
46
|
-
AND status = 'in_progress'
|
|
47
|
-
ORDER BY updated_at DESC
|
|
48
|
-
LIMIT 1
|
|
49
|
-
SQL
|
|
50
|
-
|
|
51
|
-
return nil if rows.empty?
|
|
52
|
-
|
|
53
|
-
row = rows.first
|
|
54
|
-
build_task(row)
|
|
18
|
+
claim_next_pending_task(db, agent_name)
|
|
19
|
+
fetch_claimed_task(db, agent_name)
|
|
55
20
|
rescue StandardError => e
|
|
56
|
-
# If anything goes wrong (e.g. task was already claimed between
|
|
57
|
-
# our SELECT and UPDATE, or a constraint violation) we treat it
|
|
58
|
-
# as "no work available" rather than crashing the daemon.
|
|
59
21
|
RubynCode.logger.warn("TaskClaimer: failed to claim task: #{e.message}") if RubynCode.respond_to?(:logger)
|
|
60
22
|
nil
|
|
61
23
|
end
|
|
@@ -63,6 +25,40 @@ module RubynCode
|
|
|
63
25
|
class << self
|
|
64
26
|
private
|
|
65
27
|
|
|
28
|
+
def claim_next_pending_task(db, agent_name)
|
|
29
|
+
db.execute(<<~SQL, [agent_name])
|
|
30
|
+
UPDATE tasks
|
|
31
|
+
SET owner = ?,
|
|
32
|
+
status = 'in_progress',
|
|
33
|
+
updated_at = datetime('now')
|
|
34
|
+
WHERE id = (
|
|
35
|
+
SELECT id FROM tasks
|
|
36
|
+
WHERE status = 'pending'
|
|
37
|
+
AND (owner IS NULL OR owner = '')
|
|
38
|
+
ORDER BY priority DESC, created_at ASC
|
|
39
|
+
LIMIT 1
|
|
40
|
+
)
|
|
41
|
+
AND status = 'pending'
|
|
42
|
+
AND (owner IS NULL OR owner = '')
|
|
43
|
+
SQL
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def fetch_claimed_task(db, agent_name)
|
|
47
|
+
rows = db.query(<<~SQL, [agent_name]).to_a
|
|
48
|
+
SELECT id, session_id, title, description, status,
|
|
49
|
+
priority, owner, result, metadata, created_at, updated_at
|
|
50
|
+
FROM tasks
|
|
51
|
+
WHERE owner = ?
|
|
52
|
+
AND status = 'in_progress'
|
|
53
|
+
ORDER BY updated_at DESC
|
|
54
|
+
LIMIT 1
|
|
55
|
+
SQL
|
|
56
|
+
|
|
57
|
+
return nil if rows.empty?
|
|
58
|
+
|
|
59
|
+
build_task(rows.first)
|
|
60
|
+
end
|
|
61
|
+
|
|
66
62
|
# @param row [Hash] a database row hash
|
|
67
63
|
# @return [Tasks::Task]
|
|
68
64
|
def build_task(row)
|
|
@@ -93,91 +93,79 @@ module RubynCode
|
|
|
93
93
|
private
|
|
94
94
|
|
|
95
95
|
def execute_job(job_id, command, timeout_seconds)
|
|
96
|
-
stdout, stderr, =
|
|
97
|
-
final_status = :completed
|
|
98
|
-
|
|
99
|
-
begin
|
|
100
|
-
stdin_io, stdout_io, stderr_io, wait_thr = Open3.popen3(command, chdir: @project_root)
|
|
101
|
-
stdin_io.close
|
|
102
|
-
out_buf = +''
|
|
103
|
-
err_buf = +''
|
|
104
|
-
out_reader = Thread.new do
|
|
105
|
-
out_buf << stdout_io.read
|
|
106
|
-
rescue StandardError
|
|
107
|
-
nil
|
|
108
|
-
end
|
|
109
|
-
err_reader = Thread.new do
|
|
110
|
-
err_buf << stderr_io.read
|
|
111
|
-
rescue StandardError
|
|
112
|
-
nil
|
|
113
|
-
end
|
|
96
|
+
stdout, stderr, final_status = run_process(command, timeout_seconds)
|
|
114
97
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Process.kill('TERM', wait_thr.pid)
|
|
118
|
-
rescue StandardError
|
|
119
|
-
nil
|
|
120
|
-
end
|
|
121
|
-
sleep 0.1
|
|
122
|
-
begin
|
|
123
|
-
Process.kill('KILL', wait_thr.pid)
|
|
124
|
-
rescue StandardError
|
|
125
|
-
nil
|
|
126
|
-
end
|
|
127
|
-
wait_thr.join(5)
|
|
128
|
-
out_reader.join(2)
|
|
129
|
-
err_reader.join(2)
|
|
130
|
-
[stdout_io, stderr_io].each do |io|
|
|
131
|
-
io.close
|
|
132
|
-
rescue StandardError
|
|
133
|
-
nil
|
|
134
|
-
end
|
|
135
|
-
raise Timeout::Error
|
|
136
|
-
end
|
|
98
|
+
result = build_result(stdout, stderr)
|
|
99
|
+
completed_job = finalize_job(job_id, command, final_status, result)
|
|
137
100
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
101
|
+
@notifier.push({
|
|
102
|
+
type: :job_completed, job_id: job_id,
|
|
103
|
+
status: final_status, result: result,
|
|
104
|
+
duration: completed_job.duration
|
|
105
|
+
})
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def run_process(command, timeout_seconds)
|
|
109
|
+
stdin_io, stdout_io, stderr_io, wait_thr = Open3.popen3(command, chdir: @project_root)
|
|
110
|
+
stdin_io.close
|
|
111
|
+
io_state = { stdout_io: stdout_io, stderr_io: stderr_io }
|
|
112
|
+
out_reader, err_reader, out_buf, err_buf = start_readers(stdout_io, stderr_io)
|
|
113
|
+
io_state.merge!(out_reader: out_reader, err_reader: err_reader)
|
|
114
|
+
|
|
115
|
+
handle_wait(wait_thr, timeout_seconds, io_state)
|
|
116
|
+
|
|
117
|
+
status = wait_thr.value.success? ? :completed : :error
|
|
118
|
+
[out_buf, err_buf, status]
|
|
119
|
+
rescue Timeout::Error
|
|
120
|
+
[nil, "Command timed out after #{timeout_seconds} seconds", :timeout]
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
[nil, e.message, :error]
|
|
123
|
+
end
|
|
145
124
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
rescue
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
125
|
+
def start_readers(stdout_io, stderr_io)
|
|
126
|
+
out_buf = +''
|
|
127
|
+
err_buf = +''
|
|
128
|
+
out_reader = Thread.new { out_buf << stdout_io.read rescue nil } # rubocop:disable Style/RescueModifier
|
|
129
|
+
err_reader = Thread.new { err_buf << stderr_io.read rescue nil } # rubocop:disable Style/RescueModifier
|
|
130
|
+
[out_reader, err_reader, out_buf, err_buf]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def handle_wait(wait_thr, timeout_seconds, io_state)
|
|
134
|
+
unless wait_thr.join(timeout_seconds)
|
|
135
|
+
kill_process(wait_thr)
|
|
136
|
+
cleanup_io(io_state)
|
|
137
|
+
raise Timeout::Error
|
|
158
138
|
end
|
|
159
139
|
|
|
160
|
-
|
|
161
|
-
|
|
140
|
+
cleanup_io(io_state)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def cleanup_io(io_state)
|
|
144
|
+
cleanup_readers(io_state[:out_reader], io_state[:err_reader],
|
|
145
|
+
io_state[:stdout_io], io_state[:stderr_io])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def kill_process(wait_thr)
|
|
149
|
+
Process.kill('TERM', wait_thr.pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
150
|
+
sleep 0.1
|
|
151
|
+
Process.kill('KILL', wait_thr.pid) rescue nil # rubocop:disable Style/RescueModifier
|
|
152
|
+
wait_thr.join(5)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def cleanup_readers(out_reader, err_reader, stdout_io, stderr_io)
|
|
156
|
+
out_reader.join(5)
|
|
157
|
+
err_reader.join(5)
|
|
158
|
+
[stdout_io, stderr_io].each { |io| io.close rescue nil } # rubocop:disable Style/RescueModifier
|
|
159
|
+
end
|
|
162
160
|
|
|
163
|
-
|
|
161
|
+
def finalize_job(job_id, command, final_status, result)
|
|
162
|
+
@mutex.synchronize do
|
|
164
163
|
@jobs[job_id] = Job.new(
|
|
165
|
-
id: job_id,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
result: result,
|
|
169
|
-
started_at: @jobs[job_id].started_at,
|
|
170
|
-
completed_at: completed_at
|
|
164
|
+
id: job_id, command: command, status: final_status,
|
|
165
|
+
result: result, started_at: @jobs[job_id].started_at,
|
|
166
|
+
completed_at: Time.now
|
|
171
167
|
)
|
|
172
168
|
end
|
|
173
|
-
|
|
174
|
-
@notifier.push({
|
|
175
|
-
type: :job_completed,
|
|
176
|
-
job_id: job_id,
|
|
177
|
-
status: final_status,
|
|
178
|
-
result: result,
|
|
179
|
-
duration: completed_job.duration
|
|
180
|
-
})
|
|
181
169
|
end
|
|
182
170
|
|
|
183
171
|
def build_result(stdout, stderr)
|
data/lib/rubyn_code/cli/app.rb
CHANGED
|
@@ -14,71 +14,120 @@ module RubynCode
|
|
|
14
14
|
|
|
15
15
|
def run
|
|
16
16
|
RubynCode::Debug.enable! if @options[:debug]
|
|
17
|
-
|
|
18
|
-
case @options[:command]
|
|
19
|
-
when :version
|
|
20
|
-
puts "rubyn-code #{RubynCode::VERSION}"
|
|
21
|
-
when :auth
|
|
22
|
-
run_auth
|
|
23
|
-
when :setup
|
|
24
|
-
run_setup
|
|
25
|
-
when :help
|
|
26
|
-
display_help
|
|
27
|
-
when :run
|
|
28
|
-
run_single_prompt(@options[:prompt])
|
|
29
|
-
when :daemon
|
|
30
|
-
run_daemon
|
|
31
|
-
when :repl
|
|
32
|
-
run_repl
|
|
33
|
-
end
|
|
17
|
+
dispatch_command(@options[:command])
|
|
34
18
|
end
|
|
35
19
|
|
|
20
|
+
HELP_TEXT = <<~HELP
|
|
21
|
+
rubyn-code - Ruby & Rails Agentic Coding Assistant
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
rubyn-code Start interactive REPL
|
|
25
|
+
rubyn-code -p "prompt" Run a single prompt and exit
|
|
26
|
+
rubyn-code --resume [ID] Resume a previous session
|
|
27
|
+
rubyn-code --setup Pin rubyn-code to bypass rbenv/rvm
|
|
28
|
+
rubyn-code --auth Authenticate with Claude
|
|
29
|
+
rubyn-code --version Show version
|
|
30
|
+
rubyn-code --help Show this help
|
|
31
|
+
|
|
32
|
+
Daemon Mode:
|
|
33
|
+
rubyn-code daemon Start autonomous daemon (GOLEM)
|
|
34
|
+
rubyn-code daemon --name NAME Agent name (default: golem-<random>)
|
|
35
|
+
rubyn-code daemon --role ROLE Agent role description
|
|
36
|
+
rubyn-code daemon --max-runs N Max tasks before shutdown (default: 100)
|
|
37
|
+
rubyn-code daemon --max-cost N Max USD spend before shutdown (default: 10.0)
|
|
38
|
+
rubyn-code daemon --idle-timeout N Seconds idle before shutdown (default: 60)
|
|
39
|
+
rubyn-code daemon --poll-interval N Seconds between polls (default: 5)
|
|
40
|
+
|
|
41
|
+
Interactive Commands:
|
|
42
|
+
/help Show available commands
|
|
43
|
+
/quit Exit
|
|
44
|
+
/compact Compress context
|
|
45
|
+
/cost Show usage costs
|
|
46
|
+
/tasks List tasks
|
|
47
|
+
/skill [name] Load or list skills
|
|
48
|
+
|
|
49
|
+
Environment:
|
|
50
|
+
Config: ~/.rubyn-code/config.yml
|
|
51
|
+
Data: ~/.rubyn-code/rubyn_code.db
|
|
52
|
+
Tokens: ~/.rubyn-code/tokens.yml
|
|
53
|
+
HELP
|
|
54
|
+
|
|
55
|
+
SIMPLE_FLAGS = {
|
|
56
|
+
'--version' => :version, '-v' => :version,
|
|
57
|
+
'--help' => :help, '-h' => :help,
|
|
58
|
+
'--auth' => :auth, '--setup' => :setup
|
|
59
|
+
}.freeze
|
|
60
|
+
BOOLEAN_FLAGS = { '--yolo' => :yolo, '--debug' => :debug }.freeze
|
|
61
|
+
DAEMON_INT_FLAGS = { '--max-runs' => :max_runs, '--idle-timeout' => :idle_timeout,
|
|
62
|
+
'--poll-interval' => :poll_interval }.freeze
|
|
63
|
+
DAEMON_STR_FLAGS = { '--name' => :agent_name, '--role' => :role }.freeze
|
|
64
|
+
|
|
36
65
|
private
|
|
37
66
|
|
|
38
|
-
def
|
|
67
|
+
def dispatch_command(command) # rubocop:disable Metrics/CyclomaticComplexity -- unavoidable dispatch switch
|
|
68
|
+
case command
|
|
69
|
+
when :version then puts "rubyn-code #{RubynCode::VERSION}"
|
|
70
|
+
when :auth then run_auth
|
|
71
|
+
when :setup then run_setup
|
|
72
|
+
when :help then display_help
|
|
73
|
+
when :run then run_single_prompt(@options[:prompt])
|
|
74
|
+
when :daemon then run_daemon
|
|
75
|
+
when :repl then run_repl
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_options(argv)
|
|
39
80
|
options = { command: :repl }
|
|
81
|
+
idx = 0
|
|
82
|
+
while idx < argv.length
|
|
83
|
+
idx = parse_single_option(argv, idx, options)
|
|
84
|
+
idx += 1
|
|
85
|
+
end
|
|
86
|
+
options
|
|
87
|
+
end
|
|
40
88
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
when '--resume', '-r'
|
|
51
|
-
options[:session_id] = argv[i + 1]
|
|
52
|
-
i += 1
|
|
53
|
-
when '-p', '--prompt'
|
|
54
|
-
options[:command] = :run
|
|
55
|
-
options[:prompt] = argv[i + 1]
|
|
56
|
-
i += 1
|
|
57
|
-
when '--yolo'
|
|
58
|
-
options[:yolo] = true
|
|
59
|
-
when '--debug'
|
|
60
|
-
options[:debug] = true
|
|
61
|
-
when '--setup'
|
|
62
|
-
options[:command] = :setup
|
|
63
|
-
when 'daemon'
|
|
64
|
-
options[:command] = :daemon
|
|
65
|
-
parse_daemon_options!(argv, i + 1, options)
|
|
66
|
-
break
|
|
67
|
-
end
|
|
68
|
-
i += 1
|
|
89
|
+
# -- option parser
|
|
90
|
+
def parse_single_option(argv, idx, options)
|
|
91
|
+
arg = argv[idx]
|
|
92
|
+
if SIMPLE_FLAGS.key?(arg)
|
|
93
|
+
options[:command] = SIMPLE_FLAGS[arg]
|
|
94
|
+
elsif BOOLEAN_FLAGS.key?(arg)
|
|
95
|
+
options[BOOLEAN_FLAGS[arg]] = true
|
|
96
|
+
else
|
|
97
|
+
idx = parse_value_option(argv, idx, options)
|
|
69
98
|
end
|
|
99
|
+
idx
|
|
100
|
+
end
|
|
70
101
|
|
|
71
|
-
|
|
102
|
+
def parse_value_option(argv, idx, options)
|
|
103
|
+
case argv[idx]
|
|
104
|
+
when '--resume', '-r'
|
|
105
|
+
options[:session_id] = argv[idx + 1]
|
|
106
|
+
idx + 1
|
|
107
|
+
when '-p', '--prompt'
|
|
108
|
+
options[:command] = :run
|
|
109
|
+
options[:prompt] = argv[idx + 1]
|
|
110
|
+
idx + 1
|
|
111
|
+
when 'daemon'
|
|
112
|
+
options[:command] = :daemon
|
|
113
|
+
parse_daemon_options!(argv, idx + 1, options)
|
|
114
|
+
argv.length - 1
|
|
115
|
+
else
|
|
116
|
+
idx
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def parse_daemon_options!(argv, start, options)
|
|
121
|
+
options[:daemon] = default_daemon_options
|
|
122
|
+
idx = start
|
|
123
|
+
while idx < argv.length
|
|
124
|
+
idx = parse_single_daemon_option(argv, idx, options)
|
|
125
|
+
idx += 1
|
|
126
|
+
end
|
|
72
127
|
end
|
|
73
128
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
# @param argv [Array<String>]
|
|
77
|
-
# @param start [Integer]
|
|
78
|
-
# @param options [Hash]
|
|
79
|
-
# @return [void]
|
|
80
|
-
def parse_daemon_options!(argv, start, options) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity
|
|
81
|
-
options[:daemon] = {
|
|
129
|
+
def default_daemon_options
|
|
130
|
+
{
|
|
82
131
|
max_runs: 100,
|
|
83
132
|
max_cost: 10.0,
|
|
84
133
|
idle_timeout: 60,
|
|
@@ -86,33 +135,31 @@ module RubynCode
|
|
|
86
135
|
agent_name: "golem-#{SecureRandom.hex(4)}",
|
|
87
136
|
role: 'autonomous coding agent'
|
|
88
137
|
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_single_daemon_option(argv, idx, options)
|
|
141
|
+
case argv[idx]
|
|
142
|
+
when '--debug'
|
|
143
|
+
options[:debug] = true
|
|
144
|
+
else
|
|
145
|
+
idx = parse_daemon_value_option(argv, idx, options)
|
|
146
|
+
end
|
|
147
|
+
idx
|
|
148
|
+
end
|
|
89
149
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
i += 1
|
|
102
|
-
when '--poll-interval'
|
|
103
|
-
options[:daemon][:poll_interval] = argv[i + 1].to_i
|
|
104
|
-
i += 1
|
|
105
|
-
when '--name'
|
|
106
|
-
options[:daemon][:agent_name] = argv[i + 1]
|
|
107
|
-
i += 1
|
|
108
|
-
when '--role'
|
|
109
|
-
options[:daemon][:role] = argv[i + 1]
|
|
110
|
-
i += 1
|
|
111
|
-
when '--debug'
|
|
112
|
-
options[:debug] = true
|
|
113
|
-
end
|
|
114
|
-
i += 1
|
|
150
|
+
def parse_daemon_value_option(argv, idx, options) # rubocop:disable Metrics/AbcSize -- option dispatch with hash lookup
|
|
151
|
+
arg = argv[idx]
|
|
152
|
+
daemon = options[:daemon]
|
|
153
|
+
if DAEMON_INT_FLAGS.key?(arg)
|
|
154
|
+
daemon[DAEMON_INT_FLAGS[arg]] = argv[idx + 1].to_i
|
|
155
|
+
elsif arg == '--max-cost'
|
|
156
|
+
daemon[:max_cost] = argv[idx + 1].to_f
|
|
157
|
+
elsif DAEMON_STR_FLAGS.key?(arg)
|
|
158
|
+
daemon[DAEMON_STR_FLAGS[arg]] = argv[idx + 1]
|
|
159
|
+
else
|
|
160
|
+
return idx
|
|
115
161
|
end
|
|
162
|
+
idx + 1
|
|
116
163
|
end
|
|
117
164
|
|
|
118
165
|
def run_auth
|
|
@@ -154,40 +201,7 @@ module RubynCode
|
|
|
154
201
|
end
|
|
155
202
|
|
|
156
203
|
def display_help
|
|
157
|
-
puts
|
|
158
|
-
rubyn-code - Ruby & Rails Agentic Coding Assistant
|
|
159
|
-
|
|
160
|
-
Usage:
|
|
161
|
-
rubyn-code Start interactive REPL
|
|
162
|
-
rubyn-code -p "prompt" Run a single prompt and exit
|
|
163
|
-
rubyn-code --resume [ID] Resume a previous session
|
|
164
|
-
rubyn-code --setup Pin rubyn-code to bypass rbenv/rvm
|
|
165
|
-
rubyn-code --auth Authenticate with Claude
|
|
166
|
-
rubyn-code --version Show version
|
|
167
|
-
rubyn-code --help Show this help
|
|
168
|
-
|
|
169
|
-
Daemon Mode:
|
|
170
|
-
rubyn-code daemon Start autonomous daemon (GOLEM)
|
|
171
|
-
rubyn-code daemon --name NAME Agent name (default: golem-<random>)
|
|
172
|
-
rubyn-code daemon --role ROLE Agent role description
|
|
173
|
-
rubyn-code daemon --max-runs N Max tasks before shutdown (default: 100)
|
|
174
|
-
rubyn-code daemon --max-cost N Max USD spend before shutdown (default: 10.0)
|
|
175
|
-
rubyn-code daemon --idle-timeout N Seconds idle before shutdown (default: 60)
|
|
176
|
-
rubyn-code daemon --poll-interval N Seconds between polls (default: 5)
|
|
177
|
-
|
|
178
|
-
Interactive Commands:
|
|
179
|
-
/help Show available commands
|
|
180
|
-
/quit Exit
|
|
181
|
-
/compact Compress context
|
|
182
|
-
/cost Show usage costs
|
|
183
|
-
/tasks List tasks
|
|
184
|
-
/skill [name] Load or list skills
|
|
185
|
-
|
|
186
|
-
Environment:
|
|
187
|
-
Config: ~/.rubyn-code/config.yml
|
|
188
|
-
Data: ~/.rubyn-code/rubyn_code.db
|
|
189
|
-
Tokens: ~/.rubyn-code/tokens.yml
|
|
190
|
-
HELP
|
|
204
|
+
puts HELP_TEXT
|
|
191
205
|
end
|
|
192
206
|
end
|
|
193
207
|
end
|
|
@@ -5,30 +5,87 @@ module RubynCode
|
|
|
5
5
|
module Commands
|
|
6
6
|
class Model < Base
|
|
7
7
|
def self.command_name = '/model'
|
|
8
|
-
def self.description = 'Show or switch model (/model [
|
|
9
|
-
|
|
10
|
-
KNOWN_MODELS = %w[
|
|
11
|
-
claude-haiku-4-5
|
|
12
|
-
claude-sonnet-4-20250514
|
|
13
|
-
claude-opus-4-20250514
|
|
14
|
-
].freeze
|
|
8
|
+
def self.description = 'Show or switch model (/model [provider:model])'
|
|
15
9
|
|
|
16
10
|
def execute(args, ctx)
|
|
17
11
|
name = args.first
|
|
12
|
+
return show_current(ctx) unless name
|
|
13
|
+
|
|
14
|
+
provider, model = parse_model_arg(name)
|
|
15
|
+
switch_model(provider, model, ctx)
|
|
16
|
+
end
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Parse "provider:model" or just "model".
|
|
21
|
+
# Examples:
|
|
22
|
+
# "openai:gpt-4o" → ["openai", "gpt-4o"]
|
|
23
|
+
# "claude-sonnet-4-20250514" → [nil, "claude-sonnet-4-20250514"]
|
|
24
|
+
# "anthropic:" → ["anthropic", nil]
|
|
25
|
+
def parse_model_arg(arg)
|
|
26
|
+
return [arg.chomp(':'), nil] if arg.end_with?(':')
|
|
27
|
+
return [Regexp.last_match(1), Regexp.last_match(2)] if arg.match(/\A([^:]+):(.+)\z/)
|
|
28
|
+
|
|
29
|
+
[nil, arg]
|
|
30
|
+
end
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
def switch_model(provider, model, ctx)
|
|
33
|
+
if provider
|
|
34
|
+
switch_provider_and_model(provider, model, ctx)
|
|
28
35
|
else
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
switch_model_only(model, ctx)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def switch_provider_and_model(provider, model, ctx)
|
|
41
|
+
validate_model_for_provider!(provider, model, ctx) if model
|
|
42
|
+
ctx.renderer.info("Switched to provider: #{provider}#{", model: #{model}" if model}")
|
|
43
|
+
{ action: :set_provider, provider: provider, model: model }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def switch_model_only(model, ctx)
|
|
47
|
+
unless known_model?(model, ctx)
|
|
48
|
+
ctx.renderer.warning("Unknown model: #{model}")
|
|
49
|
+
show_available(ctx)
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
ctx.renderer.info("Model switched to #{model}")
|
|
54
|
+
{ action: :set_model, model: model }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_model_for_provider!(provider, model, ctx)
|
|
58
|
+
adapter_models = models_for_provider(provider)
|
|
59
|
+
return if adapter_models.empty? # Unknown provider — can't validate
|
|
60
|
+
return if adapter_models.include?(model)
|
|
61
|
+
|
|
62
|
+
ctx.renderer.warning("Unknown model '#{model}' for #{provider}. Known: #{adapter_models.join(', ')}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def show_current(ctx)
|
|
66
|
+
client = ctx.llm_client
|
|
67
|
+
provider = client.provider_name
|
|
68
|
+
current = client.model
|
|
69
|
+
ctx.renderer.info("Provider: #{provider}")
|
|
70
|
+
ctx.renderer.info("Current model: #{current}")
|
|
71
|
+
show_available(ctx)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def show_available(ctx)
|
|
75
|
+
client = ctx.llm_client
|
|
76
|
+
ctx.renderer.info("Available: #{client.models.join(', ')}")
|
|
77
|
+
ctx.renderer.info('Tip: /model provider:model to switch providers (e.g., /model openai:gpt-4o)')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def known_model?(model, ctx)
|
|
81
|
+
ctx.llm_client.models.include?(model)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def models_for_provider(provider)
|
|
85
|
+
case provider
|
|
86
|
+
when 'anthropic' then LLM::Adapters::Anthropic::AVAILABLE_MODELS
|
|
87
|
+
when 'openai' then LLM::Adapters::OpenAI::AVAILABLE_MODELS
|
|
88
|
+
else []
|
|
32
89
|
end
|
|
33
90
|
end
|
|
34
91
|
end
|