rubyn-code 0.2.2 → 0.4.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 +151 -5
- data/db/migrations/013_add_failed_status_to_tasks.rb +51 -0
- data/lib/rubyn_code/agent/background_job_handler.rb +71 -0
- data/lib/rubyn_code/agent/conversation.rb +84 -56
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +152 -0
- data/lib/rubyn_code/agent/feedback_handler.rb +49 -0
- data/lib/rubyn_code/agent/llm_caller.rb +157 -0
- data/lib/rubyn_code/agent/loop.rb +182 -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 +211 -0
- data/lib/rubyn_code/agent/tool_processor.rb +178 -0
- data/lib/rubyn_code/agent/usage_tracker.rb +59 -0
- data/lib/rubyn_code/auth/key_encryption.rb +118 -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 +80 -52
- data/lib/rubyn_code/autonomous/daemon.rb +146 -32
- data/lib/rubyn_code/autonomous/idle_poller.rb +4 -24
- data/lib/rubyn_code/autonomous/task_claimer.rb +46 -44
- data/lib/rubyn_code/background/worker.rb +64 -76
- data/lib/rubyn_code/cli/app.rb +159 -114
- data/lib/rubyn_code/cli/commands/doctor.rb +73 -0
- data/lib/rubyn_code/cli/commands/mcp.rb +77 -0
- data/lib/rubyn_code/cli/commands/model.rb +105 -18
- data/lib/rubyn_code/cli/commands/new_session.rb +45 -0
- data/lib/rubyn_code/cli/commands/provider.rb +123 -0
- data/lib/rubyn_code/cli/commands/skill.rb +52 -3
- data/lib/rubyn_code/cli/daemon_runner.rb +64 -11
- data/lib/rubyn_code/cli/first_run.rb +159 -0
- data/lib/rubyn_code/cli/renderer.rb +109 -60
- data/lib/rubyn_code/cli/repl.rb +48 -374
- data/lib/rubyn_code/cli/repl_commands.rb +177 -0
- data/lib/rubyn_code/cli/repl_lifecycle.rb +76 -0
- data/lib/rubyn_code/cli/repl_setup.rb +181 -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 +11 -0
- data/lib/rubyn_code/config/project_profile.rb +185 -0
- data/lib/rubyn_code/config/schema.json +49 -0
- data/lib/rubyn_code/config/settings.rb +103 -1
- data/lib/rubyn_code/config/validator.rb +63 -0
- data/lib/rubyn_code/context/auto_compact.rb +1 -1
- data/lib/rubyn_code/context/context_budget.rb +182 -0
- data/lib/rubyn_code/context/context_collapse.rb +34 -4
- data/lib/rubyn_code/context/decision_compactor.rb +99 -0
- data/lib/rubyn_code/context/manager.rb +44 -8
- data/lib/rubyn_code/context/manual_compact.rb +1 -1
- 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/hooks/registry.rb +4 -0
- data/lib/rubyn_code/ide/adapters/tool_output.rb +330 -0
- data/lib/rubyn_code/ide/client.rb +110 -0
- data/lib/rubyn_code/ide/handlers/accept_edit_handler.rb +35 -0
- data/lib/rubyn_code/ide/handlers/approve_tool_use_handler.rb +34 -0
- data/lib/rubyn_code/ide/handlers/cancel_handler.rb +41 -0
- data/lib/rubyn_code/ide/handlers/config_get_handler.rb +63 -0
- data/lib/rubyn_code/ide/handlers/config_set_handler.rb +86 -0
- data/lib/rubyn_code/ide/handlers/initialize_handler.rb +79 -0
- data/lib/rubyn_code/ide/handlers/models_list_handler.rb +39 -0
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +215 -0
- data/lib/rubyn_code/ide/handlers/review_handler.rb +110 -0
- data/lib/rubyn_code/ide/handlers/session_fork_handler.rb +49 -0
- data/lib/rubyn_code/ide/handlers/session_list_handler.rb +41 -0
- data/lib/rubyn_code/ide/handlers/session_reset_handler.rb +31 -0
- data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +42 -0
- data/lib/rubyn_code/ide/handlers/shutdown_handler.rb +37 -0
- data/lib/rubyn_code/ide/handlers.rb +76 -0
- data/lib/rubyn_code/ide/protocol.rb +111 -0
- data/lib/rubyn_code/ide/server.rb +186 -0
- data/lib/rubyn_code/index/codebase_index.rb +311 -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 +274 -0
- data/lib/rubyn_code/llm/adapters/anthropic_compatible.rb +60 -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 +50 -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 +75 -247
- 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 +10 -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/search.rb +1 -0
- 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/self_test.rb +315 -0
- data/lib/rubyn_code/skills/catalog.rb +66 -0
- data/lib/rubyn_code/skills/document.rb +33 -29
- data/lib/rubyn_code/skills/loader.rb +43 -0
- 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/tasks/models.rb +1 -0
- 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 +39 -32
- data/lib/rubyn_code/tools/bash.rb +7 -1
- data/lib/rubyn_code/tools/edit_file.rb +130 -17
- data/lib/rubyn_code/tools/executor.rb +130 -25
- 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 +29 -7
- data/lib/rubyn_code/tools/grep.rb +8 -1
- data/lib/rubyn_code/tools/ide_diagnostics.rb +51 -0
- data/lib/rubyn_code/tools/ide_symbols.rb +53 -0
- 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 +190 -0
- data/lib/rubyn_code/tools/read_file.rb +17 -6
- data/lib/rubyn_code/tools/registry.rb +11 -0
- 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 +76 -1
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +62 -1
- data/skills/rubyn_self_test.md +133 -0
- metadata +83 -1
|
@@ -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,128 @@ 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 --ide Start IDE server (VS Code extension)
|
|
30
|
+
rubyn-code --permission-mode MODE Set permission mode (default, accept_edits, plan_only, auto, dont_ask, bypass)
|
|
31
|
+
rubyn-code --version Show version
|
|
32
|
+
rubyn-code --help Show this help
|
|
33
|
+
|
|
34
|
+
Daemon Mode:
|
|
35
|
+
rubyn-code daemon Start autonomous daemon (GOLEM)
|
|
36
|
+
rubyn-code daemon --name NAME Agent name (default: golem-<random>)
|
|
37
|
+
rubyn-code daemon --role ROLE Agent role description
|
|
38
|
+
rubyn-code daemon --max-runs N Max tasks before shutdown (default: 100)
|
|
39
|
+
rubyn-code daemon --max-cost N Max USD spend before shutdown (default: 10.0)
|
|
40
|
+
rubyn-code daemon --idle-timeout N Seconds idle before shutdown (default: 60)
|
|
41
|
+
rubyn-code daemon --poll-interval N Seconds between polls (default: 5)
|
|
42
|
+
|
|
43
|
+
Interactive Commands:
|
|
44
|
+
/help Show available commands
|
|
45
|
+
/quit Exit
|
|
46
|
+
/compact Compress context
|
|
47
|
+
/cost Show usage costs
|
|
48
|
+
/tasks List tasks
|
|
49
|
+
/skill [name] Load or list skills
|
|
50
|
+
|
|
51
|
+
Environment:
|
|
52
|
+
Config: ~/.rubyn-code/config.yml
|
|
53
|
+
Data: ~/.rubyn-code/rubyn_code.db
|
|
54
|
+
Tokens: ~/.rubyn-code/tokens.yml
|
|
55
|
+
HELP
|
|
56
|
+
|
|
57
|
+
SIMPLE_FLAGS = {
|
|
58
|
+
'--version' => :version, '-v' => :version,
|
|
59
|
+
'--help' => :help, '-h' => :help,
|
|
60
|
+
'--auth' => :auth, '--setup' => :setup
|
|
61
|
+
}.freeze
|
|
62
|
+
BOOLEAN_FLAGS = { '--yolo' => :yolo, '--debug' => :debug, '--skip-setup' => :skip_setup, '--ide' => :ide }.freeze
|
|
63
|
+
VALUE_FLAGS = { '--permission-mode' => :permission_mode }.freeze
|
|
64
|
+
DAEMON_INT_FLAGS = { '--max-runs' => :max_runs, '--idle-timeout' => :idle_timeout,
|
|
65
|
+
'--poll-interval' => :poll_interval }.freeze
|
|
66
|
+
DAEMON_STR_FLAGS = { '--name' => :agent_name, '--role' => :role }.freeze
|
|
67
|
+
|
|
36
68
|
private
|
|
37
69
|
|
|
38
|
-
def
|
|
70
|
+
def dispatch_command(command) # rubocop:disable Metrics/CyclomaticComplexity -- unavoidable dispatch switch
|
|
71
|
+
case command
|
|
72
|
+
when :version then puts "rubyn-code #{RubynCode::VERSION}"
|
|
73
|
+
when :auth then run_auth
|
|
74
|
+
when :setup then run_setup
|
|
75
|
+
when :help then display_help
|
|
76
|
+
when :run then run_single_prompt(@options[:prompt])
|
|
77
|
+
when :ide then run_ide
|
|
78
|
+
when :daemon then run_daemon
|
|
79
|
+
when :repl then run_repl
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_options(argv)
|
|
39
84
|
options = { command: :repl }
|
|
85
|
+
idx = 0
|
|
86
|
+
while idx < argv.length
|
|
87
|
+
idx = parse_single_option(argv, idx, options)
|
|
88
|
+
idx += 1
|
|
89
|
+
end
|
|
90
|
+
options[:command] = :ide if options[:ide]
|
|
91
|
+
options
|
|
92
|
+
end
|
|
40
93
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
94
|
+
# -- option parser
|
|
95
|
+
def parse_single_option(argv, idx, options)
|
|
96
|
+
arg = argv[idx]
|
|
97
|
+
if SIMPLE_FLAGS.key?(arg)
|
|
98
|
+
options[:command] = SIMPLE_FLAGS[arg]
|
|
99
|
+
elsif BOOLEAN_FLAGS.key?(arg)
|
|
100
|
+
options[BOOLEAN_FLAGS[arg]] = true
|
|
101
|
+
elsif VALUE_FLAGS.key?(arg)
|
|
102
|
+
options[VALUE_FLAGS[arg]] = argv[idx + 1]
|
|
103
|
+
idx += 1
|
|
104
|
+
else
|
|
105
|
+
idx = parse_value_option(argv, idx, options)
|
|
69
106
|
end
|
|
107
|
+
idx
|
|
108
|
+
end
|
|
70
109
|
|
|
71
|
-
|
|
110
|
+
def parse_value_option(argv, idx, options)
|
|
111
|
+
case argv[idx]
|
|
112
|
+
when '--resume', '-r'
|
|
113
|
+
options[:session_id] = argv[idx + 1]
|
|
114
|
+
idx + 1
|
|
115
|
+
when '-p', '--prompt'
|
|
116
|
+
options[:command] = :run
|
|
117
|
+
options[:prompt] = argv[idx + 1]
|
|
118
|
+
idx + 1
|
|
119
|
+
when 'daemon'
|
|
120
|
+
options[:command] = :daemon
|
|
121
|
+
parse_daemon_options!(argv, idx + 1, options)
|
|
122
|
+
argv.length - 1
|
|
123
|
+
else
|
|
124
|
+
idx
|
|
125
|
+
end
|
|
72
126
|
end
|
|
73
127
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
128
|
+
def parse_daemon_options!(argv, start, options)
|
|
129
|
+
options[:daemon] = default_daemon_options
|
|
130
|
+
idx = start
|
|
131
|
+
while idx < argv.length
|
|
132
|
+
idx = parse_single_daemon_option(argv, idx, options)
|
|
133
|
+
idx += 1
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def default_daemon_options
|
|
138
|
+
{
|
|
82
139
|
max_runs: 100,
|
|
83
140
|
max_cost: 10.0,
|
|
84
141
|
idle_timeout: 60,
|
|
@@ -86,33 +143,31 @@ module RubynCode
|
|
|
86
143
|
agent_name: "golem-#{SecureRandom.hex(4)}",
|
|
87
144
|
role: 'autonomous coding agent'
|
|
88
145
|
}
|
|
146
|
+
end
|
|
89
147
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
when '--max-cost'
|
|
97
|
-
options[:daemon][:max_cost] = argv[i + 1].to_f
|
|
98
|
-
i += 1
|
|
99
|
-
when '--idle-timeout'
|
|
100
|
-
options[:daemon][:idle_timeout] = argv[i + 1].to_i
|
|
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
|
|
148
|
+
def parse_single_daemon_option(argv, idx, options)
|
|
149
|
+
case argv[idx]
|
|
150
|
+
when '--debug'
|
|
151
|
+
options[:debug] = true
|
|
152
|
+
else
|
|
153
|
+
idx = parse_daemon_value_option(argv, idx, options)
|
|
115
154
|
end
|
|
155
|
+
idx
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def parse_daemon_value_option(argv, idx, options) # rubocop:disable Metrics/AbcSize -- option dispatch with hash lookup
|
|
159
|
+
arg = argv[idx]
|
|
160
|
+
daemon = options[:daemon]
|
|
161
|
+
if DAEMON_INT_FLAGS.key?(arg)
|
|
162
|
+
daemon[DAEMON_INT_FLAGS[arg]] = argv[idx + 1].to_i
|
|
163
|
+
elsif arg == '--max-cost'
|
|
164
|
+
daemon[:max_cost] = argv[idx + 1].to_f
|
|
165
|
+
elsif DAEMON_STR_FLAGS.key?(arg)
|
|
166
|
+
daemon[DAEMON_STR_FLAGS[arg]] = argv[idx + 1]
|
|
167
|
+
else
|
|
168
|
+
return idx
|
|
169
|
+
end
|
|
170
|
+
idx + 1
|
|
116
171
|
end
|
|
117
172
|
|
|
118
173
|
def run_auth
|
|
@@ -141,11 +196,17 @@ module RubynCode
|
|
|
141
196
|
puts response
|
|
142
197
|
end
|
|
143
198
|
|
|
199
|
+
def run_ide
|
|
200
|
+
mode = resolve_permission_mode
|
|
201
|
+
IDE::Server.new(permission_mode: mode).run
|
|
202
|
+
end
|
|
203
|
+
|
|
144
204
|
def run_daemon
|
|
145
205
|
DaemonRunner.new(@options).run
|
|
146
206
|
end
|
|
147
207
|
|
|
148
208
|
def run_repl
|
|
209
|
+
maybe_first_run!
|
|
149
210
|
REPL.new(
|
|
150
211
|
session_id: @options[:session_id],
|
|
151
212
|
project_root: Dir.pwd,
|
|
@@ -153,41 +214,25 @@ module RubynCode
|
|
|
153
214
|
).run
|
|
154
215
|
end
|
|
155
216
|
|
|
217
|
+
def maybe_first_run!
|
|
218
|
+
return unless FirstRun.needed?
|
|
219
|
+
return if FirstRun.skipped?(skip_flag: @options[:skip_setup])
|
|
220
|
+
|
|
221
|
+
FirstRun.new.run
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def resolve_permission_mode
|
|
225
|
+
if @options[:permission_mode]
|
|
226
|
+
@options[:permission_mode].to_sym
|
|
227
|
+
elsif @options[:yolo]
|
|
228
|
+
:bypass
|
|
229
|
+
else
|
|
230
|
+
:default
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
156
234
|
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
|
|
235
|
+
puts HELP_TEXT
|
|
191
236
|
end
|
|
192
237
|
end
|
|
193
238
|
end
|
|
@@ -14,6 +14,9 @@ module RubynCode
|
|
|
14
14
|
check_auth
|
|
15
15
|
check_skills
|
|
16
16
|
check_project
|
|
17
|
+
check_mcp
|
|
18
|
+
check_codebase_index
|
|
19
|
+
check_skill_catalog
|
|
17
20
|
].freeze
|
|
18
21
|
|
|
19
22
|
def execute(_args, ctx)
|
|
@@ -97,6 +100,76 @@ module RubynCode
|
|
|
97
100
|
['Project detected', true, "#{type} at #{ctx.project_root}"]
|
|
98
101
|
end
|
|
99
102
|
|
|
103
|
+
def check_mcp(ctx)
|
|
104
|
+
config_path = File.join(ctx.project_root, MCP::Config::CONFIG_FILENAME)
|
|
105
|
+
return ['MCP connectivity', false, 'mcp.json not found'] unless File.exist?(config_path)
|
|
106
|
+
|
|
107
|
+
servers = MCP::Config.load(ctx.project_root)
|
|
108
|
+
return ['MCP connectivity', false, 'no servers configured'] if servers.empty?
|
|
109
|
+
|
|
110
|
+
reachable = servers.count { |s| mcp_server_reachable?(s) }
|
|
111
|
+
detail = "#{reachable}/#{servers.size} servers reachable"
|
|
112
|
+
['MCP connectivity', reachable == servers.size, detail]
|
|
113
|
+
rescue StandardError => e
|
|
114
|
+
['MCP connectivity', false, e.message]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def mcp_server_reachable?(server)
|
|
118
|
+
command = server[:command]
|
|
119
|
+
return false if command.nil? || command.empty?
|
|
120
|
+
|
|
121
|
+
# Check if the command binary exists on PATH
|
|
122
|
+
system("command -v #{command} > /dev/null 2>&1")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def check_codebase_index(ctx)
|
|
126
|
+
index_path = File.join(ctx.project_root, Index::CodebaseIndex::INDEX_DIR,
|
|
127
|
+
Index::CodebaseIndex::INDEX_FILE)
|
|
128
|
+
return ['Codebase index', false, 'index not found'] unless File.exist?(index_path)
|
|
129
|
+
|
|
130
|
+
mtime = File.mtime(index_path)
|
|
131
|
+
age_hours = ((Time.now - mtime) / 3600).round(1)
|
|
132
|
+
stale = age_hours > 24
|
|
133
|
+
detail = "#{age_hours}h old#{' (stale — consider reindexing)' if stale}"
|
|
134
|
+
['Codebase index', !stale, detail]
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
['Codebase index', false, e.message]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def check_skill_catalog(ctx)
|
|
140
|
+
catalog = ctx.skill_loader.catalog
|
|
141
|
+
entries = catalog.available
|
|
142
|
+
return ['Skill catalog', false, 'no skills found'] if entries.empty?
|
|
143
|
+
|
|
144
|
+
malformed = count_malformed_skills(catalog.skills_dirs)
|
|
145
|
+
detail = "#{entries.size} skills loaded"
|
|
146
|
+
detail += ", #{malformed} malformed" if malformed.positive?
|
|
147
|
+
['Skill catalog', malformed.zero?, detail]
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
['Skill catalog', false, e.message]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def count_malformed_skills(skills_dirs)
|
|
153
|
+
count = 0
|
|
154
|
+
skills_dirs.each do |dir|
|
|
155
|
+
next unless File.directory?(dir)
|
|
156
|
+
|
|
157
|
+
Dir.glob(File.join(dir, '**/*.md')).each do |path|
|
|
158
|
+
count += 1 unless valid_skill_file?(path)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
count
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def valid_skill_file?(path)
|
|
165
|
+
content = File.read(path, 1024, encoding: 'UTF-8')
|
|
166
|
+
.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
|
|
167
|
+
doc = Skills::Document.parse(content, filename: path)
|
|
168
|
+
!doc.name.nil? && !doc.name.empty?
|
|
169
|
+
rescue StandardError
|
|
170
|
+
false
|
|
171
|
+
end
|
|
172
|
+
|
|
100
173
|
def detect_project_type(root)
|
|
101
174
|
return 'Rails' if File.exist?(File.join(root, 'config', 'application.rb'))
|
|
102
175
|
return 'Ruby' if File.exist?(File.join(root, 'Rakefile'))
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
class Mcp < Base
|
|
7
|
+
def self.command_name = '/mcp'
|
|
8
|
+
def self.description = 'MCP server status'
|
|
9
|
+
|
|
10
|
+
def execute(_args, ctx)
|
|
11
|
+
configs = load_configs(ctx.project_root)
|
|
12
|
+
|
|
13
|
+
if configs.empty?
|
|
14
|
+
ctx.renderer.info('No MCP servers configured.')
|
|
15
|
+
puts ' Add servers to .rubyn-code/mcp.json — see docs/MCP.md for details.'
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
ctx.renderer.info("MCP servers (#{configs.size}):")
|
|
20
|
+
puts
|
|
21
|
+
|
|
22
|
+
configs.each { |cfg| render_server(cfg) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def load_configs(project_root)
|
|
28
|
+
MCP::Config.load(project_root)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render_server(cfg)
|
|
32
|
+
client = build_client(cfg)
|
|
33
|
+
status, tool_count = probe_server(client)
|
|
34
|
+
icon = status_icon(status)
|
|
35
|
+
tools_label = tool_count ? " (#{tool_count} tools)" : ''
|
|
36
|
+
|
|
37
|
+
puts " #{icon} #{cfg[:name]} [#{status}]#{tools_label}"
|
|
38
|
+
render_transport_info(cfg)
|
|
39
|
+
ensure
|
|
40
|
+
client&.disconnect! if client&.connected?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build_client(cfg)
|
|
44
|
+
MCP::Client.from_config(cfg)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def probe_server(client)
|
|
48
|
+
client.connect!
|
|
49
|
+
tool_count = client.tools.size
|
|
50
|
+
[:connected, tool_count]
|
|
51
|
+
rescue StandardError
|
|
52
|
+
[:error, nil]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_transport_info(cfg)
|
|
56
|
+
if cfg[:url]
|
|
57
|
+
puts " transport: SSE url: #{cfg[:url]}"
|
|
58
|
+
else
|
|
59
|
+
puts " transport: stdio command: #{cfg[:command]} #{cfg[:args].join(' ')}".rstrip
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def status_icon(status)
|
|
64
|
+
case status
|
|
65
|
+
when :connected then green('*')
|
|
66
|
+
when :error then red('x')
|
|
67
|
+
else yellow('?')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def green(text) = "\e[32m#{text}\e[0m"
|
|
72
|
+
def red(text) = "\e[31m#{text}\e[0m"
|
|
73
|
+
def yellow(text) = "\e[33m#{text}\e[0m"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|